diff --git a/AUDIT.md b/AUDIT.md index 2f40bd1..3f98d38 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -17,117 +17,80 @@ document. --- -## Current verdict (as of 2026-07-28) +## Current verdict (as of 2026-07-29) -The substrate (Layer 1) is solid. Layers 2–4 were originally shaped-but-not- -wired; most of the blocking defects below are now closed, verified by -reading the current code (not just by trusting this file) and by targeted -tests. Three things are not yet proven: +The core code and targeted tests are substantially ahead of the deployed +operator surface. The live API is reachable over HTTP at `orchestra.kvmx.ru` +(HTTPS currently returns nginx `502`), but its historical `test-e2e` backlog +is not an operationally useful control plane yet. -1. **A real end-to-end unattended run.** The live attempts of 2026-07-28 - surfaced B12 through B17. All are now closed in code (B17 on the worker - path only), but **B13 through B17 were each found live and none has been - re-verified live** — occupancy/rotation/handoff/completion still haven't - been exercised end-to-end against a session guaranteed to exist. Treat - "closed" here as "the code no longer contains the defect," not as - operational proof. -2. **The operator surface was unauthenticated in the live deployment.** - Fixed in code (**B18**: mandatory Web token, cookie sessions for the - browser), but the fix is a breaking config change — the service will not - start until `.orchestra-config/orchestra.env` sets - `ORCHESTRA_WEB_TOKEN`. See B18 and the command-channel section. -3. **Cross-machine (federation) correctness.** Deliberately deferred — see - "The federation fork" below. +### Live evidence, checked 2026-07-29 -`go build ./...`, `go vet ./...`, and `go test ./...` all pass. +- One `workpc-opencode` worker is heartbeating and reports capacity 1. Its + Unix-socket herdr path is the meaningful reachability signal; legacy + coordinator TCP probes are not. +- The API reports 28 tasks: **22 blocked, 5 completed, 1 failed, and no + queued, leased, approval-pending, or captured sessions**. The visible queue + is therefore historical E2E residue, not current work. +- A real browser loaded the deployed SPA with a valid Web session and crashed + before painting: `overview.sessions` is JSON `null`, while the bundle calls + `sessions.length`. Task detail has the same fault for `events: null`. + Source now emits empty arrays and the frontend is defensive; the current + local build renders the live board and task record. **The deployment still + needs a rebuild/redeploy.** +- The task record for a blocked task and the failed E2E task had no retained + session and no lifecycle events. Every lifecycle control was correctly + disabled, but neither record could explain its state. +- The live image predates the source session/logout work: `DELETE + /v1/ui/session` returns `405`. Do not treat token rotation as session + revocation until that endpoint is deployed. -### Follow-up live verification (2026-07-28) +Earlier successful OpenCode approval and worker-flow checks remain useful +historical evidence, but they do not prove the current deployment or a +successful worker-owned handoff/release/pickup. Cross-machine continuity is +still deliberately incomplete; Design A must not operate a non-local +checkout. -The OpenCode launch path has now been exercised against workpc herdr through -an isolated `test-e2e` worktree. The verified outcomes are deliberately -narrow: +### Operator UI: remaining work -- Initial prompts use a bounded retry only after an explicit JSON-RPC - rejection (herdr's short post-start readiness window). Transport timeouts - and disconnects are never replayed. A pane is inspected first; blocked or - permission-dialog panes are refused. -- Herdr accepts task-scoped unique names (`oc-`); active sessions - are visible by that name in `agent.list`. Prompt routing uses that name, - while pane reads remain pane-scoped. -- The initial launch message carries the task title and description. This is - necessary under Design A because homesrv cannot write `TASK.md` inside a - workpc checkout. A live OpenCode run received the full instruction and, - after explicit operator approval of two file edits, created the requested - marker and report without touching other files. -- The initial launch does **not** wait for the agent to become idle. Waiting - converted an ordinary long-running first turn into a false `TaskBlocked`; - a fresh run now remains `TaskLeased` while OpenCode is paused at its normal - permission boundary. +The current UI should become a diagnosis surface, not a five-column task +catalogue. -### Deployment state observed live (2026-07-29) - -Checked against the running system, not the docs: - -- The deployment **moved from `orchestra.service` to Docker Compose** - (`/home/kami/docker-apps/orchestra-web-ui`). `orchestra-api` and - `orchestra-web-ui` are both up; `/healthz` and the UI answer 200. The - stopped systemd unit is the retired predecessor, not an outage — see the - deployment section of `CLAUDE.md`. -- The **running image predates the last two commits** (built 2026-07-28 - 21:28; `0b4b52a` "Require a token for the web UI" landed 23:15). So B18's - auth fix is *not* in the live container. Rebuild before drawing any - conclusion about the live surface's auth behaviour. -- **All six herdrs are unreachable** — workpc `192.168.1.105:9245-7` refuses, - homesrv `192.168.1.104:9245-7` times out (filtered). Probed directly, since - a herdr that connects logs nothing. Nothing can be leased; every task the - API serves is history. This is the sole remaining blocker on live proof for - B13–B17. -- `GET /v1/tasks` has **no authz check at all** (`cmd/orchestra/main.go:286` - returns before any gate). B18 gated the mutation and approval controls, not - the read path. Accepted for now: the `0.0.0.0:9145` bind is deliberate and - ufw restricts the port to a single other LAN machine, so the read path's - trust boundary is the LAN, not the host. Revisit if that bind is ever - widened. -- ntfy delivery still fails `403 Forbidden` on every send (last observed - 2026-07-28) — a wrong or expired credential, separate from S12's - `ORCHESTRA_NTFY_SURFACE_TOKEN`. - -This does not complete the cross-machine continuity design. `clients/herdr- -bridge.go` is only a byte proxy; it is not a worker. The worker-side process -described below remains required before a remote checkout can author -`TASK.md`, derive a canonical handoff anchor, scratch-commit WIP, or safely -release it. Until then, homesrv must not rotate or clean up a non-local -worktree. - -| Spec layer | State | -|---|---| -| L1 substrate (§3, §4) | Built and correct in the main path. | -| L2 harness (§5) | Occupancy, rotation, and completion all wired and reachable. B12 fixed and confirmed live; B13–B16 fixed in code but not re-verified live. Unique per-task agent names (B16) and ambiguity-safe prompting (B14) are the substantive changes. | -| L3 continuity (§6) | Handoff schema, pickup validation, scratch branches, TASK.md are all wired into the live path (Phase 4 complete). | -| L4 surfaces (§7) | Brief/standup/delivery real; quota has a post-hoc producer only (no live push feed yet); B8's authz bypass closed, but the new web UI reintroduces an equivalent one (B18). | +1. **Persist and show a blocked diagnosis.** Project the structured blocking + reason/error into the task read model. Group the board by reason and age + (for example: lease failure, worker offline, expired lease, approval, + handoff validation, operator block) rather than rendering a giant generic + “Blocked” lane. +2. **Persist last-session and pane evidence.** Before a worker/coordinator + drops a terminal session mapping, retain harness, pane ID, last successful + capture/check, and a pane status of `open`, `closed`, `unreachable`, or + `unknown`. The UI must show the source and timestamp. For old records that + lack this evidence, say **unknown — legacy record has no retained + blocker/pane evidence**, never imply a live or closed pane. +3. **Make the task page lead with the diagnosis.** Put reason, last activity, + pane state, and next safe action first. Hide unavailable lifecycle forms + behind an “Unavailable actions” disclosure; an empty session must not + consume most of the page with disabled controls. +4. **Separate active work from history.** Default the board to active, + waiting, and needs-attention work; move completed/failed/test residue to + filters or a compact history view. Add search, project filtering, and an + explicit “no live work” state. +5. **Complete the worker truth model.** Show worker-owned heartbeat, local + herdr reachability, active task/pane, and last error separately from legacy + coordinator probes. +6. **Deploy and verify the browser path.** Ship the null-collection fixes and + session logout endpoint, then record real login, refresh, expiry, board, + task-detail, and approval browser flows. Keep `web/dist` and embedded + assets synchronized as part of that build. --- ## Blocking defects -B14, B15 and B16 are now closed by code and covered by tests; none has been -re-verified against a live cross-machine run. B17 is closed on the worker -path only and remains open for Design A. B18 (the unauthenticated web -surface) is closed in code but **requires an env change before the service -will start** — see B18 and "What's next". - -Nothing is currently blocking in the sense B12/B13 were. B19–B21 and S12–S13 -— the correctness and hygiene gaps found while implementing B18 — are now -fixed in code and covered by tests (2026-07-28); each is marked closed in -its own section below. The flaky router test is also fixed, and the flake -was in the test, not in assignment. - -The real remaining risk is entirely evidential now: B13 through B17 were -each found live and **none has been re-verified live**, and none of the -2026-07-28 fixes has run on the deployed instance either — the service is -stopped, `/usr/local/bin/orchestra` predates all of them, and installing a -new binary or editing `/etc/orchestra/orchestra.env` needs privileges this -sandbox does not have. +The sections below are retained defect provenance and implementation detail. +Their dated deployment claims do not supersede the current verdict above. +Read them when changing the affected path; use the current verdict for the +live state and remaining operator work. ### B18 — the web UI surface is unauthenticated (found by audit 2026-07-28, uncommitted working tree) — closed (code fix; requires an env change before restart) @@ -371,7 +334,7 @@ Delivery` asserting the prompt target is the unique name. Nothing yet starts two same-harness sessions in distinct panes and verifies both attach, so the `agent_name_taken` failure mode itself is untested end-to-end. -### B17 — opaque harnesses must not author canonical handoff anchors (found live 2026-07-28) — closed on the worker path; open for Design A +### B17 — opaque harnesses must not author canonical handoff anchors (found live 2026-07-28) — closed in code; live success still unproven The real OpenCode healthcheck run showed that prompting an opaque agent to write the full `continuity.Handoff` schema is the wrong ownership boundary. @@ -427,7 +390,7 @@ attached Claude at 08:00:43Z. The renewed attempt then hit B14's prompt timeout and became blocked. This is expected retry behavior, but illustrates why lifecycle retries must be visible to operators. -**Status (2026-07-28): satisfied on the worker path, unchanged for Design A.** +**Status:** satisfied on the worker path. `cmd/orchestra-worker` now implements the required ownership boundary. The harness is asked once for a bounded semantic report; `releaseReady` (`cmd/orchestra-worker/main.go:180`) then runs `CLIAdapter.Release` and @@ -437,11 +400,23 @@ canonical artifact is validated before publication. The handoff-provenance correction documented at the end of this file supplies the parsing and validation half. -The homesrv coordinator's Design A path is unchanged and still cannot -satisfy this: it does not own the workpc checkout, so any Git-derived fact -it seals is derived from the wrong filesystem. B17 is therefore only closed -for tasks that actually run through a worker. The standing rule holds — -homesrv must not rotate or clean up a non-local worktree. +The coordinator no longer accepts a non-local herdr operation; the standing +rule is enforced rather than advisory. A successful live worker handoff, +release, and pickup remains the required operational proof. + +**QA attempt (2026-07-29):** the bounded disposable task +`06FTSHBPYHQXN8MM849PFA1V6M` reached the worker and wrote its semantic +report, but publication correctly refused it. Its prescribed fields combined +to the canonical action `inspect the marker file — verify worker-owned +handoff construction`; the circular-action guard rejects that word even when +it occurs in the explanatory `WHY` text. This is an invalid QA fixture, not a +successful handoff or a release-path regression. The worker must be given a +fresh fixture whose combined `NEXT`/`WHY` action does not mention a handoff +(for example, `WHY: verify canonical artifact construction`), then the +release and successor pickup must be observed. The coordinator is token-gated +and this host cannot read the protected worker credential, so this session +could not enqueue that replacement task. The failed disposable pane was left +untouched; no destructive herdr calls were issued. ### B13 — `agent.start` silently no-ops under back-to-back leases (found live 2026-07-28) — closed (code fix; not yet re-verified live) @@ -639,14 +614,7 @@ action without releasing the pane. **Known weaknesses of this channel.** The defects found in it are filed individually below — **B19** (federated approvals emit no event), **B20** (the local capture revision is a timestamp, not a change counter) and -**B21** (`Registry.commands` never prunes). Two more, not separately filed: - -- **Captures and commands are in-memory only.** `Registry.captures` and - `Registry.commands` have no persistence, so a coordinator restart drops - pending approvals silently and resets revision counters — which, with a - reset counter, could let a stale command match a new capture. This is the - other half of B21: the lifecycle forgets across restarts and never forgets - within one. +**B21** (`Registry.commands` never prunes). Two further gaps remain: - **`RespondApproval` (coordinator path) has no test**, unlike its worker counterpart — and per B20 it is the path whose revision is meaningless, so its text-comparison guard is the only thing actually binding the @@ -729,14 +697,16 @@ rescans the entire history on every worker poll. Combined with the in-memory-only storage already noted, the lifecycle is wrong at both ends: it forgets across restarts and never forgets within one. -**Fix (within-process half only).** `Registry.pruneCommands` drops resolved +**Fix.** `Registry.pruneCommands` drops resolved commands older than `CommandRetention` (30 minutes), running on both `Queue` and `Commands`, so the per-worker list is bounded and the poll path no longer rescans unbounded history. Pending commands are never pruned, at any -age — dropping one would silently discard an operator decision. Covered by -`TestResolvedCommandsArePrunedButPendingOnesSurvive`. **The persistence half -of this defect is still open:** captures and commands remain in-memory only, -so a coordinator restart still drops pending approvals silently. +age — dropping one would silently discard an operator decision. Captures, +commands, and the worker identity/token binding now persist atomically in +`$ORCHESTRA_DATA/federation-state.json` (mode 0600), so the worker must +re-register with its original token after restart before it can consume a +recovered command. Covered by `TestResolvedCommandsArePrunedButPendingOnesSurvive` +and `TestPendingApprovalSurvivesRegistryRestart`. ### S12 — `ORCHESTRA_NTFY_TOKEN` serves two unrelated purposes — closed (2026-07-28) @@ -775,41 +745,18 @@ bypass is closed by construction rather than by the header check alone. In dependency order, not importance order: -1. **Finish applying the B18 env change on the deployed instance.** The - repo-side `.orchestra-config/orchestra.env` now sets a generated - `ORCHESTRA_WEB_TOKEN` and `ORCHESTRA_UI_INSECURE_COOKIE=1`, but the unit - reads `/etc/orchestra/orchestra.env`, which is `orchestra:orchestra 0600` - and unreadable from this sandbox — whether it carries the token could not - be confirmed. It was last modified 2026-07-28 14:30 and the service did - start at 16:17 with the B18 build absent, so this is unverified either - way. Also newly required: `ORCHESTRA_NTFY_SURFACE_TOKEN` (S12) if the - ntfy surface should stay gated, and the token for every existing - unauthenticated `/v1/` client, which now defaults to the Web surface. -2. **Install the new binary and restart.** `orchestra.service` has been - stopped since 2026-07-28 19:46 and `/usr/local/bin/orchestra` predates - every fix in this section. Needs privileges the sandbox lacks: - `go build -o /tmp/orchestra ./cmd/orchestra && sudo install /tmp/orchestra - /usr/local/bin/orchestra && sudo systemctl restart orchestra.service`. -3. **The live re-verification** that B13–B17 all still lack, plus a first - live exercise of B18–B21. Each of B13–B17 was found live and closed on - paper; the code fixes are unproven against a real cross-machine run, and - that remains the single biggest gap between this document and reality. - Note that no herdr is currently reachable at all — the 16:17 startup logs - `connection refused` for **all six**, including workpc's, which was live - on 2026-07-27 — so a live run needs a herdr brought up first. -4. **Two operational faults visible in the journal**, unrelated to this - audit's defects but blocking a clean live run: every herdr is refusing - connections (above), and ntfy delivery is failing `403 Forbidden` on - every send (16:43 and 16:46), i.e. the ntfy server credential is wrong or - expired. -5. **The persistence half of B21** — captures and commands are in-memory - only, so a restart still silently drops pending approvals. The retention - fix bounds growth within a process; it does not make the lifecycle - durable. -6. **`RespondApproval` (coordinator path) still has no test**, unlike its - worker counterpart. With B20 fixed its revision is now meaningful, but - its text-comparison guard remains the thing actually binding a decision - to what the operator saw, and that guard is untested. +1. **Prove a successful B17 worker handoff.** Use a bounded disposable task + with a concrete next action and verify canonical report upload, release, + follow-up pickup, and cleanup. The rejection path is proven; success is + not. +2. **Fix health semantics and ntfy.** Report worker/local-herdr health + separately from legacy TCP registry probes, and correct the ntfy server + credential causing `403 Forbidden`. +3. **Address the Web UI backlog above**, starting with queue explanations, + actionable worker health, and a usable approval/recovery workflow. +4. **Record a browser smoke.** Confirm login, refresh, invalid token, and + expired-session behavior in an actual browser after the deployed assets + settle. Deliberately *not* next: building further on Design A's cross-machine calls, and closing B17 for the coordinator path. Both wait on the federation-fork @@ -864,9 +811,9 @@ event appended with `Surface: system` set directly in Go, consistent with B8. Extended to Codex/opencode via an optional `harness` field in the request body (`"codex"` → `CodexUsage`, `"opencode"` → `OpenCodeUsage`). -**Still open:** no automated test for the HTTP handler (`cmd/orchestra/main.go` -has zero handler test coverage of any kind, pre-existing gap — this follows -the existing pattern rather than introducing a one-off harness). +Covered by `TestHarnessCompletionBuildsReceiptAndQuotaFromTranscript`, which +exercises authentication, transcript-derived receipts, `TaskCompleted`, and +the matching `QuotaReported` event through the actual HTTP handler. ### B4 — Router counted rotation as a retry (§5.3, §5.4) — closed @@ -957,11 +904,10 @@ Covered by `TestGitWorktreesCommitsTaskFile`, `TestStartBlocksOnInvalidPickup`, `TestReleaseRefusesOnStaleDirtyFile`, `TestConventionsDriftNotifiesActiveSession`, `TestRotationRequestsHandoffBeforeReleasing` (internal/herdr, internal/orchestrator). -**Caveat still open:** TASK.md hashing is best-effort/untested for the -herdr-hosted (`WorktreeCreator`) worktree path specifically — no adapter or -test exercises that path with a real `handoff_ref`, so pickup validation -there runs with an empty `taskFileSHA` (anchor + dirty-file hashes still -checked). Same cross-host caveat as the federation fork, below. +The coordinator no longer supports a herdr-hosted `WorktreeCreator` path. +It always creates the local, committed `TASK.md` before leasing a local pane; +remote worktrees are exclusively worker-owned, so coordinator pickup +validation never runs against a remote filesystem path. ### B7 — Quota projection had no producer (§7.2) — closed (post-hoc only) @@ -1221,42 +1167,25 @@ per-tool-call source at all. Two incompatible federation designs coexist in the tree. -**Design A — "drive the remote socket" (currently deployed, -`clients/herdr-bridge.go`)**: homesrv calls `worktree.create`/`agent.start`/ -etc. directly on workpc's herdr over TCP as if it were local — meaning -anchor validation (`git rev-parse HEAD`) executes on the *wrong machine* -relative to the actual checkout. Two outcomes if the coordinator's -`session.Worktree` path happens to also exist on homesrv (likely, since -every project shares the same directory layout): either `HeadSHA` errors and -rotation silently skips forever, or — the dangerous case — it returns -**homesrv's HEAD for an unrelated checkout**, passing validation while -certifying a commit the agent never touched. Same class of bug applies to -per-host quota accounting and to `cleanupCompleted`'s `git worktree remove`, -which runs on homesrv for a worktree that lives on workpc. +**Design A — "drive the remote socket" (`clients/herdr-bridge.go`)** remains +as a legacy byte proxy but is no longer a coordinator execution path. A +multi-machine registry now requires `ORCHESTRA_MACHINE_ID`; the coordinator +rejects every non-local herdr before leasing, capture, approval, rotation, +release, expiry cleanup, or worktree creation. It always creates the local +committed `TASK.md`; remote panes/checkouts are worker-owned. This prevents +the old wrong-host `git rev-parse`, anchor validation, and cleanup failures. -**Design B — "workers pull tasks" (`/v1/federation/*`)**: fully built -server-side (registration, heartbeat/TTL offline detection, event-cursor -polling/ack, lease claim), zero clients — no worker binary exists anywhere -in this repo. This is what the spec actually describes (§2.1: "everything -crossing a machine boundary is git + a validated artifact, never live state -over the wire"), but every endpoint is currently unreachable in the real -deployment. Latent, never-surfaced defects in this unused half: offline -detection only runs inside `Snapshot()`, called solely from a GET endpoint — -nothing ticks it on its own, so `OnOffline` (the hook that releases leases -held by a vanished worker) only fires if a human hits that endpoint; the -registry is in-memory with no persistence, so a restart forgets all workers -and cursors. +**Design B — "workers pull tasks" (`/v1/federation/*`)** is the sole remote +execution path. `cmd/orchestra-worker` registers, heartbeats, polls/acks +leases, and performs Git/herdr operations on the owning machine; coordinator +state for captures and pending approvals is durable across restart. Worker +offline detection is ticked by the API process rather than depending on a +human health request. -**Decision, unchanged:** keep Design A through Phase 5 (single-host concerns -— occupancy, Face B, rotation, continuity — are provable on homesrv alone -with workpc's herdr as just another pane host); commit to Design B in Phase -6. Two guardrails were meant to land immediately so Design A can't corrupt -state in the meantime: (1) refuse to rotate a lease held by a non-local -herdr rather than validate against the wrong checkout, since the protocol -schema can't run `rev-parse` where the checkout is; (2) same treatment for -`cleanupCompleted`'s worktree removal. **Status of these two guardrails is -unverified in this pass** — re-check `internal/orchestrator` before assuming -they landed; they are not confirmed closed above the way B1–B11/S1–S11 are. +**Decision, now enforced:** federation workers are the sole remote execution +path. The coordinator-side Design A operations are fail-closed rather than +guarded piecemeal; `clients/herdr-bridge.go` should be retired from deployment +once no other consumer needs it. If the worker binary is ever abandoned, delete `/v1/federation/*` and record the deviation — leaving both designs in place unmarked is explicitly not @@ -1305,6 +1234,14 @@ Numbered defects are not repeated here — B19, B20, B21, S12 and S13 are in in "What's next". This list is the unnumbered residue: conditions that are known, accepted, or not actionable as a single fix. +- **Vikunja must become a first-class automatic task source.** The intended + path is `Vikunja task → Orchestra task → agent work → Vikunja update`, not + manual re-entry through the UI. Its provider needs stable external-ID + deduplication, explicit list/project and status/label eligibility mapping, + and guarded completion/blocker write-back that cannot create an ingestion + loop. Treat this alongside Gitea and JSONL ingestion when building the + automatic task-source surface. + - **B18's env change is applied repo-side only.** `.orchestra-config/ orchestra.env` now sets `ORCHESTRA_WEB_TOKEN`, but the unit loads `/etc/orchestra/orchestra.env`, which is not readable or writable from @@ -1323,11 +1260,11 @@ known, accepted, or not actionable as a single fix. *which* of two equal-priority tasks wins is still nondeterministic, since `sort.SliceStable` is applied to a randomly ordered slice; that is a real property of the router, not a test artifact. -- **B14/B15/B16 are code-fixed but not re-verified live**, and B17 is closed - only for tasks that run through a worker. Each was found live, so a code - fix plus unit tests is weaker evidence than the failure that produced it. - In particular B16 still lacks the requested two-pane same-harness attach - contract test, so its actual `agent_name_taken` failure mode is untested. +- **B14/B15/B16 are code-fixed but not re-verified under their original live + races**, and B17 is closed only for tasks that run through a worker. Each + was found live, so deterministic coverage is weaker evidence than the + failure that produced it; B16's two-same-harness naming contract is now + covered by fake-herdr tests. - **B15's reconciliation is operator-mediated only.** Blocked tasks keep their session and are correctable through the UI, but nothing promotes them automatically and the router still never retries a blocked task. diff --git a/cmd/orchestra-worker/main.go b/cmd/orchestra-worker/main.go index 62e2474..19539a4 100644 --- a/cmd/orchestra-worker/main.go +++ b/cmd/orchestra-worker/main.go @@ -34,7 +34,43 @@ type worker struct { statePath string hard float64 registration federation.Worker + lastError string + lastErrorAt time.Time } + +func (w *worker) recordError(err error) { + if err == nil { + return + } + w.lastError = err.Error() + w.lastErrorAt = time.Now().UTC() +} + +func (w *worker) health(ctx context.Context) federation.WorkerHealth { + h := federation.WorkerHealth{HerdrStatus: "unknown"} + for taskID, session := range w.sessions { + // Workers currently advertise capacity one. Pick deterministically so a + // recovered legacy state with more sessions remains intelligible. + if h.ActiveTask == "" || taskID < h.ActiveTask { + h.ActiveTask, h.ActivePane = taskID, session.PaneID + } + } + if w.herdr != nil { + checkCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + err := w.herdr.CheckProtocol(checkCtx, "17") + cancel() + h.CheckedAt = time.Now().UTC() + if err == nil { + h.HerdrStatus = "reachable" + } else { + h.HerdrStatus = "unreachable" + w.recordError(fmt.Errorf("local herdr: %w", err)) + } + } + h.LastError, h.ErrorAt = w.lastError, w.lastErrorAt + return h +} + type lease struct { HandoffRef string `json:"handoff_ref,omitempty"` } @@ -248,22 +284,29 @@ func (w *worker) publishCaptures(ctx context.Context) { } } -func approvalResponse(text, kind string) (string, bool) { +type approvalInput struct { + Text string + Keys []string +} + +func approvalResponse(text, kind string) (approvalInput, bool) { low := strings.ToLower(text) // Never invent a keystroke. y/n prompts label both decisions directly. if strings.Contains(low, "[y/n]") || strings.Contains(low, "(y/n)") { if kind == "grant_approval" { - return "y\n", true + return approvalInput{Text: "y\n"}, true } - return "n\n", true + return approvalInput{Text: "n\n"}, true } // OpenCode's explicit selector states "Allow once Allow always Reject" - // and "enter confirm". Enter is consequently a bounded one-time grant; + // and "enter confirm". Send a real ENTER key, not a newline through + // pane.send_text: OpenCode's selector does not treat the latter as input. + // Enter is consequently a bounded one-time grant; // rejection would require unobservable selector navigation, so refuse it. if kind == "grant_approval" && strings.Contains(low, "allow once") && strings.Contains(low, "allow always") && strings.Contains(low, "reject") && strings.Contains(low, "enter confirm") { - return "\n", true + return approvalInput{Keys: []string{"ENTER"}}, true } - return "", false + return approvalInput{}, false } func (w *worker) runCommands(ctx context.Context) { commands, err := w.api.Commands(ctx) @@ -296,7 +339,11 @@ func (w *worker) runCommands(ctx context.Context) { _ = w.api.ResolveCommand(ctx, command.ID, "rejected", "prompt does not expose an executable approval control") continue } - if err := w.herdr.Call(ctx, "pane.send_text", map[string]any{"pane_id": session.PaneID, "text": input}, nil); err != nil { + method, params := "pane.send_text", map[string]any{"pane_id": session.PaneID, "text": input.Text} + if len(input.Keys) > 0 { + method, params = "pane.send_keys", map[string]any{"pane_id": session.PaneID, "keys": input.Keys} + } + if err := w.herdr.Call(ctx, method, params, nil); err != nil { _ = w.api.ResolveCommand(ctx, command.ID, "rejected", "herdr did not acknowledge input: "+err.Error()) continue } @@ -450,13 +497,15 @@ func main() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { - if err := w.api.Heartbeat(ctx); err != nil { + if err := w.api.Heartbeat(ctx, w.health(ctx)); err != nil { + w.recordError(fmt.Errorf("heartbeat: %w", err)) log.Printf("heartbeat: %v", err) if w.reRegisterAfterCoordinatorRestart(ctx, err) { continue } } if err := w.once(ctx); err != nil { + w.recordError(fmt.Errorf("poll: %w", err)) log.Printf("poll: %v", err) w.reRegisterAfterCoordinatorRestart(ctx, err) } diff --git a/cmd/orchestra-worker/main_test.go b/cmd/orchestra-worker/main_test.go index 8650546..60fa82f 100644 --- a/cmd/orchestra-worker/main_test.go +++ b/cmd/orchestra-worker/main_test.go @@ -14,6 +14,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "testing" "time" ) @@ -248,11 +249,11 @@ func TestWorkerApprovalCommandIsRevisionBoundAndAcknowledged(t *testing.T) { func TestApprovalResponseOpenCodeAllowOnce(t *testing.T) { text := "Permission required\nAllow once Allow always Reject\n⇆ select enter confirm" - if got, ok := approvalResponse(text, "grant_approval"); !ok || got != "\n" { - t.Fatalf("grant response = %q, %v", got, ok) + if got, ok := approvalResponse(text, "grant_approval"); !ok || !reflect.DeepEqual(got.Keys, []string{"ENTER"}) || got.Text != "" { + t.Fatalf("grant response = %+v, %v", got, ok) } - if got, ok := approvalResponse(text, "deny_approval"); ok || got != "" { - t.Fatalf("deny response = %q, %v; reject must not guess selector navigation", got, ok) + if got, ok := approvalResponse(text, "deny_approval"); ok || got.Text != "" || len(got.Keys) != 0 { + t.Fatalf("deny response = %+v, %v; reject must not guess selector navigation", got, ok) } } diff --git a/cmd/orchestra/main.go b/cmd/orchestra/main.go index e474af8..4b02e7c 100644 --- a/cmd/orchestra/main.go +++ b/cmd/orchestra/main.go @@ -5,6 +5,7 @@ import ( "crypto/subtle" "encoding/json" "errors" + "fmt" "io" "log" "net" @@ -92,6 +93,95 @@ func (a federatedAvailability) Available(h registry.Herdr) bool { return a.workers.Available(h.ID) } +func validateLocalMachine(rr registry.Registry, localMachine string) error { + machines := rr.Machines() + if len(machines) <= 1 { + return nil + } + if localMachine == "" { + return fmt.Errorf("ORCHESTRA_MACHINE_ID is required for a multi-machine registry; refusing unsafe remote-herdr coordination") + } + if _, ok := rr.Machine(localMachine); !ok { + return fmt.Errorf("ORCHESTRA_MACHINE_ID %q is not in the registry", localMachine) + } + return nil +} + +type harnessCompletion struct { + store *store.Store + route func(domain.Event) error + token string +} + +func (h harnessCompletion) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if h.token != "" && r.Header.Get("Authorization") != "Bearer "+h.token { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + var p struct { + TaskID string `json:"task_id"` + Harness string `json:"harness"` + TranscriptPath string `json:"transcript_path"` + Report string `json:"report"` + } + if json.NewDecoder(r.Body).Decode(&p) != nil || p.TaskID == "" || p.Report == "" || p.TranscriptPath == "" { + http.Error(w, "task_id, transcript_path, and report are required", http.StatusBadRequest) + return + } + t, ok := h.store.Task(p.TaskID) + if !ok { + http.Error(w, "task not found", http.StatusNotFound) + return + } + var usage herdr.Usage + var err error + switch p.Harness { + case "codex": + usage, err = herdr.CodexUsage(p.TranscriptPath) + case "opencode": + usage, err = herdr.OpenCodeUsage(p.TranscriptPath) + case "", "claude": + usage, err = herdr.ClaudeUsage(p.TranscriptPath) + default: + http.Error(w, "unknown harness: "+p.Harness, http.StatusBadRequest) + return + } + if err != nil { + http.Error(w, "reading transcript: "+err.Error(), http.StatusBadRequest) + return + } + ref, err := h.store.PutArtifact([]byte(p.Report)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + payload, _ := json.Marshal(map[string]any{"report_ref": ref, "receipt": map[string]any{ + "input_tokens": usage.Input, "cache_read_tokens": usage.CacheRead, + "cache_write_tokens": usage.CacheWrite, "output_tokens": usage.Output, "numerator": usage.Numerator(), + }}) + e := domain.Event{ID: id(), Type: "TaskCompleted", TaskID: p.TaskID, Version: t.Version + 1, Payload: payload, Surface: string(authz.System)} + if err := h.store.Append(e); err != nil { + http.Error(w, err.Error(), http.StatusConflict) + return + } + if h.route != nil { + if err := h.route(e); err != nil { + log.Printf("route task: %v", err) + } + } + if t.Lease != nil && t.Lease.HarnessID != "" { + qp, _ := json.Marshal(map[string]any{"harness_id": t.Lease.HarnessID, "consumed": float64(usage.Numerator())}) + if err := h.store.Append(domain.Event{ID: id(), Type: "QuotaReported", TaskID: "system", Payload: qp, Surface: string(authz.System)}); err != nil { + log.Printf("quota report: %v", err) + } + } + json.NewEncoder(w).Encode(e) +} + func main() { dir := os.Getenv("ORCHESTRA_DATA") if dir == "" { @@ -105,11 +195,17 @@ func main() { var rt *router.Router var coordinator *orchestrator.Coordinator localMachine := os.Getenv("ORCHESTRA_MACHINE_ID") - workers := &federation.Registry{AdmitToken: os.Getenv("ORCHESTRA_FEDERATION_ADMIT_TOKEN")} + workers := &federation.Registry{AdmitToken: os.Getenv("ORCHESTRA_FEDERATION_ADMIT_TOKEN"), StatePath: filepath.Join(dir, "federation-state.json")} + if err := workers.Load(); err != nil { + log.Fatalf("load federation state: %v", err) + } if config := os.Getenv("ORCHESTRA_CONFIG"); config != "" { if rr, err = registry.Load(config); err != nil { log.Fatalf("load orchestra config: %v", err) } + if err := validateLocalMachine(rr, localMachine); err != nil { + log.Fatal(err) + } reachability := registry.Reachability(registry.TCPReachability{}) if localMachine != "" { reachability = federatedReachability{base: reachability, remote: remoteHerdrAddresses(rr, localMachine)} @@ -169,7 +265,10 @@ func main() { Projects: projectRepos, Default: orchestrator.GitWorktrees{Root: root, Repo: repo}, } - coordinator = &orchestrator.Coordinator{Store: s, StatePath: filepath.Join(dir, "runtime-sessions.json"), Worktrees: worktrees, Adapters: orchestrator.AdapterFactory{Herdrs: adapters}} + coordinator = &orchestrator.Coordinator{Store: s, StatePath: filepath.Join(dir, "runtime-sessions.json"), Worktrees: worktrees, Adapters: orchestrator.AdapterFactory{Herdrs: adapters}, LocalHerdr: func(id string) bool { + h, ok := rr.Herdr(id) + return ok && (localMachine == "" || h.MachineID == localMachine) + }} rt.OnLease = func(e domain.Event) error { // In federated mode the coordinator must never inspect a remote // checkout. Its worker consumes the router-issued lease event and @@ -214,6 +313,17 @@ func main() { // the token once for an HttpOnly cookie. Same credential, presentable // form; no new authority is created here. mux.HandleFunc("/v1/ui/session", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodDelete { + // Expire the browser credential even if it is already absent or stale. + // The client never has access to the HttpOnly value, so this is the + // only reliable way for an operator to end a browser session. + if cookie, err := r.Cookie(authz.SessionCookie); err == nil { + sessions.Revoke(cookie.Value) + } + http.SetCookie(w, &http.Cookie{Name: authz.SessionCookie, Value: "", Path: "/", MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteStrictMode, Secure: os.Getenv("ORCHESTRA_UI_INSECURE_COOKIE") == ""}) + w.WriteHeader(http.StatusNoContent) + return + } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return @@ -284,6 +394,13 @@ func main() { return v } mux.HandleFunc("/v1/tasks", func(w http.ResponseWriter, r *http.Request) { + if wid := r.Header.Get("X-Orchestra-Worker"); wid != "" { + token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + if err := workers.Authenticate(wid, token); err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + } if r.Method == "GET" { json.NewEncoder(w).Encode(s.Tasks()) return @@ -353,6 +470,13 @@ func main() { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if wid := r.Header.Get("X-Orchestra-Worker"); wid != "" { + token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + if err := workers.Authenticate(wid, token); err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + } // Reports and handoffs are content-addressed evidence. Keep uploads // bounded because event payloads only carry their resulting hash. r.Body = http.MaxBytesReader(w, r.Body, 4<<20) @@ -407,94 +531,13 @@ func main() { // same session-file assumption as CLIAdapter.Occupancy — rather than // trusting a self-reported number. harnessToken := os.Getenv("ORCHESTRA_HARNESS_TOKEN") - mux.HandleFunc("/v1/harness/complete", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return + mux.Handle("/v1/harness/complete", harnessCompletion{store: s, token: harnessToken, route: func(e domain.Event) error { + if rt == nil { + return nil } - if harnessToken != "" && r.Header.Get("Authorization") != "Bearer "+harnessToken { - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - var p struct { - TaskID string `json:"task_id"` - Harness string `json:"harness"` - TranscriptPath string `json:"transcript_path"` - Report string `json:"report"` - } - if json.NewDecoder(r.Body).Decode(&p) != nil || p.TaskID == "" || p.Report == "" || p.TranscriptPath == "" { - http.Error(w, "task_id, transcript_path, and report are required", http.StatusBadRequest) - return - } - t, ok := s.Task(p.TaskID) - if !ok { - http.Error(w, "task not found", http.StatusNotFound) - return - } - // Harness-specific session-state readers (AUDIT.md "Codex/opencode - // completion producers"). Same local-filesystem assumption B3 already - // made for Claude: the caller supplies the path to its own session - // state (transcript / rollout / message file), never a herdr pane id. - var usage herdr.Usage - var err error - switch p.Harness { - case "codex": - usage, err = herdr.CodexUsage(p.TranscriptPath) - case "opencode": - usage, err = herdr.OpenCodeUsage(p.TranscriptPath) - case "", "claude": - usage, err = herdr.ClaudeUsage(p.TranscriptPath) - default: - http.Error(w, "unknown harness: "+p.Harness, http.StatusBadRequest) - return - } - if err != nil { - http.Error(w, "reading transcript: "+err.Error(), http.StatusBadRequest) - return - } - ref, err := s.PutArtifact([]byte(p.Report)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - payload, _ := json.Marshal(map[string]any{ - "report_ref": ref, - "receipt": map[string]any{ - "input_tokens": usage.Input, - "cache_read_tokens": usage.CacheRead, - "cache_write_tokens": usage.CacheWrite, - "output_tokens": usage.Output, - "numerator": usage.Numerator(), - }, - }) - e := domain.Event{ID: id(), Type: "TaskCompleted", TaskID: p.TaskID, Version: t.Version + 1, Payload: payload, Surface: string(authz.System)} - if err := s.Append(e); err != nil { - http.Error(w, err.Error(), http.StatusConflict) - return - } - if rt != nil { - if _, routeErr := rt.HandleEvent(e); routeErr != nil { - log.Printf("route task: %v", routeErr) - } - } - // B7 (AUDIT.md): QuotaReported has no other producer, so router's - // 5h/weekly availability filter and the brief's quota_consumed are - // permanently zero without this. Fed by the same per-harness usage - // read as the receipt above (spec §7.2 — "same per-harness session - // state as §5.2.1"); harness_id comes from the lease this completion - // closes out, before it's released. - if t.Lease != nil && t.Lease.HarnessID != "" { - qp, _ := json.Marshal(map[string]any{ - "harness_id": t.Lease.HarnessID, - "consumed": float64(usage.Numerator()), - }) - qe := domain.Event{ID: id(), Type: "QuotaReported", TaskID: "system", Payload: qp, Surface: string(authz.System)} - if err := s.Append(qe); err != nil { - log.Printf("quota report: %v", err) - } - } - json.NewEncoder(w).Encode(e) - }) + _, err := rt.HandleEvent(e) + return err + }}) // /v1/harness/turn is the unified turn-decision endpoint (AUDIT.md Phase // 2 items 1-2): the Face-B stop hook posts here on every ordinary turn // boundary (report marker absent — /v1/harness/complete covers task @@ -1009,7 +1052,12 @@ func main() { return } if strings.HasSuffix(r.URL.Path, "/heartbeat") { - if err := workers.Heartbeat(parts[3]); err != nil { + var health federation.WorkerHealth + if err := json.NewDecoder(r.Body).Decode(&health); err != nil && !errors.Is(err, io.EOF) { + http.Error(w, "invalid worker health", 400) + return + } + if err := workers.Heartbeat(parts[3], health); err != nil { http.Error(w, err.Error(), 404) return } @@ -1049,7 +1097,13 @@ func main() { http.Error(w, "task not found", 404) return } - if t.State != domain.StateLeased || t.Lease == nil || t.Lease.HarnessID != parts[3] { + ownedLease := t.State == domain.StateLeased && t.Lease != nil && t.Lease.HarnessID == parts[3] + // A prompt timeout can block the coordinator after herdr already + // accepted the request. If that same authenticated worker later reports + // a durable completion, reconcile it rather than preserving a known + // false blocked state. No other blocked task is admitted here. + recoverableBlocked := strings.HasSuffix(r.URL.Path, "/complete") && t.State == domain.StateBlocked && t.LastHarness == parts[3] + if !ownedLease && !recoverableBlocked { http.Error(w, "lease not owned", 409) return } diff --git a/cmd/orchestra/main_test.go b/cmd/orchestra/main_test.go index d13bb35..2e06169 100644 --- a/cmd/orchestra/main_test.go +++ b/cmd/orchestra/main_test.go @@ -1,12 +1,19 @@ package main import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" "time" + "orchestra/internal/authz" + "orchestra/internal/domain" "orchestra/internal/registry" + "orchestra/internal/store" ) type unreachable struct{} @@ -33,3 +40,93 @@ func TestFederatedReachabilityDefersRemoteHerdrToWorkerHeartbeat(t *testing.T) { t.Fatal("local herdr should still require its TCP probe") } } + +func TestHarnessCompletionBuildsReceiptAndQuotaFromTranscript(t *testing.T) { + s, err := store.Open(t.TempDir()) + if err != nil { + t.Fatal(err) + } + if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "done", Surface: string(authz.System), Payload: []byte(`{"source":"qa","external_id":"done","project":"p"}`)}); err != nil { + t.Fatal(err) + } + if _, err := s.Lease("done", "local-claude", time.Minute); err != nil { + t.Fatal(err) + } + transcript := filepath.Join(t.TempDir(), "transcript.jsonl") + if err := os.WriteFile(transcript, []byte(`{"message":{"usage":{"input_tokens":100,"cache_read_input_tokens":20,"cache_creation_input_tokens":5,"output_tokens":7}}}`+"\n"), 0600); err != nil { + t.Fatal(err) + } + body, _ := json.Marshal(map[string]string{"task_id": "done", "transcript_path": transcript, "report": "# done"}) + req := httptest.NewRequest(http.MethodPost, "/v1/harness/complete", bytes.NewReader(body)) + res := httptest.NewRecorder() + harnessCompletion{store: s, token: "secret"}.ServeHTTP(res, req) + if res.Code != http.StatusUnauthorized { + t.Fatalf("missing token status = %d, want 401", res.Code) + } + req.Header.Set("Authorization", "Bearer secret") + req = httptest.NewRequest(http.MethodPost, "/v1/harness/complete", bytes.NewReader(body)) + req.Header.Set("Authorization", "Bearer secret") + res = httptest.NewRecorder() + harnessCompletion{store: s, token: "secret"}.ServeHTTP(res, req) + if res.Code != http.StatusOK { + t.Fatalf("completion status = %d: %s", res.Code, res.Body.String()) + } + task, ok := s.Task("done") + if !ok || task.State != domain.StateCompleted { + t.Fatalf("task after completion = %#v, present=%v", task, ok) + } + var completed struct { + Receipt struct { + Input int `json:"input_tokens"` + CacheRead int `json:"cache_read_tokens"` + CacheWrite int `json:"cache_write_tokens"` + Output int `json:"output_tokens"` + } `json:"receipt"` + } + for _, e := range s.Events(0) { + if e.Type == "TaskCompleted" { + if err := json.Unmarshal(e.Payload, &completed); err != nil { + t.Fatal(err) + } + } + } + if completed.Receipt.Input != 100 || completed.Receipt.CacheRead != 20 || completed.Receipt.CacheWrite != 5 || completed.Receipt.Output != 7 { + t.Fatalf("receipt = %#v", completed.Receipt) + } + var quota struct { + Harness string `json:"harness_id"` + Consumed float64 `json:"consumed"` + } + found := false + for _, e := range s.Events(0) { + if e.Type == "QuotaReported" { + _ = json.Unmarshal(e.Payload, "a) + found = true + } + } + if !found || quota.Harness != "local-claude" || quota.Consumed != 125 { + t.Fatalf("quota report = %#v, found=%v", quota, found) + } +} + +func TestMultiMachineRegistryRequiresKnownLocalMachine(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(path, []byte(`{ + "machines":[{"id":"homesrv","address":"192.168.1.104:9145"},{"id":"workpc","address":"192.168.1.105:9145"}] +}`), 0o600); err != nil { + t.Fatal(err) + } + r, err := registry.Load(path) + if err != nil { + t.Fatal(err) + } + if err := validateLocalMachine(r, ""); err == nil { + t.Fatal("missing local machine accepted") + } + if err := validateLocalMachine(r, "missing"); err == nil { + t.Fatal("unknown local machine accepted") + } + if err := validateLocalMachine(r, "homesrv"); err != nil { + t.Fatalf("known local machine rejected: %v", err) + } +} diff --git a/deploy/orchestra.env.example b/deploy/orchestra.env.example index f08a589..dc6248f 100644 --- a/deploy/orchestra.env.example +++ b/deploy/orchestra.env.example @@ -18,7 +18,7 @@ ORCHESTRA_PORT=9145 # ORCHESTRA_WORKER_HERDR=/home/orchestra/.config/herdr/herdr.sock # ORCHESTRA_WORKER_STATE=/var/lib/orchestra-worker/state.json # ORCHESTRA_GIT_REMOTE=origin -# ORCHESTRA_MACHINE_ID=homesrv # set on the authoritative coordinator +ORCHESTRA_MACHINE_ID=homesrv # required when the registry has multiple machines # Static project/machine/herdr topology (registry.Load). Required for # routing across more than one machine; validated at startup. diff --git a/internal/authz/authz.go b/internal/authz/authz.go index bc86b1d..bf65898 100644 --- a/internal/authz/authz.go +++ b/internal/authz/authz.go @@ -141,6 +141,19 @@ func (s *Sessions) Valid(v string) bool { return true } +// Revoke removes one browser session. It is deliberately idempotent so a +// logout request remains safe after expiry or after a cookie was cleared by +// the browser. +func (s *Sessions) Revoke(v string) { + if v == "" { + return + } + sum := sha256.Sum256([]byte(v)) + s.mu.Lock() + defer s.mu.Unlock() + delete(s.ids, hex.EncodeToString(sum[:])) +} + // HTTP enforces the same policy at the bus boundary. Authentication is // optional for local development; when a token is supplied, control surfaces // must present it as a Bearer token. @@ -153,6 +166,21 @@ func HTTP(tokens map[Surface]string, next http.Handler) http.Handler { // still has to present the token directly. func HTTPWithSessions(tokens map[Surface]string, sessions *Sessions, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Federation has per-worker credentials, not one shared surface token. + // Let only its registration request and requests that name a worker + // reach their handlers; those handlers authenticate the admission token + // or worker token respectively. Without this exception, an authenticated + // worker is incorrectly treated as the default Web surface. + worker := r.Header.Get("X-Orchestra-Worker") != "" + federationRegistration := r.Method == http.MethodPost && r.URL.Path == "/v1/federation/workers" + workerPath := strings.HasPrefix(r.URL.Path, "/v1/federation/") || + (r.Method == http.MethodGet && r.URL.Path == "/v1/tasks") || + (r.Method == http.MethodPost && r.URL.Path == "/v1/artifacts") || + (r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/v1/artifacts/")) + if federationRegistration || (worker && workerPath) { + next.ServeHTTP(w, r) + return + } s := ParseSurface(r.Header.Get("X-Orchestra-Surface")) if s == "" { s = Web diff --git a/internal/authz/authz_test.go b/internal/authz/authz_test.go index 6511d06..ad0a638 100644 --- a/internal/authz/authz_test.go +++ b/internal/authz/authz_test.go @@ -96,6 +96,41 @@ func TestWebSessionCookieGatesControlPathsOnly(t *testing.T) { } } +func TestFederationRequestsUseTheirOwnCredentials(t *testing.T) { + tokens := map[Surface]string{Web: "web-secret"} + h := HTTPWithSessions(tokens, nil, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + + for _, tc := range []struct { + name string + method string + path string + worker string + want int + }{ + {name: "registration reaches admission handler", method: http.MethodPost, path: "/v1/federation/workers", want: http.StatusNoContent}, + {name: "worker request reaches worker handler", method: http.MethodGet, path: "/v1/federation/events", worker: "workpc-opencode", want: http.StatusNoContent}, + {name: "worker task reconciliation reaches worker handler", method: http.MethodGet, path: "/v1/tasks", worker: "workpc-opencode", want: http.StatusNoContent}, + {name: "worker artifact read reaches worker handler", method: http.MethodGet, path: "/v1/artifacts/ref", worker: "workpc-opencode", want: http.StatusNoContent}, + {name: "worker artifact upload reaches worker handler", method: http.MethodPost, path: "/v1/artifacts", worker: "workpc-opencode", want: http.StatusNoContent}, + {name: "unnamed worker request remains web gated", method: http.MethodGet, path: "/v1/federation/events", want: http.StatusUnauthorized}, + {name: "worker list remains web gated", method: http.MethodGet, path: "/v1/federation/workers", want: http.StatusUnauthorized}, + } { + t.Run(tc.name, func(t *testing.T) { + r := httptest.NewRequest(tc.method, tc.path, nil) + if tc.worker != "" { + r.Header.Set("X-Orchestra-Worker", tc.worker) + } + w := httptest.NewRecorder() + h.ServeHTTP(w, r) + if w.Code != tc.want { + t.Fatalf("status = %d, want %d", w.Code, tc.want) + } + }) + } +} + func TestSessionExpires(t *testing.T) { s := &Sessions{TTL: time.Millisecond} v, err := s.Issue() @@ -110,3 +145,18 @@ func TestSessionExpires(t *testing.T) { t.Fatal("empty session accepted") } } + +func TestSessionRevoke(t *testing.T) { + s := &Sessions{} + v, err := s.Issue() + if err != nil { + t.Fatal(err) + } + if !s.Valid(v) { + t.Fatal("fresh session must be valid") + } + s.Revoke(v) + if s.Valid(v) { + t.Fatal("revoked session must not be valid") + } +} diff --git a/internal/domain/domain.go b/internal/domain/domain.go index 2c401e7..2e3128f 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -66,6 +66,13 @@ type Task struct { Version int `json:"version"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` + // Block evidence is projected from TaskBlocked so terminal records remain + // diagnosable after the live coordinator mapping is gone. + Blocker string `json:"blocker,omitempty"` + BlockedAt time.Time `json:"blocked_at,omitempty"` + LastPaneID string `json:"last_pane_id,omitempty"` + LastHarness string `json:"last_harness_id,omitempty"` + PaneState string `json:"pane_state,omitempty"` // open, closed, unreachable, unknown } type Event struct { @@ -189,6 +196,9 @@ func ValidatePayload(typ string, p map[string]any) error { return err } } + if v, ok := p["pane_state"]; ok && v != "open" && v != "closed" && v != "unreachable" && v != "unknown" { + return fmt.Errorf("%w: pane_state invalid", ErrInvalid) + } case "TaskAmended": if len(p) == 0 { return fmt.Errorf("%w: amendment cannot be empty", ErrInvalid) diff --git a/internal/federation/client.go b/internal/federation/client.go index ab52553..990eb52 100644 --- a/internal/federation/client.go +++ b/internal/federation/client.go @@ -124,8 +124,8 @@ func (c Client) Ack(ctx context.Context, cursor uint64) error { } return err } -func (c Client) Heartbeat(ctx context.Context) error { - resp, err := c.request(ctx, http.MethodPost, "/v1/federation/workers/"+url.PathEscape(c.WorkerID)+"/heartbeat", nil) +func (c Client) Heartbeat(ctx context.Context, health WorkerHealth) error { + resp, err := c.request(ctx, http.MethodPost, "/v1/federation/workers/"+url.PathEscape(c.WorkerID)+"/heartbeat", health) if resp != nil { resp.Body.Close() } @@ -146,6 +146,8 @@ func (c Client) PutArtifact(ctx context.Context, b []byte) (string, error) { return "", err } req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("X-Orchestra-Worker", c.WorkerID) + req.Header.Set("Authorization", "Bearer "+c.Token) h := c.HTTP if h == nil { h = http.DefaultClient diff --git a/internal/federation/federation.go b/internal/federation/federation.go index 34839a5..42c82c7 100644 --- a/internal/federation/federation.go +++ b/internal/federation/federation.go @@ -2,8 +2,11 @@ package federation import ( "crypto/sha256" + "encoding/json" "errors" "fmt" + "os" + "path/filepath" "sync" "time" ) @@ -12,12 +15,26 @@ var ErrUnknownWorker = errors.New("unknown worker") var ErrUnauthorized = errors.New("worker authentication failed") type Worker struct { - ID string `json:"id"` - Address string `json:"address"` - Capacity int `json:"capacity"` - LastSeen time.Time `json:"last_seen"` - Online bool `json:"online"` - Token string `json:"-"` + ID string `json:"id"` + Address string `json:"address"` + Capacity int `json:"capacity"` + LastSeen time.Time `json:"last_seen"` + Online bool `json:"online"` + Health WorkerHealth `json:"health"` + Token string `json:"-"` +} + +// WorkerHealth is reported by the worker that owns the local herdr socket. +// It intentionally does not reuse coordinator TCP-probe state: a remote +// socket is meaningful only from the machine where the worker and checkout +// live. +type WorkerHealth struct { + HerdrStatus string `json:"herdr_status"` // reachable, unreachable, or unknown + CheckedAt time.Time `json:"checked_at,omitempty"` + ActiveTask string `json:"active_task_id,omitempty"` + ActivePane string `json:"active_pane_id,omitempty"` + LastError string `json:"last_error,omitempty"` + ErrorAt time.Time `json:"error_at,omitempty"` } // Capture is published by a worker that owns the pane. The coordinator never @@ -53,6 +70,88 @@ type Registry struct { cursors map[string]uint64 captures map[string]Capture // worker/task commands map[string][]Command + // StatePath preserves worker-owned pane captures and pending approval + // commands across coordinator restarts. A worker must still re-register to + // be online before it can read or act on recovered state. + StatePath string +} + +type persistedState struct { + Captures map[string]Capture `json:"captures"` + Commands map[string][]Command `json:"commands"` + Workers map[string]persistedWorker `json:"workers"` +} + +// persistedWorker deliberately includes the per-worker token. The state file +// is mode 0600, and retaining this binding prevents an arbitrary process from +// registering a recovered worker ID and executing its pending approval. +type persistedWorker struct { + ID string `json:"id"` + Address string `json:"address"` + Capacity int `json:"capacity"` + Token string `json:"token"` +} + +// Load restores durable capture/command state. Call this before accepting +// federation requests; an unreadable state file is unsafe because it could +// otherwise make a pending approval silently disappear. +func (r *Registry) Load() error { + r.mu.Lock() + defer r.mu.Unlock() + r.init() + if r.StatePath == "" { + return nil + } + b, err := os.ReadFile(r.StatePath) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + var state persistedState + if err := json.Unmarshal(b, &state); err != nil { + return fmt.Errorf("invalid federation state: %w", err) + } + if state.Captures != nil { + r.captures = state.Captures + } + if state.Commands != nil { + r.commands = state.Commands + } + for id, w := range state.Workers { + if id == "" || w.ID != id || w.Token == "" { + return fmt.Errorf("invalid federation worker %q", id) + } + r.workers[id] = Worker{ID: w.ID, Address: w.Address, Capacity: w.Capacity, Token: w.Token} + } + return nil +} + +// persistLocked atomically replaces the state file. Callers hold r.mu. +func (r *Registry) persistLocked() error { + if r.StatePath == "" { + return nil + } + workers := make(map[string]persistedWorker, len(r.workers)) + for id, w := range r.workers { + workers[id] = persistedWorker{ID: w.ID, Address: w.Address, Capacity: w.Capacity, Token: w.Token} + } + b, err := json.Marshal(persistedState{Captures: r.captures, Commands: r.commands, Workers: workers}) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(r.StatePath), 0755); err != nil { + return err + } + tmp := r.StatePath + ".tmp" + if err := os.WriteFile(tmp, b, 0600); err != nil { + return err + } + if err := os.Rename(tmp, r.StatePath); err != nil { + return err + } + return os.Chmod(r.StatePath, 0600) } func (r *Registry) init() { @@ -94,6 +193,9 @@ func (r *Registry) PutCapture(worker string, c Capture) (Capture, error) { } c.At = time.Now().UTC() r.captures[k] = c + if err := r.persistLocked(); err != nil { + return Capture{}, fmt.Errorf("persist capture: %w", err) + } return c, nil } func (r *Registry) Capture(worker, task string) (Capture, bool) { @@ -118,6 +220,9 @@ func (r *Registry) Queue(worker string, c Command) (Command, error) { c.Status = "pending" r.commands[worker] = append(r.commands[worker], c) r.pruneCommands(worker) + if err := r.persistLocked(); err != nil { + return Command{}, fmt.Errorf("persist command: %w", err) + } return c, nil } @@ -129,7 +234,7 @@ const CommandRetention = 30 * time.Minute // pruneCommands drops resolved commands past CommandRetention. B21: this list // was append-only, so resolved commands accumulated for the process lifetime // and every worker poll rescanned the entire history. Callers hold r.mu. -func (r *Registry) pruneCommands(worker string) { +func (r *Registry) pruneCommands(worker string) bool { cutoff := time.Now().UTC().Add(-CommandRetention) in := r.commands[worker] out := in[:0] @@ -139,10 +244,12 @@ func (r *Registry) pruneCommands(worker string) { } } if len(out) == 0 { + changed := len(in) != 0 delete(r.commands, worker) - return + return changed } r.commands[worker] = out + return len(out) != len(in) } func (r *Registry) Commands(worker string) ([]Command, error) { r.mu.Lock() @@ -151,7 +258,11 @@ func (r *Registry) Commands(worker string) ([]Command, error) { if _, ok := r.workers[worker]; !ok { return nil, ErrUnknownWorker } - r.pruneCommands(worker) + if r.pruneCommands(worker) { + if err := r.persistLocked(); err != nil { + return nil, fmt.Errorf("persist pruned commands: %w", err) + } + } var out []Command for _, c := range r.commands[worker] { if c.Status == "pending" { @@ -182,6 +293,9 @@ func (r *Registry) CompleteCommand(worker, id, status, message string) error { } r.commands[worker][i].Status = status r.commands[worker][i].Error = message + if err := r.persistLocked(); err != nil { + return fmt.Errorf("persist command resolution: %w", err) + } return nil } } @@ -214,6 +328,9 @@ func (r *Registry) Register(w Worker, admitToken string) error { if _, ok := r.cursors[w.ID]; !ok { r.cursors[w.ID] = 0 } + if err := r.persistLocked(); err != nil { + return fmt.Errorf("persist worker registration: %w", err) + } return nil } func (r *Registry) Authenticate(id, token string) error { @@ -251,7 +368,7 @@ func (r *Registry) Ack(id string, cursor uint64) error { r.cursors[id] = cursor return nil } -func (r *Registry) Heartbeat(id string) error { +func (r *Registry) Heartbeat(id string, health ...WorkerHealth) error { r.mu.Lock() defer r.mu.Unlock() r.init() @@ -261,6 +378,9 @@ func (r *Registry) Heartbeat(id string) error { } w.LastSeen = time.Now().UTC() w.Online = true + if len(health) > 0 { + w.Health = health[0] + } r.workers[id] = w return nil } diff --git a/internal/federation/federation_test.go b/internal/federation/federation_test.go index 8f7d808..d289e6e 100644 --- a/internal/federation/federation_test.go +++ b/internal/federation/federation_test.go @@ -1,6 +1,8 @@ package federation import ( + "os" + "path/filepath" "testing" "time" ) @@ -27,6 +29,68 @@ func TestCursorIsMonotonicAndAuthenticationIsRequired(t *testing.T) { } } +func TestPendingApprovalSurvivesRegistryRestart(t *testing.T) { + path := filepath.Join(t.TempDir(), "federation-state.json") + r := &Registry{StatePath: path} + if err := r.Load(); err != nil { + t.Fatal(err) + } + if err := r.Register(Worker{ID: "w", Token: "t"}, ""); err != nil { + t.Fatal(err) + } + capture, err := r.PutCapture("w", Capture{TaskID: "task", PaneID: "pane", Text: "Allow command?"}) + if err != nil { + t.Fatal(err) + } + queued, err := r.Queue("w", Command{TaskID: "task", Kind: "grant_approval", PaneID: "pane", CaptureRevision: capture.Revision}) + if err != nil { + t.Fatal(err) + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0600 { + t.Fatalf("federation state permissions = %o, want 0600", info.Mode().Perm()) + } + + restarted := &Registry{StatePath: path} + if err := restarted.Load(); err != nil { + t.Fatal(err) + } + if err := restarted.Register(Worker{ID: "w", Token: "intruder"}, ""); err != ErrUnauthorized { + t.Fatalf("recovered worker identity was hijackable: %v", err) + } + // A restart does not mark the worker online; it must prove its retained + // identity by registering again before recovered controls become available. + if err := restarted.Register(Worker{ID: "w", Token: "t"}, ""); err != nil { + t.Fatal(err) + } + gotCapture, ok := restarted.Capture("w", "task") + if !ok || gotCapture.Revision != capture.Revision || gotCapture.Text != capture.Text { + t.Fatalf("capture after restart = %#v, present=%v", gotCapture, ok) + } + commands, err := restarted.Commands("w") + if err != nil || len(commands) != 1 || commands[0].ID != queued.ID { + t.Fatalf("commands after restart = %#v, err=%v", commands, err) + } + if err := restarted.CompleteCommand("w", queued.ID, "acknowledged", ""); err != nil { + t.Fatal(err) + } + + again := &Registry{StatePath: path} + if err := again.Load(); err != nil { + t.Fatal(err) + } + if err := again.Register(Worker{ID: "w", Token: "t"}, ""); err != nil { + t.Fatal(err) + } + commands, err = again.Commands("w") + if err != nil || len(commands) != 0 { + t.Fatalf("resolved command recovered as pending: %#v, err=%v", commands, err) + } +} + func TestRegisterRequiresAdmitTokenAndOwnToken(t *testing.T) { r := &Registry{AdmitToken: "admit-secret"} if err := r.Register(Worker{ID: "workpc", Token: "secret"}, "wrong"); err != ErrUnauthorized { @@ -76,6 +140,29 @@ func TestOfflineHookRunsOnceOnTransition(t *testing.T) { } } +func TestHeartbeatProjectsWorkerOwnedHealth(t *testing.T) { + r := &Registry{} + if err := r.Register(Worker{ID: "workpc-opencode", Token: "secret"}, ""); err != nil { + t.Fatal(err) + } + checked := time.Now().UTC().Round(0) + errAt := checked.Add(-time.Minute) + if err := r.Heartbeat("workpc-opencode", WorkerHealth{ + HerdrStatus: "unreachable", CheckedAt: checked, ActiveTask: "task-1", ActivePane: "pane-1", + LastError: "local herdr: connection refused", ErrorAt: errAt, + }); err != nil { + t.Fatal(err) + } + workers := r.Snapshot() + if len(workers) != 1 { + t.Fatalf("workers=%#v", workers) + } + h := workers[0].Health + if h.HerdrStatus != "unreachable" || h.ActiveTask != "task-1" || h.ActivePane != "pane-1" || h.LastError == "" || !h.CheckedAt.Equal(checked) || !h.ErrorAt.Equal(errAt) { + t.Fatalf("health=%#v", h) + } +} + func TestCaptureRevisionAndCommandQueue(t *testing.T) { r := &Registry{} if err := r.Register(Worker{ID: "w", Token: "t"}, ""); err != nil { diff --git a/internal/herdr/activity.go b/internal/herdr/activity.go index a907dba..ddc5275 100644 --- a/internal/herdr/activity.go +++ b/internal/herdr/activity.go @@ -142,7 +142,7 @@ func ClaudeActivity(path string) ([]ToolCall, error) { // 2. Shell commands: Codex's actual tool surface is a single freeform // `custom_tool_call` named "exec" whose `input` is a JS snippet calling // `tools.exec_command({cmd:"...", ...})` — not a flat arguments object. -// codexExecCommand best-effort-extracts the first embedded cmd string. +// codexExecCommands extracts every embedded cmd string in source order. // Success is read from the paired `custom_tool_call_output`'s text // blocks: a failed script's output observably starts with "Script // error:" on this machine's real transcripts (both a JS syntax error and @@ -173,7 +173,7 @@ func CodexActivity(path string) ([]ToolCall, error) { Payload payload `json:"payload"` } - pending := map[string]ToolCall{} + pending := map[string][]ToolCall{} var calls []ToolCall s := bufio.NewScanner(f) s.Buffer(make([]byte, 1<<20), 10<<20) @@ -188,13 +188,20 @@ func CodexActivity(path string) ([]ToolCall, error) { calls = append(calls, ToolCall{Name: "apply_patch", Kind: "file", Key: path, Success: e.Payload.Success}) } case e.Type == "response_item" && e.Payload.Type == "custom_tool_call": - kind, key := "", "" - if cmd := codexExecCommand(e.Payload.Input); cmd != "" { - kind, key = "command", cmd + var pendingCalls []ToolCall + if e.Payload.Name == "exec" { + for _, cmd := range codexExecCommands(e.Payload.Input) { + pendingCalls = append(pendingCalls, ToolCall{Name: e.Payload.Name, Kind: "command", Key: cmd, IsTest: isTestCommand("command", cmd)}) + } } - pending[e.Payload.CallID] = ToolCall{Name: e.Payload.Name, Kind: kind, Key: key, IsTest: isTestCommand(kind, key)} + // Retain a resolved call without an extractable command as activity: + // it is useful for ordering, but deliberately carries no key. + if len(pendingCalls) == 0 { + pendingCalls = []ToolCall{{Name: e.Payload.Name}} + } + pending[e.Payload.CallID] = pendingCalls case e.Type == "response_item" && e.Payload.Type == "custom_tool_call_output": - if tc, ok := pending[e.Payload.CallID]; ok { + if pendingCalls, ok := pending[e.Payload.CallID]; ok { failed := false for _, o := range e.Payload.Output { if strings.HasPrefix(strings.TrimSpace(o.Text), "Script error:") { @@ -202,8 +209,10 @@ func CodexActivity(path string) ([]ToolCall, error) { break } } - tc.Success = !failed - calls = append(calls, tc) + for _, tc := range pendingCalls { + tc.Success = !failed + calls = append(calls, tc) + } delete(pending, e.Payload.CallID) } } @@ -211,18 +220,21 @@ func CodexActivity(path string) ([]ToolCall, error) { return calls, s.Err() } -// codexExecCmdRe extracts the first `cmd:"..."` argument out of an "exec" -// custom-tool-call's JS-scripted input. Only the first embedded command in a -// multi-call script is captured — a documented limitation, not an oversight. +// codexExecCmdRe extracts `cmd:"..."` arguments out of an "exec" +// custom-tool-call's JS-scripted input. A single script can invoke several +// commands; their source order is the observable execution order. var codexExecCmdRe = regexp.MustCompile(`cmd\s*:\s*"((?:[^"\\]|\\.)*)"`) -func codexExecCommand(input string) string { - m := codexExecCmdRe.FindStringSubmatch(input) - if m == nil { - return "" +func codexExecCommands(input string) []string { + matches := codexExecCmdRe.FindAllStringSubmatch(input, -1) + commands := make([]string, 0, len(matches)) + for _, m := range matches { + cmd := strings.TrimSpace(strings.NewReplacer(`\"`, `"`, `\n`, "\n", `\t`, "\t", `\\`, `\`).Replace(m[1])) + if cmd != "" { + commands = append(commands, cmd) + } } - cmd := strings.NewReplacer(`\"`, `"`, `\n`, "\n", `\t`, "\t", `\\`, `\`).Replace(m[1]) - return strings.TrimSpace(cmd) + return commands } // OpenCodeActivity has no verified source. OpenCodeUsage already only reads diff --git a/internal/herdr/activity_test.go b/internal/herdr/activity_test.go index b9cd999..b5885f7 100644 --- a/internal/herdr/activity_test.go +++ b/internal/herdr/activity_test.go @@ -178,6 +178,26 @@ func TestCodexActivityMarksScriptErrorAsFailure(t *testing.T) { } } +func TestCodexActivityPreservesAllCommandsInAScript(t *testing.T) { + path := writeJSONL(t, []string{ + codexExecCallLine(t, "c1", `const test = await tools.exec_command({cmd:"go test ./..."}); const commit = await tools.exec_command({cmd:"git commit -am done"}); text(test.output); text(commit.output)`), + codexExecOutputLine(t, "c1", "Script completed"), + }) + calls, err := CodexActivity(path) + if err != nil { + t.Fatal(err) + } + if len(calls) != 2 { + t.Fatalf("calls=%+v, want both commands", calls) + } + if calls[0].Key != "go test ./..." || calls[1].Key != "git commit -am done" || !calls[0].Success || !calls[1].Success { + t.Fatalf("calls=%+v, want successful commands in source order", calls) + } + if !DetectMilestone(calls) { + t.Fatalf("want the later successful git commit to be a milestone") + } +} + func TestDetectThrashConsecutiveTestFailures(t *testing.T) { calls := []ToolCall{ {Name: "Bash", Kind: "command", Key: "go test ./...", Success: false, IsTest: true}, diff --git a/internal/herdr/adapter.go b/internal/herdr/adapter.go index de3366b..4426b54 100644 --- a/internal/herdr/adapter.go +++ b/internal/herdr/adapter.go @@ -42,10 +42,6 @@ type PromptLeaser interface { LeasePrompt(context.Context, string, string, string) (Session, error) } -type WorktreeCreator interface { - CreateWorktree(context.Context, string, string, string) (string, error) -} - // TurnBoundary is optional so older herdr deployments remain usable. A true // result means the current harness turn has ended and handoff is safe. type TurnBoundary interface { @@ -99,17 +95,6 @@ const HandoffFile = ".orchestra-handoff.json" // seals the resulting canonical JSON. const HandoffReportFile = ".orchestra-handoff-report.md" -func (a CLIAdapter) CreateWorktree(ctx context.Context, repo, root, taskID string) (string, error) { - path, err := a.Client.Worktree(ctx, repo, filepath.Join(root, taskID), "orchestra/"+taskID) - if err != nil { - return "", err - } - if path == "" { - return "", fmt.Errorf("adapter: herdr returned empty worktree path") - } - return path, nil -} - func (a CLIAdapter) Lease(ctx context.Context, task, worktree string) (Session, error) { return a.LeasePrompt(ctx, task, worktree, defaultTaskPrompt(task)) } diff --git a/internal/herdr/herdr.go b/internal/herdr/herdr.go index c032989..56e1ea8 100644 --- a/internal/herdr/herdr.go +++ b/internal/herdr/herdr.go @@ -193,14 +193,16 @@ const ( // after 2+ minutes of polling, with no error surfaced anywhere. A // legitimate attach has been observed taking "well over a minute", so // this window is deliberately longer than bootRetryWindow. - agentAttachWindow = 90 * time.Second - agentAttachPoll = 2 * time.Second - claudeTrustObserveWindow = 15 * time.Second claudeTrustClearWindow = 15 * time.Second claudeTrustPoll = 500 * time.Millisecond ) +var ( + agentAttachWindow = 90 * time.Second + agentAttachPoll = 2 * time.Second +) + type paneStatus struct { Agent string `json:"agent"` AgentStatus string `json:"agent_status"` diff --git a/internal/herdr/herdr_test.go b/internal/herdr/herdr_test.go index a95f8d4..ad27d6c 100644 --- a/internal/herdr/herdr_test.go +++ b/internal/herdr/herdr_test.go @@ -7,6 +7,7 @@ import ( "net" "reflect" "regexp" + "strings" "testing" "time" ) @@ -82,6 +83,97 @@ func TestStartAgentPassesEmptyHarnessArgs(t *testing.T) { } } +func TestStartAgentAttachesTwoSameHarnessSessionsWithDistinctNames(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = ln.Close() }) + starts := make(chan Request, 2) + go func() { + for i := 0; i < 4; i++ { + conn, err := ln.Accept() + if err != nil { + return + } + var req Request + if json.NewDecoder(bufio.NewReader(conn)).Decode(&req) == nil { + switch req.Method { + case "agent.start": + starts <- req + _ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{}`)}) + case "pane.get": + _ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{"type":"pane_info","pane":{"agent":"opencode","agent_status":"idle"}}`)}) + } + } + _ = conn.Close() + } + }() + c := &Client{Path: ln.Addr().String(), panes: map[string]string{"/one": "w1:p1", "/two": "w2:p1"}, dial: func() (net.Conn, error) { + return net.Dial("tcp", ln.Addr().String()) + }} + first, err := c.StartAgent(context.Background(), "", "/one", "", "opencode", "first-task") + if err != nil { + t.Fatal(err) + } + second, err := c.StartAgent(context.Background(), "", "/two", "", "opencode", "second-task") + if err != nil { + t.Fatal(err) + } + if first.AgentName == second.AgentName || first.AgentName == "" || second.AgentName == "" { + t.Fatalf("agent names must be distinct and persisted: %+v / %+v", first, second) + } + for _, want := range []string{first.AgentName, second.AgentName} { + req := <-starts + params, _ := json.Marshal(req.Params) + var got struct { + Name string `json:"name"` + } + _ = json.Unmarshal(params, &got) + if got.Name != want { + t.Fatalf("agent.start name = %q, want %q", got.Name, want) + } + } +} + +func TestStartAgentRejectsSuccessWithoutAttachment(t *testing.T) { + oldWindow, oldPoll := agentAttachWindow, agentAttachPoll + agentAttachWindow, agentAttachPoll = 25*time.Millisecond, time.Millisecond + t.Cleanup(func() { agentAttachWindow, agentAttachPoll = oldWindow, oldPoll }) + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = ln.Close() }) + go func() { + for { + conn, err := ln.Accept() + if err != nil { + return + } + go func() { + defer conn.Close() + var req Request + if json.NewDecoder(bufio.NewReader(conn)).Decode(&req) != nil { + return + } + result := json.RawMessage(`{"type":"pane_info","pane":{"agent_status":"unknown"}}`) + if req.Method == "agent.start" { + result = json.RawMessage(`{}`) + } + _ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: result}) + }() + } + }() + c := &Client{Path: ln.Addr().String(), panes: map[string]string{"/worktree": "w1:p1"}, dial: func() (net.Conn, error) { + return net.Dial("tcp", ln.Addr().String()) + }} + _, err = c.StartAgent(context.Background(), "", "/worktree", "", "opencode", "silent-noop") + if err == nil || !strings.Contains(err.Error(), "no agent attached") { + t.Fatalf("StartAgent error = %v, want explicit missing attachment", err) + } +} + func TestAgentNameIsBoundedAndValid(t *testing.T) { got := agentName("OpenCode", "TASK With spaces / and symbols !!! 0123456789") if len(got) > 32 || !regexp.MustCompile(`^[a-z0-9_-]+$`).MatchString(got) { diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 52d106b..3f2137a 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -170,11 +170,15 @@ type Coordinator struct { Worktrees Worktrees Adapters Adapters StatePath string - mu sync.Mutex - sessions map[string]herdr.Session - loaded bool - healthMu sync.RWMutex - health MonitorHealth + // LocalHerdr, when set, is the coordinator's machine-ownership boundary. + // A coordinator must never operate a pane or checkout owned by another + // machine; federation workers own those operations locally. + LocalHerdr func(string) bool + mu sync.Mutex + sessions map[string]herdr.Session + loaded bool + healthMu sync.RWMutex + health MonitorHealth // Hard is the occupancy threshold Monitor's periodic rotate() runs // against, mirrored here so TurnDecision (the synchronous, per-turn // counterpart driven by the Face-B stop hook) evaluates the same @@ -300,6 +304,9 @@ func (c *Coordinator) adapterFor(taskID string, session herdr.Session) (herdr.Ad if id == "" { id = session.Harness } + if c.LocalHerdr != nil && !c.LocalHerdr(id) { + return nil, fmt.Errorf("session %s is owned by non-local herdr %s", taskID, id) + } return c.Adapters.Adapter(id) } @@ -875,31 +882,20 @@ func (c *Coordinator) Start(ctx context.Context, e domain.Event) error { if err := json.Unmarshal(e.Payload, &p); err != nil || p.HarnessID == "" { return fmt.Errorf("orchestrator: invalid lease") } + if c.LocalHerdr != nil && !c.LocalHerdr(p.HarnessID) { + return c.block(t, "remote herdr must be operated by its federation worker") + } a, err := c.Adapters.Adapter(p.HarnessID) if err != nil { return c.block(t, "adapter: "+err.Error()) } - var w string - if creator, ok := a.(herdr.WorktreeCreator); ok { - planner, planned := c.Worktrees.(WorktreeSpec) - if !planned { - return c.block(t, "worktree: repository specification unavailable") - } - repo, root, valid := planner.Spec(t) - if !valid { - return c.block(t, "worktree: repository and root required") - } - w, err = creator.CreateWorktree(ctx, repo, root, t.ID) - } else { - w, err = c.Worktrees.Create(ctx, t) - } + // Worktrees, including immutable TASK.md, are coordinator-local state. + // A remote herdr must be driven by its federation worker instead of being + // asked to create an opaque checkout that this coordinator cannot validate. + w, err := c.Worktrees.Create(ctx, t) if err != nil { return c.block(t, "worktree: "+err.Error()) } - // Best-effort: TASK.md only exists for worktrees this process can read - // locally (the GitWorktrees path). A herdr-hosted worktree on a remote - // machine (WorktreeCreator path) is the same cross-host gap named in - // AUDIT.md's federation-fork section — not solved here. taskFileSHA, _ := continuity.TaskFileHash(w) prompt := taskLaunchPrompt(t) var s herdr.Session @@ -983,7 +979,13 @@ func (c *Coordinator) rememberSession(taskID string, s herdr.Session) error { } func (c *Coordinator) block(t domain.Task, reason string) error { - b, _ := json.Marshal(map[string]string{"blocker": reason}) + p := map[string]string{"blocker": reason, "pane_state": "unknown"} + if s, ok := c.Session(t.ID); ok { + p["pane_id"] = s.PaneID + p["harness_id"] = s.HerdrID + p["pane_state"] = "open" + } + b, _ := json.Marshal(p) return c.Store.Append(domain.Event{ID: domain.NewID(), Type: "TaskBlocked", TaskID: t.ID, Version: t.Version + 1, Payload: b, Surface: string(authz.System)}) } diff --git a/internal/orchestrator/rotation_test.go b/internal/orchestrator/rotation_test.go index 1588d59..6956871 100644 --- a/internal/orchestrator/rotation_test.go +++ b/internal/orchestrator/rotation_test.go @@ -21,9 +21,17 @@ type fakeAdapter struct { boundary bool ref string releases int + leases int + approval struct { + called bool + grant bool + session herdr.Session + capture string + } } func (a *fakeAdapter) Lease(_ context.Context, _ string, worktree string) (herdr.Session, error) { + a.leases++ return herdr.Session{Harness: "h1", PaneID: "pane-1", Worktree: worktree}, nil } func (a *fakeAdapter) Bootstrap(context.Context, herdr.Session, string) error { return nil } @@ -36,6 +44,13 @@ func (a *fakeAdapter) Occupancy(herdr.Session) (float64, error) { return a.occu func (a *fakeAdapter) AtTurnBoundary(context.Context, herdr.Session) (bool, error) { return a.boundary, nil } +func (a *fakeAdapter) RespondApproval(_ context.Context, s herdr.Session, grant bool, capture string) error { + a.approval.called = true + a.approval.grant = grant + a.approval.session = s + a.approval.capture = capture + return nil +} type worktrees struct{ path string } @@ -45,6 +60,12 @@ type adapters struct{ a herdr.Adapter } func (a adapters) Adapter(string) (herdr.Adapter, error) { return a.a, nil } +type promptFailureAdapter struct{ fakeAdapter } + +func (a *promptFailureAdapter) LeasePrompt(_ context.Context, _ string, worktree, _ string) (herdr.Session, error) { + return herdr.Session{Harness: "h1", PaneID: "pane-created-before-timeout", Worktree: worktree}, errors.New("prompt delivery uncertain") +} + func run(t *testing.T, dir string, args ...string) { t.Helper() cmd := exec.Command("git", append([]string{"-C", dir}, args...)...) @@ -53,6 +74,105 @@ func run(t *testing.T, dir string, args ...string) { } } +func TestPromptFailureRetainsLivePaneForBlockedTaskAcrossRestart(t *testing.T) { + s, err := store.Open(t.TempDir()) + if err != nil { + t.Fatal(err) + } + if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "blocked-live-pane", Surface: string(authz.System), Payload: mustJSON(map[string]any{ + "source": "qa", "external_id": "prompt-timeout", "project": "p", + })}); err != nil { + t.Fatal(err) + } + task, ok := s.Task("blocked-live-pane") + if !ok { + t.Fatal("created task missing") + } + lease, err := s.Lease(task.ID, "h1", time.Minute) + if err != nil { + t.Fatal(err) + } + statePath := t.TempDir() + "/sessions.json" + a := &promptFailureAdapter{} + c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: t.TempDir()}, Adapters: adapters{a}, StatePath: statePath} + if err := c.Start(context.Background(), lease); err != nil { + t.Fatal(err) + } + if got, ok := s.Task(task.ID); !ok || got.State != domain.StateBlocked { + t.Fatalf("task state = %+v, want blocked", got) + } + if session, ok := c.Session(task.ID); !ok || session.PaneID != "pane-created-before-timeout" || session.HerdrID != "h1" { + t.Fatalf("retained session = %+v, present=%v", session, ok) + } + // A fresh coordinator must retain the mapping for a blocked task rather + // than treating it as an orphan after restart. + restarted := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: t.TempDir()}, Adapters: adapters{a}, StatePath: statePath} + if err := restarted.Reconcile(context.Background()); err != nil { + t.Fatal(err) + } + if session, ok := restarted.Session(task.ID); !ok || session.PaneID != "pane-created-before-timeout" { + t.Fatalf("restarted session = %+v, present=%v", session, ok) + } +} + +func TestRespondApprovalUsesOwningSessionAndPreservesCaptureBinding(t *testing.T) { + s, err := store.Open(t.TempDir()) + if err != nil { + t.Fatal(err) + } + if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "approval-task", Surface: string(authz.System), Payload: mustJSON(map[string]any{ + "source": "qa", "external_id": "approval", "project": "p", + })}); err != nil { + t.Fatal(err) + } + lease, err := s.Lease("approval-task", "herdr-1", time.Minute) + if err != nil { + t.Fatal(err) + } + a := &fakeAdapter{} + c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: t.TempDir()}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"} + if err := c.Start(context.Background(), lease); err != nil { + t.Fatal(err) + } + const capture = "Approval required\n$ go test ./...\n[y/n]" + if err := c.RespondApproval(context.Background(), "approval-task", true, capture); err != nil { + t.Fatal(err) + } + if !a.approval.called || !a.approval.grant || a.approval.capture != capture { + t.Fatalf("approval invocation = %#v", a.approval) + } + if a.approval.session.HerdrID != "herdr-1" || a.approval.session.PaneID == "" { + t.Fatalf("approval used wrong session: %#v", a.approval.session) + } +} + +func TestCoordinatorRefusesRemoteHerdrOperations(t *testing.T) { + s, err := store.Open(t.TempDir()) + if err != nil { + t.Fatal(err) + } + if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "remote", Surface: string(authz.System), Payload: mustJSON(map[string]any{ + "source": "qa", "external_id": "remote", "project": "p", + })}); err != nil { + t.Fatal(err) + } + lease, err := s.Lease("remote", "remote", time.Minute) + if err != nil { + t.Fatal(err) + } + a := &fakeAdapter{} + c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: t.TempDir()}, Adapters: adapters{a}, LocalHerdr: func(id string) bool { return id == "local" }} + if err := c.Start(context.Background(), lease); err != nil { + t.Fatal(err) + } + if a.leases != 0 { + t.Fatal("remote adapter was started by coordinator") + } + if task, ok := s.Task("remote"); !ok || task.State != domain.StateBlocked { + t.Fatalf("remote task state = %#v, present=%v; want blocked", task, ok) + } +} + // TestRotationEmitsValidReleaseWithAnchorSHA guards the highest-priority spec // defect noted in progress.md: automated rotation must emit a TaskReleased // event that satisfies domain.ValidatePayload (handoff_ref + anchor_sha), not diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 6b41e41..8798cc0 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -174,6 +174,14 @@ func putID[T any](m map[string]T, id, kind string) error { func (r Registry) Project(id string) (Project, bool) { p, ok := r.projects[id]; return p, ok } func (r Registry) Machine(id string) (Machine, bool) { m, ok := r.machines[id]; return m, ok } func (r Registry) Herdr(id string) (Herdr, bool) { h, ok := r.herdrs[id]; return h, ok } +func (r Registry) Machines() []Machine { + out := make([]Machine, 0, len(r.machines)) + for _, m := range r.machines { + out = append(out, m) + } + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) + return out +} func (r Registry) Herdrs() []Herdr { out := make([]Herdr, 0, len(r.herdrs)) for _, h := range r.herdrs { diff --git a/internal/store/store.go b/internal/store/store.go index e6f7b51..3f383d6 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -155,6 +155,14 @@ func (s *Store) apply(e domain.Event) error { case "TaskBlocked": t.State = domain.StateBlocked t.Lease = nil + t.Blocker, _ = p["blocker"].(string) + t.BlockedAt = e.At + t.LastPaneID, _ = p["pane_id"].(string) + t.LastHarness, _ = p["harness_id"].(string) + t.PaneState, _ = p["pane_state"].(string) + if t.PaneState == "" { + t.PaneState = "unknown" + } case "TaskAmended": if v, ok := p["title"].(string); ok { t.Title = v diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 9dd9b16..935a7ce 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -136,7 +136,9 @@ func (s Server) detail(ctx context.Context, id string) (TaskDetail, error) { if !ok { return TaskDetail{}, domain.ErrNotFound } - d := TaskDetail{Task: t, Actions: actions(t)} + // Keep collection fields as JSON arrays for browser clients, including + // older task records that genuinely have no retained events. + d := TaskDetail{Task: t, Events: []domain.Event{}, Actions: actions(t)} for _, e := range s.Store.Events(0) { if e.TaskID != id { continue @@ -165,9 +167,14 @@ func (s Server) detail(ctx context.Context, id string) (TaskDetail, error) { session.Blocker = "capture unavailable: " + err.Error() } d.Session = session + } else if t.State == domain.StateBlocked { + // A blocked task has no active lease by definition, but must retain its + // last observed pane evidence instead of rendering an unexplained void. + d.Session = &Session{PaneID: t.LastPaneID, HarnessID: t.LastHarness, AgentStatus: t.PaneState, Blocker: t.Blocker} } return d, nil } + // captureRevision identifies *what the operator saw*, not when they saw it. // B20: this was UnixNano, so it changed on every read and said nothing about // whether the pane had changed. A content hash changes if and only if the @@ -206,7 +213,15 @@ func actions(t domain.Task) []Action { return []Action{{ID: "handoff", Enabled: active, Reason: "requires a live leased session"}, {ID: "release", Enabled: active, Needs: []string{"reason or handoff_ref"}}, {ID: "block", Enabled: active, Needs: []string{"blocker"}}, {ID: "complete", Enabled: active, Needs: []string{"report_ref", "receipt"}}} } func (s Server) Overview(ctx context.Context) Overview { - out := Overview{Tasks: s.Store.Tasks(), UpdatedAt: time.Now().UTC()} + // JSON null is not an empty collection to browser clients. In particular, + // the shell renders the number of active sessions before any page-level + // loading state, so a nil Sessions slice made an otherwise healthy empty + // worker pool crash the entire SPA on `sessions.length`. + tasks := s.Store.Tasks() + if tasks == nil { + tasks = []domain.Task{} + } + out := Overview{Tasks: tasks, Workers: []federation.Worker{}, Sessions: []Session{}, UpdatedAt: time.Now().UTC()} if s.Workers != nil { out.Workers = s.Workers.Snapshot() } diff --git a/internal/webui/assets/assets/index-B082_NkA.js b/internal/webui/assets/assets/index-B082_NkA.js deleted file mode 100644 index db1a4b1..0000000 --- a/internal/webui/assets/assets/index-B082_NkA.js +++ /dev/null @@ -1,60 +0,0 @@ -var Zm=i=>{throw TypeError(i)};var tr=(i,c,f)=>c.has(i)||Zm("Cannot "+f);var v=(i,c,f)=>(tr(i,c,"read from private field"),f?f.call(i):c.get(i)),W=(i,c,f)=>c.has(i)?Zm("Cannot add the same private member more than once"):c instanceof WeakSet?c.add(i):c.set(i,f),K=(i,c,f,s)=>(tr(i,c,"write to private field"),s?s.call(i,f):c.set(i,f),f),st=(i,c,f)=>(tr(i,c,"access private method"),f);var Sc=(i,c,f,s)=>({set _(o){K(i,c,o,f)},get _(){return v(i,c,s)}});(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))s(o);new MutationObserver(o=>{for(const d of o)if(d.type==="childList")for(const p of d.addedNodes)p.tagName==="LINK"&&p.rel==="modulepreload"&&s(p)}).observe(document,{childList:!0,subtree:!0});function f(o){const d={};return o.integrity&&(d.integrity=o.integrity),o.referrerPolicy&&(d.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?d.credentials="include":o.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function s(o){if(o.ep)return;o.ep=!0;const d=f(o);fetch(o.href,d)}})();function Np(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var er={exports:{}},Yu={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Km;function Hp(){if(Km)return Yu;Km=1;var i=Symbol.for("react.transitional.element"),c=Symbol.for("react.fragment");function f(s,o,d){var p=null;if(d!==void 0&&(p=""+d),o.key!==void 0&&(p=""+o.key),"key"in o){d={};for(var E in o)E!=="key"&&(d[E]=o[E])}else d=o;return o=d.ref,{$$typeof:i,type:s,key:p,ref:o!==void 0?o:null,props:d}}return Yu.Fragment=c,Yu.jsx=f,Yu.jsxs=f,Yu}var Vm;function jp(){return Vm||(Vm=1,er.exports=Hp()),er.exports}var w=jp(),lr={exports:{}},nt={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Jm;function qp(){if(Jm)return nt;Jm=1;var i=Symbol.for("react.transitional.element"),c=Symbol.for("react.portal"),f=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),o=Symbol.for("react.profiler"),d=Symbol.for("react.consumer"),p=Symbol.for("react.context"),E=Symbol.for("react.forward_ref"),g=Symbol.for("react.suspense"),m=Symbol.for("react.memo"),O=Symbol.for("react.lazy"),T=Symbol.for("react.activity"),N=Symbol.iterator;function B(b){return b===null||typeof b!="object"?null:(b=N&&b[N]||b["@@iterator"],typeof b=="function"?b:null)}var Y={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Q=Object.assign,L={};function q(b,j,V){this.props=b,this.context=j,this.refs=L,this.updater=V||Y}q.prototype.isReactComponent={},q.prototype.setState=function(b,j){if(typeof b!="object"&&typeof b!="function"&&b!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,b,j,"setState")},q.prototype.forceUpdate=function(b){this.updater.enqueueForceUpdate(this,b,"forceUpdate")};function F(){}F.prototype=q.prototype;function X(b,j,V){this.props=b,this.context=j,this.refs=L,this.updater=V||Y}var G=X.prototype=new F;G.constructor=X,Q(G,q.prototype),G.isPureReactComponent=!0;var et=Array.isArray;function ft(){}var I={H:null,A:null,T:null,S:null},lt=Object.prototype.hasOwnProperty;function yt(b,j,V){var $=V.ref;return{$$typeof:i,type:b,key:j,ref:$!==void 0?$:null,props:V}}function Ut(b,j){return yt(b.type,j,b.props)}function Yt(b){return typeof b=="object"&&b!==null&&b.$$typeof===i}function Gt(b){var j={"=":"=0",":":"=2"};return"$"+b.replace(/[=:]/g,function(V){return j[V]})}var Ee=/\/+/g;function te(b,j){return typeof b=="object"&&b!==null&&b.key!=null?Gt(""+b.key):j.toString(36)}function xt(b){switch(b.status){case"fulfilled":return b.value;case"rejected":throw b.reason;default:switch(typeof b.status=="string"?b.then(ft,ft):(b.status="pending",b.then(function(j){b.status==="pending"&&(b.status="fulfilled",b.value=j)},function(j){b.status==="pending"&&(b.status="rejected",b.reason=j)})),b.status){case"fulfilled":return b.value;case"rejected":throw b.reason}}throw b}function U(b,j,V,$,ut){var rt=typeof b;(rt==="undefined"||rt==="boolean")&&(b=null);var Et=!1;if(b===null)Et=!0;else switch(rt){case"bigint":case"string":case"number":Et=!0;break;case"object":switch(b.$$typeof){case i:case c:Et=!0;break;case O:return Et=b._init,U(Et(b._payload),j,V,$,ut)}}if(Et)return ut=ut(b),Et=$===""?"."+te(b,0):$,et(ut)?(V="",Et!=null&&(V=Et.replace(Ee,"$&/")+"/"),U(ut,j,V,"",function(Jn){return Jn})):ut!=null&&(Yt(ut)&&(ut=Ut(ut,V+(ut.key==null||b&&b.key===ut.key?"":(""+ut.key).replace(Ee,"$&/")+"/")+Et)),j.push(ut)),1;Et=0;var se=$===""?".":$+":";if(et(b))for(var wt=0;wt>>1,_t=U[At];if(0>>1;Ato(V,at))$<_t&&0>o(ut,V)?(U[At]=ut,U[$]=at,At=$):(U[At]=V,U[j]=at,At=j);else if($<_t&&0>o(ut,at))U[At]=ut,U[$]=at,At=$;else break t}}return Z}function o(U,Z){var at=U.sortIndex-Z.sortIndex;return at!==0?at:U.id-Z.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var d=performance;i.unstable_now=function(){return d.now()}}else{var p=Date,E=p.now();i.unstable_now=function(){return p.now()-E}}var g=[],m=[],O=1,T=null,N=3,B=!1,Y=!1,Q=!1,L=!1,q=typeof setTimeout=="function"?setTimeout:null,F=typeof clearTimeout=="function"?clearTimeout:null,X=typeof setImmediate<"u"?setImmediate:null;function G(U){for(var Z=f(m);Z!==null;){if(Z.callback===null)s(m);else if(Z.startTime<=U)s(m),Z.sortIndex=Z.expirationTime,c(g,Z);else break;Z=f(m)}}function et(U){if(Q=!1,G(U),!Y)if(f(g)!==null)Y=!0,ft||(ft=!0,Gt());else{var Z=f(m);Z!==null&&xt(et,Z.startTime-U)}}var ft=!1,I=-1,lt=5,yt=-1;function Ut(){return L?!0:!(i.unstable_now()-ytU&&Ut());){var At=T.callback;if(typeof At=="function"){T.callback=null,N=T.priorityLevel;var _t=At(T.expirationTime<=U);if(U=i.unstable_now(),typeof _t=="function"){T.callback=_t,G(U),Z=!0;break e}T===f(g)&&s(g),G(U)}else s(g);T=f(g)}if(T!==null)Z=!0;else{var b=f(m);b!==null&&xt(et,b.startTime-U),Z=!1}}break t}finally{T=null,N=at,B=!1}Z=void 0}}finally{Z?Gt():ft=!1}}}var Gt;if(typeof X=="function")Gt=function(){X(Yt)};else if(typeof MessageChannel<"u"){var Ee=new MessageChannel,te=Ee.port2;Ee.port1.onmessage=Yt,Gt=function(){te.postMessage(null)}}else Gt=function(){q(Yt,0)};function xt(U,Z){I=q(function(){U(i.unstable_now())},Z)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(U){U.callback=null},i.unstable_forceFrameRate=function(U){0>U||125At?(U.sortIndex=at,c(m,U),f(g)===null&&U===f(m)&&(Q?(F(I),I=-1):Q=!0,xt(et,at-At))):(U.sortIndex=_t,c(g,U),Y||B||(Y=!0,ft||(ft=!0,Gt()))),U},i.unstable_shouldYield=Ut,i.unstable_wrapCallback=function(U){var Z=N;return function(){var at=N;N=Z;try{return U.apply(this,arguments)}finally{N=at}}}})(ur)),ur}var km;function Lp(){return km||(km=1,nr.exports=Qp()),nr.exports}var ir={exports:{}},ie={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Wm;function Yp(){if(Wm)return ie;Wm=1;var i=Cr();function c(g){var m="https://react.dev/errors/"+g;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(c){console.error(c)}}return i(),ir.exports=Yp(),ir.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Im;function wp(){if(Im)return Gu;Im=1;var i=Lp(),c=Cr(),f=Gp();function s(t){var e="https://react.dev/errors/"+t;if(1_t||(t.current=At[_t],At[_t]=null,_t--)}function V(t,e){_t++,At[_t]=t.current,t.current=e}var $=b(null),ut=b(null),rt=b(null),Et=b(null);function se(t,e){switch(V(rt,e),V(ut,t),V($,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?dm(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=dm(e),t=mm(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}j($),V($,t)}function wt(){j($),j(ut),j(rt)}function Jn(t){t.memoizedState!==null&&V(Et,t);var e=$.current,l=mm(e,t.type);e!==l&&(V(ut,t),V($,l))}function ei(t){ut.current===t&&(j($),j(ut)),Et.current===t&&(j(Et),qu._currentValue=at)}var jc,wr;function ma(t){if(jc===void 0)try{throw Error()}catch(l){var e=l.stack.trim().match(/\n( *(at )?)/);jc=e&&e[1]||"",wr=-1)":-1n||y[a]!==z[n]){var D=` -`+y[a].replace(" at new "," at ");return t.displayName&&D.includes("")&&(D=D.replace("",t.displayName)),D}while(1<=a&&0<=n);break}}}finally{qc=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?ma(l):""}function rv(t,e){switch(t.tag){case 26:case 27:case 5:return ma(t.type);case 16:return ma("Lazy");case 13:return t.child!==e&&e!==null?ma("Suspense Fallback"):ma("Suspense");case 19:return ma("SuspenseList");case 0:case 15:return Bc(t.type,!1);case 11:return Bc(t.type.render,!1);case 1:return Bc(t.type,!0);case 31:return ma("Activity");default:return""}}function Xr(t){try{var e="",l=null;do e+=rv(t,l),l=t,t=t.return;while(t);return e}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}var Qc=Object.prototype.hasOwnProperty,Lc=i.unstable_scheduleCallback,Yc=i.unstable_cancelCallback,ov=i.unstable_shouldYield,hv=i.unstable_requestPaint,Te=i.unstable_now,dv=i.unstable_getCurrentPriorityLevel,Zr=i.unstable_ImmediatePriority,Kr=i.unstable_UserBlockingPriority,li=i.unstable_NormalPriority,mv=i.unstable_LowPriority,Vr=i.unstable_IdlePriority,yv=i.log,vv=i.unstable_setDisableYieldValue,Fn=null,Re=null;function xl(t){if(typeof yv=="function"&&vv(t),Re&&typeof Re.setStrictMode=="function")try{Re.setStrictMode(Fn,t)}catch{}}var Oe=Math.clz32?Math.clz32:Sv,pv=Math.log,gv=Math.LN2;function Sv(t){return t>>>=0,t===0?32:31-(pv(t)/gv|0)|0}var ai=256,ni=262144,ui=4194304;function ya(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function ii(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,u=t.suspendedLanes,r=t.pingedLanes;t=t.warmLanes;var h=a&134217727;return h!==0?(a=h&~u,a!==0?n=ya(a):(r&=h,r!==0?n=ya(r):l||(l=h&~t,l!==0&&(n=ya(l))))):(h=a&~u,h!==0?n=ya(h):r!==0?n=ya(r):l||(l=a&~t,l!==0&&(n=ya(l)))),n===0?0:e!==0&&e!==n&&(e&u)===0&&(u=n&-n,l=e&-e,u>=l||u===32&&(l&4194048)!==0)?e:n}function $n(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function bv(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Jr(){var t=ui;return ui<<=1,(ui&62914560)===0&&(ui=4194304),t}function Gc(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function kn(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Ev(t,e,l,a,n,u){var r=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var h=t.entanglements,y=t.expirationTimes,z=t.hiddenUpdates;for(l=r&~l;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var Cv=/[\n"\\]/g;function je(t){return t.replace(Cv,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function Jc(t,e,l,a,n,u,r,h){t.name="",r!=null&&typeof r!="function"&&typeof r!="symbol"&&typeof r!="boolean"?t.type=r:t.removeAttribute("type"),e!=null?r==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+He(e)):t.value!==""+He(e)&&(t.value=""+He(e)):r!=="submit"&&r!=="reset"||t.removeAttribute("value"),e!=null?Fc(t,r,He(e)):l!=null?Fc(t,r,He(l)):a!=null&&t.removeAttribute("value"),n==null&&u!=null&&(t.defaultChecked=!!u),n!=null&&(t.checked=n&&typeof n!="function"&&typeof n!="symbol"),h!=null&&typeof h!="function"&&typeof h!="symbol"&&typeof h!="boolean"?t.name=""+He(h):t.removeAttribute("name")}function io(t,e,l,a,n,u,r,h){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(t.type=u),e!=null||l!=null){if(!(u!=="submit"&&u!=="reset"||e!=null)){Vc(t);return}l=l!=null?""+He(l):"",e=e!=null?""+He(e):l,h||e===t.value||(t.value=e),t.defaultValue=e}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=h?t.checked:!!a,t.defaultChecked=!!a,r!=null&&typeof r!="function"&&typeof r!="symbol"&&typeof r!="boolean"&&(t.name=r),Vc(t)}function Fc(t,e,l){e==="number"&&fi(t.ownerDocument)===t||t.defaultValue===""+l||(t.defaultValue=""+l)}function $a(t,e,l,a){if(t=t.options,e){e={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ic=!1;if(sl)try{var tu={};Object.defineProperty(tu,"passive",{get:function(){Ic=!0}}),window.addEventListener("test",tu,tu),window.removeEventListener("test",tu,tu)}catch{Ic=!1}var Hl=null,ts=null,oi=null;function mo(){if(oi)return oi;var t,e=ts,l=e.length,a,n="value"in Hl?Hl.value:Hl.textContent,u=n.length;for(t=0;t=au),bo=" ",Eo=!1;function To(t,e){switch(t){case"keyup":return e0.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ro(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Ia=!1;function a0(t,e){switch(t){case"compositionend":return Ro(e);case"keypress":return e.which!==32?null:(Eo=!0,bo);case"textInput":return t=e.data,t===bo&&Eo?null:t;default:return null}}function n0(t,e){if(Ia)return t==="compositionend"||!us&&To(t,e)?(t=mo(),oi=ts=Hl=null,Ia=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:l,offset:e-t};t=a}t:{for(;l;){if(l.nextSibling){l=l.nextSibling;break t}l=l.parentNode}l=void 0}l=Uo(l)}}function No(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?No(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function Ho(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=fi(t.document);e instanceof t.HTMLIFrameElement;){try{var l=typeof e.contentWindow.location.href=="string"}catch{l=!1}if(l)t=e.contentWindow;else break;e=fi(t.document)}return e}function ss(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var h0=sl&&"documentMode"in document&&11>=document.documentMode,tn=null,fs=null,cu=null,rs=!1;function jo(t,e,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;rs||tn==null||tn!==fi(a)||(a=tn,"selectionStart"in a&&ss(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),cu&&iu(cu,a)||(cu=a,a=nc(fs,"onSelect"),0>=r,n-=r,We=1<<32-Oe(e)+n|l<ct?(mt=k,k=null):mt=k.sibling;var St=C(R,k,A[ct],x);if(St===null){k===null&&(k=mt);break}t&&k&&St.alternate===null&&e(R,k),S=u(St,S,ct),gt===null?P=St:gt.sibling=St,gt=St,k=mt}if(ct===A.length)return l(R,k),vt&&rl(R,ct),P;if(k===null){for(;ctct?(mt=k,k=null):mt=k.sibling;var la=C(R,k,St.value,x);if(la===null){k===null&&(k=mt);break}t&&k&&la.alternate===null&&e(R,k),S=u(la,S,ct),gt===null?P=la:gt.sibling=la,gt=la,k=mt}if(St.done)return l(R,k),vt&&rl(R,ct),P;if(k===null){for(;!St.done;ct++,St=A.next())St=H(R,St.value,x),St!==null&&(S=u(St,S,ct),gt===null?P=St:gt.sibling=St,gt=St);return vt&&rl(R,ct),P}for(k=a(k);!St.done;ct++,St=A.next())St=_(k,R,ct,St.value,x),St!==null&&(t&&St.alternate!==null&&k.delete(St.key===null?ct:St.key),S=u(St,S,ct),gt===null?P=St:gt.sibling=St,gt=St);return t&&k.forEach(function(xp){return e(R,xp)}),vt&&rl(R,ct),P}function Mt(R,S,A,x){if(typeof A=="object"&&A!==null&&A.type===Q&&A.key===null&&(A=A.props.children),typeof A=="object"&&A!==null){switch(A.$$typeof){case B:t:{for(var P=A.key;S!==null;){if(S.key===P){if(P=A.type,P===Q){if(S.tag===7){l(R,S.sibling),x=n(S,A.props.children),x.return=R,R=x;break t}}else if(S.elementType===P||typeof P=="object"&&P!==null&&P.$$typeof===lt&&za(P)===S.type){l(R,S.sibling),x=n(S,A.props),du(x,A),x.return=R,R=x;break t}l(R,S);break}else e(R,S);S=S.sibling}A.type===Q?(x=Ea(A.props.children,R.mode,x,A.key),x.return=R,R=x):(x=Ei(A.type,A.key,A.props,null,R.mode,x),du(x,A),x.return=R,R=x)}return r(R);case Y:t:{for(P=A.key;S!==null;){if(S.key===P)if(S.tag===4&&S.stateNode.containerInfo===A.containerInfo&&S.stateNode.implementation===A.implementation){l(R,S.sibling),x=n(S,A.children||[]),x.return=R,R=x;break t}else{l(R,S);break}else e(R,S);S=S.sibling}x=ps(A,R.mode,x),x.return=R,R=x}return r(R);case lt:return A=za(A),Mt(R,S,A,x)}if(xt(A))return J(R,S,A,x);if(Gt(A)){if(P=Gt(A),typeof P!="function")throw Error(s(150));return A=P.call(A),tt(R,S,A,x)}if(typeof A.then=="function")return Mt(R,S,Mi(A),x);if(A.$$typeof===X)return Mt(R,S,Oi(R,A),x);_i(R,A)}return typeof A=="string"&&A!==""||typeof A=="number"||typeof A=="bigint"?(A=""+A,S!==null&&S.tag===6?(l(R,S.sibling),x=n(S,A),x.return=R,R=x):(l(R,S),x=vs(A,R.mode,x),x.return=R,R=x),r(R)):l(R,S)}return function(R,S,A,x){try{hu=0;var P=Mt(R,S,A,x);return hn=null,P}catch(k){if(k===on||k===zi)throw k;var gt=ze(29,k,null,R.mode);return gt.lanes=x,gt.return=R,gt}finally{}}}var Ma=nh(!0),uh=nh(!1),Ll=!1;function _s(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ds(t,e){t=t.updateQueue,e.updateQueue===t&&(e.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function Yl(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function Gl(t,e,l){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(bt&2)!==0){var n=a.pending;return n===null?e.next=e:(e.next=n.next,n.next=e),a.pending=e,e=bi(t),wo(t,null,l),e}return Si(t,a,e,l),bi(t)}function mu(t,e,l){if(e=e.updateQueue,e!==null&&(e=e.shared,(l&4194048)!==0)){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,$r(t,l)}}function Us(t,e){var l=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var n=null,u=null;if(l=l.firstBaseUpdate,l!==null){do{var r={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};u===null?n=u=r:u=u.next=r,l=l.next}while(l!==null);u===null?n=u=e:u=u.next=e}else n=u=e;l={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:u,shared:a.shared,callbacks:a.callbacks},t.updateQueue=l;return}t=l.lastBaseUpdate,t===null?l.firstBaseUpdate=e:t.next=e,l.lastBaseUpdate=e}var xs=!1;function yu(){if(xs){var t=rn;if(t!==null)throw t}}function vu(t,e,l,a){xs=!1;var n=t.updateQueue;Ll=!1;var u=n.firstBaseUpdate,r=n.lastBaseUpdate,h=n.shared.pending;if(h!==null){n.shared.pending=null;var y=h,z=y.next;y.next=null,r===null?u=z:r.next=z,r=y;var D=t.alternate;D!==null&&(D=D.updateQueue,h=D.lastBaseUpdate,h!==r&&(h===null?D.firstBaseUpdate=z:h.next=z,D.lastBaseUpdate=y))}if(u!==null){var H=n.baseState;r=0,D=z=y=null,h=u;do{var C=h.lane&-536870913,_=C!==h.lane;if(_?(dt&C)===C:(a&C)===C){C!==0&&C===fn&&(xs=!0),D!==null&&(D=D.next={lane:0,tag:h.tag,payload:h.payload,callback:null,next:null});t:{var J=t,tt=h;C=e;var Mt=l;switch(tt.tag){case 1:if(J=tt.payload,typeof J=="function"){H=J.call(Mt,H,C);break t}H=J;break t;case 3:J.flags=J.flags&-65537|128;case 0:if(J=tt.payload,C=typeof J=="function"?J.call(Mt,H,C):J,C==null)break t;H=T({},H,C);break t;case 2:Ll=!0}}C=h.callback,C!==null&&(t.flags|=64,_&&(t.flags|=8192),_=n.callbacks,_===null?n.callbacks=[C]:_.push(C))}else _={lane:C,tag:h.tag,payload:h.payload,callback:h.callback,next:null},D===null?(z=D=_,y=H):D=D.next=_,r|=C;if(h=h.next,h===null){if(h=n.shared.pending,h===null)break;_=h,h=_.next,_.next=null,n.lastBaseUpdate=_,n.shared.pending=null}}while(!0);D===null&&(y=H),n.baseState=y,n.firstBaseUpdate=z,n.lastBaseUpdate=D,u===null&&(n.shared.lanes=0),Vl|=r,t.lanes=r,t.memoizedState=H}}function ih(t,e){if(typeof t!="function")throw Error(s(191,t));t.call(e)}function ch(t,e){var l=t.callbacks;if(l!==null)for(t.callbacks=null,t=0;tu?u:8;var r=U.T,h={};U.T=h,Ws(t,!1,e,l);try{var y=n(),z=U.S;if(z!==null&&z(h,y),y!==null&&typeof y=="object"&&typeof y.then=="function"){var D=E0(y,a);Su(t,e,D,Ue(t))}else Su(t,e,a,Ue(t))}catch(H){Su(t,e,{then:function(){},status:"rejected",reason:H},Ue())}finally{Z.p=u,r!==null&&h.types!==null&&(r.types=h.types),U.T=r}}function C0(){}function $s(t,e,l,a){if(t.tag!==5)throw Error(s(476));var n=Lh(t).queue;Qh(t,n,e,at,l===null?C0:function(){return Yh(t),l(a)})}function Lh(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:at,baseState:at,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ml,lastRenderedState:at},next:null};var l={};return e.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ml,lastRenderedState:l},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function Yh(t){var e=Lh(t);e.next===null&&(e=t.alternate.memoizedState),Su(t,e.next.queue,{},Ue())}function ks(){return ae(qu)}function Gh(){return Zt().memoizedState}function wh(){return Zt().memoizedState}function M0(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var l=Ue();t=Yl(l);var a=Gl(e,t,l);a!==null&&(ge(a,e,l),mu(a,e,l)),e={cache:As()},t.payload=e;return}e=e.return}}function _0(t,e,l){var a=Ue();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},Li(t)?Zh(e,l):(l=ms(t,e,l,a),l!==null&&(ge(l,t,a),Kh(l,e,a)))}function Xh(t,e,l){var a=Ue();Su(t,e,l,a)}function Su(t,e,l,a){var n={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(Li(t))Zh(e,n);else{var u=t.alternate;if(t.lanes===0&&(u===null||u.lanes===0)&&(u=e.lastRenderedReducer,u!==null))try{var r=e.lastRenderedState,h=u(r,l);if(n.hasEagerState=!0,n.eagerState=h,Ae(h,r))return Si(t,e,n,0),Dt===null&&gi(),!1}catch{}finally{}if(l=ms(t,e,n,a),l!==null)return ge(l,t,a),Kh(l,e,a),!0}return!1}function Ws(t,e,l,a){if(a={lane:2,revertLane:Df(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},Li(t)){if(e)throw Error(s(479))}else e=ms(t,l,a,2),e!==null&&ge(e,t,2)}function Li(t){var e=t.alternate;return t===it||e!==null&&e===it}function Zh(t,e){mn=xi=!0;var l=t.pending;l===null?e.next=e:(e.next=l.next,l.next=e),t.pending=e}function Kh(t,e,l){if((l&4194048)!==0){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,$r(t,l)}}var bu={readContext:ae,use:ji,useCallback:Bt,useContext:Bt,useEffect:Bt,useImperativeHandle:Bt,useLayoutEffect:Bt,useInsertionEffect:Bt,useMemo:Bt,useReducer:Bt,useRef:Bt,useState:Bt,useDebugValue:Bt,useDeferredValue:Bt,useTransition:Bt,useSyncExternalStore:Bt,useId:Bt,useHostTransitionStatus:Bt,useFormState:Bt,useActionState:Bt,useOptimistic:Bt,useMemoCache:Bt,useCacheRefresh:Bt};bu.useEffectEvent=Bt;var Vh={readContext:ae,use:ji,useCallback:function(t,e){return fe().memoizedState=[t,e===void 0?null:e],t},useContext:ae,useEffect:_h,useImperativeHandle:function(t,e,l){l=l!=null?l.concat([t]):null,Bi(4194308,4,Nh.bind(null,e,t),l)},useLayoutEffect:function(t,e){return Bi(4194308,4,t,e)},useInsertionEffect:function(t,e){Bi(4,2,t,e)},useMemo:function(t,e){var l=fe();e=e===void 0?null:e;var a=t();if(_a){xl(!0);try{t()}finally{xl(!1)}}return l.memoizedState=[a,e],a},useReducer:function(t,e,l){var a=fe();if(l!==void 0){var n=l(e);if(_a){xl(!0);try{l(e)}finally{xl(!1)}}}else n=e;return a.memoizedState=a.baseState=n,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:n},a.queue=t,t=t.dispatch=_0.bind(null,it,t),[a.memoizedState,t]},useRef:function(t){var e=fe();return t={current:t},e.memoizedState=t},useState:function(t){t=Zs(t);var e=t.queue,l=Xh.bind(null,it,e);return e.dispatch=l,[t.memoizedState,l]},useDebugValue:Js,useDeferredValue:function(t,e){var l=fe();return Fs(l,t,e)},useTransition:function(){var t=Zs(!1);return t=Qh.bind(null,it,t.queue,!0,!1),fe().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,l){var a=it,n=fe();if(vt){if(l===void 0)throw Error(s(407));l=l()}else{if(l=e(),Dt===null)throw Error(s(349));(dt&127)!==0||dh(a,e,l)}n.memoizedState=l;var u={value:l,getSnapshot:e};return n.queue=u,_h(yh.bind(null,a,u,t),[t]),a.flags|=2048,vn(9,{destroy:void 0},mh.bind(null,a,u,l,e),null),l},useId:function(){var t=fe(),e=Dt.identifierPrefix;if(vt){var l=Pe,a=We;l=(a&~(1<<32-Oe(a)-1)).toString(32)+l,e="_"+e+"R_"+l,l=Ni++,0<\/script>",u=u.removeChild(u.firstChild);break;case"select":u=typeof a.is=="string"?r.createElement("select",{is:a.is}):r.createElement("select"),a.multiple?u.multiple=!0:a.size&&(u.size=a.size);break;default:u=typeof a.is=="string"?r.createElement(n,{is:a.is}):r.createElement(n)}}u[ee]=e,u[he]=a;t:for(r=e.child;r!==null;){if(r.tag===5||r.tag===6)u.appendChild(r.stateNode);else if(r.tag!==4&&r.tag!==27&&r.child!==null){r.child.return=r,r=r.child;continue}if(r===e)break t;for(;r.sibling===null;){if(r.return===null||r.return===e)break t;r=r.return}r.sibling.return=r.return,r=r.sibling}e.stateNode=u;t:switch(ue(u,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&vl(e)}}return Ht(e),hf(e,e.type,t===null?null:t.memoizedProps,e.pendingProps,l),null;case 6:if(t&&e.stateNode!=null)t.memoizedProps!==a&&vl(e);else{if(typeof a!="string"&&e.stateNode===null)throw Error(s(166));if(t=rt.current,cn(e)){if(t=e.stateNode,l=e.memoizedProps,a=null,n=le,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}t[ee]=e,t=!!(t.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||om(t.nodeValue,l)),t||Bl(e,!0)}else t=uc(t).createTextNode(a),t[ee]=e,e.stateNode=t}return Ht(e),null;case 31:if(l=e.memoizedState,t===null||t.memoizedState!==null){if(a=cn(e),l!==null){if(t===null){if(!a)throw Error(s(318));if(t=e.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(s(557));t[ee]=e}else Ta(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Ht(e),t=!1}else l=Es(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=l),t=!0;if(!t)return e.flags&256?(Me(e),e):(Me(e),null);if((e.flags&128)!==0)throw Error(s(558))}return Ht(e),null;case 13:if(a=e.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(n=cn(e),a!==null&&a.dehydrated!==null){if(t===null){if(!n)throw Error(s(318));if(n=e.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(s(317));n[ee]=e}else Ta(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Ht(e),n=!1}else n=Es(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=n),n=!0;if(!n)return e.flags&256?(Me(e),e):(Me(e),null)}return Me(e),(e.flags&128)!==0?(e.lanes=l,e):(l=a!==null,t=t!==null&&t.memoizedState!==null,l&&(a=e.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),u=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(u=a.memoizedState.cachePool.pool),u!==n&&(a.flags|=2048)),l!==t&&l&&(e.child.flags|=8192),Zi(e,e.updateQueue),Ht(e),null);case 4:return wt(),t===null&&Hf(e.stateNode.containerInfo),Ht(e),null;case 10:return hl(e.type),Ht(e),null;case 19:if(j(Xt),a=e.memoizedState,a===null)return Ht(e),null;if(n=(e.flags&128)!==0,u=a.rendering,u===null)if(n)Tu(a,!1);else{if(Qt!==0||t!==null&&(t.flags&128)!==0)for(t=e.child;t!==null;){if(u=Ui(t),u!==null){for(e.flags|=128,Tu(a,!1),t=u.updateQueue,e.updateQueue=t,Zi(e,t),e.subtreeFlags=0,t=l,l=e.child;l!==null;)Xo(l,t),l=l.sibling;return V(Xt,Xt.current&1|2),vt&&rl(e,a.treeForkCount),e.child}t=t.sibling}a.tail!==null&&Te()>$i&&(e.flags|=128,n=!0,Tu(a,!1),e.lanes=4194304)}else{if(!n)if(t=Ui(u),t!==null){if(e.flags|=128,n=!0,t=t.updateQueue,e.updateQueue=t,Zi(e,t),Tu(a,!0),a.tail===null&&a.tailMode==="hidden"&&!u.alternate&&!vt)return Ht(e),null}else 2*Te()-a.renderingStartTime>$i&&l!==536870912&&(e.flags|=128,n=!0,Tu(a,!1),e.lanes=4194304);a.isBackwards?(u.sibling=e.child,e.child=u):(t=a.last,t!==null?t.sibling=u:e.child=u,a.last=u)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=Te(),t.sibling=null,l=Xt.current,V(Xt,n?l&1|2:l&1),vt&&rl(e,a.treeForkCount),t):(Ht(e),null);case 22:case 23:return Me(e),Hs(),a=e.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(e.flags|=8192):a&&(e.flags|=8192),a?(l&536870912)!==0&&(e.flags&128)===0&&(Ht(e),e.subtreeFlags&6&&(e.flags|=8192)):Ht(e),l=e.updateQueue,l!==null&&Zi(e,l.retryQueue),l=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(l=t.memoizedState.cachePool.pool),a=null,e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(a=e.memoizedState.cachePool.pool),a!==l&&(e.flags|=2048),t!==null&&j(Aa),null;case 24:return l=null,t!==null&&(l=t.memoizedState.cache),e.memoizedState.cache!==l&&(e.flags|=2048),hl(Kt),Ht(e),null;case 25:return null;case 30:return null}throw Error(s(156,e.tag))}function H0(t,e){switch(Ss(e),e.tag){case 1:return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 3:return hl(Kt),wt(),t=e.flags,(t&65536)!==0&&(t&128)===0?(e.flags=t&-65537|128,e):null;case 26:case 27:case 5:return ei(e),null;case 31:if(e.memoizedState!==null){if(Me(e),e.alternate===null)throw Error(s(340));Ta()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 13:if(Me(e),t=e.memoizedState,t!==null&&t.dehydrated!==null){if(e.alternate===null)throw Error(s(340));Ta()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 19:return j(Xt),null;case 4:return wt(),null;case 10:return hl(e.type),null;case 22:case 23:return Me(e),Hs(),t!==null&&j(Aa),t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 24:return hl(Kt),null;case 25:return null;default:return null}}function vd(t,e){switch(Ss(e),e.tag){case 3:hl(Kt),wt();break;case 26:case 27:case 5:ei(e);break;case 4:wt();break;case 31:e.memoizedState!==null&&Me(e);break;case 13:Me(e);break;case 19:j(Xt);break;case 10:hl(e.type);break;case 22:case 23:Me(e),Hs(),t!==null&&j(Aa);break;case 24:hl(Kt)}}function Ru(t,e){try{var l=e.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var n=a.next;l=n;do{if((l.tag&t)===t){a=void 0;var u=l.create,r=l.inst;a=u(),r.destroy=a}l=l.next}while(l!==n)}}catch(h){Ot(e,e.return,h)}}function Zl(t,e,l){try{var a=e.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var u=n.next;a=u;do{if((a.tag&t)===t){var r=a.inst,h=r.destroy;if(h!==void 0){r.destroy=void 0,n=e;var y=l,z=h;try{z()}catch(D){Ot(n,y,D)}}}a=a.next}while(a!==u)}}catch(D){Ot(e,e.return,D)}}function pd(t){var e=t.updateQueue;if(e!==null){var l=t.stateNode;try{ch(e,l)}catch(a){Ot(t,t.return,a)}}}function gd(t,e,l){l.props=Da(t.type,t.memoizedProps),l.state=t.memoizedState;try{l.componentWillUnmount()}catch(a){Ot(t,e,a)}}function Ou(t,e){try{var l=t.ref;if(l!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof l=="function"?t.refCleanup=l(a):l.current=a}}catch(n){Ot(t,e,n)}}function Ie(t,e){var l=t.ref,a=t.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(n){Ot(t,e,n)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(n){Ot(t,e,n)}else l.current=null}function Sd(t){var e=t.type,l=t.memoizedProps,a=t.stateNode;try{t:switch(e){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break t;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(n){Ot(t,t.return,n)}}function df(t,e,l){try{var a=t.stateNode;lp(a,t.type,l,e),a[he]=e}catch(n){Ot(t,t.return,n)}}function bd(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Wl(t.type)||t.tag===4}function mf(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||bd(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Wl(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function yf(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(t,e):(e=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,e.appendChild(t),l=l._reactRootContainer,l!=null||e.onclick!==null||(e.onclick=cl));else if(a!==4&&(a===27&&Wl(t.type)&&(l=t.stateNode,e=null),t=t.child,t!==null))for(yf(t,e,l),t=t.sibling;t!==null;)yf(t,e,l),t=t.sibling}function Ki(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?l.insertBefore(t,e):l.appendChild(t);else if(a!==4&&(a===27&&Wl(t.type)&&(l=t.stateNode),t=t.child,t!==null))for(Ki(t,e,l),t=t.sibling;t!==null;)Ki(t,e,l),t=t.sibling}function Ed(t){var e=t.stateNode,l=t.memoizedProps;try{for(var a=t.type,n=e.attributes;n.length;)e.removeAttributeNode(n[0]);ue(e,a,l),e[ee]=t,e[he]=l}catch(u){Ot(t,t.return,u)}}var pl=!1,Ft=!1,vf=!1,Td=typeof WeakSet=="function"?WeakSet:Set,Pt=null;function j0(t,e){if(t=t.containerInfo,Bf=hc,t=Ho(t),ss(t)){if("selectionStart"in t)var l={start:t.selectionStart,end:t.selectionEnd};else t:{l=(l=t.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var n=a.anchorOffset,u=a.focusNode;a=a.focusOffset;try{l.nodeType,u.nodeType}catch{l=null;break t}var r=0,h=-1,y=-1,z=0,D=0,H=t,C=null;e:for(;;){for(var _;H!==l||n!==0&&H.nodeType!==3||(h=r+n),H!==u||a!==0&&H.nodeType!==3||(y=r+a),H.nodeType===3&&(r+=H.nodeValue.length),(_=H.firstChild)!==null;)C=H,H=_;for(;;){if(H===t)break e;if(C===l&&++z===n&&(h=r),C===u&&++D===a&&(y=r),(_=H.nextSibling)!==null)break;H=C,C=H.parentNode}H=_}l=h===-1||y===-1?null:{start:h,end:y}}else l=null}l=l||{start:0,end:0}}else l=null;for(Qf={focusedElem:t,selectionRange:l},hc=!1,Pt=e;Pt!==null;)if(e=Pt,t=e.child,(e.subtreeFlags&1028)!==0&&t!==null)t.return=e,Pt=t;else for(;Pt!==null;){switch(e=Pt,u=e.alternate,t=e.flags,e.tag){case 0:if((t&4)!==0&&(t=e.updateQueue,t=t!==null?t.events:null,t!==null))for(l=0;l title"))),ue(u,a,l),u[ee]=t,Wt(u),a=u;break t;case"link":var r=Mm("link","href",n).get(a+(l.href||""));if(r){for(var h=0;hMt&&(r=Mt,Mt=tt,tt=r);var R=xo(h,tt),S=xo(h,Mt);if(R&&S&&(_.rangeCount!==1||_.anchorNode!==R.node||_.anchorOffset!==R.offset||_.focusNode!==S.node||_.focusOffset!==S.offset)){var A=H.createRange();A.setStart(R.node,R.offset),_.removeAllRanges(),tt>Mt?(_.addRange(A),_.extend(S.node,S.offset)):(A.setEnd(S.node,S.offset),_.addRange(A))}}}}for(H=[],_=h;_=_.parentNode;)_.nodeType===1&&H.push({element:_,left:_.scrollLeft,top:_.scrollTop});for(typeof h.focus=="function"&&h.focus(),h=0;hl?32:l,U.T=null,l=Rf,Rf=null;var u=Fl,r=Tl;if($t=0,En=Fl=null,Tl=0,(bt&6)!==0)throw Error(s(331));var h=bt;if(bt|=4,Nd(u.current),Dd(u,u.current,r,l),bt=h,Du(0,!1),Re&&typeof Re.onPostCommitFiberRoot=="function")try{Re.onPostCommitFiberRoot(Fn,u)}catch{}return!0}finally{Z.p=n,U.T=a,Wd(t,e)}}function Id(t,e,l){e=Be(l,e),e=ef(t.stateNode,e,2),t=Gl(t,e,2),t!==null&&(kn(t,2),tl(t))}function Ot(t,e,l){if(t.tag===3)Id(t,t,l);else for(;e!==null;){if(e.tag===3){Id(e,t,l);break}else if(e.tag===1){var a=e.stateNode;if(typeof e.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(Jl===null||!Jl.has(a))){t=Be(l,t),l=td(2),a=Gl(e,l,2),a!==null&&(ed(l,a,e,t),kn(a,2),tl(a));break}}e=e.return}}function Cf(t,e,l){var a=t.pingCache;if(a===null){a=t.pingCache=new Q0;var n=new Set;a.set(e,n)}else n=a.get(e),n===void 0&&(n=new Set,a.set(e,n));n.has(l)||(Sf=!0,n.add(l),t=X0.bind(null,t,e,l),e.then(t,t))}function X0(t,e,l){var a=t.pingCache;a!==null&&a.delete(e),t.pingedLanes|=t.suspendedLanes&l,t.warmLanes&=~l,Dt===t&&(dt&l)===l&&(Qt===4||Qt===3&&(dt&62914560)===dt&&300>Te()-Fi?(bt&2)===0&&Tn(t,0):bf|=l,bn===dt&&(bn=0)),tl(t)}function tm(t,e){e===0&&(e=Jr()),t=ba(t,e),t!==null&&(kn(t,e),tl(t))}function Z0(t){var e=t.memoizedState,l=0;e!==null&&(l=e.retryLane),tm(t,l)}function K0(t,e){var l=0;switch(t.tag){case 31:case 13:var a=t.stateNode,n=t.memoizedState;n!==null&&(l=n.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(s(314))}a!==null&&a.delete(e),tm(t,l)}function V0(t,e){return Lc(t,e)}var ec=null,On=null,Mf=!1,lc=!1,_f=!1,kl=0;function tl(t){t!==On&&t.next===null&&(On===null?ec=On=t:On=On.next=t),lc=!0,Mf||(Mf=!0,F0())}function Du(t,e){if(!_f&&lc){_f=!0;do for(var l=!1,a=ec;a!==null;){if(t!==0){var n=a.pendingLanes;if(n===0)var u=0;else{var r=a.suspendedLanes,h=a.pingedLanes;u=(1<<31-Oe(42|t)+1)-1,u&=n&~(r&~h),u=u&201326741?u&201326741|1:u?u|2:0}u!==0&&(l=!0,nm(a,u))}else u=dt,u=ii(a,a===Dt?u:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(u&3)===0||$n(a,u)||(l=!0,nm(a,u));a=a.next}while(l);_f=!1}}function J0(){em()}function em(){lc=Mf=!1;var t=0;kl!==0&&np()&&(t=kl);for(var e=Te(),l=null,a=ec;a!==null;){var n=a.next,u=lm(a,e);u===0?(a.next=null,l===null?ec=n:l.next=n,n===null&&(On=l)):(l=a,(t!==0||(u&3)!==0)&&(lc=!0)),a=n}$t!==0&&$t!==5||Du(t),kl!==0&&(kl=0)}function lm(t,e){for(var l=t.suspendedLanes,a=t.pingedLanes,n=t.expirationTimes,u=t.pendingLanes&-62914561;0h)break;var D=y.transferSize,H=y.initiatorType;D&&hm(H)&&(y=y.responseEnd,r+=D*(y"u"?null:document;function Om(t,e,l){var a=An;if(a&&typeof e=="string"&&e){var n=je(e);n='link[rel="'+t+'"][href="'+n+'"]',typeof l=="string"&&(n+='[crossorigin="'+l+'"]'),Rm.has(n)||(Rm.add(n),t={rel:t,crossOrigin:l,href:e},a.querySelector(n)===null&&(e=a.createElement("link"),ue(e,"link",t),Wt(e),a.head.appendChild(e)))}}function dp(t){Rl.D(t),Om("dns-prefetch",t,null)}function mp(t,e){Rl.C(t,e),Om("preconnect",t,e)}function yp(t,e,l){Rl.L(t,e,l);var a=An;if(a&&t&&e){var n='link[rel="preload"][as="'+je(e)+'"]';e==="image"&&l&&l.imageSrcSet?(n+='[imagesrcset="'+je(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(n+='[imagesizes="'+je(l.imageSizes)+'"]')):n+='[href="'+je(t)+'"]';var u=n;switch(e){case"style":u=zn(t);break;case"script":u=Cn(t)}Xe.has(u)||(t=T({rel:"preload",href:e==="image"&&l&&l.imageSrcSet?void 0:t,as:e},l),Xe.set(u,t),a.querySelector(n)!==null||e==="style"&&a.querySelector(Hu(u))||e==="script"&&a.querySelector(ju(u))||(e=a.createElement("link"),ue(e,"link",t),Wt(e),a.head.appendChild(e)))}}function vp(t,e){Rl.m(t,e);var l=An;if(l&&t){var a=e&&typeof e.as=="string"?e.as:"script",n='link[rel="modulepreload"][as="'+je(a)+'"][href="'+je(t)+'"]',u=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":u=Cn(t)}if(!Xe.has(u)&&(t=T({rel:"modulepreload",href:t},e),Xe.set(u,t),l.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(ju(u)))return}a=l.createElement("link"),ue(a,"link",t),Wt(a),l.head.appendChild(a)}}}function pp(t,e,l){Rl.S(t,e,l);var a=An;if(a&&t){var n=Ja(a).hoistableStyles,u=zn(t);e=e||"default";var r=n.get(u);if(!r){var h={loading:0,preload:null};if(r=a.querySelector(Hu(u)))h.loading=5;else{t=T({rel:"stylesheet",href:t,"data-precedence":e},l),(l=Xe.get(u))&&Kf(t,l);var y=r=a.createElement("link");Wt(y),ue(y,"link",t),y._p=new Promise(function(z,D){y.onload=z,y.onerror=D}),y.addEventListener("load",function(){h.loading|=1}),y.addEventListener("error",function(){h.loading|=2}),h.loading|=4,cc(r,e,a)}r={type:"stylesheet",instance:r,count:1,state:h},n.set(u,r)}}}function gp(t,e){Rl.X(t,e);var l=An;if(l&&t){var a=Ja(l).hoistableScripts,n=Cn(t),u=a.get(n);u||(u=l.querySelector(ju(n)),u||(t=T({src:t,async:!0},e),(e=Xe.get(n))&&Vf(t,e),u=l.createElement("script"),Wt(u),ue(u,"link",t),l.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function Sp(t,e){Rl.M(t,e);var l=An;if(l&&t){var a=Ja(l).hoistableScripts,n=Cn(t),u=a.get(n);u||(u=l.querySelector(ju(n)),u||(t=T({src:t,async:!0,type:"module"},e),(e=Xe.get(n))&&Vf(t,e),u=l.createElement("script"),Wt(u),ue(u,"link",t),l.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function Am(t,e,l,a){var n=(n=rt.current)?ic(n):null;if(!n)throw Error(s(446));switch(t){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(e=zn(l.href),l=Ja(n).hoistableStyles,a=l.get(e),a||(a={type:"style",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){t=zn(l.href);var u=Ja(n).hoistableStyles,r=u.get(t);if(r||(n=n.ownerDocument||n,r={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},u.set(t,r),(u=n.querySelector(Hu(t)))&&!u._p&&(r.instance=u,r.state.loading=5),Xe.has(t)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},Xe.set(t,l),u||bp(n,t,l,r.state))),e&&a===null)throw Error(s(528,""));return r}if(e&&a!==null)throw Error(s(529,""));return null;case"script":return e=l.async,l=l.src,typeof l=="string"&&e&&typeof e!="function"&&typeof e!="symbol"?(e=Cn(l),l=Ja(n).hoistableScripts,a=l.get(e),a||(a={type:"script",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(s(444,t))}}function zn(t){return'href="'+je(t)+'"'}function Hu(t){return'link[rel="stylesheet"]['+t+"]"}function zm(t){return T({},t,{"data-precedence":t.precedence,precedence:null})}function bp(t,e,l,a){t.querySelector('link[rel="preload"][as="style"]['+e+"]")?a.loading=1:(e=t.createElement("link"),a.preload=e,e.addEventListener("load",function(){return a.loading|=1}),e.addEventListener("error",function(){return a.loading|=2}),ue(e,"link",l),Wt(e),t.head.appendChild(e))}function Cn(t){return'[src="'+je(t)+'"]'}function ju(t){return"script[async]"+t}function Cm(t,e,l){if(e.count++,e.instance===null)switch(e.type){case"style":var a=t.querySelector('style[data-href~="'+je(l.href)+'"]');if(a)return e.instance=a,Wt(a),a;var n=T({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),Wt(a),ue(a,"style",n),cc(a,l.precedence,t),e.instance=a;case"stylesheet":n=zn(l.href);var u=t.querySelector(Hu(n));if(u)return e.state.loading|=4,e.instance=u,Wt(u),u;a=zm(l),(n=Xe.get(n))&&Kf(a,n),u=(t.ownerDocument||t).createElement("link"),Wt(u);var r=u;return r._p=new Promise(function(h,y){r.onload=h,r.onerror=y}),ue(u,"link",a),e.state.loading|=4,cc(u,l.precedence,t),e.instance=u;case"script":return u=Cn(l.src),(n=t.querySelector(ju(u)))?(e.instance=n,Wt(n),n):(a=l,(n=Xe.get(u))&&(a=T({},l),Vf(a,n)),t=t.ownerDocument||t,n=t.createElement("script"),Wt(n),ue(n,"link",a),t.head.appendChild(n),e.instance=n);case"void":return null;default:throw Error(s(443,e.type))}else e.type==="stylesheet"&&(e.state.loading&4)===0&&(a=e.instance,e.state.loading|=4,cc(a,l.precedence,t));return e.instance}function cc(t,e,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,u=n,r=0;r title"):null)}function Ep(t,e,l){if(l===1||e.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof e.precedence!="string"||typeof e.href!="string"||e.href==="")break;return!0;case"link":if(typeof e.rel!="string"||typeof e.href!="string"||e.href===""||e.onLoad||e.onError)break;switch(e.rel){case"stylesheet":return t=e.disabled,typeof e.precedence=="string"&&t==null;default:return!0}case"script":if(e.async&&typeof e.async!="function"&&typeof e.async!="symbol"&&!e.onLoad&&!e.onError&&e.src&&typeof e.src=="string")return!0}return!1}function Dm(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function Tp(t,e,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var n=zn(a.href),u=e.querySelector(Hu(n));if(u){e=u._p,e!==null&&typeof e=="object"&&typeof e.then=="function"&&(t.count++,t=fc.bind(t),e.then(t,t)),l.state.loading|=4,l.instance=u,Wt(u);return}u=e.ownerDocument||e,a=zm(a),(n=Xe.get(n))&&Kf(a,n),u=u.createElement("link"),Wt(u);var r=u;r._p=new Promise(function(h,y){r.onload=h,r.onerror=y}),ue(u,"link",a),l.instance=u}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(l,e),(e=l.state.preload)&&(l.state.loading&3)===0&&(t.count++,l=fc.bind(t),e.addEventListener("load",l),e.addEventListener("error",l))}}var Jf=0;function Rp(t,e){return t.stylesheets&&t.count===0&&oc(t,t.stylesheets),0Jf?50:800)+e);return t.unsuspend=l,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function fc(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)oc(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var rc=null;function oc(t,e){t.stylesheets=null,t.unsuspend!==null&&(t.count++,rc=new Map,e.forEach(Op,t),rc=null,fc.call(t))}function Op(t,e){if(!(e.state.loading&4)){var l=rc.get(t);if(l)var a=l.get(null);else{l=new Map,rc.set(t,l);for(var n=t.querySelectorAll("link[data-precedence],style[data-precedence]"),u=0;u"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(c){console.error(c)}}return i(),ar.exports=wp(),ar.exports}var Zp=Xp();/** - * react-router v7.18.1 - * - * Copyright (c) Remix Software Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE.md file in the root directory of this source tree. - * - * @license MIT - */var Mr=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,_y=/^[\\/]{2}/;function Kp(i,c){return c+i.replace(/\\/g,"/")}var ey="popstate";function ly(i){return typeof i=="object"&&i!=null&&"pathname"in i&&"search"in i&&"hash"in i&&"state"in i&&"key"in i}function Vp(i={}){function c(s,o){var m;let d=(m=o.state)==null?void 0:m.masked,{pathname:p,search:E,hash:g}=d||s.location;return rr("",{pathname:p,search:E,hash:g},o.state&&o.state.usr||null,o.state&&o.state.key||"default",d?{pathname:s.location.pathname,search:s.location.search,hash:s.location.hash}:void 0)}function f(s,o){return typeof o=="string"?o:Zu(o)}return Fp(c,f,null,i)}function qt(i,c){if(i===!1||i===null||typeof i>"u")throw new Error(c)}function nl(i,c){if(!i){typeof console<"u"&&console.warn(c);try{throw new Error(c)}catch{}}}function Jp(){return Math.random().toString(36).substring(2,10)}function ay(i,c){return{usr:i.state,key:i.key,idx:c,masked:i.mask?{pathname:i.pathname,search:i.search,hash:i.hash}:void 0}}function rr(i,c,f=null,s,o){return{pathname:typeof i=="string"?i:i.pathname,search:"",hash:"",...typeof c=="string"?Xn(c):c,state:f,key:c&&c.key||s||Jp(),mask:o}}function Zu({pathname:i="/",search:c="",hash:f=""}){return c&&c!=="?"&&(i+=c.charAt(0)==="?"?c:"?"+c),f&&f!=="#"&&(i+=f.charAt(0)==="#"?f:"#"+f),i}function Xn(i){let c={};if(i){let f=i.indexOf("#");f>=0&&(c.hash=i.substring(f),i=i.substring(0,f));let s=i.indexOf("?");s>=0&&(c.search=i.substring(s),i=i.substring(0,s)),i&&(c.pathname=i)}return c}function Fp(i,c,f,s={}){let{window:o=document.defaultView,v5Compat:d=!1}=s,p=o.history,E="POP",g=null,m=O();m==null&&(m=0,p.replaceState({...p.state,idx:m},""));function O(){return(p.state||{idx:null}).idx}function T(){E="POP";let L=O(),q=L==null?null:L-m;m=L,g&&g({action:E,location:Q.location,delta:q})}function N(L,q){E="PUSH";let F=ly(L)?L:rr(Q.location,L,q);m=O()+1;let X=ay(F,m),G=Q.createHref(F.mask||F);try{p.pushState(X,"",G)}catch(et){if(et instanceof DOMException&&et.name==="DataCloneError")throw et;o.location.assign(G)}d&&g&&g({action:E,location:Q.location,delta:1})}function B(L,q){E="REPLACE";let F=ly(L)?L:rr(Q.location,L,q);m=O();let X=ay(F,m),G=Q.createHref(F.mask||F);p.replaceState(X,"",G),d&&g&&g({action:E,location:Q.location,delta:0})}function Y(L){return $p(o,L)}let Q={get action(){return E},get location(){return i(o,p)},listen(L){if(g)throw new Error("A history only accepts one active listener");return o.addEventListener(ey,T),g=L,()=>{o.removeEventListener(ey,T),g=null}},createHref(L){return c(o,L)},createURL:Y,encodeLocation(L){let q=Y(L);return{pathname:q.pathname,search:q.search,hash:q.hash}},push:N,replace:B,go(L){return p.go(L)}};return Q}function $p(i,c,f=!1){let s="http://localhost";i&&(s=i.location.origin!=="null"?i.location.origin:i.location.href),qt(s,"No window.location.(origin|href) available to create URL");let o=typeof c=="string"?c:Zu(c);return o=o.replace(/ $/,"%20"),!f&&_y.test(o)&&(o=s+o),new URL(o,s)}function Dy(i,c,f="/"){return kp(i,c,f,!1)}function kp(i,c,f,s,o){let d=typeof c=="string"?Xn(c):c,p=Dl(d.pathname||"/",f);if(p==null)return null;let E=Wp(i),g=null,m=sg(p);for(let O=0;g==null&&O{let O={relativePath:m===void 0?p.path||"":m,caseSensitive:p.caseSensitive===!0,childrenIndex:E,route:p};if(O.relativePath.startsWith("/")){if(!O.relativePath.startsWith(s)&&g)return;qt(O.relativePath.startsWith(s),`Absolute route path "${O.relativePath}" nested under path "${s}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),O.relativePath=O.relativePath.slice(s.length)}let T=ke([s,O.relativePath]),N=f.concat(O);p.children&&p.children.length>0&&(qt(p.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${T}".`),Uy(p.children,c,N,T,g)),!(p.path==null&&!p.index)&&c.push({path:T,score:ug(T,p.index),routesMeta:N.map((B,Y)=>{let[Q,L]=Hy(B.relativePath,B.caseSensitive,Y===N.length-1);return{...B,matcher:Q,compiledParams:L}})})};return i.forEach((p,E)=>{var g;if(p.path===""||!((g=p.path)!=null&&g.includes("?")))d(p,E);else for(let m of xy(p.path))d(p,E,!0,m)}),c}function xy(i){let c=i.split("/");if(c.length===0)return[];let[f,...s]=c,o=f.endsWith("?"),d=f.replace(/\?$/,"");if(s.length===0)return o?[d,""]:[d];let p=xy(s.join("/")),E=[];return E.push(...p.map(g=>g===""?d:[d,g].join("/"))),o&&E.push(...p),E.map(g=>i.startsWith("/")&&g===""?"/":g)}function Pp(i){i.sort((c,f)=>c.score!==f.score?f.score-c.score:ig(c.routesMeta.map(s=>s.childrenIndex),f.routesMeta.map(s=>s.childrenIndex)))}var Ip=/^:[\w-]+$/,tg=3,eg=2,lg=1,ag=10,ng=-2,ny=i=>i==="*";function ug(i,c){let f=i.split("/"),s=f.length;return f.some(ny)&&(s+=ng),c&&(s+=eg),f.filter(o=>!ny(o)).reduce((o,d)=>o+(Ip.test(d)?tg:d===""?lg:ag),s)}function ig(i,c){return i.length===c.length&&i.slice(0,-1).every((s,o)=>s===c[o])?i[i.length-1]-c[c.length-1]:0}function cg(i,c,f=!1){let{routesMeta:s}=i,o={},d="/",p=[];for(let E=0;E{if(O==="*"){let Y=E[N]||"";p=d.slice(0,d.length-Y.length).replace(/(.)\/+$/,"$1")}const B=E[N];return T&&!B?m[O]=void 0:m[O]=(B||"").replace(/%2F/g,"/"),m},{}),pathname:d,pathnameBase:p,pattern:i}}function Hy(i,c=!1,f=!0){nl(i==="*"||!i.endsWith("*")||i.endsWith("/*"),`Route path "${i}" will be treated as if it were "${i.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${i.replace(/\*$/,"/*")}".`);let s=[],o="^"+i.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(p,E,g,m,O)=>{if(s.push({paramName:E,isOptional:g!=null}),g){let T=O.charAt(m+p.length);return T&&T!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return i.endsWith("*")?(s.push({paramName:"*"}),o+=i==="*"||i==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):f?o+="\\/*$":i!==""&&i!=="/"&&(o+="(?:(?=\\/|$))"),[new RegExp(o,c?void 0:"i"),s]}function sg(i){try{return i.split("/").map(c=>decodeURIComponent(c).replace(/\//g,"%2F")).join("/")}catch(c){return nl(!1,`The URL path "${i}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${c}).`),i}}function Dl(i,c){if(c==="/")return i;if(!i.toLowerCase().startsWith(c.toLowerCase()))return null;let f=c.endsWith("/")?c.length-1:c.length,s=i.charAt(f);return s&&s!=="/"?null:i.slice(f)||"/"}function fg(i,c="/"){let{pathname:f,search:s="",hash:o=""}=typeof i=="string"?Xn(i):i,d;return f?(f=qy(f),f.startsWith("/")?d=uy(f.substring(1),"/"):d=uy(f,c)):d=c,{pathname:d,search:hg(s),hash:dg(o)}}function uy(i,c){let f=Mc(c).split("/");return i.split("/").forEach(o=>{o===".."?f.length>1&&f.pop():o!=="."&&f.push(o)}),f.length>1?f.join("/"):"/"}function cr(i,c,f,s){return`Cannot include a '${i}' character in a manually specified \`to.${c}\` field [${JSON.stringify(s)}]. Please separate it out to the \`to.${f}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function rg(i){return i.filter((c,f)=>f===0||c.route.path&&c.route.path.length>0)}function jy(i){let c=rg(i);return c.map((f,s)=>s===c.length-1?f.pathname:f.pathnameBase)}function _r(i,c,f,s=!1){let o;typeof i=="string"?o=Xn(i):(o={...i},qt(!o.pathname||!o.pathname.includes("?"),cr("?","pathname","search",o)),qt(!o.pathname||!o.pathname.includes("#"),cr("#","pathname","hash",o)),qt(!o.search||!o.search.includes("#"),cr("#","search","hash",o)));let d=i===""||o.pathname==="",p=d?"/":o.pathname,E;if(p==null)E=f;else{let T=c.length-1;if(!s&&p.startsWith("..")){let N=p.split("/");for(;N[0]==="..";)N.shift(),T-=1;o.pathname=N.join("/")}E=T>=0?c[T]:"/"}let g=fg(o,E),m=p&&p!=="/"&&p.endsWith("/"),O=(d||p===".")&&f.endsWith("/");return!g.pathname.endsWith("/")&&(m||O)&&(g.pathname+="/"),g}var qy=i=>i.replace(/[\\/]{2,}/g,"/"),ke=i=>qy(i.join("/")),Mc=i=>i.replace(/\/+$/,""),og=i=>Mc(i).replace(/^\/*/,"/"),hg=i=>!i||i==="?"?"":i.startsWith("?")?i:"?"+i,dg=i=>!i||i==="#"?"":i.startsWith("#")?i:"#"+i,mg=class{constructor(i,c,f,s=!1){this.status=i,this.statusText=c||"",this.internal=s,f instanceof Error?(this.data=f.toString(),this.error=f):this.data=f}};function yg(i){return i!=null&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.internal=="boolean"&&"data"in i}function vg(i){let c=i.map(f=>f.route.path).filter(Boolean);return ke(c)||"/"}var By=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Qy(i,c){let f=i;if(typeof f!="string"||!Mr.test(f))return{absoluteURL:void 0,isExternal:!1,to:f};let s=f,o=!1;if(By)try{let d=new URL(window.location.href),p=_y.test(f)?new URL(Kp(f,d.protocol)):new URL(f),E=Dl(p.pathname,c);p.origin===d.origin&&E!=null?f=E+p.search+p.hash:o=!0}catch{nl(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:s,isExternal:o,to:f}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Ly=["POST","PUT","PATCH","DELETE"];new Set(Ly);var pg=["GET",...Ly];new Set(pg);var gg=["about:","blob:","chrome:","chrome-untrusted:","content:","data:","devtools:","file:","filesystem:","javascript:"];function Sg(i){try{return gg.includes(new URL(i).protocol)}catch{return!1}}var Zn=M.createContext(null);Zn.displayName="DataRouter";var Uc=M.createContext(null);Uc.displayName="DataRouterState";var Yy=M.createContext(!1);function bg(){return M.useContext(Yy)}var Gy=M.createContext({isTransitioning:!1});Gy.displayName="ViewTransition";var Eg=M.createContext(new Map);Eg.displayName="Fetchers";var Tg=M.createContext(null);Tg.displayName="Await";var Ke=M.createContext(null);Ke.displayName="Navigation";var Pu=M.createContext(null);Pu.displayName="Location";var ul=M.createContext({outlet:null,matches:[],isDataRoute:!1});ul.displayName="Route";var Dr=M.createContext(null);Dr.displayName="RouteError";var wy="REACT_ROUTER_ERROR",Rg="REDIRECT",Og="ROUTE_ERROR_RESPONSE";function Ag(i){if(i.startsWith(`${wy}:${Rg}:{`))try{let c=JSON.parse(i.slice(28));if(typeof c=="object"&&c&&typeof c.status=="number"&&typeof c.statusText=="string"&&typeof c.location=="string"&&typeof c.reloadDocument=="boolean"&&typeof c.replace=="boolean")return c}catch{}}function zg(i){if(i.startsWith(`${wy}:${Og}:{`))try{let c=JSON.parse(i.slice(40));if(typeof c=="object"&&c&&typeof c.status=="number"&&typeof c.statusText=="string")return new mg(c.status,c.statusText,c.data)}catch{}}function Cg(i,{relative:c}={}){qt(Iu(),"useHref() may be used only in the context of a component.");let{basename:f,navigator:s}=M.useContext(Ke),{hash:o,pathname:d,search:p}=ti(i,{relative:c}),E=d;return f!=="/"&&(E=d==="/"?f:ke([f,d])),s.createHref({pathname:E,search:p,hash:o})}function Iu(){return M.useContext(Pu)!=null}function Ul(){return qt(Iu(),"useLocation() may be used only in the context of a component."),M.useContext(Pu).location}var Xy="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Zy(i){M.useContext(Ke).static||M.useLayoutEffect(i)}function Ky(){let{isDataRoute:i}=M.useContext(ul);return i?Gg():Mg()}function Mg(){qt(Iu(),"useNavigate() may be used only in the context of a component.");let i=M.useContext(Zn),{basename:c,navigator:f}=M.useContext(Ke),{matches:s}=M.useContext(ul),{pathname:o}=Ul(),d=JSON.stringify(jy(s)),p=M.useRef(!1);return Zy(()=>{p.current=!0}),M.useCallback((g,m={})=>{if(nl(p.current,Xy),!p.current)return;if(typeof g=="number"){f.go(g);return}let O=_r(g,JSON.parse(d),o,m.relative==="path");i==null&&c!=="/"&&(O.pathname=O.pathname==="/"?c:ke([c,O.pathname])),(m.replace?f.replace:f.push)(O,m.state,m)},[c,f,d,o,i])}M.createContext(null);function _g(){let{matches:i}=M.useContext(ul),c=i[i.length-1];return(c==null?void 0:c.params)??{}}function ti(i,{relative:c}={}){let{matches:f}=M.useContext(ul),{pathname:s}=Ul(),o=JSON.stringify(jy(f));return M.useMemo(()=>_r(i,JSON.parse(o),s,c==="path"),[i,o,s,c])}function Dg(i,c){return Vy(i,c)}function Vy(i,c,f){var L;qt(Iu(),"useRoutes() may be used only in the context of a component.");let{navigator:s}=M.useContext(Ke),{matches:o}=M.useContext(ul),d=o[o.length-1],p=d?d.params:{},E=d?d.pathname:"/",g=d?d.pathnameBase:"/",m=d&&d.route;{let q=m&&m.path||"";Fy(E,!m||q.endsWith("*")||q.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${E}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let O=Ul(),T;if(c){let q=typeof c=="string"?Xn(c):c;qt(g==="/"||((L=q.pathname)==null?void 0:L.startsWith(g)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${g}" but pathname "${q.pathname}" was given in the \`location\` prop.`),T=q}else T=O;let N=T.pathname||"/",B=N;if(g!=="/"){let q=g.replace(/^\//,"").split("/");B="/"+N.replace(/^\//,"").split("/").slice(q.length).join("/")}let Y=f&&f.state.matches.length?f.state.matches.map(q=>Object.assign(q,{route:f.manifest[q.route.id]||q.route})):Dy(i,{pathname:B});nl(m||Y!=null,`No routes matched location "${T.pathname}${T.search}${T.hash}" `),nl(Y==null||Y[Y.length-1].route.element!==void 0||Y[Y.length-1].route.Component!==void 0||Y[Y.length-1].route.lazy!==void 0,`Matched leaf route at location "${T.pathname}${T.search}${T.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let Q=jg(Y&&Y.map(q=>Object.assign({},q,{params:Object.assign({},p,q.params),pathname:ke([g,s.encodeLocation?s.encodeLocation(q.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:q.pathname]),pathnameBase:q.pathnameBase==="/"?g:ke([g,s.encodeLocation?s.encodeLocation(q.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:q.pathnameBase])})),o,f);return c&&Q?M.createElement(Pu.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...T},navigationType:"POP"}},Q):Q}function Ug(){let i=Yg(),c=yg(i)?`${i.status} ${i.statusText}`:i instanceof Error?i.message:JSON.stringify(i),f=i instanceof Error?i.stack:null,s="rgba(200,200,200, 0.5)",o={padding:"0.5rem",backgroundColor:s},d={padding:"2px 4px",backgroundColor:s},p=null;return console.error("Error handled by React Router default ErrorBoundary:",i),p=M.createElement(M.Fragment,null,M.createElement("p",null,"💿 Hey developer 👋"),M.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",M.createElement("code",{style:d},"ErrorBoundary")," or"," ",M.createElement("code",{style:d},"errorElement")," prop on your route.")),M.createElement(M.Fragment,null,M.createElement("h2",null,"Unexpected Application Error!"),M.createElement("h3",{style:{fontStyle:"italic"}},c),f?M.createElement("pre",{style:o},f):null,p)}var xg=M.createElement(Ug,null),Jy=class extends M.Component{constructor(i){super(i),this.state={location:i.location,revalidation:i.revalidation,error:i.error}}static getDerivedStateFromError(i){return{error:i}}static getDerivedStateFromProps(i,c){return c.location!==i.location||c.revalidation!=="idle"&&i.revalidation==="idle"?{error:i.error,location:i.location,revalidation:i.revalidation}:{error:i.error!==void 0?i.error:c.error,location:c.location,revalidation:i.revalidation||c.revalidation}}componentDidCatch(i,c){this.props.onError?this.props.onError(i,c):console.error("React Router caught the following error during render",i)}render(){let i=this.state.error;if(this.context&&typeof i=="object"&&i&&"digest"in i&&typeof i.digest=="string"){const f=zg(i.digest);f&&(i=f)}let c=i!==void 0?M.createElement(ul.Provider,{value:this.props.routeContext},M.createElement(Dr.Provider,{value:i,children:this.props.component})):this.props.children;return this.context?M.createElement(Ng,{error:i},c):c}};Jy.contextType=Yy;var sr=new WeakMap;function Ng({children:i,error:c}){let{basename:f}=M.useContext(Ke);if(typeof c=="object"&&c&&"digest"in c&&typeof c.digest=="string"){let s=Ag(c.digest);if(s){let o=sr.get(c);if(o)throw o;let d=Qy(s.location,f),p=d.absoluteURL||d.to;if(Sg(p))throw new Error("Invalid redirect location");if(By&&!sr.get(c))if(d.isExternal||s.reloadDocument)window.location.href=p;else{const E=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(d.to,{replace:s.replace}));throw sr.set(c,E),E}return M.createElement("meta",{httpEquiv:"refresh",content:`0;url=${p}`})}}return i}function Hg({routeContext:i,match:c,children:f}){let s=M.useContext(Zn);return s&&s.static&&s.staticContext&&(c.route.errorElement||c.route.ErrorBoundary)&&(s.staticContext._deepestRenderedBoundaryId=c.route.id),M.createElement(ul.Provider,{value:i},f)}function jg(i,c=[],f){let s=f==null?void 0:f.state;if(i==null){if(!s)return null;if(s.errors)i=s.matches;else if(c.length===0&&!s.initialized&&s.matches.length>0)i=s.matches;else return null}let o=i,d=s==null?void 0:s.errors;if(d!=null){let O=o.findIndex(T=>T.route.id&&(d==null?void 0:d[T.route.id])!==void 0);qt(O>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(d).join(",")}`),o=o.slice(0,Math.min(o.length,O+1))}let p=!1,E=-1;if(f&&s){p=s.renderFallback;for(let O=0;O=0?o=o.slice(0,E+1):o=[o[0]];break}}}}let g=f==null?void 0:f.onError,m=s&&g?(O,T)=>{var N,B;g(O,{location:s.location,params:((B=(N=s.matches)==null?void 0:N[0])==null?void 0:B.params)??{},pattern:vg(s.matches),errorInfo:T})}:void 0;return o.reduceRight((O,T,N)=>{let B,Y=!1,Q=null,L=null;s&&(B=d&&T.route.id?d[T.route.id]:void 0,Q=T.route.errorElement||xg,p&&(E<0&&N===0?(Fy("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),Y=!0,L=null):E===N&&(Y=!0,L=T.route.hydrateFallbackElement||null)));let q=c.concat(o.slice(0,N+1)),F=()=>{let X;return B?X=Q:Y?X=L:T.route.Component?X=M.createElement(T.route.Component,null):T.route.element?X=T.route.element:X=O,M.createElement(Hg,{match:T,routeContext:{outlet:O,matches:q,isDataRoute:s!=null},children:X})};return s&&(T.route.ErrorBoundary||T.route.errorElement||N===0)?M.createElement(Jy,{location:s.location,revalidation:s.revalidation,component:Q,error:B,children:F(),routeContext:{outlet:null,matches:q,isDataRoute:!0},onError:m}):F()},null)}function Ur(i){return`${i} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function qg(i){let c=M.useContext(Zn);return qt(c,Ur(i)),c}function Bg(i){let c=M.useContext(Uc);return qt(c,Ur(i)),c}function Qg(i){let c=M.useContext(ul);return qt(c,Ur(i)),c}function xr(i){let c=Qg(i),f=c.matches[c.matches.length-1];return qt(f.route.id,`${i} can only be used on routes that contain a unique "id"`),f.route.id}function Lg(){return xr("useRouteId")}function Yg(){var s;let i=M.useContext(Dr),c=Bg("useRouteError"),f=xr("useRouteError");return i!==void 0?i:(s=c.errors)==null?void 0:s[f]}function Gg(){let{router:i}=qg("useNavigate"),c=xr("useNavigate"),f=M.useRef(!1);return Zy(()=>{f.current=!0}),M.useCallback(async(o,d={})=>{nl(f.current,Xy),f.current&&(typeof o=="number"?await i.navigate(o):await i.navigate(o,{fromRouteId:c,...d}))},[i,c])}var iy={};function Fy(i,c,f){!c&&!iy[i]&&(iy[i]=!0,nl(!1,f))}M.memo(wg);function wg({routes:i,manifest:c,future:f,state:s,isStatic:o,onError:d}){return Vy(i,void 0,{manifest:c,state:s,isStatic:o,onError:d})}function Rc(i){qt(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Xg({basename:i="/",children:c=null,location:f,navigationType:s="POP",navigator:o,static:d=!1,useTransitions:p}){qt(!Iu(),"You cannot render a inside another . You should never have more than one in your app.");let E=i.replace(/^\/*/,"/"),g=M.useMemo(()=>({basename:E,navigator:o,static:d,useTransitions:p,future:{}}),[E,o,d,p]);typeof f=="string"&&(f=Xn(f));let{pathname:m="/",search:O="",hash:T="",state:N=null,key:B="default",mask:Y}=f,Q=M.useMemo(()=>{let L=Dl(m,E);return L==null?null:{location:{pathname:L,search:O,hash:T,state:N,key:B,mask:Y},navigationType:s}},[E,m,O,T,N,B,s,Y]);return nl(Q!=null,` is not able to match the URL "${m}${O}${T}" because it does not start with the basename, so the won't render anything.`),Q==null?null:M.createElement(Ke.Provider,{value:g},M.createElement(Pu.Provider,{children:c,value:Q}))}function Zg({children:i,location:c}){return Dg(or(i),c)}function or(i,c=[]){let f=[];return M.Children.forEach(i,(s,o)=>{if(!M.isValidElement(s))return;let d=[...c,o];if(s.type===M.Fragment){f.push.apply(f,or(s.props.children,d));return}qt(s.type===Rc,`[${typeof s.type=="string"?s.type:s.type.name}] is not a component. All component children of must be a or `),qt(!s.props.index||!s.props.children,"An index route cannot have child routes.");let p={id:s.props.id||d.join("-"),caseSensitive:s.props.caseSensitive,element:s.props.element,Component:s.props.Component,index:s.props.index,path:s.props.path,middleware:s.props.middleware,loader:s.props.loader,action:s.props.action,hydrateFallbackElement:s.props.hydrateFallbackElement,HydrateFallback:s.props.HydrateFallback,errorElement:s.props.errorElement,ErrorBoundary:s.props.ErrorBoundary,hasErrorBoundary:s.props.hasErrorBoundary===!0||s.props.ErrorBoundary!=null||s.props.errorElement!=null,shouldRevalidate:s.props.shouldRevalidate,handle:s.props.handle,lazy:s.props.lazy};s.props.children&&(p.children=or(s.props.children,d)),f.push(p)}),f}var Oc="get",Ac="application/x-www-form-urlencoded";function xc(i){return typeof HTMLElement<"u"&&i instanceof HTMLElement}function Kg(i){return xc(i)&&i.tagName.toLowerCase()==="button"}function Vg(i){return xc(i)&&i.tagName.toLowerCase()==="form"}function Jg(i){return xc(i)&&i.tagName.toLowerCase()==="input"}function Fg(i){return!!(i.metaKey||i.altKey||i.ctrlKey||i.shiftKey)}function $g(i,c){return i.button===0&&(!c||c==="_self")&&!Fg(i)}var bc=null;function kg(){if(bc===null)try{new FormData(document.createElement("form"),0),bc=!1}catch{bc=!0}return bc}var Wg=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function fr(i){return i!=null&&!Wg.has(i)?(nl(!1,`"${i}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Ac}"`),null):i}function Pg(i,c){let f,s,o,d,p;if(Vg(i)){let E=i.getAttribute("action");s=E?Dl(E,c):null,f=i.getAttribute("method")||Oc,o=fr(i.getAttribute("enctype"))||Ac,d=new FormData(i)}else if(Kg(i)||Jg(i)&&(i.type==="submit"||i.type==="image")){let E=i.form;if(E==null)throw new Error('Cannot submit a )}{!matches.length&&

No matching command.

}

run esc close

} -function Shell({children}:{children:React.ReactNode}){const where=useLocation(),nav=useNavigate(),[palette,setPalette]=useState(false);const overview=useQuery({queryKey:['overview'],queryFn:api.overview,refetchInterval:5000});const sessions=overview.data?.sessions.length??0;useEffect(()=>{const key=(e:KeyboardEvent)=>{if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='k'){e.preventDefault();setPalette(true)}if(e.key==='n'&&!e.metaKey&&!e.ctrlKey&&!(e.target instanceof HTMLInputElement)&&!(e.target instanceof HTMLTextAreaElement)){nav('/');window.dispatchEvent(new Event('orchestra:new-task'))}};window.addEventListener('keydown',key);return()=>window.removeEventListener('keydown',key)},[nav]);return
Orchestra {where.pathname==='/'?'dispatch board':where.pathname==='/workers'?'worker pool':'task record'}
SESSIONS {sessions}SYNC 5s
{children}{palette&&setPalette(false)}/>}
} -function Board({tasks}:{tasks:Task[]}){return
{states.map(state=>{const lane=tasks.filter(t=>t.state===state);return
{label[state]}{lane.length}
{lane.map(t=>{t.title||t.id}{t.project}{t.id.slice(-5)})}{!lane.length&&

Nothing here. New work will appear in this branch.

}
})}
} -function Overview(){const q=useQuery({queryKey:['overview'],queryFn:api.overview,refetchInterval:5000}),[createOpen,setCreateOpen]=useState(false);useEffect(()=>{const open=()=>setCreateOpen(true);window.addEventListener('orchestra:new-task',open);return()=>window.removeEventListener('orchestra:new-task',open)},[]);if(q.isLoading)return

Dispatch board

Fetching queue state from the coordinator…

;if(q.error)return
Queue unavailable: {String(q.error)}
;const d=q.data!,active=d.tasks.filter(t=>t.state==='leased').length,attention=d.tasks.filter(t=>t.state==='blocked'||t.state==='failed').length,approvals=new Set(d.sessions.filter(s=>s.pending_approval).map(s=>s.capture?.task_id));return

Agent dispatch

Keep the work moving.

Live task state across every connected harness. The board refreshes every five seconds.

Tasks{d.tasks.length}
In session{active}
Blocked or failed{attention}
Awaiting approval{approvals.size}

Task flow

SYNCED {new Date(d.updated_at).toLocaleTimeString()}
{createOpen&&setCreateOpen(false)}/>}
} -function Create({close}:{close:()=>void}){const qc=useQueryClient(),nav=useNavigate(),first=useRef(null),[title,setTitle]=useState(''),[description,setDescription]=useState(''),[project,setProject]=useState('default'),[capability,setCapability]=useState(''),[advanced,setAdvanced]=useState(''),[formError,setFormError]=useState('');useEffect(()=>{first.current?.focus()},[]);const m=useMutation({mutationFn:()=>{let extra:Record={};if(advanced.trim()){try{extra=JSON.parse(advanced)}catch{return Promise.reject(new Error('Additional fields must be valid JSON.'))}}return api.create({...extra,source:'web',external_id:crypto.randomUUID(),project,title,description,capability:capability.split(',').map(x=>x.trim()).filter(Boolean)})},onSuccess:(e:any)=>{qc.invalidateQueries({queryKey:['overview']});nav('/tasks/'+e.task_id)}});const submit=(e:React.FormEvent)=>{e.preventDefault();setFormError('');if(title.trim().length<3){setFormError('Give the task a title of at least three characters.');return}if(description.trim().length<10){setFormError('Add enough immutable instructions for an agent to act safely.');return}m.mutate()};return
e.stopPropagation()}>

Dispatch new work

New task

Give the harness a clear objective and immutable operating instructions.