diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44ebb1a1a..75e9898fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,12 +15,21 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Extract version from tag + - name: Extract and validate version from tag id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Release tags must be vMAJOR.MINOR.PATCH; refusing ${GITHUB_REF_NAME@Q}" >&2 + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Set version in pyproject.toml - run: sed -i "s/^version = .*/version = \"${{ steps.version.outputs.version }}\"/" pyproject.toml + env: + VERSION: ${{ steps.version.outputs.version }} + run: sed -i "s/^version = .*/version = \"${VERSION}\"/" pyproject.toml - name: Install nfpm run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 43bdde6ea..bc7ca307d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,117 @@ Each GitHub release body is the matching section of this file. ## [Unreleased] +### Changed + +- Compatible and OpenRouter models budget chat, loop and agent history against + the output each request actually reserves (the profile's maximum output, + capped at 32,768 tokens), the same figure agent eligibility already used. + Long tool-using turns keep more history before summarizing (DeepSeek V4 at + the default 75%: 491,520 to 761,856 working tokens), so those turns send + more input tokens per request; models whose declared output nearly equals + their context window no longer run with no history budget and no overflow + rescue. An agent whose compatible provider rejects a payload already below + the provider-reported window now compacts instead of re-sending it. Codex is + unchanged. To retain the previous smaller compatible working set, lower + `openai_compatible.context_utilization` (about 48 for DeepSeek V4's previous + 491,520 tokens). +- One-time check, workflow and webhook schedules interrupted by a restart are + now paused as inert rather than run again from the beginning. Their reason + is shown to operators; set a new `run_at` to re-arm after checking effects. + Reminders and digests still replay, and recurring schedules are unchanged. + No action is required on upgrade. +- Documented webhook success, partial-delivery and retry behaviour for + scheduled webhook actions and inbound webhooks. +- History search finds archived conversation summaries again; the first start + after upgrading indexes existing archives in the background. Expect the + session and full-text databases to grow during this one-time migration. + +### Fixed + +- Computer-use requests refused before any input (unknown source IDs, export + on an attached desktop, Hyprland target discovery failures, and stale or + unknown session references) now report `not_dispatched` with a safe next + step instead of unknown-outcome RELEASE-ALL guidance. Such refusals no longer + cancel a running session, including another session in the same channel. + Successful computer calls are audited with a success reason code rather + than `computer_rejected`. +- `email_send` reports recipients the mail server refused (To, CC or BCC) + instead of claiming the message went to everyone; a partial send is reported, + never retried. +- `email_read` no longer shows a text attachment as the message body, and its + attachment list follows the MIME disposition (any capitalisation) instead of + a text match. +- `http_probe` sends request bodies starting with `@` literally instead of + uploading a file, and rejects header names starting with `@`. Such bodies + require curl 7.43 or newer on the probing host. +- `validate_action` process checks no longer find their own command instead of + the target process; a matching ancestor still counts. Missing `pgrep` and + unusable patterns now report errors rather than false health. +- `validate_action` log checks report an error when the journal cannot be read + or is only partly readable instead of claiming it is clean. Invalid patterns + report errors and journalctl notices no longer count as log lines. Operators + whose service user lacks journal access must grant it (for example via the + `systemd-journal` group) before relying on these checks. +- `validate_action` HTTP checks accept explicitly expected 4xx/5xx statuses; + connection failures and timeouts never count as a received status. +- The release workflow rejects tags that are not `vMAJOR.MINOR.PATCH` before + touching package metadata, and passes a validated version to shell steps as + data rather than embedding tag text in a command. +- Documentation and `scripts/monitor.sh` now point to standard output and the + systemd journal rather than a log file Odin never writes. Fresh packages since + v4.0.0 start immediately in loopback-only bootstrap mode; the computer-use + handoff and install pages now say so. +- Logging in to the WebUI without **Stay logged in** now replaces an older + saved login, so reloading after a restart retains the new session instead of + returning to the login screen. A persistent login also clears the tab's old + session-only credential. +- Compatible endpoints configured for GLM preserved thinking now count replayed + reasoning when sizing context, so history is summarized before it overflows + and overflow recovery no longer claims a fit while that reasoning is still + sent; replayed reasoning is never shortened or edited. +- Verify integrity checks every retained rotated audit file, not only the active + one, and lists each file's result (verified, predates signing, break at line N, + unreadable, missing); files are streamed in a worker thread instead of read into + memory. +- A tool that hit its time limit no longer lets the turn continue when its + ledger record could not be saved; the turn stops with an error, as every + other ledger write failure already does. +- Resuming interrupted work no longer leaves a tool call unanswered, or answers + it with an earlier call's result, when a model provider reuses tool-call IDs + across replies. Agents also accept IDs reused across separate replies while + still refusing duplicates within one reply. +- Scheduled digests report disk and memory again. They run under the schedule's + identity (the creator, or the `scheduler` system identity) and list hosts they + cannot reach as collection failures. +- Grafana-triggered schedules with an alert name now match any alert in a + notification, not just the first; each schedule still runs once per + notification. +- Browser tool descriptions state that every call starts a fresh browser session: + `browser_click` no longer suggests reading the clicked page with a later + `browser_read_page`, `browser_fill` points to `submit=true`, and + `browser_evaluate` notes that navigation it starts is not awaited. Browser + behaviour is unchanged. +- Compressed tool history labels failed calls with a short reason (for example + `run_command→ERR (blocked)`, `→ERR (timed out)` or `→ERR (disallowed host)`) + instead of incorrectly marking these failures OK; Recent Actions marks + failed calls ERROR. Successful calls with explicit outcome metadata remain OK. + +- Long replies split around code blocks keep their formatting: text after a + block no longer shows as code, no message ends with an empty code block, and + a split with a long language tag no longer creates an over-limit message. + Truncated workflow and loop posts close open code blocks before the marker. +- `parse_time` uses every part of an expression, including compound durations + (`in 1 hour 30 minutes`), a day after a time (`5pm tomorrow`), a time after a + weekday (`friday 3pm`), and `in 2 days at 9am`. Unused date or time words now + return an error instead of silently scheduling the wrong time. Hours and + minutes measure elapsed time across daylight-saving changes; days retain the + local clock time. +- A cancelled knowledge, memory, list or learned write can no longer overwrite (or corrupt) a newer save that already succeeded. +- A failed knowledge ingest no longer leaves search hits for a document that was not stored or blocks that name; leftovers from earlier failures are removed at startup. +- Knowledge ingest reports failure when its version record cannot be saved, and re-ingesting the same content repairs a missing version snapshot. +- The Sessions page User ID filter now applies to every result source; summaries + and index results that cannot be attributed are left out. + ## [4.7.0] - 2026-09-24 ### Added diff --git a/README.md b/README.md index 72008cf3c..517286709 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,10 @@ sudo -u odin /opt/odin/.venv/bin/python /opt/odin/scripts/codex_login.py \ --credentials-path /var/lib/odin/codex_auth.json --device sudoedit /etc/odin/config.yml # set web.api_token; bind web.host to 127.0.0.1 unless it sits behind TLS; # review permissions.default_tier (template: admin) and tools.hosts -sudo systemctl start odin # WebUI on the configured web.port (default 3000) +sudo systemctl restart odin # WebUI on the configured web.port (default 3000) ``` -**Upcoming branch packages:** unlike the published v3.98.0 procedure above, a fresh install automatically enables and starts a restricted loopback bootstrap service. Open `http://127.0.0.1:3000/ui/` locally. For a remote install, run `ssh -L 3000:127.0.0.1:3000 user@odin-host` on your workstation, then open that same local URL. Do not publish pending setup through a reverse proxy. +**Packages since v4.0.0:** unlike the v3.98.0 procedure above, a fresh install automatically enables and starts a restricted loopback bootstrap service. Open `http://127.0.0.1:3000/ui/` locally. For a remote install, run `ssh -L 3000:127.0.0.1:3000 user@odin-host` on your workstation, then open that same local URL. Do not publish pending setup through a reverse proxy. Finish setup with a strong Web API token, then sign in as administrator. **System → Config → Web listener exposure** shows the configured host, whether it came from an explicit `web.host` key or the schema default, and the addresses the current process actually owns. Authorize beyond-loopback access there and re-enter a current admin API token. This records permission for the next start, not a live rebind. After arranging TLS and network access controls, restart manually with `sudo systemctl restart odin`. The same card can revoke authorization and narrow the next start to loopback. A Discord token alone never authenticates or widens the Web listener. @@ -234,7 +234,7 @@ The package installs: | Environment file | `/etc/odin/.env` | | Persistent data | `/var/lib/odin` | | Local command workspace | `/var/lib/odin-workspace` | -| Logs | `/var/log/odin` | +| Logs | systemd journal (`sudo journalctl -u odin`); `/var/log/odin` is created but not written | | Systemd unit | `/usr/lib/systemd/system/odin.service` | | Private computer evidence (service-owned, 0700) | `/var/lib/odin/computer` | | Precompiled root-owned Wayland guardian | `/usr/libexec/odin-computer-wayland-input` | @@ -242,7 +242,7 @@ The package installs: | Computer-use installation handoff | `/usr/share/doc/odin/computer-use/PACKAGING.md` | | Computer-use setup and recovery | `/usr/share/doc/odin/computer-use/OPERATOR.md`, `RECOVERY.md` | -The package installs the application files and systemd unit. Its post-install script creates the `odin` service account, virtual environment, SSH key, data directories, configuration links, and local command workspace. Upcoming branch packages automatically enable and start a new installation in loopback-only bootstrap mode; published v3.98.0 packages are enabled but require manual configuration and start. See the [Quick start](#quick-start) for local access and SSH forwarding. Upgrades preserve configuration and data and restart the service only if it was already running. +The package installs the application files and systemd unit. Its post-install script creates the `odin` service account, virtual environment, SSH key, data directories, configuration links, and local command workspace. Since v4.0.0, a fresh installation is enabled and started in loopback-only bootstrap mode; v3.98.0 and earlier packages were enabled but required manual configuration and start. See the [Quick start](#quick-start) for local access and SSH forwarding. Upgrades preserve configuration and data and restart the service only if it was already running. Fresh installs and upgrades install the `pdf` and `computer` Python extras and provision private computer state with symlink rejection. Computer enablement is @@ -304,7 +304,7 @@ sudoedit /etc/odin/config.yml 5. Start the service and inspect startup: ```bash -sudo systemctl start odin +sudo systemctl restart odin sudo systemctl status odin sudo journalctl -u odin -f ``` diff --git a/docs/architecture.md b/docs/architecture.md index f7de5445e..3b1b0a945 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -193,8 +193,9 @@ attempts; a healthy in-flight generation has its own transport limits. [`TurnResumeManager`][resume] rechecks the original request and current authorization before resuming suspended chat work. In-process auto-resume also requires the session not to have advanced. After restart, resume is explicit. -Ledger repair supplies stored results or explicit uncertainty for unmatched tool -calls; it does not automatically execute them again. Unresolved external effects +Ledger repair pairs calls by transcript position and generation, supplying stored +results or explicit uncertainty for unmatched tool calls; it does not automatically +execute them again. Unresolved external effects block automatic continuation. ## Managed hosts: desired state, runtime identity, access @@ -223,6 +224,10 @@ result summaries, elapsed time, errors, and optional risk/diff metadata. reflection/lifecycle hooks. These explain what happened; the durable turn ledger governs replay safety. Neither replaces the other. +Each retained audit file carries its own HMAC chain from genesis; Verify integrity +checks and reports every retained file independently. It cannot detect files +already removed by rotation or deletion from the oldest end. + Large outputs should be retrieved, not regenerated merely because a preview was short. The retention contracts are intentionally different: diff --git a/docs/computer-use/PACKAGING.md b/docs/computer-use/PACKAGING.md index 6d83f5cfb..972ebf5d2 100644 --- a/docs/computer-use/PACKAGING.md +++ b/docs/computer-use/PACKAGING.md @@ -76,7 +76,12 @@ storage safely, including for source installs; they never repair existing unsafe objects or move receipts. System Python dependencies remain distinct from the venv. Fresh installation leaves computer use disabled by default. The base Odin service -is enabled but not started until the operator completes setup. Ordinary upgrades +is enabled and started immediately in a restricted bootstrap mode: it listens on +loopback only, whatever `web.host` says, and serves guided setup at +`http://127.0.0.1:3000` (use an SSH tunnel for a remote host, never a reverse +proxy) until the operator completes it; widening beyond loopback later is an +explicit, authenticated operator choice. Packages up to v3.98.0 enabled the +service without starting it. Ordinary upgrades preserve configuration, computer enablement, data and prior service state: an already-running service is restarted; an inactive one remains inactive. No computer task, desktop capture, input action, extension activation, session-bus connection, diff --git a/docs/computer-use/RECOVERY.md b/docs/computer-use/RECOVERY.md index 0d114a5ae..805b859a0 100644 --- a/docs/computer-use/RECOVERY.md +++ b/docs/computer-use/RECOVERY.md @@ -60,6 +60,13 @@ Those measures apply only when release is unverified or outcome is unknown. inspect fresh evidence before planning a different action. No category permits replaying an action whose effect may already have occurred. +Observe, source selection, export, status, target inventory and start preflight +refusals that occur before input, focus recovery or cleanup carry +`not_dispatched` and leave an existing session running. A rejected request for +an unknown source ID or an unavailable attached-desktop export therefore does +not require RELEASE-ALL. Successful calls carry the audit reason +`computer_succeeded`, or `verified`/`executed` for successful action receipts. + For the separate alternate-input rule, see [OPERATOR.md](OPERATOR.md#alternate-input-paths). For persisted quarantine, **System > Computer** remains available even when input diff --git a/docs/configuration.md b/docs/configuration.md index 365ea2b83..55e99b0f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -185,6 +185,9 @@ and OpenRouter. URLs are used verbatim: Odin never appends `/v1`. Local examples are vLLM `http://127.0.0.1:8000/v1`, llama.cpp `:8080/v1`, and LM Studio `:1234/v1`. Profiles declare reasoning dialect plus reasoning-content feedback; the safe default is not to echo provider reasoning into history. +A compatible model's usable prompt budget is its total window minus the output +each request asks for (the profile's max output, capped at 32,768 tokens); +`openai_compatible.context_utilization` sets how much of that history may use. `iteration_timeout_seconds` bounds each agent LLM call. It is a backstop against a hung call, not a working limit — set it well above a legitimate @@ -248,6 +251,8 @@ browser: Leave `cdp_url` empty to launch a local headless Chromium. Set to `ws://host:port?token=secret` for remote Browserless. +Each browser tool call runs in a new, empty browser context that is closed when the call ends; cookies, storage, form input and page state never carry over between calls. + Run `playwright install chromium` after installation. ## Image Generation @@ -312,6 +317,10 @@ Runtime overrides persist in `data/permissions.json` and take precedence. ## Webhooks +For scheduled HTTP actions and inbound webhook delivery, see +[Schedules & webhooks](scheduling.md). This includes retry and partial-delivery +behaviour; outbound notifications use a separate path. + ```yaml webhook: enabled: false @@ -333,9 +342,14 @@ context: ```yaml logging: level: INFO # DEBUG, INFO, WARNING, ERROR - directory: ./data/logs + directory: ./data/logs # reserved path kept out of the command workspace; Odin writes no log files here ``` +Odin writes application logs to standard output/error: for the service use +`journalctl -u odin -f`, for Docker use `docker logs odin-bot`, and for a source +run use its terminal. `logging.level` sets verbosity. Tool executions and events +go to the audit log (`data/audit.jsonl`), which the WebUI Audit and Logs pages read. + ## File Paths (DEB install) | Purpose | Path | @@ -343,7 +357,7 @@ logging: | Config | `/etc/odin/config.yml` | | Secrets | `/etc/odin/.env` | | Data | `/var/lib/odin/` | -| Logs | `/var/log/odin/` | +| Logs | systemd journal (`sudo journalctl -u odin`); `/var/log/odin` is created but not written | | Application | `/opt/odin/` | | Systemd | `/usr/lib/systemd/system/odin.service` | diff --git a/docs/install.md b/docs/install.md index 7864d6019..44670eb2d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,7 +1,7 @@ # Install -The WebUI-first bootstrap flow below describes this branch's upcoming release. -Published v3.98.0 packages still require the earlier manual configuration and start procedure. +The WebUI-first bootstrap flow below applies to packages since v4.0.0. +v3.98.0 and earlier packages used the manual configuration and start procedure. Odin ships as an amd64 Debian package and as a source checkout. The package path is for a long-running service; the source path is for development. @@ -27,7 +27,7 @@ The package installs a dedicated `odin` system user, a Python virtual environmen | Environment file | `/etc/odin/.env` | | Persistent data | `/var/lib/odin` | | Local command workspace | `/var/lib/odin-workspace` | -| Logs | `/var/log/odin` | +| Logs | systemd journal (`sudo journalctl -u odin`); `/var/log/odin` is created but not written | | Systemd unit | `/usr/lib/systemd/system/odin.service` | ## First-time setup diff --git a/docs/reference/api.md b/docs/reference/api.md index c37d2bb9a..7b574cd8b 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -115,19 +115,19 @@ This describes the normal authenticated deployment. With no configured tokens (i | POST | /api/grafana-alerts/rules | [src.web.api.integrations](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/integrations.py#L540) | Yes | — | | DELETE | /api/grafana-alerts/rules/{rule_id} | [src.web.api.integrations](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/integrations.py#L574) | Yes | — | | GET | /api/grafana-alerts/remediations | [src.web.api.integrations](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/integrations.py#L585) | Yes | — | -| GET | /api/knowledge | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L94) | Yes | — | -| POST | /api/knowledge | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L101) | Yes | — | -| DELETE | /api/knowledge/{source} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L126) | Yes | — | -| POST | /api/knowledge/{source}/reingest | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L137) | Yes | — | -| GET | /api/knowledge/search | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L158) | Yes | — | -| GET | /api/knowledge/{source}/chunks | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L179) | Yes | — | -| GET | /api/knowledge/duplicates | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L193) | Yes | — | -| POST | /api/knowledge/merge | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L207) | Yes | — | -| GET | /api/knowledge/{source}/versions | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L234) | Yes | — | -| GET | /api/knowledge/{source}/versions/{version:\d+} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L243) | Yes | — | -| POST | /api/knowledge/{source}/versions/{version:\d+}/restore | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L255) | Yes | — | -| GET | /api/knowledge/{source}/versions/{v1:\d+}/diff/{v2:\d+} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L278) | Yes | — | -| POST | /api/knowledge/import | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L294) | Yes | — | +| GET | /api/knowledge | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L95) | Yes | — | +| POST | /api/knowledge | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L102) | Yes | — | +| DELETE | /api/knowledge/{source} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L127) | Yes | — | +| POST | /api/knowledge/{source}/reingest | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L138) | Yes | — | +| GET | /api/knowledge/search | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L159) | Yes | — | +| GET | /api/knowledge/{source}/chunks | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L180) | Yes | — | +| GET | /api/knowledge/duplicates | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L194) | Yes | — | +| POST | /api/knowledge/merge | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L208) | Yes | — | +| GET | /api/knowledge/{source}/versions | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L235) | Yes | — | +| GET | /api/knowledge/{source}/versions/{version:\d+} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L244) | Yes | — | +| POST | /api/knowledge/{source}/versions/{version:\d+}/restore | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L256) | Yes | — | +| GET | /api/knowledge/{source}/versions/{v1:\d+}/diff/{v2:\d+} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L279) | Yes | — | +| POST | /api/knowledge/import | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L295) | Yes | — | | GET | /api/schedules/status | [src.web.api.schedules_api](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/schedules_api.py#L39) | Yes | — | | GET | /api/schedules | [src.web.api.schedules_api](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/schedules_api.py#L43) | Yes | — | | POST | /api/schedules | [src.web.api.schedules_api](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/schedules_api.py#L47) | Yes | — | @@ -159,12 +159,12 @@ This describes the normal authenticated deployment. With no configured tokens (i | GET | /api/audit/verify | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L340) | Yes | — | | GET | /api/logs/search | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L353) | Yes | — | | GET | /api/logs/stats | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L380) | Yes | — | -| GET | /api/memory | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L375) | Yes | — | -| GET | /api/memory/{scope} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L396) | Yes | Every key/value in one scope, in ONE request. | -| GET | /api/memory/{scope}/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L417) | Yes | — | -| PUT | /api/memory/{scope}/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L433) | Yes | — | -| DELETE | /api/memory/{scope}/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L457) | Yes | — | -| POST | /api/memory/bulk-delete | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L476) | Yes | — | +| GET | /api/memory | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L376) | Yes | — | +| GET | /api/memory/{scope} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L397) | Yes | Every key/value in one scope, in ONE request. | +| GET | /api/memory/{scope}/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L418) | Yes | — | +| PUT | /api/memory/{scope}/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L434) | Yes | — | +| DELETE | /api/memory/{scope}/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L458) | Yes | — | +| POST | /api/memory/bulk-delete | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L477) | Yes | — | | GET | /api/risk/stats | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L392) | Yes | — | | GET | /api/risk/recent | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L399) | Yes | — | | GET | /api/governor/stats | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L410) | Yes | — | @@ -234,9 +234,9 @@ This describes the normal authenticated deployment. With no configured tokens (i | GET | /api/freshness/stats | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L459) | Yes | — | | GET | /api/freshness/recent | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L466) | Yes | — | | GET | /api/validation/stats | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L481) | Yes | — | -| GET | /api/learned | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L524) | Yes | — | -| DELETE | /api/learned/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L530) | Yes | — | -| PUT | /api/learned/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L537) | Yes | — | +| GET | /api/learned | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L525) | Yes | — | +| DELETE | /api/learned/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L531) | Yes | — | +| PUT | /api/learned/{key} | [src.web.api.knowledge_mem](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/knowledge_mem.py#L538) | Yes | — | | GET | /api/affordances | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L495) | Yes | — | | GET | /api/compression/stats | [src.web.api.observability](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/observability.py#L507) | Yes | — | | GET | /api/startup/diagnostics | [src.web.api.config_admin](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/web/api/config_admin.py#L1214) | Yes | — | @@ -266,9 +266,9 @@ Registered by [HealthServer](https://github.com/Calmingstorm/Odin/blob/e1318eca8 | GET | /metrics | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1350) | HealthServer construction; no API authentication | Prometheus metrics endpoint. | | POST | /webhook/gitea | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1444) | webhooks.enabled; handler verifies webhook signature/shared secret | — | | POST | /webhook/grafana | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1492) | webhooks.enabled; handler verifies webhook signature/shared secret | — | -| POST | /webhook/generic | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1566) | webhooks.enabled; handler verifies webhook signature/shared secret | — | -| POST | /webhook/github | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1589) | webhooks.enabled; handler verifies webhook signature/shared secret | — | -| POST | /webhook/gitlab | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1662) | webhooks.enabled; handler verifies webhook signature/shared secret | — | +| POST | /webhook/generic | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1567) | webhooks.enabled; handler verifies webhook signature/shared secret | — | +| POST | /webhook/github | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1590) | webhooks.enabled; handler verifies webhook signature/shared secret | — | +| POST | /webhook/gitlab | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1663) | webhooks.enabled; handler verifies webhook signature/shared secret | — | | GET | / | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1051) | web.enabled + UI directory exists; no API authentication | Redirect / to /ui/. | | GET | /ui/{path:.*} | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1055) | web.enabled + UI directory exists; no API authentication | Serve static UI files, defaulting to index.html for SPA routing. | | GET | /ui | [src.health.server](https://github.com/Calmingstorm/Odin/blob/e1318eca83a40b9418d873ad85c31686ea6b57d0/src/health/server.py#L1051) | web.enabled + UI directory exists; no API authentication | Redirect / to /ui/. | diff --git a/docs/reference/tools.md b/docs/reference/tools.md index 88909d65b..408acda78 100644 --- a/docs/reference/tools.md +++ b/docs/reference/tools.md @@ -575,7 +575,7 @@ Source: [`src/tools/defs/browser_web.py`](https://github.com/Calmingstorm/Odin/b **Core:** No -
Navigates to a URL and clicks an element by CSS selector. Returns a confirmation summary after clicking. To fill forms, use browser_fill. To read page content after clicking, follow up with browser_read_page.
+
Loads a URL in a fresh browser session, clicks an element by CSS selector, and returns the resulting page's title and URL. Cookies, storage and page state end with the call; a later browser_read_page reloads the URL without them. To fill and submit in one call, use browser_fill with submit=true.

[affordances: cost=high risk=high latency=seconds] (requires: browser enabled; installed Chromium or reachable configured CDP endpoint)

@@ -589,7 +589,7 @@ Source: [`src/tools/defs/browser_web.py`](https://github.com/Calmingstorm/Odin/b **Core:** No -
Navigates to a URL and fills a form field by CSS selector. Optionally submits by pressing Enter. To click buttons, use browser_click.
+
Loads a URL in a fresh browser session, fills one field by CSS selector, optionally presses Enter (submit=true), and returns the page's title and URL. The value is gone when the call ends, so a later browser_click cannot submit it; use submit=true, or one browser_evaluate for several fields.

[affordances: cost=high risk=high latency=seconds] (requires: browser enabled; installed Chromium or reachable configured CDP endpoint)

@@ -604,7 +604,7 @@ Source: [`src/tools/defs/browser_web.py`](https://github.com/Calmingstorm/Odin/b **Core:** No -
Evaluates JavaScript on a URL and returns the result. For custom scraping or interaction. Large results have retained previews; use get_tool_output(cursor=...) without re-running the expression.
+
Loads a URL in a fresh browser session, evaluates a JavaScript expression, and returns its result (a returned Promise is awaited). Use it for scraping or several interaction steps in one call; the session closes when it returns, so a navigation it starts may not complete. Large results have retained previews; use get_tool_output(cursor=...) without re-running the expression.

[affordances: cost=high risk=high latency=seconds] (requires: browser enabled; installed Chromium or reachable configured CDP endpoint)

diff --git a/docs/scheduling.md b/docs/scheduling.md new file mode 100644 index 000000000..3a948d4b1 --- /dev/null +++ b/docs/scheduling.md @@ -0,0 +1,95 @@ +# Schedules & webhooks + +Schedules can run checks, multi-step workflows, reminders, digests and HTTP +webhook actions. An inbound webhook can also trigger a schedule. These are two +different paths: an outbound notifier is a separate subsystem, not covered here. + +## Who scheduled work runs as + +Discord-created schedules run as their creator. System schedules created through +the WebUI, REST API or a skill without a requester run as `scheduler` with the +admin tool tier. Host access is *still enforced*: a Host Access entry named +`scheduler` supplies its allowed hosts and default host, if present; otherwise +the Default Policy applies. To create that entry (the WebUI Add user picker only +accepts numeric Discord IDs), use `PUT /api/host-access/user/scheduler` with +`{"allowed_hosts":["your-host"],"default_host":""}`. It then appears on +System → Host Access. Widening the Default Policy grants access to every +unlisted user, not just system schedules. Digests report denied or failed host +probes as collection failures; if every probe fails, the digest run fails. + +## Interrupted one-time runs + +A side-effecting one-time `check`, `workflow` or `webhook` run is marked as +started **before** the first effect. If Odin stops during the run, or cannot +save its completion, the schedule becomes paused and **inert** on restart or +the next scheduler tick. It is not automatically run again from step one. +The reason is shown in the schedule listing, REST response and WebUI. Check +what already happened, then set a **new `run_at`** to re-arm it. Merely +unpausing it is refused. A run that never began, including a queued run, +remains eligible; a normally recorded failure still follows its configured +retry behaviour. One-time reminders and digests have no start marker and +still replay after an interruption; recurring and trigger schedules retain +their existing cadence. An interruption may happen after a marker is saved +but before an effect starts. Quarantine chooses safety over automatic replay. + +## Scheduled webhook actions + +- Each run sends one request through a shared aiohttp session. Methods are + `GET`, `POST` (default), `PUT`, `PATCH`, `DELETE` and `HEAD`; the WebUI does + not offer `HEAD`. A dict or list body is sent as JSON, any other body as + text. JSON-looking text entered in the WebUI is sent as text unless you set + its `Content-Type` header. +- The total timeout defaults to 30 seconds and can be set up to 300 seconds. + Redirects are followed, up to ten. The request does not need an active + Discord connection. +- With `expected_status_codes` **omitted or empty (`[]`)**, *any* HTTP + response counts as success, even 4xx or 5xx. If specified, a status outside + the list fails with `Webhook returned status X, expected one of […]`. + DNS, connection, TLS, timeout and redirect failures also fail. An undecodable + response body (using its declared charset, UTF-8 by default) can mark an + already-delivered request as failed. +- History records success or failure, duration and a failure error (up to 500 + characters), along with `last_error` and `consecutive_failures`. Neither + the response status nor body is stored; success status is logged. +- Retries default to **off** (`max_retries: 0`). REST can set the retry count; + the WebUI cannot. Delays are `retry_backoff_seconds` (default 60) times + `2^(attempt−1)`, capped at 3600 seconds. A retry sends the **entire** request + again. Delivery is at least once; Odin provides no idempotency key. A cron + slot missed during a pending retry runs once immediately afterward, then + normal cadence resumes. +- A successful one-time webhook is removed. On a recorded final failure, it + remains without `next_run` until a manual run or a new `run_at`. If its + completion was **not recorded** after starting, it instead becomes inert + under the interrupted-run rule above. Verify external effects before + re-arming it. +- Every third consecutive failure sends a Discord alert, provided a channel + and connection are available. A manual run returns `{"status":"success"}` + or `{"status":"failure","error":"…"}`. Overlapping runs of the same + schedule are dropped. + +## Inbound webhooks + +Endpoints are `/webhook/gitea`, `/webhook/github`, `/webhook/gitlab`, +`/webhook/grafana` and `/webhook/generic`, registered only with +`webhook.enabled`. Gitea and GitHub use HMAC authentication; GitLab, Grafana +and generic use a shared token. Authentication fails closed with 403 when +no secret is configured. Invalid JSON receives 400. The body limit is +10 MiB, and there is no per-endpoint rate limit. + +Processing order: parse the request, run Grafana remediation where applicable, +run matching schedules **to completion**, then post the channel notification +and respond. A slow action can outlast the sender's timeout; the sender may +retry while Odin is still working. A named Grafana trigger compares its +case-insensitive alert-name substring against **every** alert in a notification, +including resolved alerts. Each schedule fires at most once per delivery, +even if several alerts match. Unnamed triggers also fire once per delivery. + +The HTTP response describes **the notification only**, not whether triggered +actions succeeded: 200 `{"status":"delivered"}` means notification posted; +500 means no channel was configured or sending failed; 503 means the bot was +not ready to send. Schedules already fired even if this final delivery fails. +Trigger machinery errors are logged without changing the response; individual +action failures appear in that schedule's history. A sender retrying after a +5xx can repeat the effects. Redeliveries are not deduplicated. While Discord +is disconnected, non-webhook triggered schedules and paused schedules are +skipped rather than queued. diff --git a/package.json b/package.json index f847d8bd2..8f1a6dea7 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,17 @@ "check:templates": "node scripts/check-vue-templates.mjs", "check:codex-quota-ui": "node scripts/check-codex-quota-ui.mjs", "check:llm-config-refresh": "node --experimental-vm-modules scripts/check-llm-config-refresh.mjs", + "check:llm-provider-visibility": "node scripts/check-llm-provider-visibility-browser.mjs", "check:setup-ui": "node scripts/check-setup-ui.mjs", "check:template-bindings": "node scripts/check-template-bindings.mjs", "check:modal-race": "node scripts/check-modal-race.mjs", - "check": "npm run check:templates && npm run check:codex-quota-ui && npm run check:llm-config-refresh && npm run check:template-bindings && npm run check:permission-token-repair && npm run check:listener-consent && npm run check:w1-webui && npm run check:modal-race && npm run check:host-access-races && npm run check:host-enrollment && npm run check:socket-identity && npm run check:ws-lifecycle && npm run check:w2-freshness && npm run check:toggle-serialization && npm run check:search-races && npm run check:config-keydown && npm run check:css-tokens && npm run check:turn-state-webui && npm run check:audit-verify && npm run check:schedule-time && npm run check:schedule-report-format && npm run check:config-health && npm run check:config-center-ui2 && npm run check:mcp-webui && npm run check:tools-webui && npm run check:learning-toggle && npm run check:config-save-boundaries && npm run check:usage-activity && npm run check:output-renderer && npm run check:live-logs && npm run check:logs-preset-search-races && npm run check:discord-identity && npm run build", + "check": "npm run check:templates && npm run check:codex-quota-ui && npm run check:llm-config-refresh && npm run check:llm-provider-visibility && npm run check:template-bindings && npm run check:permission-token-repair && npm run check:listener-consent && npm run check:w1-webui && npm run check:modal-race && npm run check:host-access-races && npm run check:host-enrollment && npm run check:socket-identity && npm run check:ws-lifecycle && npm run check:login-persistence && npm run check:w2-freshness && npm run check:toggle-serialization && npm run check:search-races && npm run check:config-keydown && npm run check:css-tokens && npm run check:turn-state-webui && npm run check:audit-verify && npm run check:schedule-time && npm run check:schedule-report-format && npm run check:config-health && npm run check:config-center-ui2 && npm run check:mcp-webui && npm run check:tools-webui && npm run check:learning-toggle && npm run check:config-save-boundaries && npm run check:usage-activity && npm run check:output-renderer && npm run check:live-logs && npm run check:logs-preset-search-races && npm run check:discord-identity && npm run build", "check:listener-consent": "node scripts/check-listener-consent.mjs", "check:host-access-races": "node scripts/check-host-access-races.mjs && node scripts/check-host-access-redesign.mjs && node scripts/check-host-access-modal.mjs", "check:host-enrollment": "node scripts/check-host-enrollment-browser.mjs", "check:socket-identity": "node scripts/check-socket-identity.mjs", "check:ws-lifecycle": "node scripts/check-ws-lifecycle.mjs", + "check:login-persistence": "node scripts/check-login-persistence.mjs", "check:schedule-time": "node scripts/check-schedule-time.mjs", "check:config-health": "node scripts/check-config-health.mjs", "check:config-center-ui2": "node scripts/check-config-center-ui2.mjs", diff --git a/scripts/check-audit-verify.mjs b/scripts/check-audit-verify.mjs index 41700c26e..d79b1215b 100644 --- a/scripts/check-audit-verify.mjs +++ b/scripts/check-audit-verify.mjs @@ -71,25 +71,73 @@ async function renderState(state) { return html; } -// Valid chain with a permanent unsigned prefix: green verdict, and the -// prefix explained as history — the words "expected, not tampering" carry it. +const seg = (file, position, status, extra = {}) => ({ + file, position, status, total: 0, verified: 0, unsigned_prefix: 0, + first_bad: null, reason: null, error: null, ...extra, +}); + +// All retained files are shown, including one written before signing. { - const state = setupPage({ valid: true, availability: 'available', total: 500, verified: 420, unsigned_prefix: 80, first_bad: null }); + const state = setupPage({ + valid: true, availability: 'available', scope: 'retained_files', total: 600, verified: 420, + unsigned_prefix: 180, first_bad: null, first_bad_file: null, + segments: [ + seg('audit.jsonl', 0, 'verified', { total: 320, verified: 320 }), + seg('audit.jsonl.1', 1, 'verified', { total: 180, verified: 100, unsigned_prefix: 80 }), + seg('audit.jsonl.2', 2, 'unsigned', { total: 100, unsigned_prefix: 100 }), + ], + }); await state.verifyIntegrity(); const html = await renderState(state); - assert.match(html, /Chain valid — 420 signed entries verified/); - assert.match(html, /80 older entries predate signing/); + assert.match(html, /Chain valid — 420 signed entries verified across 3 retained files/); + assert.match(html, /180 older entries predate signing/); assert.match(html, /expected, not tampering/); + assert.match(html, /audit\.jsonl<\/span> \(current\): verified — 320 signed entries/); + assert.match(html, /audit\.jsonl\.1<\/span>: verified — 100 signed entries; 80 older entries predate signing/); + assert.match(html, /audit\.jsonl\.2<\/span>: no signatures — written before signing was enabled/); + assert.match(html, /cannot be detected/); assert.ok(!/Chain INVALID/.test(html)); } -// Broken chain arrives as a 409 with the verifier's structured verdict. +// Rotated break arrives as a 409 identifying the file and line. { - const state = setupPage({ valid: false, availability: 'available', total: 500, verified: 12, unsigned_prefix: 0, first_bad: 13, error: 'Line 13: HMAC verification failed' }, 409); + const state = setupPage({ + valid: false, availability: 'available', scope: 'retained_files', total: 500, verified: 412, + unsigned_prefix: 0, first_bad: 13, first_bad_file: 'audit.jsonl.1', + error: 'audit.jsonl.1: Line 13: HMAC verification failed (tampered or reordered)', + segments: [ + seg('audit.jsonl', 0, 'verified', { total: 400, verified: 400 }), + seg('audit.jsonl.1', 1, 'broken', { total: 13, verified: 12, first_bad: 13, reason: 'hmac_mismatch' }), + ], + }, 409); await state.verifyIntegrity(); assert.equal(state.verifyResult.value.valid, false); const html = await renderState(state); - assert.match(html, /Chain INVALID — first break at entry 13; 12 verified before it/); + assert.match(html, /Chain INVALID — problem in audit\.jsonl\.1 at line 13/); + assert.match(html, /break at line 13 — entry altered, reordered, or signed with a different key; 12 entries verified before it; later lines not checked/); + assert.match(html, /audit\.jsonl<\/span> \(current\): verified — 400 signed entries/); +} + +// Unreadable, missing and file-level signing gap are findings, never skipped. +{ + const state = setupPage({ + valid: false, availability: 'available', scope: 'retained_files', total: 3, verified: 2, + unsigned_prefix: 1, first_bad: null, first_bad_file: 'audit.jsonl.1', + error: 'audit.jsonl.1: no signatures although older files are signed', + segments: [ + seg('audit.jsonl', 0, 'verified', { total: 1, verified: 1 }), + seg('audit.jsonl.1', 1, 'broken', { total: 1, unsigned_prefix: 1, reason: 'signing_gap' }), + seg('audit.jsonl.2', 2, 'missing', { error: 'expected file not found' }), + seg('audit.jsonl.3', 3, 'unreadable', { error: 'PermissionError' }), + seg('audit.jsonl.4', 4, 'verified', { total: 1, verified: 1 }), + ], + }, 409); + await state.verifyIntegrity(); + const html = await renderState(state); + assert.match(html, /Chain INVALID — problem in audit\.jsonl\.1\./); + assert.match(html, /no signatures although older files are signed/); + assert.match(html, /audit\.jsonl\.2<\/span>: missing/); + assert.match(html, /audit\.jsonl\.3<\/span>: could not be read \(PermissionError\)/); } // Signing not enabled is a configuration fact, not an alarm. @@ -120,4 +168,4 @@ const auditSource = readFileSync(new URL('../ui/js/pages/audit.js', import.meta. assert.match(auditSource, /e\.data\.availability === 'not_enabled'/); assert.doesNotMatch(auditSource, /e\.data\.error\s*\?\s*\{[^}]*not_enabled/s); -console.log('audit-verify: all four verifier states and the honest-prefix copy pinned'); +console.log('audit-verify: every verifier state, per-file findings and honest-prefix copy pinned'); diff --git a/scripts/check-llm-config-refresh.mjs b/scripts/check-llm-config-refresh.mjs index cfd120745..0ab35799f 100644 --- a/scripts/check-llm-config-refresh.mjs +++ b/scripts/check-llm-config-refresh.mjs @@ -3,6 +3,8 @@ import fs from 'node:fs'; import vm from 'node:vm'; const source = fs.readFileSync('ui/js/pages/llm-config.js', 'utf8'); +// Run the same request-budget contract with both saved provider states. +for (const disabled of [false, true]) { const requests = []; const mounted = []; let timerCallback = null; @@ -14,9 +16,9 @@ const api = { if (path === '/api/llm/status') return { main_model: 'gpt-5.6-sol', codex: { enabled: true, model: 'gpt-5.6-sol' }, - ollama: { enabled: true, base_url: 'http://localhost:11434', model: 'local' }, + ollama: { enabled: !disabled, base_url: 'http://localhost:11434', model: 'local' }, openai_compatible: { - enabled: true, base_url: 'https://openrouter.ai/api/v1', model: 'vendor/model', + enabled: !disabled, base_url: 'https://openrouter.ai/api/v1', model: 'vendor/model', model_profiles: {}, openrouter: {}, }, }; @@ -53,6 +55,7 @@ const modules = new Map([ ['../confirm.js', synthetic('confirm', { async confirmDialog() { return false; } })], ['vue', synthetic('vue', { computed(fn) { return { get value() { return fn(); } }; }, + watch() {}, onActivated() {}, onDeactivated() {}, onMounted(fn) { mounted.push(fn); }, onUnmounted() {}, ref(value) { return { value }; }, })], @@ -76,20 +79,23 @@ await page.evaluate(); page.namespace.default.setup(); assert.equal(mounted.length, 1, 'LLM config must register one mount callback'); mounted[0](); -for (let attempt = 0; attempt < 20 && !requests.includes('/api/openrouter/catalogue'); attempt += 1) { +for (let attempt = 0; attempt < 20 && !requests.includes('/api/context/windows'); attempt += 1) { await new Promise(resolve => setTimeout(resolve, 0)); } assert.equal(typeof timerCallback, 'function', 'mount must arm the live-refresh timer'); assert.equal(timerInterval, 15000, 'live refresh must run no more often than every 15 seconds'); for (let attempt = 0; attempt < 3; attempt += 1) await new Promise(resolve => setTimeout(resolve, 0)); -for (const catalogue of ['/api/openrouter/catalogue', '/api/openai-compatible/models', '/api/ollama/models']) { +for (const catalogue of ['/api/openai-compatible/models', '/api/ollama/models']) { assert.equal(requests.filter(path => path === catalogue).length, 1, `${catalogue} must load once on mount`); } +assert.equal(requests.filter(path => path === '/api/openrouter/catalogue').length, disabled ? 0 : 1, + 'OpenRouter catalogue fetched on mount only when compatible is saved enabled'); requests.length = 0; await timerCallback(); assert.deepEqual(requests, [ '/api/llm/status', '/api/ollama/status', '/api/openai-compatible/status', '/api/agents/model', '/api/codex/status', '/api/context/windows', -], 'one live-refresh cycle must cost exactly six status/config requests'); -console.log('LLM config refresh request budget OK'); +], 'one live-refresh cycle must cost exactly six status/config requests, with no catalogue downloads'); +} +console.log('LLM config refresh request budget OK (enabled and disabled provider fixtures)'); diff --git a/scripts/check-llm-provider-visibility-browser.mjs b/scripts/check-llm-provider-visibility-browser.mjs new file mode 100644 index 000000000..4f56bc475 --- /dev/null +++ b/scripts/check-llm-provider-visibility-browser.mjs @@ -0,0 +1,294 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { chromium } from 'playwright-core'; +import { createServer } from 'vite'; + +// Real Vue DOM against an isolated API fixture; no live settings or server calls. +const server = await createServer({ configFile:false, root:process.cwd(), appType:'custom', + resolve:{alias:{vue:'vue/dist/vue.esm-bundler.js'}}, + define:{__VUE_OPTIONS_API__:'true',__VUE_PROD_DEVTOOLS__:'false',__VUE_PROD_HYDRATION_MISMATCH_DETAILS__:'false'}, + server:{host:'127.0.0.1',port:0,watch:null} }); +const html = `
`; +server.middlewares.use(async(req,res,next)=>{ + if(req.url!=='/__llm_visibility__.html')return next(); + res.setHeader('Content-Type','text/html');res.end(await server.transformIndexHtml(req.url,html)); +}); +const compat={enabled:false,configured:true,base_url:'https://openrouter.ai/api/v1',model:'vendor/alpha',model_profiles:{},openrouter:{}}; +const ollama={enabled:false,configured:true,base_url:'http://localhost:11434',model:'quartz'}; +const codex={enabled:true,configured:true,model:'gpt-6-sol',reasoning_effort:'high'}; +const entries=['compat:vendor/alpha',{model:'ollama:quartz',thinking_mode:'enabled'},'gpt-6-sol']; +const agents={model:'compat:vendor/alpha',auto_model_allowlist:entries,model_selection_hints:{'compat:vendor/alpha':'First for code'}}; +let main='compat:vendor/alpha'; +let failSave=false,holdCatalogue=null; +const writes=[]; +const catalog=()=>({ + codex:['gpt-6-astra','gpt-6-sol','gpt-6-luna','gpt-5.6-sol'].map(name=>({ref:name,name,provider:'codex',available:true,capability:'reasoning'})), + compat:[{ref:'compat:vendor/alpha',name:'vendor/alpha',provider:'compat',available:compat.enabled,unavailable_reason:compat.enabled?'':'disabled',capability:'reasoning',agent_available:true}], + ollama:[{ref:'ollama:quartz',name:'quartz',provider:'ollama',available:ollama.enabled,unavailable_reason:ollama.enabled?'':'disabled',capability:'none',agent_available:true}], +}); +const requests=[],errors=[],unexpected=[]; +let browser; +try{ + await server.listen(); + const executablePath=[process.env.CHROME_PATH,'/usr/bin/google-chrome','/usr/bin/chromium'].filter(Boolean).find(fs.existsSync); + assert.ok(executablePath,'Chromium is required'); + browser=await chromium.launch({executablePath,headless:true,args:['--no-sandbox']}); + const page=await browser.newPage();page.on('pageerror',e=>errors.push(e.message)); + await page.route('**/api/**',async route=>{ + const req=route.request(),path=new URL(req.url()).pathname; + requests.push(`${req.method()} ${path}`); + let body={}; + if(req.method()==='GET'){ + if(path==='/api/llm/status')body={main_model:main,codex:structuredClone(codex),openai_compatible:structuredClone(compat),ollama:structuredClone(ollama),auxiliary:{enabled:true,model:'ollama:quartz'},model_catalogue:catalog()}; + else if(path==='/api/ollama/status')body={configured:true,model:'quartz',health:{healthy:false}}; + else if(path==='/api/ollama/models')body={models:[{name:'quartz'}]}; + else if(path==='/api/openai-compatible/status')body={configured:true,model:'vendor/alpha',health:{healthy:false}}; + else if(path==='/api/openai-compatible/models')body={models:[{name:'vendor/alpha'}]}; + else if(path==='/api/agents/model')body=structuredClone(agents); + else if(path==='/api/codex/status')body={configured:true,accounts:[]}; + else if(path==='/api/context/windows')body={models:{}}; + else if(path==='/api/openrouter/catalogue'){ + if(holdCatalogue)await holdCatalogue; + body={models:[{id:'vendor/alpha',name:'Alpha Display Name',vendor:'vendor',supports_tools:true,supports_reasoning:true,supported_efforts:['high'],agent_eligible:true,variant:'standard',profile:{total_window_tokens:131072,max_output_tokens:8192}}]}; + } + else unexpected.push(`${req.method()} ${path}`); + }else if(req.method()==='PUT'){ + const payload=req.postDataJSON();writes.push({path,payload}); + if(failSave)body={error:'fixture save failure'}; + else if(path==='/api/openai-compatible/config')Object.assign(compat,payload); + else if(path==='/api/llm/ollama/config')Object.assign(ollama,payload); + else if(path==='/api/agents/model')Object.assign(agents,payload); + else if(path==='/api/llm/main-model')main=payload.model; + else unexpected.push(`${req.method()} ${path}`); + }else unexpected.push(`${req.method()} ${path}`); + await route.fulfill({status:failSave&&req.method()==='PUT'?503:200,contentType:'application/json',body:JSON.stringify(body)}); + }); + await page.goto(`http://127.0.0.1:${server.httpServer.address().port}/__llm_visibility__.html`); + await page.waitForFunction(()=>window.view&&!view.loading); + assert.deepEqual(errors,[],'component must render without runtime errors'); + const mainSelect=page.locator('label').filter({hasText:'Main model'}).first().locator('select').first(); + const agentSelect=page.locator('label').filter({hasText:'Agent model'}).first().locator('select').first(); + const auxSelect=page.locator('label').filter({hasText:'Auxiliary model'}).first().locator('select').first(); + const selectors=[mainSelect,agentSelect,auxSelect]; + assert.equal(await page.evaluate(()=>view.savedProviderEnabled('compat')),false); + async function checkDisabled(){ + for(const [selector,value] of [[mainSelect,'compat:vendor/alpha'],[agentSelect,'compat:vendor/alpha'],[auxSelect,'ollama:quartz']]){ + assert.equal(await selector.inputValue(),value,'saved selection survives disabling'); + const option=selector.locator(`option[value="${value}"]`); + assert.equal(await option.count(),1,'selected-disabled sentinel appears exactly once'); + assert.equal(await option.evaluate(el=>el.disabled),true,'selected sentinel itself is disabled'); + assert.match(await option.innerText(),/configured:.*provider disabled/i); + } + for(const [index,selector] of selectors.entries()){ + assert.equal(await selector.locator('optgroup[label="OpenAI-compatible"], optgroup[label="Ollama"]').count(),0); + assert.equal(await selector.locator('option[value="compat:vendor/alpha"]').count(),index===2?0:1); + assert.equal(await selector.locator('option[value="ollama:quartz"]').count(),index===2?1:0); + } + } + await checkDisabled(); + assert.equal(requests.filter(x=>x==='GET /api/openrouter/catalogue').length,0,'disabled compat must not fetch OpenRouter'); + await page.getByRole('button',{name:'Configure agent allowlist'}).click(); + const dialog=page.getByRole('dialog'); + assert.match(await dialog.innerText(),/excluded: provider disabled/i); + assert.match(await dialog.innerText(),/configured.*effective/i); + assert.deepEqual(await page.evaluate(()=>view.effectiveAllowlist),['compat:vendor/alpha','ollama:quartz','gpt-6-sol']); + await dialog.getByRole('button',{name:'Close'}).click(); + await page.evaluate(()=>{view.modelSelectorSearch='quartz';}); + await checkDisabled(); + assert.equal(await agentSelect.locator('option[value="auto"]').count(),1); + assert.equal(await agentSelect.locator('option[value=""]').count(),1); + assert.equal(await auxSelect.locator('option[value=""]').count(),1); + assert.equal(await page.getByRole('textbox',{name:'Search model catalogue'}).count(),0); + await page.evaluate(()=>{view.compatibleForm.enabled=true;view.ollamaForm.enabled=true;}); + await checkDisabled(); // unsaved drafts are not authoritative + // The plain Codex-only list must not disappear on an arbitrary stale search query. + assert.equal(await mainSelect.locator('optgroup').count(),0); + assert.equal(await mainSelect.locator('option[value="gpt-6-sol"]').count(),1); + assert.equal(await agentSelect.locator('option[value="gpt-6-astra"]').count(),1); + // Explicit successful save can change provider visibility, preserving unrelated advanced drafts. + await page.evaluate(()=>{view.compatibleForm.model_profiles={other:{supports_reasoning:true}};}); + await page.evaluate(async()=>await view.saveCompatibleConfig()); + await page.evaluate(()=>{view.modelSelectorSearch='';}); + assert.equal(compat.enabled,true); + assert.equal(await mainSelect.locator('option[value="compat:vendor/alpha"]').count(),1); + assert.deepEqual(await page.evaluate(()=>view.disableWarningRoles('compat')),['Main','Agent']); + await page.evaluate(()=>{view.agentsConfig.model='';}); + assert.deepEqual(await page.evaluate(()=>view.disableWarningRoles('compat')),['Main','Agent'], + 'an inherited Agent also depends on Main'); + await page.evaluate(()=>{view.agentsConfig.model='compat:vendor/alpha';}); + assert.deepEqual(await page.evaluate(()=>view.compatibleForm.model_profiles),{other:{supports_reasoning:true}}); + assert.equal(requests.filter(x=>x==='GET /api/openrouter/catalogue').length,1); + // A failed disable cannot hide saved-compatible choices; the operator's pending draft survives. + failSave=true; + await page.evaluate(()=>{view.compatibleForm.enabled=false;}); + const warning=page.getByRole('dialog',{name:'Disable OpenAI-compatible?'}); + const failedSave=page.evaluate(async()=>await view.saveCompatibleConfig()); + await warning.waitFor(); + assert.match(await warning.innerText(),/Main.*Agent/); + await warning.getByRole('button',{name:'Disable provider'}).click(); + await failedSave; + assert.equal(compat.enabled,true); + assert.equal(await mainSelect.locator('option[value="compat:vendor/alpha"]').count(),1); + assert.equal(await page.evaluate(()=>view.compatibleForm.enabled),false); + failSave=false; + const successfulDisable=page.evaluate(async()=>await view.saveCompatibleConfig()); + await warning.waitFor(); + await warning.getByRole('button',{name:'Disable provider'}).click(); + await successfulDisable; + await page.waitForFunction(()=>!view.savingCompatible); + assert.equal(compat.enabled,false); + await checkDisabled(); + // Empty allowlist catalogue and an unknown saved ref must not silently replace the fixed selection. + const originalEntries=structuredClone(agents.auto_model_allowlist); + agents.model='compat:future/model'; + agents.auto_model_allowlist=['compat:future/model']; + await page.evaluate(async()=>await view.fetchAll()); + assert.equal(await agentSelect.locator('option[value="compat:future/model"]').count(),1); + assert.equal(await agentSelect.inputValue(),'compat:future/model'); + assert.match(await agentSelect.locator('option[value="compat:future/model"]').innerText(),/configured:.*provider disabled/i); + assert.equal(await page.evaluate(()=>view.effectiveAutoAllowlistCount),0,'disabled-only Agent Auto has no effective choices'); + agents.model='auto'; + await page.evaluate(async()=>await view.fetchAll()); + assert.match(await page.getByText(/1 configured, 0 effective models/).first().innerText(),/1 configured, 0 effective models/); + compat.enabled=true; + await page.evaluate(async()=>await view.fetchLLMStatus()); + const autoWarning=await page.evaluate(()=>view.disableWarningRoles('compat')); + assert.ok(autoWarning.includes('Agent Auto (zero effective choices)'), + 'disabling the last Agent Auto provider warns before saving'); + assert.ok(!autoWarning.includes('Agent'),'Auto is not a fixed provider reference'); + compat.enabled=false; + await page.evaluate(async()=>await view.fetchLLMStatus()); + agents.model='compat:vendor/alpha';agents.auto_model_allowlist=originalEntries; + await page.evaluate(async()=>await view.fetchAll()); + // Enabling both providers by status (health remains unreachable) exposes their disabled options. + compat.enabled=true;ollama.enabled=true; + await page.evaluate(async()=>await view.fetchLLMStatus()); + await page.evaluate(()=>{view.modelSelectorSearch='';}); + await page.waitForFunction(()=>view.modelGroups.some(g=>g.id==='compat')&&view.modelGroups.some(g=>g.id==='ollama')); + for(const selector of selectors){ + assert.equal(await selector.locator('optgroup[label="OpenAI-compatible"]').count(),1); + assert.equal(await selector.locator('optgroup[label="Ollama"]').count(),1); + } + assert.equal(await mainSelect.locator('option[value="compat:vendor/alpha"]').count(),1); + assert.equal(await auxSelect.locator('option[value="ollama:quartz"]').count(),1); + assert.deepEqual(agents.auto_model_allowlist,entries); + // Saved provider is enabled but health is false: preserve current server-marked availability. + assert.equal(await mainSelect.locator('option[value="compat:vendor/alpha"]').evaluate(el=>el.disabled),false); + assert.equal(await page.evaluate(()=>view.savedProviderEnabled('compat')),true); + assert.equal(await page.evaluate(()=>view.savedProviderEnabled('ollama')),true); + await page.evaluate(async()=>await view.fetchAll()); + await page.waitForFunction(()=>view.openRouterCatalogue?.models?.length); + await page.evaluate(()=>{ + view.llmStatus.model_catalogue.compat[0].capability='none'; + view.llmStatus.model_catalogue.compat[0].efforts=['low']; + }); + for(const selector of selectors){ + assert.match(await selector.locator('option[value="compat:vendor/alpha"]').innerText(),/^Alpha Display Name/, + 'enabled provider retains the OpenRouter display name'); + } + assert.deepEqual(await page.evaluate(()=>{ + const model=view.modelCatalog.find(m=>m.ref==='compat:vendor/alpha'); + return [model.name,model.capability,model.efforts,model.agent_available]; + }),['Alpha Display Name','reasoning',['high'],true],'presentation metadata retains catalogue precedence'); + // Bare unknown Codex names remain Codex selections, not mistaken for provider IDs. + codex.enabled=false;agents.model='auto'; + await page.evaluate(async()=>await view.fetchAll()); + assert.equal(await agentSelect.inputValue(),'auto'); + assert.equal(await agentSelect.locator('option[value="auto"]').count(),1,'Auto has only its fixed option'); + assert.equal(await page.evaluate(()=>view.configuredDisabledSelection('agent')),false); + assert.equal(await page.getByText(/Agent model is configured on a disabled provider/).count(),0); + assert.ok(!(await page.evaluate(()=>view.disableWarningRoles('codex'))).includes('Agent'), + 'disabling Codex must not claim Agent Auto is a fixed Codex selection'); + agents.model='gpt-7-future'; + await page.evaluate(async()=>await view.fetchAll()); + const future=agentSelect.locator('option[value="gpt-7-future"]'); + assert.equal(await future.count(),1); + assert.match(await future.innerText(),/configured:.*provider disabled/i); + assert.equal(await future.evaluate(el=>el.disabled),true); + assert.equal(await agentSelect.locator('optgroup[label="Codex"]').count(),0); + codex.enabled=true;agents.model='compat:vendor/alpha'; + await page.evaluate(async()=>await view.fetchAll()); + // Health is separate from saved enablement: server-marked unreachable entries remain listed. + await page.evaluate(()=>{ + for(const provider of ['compat','ollama']){ + view.llmStatus.model_catalogue[provider][0].available=false; + view.llmStatus.model_catalogue[provider][0].unavailable_reason='unreachable'; + } + }); + assert.equal(await mainSelect.locator('optgroup[label="OpenAI-compatible"] option').count(),1); + assert.match(await mainSelect.locator('option[value="compat:vendor/alpha"]').innerText(),/unreachable/); + assert.equal(await mainSelect.locator('option[value="compat:vendor/alpha"]').evaluate(el=>el.disabled),true); + await page.waitForFunction(()=>view.openRouterCatalogue?.models?.length); + await page.evaluate(()=>{view.llmStatus.model_catalogue.compat[0].agent_available=false;}); + for(const selector of selectors){ + assert.match(await selector.locator('option[value="compat:vendor/alpha"]').innerText(),/^Alpha Display Name \(unreachable\)/, + 'display name survives a server-unavailable verdict'); + } + assert.deepEqual(await page.evaluate(()=>{ + const model=view.modelCatalog.find(m=>m.ref==='compat:vendor/alpha'); + return [model.available,model.unavailable_reason,model.agent_available,model.name,model.capability,model.efforts]; + }),[false,'unreachable',false,'Alpha Display Name','reasoning',['high']], + 'catalogue metadata cannot upgrade server availability or agent admission'); + // Cached OpenRouter metadata cannot give disabled compat an eligible model. + await page.evaluate(async()=>await view.fetchAll()); + assert.ok(await page.evaluate(()=>view.openRouterCatalogue?.models?.length)); + compat.enabled=false; + await page.evaluate(async()=>await view.fetchLLMStatus()); + assert.equal(await page.evaluate(()=>view.openRouterCatalogueActive),false); + assert.equal(await page.evaluate(()=>view.autoAllowlistModels.some(m=>m.provider==='compat')),false); + assert.equal(await page.evaluate(()=>view.openRouterResults.length),0); + assert.equal(await page.getByRole('dialog').count(),0); + assert.equal(await mainSelect.locator('option[value="compat:vendor/alpha"]').count(),1); + // A delayed response from the old endpoint is discarded after the saved endpoint changes. + compat.enabled=true; + await page.evaluate(async()=>await view.fetchLLMStatus()); + let release; + holdCatalogue=new Promise(resolve=>{release=resolve;}); + const pending=page.evaluate(async()=>await view.fetchAll()); + await page.waitForFunction(()=>view.openRouterCatalogueLoading); + compat.base_url='https://router.example.net/v1'; + await page.evaluate(async()=>await view.fetchLLMStatus()); + release();holdCatalogue=null; + await pending; + assert.equal(await page.evaluate(()=>view.openRouterCatalogue),null,'stale OpenRouter response discarded after endpoint change'); + assert.equal(await page.evaluate(()=>view.openRouterCatalogueActive),false); + assert.equal(await page.evaluate(()=>view.openRouterResults.length),0); + // Now race the same request against disabling rather than endpoint replacement. + compat.enabled=true;compat.base_url='https://openrouter.ai/api/v1'; + await page.evaluate(async()=>await view.fetchLLMStatus()); + let releaseDisabled; + holdCatalogue=new Promise(resolve=>{releaseDisabled=resolve;}); + const pendingDisable=page.evaluate(async()=>await view.fetchAll()); + await page.waitForFunction(()=>view.openRouterCatalogueLoading); + compat.enabled=false; + await page.evaluate(async()=>await view.fetchLLMStatus()); + releaseDisabled();holdCatalogue=null; + await pendingDisable; + assert.equal(await page.evaluate(()=>view.openRouterCatalogue),null,'stale response discarded after saved disable'); + assert.equal(await page.evaluate(()=>view.openRouterResults.length),0); + // An explicit Refresh updates saved provider visibility but cannot discard + // unsaved Agent model/allowlist/hints or bless them as the server snapshot. + await page.evaluate(()=>{view.agentsConfig.model='gpt-6-luna';view.agentsConfig.auto_model_allowlist=['gpt-6-luna'];view.agentsConfig.model_selection_hints={'gpt-6-luna':'Draft hint'};}); + agents.model='auto';agents.auto_model_allowlist=['gpt-6-sol']; + await page.evaluate(async()=>await view.fetchAll()); + assert.deepEqual(await page.evaluate(()=>({model:view.agentsConfig.model,allowlist:view.agentsConfig.auto_model_allowlist,hints:view.agentsConfig.model_selection_hints})), + {model:'gpt-6-luna',allowlist:['gpt-6-luna'],hints:{'gpt-6-luna':'Draft hint'}}); + // A Main save racing an explicit status refresh must keep the draft until + // the write settles. A failed write leaves the saved provider visible. + await page.evaluate(()=>{view.modelSelection.main='gpt-6-luna';}); + failSave=true; + const failedMain=page.evaluate(async()=>await view.saveMainModel()); + await page.evaluate(async()=>await view.fetchLLMStatus()); + assert.equal(await mainSelect.inputValue(),'gpt-6-luna'); + await failedMain; + assert.equal(await mainSelect.inputValue(),'gpt-6-luna'); + assert.equal(await page.evaluate(()=>view.savedProviderEnabled('codex')),true); + failSave=false; + assert.deepEqual(errors,[]);assert.deepEqual(unexpected,[]); + console.log('LLM provider visibility DOM check OK'); +}finally{await browser?.close();await server.close();} diff --git a/scripts/check-login-persistence.mjs b/scripts/check-login-persistence.mjs new file mode 100644 index 000000000..faafe7581 --- /dev/null +++ b/scripts/check-login-persistence.mjs @@ -0,0 +1,77 @@ +// Exercise the real API client against browser storage and an in-memory server. +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const source = readFileSync(join(here, '../ui/js/api.js'), 'utf8'); +const storage = (seed = {}) => { + const values = new Map(Object.entries(seed)); + return { + getItem: key => values.get(key) ?? null, + setItem: (key, value) => values.set(key, String(value)), + removeItem: key => values.delete(key), + entries: () => Object.fromEntries(values), + }; +}; +globalThis.window = globalThis; +globalThis.location = { protocol: 'http:', host: 'localhost:3002' }; +globalThis.document = { addEventListener() {}, removeEventListener() {} }; +globalThis.setInterval = () => 0; +globalThis.clearInterval = () => {}; +globalThis.localStorage = storage(); +globalThis.sessionStorage = storage(); +const { OdinAPI } = await import('data:text/javascript;base64,' + Buffer.from(`${source}\nexport { OdinAPI };`).toString('base64')); +const reload = () => new OdinAPI(); +const reset = (local = {}, session = {}) => { + globalThis.localStorage = storage(local); + globalThis.sessionStorage = storage(session); +}; +const old = { odin_persist: '1', odin_token: 'synthetic-old', odin_session_timeout: '42' }; + +reset(old); +let api = reload(); +api.setPersist(false); +api.setToken('synthetic-new', 60); +assert.deepEqual(localStorage.entries(), {}); +assert.deepEqual(sessionStorage.entries(), { odin_token: 'synthetic-new', odin_session_timeout: '60' }); +assert.equal(reload().token, 'synthetic-new'); +assert.equal(reload().sessionTimeout, 60); + +reset({}, { odin_token: 'synthetic-old', odin_session_timeout: '42' }); +api = reload(); +api.setPersist(true); +api.setToken('synthetic-new'); +assert.deepEqual(sessionStorage.entries(), {}); +assert.deepEqual(localStorage.entries(), { odin_token: 'synthetic-new', odin_persist: '1' }); +assert.equal(reload().token, 'synthetic-new'); + +const valid = new Set(); +globalThis.fetch = async (path, opts = {}) => { + if (path === '/api/auth/login') { + valid.add('synthetic-new-session'); + return { status: 200, ok: true, json: async () => ({ session_id: 'synthetic-new-session', timeout_seconds: 42 }) }; + } + const bearer = (opts.headers?.Authorization || '').replace('Bearer ', ''); + if (!valid.has(bearer)) return { status: 401, ok: false, json: async () => ({}) }; + return { status: 200, ok: true, json: async () => ({ status: 'online' }) }; +}; +reset(old); +api = reload(); +assert.equal((await api.check()).needsAuth, true); +api.setPersist(false); +await api.login('synthetic-login'); +assert.equal(reload().token, 'synthetic-new-session'); +assert.equal((await reload().check()).ok, true); +await reload().logout(); +assert.deepEqual(localStorage.entries(), {}); +assert.deepEqual(sessionStorage.entries(), {}); + +reset(old); +api = reload(); +api.setPersist(true); +api.setToken('synthetic-new'); +assert.deepEqual(localStorage.entries(), { odin_persist: '1', odin_token: 'synthetic-new' }); +assert.equal(reload().token, 'synthetic-new'); +console.log('login-persistence: 13 assertions passed'); diff --git a/scripts/incus-deploy.sh b/scripts/incus-deploy.sh index 17a355bba..2e6f4c525 100755 --- a/scripts/incus-deploy.sh +++ b/scripts/incus-deploy.sh @@ -147,8 +147,6 @@ echo " incus exec $INSTANCE -- systemctl start odin" echo "" echo "View logs:" echo " incus exec $INSTANCE -- journalctl -u odin -f" -echo " # or" -echo " incus exec $INSTANCE -- tail -f /app/data/logs/odin.log" echo "" echo "Update config:" echo " incus file push config.yml $INSTANCE/app/config.yml" diff --git a/scripts/monitor.sh b/scripts/monitor.sh index 62d9aabe6..97cd2ae41 100755 --- a/scripts/monitor.sh +++ b/scripts/monitor.sh @@ -6,6 +6,9 @@ # Supports Docker, Incus, and bare metal deployments. # Auto-detects deployment type, or set ODIN_DEPLOY=docker|incus|local # For Incus: set ODIN_INCUS_INSTANCE (default: odin) +# Odin logs to standard output/error, not a file: the systemd journal for a +# service (journalctl -u odin), docker logs for Docker, or a source-run terminal. +# Set ODIN_LOG_FILE to tail a file you explicitly redirect that output to. set -e @@ -74,17 +77,27 @@ bot_logs() { ;; incus) INSTANCE="${ODIN_INCUS_INSTANCE:-odin}" - incus exec "$INSTANCE" -- tail -"$COUNT" /app/data/logs/odin.log 2>/dev/null || \ - incus exec "$INSTANCE" -- journalctl -u odin -n "$COUNT" --no-pager 2>/dev/null || \ - echo "Could not read logs from Incus instance '$INSTANCE'" + if [ -n "${ODIN_LOG_FILE:-}" ]; then + incus exec "$INSTANCE" -- tail -"$COUNT" "$ODIN_LOG_FILE" + else + incus exec "$INSTANCE" -- journalctl -u odin -n "$COUNT" --no-pager + fi ;; local) - LOG_FILE="${ODIN_LOG_FILE:-$SCRIPT_DIR/data/logs/odin.log}" - if [ -f "$LOG_FILE" ]; then - tail -"$COUNT" "$LOG_FILE" + if [ -n "${ODIN_LOG_FILE:-}" ]; then + if [ -f "$ODIN_LOG_FILE" ]; then + tail -"$COUNT" "$ODIN_LOG_FILE" + else + echo "Log file not found: $ODIN_LOG_FILE" + echo "Set ODIN_LOG_FILE to the correct log path" + fi + elif command -v journalctl >/dev/null 2>&1 && systemctl cat odin.service >/dev/null 2>&1; then + journalctl -u odin -n "$COUNT" --no-pager else - echo "Log file not found: $LOG_FILE" - echo "Set ODIN_LOG_FILE to the correct log path" + echo "Odin logs to standard output/error, not to a file." + echo "For a source run, check the terminal where Odin was started." + echo "For a service, use journalctl -u odin; for Docker, use docker logs odin-bot." + echo "Set ODIN_LOG_FILE to a file you redirected stdout/stderr to." fi ;; esac diff --git a/site/.vitepress/config.mts b/site/.vitepress/config.mts index 106c77fdd..d4553d341 100644 --- a/site/.vitepress/config.mts +++ b/site/.vitepress/config.mts @@ -42,6 +42,7 @@ export default withMermaid(defineConfig({ { text: 'Guide', items: [ { text: 'Install', link: '/install' }, { text: 'Configuration', link: '/configuration' }, + { text: 'Schedules & webhooks', link: '/scheduling' }, { text: 'Security model', link: '/security' }, { text: 'Runtime skills', link: '/skills' }, ]}, diff --git a/src/agents/manager.py b/src/agents/manager.py index a61a3909a..681a55dd7 100644 --- a/src/agents/manager.py +++ b/src/agents/manager.py @@ -380,7 +380,6 @@ class AgentInfo: messages: list[dict] = field(default_factory=list) tools_used: list[str] = field(default_factory=list) tool_execution_count: int = 0 - _tool_call_ids: set[str] = field(default_factory=set, repr=False) iteration_count: int = 0 last_activity: float = field(default_factory=time.time) phase: str = "ready" @@ -1487,7 +1486,7 @@ def _check_lifetime() -> bool: # Transition READY → EXECUTING for LLM call agent.transition(AgentState.EXECUTING, f"iteration {iteration + 1}") - agent._tool_call_ids.update(settled_call_ids(agent.messages)) + settled_call_ids(agent.messages) agent.set_phase( "generating", time.time() + min(agent.iteration_timeout, _remaining_lifetime(agent)) ) @@ -1555,9 +1554,7 @@ def _check_lifetime() -> bool: usage_response = {**response, **usage_facts} text = content_text(response.get("text", "")) - tool_calls = normalize_tool_calls( - response.get("tool_calls", []), used_ids=agent._tool_call_ids - ) + tool_calls = normalize_tool_calls(response.get("tool_calls", [])) context_density, context_density_source, context_primary_chars = _budget_observation( generation_state ) @@ -2277,6 +2274,8 @@ async def _call_llm_with_recovery( # missing) ladder is a real terminal outcome and must not # silently widen through the unknown-model fallback. compatible_target = _compatible_overflow_target_chars(exc, plan) + if compatible_target is not None and compatible_target >= attempt_chars: + compatible_target = None active_ladder = ( (compatible_target,) if compatible_target is not None diff --git a/src/async_utils.py b/src/async_utils.py index 7c5c2814d..59dd4d43f 100644 --- a/src/async_utils.py +++ b/src/async_utils.py @@ -2,9 +2,14 @@ from __future__ import annotations import asyncio +import functools +from collections.abc import Callable +from typing import Any, TypeVar from .odin_log import get_logger +_T = TypeVar("_T") + _log = get_logger("async_utils") @@ -22,3 +27,53 @@ def _done_cb(t: asyncio.Task) -> None: task.add_done_callback(_done_cb) return task + + +async def _settle_executor_call(call: Callable[[], Any]) -> tuple[asyncio.Future, bool]: + """Run ``call`` on the default executor and wait until it has physically finished. + + The wait survives any number of caller cancellations: each one is recorded + and the executor future is awaited again through ``asyncio.shield``. An + executor future is not a ``Task``, so the shutdown drain that cancels + ``asyncio.all_tasks()`` cannot cancel it either. + Returns ``(done_future, was_cancelled)``. + """ + loop = asyncio.get_running_loop() + fut = loop.run_in_executor(None, call) + was_cancelled = False + while not fut.done(): + try: + await asyncio.shield(fut) + except asyncio.CancelledError: + was_cancelled = True + except Exception: + break # worker raised; fut.done() is now True + return fut, was_cancelled + + +async def run_persist_settled( + persist_sync: Callable[[], Any], +) -> tuple[BaseException | None, bool]: + """Run a sync write to settlement; never raise. Returns ``(exc, was_cancelled)``. + + The caller commits or restores its own state, then re-raises + cancellation when ``was_cancelled`` is true. + """ + fut, was_cancelled = await _settle_executor_call(persist_sync) + return fut.exception(), was_cancelled + + +async def to_thread_settled(func: Callable[..., _T], /, *args: Any, **kwargs: Any) -> _T: + """``asyncio.to_thread`` for work done while an ``asyncio.Lock`` is held. + + Cancelling ``asyncio.to_thread`` releases the caller's lock while the + worker keeps running. This keeps the caller (and so the lock) until the + worker has finished, then re-raises the cancellation. Without a + cancellation it returns the result or raises the worker's exception, + exactly like ``asyncio.to_thread``. + """ + fut, was_cancelled = await _settle_executor_call(functools.partial(func, *args, **kwargs)) + if was_cancelled: + fut.exception() # retrieve, so a worker error is never "never retrieved" + raise asyncio.CancelledError + return fut.result() diff --git a/src/audit/logger.py b/src/audit/logger.py index 9598abe31..c7a79f528 100644 --- a/src/audit/logger.py +++ b/src/audit/logger.py @@ -6,7 +6,7 @@ from collections.abc import AsyncIterator, Callable from datetime import UTC, datetime from pathlib import Path -from typing import BinaryIO, Literal +from typing import BinaryIO, Literal, cast import aiofiles @@ -15,7 +15,7 @@ from ..observability.failure_classes import classify_failure from ..odin_log import get_logger from ..permissions.persistence import write_private_atomic -from .signer import GENESIS_HASH, AuditSigner, verify_log +from .signer import GENESIS_HASH, AuditSigner, verify_log, verify_segment log = get_logger("audit") @@ -256,9 +256,8 @@ def _maybe_rotate(self) -> None: each append. Bounds total growth to roughly max_bytes * (max_files + 1). The HMAC chain (if enabled) starts fresh in the new current file — the signer's prev-hash is reset to GENESIS after - rotation, so verify_integrity() (which reads the current file from - genesis) stays valid instead of chaining across the rotation boundary - and failing on the first post-rotation entry.""" + rotation, so each file verifies independently from genesis instead + of chaining across the rotation boundary.""" try: if not self.path.exists() or self.path.stat().st_size < self._max_bytes: return @@ -951,12 +950,62 @@ async def initialize_chain(self) -> None: ) self._chain_initialized = True - async def verify_integrity(self) -> dict: - """Verify the HMAC chain of the audit log. + async def _open_verify_snapshot(self) -> list[dict]: + """Open bounded descriptors of every retained generation under the append lock. - Returns a dict with ``valid``, ``total``, ``verified``, ``first_bad``, - and ``error`` fields. Requires signing to be enabled. + Unlike search, verification reports unreadable positions and interior + gaps. Scan outside the lock; descriptor identity survives rotation. """ + rows: list[dict] = [] + seen: set[tuple[int, int]] = set() + try: + async with self._persist_lock: + present: list[int] = [] + for index in range(self._max_files + 1): + path = self.path if index == 0 else self.path.with_name( + self.path.name + f".{index}") + row = {"file": path.name, "position": index, "handle": None, "size": 0, + "open_error": None, "absent": False} + try: + handle = open(path, "rb") + except FileNotFoundError: + row["absent"] = True + rows.append(row) + continue + except OSError as exc: + row["open_error"] = type(exc).__name__ + rows.append(row) + present.append(index) + continue + try: + stat = os.fstat(handle.fileno()) + except OSError as exc: + handle.close() + row["open_error"] = type(exc).__name__ + rows.append(row) + present.append(index) + continue + identity = (stat.st_dev, stat.st_ino) + if identity in seen: + handle.close() + continue + seen.add(identity) + row.update(handle=handle, size=stat.st_size) + rows.append(row) + present.append(index) + highest = max((index for index in present if index > 0), default=0) + return [ + row for row in rows + if not row["absent"] or row["position"] == 0 or row["position"] < highest + ] + except BaseException: + for row in rows: + if row["handle"] is not None: + cast(BinaryIO, row["handle"]).close() + raise + + async def verify_integrity(self) -> dict: + """Check each retained file's independent HMAC chain without blocking appends.""" if not self._signer: return { "valid": False, @@ -964,16 +1013,71 @@ async def verify_integrity(self) -> dict: "verified": 0, "unsigned_prefix": 0, "first_bad": None, + "first_bad_file": None, "availability": "not_enabled", "error": "Signing not enabled (no hmac_key configured)", + "segments": [], } - result = await verify_log(self.path, self._signer._key.decode()) - # Availability is distinct from verdict. A configured verifier that - # returns valid=False is a failure even when its diagnostic has an - # error string; only the explicit not_enabled shape is soft copy. - result["availability"] = "available" - result["durability"] = ( - "repair_required" if self.repair_required - else "degraded" if self.durability_degraded or not result["valid"] else "durable" - ) - return result + rows = await self._open_verify_snapshot() + segments: list[dict] = [] + try: + for row in rows: + seg = {"file": row["file"], "position": row["position"], "total": 0, + "verified": 0, "unsigned_prefix": 0, "first_bad": None, + "reason": None, "error": None} + if row["absent"]: + if row["position"] == 0: + seg["status"] = "verified" # No active entries yet. + else: + seg.update(status="missing", error="expected file not found") + elif row["open_error"]: + seg.update(status="unreadable", error=row["open_error"]) + else: + try: + result = await asyncio.to_thread( + verify_segment, row["handle"], row["size"], self._signer._key) + except OSError as exc: + seg.update(status="unreadable", error=type(exc).__name__) + else: + for field in ("total", "verified", "unsigned_prefix", + "first_bad", "reason", "error"): + seg[field] = result[field] + seg["status"] = ( + "broken" if not result["valid"] + else "unsigned" if result["verified"] == 0 and result["total"] > 0 + else "verified" + ) + segments.append(seg) + finally: + for row in rows: + if row["handle"] is not None: + row["handle"].close() + # Files are independent chains, but a wholly unsigned generation + # newer than a signed one is still a signing gap. + signed_seen = False + for seg in reversed(segments): + if seg["status"] == "unsigned" and signed_seen: + seg.update(status="broken", reason="signing_gap", + error="no signatures although older files are signed") + if seg["verified"] > 0: + signed_seen = True + problems = [seg for seg in segments if seg["status"] in ("broken", "unreadable", "missing")] + first = problems[0] if problems else None + active = segments[0] if segments else {"status": "verified"} + return { + "valid": not problems, + "availability": "available", + "scope": "retained_files", + "total": sum(seg["total"] for seg in segments), + "verified": sum(seg["verified"] for seg in segments), + "unsigned_prefix": sum(seg["unsigned_prefix"] for seg in segments), + "first_bad": first["first_bad"] if first else None, + "first_bad_file": first["file"] if first else None, + "error": f"{first['file']}: {first['error']}" if first else None, + "durability": ( + "repair_required" if self.repair_required + else "degraded" if self.durability_degraded or active["status"] == "broken" + else "durable" + ), + "segments": segments, + } diff --git a/src/audit/signer.py b/src/audit/signer.py index e087fcece..2b79c1aff 100644 --- a/src/audit/signer.py +++ b/src/audit/signer.py @@ -19,7 +19,7 @@ class AuditSigner: file from top to bottom and checks that every link in the chain is valid. """ - def __init__(self, key: str) -> None: + def __init__(self, key: str | bytes) -> None: self._key = key.encode() if isinstance(key, str) else key self._prev_hmac: str = GENESIS_HASH @@ -70,6 +70,74 @@ def _canonical(entry: dict) -> str: return json.dumps(filtered, sort_keys=True, default=str, separators=(",", ":")) +REASON_INVALID_JSON = "invalid_json" +REASON_NON_OBJECT = "non_object" +REASON_UNSIGNED_AFTER_SIGNED = "unsigned_after_signed" +REASON_HMAC_MISMATCH = "hmac_mismatch" + + +def verify_segment(handle, size: int, key: str | bytes) -> dict: + """Verify a single bounded file snapshot from GENESIS, one line at a time. + + The bound is captured while no append is in flight, so a later append is + neither scanned nor mistaken for a torn audit entry. Physical line numbers + include blank lines. This synchronous function belongs in a worker thread. + """ + signer = AuditSigner(key) + prev = GENESIS_HASH + total = verified = unsigned_prefix = 0 + remaining = size + lineno = 0 + + def failure(line: int, reason: str, message: str) -> dict: + return { + "valid": False, "total": total, "verified": verified, + "unsigned_prefix": unsigned_prefix, "first_bad": line, + "reason": reason, "error": f"Line {line}: {message}", + } + + while remaining > 0: + raw = handle.readline(remaining) + if not raw: + break + remaining -= len(raw) + lineno += 1 + if not raw.strip(): + continue + total += 1 + try: + entry = json.loads(raw) + except ValueError: # Includes invalid UTF-8 from bytes input. + return failure(lineno, REASON_INVALID_JSON, "invalid JSON") + if not isinstance(entry, dict): + return failure(lineno, REASON_NON_OBJECT, "non-object audit entry") + if "_hmac" not in entry: + # verified == 0 ⟺ still in the pre-enablement prefix. + if verified == 0: + unsigned_prefix += 1 + continue + return failure( + lineno, REASON_UNSIGNED_AFTER_SIGNED, + "missing _hmac field (unsigned entry after chain began)", + ) + try: + valid = signer.verify_entry(entry, prev) + except (TypeError, ValueError): + valid = False + if not valid: + return failure( + lineno, REASON_HMAC_MISMATCH, + "HMAC verification failed (tampered or reordered)", + ) + prev = entry["_hmac"] + verified += 1 + return { + "valid": True, "total": total, "verified": verified, + "unsigned_prefix": unsigned_prefix, "first_bad": None, + "reason": None, "error": None, + } + + async def verify_log(path, key: str) -> dict: """Verify the full HMAC chain of an audit log file. @@ -86,10 +154,10 @@ async def verify_log(path, key: str) -> dict: (signed entries verified, int), ``unsigned_prefix`` (int), ``first_bad`` (int or None — 1-indexed line number), and ``error`` (str or None). """ + import asyncio + import os from pathlib import Path - import aiofiles - p = Path(path) if not p.exists(): return { @@ -101,88 +169,16 @@ async def verify_log(path, key: str) -> dict: "error": None, } - signer = AuditSigner(key) - prev = GENESIS_HASH - total = 0 - verified = 0 - unsigned_prefix = 0 + def _verify_file() -> dict: + with open(p, "rb") as handle: + return verify_segment(handle, os.fstat(handle.fileno()).st_size, key) try: - async with aiofiles.open(p) as f: - lines = await f.readlines() + result = await asyncio.to_thread(_verify_file) except Exception as exc: return { - "valid": False, - "total": 0, - "verified": 0, - "unsigned_prefix": 0, - "first_bad": None, - "error": str(exc), + "valid": False, "total": 0, "verified": 0, + "unsigned_prefix": 0, "first_bad": None, "error": str(exc), } - - for i, line in enumerate(lines, 1): - line = line.strip() - if not line: - continue - total += 1 - - try: - entry = json.loads(line) - except json.JSONDecodeError: - return { - "valid": False, - "total": total, - "verified": verified, - "unsigned_prefix": unsigned_prefix, - "first_bad": i, - "error": f"Line {i}: invalid JSON", - } - - if not isinstance(entry, dict): - return { - "valid": False, "total": total, "verified": verified, - "unsigned_prefix": unsigned_prefix, "first_bad": i, - "error": f"Line {i}: non-object audit entry", - } - - if "_hmac" not in entry: - # verified == 0 ⟺ no signed entry seen yet (a signed entry that - # fails returns immediately), i.e. still in the pre-enablement - # prefix. - if verified == 0: - unsigned_prefix += 1 - continue - return { - "valid": False, - "total": total, - "verified": verified, - "unsigned_prefix": unsigned_prefix, - "first_bad": i, - "error": f"Line {i}: missing _hmac field (unsigned entry after chain began)", - } - - try: - valid = signer.verify_entry(entry, prev) - except (TypeError, ValueError): - valid = False - if not valid: - return { - "valid": False, - "total": total, - "verified": verified, - "unsigned_prefix": unsigned_prefix, - "first_bad": i, - "error": f"Line {i}: HMAC verification failed (tampered or reordered)", - } - - prev = entry["_hmac"] - verified += 1 - - return { - "valid": True, - "total": total, - "verified": verified, - "unsigned_prefix": unsigned_prefix, - "first_bad": None, - "error": None, - } + result.pop("reason", None) + return result diff --git a/src/computer/controller.py b/src/computer/controller.py index db75b953b..9304d5272 100644 --- a/src/computer/controller.py +++ b/src/computer/controller.py @@ -1148,6 +1148,21 @@ def _input_status(self, live, grant): } async def session(self, context: RequestContext, inp: dict) -> dict: + """Attach non-dispatch evidence only before lifecycle, cleanup or input steps.""" + from .error_guidance import InputBoundaryError + + boundary: dict[str, bool | str] = {"dispatched": False, "state": "unstarted"} + try: + return await self._session(context, inp, boundary) + except ComputerError as exc: + if boundary["dispatched"] or isinstance(exc, InputBoundaryError): + raise + raise InputBoundaryError( + exc.code, execution={"injected": False, "sent": False}, + state=str(boundary["state"]), + ) from exc + + async def _session(self, context: RequestContext, inp: dict, boundary) -> dict: exact_keys( inp, { @@ -1197,7 +1212,13 @@ async def session(self, context: RequestContext, inp: dict) -> dict: "supported_next_step": "start", } try: - result = await inventory() + from .runtime.hyprland_discovery import HyprlandDiscoveryError + + try: + result = await inventory() + except HyprlandDiscoveryError as exc: + # Discovery rejected a request, not a session or input transition. + raise ComputerError(exc.code) from None await self._auth(context) if ( type(result) is not dict @@ -1322,6 +1343,7 @@ async def session(self, context: RequestContext, inp: dict) -> dict: selected, selection_proof = self._selection_binding( context, selected, with_proof=True ) + boundary["dispatched"] = True grant = self.store.create_session( context, app, @@ -1467,15 +1489,18 @@ async def session(self, context: RequestContext, inp: dict) -> dict: grant = self._grant( context, inp, same_turn=False, generation=operation in {"resume", "export", "reconcile"} ) + boundary["state"] = grant.state if operation == "status": return self._public_session(grant) if operation in {"stop", "cancel", "close"}: if grant.state in {"cancelled", "closed"} and grant.session_id not in self._live: return self._public_session(grant) + boundary["dispatched"] = True return await self._stop( grant.session_id, "closed" if operation == "close" else "cancelled" ) if operation == "pause": + boundary["dispatched"] = True return await self._pause(grant.session_id) if operation == "resume": if grant.state != "paused" or grant.session_id not in self._live: @@ -1505,6 +1530,8 @@ async def session(self, context: RequestContext, inp: dict) -> dict: if grant.platform == "wayland" else STOP_TIMEOUT_SECONDS ) + # Re-arming input has its own rollback/stop path below. + boundary["dispatched"] = True try: await _bounded(resume(consent_generation=grant.consent_generation), timeout) measured = getattr(live.backend, "capabilities", None) @@ -1572,6 +1599,8 @@ async def session(self, context: RequestContext, inp: dict) -> dict: raise return self._public_session(self.store.get_session(grant.session_id)) if operation == "reconcile": + # Owner reconciliation can release input; observe owns its own boundary. + boundary["dispatched"] = True if grant.state in {"quarantined", "closed"} and grant.session_id not in self._live: return await self.reconcile_hyprland_owner( context, grant.session_id, grant.generation) @@ -1629,7 +1658,8 @@ async def _pause(self, sid): return await self._stop(sid, "cancelled") return self._public_session(self.store.get_session(sid)) - async def _capture(self, grant, *, acknowledge_modal=False, crop=None, strict_binding=False): + async def _capture(self, grant, *, acknowledge_modal=False, crop=None, strict_binding=False, + boundary=None): from .vision import FrameCrop, FrameMetadata, _validate_png live = self._active(grant) @@ -1649,6 +1679,9 @@ async def _capture(self, grant, *, acknowledge_modal=False, crop=None, strict_bi live.observations.clear() self._delivered_observations.pop(grant.session_id, None) if self._hyprland_continuity_failure(live, exc): + if boundary is not None: + # Quarantine cleanup may release input. + boundary["dispatched"] = True await self._quarantine_hyprland(grant, live, phase="native_continuity_lost") if self.store.get_session(grant.session_id).state == "active": raise ComputerError("hyprland_recovered_fresh_observation_required") from None @@ -1753,6 +1786,21 @@ async def _capture(self, grant, *, acknowledge_modal=False, crop=None, strict_bi return obs, raw.image_bytes async def observe(self, context, inp): + """Attach non-dispatch evidence only before recovery or cleanup can act.""" + from .error_guidance import InputBoundaryError + + boundary: dict[str, bool | str] = {"dispatched": False, "state": "unstarted"} + try: + return await self._observe(context, inp, boundary) + except ComputerError as exc: + if boundary["dispatched"] or isinstance(exc, InputBoundaryError): + raise + raise InputBoundaryError( + exc.code, execution={"injected": False, "sent": False}, + state=str(boundary["state"]), + ) from exc + + async def _observe(self, context, inp, boundary): exact_keys( inp, {"session_id", "generation", "source_id", "crop", "task_context"}, @@ -1764,6 +1812,7 @@ async def observe(self, context, inp): crop = crop_arguments(crop) await self._auth(context) grant = self._grant(context, inp) + boundary["state"] = grant.state async with self._actions: live = self._active(grant) if (hints is not None and live.capabilities is not None @@ -1793,7 +1842,7 @@ async def observe(self, context, inp): self._active(grant) await self._auth(context) try: - obs, image = await self._capture(grant, crop=crop) + obs, image = await self._capture(grant, crop=crop, boundary=boundary) except ComputerError as exc: # A requested observation can be the first proof that a human # changed focus or the scoped native geometry. With no action @@ -1801,21 +1850,24 @@ async def observe(self, context, inp): # candidate and produce entirely fresh pixels. It never adopts # a different output/application or resumes input. live = self._live.get(grant.session_id) - if ( + recover = ( grant.environment == "existing_session" and live is not None and exc.code in {"stale_source_binding", "input_focus_unavailable"} and self._no_input_pending(grant.session_id) - and await self._recover_focus( - context, - grant, - live, - getattr(live.backend, "application_provenance", None), - ) + ) + if recover and live is not None and self._recovery_enabled(live): + # Native focus recovery may send input; do not claim otherwise. + boundary["dispatched"] = True + if recover and live is not None and await self._recover_focus( + context, + grant, + live, + getattr(live.backend, "application_provenance", None), ): await self._auth(context) self._active(grant) - obs, image = await self._capture(grant, crop=crop) + obs, image = await self._capture(grant, crop=crop, boundary=boundary) else: raise await self._auth(context) diff --git a/src/computer/error_guidance.py b/src/computer/error_guidance.py index 0043935ee..98b1588c7 100644 --- a/src/computer/error_guidance.py +++ b/src/computer/error_guidance.py @@ -91,6 +91,15 @@ def __init__(self, code: str, *, execution: dict, state: str): "unsupported_operation", "target_inventory_unavailable", "inventory_targets_unsupported", + "hyprland_discovery_deadline", + "hyprland_discovery_runtime_untrusted", + "hyprland_discovery_policy_invalid", + "hyprland_discovery_runtime_unavailable", + "hyprland_discovery_candidate_limit", + "hyprland_discovery_hint_invalid", + "hyprland_discovery_not_found", + "hyprland_discovery_ambiguous", + "hyprland_discovery_unavailable", "operation_unavailable", "focus_preflight_refused", "focus_candidate_unavailable", @@ -108,6 +117,20 @@ def __init__(self, code: str, *, execution: dict, state: str): _AUDIT_REASON_CODES = frozenset( { "computer_rejected", + "computer_succeeded", + "verified", + "executed", + "capture_source_not_granted", + "capture_not_active", + "hyprland_discovery_deadline", + "hyprland_discovery_runtime_untrusted", + "hyprland_discovery_policy_invalid", + "hyprland_discovery_runtime_unavailable", + "hyprland_discovery_candidate_limit", + "hyprland_discovery_hint_invalid", + "hyprland_discovery_not_found", + "hyprland_discovery_ambiguous", + "hyprland_discovery_unavailable", "computer_not_satisfied", "outcome_unknown", "permission_denied", @@ -852,6 +875,8 @@ def safety_terminal(result: dict) -> bool: or "state" in item and item["state"] not in ( + # No session/grant resolved yet; no held input possible. + "unstarted", "starting", "active", "paused", @@ -955,6 +980,23 @@ def audit_reason_code(reason: object) -> str: return "computer_rejected" +# Receipt statuses the foreground tool loop publishes as failed calls. +FAILED_RECEIPT_STATUSES = frozenset( + {"unavailable", "not_satisfied", "rejected", "failed", "unknown", "interrupted"} +) + + +def audit_outcome_code(result: object, *, succeeded: bool) -> str: + """Return fixed audit code for a result without labelling successes rejections.""" + result = result if isinstance(result, dict) else {} + evidence = result.get("verification") + evidence = evidence if isinstance(evidence, dict) else {} + value = result.get("reason") or evidence.get("reason") or result.get("status") + if succeeded and not (isinstance(value, str) and value in _AUDIT_REASON_CODES): + return "computer_succeeded" + return audit_reason_code(value) + + def failure_guidance(result: dict, *, terminal: bool = False) -> dict: """Keep native reasons and receipts intact, adding a conservative summary.""" evidence = result.get("verification") diff --git a/src/computer/integration.py b/src/computer/integration.py index 413b5a2eb..56e995818 100644 --- a/src/computer/integration.py +++ b/src/computer/integration.py @@ -16,6 +16,8 @@ from ..tools.output_authorization import tool_scope_allows from ..tools.result_validator import ToolResult from .error_guidance import ( + FAILED_RECEIPT_STATUSES, + audit_outcome_code, audit_reason_code, exception_reason, failure_guidance, @@ -304,11 +306,9 @@ async def _tool(self, name, values): image["__computer_audit_metadata__"] = { "computer_call_id": grant.call_id, "computer_turn_id": grant.context.turn_id, - "computer_reason_code": audit_reason_code( - receipt.get("reason") or ( - receipt.get("verification", {}).get("reason") - if isinstance(receipt.get("verification"), dict) else None - ) or receipt.get("status") + "computer_reason_code": audit_outcome_code( + receipt, + succeeded=receipt.get("status") not in FAILED_RECEIPT_STATUSES, ), "computer_input_outcome": input_outcome(receipt), } @@ -341,13 +341,7 @@ async def _tool(self, name, values): "failed", } rejected = rejected and not capability_refusal - safe_reason = audit_reason_code( - (result.get("reason") or ( - result.get("verification", {}).get("reason") - if isinstance(result.get("verification"), dict) else None - ) or result.get("status")) - if isinstance(result, dict) else None - ) + safe_reason = audit_outcome_code(result, succeeded=not (unknown or rejected)) return ToolResult( json.dumps(result, ensure_ascii=True, separators=(",", ":")), ok=not (unknown or rejected), diff --git a/src/computer/runtime/x11_attached.py b/src/computer/runtime/x11_attached.py index f118d522a..5a2b53c99 100644 --- a/src/computer/runtime/x11_attached.py +++ b/src/computer/runtime/x11_attached.py @@ -918,9 +918,9 @@ def _select_source(self, source_id): async def select_source(self, source_id): async with self._lock: if self._closed or self._paused or not self._started: - raise AttachedFailure("capture_not_active") + raise ComputerError("capture_not_active") if source_id not in self._sources: - raise AttachedFailure("capture_source_not_granted") + raise ComputerError("capture_source_not_granted") self._select_source(source_id) return {"selected_source": source_id, "capture_only": not self._input_enabled} @@ -1706,7 +1706,7 @@ async def revoke(): await revoke() async def export(self, name): - raise AttachedFailure("existing_session_export_not_granted") + raise ComputerError("existing_session_export_not_granted") async def pause(self): if self._pause_job is None or self._pause_job.done(): diff --git a/src/discord/channel_logger.py b/src/discord/channel_logger.py index 65b672861..c4aa8a2df 100644 --- a/src/discord/channel_logger.py +++ b/src/discord/channel_logger.py @@ -201,7 +201,9 @@ def _index_batch(self, path: Path, cursor: str | None) -> list[dict]: return self._index_batch(path, None) return batch - def search(self, query: str, limit: int = 20, channel_id: str | None = None) -> list[dict]: + def search(self, query: str, limit: int = 20, channel_id: str | None = None, + *, author_id: str | None = None, + accept=None) -> list[dict]: """Keyword search on JSONL files (fallback when FTS is unavailable). Returns dicts with content, author, channel_id, timestamp, type="channel". @@ -230,6 +232,10 @@ def search(self, query: str, limit: int = 20, channel_id: str | None = None) -> record = json.loads(line) except json.JSONDecodeError: continue + if author_id and str(record.get("author_id", "")) != author_id: + continue + if accept is not None and not accept(record): + continue content = record.get("content", "") if query_lower in content.lower(): results.append({ diff --git a/src/discord/channel_state.py b/src/discord/channel_state.py index 80978c0d6..66637b4d3 100644 --- a/src/discord/channel_state.py +++ b/src/discord/channel_state.py @@ -334,6 +334,8 @@ def track_action( result_preview: str, elapsed_ms: int, channel_id: str | None = None, + *, + failed: bool = False, ) -> None: """Record a tool execution for conversational context injection. @@ -354,7 +356,7 @@ def track_action( inp_summary = ", ".join(f"{k}={v}" for k, v in safe_input.items() if isinstance(v, str)) if len(inp_summary) > 100: inp_summary = inp_summary[:100] + "..." - status = "OK" if "error" not in result_preview.lower()[:50] else "ERROR" + status = "ERROR" if failed or "error" in result_preview.lower()[:50] else "OK" entry = f"- [{ts}] `{tool_name}`({inp_summary}) → {status} ({elapsed_ms}ms)" self.track_recent_action(channel_id, entry) diff --git a/src/discord/client.py b/src/discord/client.py index 621a37129..5ee133139 100644 --- a/src/discord/client.py +++ b/src/discord/client.py @@ -316,11 +316,15 @@ async def start_application(self) -> None: self.scheduled_events._on_scheduled_task, self.scheduled_events._on_schedule_failure, ) + if getattr(self, "_knowledge_store", None) and getattr(self, "_fts_index", None): + fire_and_forget(self._reconcile_knowledge_fts(), name="reconcile_knowledge_fts") try: await self.computer.start() except Exception: log.exception("Computer startup failed; desktop tools remain unavailable") + if getattr(self, "_vector_store", None): + fire_and_forget(self._backfill_archives(), name="backfill_archives") self._application_started = True async def close(self) -> None: @@ -455,8 +459,6 @@ async def on_ready(self, *, expected_generation: int | None = None) -> None: self.scheduled_events._on_scheduled_task, self.scheduled_events._on_schedule_failure, ) - if self._vector_store: - fire_and_forget(self._backfill_archives(), name="backfill_archives") await self.delivery.set_status(None, task_end=True) async def on_guild_join(self, guild: discord.Guild) -> None: @@ -530,22 +532,32 @@ async def _reconcile_application_commands(self, guilds=None) -> None: async def _backfill_archives(self) -> None: """Backfill semantic search index and FTS5 with existing archive files.""" try: + vector_store = getattr(self, "_vector_store", None) + embedder = getattr(self, "_embedder", None) + if vector_store is None or embedder is None: + return archive_dir = self.sessions.persist_dir / "archive" - count = await self._vector_store.backfill(archive_dir, self._embedder) # type: ignore[union-attr, arg-type] # built together under search.enabled + count = await vector_store.backfill(archive_dir, embedder) + if hasattr(vector_store, "backfill_segments"): + segment_count = await vector_store.backfill_segments(archive_dir, embedder) + if segment_count: + log.info("Backfilled segments for %d archives", segment_count) if count: log.info("Backfilled %d archive sessions into vector store", count) else: log.info("Vector store up to date") - # Backfill knowledge FTS from existing data - if self._knowledge_store and self._fts_index: - import asyncio - - kb_count = await asyncio.to_thread(self._knowledge_store.backfill_fts) - if kb_count: - log.info("Backfilled %d knowledge chunks into FTS index", kb_count) except Exception as e: log.error("Archive backfill failed: %s", e) + async def _reconcile_knowledge_fts(self) -> None: + """Reconcile knowledge FTS even on API-only installs.""" + try: + count = await self._knowledge_store.backfill_fts_async() # type: ignore[union-attr] + if count: + log.info("Backfilled %d knowledge chunks into FTS index", count) + except Exception: + log.exception("Knowledge FTS reconciliation failed") + async def on_message(self, message: discord.Message) -> None: """Intake gating chain — owned by intake_pipeline.MessageIntake.""" await self.intake.handle(message) diff --git a/src/discord/delivery.py b/src/discord/delivery.py index 82a996651..1a0067b78 100644 --- a/src/discord/delivery.py +++ b/src/discord/delivery.py @@ -225,6 +225,17 @@ def _close_generated_fallback_file(file: discord.File) -> None: } +def close_open_fence(text: str) -> str: + """Close a code block that a cut left open in a Discord-only copy. + + Uses the chunker's fence rule (a line starting with three backticks + toggles a block); text with every block closed comes back unchanged. + """ + if sum(1 for line in text.split("\n") if line.startswith("```")) % 2: + return text + "\n```" + return text + + class ResponseDelivery: STATUS_DEBOUNCE: float = 5.0 @@ -410,6 +421,9 @@ async def send_chunked(self, message, text: str) -> None: current = "" in_code_block = False code_block_lang = "" + # Where the open block's own fence line starts in ``current`` while + # that block has no content in this chunk yet; None otherwise. + opener_at: int | None = None # Pre-split any lines longer than the chunk limit so the chunker # never encounters a single line that can't fit in one chunk. @@ -422,23 +436,53 @@ async def send_chunked(self, message, text: str) -> None: lines.append(raw_line) for line in lines: - # Track code block state (toggle on ``` lines) - if line.startswith("```"): + is_fence = line.startswith("```") + # Decide the split with the fence state BEFORE this line: the + # outgoing chunk must be closed/reopened for the block it is in. + if len(current) + len(line) + 1 > DISCORD_MAX_LEN - 10: + if ( + in_code_block + and is_fence + and len(current) + len(line) + 1 <= DISCORD_MAX_LEN + ): + # The closing fence fits the reserve kept for closing: + # it ends this chunk instead of opening the next one. + chunks.append(current + line + "\n") + current = "" + in_code_block = False + code_block_lang = "" + opener_at = None + continue + if in_code_block and opener_at is not None: + # The block has no content here yet: move its opening + # fence to the next chunk instead of sending an empty block. + head, current = current[:opener_at], current[opener_at:] + if head.strip(): + chunks.append(head) + else: + if in_code_block: + current += "\n```" + if current.strip(): + chunks.append(current) + current = "" + if in_code_block: + current = f"```{code_block_lang}\n" + if in_code_block and len(current) + len(line) + 1 + 4 > DISCORD_MAX_LEN: + # A long language tag would push this chunk past the + # limit once closed: continue the block without it. + current = "```\n" + opener_at = None + if is_fence: if in_code_block: in_code_block = False code_block_lang = "" + opener_at = None else: in_code_block = True code_block_lang = line[3:].strip() - - if len(current) + len(line) + 1 > DISCORD_MAX_LEN - 10: - if in_code_block: - current += "\n```" - if current.strip(): - chunks.append(current) - current = "" - if in_code_block: - current = f"```{code_block_lang}\n" + opener_at = len(current) + elif in_code_block: + opener_at = None current += line + "\n" if current.strip(): diff --git a/src/discord/llm_gateway.py b/src/discord/llm_gateway.py index c78623387..db169c1fb 100644 --- a/src/discord/llm_gateway.py +++ b/src/discord/llm_gateway.py @@ -626,18 +626,9 @@ async def run_persist_settled(self, persist_sync): or exactly-restores, then re-raises cancellation, once state is coherent. """ - loop = asyncio.get_running_loop() - fut = loop.run_in_executor(None, persist_sync) - was_cancelled = False - while not fut.done(): - try: - await asyncio.shield(fut) - except asyncio.CancelledError: - was_cancelled = True - except Exception: - break # worker raised; fut.done() is now True - exc = fut.exception() - return exc, was_cancelled + from ..async_utils import run_persist_settled + + return await run_persist_settled(persist_sync) def _snapshot_aux_config(self) -> dict: aux_cfg = self.get_config().openai_codex.auxiliary diff --git a/src/discord/scheduled_events.py b/src/discord/scheduled_events.py index 1feec487d..399df0a91 100644 --- a/src/discord/scheduled_events.py +++ b/src/discord/scheduled_events.py @@ -24,6 +24,7 @@ from ..odin_log import get_logger from ..scheduler.scheduler import NonRetryableScheduleError from ..tools import ToolResult +from .delivery import close_open_fence from .mcp_dispatch import uncertain_outcome as mcp_uncertain_outcome from .response_guards import scrub_response_secrets from .tool_loop import _LoopMessageProxy @@ -81,7 +82,7 @@ async def _on_scheduled_digest(self, schedule: dict) -> None: log.info("Running daily digest for channel %s", channel_id) try: - raw = await self._format_digest_raw() + raw, failed, total = await self._format_digest_raw(schedule, channel) except Exception as e: log.error("Digest data collection failed: %s", e) try: @@ -97,6 +98,15 @@ async def _on_scheduled_digest(self, schedule: dict) -> None: ) from send_error raise RuntimeError(f"Digest data collection failed: {e}") from e + if total and len(failed) == total: + await channel.send( + scrub_response_secrets( + "**Daily Infrastructure Digest**\n\n" + f"Collection failed for every check ({total} of {total}).\n\n{raw[:1500]}" + ) + ) + raise RuntimeError(f"Digest collected no data: all {total} checks failed") + # Summarize the digest — prefer Codex (free), fall back to raw truncation digest_messages = [ { @@ -119,24 +129,39 @@ async def _on_scheduled_digest(self, schedule: dict) -> None: log.warning("Digest summary failed, using raw: %s", e) summary = raw[:3000] + if failed: + labels = ", ".join(failed[:10]) + if len(failed) > 10: + labels += ", …" + summary += f"\n\nCollection failed for {len(failed)} of {total} checks: {labels}" + await channel.send(scrub_response_secrets(f"**Daily Infrastructure Digest**\n\n{summary}")) # Audit log the digest - await self._audit.log_execution( - user_id="system", - user_name="scheduler", - channel_id=channel_id, - tool_name="digest", - tool_input={"schedule_id": schedule.get("id")}, - approved=True, - result_summary=summary, - execution_time_ms=0, - ) + try: + await self._audit.log_execution( + user_id="system", + user_name="scheduler", + channel_id=channel_id, + tool_name="digest", + tool_input={"schedule_id": schedule.get("id")}, + approved=True, + result_summary=summary, + execution_time_ms=0, + ) + except Exception: + # Discord delivery already succeeded. Raising here would make the + # scheduler retry and deliver the same digest a second time. + log.exception("Failed to audit delivered scheduled digest") - async def _format_digest_raw(self) -> str: + async def _format_digest_raw( + self, schedule: dict, channel: discord.abc.Messageable + ) -> tuple[str, list[str], int]: """Collect raw infrastructure data for the digest.""" tasks = [] labels = [] + req_id = schedule.get("requester_id") or None + req_name = schedule.get("requester") or schedule.get("created_by") or "scheduler" # Disk + memory checks on all hosts via run_command aliases = ( @@ -146,33 +171,48 @@ async def _format_digest_raw(self) -> str: ) for host_alias in aliases: tasks.append( - self._tool_executor.execute( + self._execute_scheduled_tool( "run_command", { "host": host_alias, "command": "df -h --exclude-type=tmpfs --exclude-type=devtmpfs", }, + channel, + req_id, + req_name, ) ) labels.append(f"Disk ({host_alias})") tasks.append( - self._tool_executor.execute( + self._execute_scheduled_tool( "run_command", {"host": host_alias, "command": "free -h"}, + channel, + req_id, + req_name, ) ) labels.append(f"Memory ({host_alias})") + if not labels: + raise RuntimeError("No configured hosts available for digest checks") + results = await asyncio.gather(*tasks, return_exceptions=True) sections = [] + failed = [] for label, result in zip(labels, results): - if isinstance(result, Exception): - sections.append(f"### {label}\nERROR: {result}") + if isinstance(result, (Exception, asyncio.CancelledError)): + failed.append(label) + sections.append(f"### {label}\nCollection failed: {str(result)[:300]}") + elif isinstance(result, ToolResult) and not result.ok: + failed.append(label) + sections.append(f"### {label}\nCollection failed: {str(result)[:300]}") else: - sections.append(f"### {label}\n{str(result)[:800]}") + output = result.output if isinstance(result, ToolResult) else str(result) + sections.append(f"### {label}\n{output[:800]}") - return "\n\n".join(sections) + return "\n\n".join(sections), failed, len(labels) def _resolve_mentions(self, text: str) -> str: """Replace @username with proper Discord <@ID> mentions.""" @@ -369,7 +409,7 @@ async def _run_scheduled_workflow( summary = "\n".join(results) text = f"**Workflow: {desc}**\n{summary}" if len(text) > 1900: - text = text[:1900] + "\n... (truncated)" + text = close_open_fence(text[:1900]) + "\n... (truncated)" try: await channel.send(scrub_response_secrets(text)) diff --git a/src/discord/tool_loop.py b/src/discord/tool_loop.py index 626757166..29dc885ba 100644 --- a/src/discord/tool_loop.py +++ b/src/discord/tool_loop.py @@ -2968,6 +2968,7 @@ async def _run_one_tool_captured(self, st: _ChatTurn, block) -> dict: result[:200], elapsed_ms, channel_id=str(st.message.channel.id), + failed=error is not None, ) except Exception: pass # Non-critical tracking @@ -3163,10 +3164,12 @@ async def _run_one_tool_with_timeout( # WI-3 (interrupted): wait_for cancelled _run_one_tool before its # own settle. The persisted effect class decides whether this is a # definite non-effect failure or an unknown external outcome. + settle_error = None try: await st.durability.after_tool_interrupted(block, error_msg) - except Exception: + except Exception as exc: log.exception("Ledger settle failed for timed-out %s", block.name) + settle_error = exc try: await self._audit.log_execution( user_id=str(st.message.author.id), @@ -3183,6 +3186,8 @@ async def _run_one_tool_with_timeout( ) except Exception: pass + if settle_error is not None: + raise settle_error return { "type": "tool_result", "tool_use_id": block.id, diff --git a/src/discord/turn_resume.py b/src/discord/turn_resume.py index 8d3d29b88..0aeedbbe1 100644 --- a/src/discord/turn_resume.py +++ b/src/discord/turn_resume.py @@ -508,7 +508,8 @@ async def _validate_and_rebuild(self, key: TurnKey, row: dict): tools = self._tool_catalog.merged_definitions() tools = self._permissions.filter_tools(str(original.author.id), tools) self._repair_unmatched_tool_use( - fields["messages"], row.get("operations") or [] + fields["messages"], row.get("operations") or [], + generation_seq=payload["generation_seq"], ) except Exception: log.exception("Checkpoint reconstruction failed — rejecting") @@ -562,16 +563,17 @@ async def _validate_and_rebuild(self, key: TurnKey, row: dict): return st, original, None @staticmethod - def _repair_unmatched_tool_use(messages: list, operations: list[dict]) -> None: + def _repair_unmatched_tool_use( + messages: list, operations: list[dict], *, generation_seq: int + ) -> None: """Guarantee matched tool_use/tool_result blocks after a crash. Missing results are synthesized from the ledger: APPLIED replays the stored result; anything else states the truth (unknown / never ran). Nothing is re-executed. """ - seen_results: set[str] = set() - use_blocks: dict[str, str] = {} - for msg in messages: + open_uses: list[tuple[int, str]] = [] + for message_index, msg in enumerate(messages): content = msg.get("content") if not isinstance(content, list): continue @@ -579,27 +581,39 @@ def _repair_unmatched_tool_use(messages: list, operations: list[dict]) -> None: if not isinstance(block, dict): continue if block.get("type") == "tool_use" and block.get("id"): - use_blocks[block["id"]] = block.get("name", "tool") + open_uses.append((message_index, block["id"])) elif block.get("type") == "tool_result" and block.get("tool_use_id"): - seen_results.add(block["tool_use_id"]) - missing = [cid for cid in use_blocks if cid not in seen_results] - if not missing: + for position, (_index, cid) in enumerate(open_uses): + if cid == block["tool_use_id"]: + open_uses.pop(position) + break + if not open_uses: return - ops_by_id = {op["tool_call_id"]: op for op in operations} + if any(index != len(messages) - 1 for index, _cid in open_uses): + raise ValueError("unmatched tool_use outside the checkpoint's final message") + ops_by_id = {} + for op in operations: + identity = (op["generation_seq"], op["tool_call_id"]) + if identity in ops_by_id: + # A dict-comprehension silently selected the last ledger row. + # Duplicate durable identities are corrupt/ambiguous; never + # guess which outcome belongs to this transcript block. + raise ValueError(f"duplicate operation identity: {identity!r}") + ops_by_id[identity] = op repaired = [] - for cid in missing: - op = ops_by_id.get(cid) - if op is not None and op["state"] in ( + for _index, cid in open_uses: + matched_op = ops_by_id.get((generation_seq, cid)) + if matched_op is not None and matched_op["state"] in ( OpState.APPLIED, OpState.RECONCILED_APPLIED, ): - content = op.get("result") or "[completed; result recorded]" - elif op is None: + content = matched_op.get("result") or "[completed; result recorded]" + elif matched_op is None: content = ( "[Interrupted before execution — this call never ran; " "re-issue it if still needed.]" ) - elif op.get("effect_class") == ToolEffectClass.EFFECT_FREE_OBSERVATION: + elif matched_op.get("effect_class") == ToolEffectClass.EFFECT_FREE_OBSERVATION: content = ( "[Interrupted observation — no external effect was left " "unresolved; repeat the observation if it is still needed.]" diff --git a/src/health/server.py b/src/health/server.py index f72807f82..328dc2c11 100644 --- a/src/health/server.py +++ b/src/health/server.py @@ -1551,6 +1551,7 @@ async def _webhook_grafana(self, request: web.Request) -> web.Response: event_data: dict = { "event": "alert", "alert_name": alert_name, + "alert_names": [a.alert_name for a in parsed_alerts] or [alert_name], "alert_count": len(parsed_alerts), "firing_count": sum(1 for a in parsed_alerts if a.status == "firing"), "resolved_count": sum(1 for a in parsed_alerts if a.status == "resolved"), diff --git a/src/knowledge/store.py b/src/knowledge/store.py index 1329063ad..5ea3ef441 100644 --- a/src/knowledge/store.py +++ b/src/knowledge/store.py @@ -14,6 +14,7 @@ from datetime import UTC, datetime from typing import TYPE_CHECKING, Literal +from ..async_utils import to_thread_settled from ..odin_log import get_logger from ..search.errors import SearchExecutionError, validate_search_query from ..search.hybrid import reciprocal_rank_fusion @@ -300,9 +301,10 @@ async def _ingest_locked( return outcome # Existing rows remain searchable until replacement is verified. - old_content = await asyncio.to_thread(self.get_source_content, source) - is_update = old_content is not None - indexed = await asyncio.to_thread( + old_content = await to_thread_settled(self.get_source_content, source) + action = "update" if old_content is not None else "create" + diff_summary = self._make_diff_summary(old_content, content) + indexed = await to_thread_settled( self._write_chunks_sync, chunks, vectors, @@ -311,23 +313,9 @@ async def _ingest_locked( now, uploader, doc_content_hash, + version=(content, action, diff_summary), ) - # A version record is a success claim, not a pre-write intention. - if indexed == len(chunks): - action = "update" if is_update else "create" - diff_summary = self._make_diff_summary(old_content, content) - await asyncio.to_thread( - self._record_version, - source, - doc_content_hash, - content, - indexed, - uploader, - action, - diff_summary, - ) - log.info("Ingested '%s': %d/%d chunks indexed", source, indexed, len(chunks)) if indexed == len(chunks): return IngestOutcome(indexed, INGEST_STORED) @@ -342,21 +330,26 @@ async def _dedup_outcome( log_non_durable: bool = True, ) -> IngestOutcome | None: """Check exact and near duplicates while the caller holds the lock.""" - existing = await asyncio.to_thread(self._find_by_doc_hash, doc_content_hash) + existing = await to_thread_settled(self._find_by_doc_hash, doc_content_hash) if existing: existing_source, count = existing - if existing_source == source and await asyncio.to_thread( + same_durable = existing_source == source and await to_thread_settled( self.source_is_durable, source, count, - ): + ) + if same_durable and await to_thread_settled( + self.get_source_snapshot, source, + ) is not None: log.info( "Skipping ingest of '%s': content unchanged (hash=%s)", source, doc_content_hash[:12], ) return IngestOutcome(count, INGEST_UNCHANGED, source) - if existing_source != source and await asyncio.to_thread( + if same_durable: + log.warning("Re-ingesting '%s': missing version snapshot; repairing it", source) + elif existing_source != source and await to_thread_settled( self.source_is_durable, existing_source, count, @@ -374,16 +367,16 @@ async def _dedup_outcome( existing_source, source, ) - elif log_non_durable: + elif log_non_durable and not same_durable: log.warning( "Ignoring non-durable duplicate source '%s' while re-ingesting it", existing_source, ) hashes = [self._content_hash(chunk) for chunk in chunks] - near_dup = await asyncio.to_thread(self._find_near_duplicate, hashes, source) + near_dup = await to_thread_settled(self._find_near_duplicate, hashes, source) if near_dup: - if await asyncio.to_thread(self.source_is_durable, near_dup[0]): + if await to_thread_settled(self.source_is_durable, near_dup[0]): log.warning( "Skipping ingest of '%s': %.0f%% chunk overlap with existing source '%s'", source, @@ -408,55 +401,54 @@ def _write_chunks_sync( now: str, uploader: str, doc_content_hash: str = "", + *, + version: tuple[str, str, str] | None = None, ) -> int: - """Install a complete DB/FTS document before retiring its old rows.""" + """Publish one source with chunks, vectors, snapshot and FTS as a unit.""" conn = self._conn assert conn is not None - old_ids = { - str(row[0]) + before_rows = [ + (str(row[0]), str(row[1]), int(row[2])) for row in conn.execute( - "SELECT chunk_id FROM knowledge_chunks WHERE source = ?", + "SELECT chunk_id, content, chunk_index FROM knowledge_chunks WHERE source = ?", (source,), ).fetchall() - } + ] + old_ids = {row[0] for row in before_rows} desired_rows: list[tuple[str, str, int]] = [] - all_writes_ok = True - for i, chunk in enumerate(chunks): - # Legacy IDs are retained wherever they belong to this source. - # A short source-hash collision must never REPLACE another owner's - # DB, FTS or vector row, even for restore and dedup=False imports. - base_id = f"{doc_hash}_{i}_{doc_content_hash[:12]}" - chunk_id = base_id - suffix = hashlib.sha256(source.encode()).hexdigest() - fallback_id = f"{base_id}_{suffix}" - # Once a source has a fallback ID, keep that identity even if - # another source later releases the original short ID. - prior = conn.execute( - "SELECT source FROM knowledge_chunks WHERE chunk_id = ?", - (fallback_id,), - ).fetchone() - if prior is not None and prior[0] == source: - chunk_id = fallback_id - owner = conn.execute( - "SELECT source FROM knowledge_chunks WHERE chunk_id = ?", - (chunk_id,), - ).fetchone() - if owner is not None and owner[0] != source: - chunk_id = fallback_id - counter = 0 - while True: - owner = conn.execute( - "SELECT source FROM knowledge_chunks WHERE chunk_id = ?", - (chunk_id,), - ).fetchone() - if owner is None or owner[0] == source: - break - counter += 1 - chunk_id = f"{base_id}_{suffix}_{counter}" - log.warning("Chunk ID collision for '%s', using source-specific ID", source) - desired_rows.append((chunk_id, chunk, i)) - chunk_hash = self._content_hash(chunk) - try: + fts_touched = False + try: + for i, chunk in enumerate(chunks): + # Legacy IDs are retained wherever they belong to this source. + # A short source-hash collision must never REPLACE another owner's + # DB, FTS or vector row, even for restore and dedup=False imports. + base_id = f"{doc_hash}_{i}_{doc_content_hash[:12]}" + chunk_id = base_id + suffix = hashlib.sha256(source.encode()).hexdigest() + fallback_id = f"{base_id}_{suffix}" + # Once a source has a fallback ID, keep that identity even if + # another source later releases the original short ID. + prior = conn.execute( + "SELECT source FROM knowledge_chunks WHERE chunk_id = ?", (fallback_id,), + ).fetchone() + if prior is not None and prior[0] == source: + chunk_id = fallback_id + owner = conn.execute( + "SELECT source FROM knowledge_chunks WHERE chunk_id = ?", (chunk_id,), + ).fetchone() + if owner is not None and owner[0] != source: + chunk_id = fallback_id + counter = 0 + while True: + owner = conn.execute( + "SELECT source FROM knowledge_chunks WHERE chunk_id = ?", (chunk_id,), + ).fetchone() + if owner is None or owner[0] == source: + break + counter += 1 + chunk_id = f"{base_id}_{suffix}_{counter}" + log.warning("Chunk ID collision for '%s', using source-specific ID", source) + desired_rows.append((chunk_id, chunk, i)) conn.execute( "INSERT OR REPLACE INTO knowledge_chunks " "(chunk_id, content, source, chunk_index, total_chunks, " @@ -470,18 +462,10 @@ def _write_chunks_sync( len(chunks), uploader, now, - chunk_hash, + self._content_hash(chunk), doc_content_hash, ), ) - if self._fts and not self._fts.index_knowledge_chunk( - chunk_id, - chunk, - source, - i, - ): - all_writes_ok = False - log.error("Failed to index chunk %d of '%s' in FTS", i, source) if vectors[i] is not None: vec_bytes = serialize_vector(vectors[i]) # type: ignore[arg-type] conn.execute( @@ -492,40 +476,11 @@ def _write_chunks_sync( "INSERT INTO knowledge_vec (chunk_id, embedding) VALUES (?, ?)", (chunk_id, vec_bytes), ) - except Exception as exc: - all_writes_ok = False - log.error("Failed to index chunk %d of '%s': %s", i, source, exc) - try: - if all_writes_ok: - conn.commit() - else: - conn.rollback() - except Exception as exc: - conn.rollback() - log.error("Failed to commit replacement for '%s': %s", source, exc) - return 0 - - if not all_writes_ok or not self._source_contains_rows(source, desired_rows): - log.error("Failed durable DB/FTS verification for '%s' after ingest", source) - return 0 - - desired_ids = {row[0] for row in desired_rows} - obsolete_ids = old_ids - desired_ids - if obsolete_ids: - obsolete_fts_ids: set[str] = set() - if self._fts: - obsolete_fts_ids = { - chunk_id for chunk_id in obsolete_ids if self._fts.has_knowledge_chunk(chunk_id) - } - if obsolete_fts_ids: - removed_fts = self._fts.delete_knowledge_chunks(obsolete_fts_ids) - if removed_fts != len(obsolete_fts_ids) or any( - self._fts.has_knowledge_chunk(chunk_id) for chunk_id in obsolete_fts_ids - ): - log.error("Failed to retire old FTS rows for '%s'", source) - return 0 - placeholders = ",".join("?" for _ in obsolete_ids) - try: + elif self._has_vec: + conn.execute("DELETE FROM knowledge_vec WHERE chunk_id = ?", (chunk_id,)) + obsolete_ids = old_ids - {row[0] for row in desired_rows} + if obsolete_ids: + placeholders = ",".join("?" for _ in obsolete_ids) if self._has_vec: conn.execute( f"DELETE FROM knowledge_vec WHERE chunk_id IN ({placeholders})", @@ -535,11 +490,54 @@ def _write_chunks_sync( f"DELETE FROM knowledge_chunks WHERE chunk_id IN ({placeholders})", tuple(obsolete_ids), ) - conn.commit() - except Exception as exc: + if version is not None: + content, action, diff_summary = version + self._insert_version_row( + source, doc_content_hash, content, len(chunks), uploader, action, diff_summary, + ) + staged = sorted( + (str(row[0]), str(row[1]), int(row[2])) + for row in conn.execute( + "SELECT chunk_id, content, chunk_index FROM knowledge_chunks WHERE source = ?", + (source,), + ).fetchall() + ) + if staged != sorted(desired_rows): + raise RuntimeError("staged DB rows do not match the replacement") + if self._has_vec: + expected_vector_ids = { + desired_rows[i][0] for i, vector in enumerate(vectors) if vector is not None + } + vector_ids = { + str(row[0]) + for row in conn.execute( + "SELECT v.chunk_id FROM knowledge_vec v " + "JOIN knowledge_chunks c ON c.chunk_id = v.chunk_id " + "WHERE c.source = ?", (source,), + ).fetchall() + } + if vector_ids != expected_vector_ids: + raise RuntimeError("staged vectors do not match the replacement") + if self._fts is not None: + if not self._fts.available: + raise RuntimeError("FTS store is unavailable") + if not self._fts.replace_knowledge_source(source, desired_rows): + raise RuntimeError("FTS replacement failed") + fts_touched = True + fts_rows = self._fts.get_knowledge_source_rows(source) + if fts_rows is None or sorted(fts_rows) != sorted(desired_rows): + raise RuntimeError("FTS rows do not match the replacement") + conn.commit() + except Exception as exc: + try: conn.rollback() - log.error("Failed to retire old DB rows for '%s': %s", source, exc) - return 0 + except Exception as rollback_exc: + log.error("Rollback failed for '%s': %s", source, rollback_exc) + if fts_touched and self._fts is not None: + if not self._fts.replace_knowledge_source(source, before_rows): + log.error("FTS compensation failed for '%s'", source) + log.error("Failed to publish replacement for '%s': %s", source, exc) + return 0 if not self.source_is_durable( source, @@ -548,51 +546,8 @@ def _write_chunks_sync( ): log.error("Final durable DB/FTS verification failed for '%s'", source) return 0 - if self._has_vec: - expected_vector_ids = { - desired_rows[i][0] for i, vector in enumerate(vectors) if vector is not None - } - vector_ids = { - str(row[0]) - for row in conn.execute( - "SELECT v.chunk_id FROM knowledge_vec v " - "JOIN knowledge_chunks c ON c.chunk_id = v.chunk_id " - "WHERE c.source = ?", - (source,), - ).fetchall() - } - if vector_ids != expected_vector_ids: - log.error("Final vector verification failed for '%s'", source) - return 0 return len(chunks) - def _source_contains_rows( - self, - source: str, - expected_rows: list[tuple[str, str, int]], - ) -> bool: - """Verify expected chunk rows exist in both DB and configured FTS.""" - if not self.available: - return False - try: - db_rows = self._conn.execute( # type: ignore[union-attr] - "SELECT chunk_id, content, chunk_index FROM knowledge_chunks WHERE source = ?", - (source,), - ).fetchall() - expected = set(expected_rows) - db_set = {(str(row[0]), str(row[1]), int(row[2])) for row in db_rows} - if not expected.issubset(db_set): - return False - if self._fts is None: - return True - if not self._fts.available: - return False - fts_rows = self._fts.get_knowledge_source_rows(source) - return fts_rows is not None and expected.issubset(set(fts_rows)) - except Exception as exc: - log.error("Chunk-row verification failed for '%s': %s", source, exc) - return False - async def search( self, query: str, @@ -890,6 +845,10 @@ def delete_source(self, source: str, *, _record_version: bool = True) -> int: log.info("Deleted %d chunks for source '%s'", len(ids), source) return len(ids) except Exception as e: + try: + self._conn.rollback() # type: ignore[union-attr] + except Exception as rollback_exc: + log.error("Delete rollback failed for '%s': %s", source, rollback_exc) log.error("Failed to delete source '%s': %s", source, e) return 0 @@ -925,7 +884,15 @@ def delete_source_confirmed( (source,), ).fetchall() if not rows: - return 0 + orphans = self._fts.count_knowledge_source(source) + if not orphans: + return 0 + self._fts.delete_knowledge_source(source) + if self._fts.has_knowledge_source(source): + log.error("Failed to remove %d orphan FTS rows for '%s'", orphans, source) + return 0 + log.warning("Removed %d orphan FTS rows for '%s'", orphans, source) + return orphans content = self.get_source_content(source) if _record_version else None content_hash = self._content_hash(content) if content else "" @@ -943,20 +910,22 @@ def delete_source_confirmed( ) return 0 if self._fts: - if not self.source_is_durable( - source, - expected_chunks=len(rows), - require_fts=True, - expected_content_hash=expected_source_hash, + if expected_source_hash is not None and any( + str(row[0] or "") != expected_source_hash + for row in conn.execute( + "SELECT doc_content_hash FROM knowledge_chunks WHERE source = ?", + (source,), + ).fetchall() ): log.error( - "Confirmed delete refused for '%s': DB/FTS rows do not match", + "Confirmed delete refused for '%s': stored content is not expected", source, ) return 0 + fts_before = self._fts.count_knowledge_source(source) removed_fts = self._fts.delete_knowledge_source(source) fts_remains = self._fts.has_knowledge_source(source) - if removed_fts != len(rows) or fts_remains: + if removed_fts != fts_before or fts_remains: # delete_knowledge_source may commit and then report/fail; # restore from the still-authoritative DB snapshot before # refusing the migration. @@ -1073,7 +1042,7 @@ async def delete_source_confirmed_async( ) -> int: """Run migration-safe confirmed deletion under the async write lock.""" async with self._write_lock: - return await asyncio.to_thread( + return await to_thread_settled( self.delete_source_confirmed, source, survivor_source=survivor_source, @@ -1091,12 +1060,12 @@ async def delete_source_async(self, source: str) -> int: could commit half-written chunks. Callers in async contexts must use this wrapper (it also moves the blocking DB work to a thread).""" async with self._write_lock: - return await asyncio.to_thread(self.delete_source, source) + return await to_thread_settled(self.delete_source, source) async def merge_sources_async(self, keep_source: str, remove_source: str) -> int: """Merge sources under the write lock, off the event loop (see delete_source_async).""" async with self._write_lock: - return await asyncio.to_thread(self.merge_sources, keep_source, remove_source) + return await to_thread_settled(self.merge_sources, keep_source, remove_source) async def search_hybrid( self, @@ -1139,7 +1108,7 @@ async def search_hybrid( ) def backfill_fts(self) -> int: - """Index existing knowledge chunks into FTS5. Returns count indexed.""" + """Reconcile FTS against DB, removing orphan rows and indexing missing rows.""" if not self._fts or not self.available: return 0 try: @@ -1149,6 +1118,18 @@ def backfill_fts(self) -> int: except Exception: return 0 + db_ids = {str(row[0]) for row in rows} + inventory = self._fts.knowledge_chunk_sources() + if inventory is None: + return 0 + orphan_ids = {chunk_id for chunk_id, _source in inventory if chunk_id not in db_ids} + if orphan_ids: + removed = self._fts.delete_knowledge_chunks(orphan_ids) + log.warning( + "Removed %d orphan knowledge FTS row(s) for %d chunk id(s) no document owns", + removed, len(orphan_ids), + ) + count = 0 for row in rows: chunk_id, content, source, chunk_index = row @@ -1165,6 +1146,11 @@ def backfill_fts(self) -> int: count += 1 return count + async def backfill_fts_async(self) -> int: + """Run reconciliation under the write lock, settled through cancellation.""" + async with self._write_lock: + return await to_thread_settled(self.backfill_fts) + # ------------------------------------------------------------------ # Deduplication helpers # ------------------------------------------------------------------ @@ -1332,6 +1318,30 @@ def _next_version(self, source: str) -> int: ).fetchone() return (row[0] or 0) + 1 + def _insert_version_row( + self, + source: str, + content_hash: str, + content: str | None, + chunk_count: int, + uploader: str, + action: str, + diff_summary: str = "", + ) -> int: + """Stage a version row in the caller's open transaction.""" + version = self._next_version(source) + self._conn.execute( # type: ignore[union-attr] + "INSERT INTO knowledge_versions " + "(source, version, content_hash, content, chunk_count, " + "uploader, action, created_at, diff_summary) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + ( + source, version, content_hash, content, chunk_count, + uploader, action, datetime.now(UTC).isoformat(), diff_summary, + ), + ) + return version + def _record_version( self, source: str, @@ -1342,32 +1352,20 @@ def _record_version( action: str, diff_summary: str = "", ) -> int: - """Record a version entry. Returns the version number.""" + """Record a version entry. Returns the version number (0 on failure).""" if not self._conn: return 0 try: - version = self._next_version(source) - now = datetime.now(UTC).isoformat() - self._conn.execute( - "INSERT INTO knowledge_versions " - "(source, version, content_hash, content, chunk_count, " - "uploader, action, created_at, diff_summary) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - ( - source, - version, - content_hash, - content, - chunk_count, - uploader, - action, - now, - diff_summary, - ), + version = self._insert_version_row( + source, content_hash, content, chunk_count, uploader, action, diff_summary, ) self._conn.commit() return version except Exception as e: + try: + self._conn.rollback() + except Exception as rollback_exc: + log.error("Version rollback failed for '%s': %s", source, rollback_exc) log.error("Failed to record version for '%s': %s", source, e) return 0 diff --git a/src/learning/reflector.py b/src/learning/reflector.py index 0a7b822bf..bcb8554a3 100644 --- a/src/learning/reflector.py +++ b/src/learning/reflector.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from ..async_utils import to_thread_settled from ..odin_log import get_logger if TYPE_CHECKING: @@ -439,7 +440,7 @@ async def delete_entry_async(self, key: str) -> bool: """Delete an entry under the reflection lock, so it can't be resurrected by a concurrent reflection/consolidation writing a stale snapshot.""" async with self._lock: - result = await asyncio.to_thread(self.delete_entry, key) + result = await to_thread_settled(self.delete_entry, key) self.invalidate_cache() return result @@ -483,7 +484,7 @@ async def update_entry_async( ) -> dict | None: """Update an entry under the reflection lock (see delete_entry_async).""" async with self._lock: - result = await asyncio.to_thread(self.update_entry, key, content, category) + result = await to_thread_settled(self.update_entry, key, content, category) self.invalidate_cache() return result @@ -801,7 +802,7 @@ async def reflect_on_operation( if not self._policy_allows(policy_token): return try: - data = await asyncio.to_thread(self._load_for_write) + data = await to_thread_settled(self._load_for_write) except StoreCorruptError as exc: log.error( "Skipping reflection merge — learned.json corrupt " @@ -893,7 +894,7 @@ async def _reflect( from ..json_store import StoreCorruptError try: - data = await asyncio.to_thread(self._load_for_write) + data = await to_thread_settled(self._load_for_write) except StoreCorruptError as exc: log.error( "Skipping reflection — learned.json corrupt (backup preserved): %s", exc diff --git a/src/llm/context_budget.py b/src/llm/context_budget.py index 9be9e3da9..6a2571b10 100644 --- a/src/llm/context_budget.py +++ b/src/llm/context_budget.py @@ -71,8 +71,8 @@ FIXED_ENVELOPE_RESERVE_TOKENS = 42_000 # Compatible requests never ask an endpoint for more output than this. Profile -# declarations may be much larger, but reserving unreachable output would make -# otherwise viable agent models fail admission. +# declarations may be much larger; every budget reserves exactly what the +# request asks for. COMPATIBLE_REQUEST_OUTPUT_CEILING = 32_768 #: Default chars-per-token, expressed in MILLICHARS per token so derivations @@ -468,7 +468,6 @@ def compatible_agent_unavailable_reason( model, compatible_config, max_context_chars=None, - output_reserve_ceiling=COMPATIBLE_REQUEST_OUTPUT_CEILING, ) if snapshot.working_budget < COMPATIBLE_RESCUE_MIN_USABLE_TOKENS: return ( @@ -478,9 +477,13 @@ def compatible_agent_unavailable_reason( return None -def compatible_usable_input_tokens( - profile: object | None, *, output_reserve_ceiling: int | None = None -) -> int | None: +def compatible_request_output_tokens(profile: object | None) -> int | None: + """One output-reserve rule shared by request, admission, runtime snapshots and rescue.""" + cap = getattr(profile, "max_output_tokens", None) + return min(cap, COMPATIBLE_REQUEST_OUTPUT_CEILING) if type(cap) is int and cap > 0 else None + + +def compatible_usable_input_tokens(profile: object | None) -> int | None: """Derive usable prompt space from a profile's total window and output. Legacy profile-shaped test/config objects exposing only @@ -493,9 +496,9 @@ def compatible_usable_input_tokens( output = getattr(profile, "max_output_tokens", None) if total is not None and output is not None: try: - reserve = int(output) - if output_reserve_ceiling is not None: - reserve = min(reserve, output_reserve_ceiling) + reserve = compatible_request_output_tokens(profile) + if reserve is None: + return None return max(0, int(total) - reserve) except (TypeError, ValueError): return None @@ -510,16 +513,13 @@ def snapshot_for_compatible_profile( compatible_config: object, *, max_context_chars: int | None, - output_reserve_ceiling: int | None = None, ) -> ContextBudgetSnapshot: """Resolve compatible budgets post-utilization, without Codex policy floors.""" canonical = canonical_compatible_model(model) profile = compatible_model_profile(canonical, compatible_config) # Unknown compatible models have no claimed window. Keep ordinary history # compaction total, but do not qualify them for rescue. - usable = compatible_usable_input_tokens( - profile, output_reserve_ceiling=output_reserve_ceiling - ) or 0 + usable = compatible_usable_input_tokens(profile) or 0 source = "compatible_profile" if profile is not None else "unknown_compatible" utilization = int(getattr(compatible_config, "context_utilization", 100)) utilization = max(0, min(100, utilization)) diff --git a/src/llm/context_compressor.py b/src/llm/context_compressor.py index 59a045b5b..1b9d9651c 100644 --- a/src/llm/context_compressor.py +++ b/src/llm/context_compressor.py @@ -271,6 +271,10 @@ def estimate_message_images(messages: list[dict]) -> int: return total +_COUNTED_BLOCK_KEYS = ("text", "content", "input", "arguments", "reasoning_content") +_ELIDABLE_BLOCK_KEYS = ("text", "content", "input", "arguments") + + def estimate_message_chars(messages: list[dict]) -> int: """Estimate total character payload across a message list.""" total = 0 @@ -282,7 +286,15 @@ def estimate_message_chars(messages: list[dict]) -> int: for block in content: if not isinstance(block, dict): continue - for key in ("text", "content", "input", "arguments"): + # OpenAI-compatible chat serializes list-valued tool results + # with json.dumps as the tool message's content. Charge the + # same payload here; otherwise large structured results are + # invisible to context budgeting even though they reach wire. + if block.get("type") == "tool_result" and isinstance( + block.get("content"), list + ): + total += len(json.dumps(block["content"], default=str)) + for key in _COUNTED_BLOCK_KEYS: val = block.get(key) if val is None: continue @@ -309,12 +321,26 @@ def estimate_message_chars(messages: list[dict]) -> int: ) +def _failure_label(result: str) -> str: + from ..tools.tool_text import failure_reason + + return f"ERR ({failure_reason(result) or 'error'})" + + +def _outcome_from_text(result: str) -> str: + from ..tools.tool_text import failure_reason + + if failure_reason(result) or result.startswith(_ERROR_PREFIXES): + return _failure_label(result) + return "OK" + + def summarize_iteration(iteration: list[dict]) -> str: """Produce a compact ``tool_name→OK/ERR`` summary for one iteration.""" tool_names: list[str] = [] call_ids: list[str | None] = [] outcomes_by_id: dict[str, str] = {} - outcomes: list[str] = [] + unkeyed_outcomes: list[str] = [] for msg in iteration: if _is_injected_directive(msg): @@ -342,12 +368,13 @@ def summarize_iteration(iteration: list[dict]) -> str: if block.get("status"): outcome = str(block["status"]) elif "is_error" in block: - outcome = "ERR" if block["is_error"] else "OK" + outcome = _failure_label(result) if block["is_error"] else "OK" else: - outcome = "ERR" if result.startswith(_ERROR_PREFIXES) else "OK" - outcomes.append(outcome) + outcome = _outcome_from_text(result) if block.get("tool_use_id"): outcomes_by_id[block["tool_use_id"]] = outcome + else: + unkeyed_outcomes.append(outcome) # Agent-style string tool results: "[Tool result: tool_name]\n..." elif isinstance(content, str) and content.startswith("[Tool result:"): # Extract tool name from "[Tool result: tool_name]" @@ -357,17 +384,20 @@ def summarize_iteration(iteration: list[dict]) -> str: tool_names.append(name) call_ids.append(None) result_body = content[end + 1 :].strip() if end > 0 else content - if result_body.startswith(_ERROR_PREFIXES): - outcomes.append("ERR") - else: - outcomes.append("OK") + unkeyed_outcomes.append(_outcome_from_text(result_body)) parts = [] - for i, name in enumerate(tool_names): - outcome = outcomes[i] if i < len(outcomes) else "?" - call_id = call_ids[i] + unkeyed_index = 0 + for name, call_id in zip(tool_names, call_ids, strict=True): if call_id: - outcome = outcomes_by_id.get(call_id, outcome) + outcome = outcomes_by_id.get(call_id, "?") + else: + outcome = ( + unkeyed_outcomes[unkeyed_index] + if unkeyed_index < len(unkeyed_outcomes) + else "?" + ) + unkeyed_index += 1 parts.append(f"{name}\u2192{outcome}") summary = ", ".join(parts) @@ -560,7 +590,8 @@ def _truncate_iteration(iteration: list[dict], max_chars: int) -> tuple[list[dic for block in content: if not isinstance(block, dict) or block.get("type") == "tool_use": continue - for key in ("text", "content", "input", "arguments"): + # Preserved reasoning is counted, but only its whole iteration may be removed. + for key in _ELIDABLE_BLOCK_KEYS: value = block.get(key) if isinstance(value, str): strings.append((block, key, value)) @@ -588,6 +619,18 @@ def _truncate_iteration(iteration: list[dict], max_chars: int) -> tuple[list[dic return work, max(0, original_chars - compressed_chars) +def _preserved_reasoning_chars(iteration: list[dict]) -> int: + return sum( + len(block["reasoning_content"]) + for msg in iteration + if isinstance(msg.get("content"), list) + for block in msg["content"] + if isinstance(block, dict) + and block.get("type") == "reasoning_content" + and isinstance(block.get("reasoning_content"), str) + ) + + def _is_emergency_summary(msg: dict) -> bool: """Identify the exact user-message shape emitted by this compressor.""" content = msg.get("content") @@ -945,8 +988,9 @@ def _assemble( for idx in range(len(iterations) - 1, -1, -1): iteration = iterations[idx] size = _iteration_chars(iteration) - if size > single_cap: - iteration, elided = _truncate_iteration(iteration, single_cap) + iteration_cap = single_cap + _preserved_reasoning_chars(iteration) + if size > iteration_cap: + iteration, elided = _truncate_iteration(iteration, iteration_cap) if elided: report["results_truncated"] += 1 report["chars_elided"] += elided @@ -1005,7 +1049,9 @@ def _assemble( report["compressed_chars"] = compressed_chars report["fits"] = compressed_chars <= target_chars if not report["fits"]: - report["unfit_reason"] = "immutable newest call or control messages exceed target" + report["unfit_reason"] = ( + "immutable newest call, preserved reasoning or control messages exceed target" + ) return messages, report if stats: stats.compressions += 1 diff --git a/src/llm/openai_compatible.py b/src/llm/openai_compatible.py index fc5b63d48..4d49a7af8 100644 --- a/src/llm/openai_compatible.py +++ b/src/llm/openai_compatible.py @@ -14,7 +14,7 @@ from .backoff import DEFAULT_BASE_DELAY, DEFAULT_MAX_DELAY, DEFAULT_MAX_RETRIES, compute_backoff from .circuit_breaker import CircuitBreaker from .client_lifecycle import leased_call -from .context_budget import COMPATIBLE_REQUEST_OUTPUT_CEILING +from .context_budget import canonical_compatible_model, compatible_request_output_tokens from .errors import LLMContextLengthError, LLMRateLimitError, LLMRequestError, LLMTransportError from .progress import GenerationProgress, GenerationProgressObserver, emit_progress from .provider import LLMProvider @@ -328,17 +328,13 @@ def _request_max_tokens( """Resolve the per-request output cap without treating zero as unset.""" if type(requested) is int and requested > 0: return requested - from .context_budget import canonical_compatible_model - canonical = canonical_compatible_model(model or self.model) profile = self.model_profiles.get(canonical) if profile is None and self.openrouter_routing is not None: derived = getattr(self.openrouter_routing, "catalogue_profiles", {}) or {} profile = derived.get(canonical) - profile_cap = getattr(profile, "max_output_tokens", None) - if type(profile_cap) is int and profile_cap > 0: - return min(profile_cap, COMPATIBLE_REQUEST_OUTPUT_CEILING) - return self.max_tokens + cap = compatible_request_output_tokens(profile) + return cap if cap is not None else self.max_tokens def _preserves_reasoning_content(self) -> bool: """Whether this endpoint explicitly requires preserved-thinking replay.""" diff --git a/src/llm/tool_history.py b/src/llm/tool_history.py index 529336b0a..7b6347676 100644 --- a/src/llm/tool_history.py +++ b/src/llm/tool_history.py @@ -26,7 +26,8 @@ def normalize_tool_calls(calls, *, used_ids: set[str] | None = None) -> list[dic A duplicate identity receives a fresh correlation ID and a paired parse error, not permission to execute an ambiguous call. Legacy callbacks with - no ID remain supported. The caller owns the lifetime identity set. + no ID remain supported. The caller owns any supplied identity set; agents + scope that set to one provider reply, not their entire transcript. """ seen = used_ids if used_ids is not None else set() normalized = [] @@ -95,6 +96,10 @@ def settled_call_ids(messages: list[dict]) -> set[str]: continue if message.get("role") == "assistant" and pending: raise ValueError("Unresolved native tool calls before assistant generation") + if message.get("role") == "assistant": + # A provider can reuse an ID in its next reply. Reject ambiguity + # within one reply, not across already-settled generations. + seen.clear() for block in content: if not isinstance(block, dict): continue diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 725aa2edc..085b43afc 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -195,6 +195,41 @@ def __init__(self, data_path: str, history_path: str | None = None) -> None: self._http_session: aiohttp.ClientSession | None = None self._load() self._degrade_removed_trigger_sources() + for schedule in self._schedules: + self._resolve_interrupted_run(schedule) + + REPLAY_SAFE_ONE_TIME_ACTIONS = frozenset({"reminder", "digest"}) + + @classmethod + def _tracks_run_start(cls, schedule: dict) -> bool: + return ( + bool(schedule.get("one_time")) + and schedule.get("action") not in cls.REPLAY_SAFE_ONE_TIME_ACTIONS + ) + + def _resolve_interrupted_run(self, schedule: dict) -> bool: + started = schedule.pop("run_started_at", None) + if not started or not self._tracks_run_start(schedule): + return False + self._quarantine_schedule( + schedule, + f"One-time schedule started at {started!r} but its completion was never recorded " + "(Odin stopped or could not save the result); it may have partly run, so it was not " + "run again. Check what it did, then set a new run_at to re-arm it", + ) + return True + + async def _mark_run_started(self, schedule: dict) -> None: + started = datetime.now(UTC).isoformat() + sid = schedule.get("id") + async with self._lock: + candidate = copy.deepcopy(self._schedules) + for current in candidate: + if current.get("id") == sid: + current["run_started_at"] = started + await self._publish(candidate) + schedule["run_started_at"] = started + return def _load(self) -> None: if self.data_path.exists(): @@ -789,7 +824,8 @@ def _trigger_matches(trigger: dict, source: str, event_data: dict) -> bool: - source: exact match (required if specified) - event: exact match against event_data["event"] - repo: case-insensitive substring match against event_data["repo"] - - alert_name: case-insensitive substring match against event_data["alert_name"] + - alert_name: case-insensitive substring match against any string in + event_data["alert_names"], falling back to event_data["alert_name"] All specified fields must match (AND logic). """ @@ -803,8 +839,16 @@ def _trigger_matches(trigger: dict, source: str, event_data: dict) -> bool: if trigger["repo"].lower() not in repo.lower(): return False if trigger.get("alert_name"): - alert = event_data.get("alert_name", "") - if trigger["alert_name"].lower() not in alert.lower(): + alert_names = event_data.get("alert_names") + if alert_names is None: + alert_names = [event_data.get("alert_name", "")] + elif not isinstance(alert_names, (list, tuple)): + alert_names = [] + if not any( + isinstance(name, str) + and trigger["alert_name"].lower() in name.lower() + for name in alert_names + ): return False return True @@ -1009,6 +1053,7 @@ async def update( # retry_at that caused this schedule to be quarantined. target.pop("retry_at", None) target.pop("inert_reason", None) + target.pop("run_started_at", None) if trigger is not None: self._validate_trigger(trigger) @@ -1105,10 +1150,15 @@ async def run_now(self, schedule_id: str) -> dict: """ schedule: dict | None = None async with self._lock: + resolved = False for s in self._schedules: if s["id"] == schedule_id: + if schedule_id not in self._in_flight: + resolved = self._resolve_interrupted_run(s) schedule = copy.deepcopy(s) break + if resolved: + await self._publish(copy.deepcopy(self._schedules)) if schedule is None: raise ValueError(f"Schedule '{schedule_id}' not found") if schedule.get("inert_reason"): @@ -1305,6 +1355,8 @@ async def _execute_and_record( ): await self._restore_unstarted_reservation(schedule, reservation) raise ScheduleConnectionUnavailableError(snapshot) + if self._tracks_run_start(schedule): + await self._mark_run_started(schedule) identity = self._execution_identity(schedule) nonce = uuid.uuid4().hex self._active_execution_nonces.add(nonce) @@ -1317,8 +1369,13 @@ async def _execute_and_record( async with self._lock: candidate = copy.deepcopy(self._schedules) for current in candidate: - if current["id"] != sid or self._execution_identity(current) != identity: + if current["id"] != sid: continue + if current.get("run_started_at") == schedule.get("run_started_at"): + current.pop("run_started_at", None) + if self._execution_identity(current) != identity: + await self._publish(candidate) + break for key in ("last_run", "consecutive_failures", "retry_count", "last_error", "last_error_at", "retry_at"): if key in schedule: @@ -1546,6 +1603,13 @@ async def _tick(self) -> None: for schedule in candidate: if schedule.get("paused"): continue + if ( + schedule.get("run_started_at") + and schedule.get("id") not in self._in_flight + ): + if self._resolve_interrupted_run(schedule): + quarantined = True + continue # Do not persist/rollback a retry reservation on every tick # while a manual run already owns this schedule. if schedule.get("id") in self._in_flight and schedule.get("retry_at"): diff --git a/src/search/fts.py b/src/search/fts.py index 99855c4ee..f6bb8669c 100644 --- a/src/search/fts.py +++ b/src/search/fts.py @@ -218,6 +218,46 @@ def index_knowledge_chunk( log.error("FTS knowledge index failed for %s: %s", chunk_id, e) return False + def replace_knowledge_source( + self, source: str, rows: Iterable[tuple[str, str, int]], + ) -> bool: + """Replace every FTS row for a source in one transaction.""" + if not self._conn: + return False + try: + with self._write_lock: + try: + self._conn.execute("DELETE FROM knowledge_fts WHERE source = ?", (source,)) + for chunk_id, content, chunk_index in rows: + self._conn.execute( + "DELETE FROM knowledge_fts WHERE chunk_id = ?", (chunk_id,), + ) + self._conn.execute( + "INSERT INTO knowledge_fts (chunk_id, content, source, chunk_index) " + "VALUES (?, ?, ?, ?)", + (chunk_id, content, source, str(chunk_index)), + ) + self._conn.commit() + except Exception: + self._rollback_after_failure() + raise + return True + except Exception as exc: + log.error("FTS knowledge replace failed for '%s': %s", source, exc) + return False + + def knowledge_chunk_sources(self) -> list[tuple[str, str]] | None: + """Inventory all FTS chunk owners; None means unreadable, not empty.""" + if not self._conn: + return None + try: + with self._write_lock: + rows = self._conn.execute("SELECT chunk_id, source FROM knowledge_fts").fetchall() + return [(str(row[0]), str(row[1])) for row in rows] + except Exception as exc: + log.error("FTS knowledge inventory failed: %s", exc) + return None + def search_knowledge(self, query: str, limit: int = 20) -> list[dict]: if not self._conn: raise SearchExecutionError("full-text search is unavailable") diff --git a/src/search/vectorstore.py b/src/search/vectorstore.py index a67fbdb6f..69a19ff2e 100644 --- a/src/search/vectorstore.py +++ b/src/search/vectorstore.py @@ -6,6 +6,7 @@ from __future__ import annotations import asyncio +import hashlib import json import sqlite3 import threading @@ -28,6 +29,13 @@ VECTOR_DIM = 384 # must match LocalEmbedder.DIMENSIONS +def summary_segment_doc_id(channel_id: str, seg: dict) -> str: + """Stable identity across restored archives containing the same segment.""" + payload = json.dumps([channel_id, seg.get("start_ts"), seg.get("end_ts"), + seg.get("summary", "")]) + return "seg:" + hashlib.sha256(payload.encode()).hexdigest()[:24] + + class SessionVectorStore: """Semantic + FTS search over archived session conversations.""" @@ -47,6 +55,7 @@ def __init__(self, db_path: str, fts_index: FullTextIndex | None = None) -> None # wrap in ``asyncio.to_thread``. WAL mode still allows concurrent # reads, so reads stay unlocked. self._write_lock = threading.Lock() + self._segment_backfill_lock = asyncio.Lock() try: conn = sqlite3.connect(db_path, check_same_thread=False) conn.execute("PRAGMA journal_mode=WAL") @@ -70,6 +79,8 @@ def __init__(self, db_path: str, fts_index: FullTextIndex | None = None) -> None conn.execute( "CREATE INDEX IF NOT EXISTS idx_session_channel ON session_archives(channel_id)" ) + conn.execute("CREATE TABLE IF NOT EXISTS segment_index_state (" + "archive_doc_id TEXT PRIMARY KEY, indexed_at REAL NOT NULL)") # Vector table (only if sqlite-vec loaded) if self._has_vec: conn.execute(f""" @@ -100,8 +111,10 @@ async def index_session(self, archive_path: Path, embedder: LocalEmbedder) -> bo doc_id = archive_path.stem # e.g. "channelid_timestamp" doc_text = self._build_document_text(data) + # A segment-only archive has no legacy document, but still needs its + # independent, searchable segment documents. if not doc_text: - return False + return await self._index_segments(data, doc_id, embedder) channel_id = str(data.get("channel_id", "")) last_active = float(data.get("last_active", 0)) @@ -120,12 +133,85 @@ async def index_session(self, archive_path: Path, embedder: LocalEmbedder) -> bo self._write_session_sync, doc_id, doc_text, channel_id, last_active, message_count, vector, ) + if not await self._index_segments(data, doc_id, embedder): + return False log.info("Indexed session %s for search", doc_id) return True except Exception as e: log.error("Session index failed for %s: %s", doc_id, e) return False + async def _index_segments(self, data: dict, archive_doc_id: str, + embedder: LocalEmbedder | None) -> bool: + """Write missing segments, then acknowledge the entire archive. + + FTS has its own database: the state row is committed only after each + FTS write acknowledges. An interrupted run repairs missing FTS rows. + """ + try: + channel_id = str(data.get("channel_id", "")) + missing: list[tuple[str, str, str, float, int, list[float] | None]] = [] + for seg in data.get("summary_segments", []): + text = seg.get("summary", "") + if not text: + continue + doc_id = summary_segment_doc_id(channel_id, seg) + exists = await asyncio.to_thread(self._segment_exists_sync, doc_id) + indexed = (await asyncio.to_thread(self._fts.has_session, doc_id) + if self._fts else True) + if exists and indexed: + continue + vector = None + if not exists and self._has_vec and embedder: + vector = await embedder.embed(text) + missing.append((doc_id, text, channel_id, float(seg.get("end_ts", 0)), + int(seg.get("source_count", 0)), vector)) + await asyncio.to_thread(self._write_segment_archive_sync, archive_doc_id, missing) + return True + except Exception as e: + log.error("Segment indexing failed for %s: %s", archive_doc_id, e) + return False + + def _segment_exists_sync(self, doc_id: str) -> bool: + return self._conn.execute( # type: ignore[union-attr] + "SELECT 1 FROM session_archives WHERE doc_id = ?", (doc_id,), + ).fetchone() is not None + + def _write_segment_archive_sync( + self, archive_doc_id: str, + missing: list[tuple[str, str, str, float, int, list[float] | None]], + ) -> None: + """Commit all missing metadata, vectors, and completion in one transaction.""" + with self._write_lock: + try: + for doc_id, text, channel_id, end_ts, source_count, vector in missing: + self._conn.execute( # type: ignore[union-attr] + "INSERT OR REPLACE INTO session_archives " + "(doc_id, content, channel_id, last_active, message_count) " + "VALUES (?, ?, ?, ?, ?)", + (doc_id, text, channel_id, end_ts, source_count), + ) + if self._fts and not self._fts.index_session( + doc_id, text, channel_id, end_ts): + raise RuntimeError("FTS did not acknowledge segment indexing") + if vector is not None: + self._conn.execute( # type: ignore[union-attr] + "DELETE FROM session_vec WHERE doc_id = ?", (doc_id,), + ) + self._conn.execute( # type: ignore[union-attr] + "INSERT INTO session_vec (doc_id, embedding) VALUES (?, ?)", + (doc_id, serialize_vector(vector)), + ) + self._conn.execute( # type: ignore[union-attr] + "INSERT OR REPLACE INTO segment_index_state " + "(archive_doc_id, indexed_at) VALUES (?, strftime('%s','now'))", + (archive_doc_id,), + ) + self._conn.commit() # type: ignore[union-attr] + except BaseException: + self._conn.rollback() # type: ignore[union-attr] + raise + @staticmethod def _read_archive_sync(archive_path: Path) -> dict: """Read and parse archive JSON (sync, for use with asyncio.to_thread).""" @@ -266,6 +352,52 @@ async def backfill(self, archive_dir: Path, embedder: LocalEmbedder) -> int: return count + async def backfill_segments(self, archive_dir: Path, embedder: LocalEmbedder) -> int: + """Bounded, single-flight legacy segment migration, one archive at a time.""" + if not self.available or not archive_dir.exists(): + return 0 + async with self._segment_backfill_lock: + # The FTS table lives in another SQLite file. Its rows can go + # missing even when the metadata/state transaction was committed + # (e.g. restoring only one of the two databases). Repair from the + # stored segment text, without rereading every archive on startup. + await asyncio.to_thread(self._repair_segment_fts_sync) + existing = await asyncio.to_thread(self._get_segment_state_sync) + paths = await asyncio.to_thread(lambda: sorted(archive_dir.glob("*.json"))[:10000]) + count = 0 + bytes_read = 0 + for path in paths: + if path.stem in existing: + continue + try: + size = await asyncio.to_thread(lambda: path.stat().st_size) + if bytes_read + size > 2_000_000_000: + break + bytes_read += size + data = await asyncio.to_thread(self._read_archive_sync, path) + if await self._index_segments(data, path.stem, embedder): + count += 1 + except Exception as e: + log.warning("Skipping unreadable archive %s: %s", path, e) + await asyncio.sleep(0) + return count + + def _get_segment_state_sync(self) -> set[str]: + return {row[0] for row in self._conn.execute( # type: ignore[union-attr] + "SELECT archive_doc_id FROM segment_index_state")} + + def _repair_segment_fts_sync(self) -> None: + if not self._fts: + return + rows = self._conn.execute( # type: ignore[union-attr] + "SELECT doc_id, content, channel_id, last_active " + "FROM session_archives WHERE doc_id LIKE 'seg:%'", + ).fetchall() + for doc_id, content, channel_id, end_ts in rows: + if not self._fts.has_session(doc_id) and not self._fts.index_session( + doc_id, content, channel_id, end_ts): + raise RuntimeError(f"FTS repair did not acknowledge {doc_id}") + def _get_indexed_ids_sync(self) -> set[str]: """Get set of already-indexed doc IDs (sync).""" # Caller (backfill) checks self.available first. diff --git a/src/sessions/manager.py b/src/sessions/manager.py index 028b408d0..6491b98ee 100644 --- a/src/sessions/manager.py +++ b/src/sessions/manager.py @@ -21,6 +21,11 @@ from ..relevance import rank as relevance_rank from ..relevance import score as relevance_score from ..search.errors import validate_search_query +from ..search.vectorstore import summary_segment_doc_id + + +def _segment_visible_to(seg: dict, user_id: str | None) -> bool: + return not user_id or user_id in seg.get("participants", []) if TYPE_CHECKING: from ..learning.reflector import ConversationReflector @@ -1377,10 +1382,12 @@ def _search_archives( user_id: str | None = None, after: float | None = None, before: float | None = None, + seen_segments: set[str] | None = None, ) -> list[dict]: """Search archived session files for keyword matches (sync, for use in thread).""" results: list[dict] = [] archive_dir = self.persist_dir / "archive" + seen_segments = seen_segments if seen_segments is not None else set() if not archive_dir.exists(): return results for path in sorted(archive_dir.glob("*.json"), reverse=True): @@ -1393,7 +1400,7 @@ def _search_archives( # it must not resurface via search, mirroring restore. reset_epoch = self._reset_epochs.get(arch_cid, 0.0) summary = data.get("summary", "") - if summary and query_lower in summary.lower(): + if not user_id and summary and query_lower in summary.lower(): ts = data.get("last_active", 0) if (after and ts < after) or (before and ts > before) or ts <= reset_epoch: pass @@ -1404,6 +1411,22 @@ def _search_archives( "timestamp": ts, "channel_id": arch_cid, }) + for seg in data.get("summary_segments", []): + if not _segment_visible_to(seg, user_id): + continue + text = seg.get("summary", "") + if not text or query_lower not in text.lower(): + continue + seg_id = summary_segment_doc_id(arch_cid, seg) + ts = seg.get("end_ts", 0) + if (seg_id in seen_segments or (after and ts < after) or + (before and ts > before) or ts <= reset_epoch): + continue + seen_segments.add(seg_id) + results.append({"type": "summary", "content": text[:500], + "timestamp": ts, "channel_id": arch_cid}) + if len(results) >= limit: + return results for msg in reversed(data.get("messages", [])): if user_id and msg.get("user_id") != user_id: continue @@ -1449,6 +1472,7 @@ async def search_history( validate_search_query(query) query_lower = query.lower() results: list[dict] = [] + seen_segments: set[str] = set() def _ts_ok(ts: float) -> bool: if after and ts < after: @@ -1464,7 +1488,7 @@ def _ts_ok(ts: float) -> bool: sessions_iter = [s] if s else [] for session in sessions_iter: - if session.summary and query_lower in session.summary.lower(): + if not user_id and session.summary and query_lower in session.summary.lower(): if _ts_ok(session.last_active): results.append({ "type": "summary", @@ -1473,10 +1497,14 @@ def _ts_ok(ts: float) -> bool: "channel_id": session.channel_id, }) for seg in session.summary_segments: + if not _segment_visible_to(seg, user_id): + continue seg_text = seg.get("summary", "") if seg_text and query_lower in seg_text.lower(): ts = seg.get("end_ts", session.last_active) - if _ts_ok(ts): + seg_id = summary_segment_doc_id(session.channel_id, seg) + if _ts_ok(ts) and seg_id not in seen_segments: + seen_segments.add(seg_id) results.append({ "type": "summary", "content": seg_text[:500], @@ -1502,7 +1530,7 @@ def _ts_ok(ts: float) -> bool: # Step 2: keyword search on archives (most recent first) archive_results = await asyncio.to_thread( self._search_archives, query_lower, limit - len(results), - channel_id, user_id, after, before, + channel_id, user_id, after, before, seen_segments, ) results.extend(archive_results) if len(results) >= limit: @@ -1515,14 +1543,27 @@ async def _append_channel_matches() -> None: remaining = limit - len(results) fts = self._fts_index channel_results = [] - if fts and hasattr(fts, "search_channel_logs"): - channel_results = fts.search_channel_logs( - query, limit=remaining, channel_id=channel_id, - ) - if not channel_results and hasattr(self._channel_logger, "search"): + if not user_id and fts and hasattr(fts, "search_channel_logs"): channel_results = await asyncio.to_thread( - self._channel_logger.search, query, remaining, + fts.search_channel_logs, query, limit=remaining, + channel_id=channel_id, ) + if not channel_results and hasattr(self._channel_logger, "search"): + if user_id: + channel_results = await asyncio.to_thread( + self._channel_logger.search, query, remaining, channel_id, + author_id=user_id, + accept=lambda record: ( + _ts_ok(record.get("ts", 0)) and + record.get("ts", 0) > self._reset_epochs.get( + record.get("channel_id", ""), 0.0)), + ) + else: + # Preserve compatibility with legacy search(query, limit) + # implementations; channel scope is filtered below. + channel_results = await asyncio.to_thread( + self._channel_logger.search, query, remaining, + ) seen = {(r.get("channel_id", ""), r.get("timestamp", 0)) for r in results} for cr in channel_results: ts = cr.get("timestamp", 0) @@ -1549,12 +1590,14 @@ async def _append_channel_matches() -> None: # Step 4: hybrid search (FTS5 + semantic) fills remaining slots. # Backend failures propagate to the caller rather than masquerading as # a keyword-only result set. - if len(results) < limit and self._vector_store: + if not user_id and len(results) < limit and self._vector_store: hybrid_results = await self._vector_store.search_hybrid( query, self._embedder, limit=limit, ) seen = {(r["channel_id"], r.get("timestamp", 0)) for r in results} for hr in hybrid_results: + if hr.get("doc_id") in seen_segments: + continue if channel_id and hr.get("channel_id") != channel_id: continue ts = hr.get("timestamp", 0) diff --git a/src/tools/autonomous_loop.py b/src/tools/autonomous_loop.py index a0b2e49c0..15a54e675 100644 --- a/src/tools/autonomous_loop.py +++ b/src/tools/autonomous_loop.py @@ -565,7 +565,9 @@ async def _post_response( try: text = response if len(text) > 2000: - text = text[:1950] + "\n... (truncated)" + from ..discord.delivery import close_open_fence + + text = close_open_fence(text[:1950]) + "\n... (truncated)" await channel.send(text) except Exception as e: log.warning("Loop %s: failed to post response: %s", info.id, e) diff --git a/src/tools/defs/browser_web.py b/src/tools/defs/browser_web.py index 903860410..8081ab3a0 100644 --- a/src/tools/defs/browser_web.py +++ b/src/tools/defs/browser_web.py @@ -100,9 +100,10 @@ { "name": "browser_click", "description": ( - "Navigates to a URL and clicks an element by CSS selector. Returns a confirmation " - "summary after clicking. To fill forms, use browser_fill. To read page content after " - "clicking, follow up with browser_read_page." + "Loads a URL in a fresh browser session, clicks an element by CSS selector, and " + "returns the resulting page's title and URL. Cookies, storage and page state end " + "with the call; a later browser_read_page reloads the URL without them. To fill and " + "submit in one call, use browser_fill with submit=true." ), "input_schema": { "type": "object", @@ -126,8 +127,10 @@ { "name": "browser_fill", "description": ( - "Navigates to a URL and fills a form field by CSS selector. Optionally submits by " - "pressing Enter. To click buttons, use browser_click." + "Loads a URL in a fresh browser session, fills one field by CSS selector, optionally " + "presses Enter (submit=true), and returns the page's title and URL. The value is gone " + "when the call ends, so a later browser_click cannot submit it; use submit=true, or " + "one browser_evaluate for several fields." ), "input_schema": { "type": "object", @@ -157,8 +160,11 @@ { "name": "browser_evaluate", "description": ( - "Evaluates JavaScript on a URL and returns the result. For custom scraping or " - "interaction. Large results have retained previews; use get_tool_output(cursor=...) " + "Loads a URL in a fresh browser session, evaluates a JavaScript expression, and " + "returns its result (a returned Promise is awaited). Use it for scraping or several " + "interaction " + "steps in one call; the session closes when it returns, so a navigation it starts may " + "not complete. Large results have retained previews; use get_tool_output(cursor=...) " "without re-running the expression." ), "input_schema": { diff --git a/src/tools/email_client.py b/src/tools/email_client.py index c82b907b0..db1469767 100644 --- a/src/tools/email_client.py +++ b/src/tools/email_client.py @@ -49,11 +49,32 @@ def _decode_header_value(raw: str | None) -> str: return " ".join(decoded) +def _is_attachment(part: email_lib.message.Message) -> bool: + return part.get_content_disposition() == "attachment" + + +def _body_parts(msg: email_lib.message.Message): + """Yield body leaves, never text nested within an attached message.""" + + def walk(part: email_lib.message.Message, inside_attachment: bool): + inside_attachment |= _is_attachment(part) + if part.is_multipart(): + children = part.get_payload() + if isinstance(children, list): + for child in children: + if isinstance(child, email_lib.message.Message): + yield from walk(child, inside_attachment) + elif not inside_attachment: + yield part + + yield from walk(msg, False) + + def _extract_body(msg: email_lib.message.Message, max_chars: int) -> str: if msg.is_multipart(): - for part in msg.walk(): + for part in _body_parts(msg): ct = part.get_content_type() - if ct == "text/plain" and part.get("Content-Disposition") != "attachment": + if ct == "text/plain": payload = part.get_payload(decode=True) if payload: charset = part.get_content_charset() or "utf-8" @@ -64,9 +85,9 @@ def _extract_body(msg: email_lib.message.Message, max_chars: int) -> str: + f"\n\n[truncated at {max_chars} chars, original {len(text)}]" ) return text - for part in msg.walk(): + for part in _body_parts(msg): ct = part.get_content_type() - if ct == "text/html" and part.get("Content-Disposition") != "attachment": + if ct == "text/html": payload = part.get_payload(decode=True) if payload: charset = part.get_content_charset() or "utf-8" @@ -91,8 +112,7 @@ def _extract_body(msg: email_lib.message.Message, max_chars: int) -> str: def _attachment_metadata(msg: email_lib.message.Message) -> list[dict]: attachments = [] for part in msg.walk(): - disp = part.get("Content-Disposition", "") - if "attachment" in disp: + if _is_attachment(part): filename = part.get_filename() or "(unnamed)" filename = _decode_header_value(filename) size = len(part.get_payload(decode=True) or b"") @@ -114,9 +134,7 @@ def _message_summary(msg: email_lib.message.Message, uid: str = "") -> dict: "subject": _decode_header_value(msg.get("Subject")), "date": msg.get("Date", ""), "message_id": msg.get("Message-ID", ""), - "has_attachments": any( - "attachment" in (p.get("Content-Disposition") or "") for p in msg.walk() - ), + "has_attachments": any(_is_attachment(p) for p in msg.walk()), } @@ -194,24 +212,74 @@ def send_email( with smtplib.SMTP(smtp_host, smtp_port, timeout=timeout) as server: server.starttls() server.login(username, password) - server.sendmail(from_address, all_recipients, msg.as_string()) + refused_map = server.sendmail(from_address, all_recipients, msg.as_string()) + except smtplib.SMTPRecipientsRefused as e: + refusals = _refusals(e.recipients, to, cc, bcc, password) + raise RuntimeError( + f"SMTP send failed: no message was sent. Refused: {_format_refusals(refusals)}" + ) from None except Exception as e: raise RuntimeError(f"SMTP send failed: {_safe_error(e, password)}") from None + refusals = _refusals(refused_map, to, cc, bcc, password) + accepted = [address for address in dict.fromkeys(all_recipients) if address not in refused_map] log.info( - "Email sent to %s, subject=%r, message_id=%s", all_recipients, subject, msg["Message-ID"] + "Email accepted by SMTP for %s, refused for %s, subject=%r, message_id=%s", + accepted, + [entry["address"] for entry in refusals], + subject, + msg["Message-ID"], ) return { - "status": "sent", + "status": "partial" if refusals else "sent", "message_id": msg["Message-ID"], "to": to, "cc": cc or [], "subject": subject, "attachments": [Path(p).name for p in (attachments or [])], + "accepted_count": len(accepted), + "refused": refusals, } +def _refusals( + refused_map: dict, + to: list[str], + cc: list[str] | None, + bcc: list[str] | None, + password: str | None, +) -> list[dict]: + """Label and sanitize SMTP refusals, including addresses hidden in BCC.""" + result: list[dict] = [] + seen: set[str] = set() + for field, addresses in (("To", to), ("CC", cc or []), ("BCC", bcc or [])): + for address in addresses: + if address in seen or address not in refused_map: + continue + seen.add(address) + code, response = refused_map[address] + text = ( + response.decode("utf-8", "replace") + if isinstance(response, bytes) + else str(response) + ) + reason = " ".join(text.split()) + if password and password in reason: + reason = reason.replace(password, "[REDACTED]") + result.append( + {"address": address, "field": field, "code": int(code), "reason": reason[:200]} + ) + return result + + +def _format_refusals(refusals: list[dict]) -> str: + return "; ".join( + f"{entry['address']} ({entry['field']}): {entry['code']} {entry['reason']}".rstrip() + for entry in refusals + ) + + def search_email( *, imap_host: str, diff --git a/src/tools/handlers/comms.py b/src/tools/handlers/comms.py index c3770be42..e1c57b489 100644 --- a/src/tools/handlers/comms.py +++ b/src/tools/handlers/comms.py @@ -54,6 +54,33 @@ async def _handle_email_send(self, inp: dict) -> str: max_attachment_bytes=cfg.max_attachment_bytes, timeout=cfg.connect_timeout_seconds, ) + refused = result.get("refused") or [] + if refused: + # Accepted recipients already have the message; do not mark + # this as an error and invite an unsafe automatic retry. + from ..email_client import _format_refusals + + rejected = {entry["address"] for entry in refused} + accepted_to = [address for address in result["to"] if address not in rejected] + accepted_cc = [ + address for address in result.get("cc") or [] if address not in rejected + ] + partial = [ + "Email partially sent: the SMTP server accepted it for " + f"{result.get('accepted_count', 0)} recipient(s) and refused " + f"{len(refused)}. Not re-sent: a retry would duplicate it for the " + "accepted recipients.", + f"Message-ID: {result['message_id']}", + ] + if accepted_to: + partial.append(f"To: {', '.join(accepted_to)}") + partial.append(f"Subject: {result['subject']}") + if accepted_cc: + partial.append(f"CC: {', '.join(accepted_cc)}") + if result.get("attachments"): + partial.append(f"Attachments: {', '.join(result['attachments'])}") + partial.append(f"Refused: {_format_refusals(refused)}") + return "\n".join(partial) parts = [ "Email sent successfully.", f"Message-ID: {result['message_id']}", diff --git a/src/tools/handlers/state.py b/src/tools/handlers/state.py index b889dd967..2b47b12aa 100644 --- a/src/tools/handlers/state.py +++ b/src/tools/handlers/state.py @@ -11,10 +11,10 @@ from __future__ import annotations -import asyncio import json from pathlib import Path +from ...async_utils import to_thread_settled from .deps import HandlerBase, HandlerDeps # Max working-memory notes retained per section (global / per-user). The full @@ -58,7 +58,7 @@ async def _handle_memory_manage(self, inp: dict, *, user_id: str | None = None) if not key: return "'key' is required for get." try: - all_mem = await asyncio.to_thread(self._load_all_memory) + all_mem = await to_thread_settled(self._load_all_memory) except StoreCorruptError as exc: return ( "Memory store is currently unreadable or corrupt (a backup copy was " @@ -73,7 +73,7 @@ async def _handle_memory_manage(self, inp: dict, *, user_id: str | None = None) if action == "list": try: - all_mem = await asyncio.to_thread(self._load_all_memory) + all_mem = await to_thread_settled(self._load_all_memory) except StoreCorruptError as exc: return ( "Memory store is currently unreadable or corrupt (a backup copy was " @@ -96,7 +96,7 @@ async def _handle_memory_manage(self, inp: dict, *, user_id: str | None = None) if not key or not value: return "Both 'key' and 'value' are required for save." try: - all_mem = await asyncio.to_thread(self._load_all_memory) + all_mem = await to_thread_settled(self._load_all_memory) except StoreCorruptError as exc: return ( "Memory store is currently unreadable or corrupt (a backup copy was " @@ -122,7 +122,7 @@ async def _handle_memory_manage(self, inp: dict, *, user_id: str | None = None) break del section_map[oldest] evicted += 1 - await asyncio.to_thread(self._save_all_memory, all_mem) + await to_thread_settled(self._save_all_memory, all_mem) scope_label = "global" if section == "global" else "personal" suffix = ( f" (evicted {evicted} oldest note(s) at cap {MEMORY_MAX_KEYS_PER_SECTION})" @@ -136,7 +136,7 @@ async def _handle_memory_manage(self, inp: dict, *, user_id: str | None = None) if not key: return "'key' is required for delete." try: - all_mem = await asyncio.to_thread(self._load_all_memory) + all_mem = await to_thread_settled(self._load_all_memory) except StoreCorruptError as exc: return ( "Memory store is currently unreadable or corrupt (a backup copy was " @@ -145,11 +145,11 @@ async def _handle_memory_manage(self, inp: dict, *, user_id: str | None = None) user_key = f"user_{user_id}" if user_id else None if user_key and key in all_mem.get(user_key, {}): del all_mem[user_key][key] - await asyncio.to_thread(self._save_all_memory, all_mem) + await to_thread_settled(self._save_all_memory, all_mem) return f"Deleted personal note '{key}'." elif key in all_mem.get("global", {}): del all_mem["global"][key] - await asyncio.to_thread(self._save_all_memory, all_mem) + await to_thread_settled(self._save_all_memory, all_mem) return f"Deleted global note '{key}'." return f"No note found with key '{key}'." @@ -242,7 +242,7 @@ async def _manage_list_locked(self, action, list_name, raw_items, owner_pref, us from ...json_store import StoreCorruptError try: - lists = await asyncio.to_thread(self._load_lists_for_write) + lists = await to_thread_settled(self._load_lists_for_write) except StoreCorruptError as exc: # Reads degrade to an empty view; mutations refuse — never # overwrite a corrupt file, which would wipe the lists. @@ -290,7 +290,7 @@ async def _manage_list_locked(self, action, list_name, raw_items, owner_pref, us return f"The '{list_name}' list is already empty." count = len(lst["items"]) lst["items"] = [] - await asyncio.to_thread(self._save_lists, lists) + await to_thread_settled(self._save_lists, lists) return f"Cleared {count} item(s) from the '{list_name}' list." if action == "add": @@ -318,7 +318,7 @@ async def _manage_list_locked(self, action, list_name, raw_items, owner_pref, us } ) added.append(name) - await asyncio.to_thread(self._save_lists, lists) + await to_thread_settled(self._save_lists, lists) parts = [] if added: parts.append(f"Added to '{list_name}': {', '.join(added)}") @@ -346,7 +346,7 @@ async def _manage_list_locked(self, action, list_name, raw_items, owner_pref, us removed.append(lst["items"].pop(idx)["name"]) else: not_found.append(name) - await asyncio.to_thread(self._save_lists, lists) + await to_thread_settled(self._save_lists, lists) parts = [] if removed: parts.append(f"Removed from '{list_name}': {', '.join(removed)}") @@ -377,7 +377,7 @@ async def _manage_list_locked(self, action, list_name, raw_items, owner_pref, us break if not found: not_found.append(name.strip()) - await asyncio.to_thread(self._save_lists, lists) + await to_thread_settled(self._save_lists, lists) parts = [] if marked: parts.append(f"Marked done: {', '.join(marked)}") @@ -405,7 +405,7 @@ async def _manage_list_locked(self, action, list_name, raw_items, owner_pref, us break if not found: not_found.append(name.strip()) - await asyncio.to_thread(self._save_lists, lists) + await to_thread_settled(self._save_lists, lists) parts = [] if marked: parts.append(f"Marked undone: {', '.join(marked)}") diff --git a/src/tools/http_probe_ops.py b/src/tools/http_probe_ops.py index 4efc28a56..6f0cda4ed 100644 --- a/src/tools/http_probe_ops.py +++ b/src/tools/http_probe_ops.py @@ -172,6 +172,11 @@ def build_http_probe_command(params: dict) -> str: headers = params.get("headers") if isinstance(headers, dict): for name, value in headers.items(): + # curl interprets -H @file as a request to read local file contents. + if str(name).startswith("@"): + raise ValueError( + f"Invalid header name {str(name)!r}: header names cannot start with '@'" + ) header_str = f"{name}: {value}" parts.append(f"-H {_sq(header_str)}") @@ -191,7 +196,9 @@ def build_http_probe_command(params: dict) -> str: raise ValueError( f"Request body is {body_bytes} bytes, over the {MAX_BODY_SIZE}-byte limit" ) - parts.append(f"-d {_sq(body)}") + # -d @path reads a local file; --data-raw sends the literal bytes. + flag = "--data-raw" if body.startswith("@") else "-d" + parts.append(f"{flag} {_sq(body)}") # URL (always last) parts.append(_sq(url)) diff --git a/src/tools/post_validation.py b/src/tools/post_validation.py index 0b0290254..07e4c2523 100644 --- a/src/tools/post_validation.py +++ b/src/tools/post_validation.py @@ -261,6 +261,38 @@ def _default_compare_for(check_type: str) -> str: }.get(check_type, "equals") +# Filter the probe's own command line, not ancestors of the probe: a real +# target can itself be an ancestor. Concurrent process probes carry this same +# marker and are filtered as well. procps and BSD/macOS both print command +# lines with -a -l -f; the inner POSIX shell works under other login shells. +_PROCESS_PROBE_PGREP = "pgrep -a -l -f --" +_PROCESS_PROBE_SCRIPT = ( + f'o=$({_PROCESS_PROBE_PGREP} "$1"); r=$?; ' + 'if [ "$r" -gt 1 ]; then echo "PROCESS_CHECK_ERROR pgrep exit $r"; exit 0; fi; ' + f'm=$(printf "%s\\n" "$o" | grep -v -F -e "{_PROCESS_PROBE_PGREP} "); ' + 'if [ -n "$m" ]; then echo PRESENT; else echo ABSENT; fi' +) + + +# Validate the ERE first, then check journal visibility without -q; the data +# run uses -q to keep journalctl's own status lines out of the matches. +_LOG_PROBE_SCRIPT = ( + 'p="$3"; ' + 'printf "" | grep -E -e "$p" >/dev/null 2>&1; ' + 'if [ "$?" -gt 1 ]; then echo "LOG_CHECK_ERROR invalid pattern"; ' + 'printf "" | grep -E -e "$p" 2>&1 | head -n 2; exit 0; fi; ' + 'if [ -n "$1" ]; then set -- -u "$1" --since "$2 seconds ago"; ' + 'else set -- --since "$2 seconds ago"; fi; ' + 'e=$(journalctl "$@" --no-pager -n 1 2>&1 >/dev/null); r=$?; ' + 'if [ "$r" -ne 0 ]; then echo "LOG_CHECK_ERROR journalctl exit $r"; ' + 'printf "%s\\n" "$e" | tail -n 1; exit 0; fi; ' + 'journalctl "$@" --no-pager -q 2>/dev/null | grep -E -e "$p" | head -n 20; ' + 'case "$e" in *"not seeing messages from"*|*"insufficient permissions"*' + '|*"No journal files were found"*) echo LOG_READ_PARTIAL;; *) echo LOG_READ_OK;; esac' +) +_LOG_STATUS_LINES = frozenset({"LOG_READ_OK", "LOG_READ_PARTIAL"}) + + def _build_command(check: Check) -> str | None: """Build the shell command the check will execute on the target host. @@ -271,10 +303,10 @@ def _build_command(check: Check) -> str | None: timeout = check.timeout_seconds if t == "http": - # curl with redirect following; print status on stderr-safe line + # HTTP errors are received responses; only transport errors fail curl. url = shlex.quote(tgt) return ( - f"curl -fsS -o /dev/null -w '%{{http_code}}' " + f"curl -sS -o /dev/null -w '%{{http_code}}' " f"--max-time {timeout} -L {url} || echo FAILED_$?" ) if t == "port": @@ -298,36 +330,45 @@ def _build_command(check: Check) -> str | None: if t == "service": return f"systemctl is-active {shlex.quote(tgt)} 2>/dev/null || true" if t == "process": - return f"pgrep -f {shlex.quote(tgt)} >/dev/null && echo PRESENT || echo ABSENT" + return f"sh -c {shlex.quote(_PROCESS_PROBE_SCRIPT)} odin-process-check {shlex.quote(tgt)}" if t in ("log_absent", "log_present"): # target format: "unit=:pattern" or just "pattern" (journalctl without unit) + unit = "" + pattern = tgt if tgt.startswith("unit="): rest = tgt[5:] if ":" not in rest: return None unit, pattern = rest.split(":", 1) - return ( - f"journalctl -u {shlex.quote(unit)} --since '{check.window_seconds} seconds ago' " - f"--no-pager 2>/dev/null | grep -E {shlex.quote(pattern)} | head -20 || true" - ) - pattern = tgt return ( - f"journalctl --since '{check.window_seconds} seconds ago' " - f"--no-pager 2>/dev/null | grep -E {shlex.quote(pattern)} | head -20 || true" + f"sh -c {shlex.quote(_LOG_PROBE_SCRIPT)} odin-log-check " + f"{shlex.quote(unit)} {int(check.window_seconds)} {shlex.quote(pattern)}" ) if t == "command": return tgt return None +def _strip_log_status(output: str) -> str: + """Only matched journal lines, without the probe's own status sentinel.""" + return "\n".join( + line for line in output.strip().splitlines() if line.strip() not in _LOG_STATUS_LINES + ).strip() + + def _evaluate(check: Check, exit_code: int, output: str) -> tuple[str, str]: """Returns (status, error_message). status in pass/fail/error.""" compare = check.compare or _default_compare_for(check.type) out_stripped = output.strip() if check.type == "http": - if out_stripped.startswith("FAILED_"): - return "fail", f"curl failed: {out_stripped}" + # curl's stderr may precede or follow the marker, and a status code + # may precede a failure after headers (404FAILED_28). + failed = re.search(r"(?:\d{3})?FAILED_(\d+)", out_stripped) + if failed: + detail = (out_stripped[: failed.start()] + out_stripped[failed.end() :]).strip() + reason = f"curl failed (exit {failed.group(1)})" + return "fail", f"{reason}: {detail[:200]}" if detail else reason status_code = out_stripped expected = check.expected if expected is None: @@ -372,18 +413,39 @@ def _evaluate(check: Check, exit_code: int, output: str) -> tuple[str, str]: return "fail", f"expected state '{expected}', got '{out_stripped}'" if check.type == "process": - if "PRESENT" in out_stripped: + lines = {line.strip() for line in out_stripped.splitlines()} + if any(line.startswith("PROCESS_CHECK_ERROR") for line in lines): + return "error", f"pgrep could not run: {out_stripped[:200]}" + if "PRESENT" in lines: return "pass", "" - return "fail", f"no process matching '{check.target}'" - - if check.type == "log_absent": - if out_stripped: - return "fail", f"unexpected log lines: {out_stripped[:200]}" - return "pass", "" - - if check.type == "log_present": - if out_stripped: + if "ABSENT" in lines: + return "fail", f"no process matching '{check.target}'" + return "error", f"process check produced no result: {out_stripped[:200]}" + + if check.type in ("log_absent", "log_present"): + log_lines = out_stripped.splitlines() + if any(line.startswith("LOG_CHECK_ERROR") for line in log_lines): + return "error", f"log check could not run: {out_stripped[:200]}" + status_lines = [line.strip() for line in log_lines if line.strip() in _LOG_STATUS_LINES] + if not status_lines: + return "error", f"log check produced no result: {out_stripped[:200]}" + partial = "LOG_READ_PARTIAL" in status_lines + matches = _strip_log_status(out_stripped) + blind = ( + "the journal is not fully readable here (the user running this check " + "needs journal access, e.g. the systemd-journal group, or the host " + "keeps no journal); " + ) + if check.type == "log_absent": + if matches: + return "fail", f"unexpected log lines: {matches[:200]}" + if partial: + return "error", blind + "cannot confirm the pattern is absent" return "pass", "" + if matches: + return "pass", "" + if partial: + return "error", blind + "cannot confirm the pattern is missing" return "fail", f"no log lines matched '{check.target}' in window" if check.type == "command": @@ -554,7 +616,10 @@ async def _run_one(idx: int, check: Check) -> CheckResult: result.status = "error" result.error = f"timed out after {check.timeout_seconds}s" return result - result.observed = output.strip()[:500] + observed = output.strip() + if check.type in ("log_absent", "log_present"): + observed = _strip_log_status(observed) + result.observed = observed[:500] status, err = _evaluate(check, exit_code, output) result.status = status result.error = err @@ -567,8 +632,17 @@ async def _run_one(idx: int, check: Check) -> CheckResult: finally: result.duration_ms = int((time.monotonic() - t0) * 1000) - coros = [_run_one(i, c) for i, c in enumerate(checks)] - results = await asyncio.gather(*coros) + # A sibling command mentioning the pattern must not satisfy a process + # check while the real process is down. Keep original report order. + results: list[CheckResult] = [None] * len(checks) # type: ignore[list-item] + phases = ( + [i for i, check in enumerate(checks) if check.type == "process"], + [i for i, check in enumerate(checks) if check.type != "process"], + ) + for phase in phases: + phase_results = await asyncio.gather(*(_run_one(i, checks[i]) for i in phase)) + for i, result in zip(phase, phase_results, strict=True): + results[i] = result passed = sum(1 for r in results if r.status == "pass") failed = sum(1 for r in results if r.status == "fail") diff --git a/src/tools/time_parser.py b/src/tools/time_parser.py index 053ca9aa8..df4a4f515 100644 --- a/src/tools/time_parser.py +++ b/src/tools/time_parser.py @@ -7,7 +7,7 @@ from __future__ import annotations import re -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from zoneinfo import ZoneInfo _default_tz = ZoneInfo("UTC") @@ -64,13 +64,32 @@ def set_default_timezone(tz_name: str) -> None: "w": 604800, } +_DAY_WORDS = "|".join(sorted(DAY_NAMES, key=len, reverse=True)) +_MONTHS = ( + "january|february|march|april|june|july|august|september|october|november|december" +) +_TIME_12H = re.compile(r"(\d{1,2})(?::(\d{2}))?\s*(am|pm)") +_TIME_24H = re.compile(r"(\d{1,2}):(\d{2})$") +_MORE_DURATION = re.compile(r"\s*(?:,\s*)?(?:and\s+)?(\d+)\s+(\w+)") +_CLOCK_LEAD = re.compile(r"[\s,]*(?:at\s+)?") +_DAY_AFTER_TIME = re.compile( + r"[\s,]*(?:on\s+)?(?:(tomorrow)|(?:next\s+)?(" + _DAY_WORDS + r"))\b" +) +_CONTINUES_TIME = re.compile( + r"[\s,]*(?:(?:and|at|on|by)\s+)?(?:the\s+)?" + + r"(?:\d|(?:an?\s+)?(?:half|quarter)\b|(?:tomorrow|noon|midnight)\b" + + r"|(?:(?:next|this)\s+)?(?:" + _DAY_WORDS + r")\b" + + r"|(?:next|this)\s+(?:week|weekend|month|year)\b|(?:" + _MONTHS + + r")\b|may\s+\d)" +) -def _parse_time_of_day(text: str) -> tuple[int, int] | None: - """Extract hour and minute from a time expression like '9am', '3:30pm', '17:00'.""" + +def _split_time_of_day(text: str) -> tuple[tuple[int, int], str] | None: + """Return a leading clock time and the unconsumed text.""" text = text.strip().lower() # 12-hour: 9am, 9:30pm, 9:30 am - m = re.match(r"(\d{1,2})(?::(\d{2}))?\s*(am|pm)", text) + m = _TIME_12H.match(text) if m: hour = int(m.group(1)) minute = int(m.group(2) or 0) @@ -78,17 +97,67 @@ def _parse_time_of_day(text: str) -> tuple[int, int] | None: hour += 12 elif m.group(3) == "am" and hour == 12: hour = 0 - return (hour, minute) + return (hour, minute), text[m.end() :] # 24-hour: 17:00, 09:30 - m = re.match(r"(\d{1,2}):(\d{2})$", text) + m = _TIME_24H.match(text) if m: - return (int(m.group(1)), int(m.group(2))) + return (int(m.group(1)), int(m.group(2))), "" # Bare hour: "9" — too ambiguous, skip return None +def _local(instant: datetime, tz) -> datetime: + return instant.astimezone(UTC).astimezone(tz) + + +def _at_clock(day: datetime, hour: int, minute: int) -> datetime: + local_time = day.replace(hour=hour, minute=minute, second=0, microsecond=0, fold=0) + return _local(local_time, day.tzinfo) + + +def _reject_unused(rest: str, expression: str) -> None: + if _CONTINUES_TIME.match(rest): + raise ValueError( + f"Cannot parse time expression: '{expression}' — could not use " + f"'{rest.strip(' ,')}'. Try formats like: 'in 1 hour 30 minutes', " + "'tomorrow at 9am', 'next Monday at 3pm', 'at 5pm tomorrow'" + ) + + +def _clock_then_day(now: datetime, hit, expression: str) -> datetime: + (hour, minute), rest = hit + day = _DAY_AFTER_TIME.match(rest) + if day: + target = ( + now + timedelta(days=1) + if day.group(1) + else _next_weekday(now, DAY_NAMES[day.group(2)]) + ) + _reject_unused(rest[day.end() :], expression) + return _at_clock(target, hour, minute) + _reject_unused(rest, expression) + result = _at_clock(now, hour, minute) + return _at_clock(now + timedelta(days=1), hour, minute) if result <= now else result + + +def _time_after_day(rest: str, expression: str) -> tuple[int, int]: + lead = re.match(r"\s+(?:at\s+)?", rest) + hit = _split_time_of_day(rest[lead.end() :]) if lead else None + if hit is None: + found = re.search(r"at\s+(.+)", rest) + if found: + hit = _split_time_of_day(found.group(1)) + if hit is None: + raise ValueError(f"Cannot parse time: {found.group(1)}") + if hit is None: + _reject_unused(rest, expression) + return 9, 0 + _reject_unused(hit[1], expression) + return hit[0] + + def _next_weekday(now: datetime, target_weekday: int) -> datetime: """Return the next occurrence of target_weekday after now.""" days_ahead = target_weekday - now.weekday() @@ -98,115 +167,104 @@ def _next_weekday(now: datetime, target_weekday: int) -> datetime: def parse_time(expression: str, now: datetime | None = None) -> str: - """Parse a natural language time expression into an ISO datetime string. - - Args: - expression: Natural language like 'in 2 hours', 'tomorrow at 9am', - 'next Monday at 3pm', 'at 5pm'. - now: Reference time (defaults to current time in configured timezone). - - Returns: - ISO datetime string with timezone (e.g. '2026-03-18T17:00:00-04:00'). - - Raises: - ValueError: If the expression cannot be parsed. - """ + """Parse a natural language time expression into an ISO datetime string.""" if now is None: now = datetime.now(_default_tz) elif now.tzinfo is None: now = now.replace(tzinfo=_default_tz) - text = expression.strip().lower() - # --- "in X units" --- m = re.match(r"in\s+(\d+)\s+(\w+)", text) if m: - amount = int(m.group(1)) - unit = m.group(2) - if unit not in UNIT_SECONDS: - raise ValueError(f"Unknown time unit: {unit}") - result = now + timedelta(seconds=amount * UNIT_SECONDS[unit]) + days = seconds = 0 + clock = None + elapsed_units = False + while m: + amount, unit = int(m.group(1)), m.group(2) + if unit not in UNIT_SECONDS: + raise ValueError(f"Unknown time unit: {unit}") + if UNIT_SECONDS[unit] >= 86400: + days += amount * UNIT_SECONDS[unit] // 86400 + else: + elapsed_units = True + seconds += amount * UNIT_SECONDS[unit] + pos = m.end() + lead = _CLOCK_LEAD.match(text, pos) + clock = _split_time_of_day(text[lead.end() :]) if lead is not None else None + if clock: + break + m = _MORE_DURATION.match(text, pos) + if clock: + if elapsed_units: + raise ValueError( + f"Cannot parse time expression: '{expression}' — a clock time can " + "follow days or weeks ('in 2 days at 9am'), not hours or minutes" + ) + _reject_unused(clock[1], expression) + return _at_clock(now + timedelta(days=days), *clock[0]).isoformat() + _reject_unused(text[pos:], expression) + result = now + if days: + result = _local((now + timedelta(days=days)).replace(fold=0), now.tzinfo) + if seconds: + result = _local(result.astimezone(UTC) + timedelta(seconds=seconds), now.tzinfo) return result.isoformat() - # --- "tomorrow [at TIME]" --- if text.startswith("tomorrow"): - tomorrow = now + timedelta(days=1) time_part = re.sub(r"^tomorrow\s*(at\s*)?", "", text).strip() if time_part: - parsed = _parse_time_of_day(time_part) - if parsed is None: + hit = _split_time_of_day(time_part) + if hit is None: raise ValueError(f"Cannot parse time: {time_part}") - hour, minute = parsed + _reject_unused(hit[1], expression) + hour, minute = hit[0] else: - hour, minute = 9, 0 # default 9am - result = tomorrow.replace(hour=hour, minute=minute, second=0, microsecond=0) - return result.isoformat() + hour, minute = 9, 0 + return _at_clock(now + timedelta(days=1), hour, minute).isoformat() - # --- "today [at TIME]" --- if text.startswith("today"): time_part = re.sub(r"^today\s*(at\s*)?", "", text).strip() if time_part: - parsed = _parse_time_of_day(time_part) - if parsed is None: + hit = _split_time_of_day(time_part) + if hit is None: raise ValueError(f"Cannot parse time: {time_part}") - hour, minute = parsed + _reject_unused(hit[1], expression) + hour, minute = hit[0] else: raise ValueError("'today' requires a time (e.g. 'today at 5pm')") - result = now.replace(hour=hour, minute=minute, second=0, microsecond=0) - return result.isoformat() + return _at_clock(now, hour, minute).isoformat() - # --- "next DAYNAME [at TIME]" --- - m = re.match(r"next\s+(\w+)(?:\s+at\s+(.+))?", text) - if m: - day_str = m.group(1) - if day_str in DAY_NAMES: - target_day = _next_weekday(now, DAY_NAMES[day_str]) - time_str = m.group(2) - if time_str: - parsed = _parse_time_of_day(time_str) - if parsed is None: - raise ValueError(f"Cannot parse time: {time_str}") - hour, minute = parsed - else: - hour, minute = 9, 0 # default 9am - result = target_day.replace(hour=hour, minute=minute, second=0, microsecond=0) - return result.isoformat() + m = re.match(r"next\s+(\w+)", text) + if m and m.group(1) in DAY_NAMES: + target_day = _next_weekday(now, DAY_NAMES[m.group(1)]) + rest = text[m.end() :] + at = re.match(r"\s+at\s+(.+)", rest) + if at: + hit = _split_time_of_day(at.group(1)) + if hit is None: + raise ValueError(f"Cannot parse time: {at.group(1)}") + _reject_unused(hit[1], expression) + hour, minute = hit[0] + else: + hour, minute = _time_after_day(rest, expression) + return _at_clock(target_day, hour, minute).isoformat() - # --- "DAYNAME [at TIME]" (without "next") --- first_word = text.split()[0] if text.split() else "" if first_word in DAY_NAMES: target_day = _next_weekday(now, DAY_NAMES[first_word]) - time_match = re.search(r"at\s+(.+)", text) - if time_match: - parsed = _parse_time_of_day(time_match.group(1)) - if parsed is None: - raise ValueError(f"Cannot parse time: {time_match.group(1)}") - hour, minute = parsed - else: - hour, minute = 9, 0 - result = target_day.replace(hour=hour, minute=minute, second=0, microsecond=0) - return result.isoformat() + hour, minute = _time_after_day(text[len(first_word) :], expression) + return _at_clock(target_day, hour, minute).isoformat() - # --- "at TIME" (today if future, tomorrow if past) --- m = re.match(r"at\s+(.+)", text) if m: - parsed = _parse_time_of_day(m.group(1)) - if parsed is None: + hit = _split_time_of_day(m.group(1)) + if hit is None: raise ValueError(f"Cannot parse time: {m.group(1)}") - hour, minute = parsed - result = now.replace(hour=hour, minute=minute, second=0, microsecond=0) - if result <= now: - result += timedelta(days=1) - return result.isoformat() + return _clock_then_day(now, hit, expression).isoformat() - # --- bare time like "5pm", "9:30am" --- - parsed = _parse_time_of_day(text) - if parsed: - hour, minute = parsed - result = now.replace(hour=hour, minute=minute, second=0, microsecond=0) - if result <= now: - result += timedelta(days=1) - return result.isoformat() + hit = _split_time_of_day(text) + if hit: + return _clock_then_day(now, hit, expression).isoformat() raise ValueError( f"Cannot parse time expression: '{expression}'. " diff --git a/src/tools/tool_text.py b/src/tools/tool_text.py index 0fc73a700..dea5401c6 100644 --- a/src/tools/tool_text.py +++ b/src/tools/tool_text.py @@ -8,6 +8,9 @@ from __future__ import annotations +import json +import re + from .result_capture import capture_active # String prefixes that mark a handler's plain-string return as an error @@ -21,6 +24,51 @@ "Unknown or disallowed host", ) +# Stable, non-sensitive reason labels for common tool failure results. +_FAILURE_REASONS = ( + (re.compile(r"Blocked\b"), "blocked"), + (re.compile(r"Unknown or disallowed host"), "disallowed host"), + (re.compile(r"Command failed"), "command failed"), + (re.compile(r"Script failed"), "script failed"), + (re.compile(r"(?:Permission denied|Denied)\b"), "permission denied"), + ( + re.compile(r"(?:Tool '?[^\s']+'? timed out|Error: tool '[^']+' timed out|Timeout)"), + "timed out", + ), + (re.compile(r"Tool '?[^\s']+'? input error"), "input error"), + (re.compile(r"Tool '[^']+' cancelled"), "cancelled"), + (re.compile(r"Unknown tool"), "unknown tool"), + (re.compile(r"\[Interrupted before execution"), "not executed"), + (re.compile(r"\[Interrupted: outcome unknown"), "outcome unknown"), + (re.compile(r"\[Interrupted observation"), "interrupted"), + (re.compile(r"Failed to "), "failed"), + (re.compile(r"Error \(tool reported failure\):"), "tool reported failure"), + (re.compile(r"(?:Error|error|ERROR)"), "error"), +) +_ENVELOPE_START = '{"kind":"tool_output","status":"' + + +def failure_reason(text: object) -> str | None: + """Return a fixed short reason when tool result text reports failure.""" + if not isinstance(text, str): + return None + body = text.lstrip() + if body.startswith(_ENVELOPE_START): + try: + envelope = json.loads(body) + except ValueError: + return None + status = envelope.get("status") if isinstance(envelope, dict) else None + if not isinstance(status, str) or status == "succeeded": + return None + head = envelope.get("head", envelope.get("text", "")) + return failure_reason(head) or status.replace("_", " ") + for pattern, reason in _FAILURE_REASONS: + if pattern.match(body): + return reason + return None + + # Maximum lines of output from run_command / run_command_multi before # truncation for direct helpers. Retained tool delivery bypasses this legacy cut. _RUN_COMMAND_MAX_LINES = 200 diff --git a/src/web/api/knowledge_mem.py b/src/web/api/knowledge_mem.py index 40d67f286..25b66bc7e 100644 --- a/src/web/api/knowledge_mem.py +++ b/src/web/api/knowledge_mem.py @@ -14,6 +14,7 @@ from aiohttp import web +from ...async_utils import to_thread_settled from ...json_store import StoreCorruptError from ...odin_log import get_logger from ...search.errors import InvalidSearchQuery @@ -445,13 +446,13 @@ async def set_memory(request: web.Request) -> web.Response: # interleave with a tool-path write; refuse (never overwrite) on corrupt. async with bot.tool_executor._memory_lock: try: - all_mem = await asyncio.to_thread(bot.tool_executor._load_all_memory) + all_mem = await to_thread_settled(bot.tool_executor._load_all_memory) except StoreCorruptError: return web.json_response(corrupt_write, status=409) if scope not in all_mem: all_mem[scope] = {} all_mem[scope][key] = str(value) - await asyncio.to_thread(bot.tool_executor._save_all_memory, all_mem) + await to_thread_settled(bot.tool_executor._save_all_memory, all_mem) return web.json_response({"status": "saved", "scope": scope, "key": key}) @routes.delete("/api/memory/{scope}/{key}") @@ -463,14 +464,14 @@ async def delete_memory(request: web.Request) -> web.Response: return denied async with bot.tool_executor._memory_lock: try: - all_mem = await asyncio.to_thread(bot.tool_executor._load_all_memory) + all_mem = await to_thread_settled(bot.tool_executor._load_all_memory) except StoreCorruptError: return web.json_response(corrupt_write, status=409) section = all_mem.get(scope, {}) if key not in section: return web.json_response({"error": "key not found"}, status=404) del all_mem[scope][key] - await asyncio.to_thread(bot.tool_executor._save_all_memory, all_mem) + await to_thread_settled(bot.tool_executor._save_all_memory, all_mem) return web.json_response({"status": "deleted", "scope": scope, "key": key}) @routes.post("/api/memory/bulk-delete") @@ -502,7 +503,7 @@ async def bulk_delete_memory(request: web.Request) -> web.Response: validated_entries.append((scope, key)) async with bot.tool_executor._memory_lock: try: - all_mem = await asyncio.to_thread(bot.tool_executor._load_all_memory) + all_mem = await to_thread_settled(bot.tool_executor._load_all_memory) except StoreCorruptError: return web.json_response(corrupt_write, status=409) deleted = 0 @@ -511,7 +512,7 @@ async def bulk_delete_memory(request: web.Request) -> web.Response: del all_mem[scope][key] deleted += 1 if deleted: - await asyncio.to_thread(bot.tool_executor._save_all_memory, all_mem) + await to_thread_settled(bot.tool_executor._save_all_memory, all_mem) return web.json_response({"status": "deleted", "count": deleted}) diff --git a/tests/characterization/test_chat_tool_loop.py b/tests/characterization/test_chat_tool_loop.py index 3c64aa380..48a2a6777 100644 --- a/tests/characterization/test_chat_tool_loop.py +++ b/tests/characterization/test_chat_tool_loop.py @@ -185,6 +185,25 @@ async def test_executor_routed_tool_dispatch(self): class TestToolFailurePaths: + async def test_failed_tool_marks_recent_actions_error(self): + bot, _fake = build( + [ + tool_call_response(("run_command", {"host": "h", "command": "echo hello"})), + text_response("noted"), + ] + ) + bot.tool_executor.execute = AsyncMock( + return_value=ToolResult( + output="Blocked [critical]: request denied", ok=False, tool_name="run_command" + ) + ) + msg = FakeMessage("check") + await run_loop(bot, msg) + assert any( + "`run_command`" in entry and "→ ERROR" in entry + for entry in bot.channel_state.recent_entries(str(msg.channel.id)) + ) + async def test_ok_false_result_gets_error_prefix(self): """ToolResult(ok=False) without an error prefix is wrapped verbatim as 'Error (tool reported failure):\\n' (PR #130 behavior).""" diff --git a/tests/characterization/test_delivery.py b/tests/characterization/test_delivery.py index 2243ef302..d82a4a0dd 100644 --- a/tests/characterization/test_delivery.py +++ b/tests/characterization/test_delivery.py @@ -80,6 +80,95 @@ async def test_code_fence_reopened_across_chunks(self, bot): assert chunks[0].rstrip().endswith("```") assert chunks[1].startswith("```python\n") + @staticmethod + def _fence_lines(chunk: str) -> list[str]: + return [line for line in chunk.split("\n") if line.startswith("```")] + + async def test_closing_fence_at_split_point_ends_the_chunk(self, bot): + """The closing fence must close the outgoing chunk, not the next one.""" + msg = FakeMessage("q") + text = "```python\n" + "x" * 1977 + "\n```\nnormal prose after code block" + await bot.delivery.send_chunked(msg, text) + assert msg.all_delivered_texts() == [ + "```python\n" + "x" * 1977 + "\n```\n", + "normal prose after code block\n", + ] + + async def test_opening_fence_at_split_point_starts_the_next_chunk(self, bot): + msg = FakeMessage("q") + prose = "p" * 1000 + "\n" + "q" * 985 + text = prose + "\n```python\nprint('hi')\n```\nprose after" + await bot.delivery.send_chunked(msg, text) + assert msg.all_delivered_texts() == [ + prose + "\n", + "```python\nprint('hi')\n```\nprose after\n", + ] + + async def test_block_whose_first_line_does_not_fit_moves_whole(self, bot): + msg = FakeMessage("q") + prose = "p" * 1000 + "\n" + "q" * 950 + code = "echo " + "z" * 60 + text = prose + "\n```bash\n" + code + "\n```\nafter" + await bot.delivery.send_chunked(msg, text) + assert msg.all_delivered_texts() == [prose + "\n", "```bash\n" + code + "\n```\nafter\n"] + + async def test_long_language_tag_continuation_stays_within_limit(self, bot): + msg = FakeMessage("q") + text = "```typescriptreact\n" + "x" * 5000 + "\n```\nprose" + await bot.delivery.send_chunked(msg, text) + chunks = msg.all_delivered_texts() + assert all(len(c) <= DISCORD_MAX_LEN for c in chunks) + assert all(len(self._fence_lines(c)) % 2 == 0 for c in chunks) + assert "".join(chunks).count("x") == 5000 + + async def test_split_inside_a_block_is_unchanged(self, bot): + msg = FakeMessage("q") + text = "```python\n" + "x" * 1900 + "\n" + "y" * 100 + "\n```\nprose after" + await bot.delivery.send_chunked(msg, text) + assert msg.all_delivered_texts() == [ + "```python\n" + "x" * 1900 + "\n\n```", + "```python\n" + "y" * 100 + "\n```\nprose after\n", + ] + + async def test_generated_code_heavy_replies_split_balanced(self, bot): + import random + + rng = random.Random(375) + for _ in range(300): + parts: list[str] = [] + target = rng.randint(2100, 7900) + while sum(len(p) + 1 for p in parts) < target: + if rng.random() < 0.5: + words = ("w" * rng.randint(1, 9) for _ in range(rng.randint(5, 120))) + parts.append(" ".join(words)) + else: + lang = rng.choice(["python", "bash", "", "json"]) + body = "\n".join("x" * rng.randint(1, 100) for _ in range(rng.randint(2, 30))) + parts.append(f"```{lang}\n{body}\n```") + text = "\n".join(parts) + if len(text) > DISCORD_MAX_LEN * 4: + continue + msg = FakeMessage("q") + await bot.delivery.send_chunked(msg, text) + chunks = msg.all_delivered_texts() + for chunk in chunks: + assert len(chunk) <= DISCORD_MAX_LEN + assert len(self._fence_lines(chunk)) % 2 == 0, chunk[-80:] + assert not chunk.endswith("\n\n```") or not chunk.split("\n")[-3].startswith("```") + sent = [ + line for c in chunks for line in c.split("\n") + if line and not line.startswith("```") + ] + want = [line for line in text.split("\n") if line and not line.startswith("```")] + assert sent == want + + def test_close_open_fence(self): + from src.discord.delivery import close_open_fence + + assert close_open_fence("```py\nx = 1") == "```py\nx = 1\n```" + assert close_open_fence("a\n```py\nx\n```\nb") == "a\n```py\nx\n```\nb" + assert close_open_fence("no fences") == "no fences" + async def test_single_overlong_line_is_presplit(self, bot): msg = FakeMessage("q") text = "y" * 5000 # one line, no newlines, still under the 4x file threshold diff --git a/tests/characterization/test_scheduled_events.py b/tests/characterization/test_scheduled_events.py index c0ce59f8d..3c435ca9f 100644 --- a/tests/characterization/test_scheduled_events.py +++ b/tests/characterization/test_scheduled_events.py @@ -93,6 +93,26 @@ async def test_check_failure_posts_and_raises(self, bot_and_channel): class TestScheduledWorkflow: + async def test_truncated_summary_closes_the_cut_code_block(self, bot_and_channel): + bot, channel = bot_and_channel + bot.tool_executor.execute = AsyncMock( + side_effect=[ + ToolResult(output="a" * 1200, tool_name="run_command"), + ToolResult(output="b" * 1200, tool_name="run_command"), + ] + ) + sched = schedule( + action="workflow", + steps=[ + {"tool_name": "run_command", "tool_input": {"host": "h", "command": "a"}}, + {"tool_name": "run_command", "tool_input": {"host": "h", "command": "b"}}, + ], + ) + await bot.scheduled_events._on_scheduled_task(sched) + summary = channel.sent_texts[0] + assert summary.endswith("\n```\n... (truncated)") + assert sum(line.startswith("```") for line in summary.split("\n")) % 2 == 0 + async def test_on_failure_abort_is_default_and_stops_workflow(self, bot_and_channel): bot, channel = bot_and_channel bot.tool_executor.execute = AsyncMock( diff --git a/tests/characterization/test_tool_parity.py b/tests/characterization/test_tool_parity.py index 9957f60c4..cc1c01241 100644 --- a/tests/characterization/test_tool_parity.py +++ b/tests/characterization/test_tool_parity.py @@ -93,9 +93,9 @@ "browser_screenshot": "89e8d695b035d5f2", "browser_read_page": "56cdc41f6ee5b6f1", "browser_read_table": "ca6a51b54774f3a5", - "browser_click": "836c03f7acbc9f6d", - "browser_fill": "6c3832a67ec9f31e", - "browser_evaluate": "03b5e7487d394311", + "browser_click": "30335145eefc79ad", + "browser_fill": "ae2f1903d62e11db", + "browser_evaluate": "81933b7d092f5bfe", "web_search": "387c3cf486568b5d", "fetch_url": "c98dffed630bc8c8", "set_permission": "c12a0a66bb423d59", diff --git a/tests/test_agent_context_overflow.py b/tests/test_agent_context_overflow.py index 6268922bc..5da5bde41 100644 --- a/tests/test_agent_context_overflow.py +++ b/tests/test_agent_context_overflow.py @@ -892,6 +892,8 @@ async def callback(messages, system, tools, *, generation_state): agent = AgentInfo( id="compat", label="c", goal="g", channel_id="c", requester_id="u", requester_name="u" ) + # The rejected payload must exceed the typed target for it to shrink anything. + agent.messages = [{"role": "user", "content": "x" * 2_400_000}] agent.iteration_timeout = 10 assert await _call_llm_with_recovery(agent, callback, "sys", [], generation_state={}) # (1,000,000 - request cap 2,000 - fixed 42,000 wire reserve) * 2.5 chars/token diff --git a/tests/test_agent_transcript_contract.py b/tests/test_agent_transcript_contract.py index 70df75faf..9bb697f20 100644 --- a/tests/test_agent_transcript_contract.py +++ b/tests/test_agent_transcript_contract.py @@ -132,6 +132,45 @@ async def test_multi_call_repeated_names_order_and_identity(provider): assert a.tool_execution_count == 3 and a.tools_used == ["same"] +async def test_agent_reused_provider_call_id_across_replies_executes_both(): + # A compatible endpoint is allowed to reuse call_0 in a later reply. + calls = [] + + async def compatible_provider(messages, *args, **kwargs): + wire = KimiClient._convert_messages(None, messages, "") + calls.append(wire) + if len(calls) < 3: + return {"tool_calls": [{"id": "call_0", "name": "run_command", + "input": {"command": f"synthetic-{len(calls)}"}}]} + return {"text": "done"} + + a = agent() + execute = AsyncMock(side_effect=["first output", "second output"]) + await _run_agent(a, "", [], compatible_provider, execute, max_iterations=4) + assert a.state == AgentState.COMPLETED + assert execute.await_count == 2 + assert [call.args[1]["command"] for call in execute.await_args_list] == [ + "synthetic-1", "synthetic-2" + ] + assert [entry["content"][0]["id"] for entry in a.messages if entry["role"] == "assistant" + and entry["content"] and entry["content"][0].get("type") == "tool_use"] == [ + "call_0", "call_0" + ] + + +async def test_agent_duplicate_id_in_one_reply_still_refused(): + a = agent() + cb = AsyncMock(side_effect=[{"tool_calls": [ + {"id": "call_0", "name": "run_command", "input": {"command": "synthetic-1"}}, + {"id": "call_0", "name": "run_command", "input": {"command": "synthetic-2"}}, + ]}, {"text": "done"}]) + execute = AsyncMock(return_value="ok") + await _run_agent(a, "", [], cb, execute) + assert execute.await_count == 1 + assert a.state == AgentState.COMPLETED + assert "Duplicate tool call identity" in str(a.messages) + + def test_normalization_no_mutation_and_ambiguous_identity_is_not_executed(): calls = [ ToolCall(id="id", name="t", input={"x": 1}), @@ -272,7 +311,12 @@ def test_summary_uses_matching_id_not_result_order(): [{"role": "assistant", "content": [{"type": "tool_use", "id": "a"}]}], [{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "missing"}]}], [{"role": "assistant", "content": [{"type": "tool_use", "id": ""}]}], - cycle(1) + cycle(1), + [{"role": "assistant", "content": [ + {"type": "tool_use", "id": "1"}, {"type": "tool_use", "id": "1"} + ]}, {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": "1"}, + {"type": "tool_result", "tool_use_id": "1"}, + ]}], cycle(1)[:1] + cycle(2), ], ) diff --git a/tests/test_async_utils_settled.py b/tests/test_async_utils_settled.py new file mode 100644 index 000000000..40f493801 --- /dev/null +++ b/tests/test_async_utils_settled.py @@ -0,0 +1,74 @@ +"""Contract of the shared settle helpers extracted from LLMGateway.run_persist_settled.""" +from __future__ import annotations + +import asyncio +import threading + +import pytest + +from src.async_utils import run_persist_settled, to_thread_settled + + +async def test_to_thread_settled_returns_result_and_raises_worker_error(): + assert await to_thread_settled(lambda a, b=0: a + b, 2, b=3) == 5 + with pytest.raises(OSError, match="disk full"): + await to_thread_settled(lambda: (_ for _ in ()).throw(OSError("disk full"))) + + +async def test_cancellation_waits_for_worker_then_reraises(): + entered, release, finished = threading.Event(), threading.Event(), threading.Event() + + def work(): + entered.set() + release.wait(5) + finished.set() + return "written" + + task = asyncio.create_task(to_thread_settled(work)) + await asyncio.to_thread(entered.wait, 5) + task.cancel() + await asyncio.sleep(0) + task.cancel() + for _ in range(20): + await asyncio.sleep(0) + assert not task.done() + release.set() + with pytest.raises(asyncio.CancelledError): + await task + assert finished.is_set() + + +async def test_shutdown_drain_cannot_release_the_lock_early(): + """src/__main__.py cancels every task in asyncio.all_tasks() at shutdown.""" + lock = asyncio.Lock() + entered, release, finished = threading.Event(), threading.Event(), threading.Event() + + def work(): + entered.set() + release.wait(5) + finished.set() + + async def owner(): + async with lock: + await to_thread_settled(work) + + task = asyncio.create_task(owner()) + await asyncio.to_thread(entered.wait, 5) + me = asyncio.current_task() + for _ in range(2): + for other in asyncio.all_tasks(): + if other is not me: + other.cancel() + for _ in range(20): + await asyncio.sleep(0) + assert lock.locked() and not finished.is_set() + release.set() + await asyncio.gather(task, return_exceptions=True) + assert finished.is_set() and not lock.locked() + + +async def test_run_persist_settled_contract_unchanged(): + exc, cancelled = await run_persist_settled(lambda: None) + assert (exc, cancelled) == (None, False) + exc, cancelled = await run_persist_settled(lambda: (_ for _ in ()).throw(OSError("x"))) + assert isinstance(exc, OSError) and cancelled is False diff --git a/tests/test_audit_logger_edge_regressions.py b/tests/test_audit_logger_edge_regressions.py new file mode 100644 index 000000000..d6f3eec71 --- /dev/null +++ b/tests/test_audit_logger_edge_regressions.py @@ -0,0 +1,46 @@ +"""Synthetic audit result envelopes exercise bounded preview edge cases.""" + +import json + +from src.audit.logger import _audit_preview, _cap_audit_text, _cap_tool_input + + +def test_preview_drops_optional_fields_to_fit_tiny_caps(): + text = "secret-free diagnostic" + preview = _audit_preview(text, 70, source={"kind": "process_output", "pid": 99}) + assert len(json.dumps(preview)) <= 70 + assert preview["audit_clipped"] is True + assert preview.get("original_chars") in {None, len(text)} + assert _audit_preview(text, 5) == {} + + +def test_parsed_json_preview_preserves_safe_envelope_facts(): + source = json.dumps({ + "kind": "tool_output", "status": "failed", "retention": "bounded", + "cursor": "opaque-cursor", "head": "x" * 800, + }, separators=(",", ":")) + result = json.loads(_cap_audit_text(source, 220)) + assert result["audit_clipped"] is True + assert result["source"]["kind"] == "tool_output" + assert result["source"]["status"] == "failed" + assert result["source"]["cursor_present"] is True + assert len(json.dumps(result)) <= 220 + + +def test_process_retention_metadata_is_kept_separate_from_body(): + metadata = json.dumps({"kind": "process_output", "pid": 17, "retained_bytes": 900}, + separators=(",", ":")) + source = "line one\npassword=fixture-secret\n[output retention] " + metadata + capped = _cap_audit_text(source, 220) + if capped.startswith("{"): + result = json.loads(capped) + assert result.get("source", {}).get("kind") == "process_output" + assert "fixture-secret" not in capped + + +def test_tool_input_with_unserializable_value_uses_safe_fallback(): + class Broken: + def __str__(self): + raise RuntimeError("cannot stringify") + + assert _cap_tool_input({"value": Broken()}, 200) == "" diff --git a/tests/test_audit_rotation_integrity_snapshot_coverage.py b/tests/test_audit_rotation_integrity_snapshot_coverage.py new file mode 100644 index 000000000..ad84ee5ff --- /dev/null +++ b/tests/test_audit_rotation_integrity_snapshot_coverage.py @@ -0,0 +1,95 @@ +"""Fake-only descriptor failures and alias races in retained audit verification.""" + +import builtins +import json +import os + +import pytest + +import src.audit.logger as logger_mod +from src.audit.logger import AuditLogger +from src.audit.signer import AuditSigner + +KEY = "fixture-signing-key" + + +def _signed_file(path, number): + entry = AuditSigner(KEY).sign({"number": number}) + path.write_text(json.dumps(entry) + "\n") + + +@pytest.mark.asyncio +async def test_fstat_failure_reports_unreadable_generation_and_closes_handle( + tmp_path, monkeypatch, +): + active = tmp_path / "audit.jsonl" + rotated = tmp_path / "audit.jsonl.1" + _signed_file(active, 0) + _signed_file(rotated, 1) + logger = AuditLogger(str(active), hmac_key=KEY, max_files=1) + original_open = builtins.open + original_fstat = os.fstat + failed_handles = [] + + def tracked_open(path, mode): + handle = original_open(path, mode) + if path == rotated: + failed_handles.append(handle) + return handle + + def one_failed_stat(fd): + if failed_handles and fd == failed_handles[0].fileno(): + raise OSError("synthetic fstat failure") + return original_fstat(fd) + + monkeypatch.setattr(logger_mod, "open", tracked_open, raising=False) + monkeypatch.setattr(logger_mod.os, "fstat", one_failed_stat) + report = await logger.verify_integrity() + + assert failed_handles and failed_handles[0].closed + assert report["valid"] is False + assert [(s["file"], s["status"], s["error"]) for s in report["segments"]] == [ + (active.name, "verified", None), (rotated.name, "unreadable", "OSError"), + ] + + +@pytest.mark.asyncio +async def test_hardlinked_rotation_alias_is_not_double_counted(tmp_path): + active = tmp_path / "audit.jsonl" + alias = tmp_path / "audit.jsonl.1" + _signed_file(active, 0) + os.link(active, alias) + logger = AuditLogger(str(active), hmac_key=KEY, max_files=1) + + report = await logger.verify_integrity() + + assert report["valid"] is True + assert report["total"] == report["verified"] == 1 + assert [segment["file"] for segment in report["segments"]] == [active.name] + + +@pytest.mark.asyncio +async def test_unexpected_snapshot_failure_closes_all_prior_descriptors(tmp_path, monkeypatch): + active = tmp_path / "audit.jsonl" + first = tmp_path / "audit.jsonl.1" + second = tmp_path / "audit.jsonl.2" + _signed_file(active, 0) + _signed_file(first, 1) + _signed_file(second, 2) + logger = AuditLogger(str(active), hmac_key=KEY, max_files=2) + original_open = builtins.open + opened = [] + + def interrupted_open(path, mode): + if path == second: + raise RuntimeError("synthetic unexpected snapshot interruption") + handle = original_open(path, mode) + opened.append(handle) + return handle + + monkeypatch.setattr(logger_mod, "open", interrupted_open, raising=False) + with pytest.raises(RuntimeError, match="synthetic unexpected snapshot interruption"): + await logger.verify_integrity() + + assert len(opened) == 2 + assert all(handle.closed for handle in opened) diff --git a/tests/test_audit_signing.py b/tests/test_audit_signing.py index 152aca8b8..5c21a1314 100644 --- a/tests/test_audit_signing.py +++ b/tests/test_audit_signing.py @@ -5,12 +5,15 @@ """ from __future__ import annotations +import asyncio import json +import threading from datetime import UTC from unittest.mock import AsyncMock, MagicMock +import src.audit.logger as logger_mod from src.audit.logger import AuditLogger -from src.audit.signer import GENESIS_HASH, AuditSigner, _canonical, verify_log +from src.audit.signer import GENESIS_HASH, AuditSigner, _canonical, verify_log, verify_segment # --------------------------------------------------------------------------- # _canonical helper @@ -710,6 +713,208 @@ def test_config_with_audit_key(self): # REST API /api/audit/verify # --------------------------------------------------------------------------- +class TestRetainedSegmentVerification: + KEY = "synthetic-test-key-not-a-secret" + + async def _rotated_logger(self, tmp_path, records=3, max_files=3): + audit = AuditLogger(str(tmp_path / "audit.jsonl"), hmac_key=self.KEY, + max_bytes=1, max_files=max_files) + await audit.initialize_chain() + for i in range(records): + await audit.log_event(event_type="test", action=f"act{i}", actor="u", detail=f"d{i}") + return audit + + @staticmethod + def _tamper_first_line(path): + lines = path.read_text().splitlines(keepends=True) + entry = json.loads(lines[0]) + entry["detail"] = "TAMPERED" + lines[0] = json.dumps(entry) + "\n" + path.write_text("".join(lines)) + + @staticmethod + def _statuses(report): + return {seg["file"]: seg["status"] for seg in report["segments"]} + + async def test_tampering_only_a_rotated_file_is_reported(self, tmp_path): + audit = await self._rotated_logger(tmp_path, records=2, max_files=2) + assert (await audit.verify_integrity())["valid"] is True + self._tamper_first_line(tmp_path / "audit.jsonl.1") + report = await audit.verify_integrity() + assert report["valid"] is False + assert report["first_bad_file"] == "audit.jsonl.1" + assert report["first_bad"] == 1 + assert self._statuses(report) == {"audit.jsonl": "verified", "audit.jsonl.1": "broken"} + assert report["segments"][1]["reason"] == "hmac_mismatch" + assert report["durability"] == "durable" + + async def test_every_retained_file_is_counted_when_intact(self, tmp_path): + audit = await self._rotated_logger(tmp_path, records=3, max_files=3) + report = await audit.verify_integrity() + assert report["valid"] is True + assert self._statuses(report) == { + "audit.jsonl": "verified", "audit.jsonl.1": "verified", "audit.jsonl.2": "verified", + } + assert report["verified"] == report["total"] == 3 + + async def test_file_predating_signing_is_unsigned_not_broken(self, tmp_path): + (tmp_path / "audit.jsonl.1").write_text(json.dumps({"action": "legacy"}) + "\n") + audit = await self._rotated_logger(tmp_path, records=0) + await audit.log_event(event_type="test", action="a", actor="u", detail="d") + report = await audit.verify_integrity() + assert report["valid"] is True + assert self._statuses(report)["audit.jsonl.1"] == "unsigned" + assert report["unsigned_prefix"] == 1 + + async def test_unsigned_file_newer_than_a_signed_file_is_a_signing_gap(self, tmp_path): + signer = AuditSigner(self.KEY) + (tmp_path / "audit.jsonl.2").write_text(json.dumps(signer.sign({"action": "older"})) + "\n") + (tmp_path / "audit.jsonl.1").write_text(json.dumps({"action": "stripped"}) + "\n") + report = await (await self._rotated_logger(tmp_path, records=0)).verify_integrity() + assert report["valid"] is False + seg = {s["file"]: s for s in report["segments"]}["audit.jsonl.1"] + assert (seg["status"], seg["reason"]) == ("broken", "signing_gap") + + async def test_unreadable_and_missing_files_are_reported_not_skipped(self, tmp_path): + signer = AuditSigner(self.KEY) + (tmp_path / "audit.jsonl.3").write_text(json.dumps(signer.sign({"action": "old"})) + "\n") + (tmp_path / "audit.jsonl.2").mkdir() + report = await (await self._rotated_logger(tmp_path, records=0)).verify_integrity() + assert report["valid"] is False + statuses = self._statuses(report) + assert statuses["audit.jsonl.1"] == "missing" + assert statuses["audit.jsonl.2"] == "unreadable" + assert statuses["audit.jsonl.3"] == "verified" + + async def test_rotation_during_verification_reads_the_snapshot(self, tmp_path, monkeypatch): + audit = await self._rotated_logger(tmp_path, records=2, max_files=3) + before = sorted(p.name for p in tmp_path.iterdir() if p.name.startswith("audit.jsonl")) + gate = threading.Event() + real = logger_mod.verify_segment + + def gated(handle, size, key): + assert gate.wait(5) + return real(handle, size, key) + + monkeypatch.setattr(logger_mod, "verify_segment", gated) + task = asyncio.create_task(audit.verify_integrity()) + await asyncio.sleep(0.05) + await audit.log_event(event_type="test", action="rotates", actor="u", detail="x") + gate.set() + report = await task + assert report["valid"] is True + assert sorted(s["file"] for s in report["segments"]) == before + + async def test_active_file_is_read_only_to_its_snapshot_size(self, tmp_path): + signer = AuditSigner(self.KEY) + path = tmp_path / "audit.jsonl" + lines = [json.dumps(signer.sign({"n": i})) + "\n" for i in range(3)] + path.write_text("".join(lines) + '{"torn": ') + size = sum(len(line.encode()) for line in lines) + with open(path, "rb") as handle: + result = verify_segment(handle, size, self.KEY) + assert (result["valid"], result["total"]) == (True, 3) + + async def test_read_error_mid_scan_is_reported_unreadable(self, tmp_path, monkeypatch): + audit = await self._rotated_logger(tmp_path, records=2, max_files=2) + + def failing(handle, size, key): + raise OSError("disk went away") + + monkeypatch.setattr(logger_mod, "verify_segment", failing) + report = await audit.verify_integrity() + assert report["valid"] is False + assert set(self._statuses(report).values()) == {"unreadable"} + + async def test_absent_active_file_is_counted_as_empty_verified_segment(self, tmp_path): + audit = await self._rotated_logger(tmp_path, records=0) + report = await audit.verify_integrity() + assert report["valid"] is True + assert self._statuses(report) == {"audit.jsonl": "verified"} + + async def test_oldest_removed_file_cannot_be_detected(self, tmp_path): + audit = await self._rotated_logger(tmp_path, records=3, max_files=3) + (tmp_path / "audit.jsonl.2").unlink() + report = await audit.verify_integrity() + assert report["valid"] is True + assert self._statuses(report) == {"audit.jsonl": "verified", "audit.jsonl.1": "verified"} + + async def test_unsigned_active_file_after_signed_retained_file_is_gap(self, tmp_path): + old = AuditSigner(self.KEY).sign({"action": "old"}) + (tmp_path / "audit.jsonl.1").write_text(json.dumps(old) + "\n") + (tmp_path / "audit.jsonl").write_text('{"action":"unsigned"}\n') + audit = AuditLogger(str(tmp_path / "audit.jsonl"), hmac_key=self.KEY) + report = await audit.verify_integrity() + assert report["first_bad_file"] == "audit.jsonl" + assert report["segments"][0]["reason"] == "signing_gap" + assert report["durability"] == "degraded" + + def test_invalid_utf8_counts_physical_line_after_blank(self, tmp_path): + path = tmp_path / "audit.jsonl" + path.write_bytes(b"\n{\"legacy\":true}\n\xff\n") + with open(path, "rb") as handle: + result = verify_segment(handle, path.stat().st_size, self.KEY) + assert (result["reason"], result["first_bad"], result["unsigned_prefix"]) == ( + "invalid_json", 3, 1, + ) + + def test_verify_segment_reports_non_object(self, tmp_path): + path = tmp_path / "audit.jsonl" + path.write_text("[]\n") + with open(path, "rb") as handle: + result = verify_segment(handle, path.stat().st_size, self.KEY) + assert (result["valid"], result["reason"], result["first_bad"]) == (False, "non_object", 1) + + def test_verify_segment_reports_unsigned_after_signed(self, tmp_path): + path = tmp_path / "audit.jsonl" + signed = AuditSigner(self.KEY).sign({"signed": True}) + path.write_text(json.dumps(signed) + "\n" + json.dumps({"unsigned": True}) + "\n") + with open(path, "rb") as handle: + result = verify_segment(handle, path.stat().st_size, self.KEY) + assert (result["valid"], result["reason"], result["first_bad"]) == ( + False, "unsigned_after_signed", 2, + ) + + def test_verify_segment_converts_entry_verifier_exceptions_to_mismatch( + self, tmp_path, monkeypatch, + ): + path = tmp_path / "audit.jsonl" + path.write_text(json.dumps(AuditSigner(self.KEY).sign({"signed": True})) + "\n") + def broken_verify(*args): + raise ValueError("bad") + + monkeypatch.setattr(AuditSigner, "verify_entry", broken_verify) + with open(path, "rb") as handle: + result = verify_segment(handle, path.stat().st_size, self.KEY) + assert (result["valid"], result["reason"]) == (False, "hmac_mismatch") + + async def test_verify_log_reports_open_exception(self, tmp_path): + path = tmp_path / "audit.jsonl" + path.mkdir() + result = await verify_log(path, self.KEY) + assert result["valid"] is False + assert result["error"] + + async def test_rest_verify_returns_409_for_a_tampered_rotated_file(self, tmp_path): + from aiohttp import web as aio_web + from aiohttp.test_utils import TestClient, TestServer + + from src.web.api import create_api_routes + + audit = await self._rotated_logger(tmp_path, records=2, max_files=2) + self._tamper_first_line(tmp_path / "audit.jsonl.1") + bot = MagicMock() + bot.audit = audit + bot.config = MagicMock() + bot.config.web.api_token = "" + app = aio_web.Application() + app.router.add_routes(create_api_routes(bot)) + async with TestClient(TestServer(app)) as client: + resp = await client.get("/api/audit/verify") + assert resp.status == 409 + data = await resp.json() + assert data["first_bad_file"] == "audit.jsonl.1" + class TestAuditVerifyAPI: def _make_bot(self, audit_logger): bot = MagicMock() diff --git a/tests/test_background_task_failure_visibility.py b/tests/test_background_task_failure_visibility.py index df5520e88..b55ba2cba 100644 --- a/tests/test_background_task_failure_visibility.py +++ b/tests/test_background_task_failure_visibility.py @@ -292,7 +292,7 @@ async def test_ingest_durability_failure_fails_real_background_step(self, tmp_pa ) try: - with patch.object(fts, "index_knowledge_chunk", return_value=False) as failed: + with patch.object(fts, "replace_knowledge_source", return_value=False) as failed: await run_background_task( task, executor, diff --git a/tests/test_browser_automation.py b/tests/test_browser_automation.py index c00eb954b..91ea73fd8 100644 --- a/tests/test_browser_automation.py +++ b/tests/test_browser_automation.py @@ -721,6 +721,55 @@ async def test_old_playwright_is_rejected_instead_of_running_unguarded(self): await mgr._install_request_guard(context) +class TestBrowserCallIsolation: + """Browser interaction descriptions must reflect per-call context isolation.""" + + def test_interaction_descriptions_state_the_fresh_session(self): + from src.tools.registry import TOOL_MAP + + for name in ("browser_click", "browser_fill", "browser_evaluate"): + assert TOOL_MAP[name]["description"].startswith( + "Loads a URL in a fresh browser session" + ), name + assert "follow up with browser_read_page" not in TOOL_MAP["browser_click"]["description"] + assert "submit=true" in TOOL_MAP["browser_fill"]["description"] + assert "Promise is awaited" in TOOL_MAP["browser_evaluate"]["description"] + + @pytest.mark.asyncio + async def test_each_call_gets_its_own_context_closed_afterwards(self): + from src.tools.browser import handle_browser_click, handle_browser_read_page + + contexts = [] + + async def new_context(**_kwargs): + page = MagicMock() + page.goto = AsyncMock() + page.click = AsyncMock() + page.wait_for_timeout = AsyncMock() + page.title = AsyncMock(return_value="Dashboard") + page.inner_text = AsyncMock(return_value="body") + page.url = "https://example.com/dashboard" + context = MagicMock() + context.new_page = AsyncMock(return_value=page) + context.close = AsyncMock() + contexts.append(context) + return context + + mgr = BrowserManager() + mgr._browser = MagicMock() + mgr._browser.is_connected.return_value = True + mgr._browser.new_context = AsyncMock(side_effect=new_context) + mgr._install_request_guard = AsyncMock() + + await handle_browser_click(mgr, {"url": "https://example.com/", "selector": "#go"}) + await handle_browser_read_page(mgr, {"url": "https://example.com/dashboard"}) + + assert len(contexts) == 2 and contexts[0] is not contexts[1] + for context in contexts: + context.close.assert_awaited_once() + context.new_page.assert_awaited_once() + + @pytest.mark.asyncio async def test_real_public_page_loads_to_completion_through_request_guard(): """A deterministic allow-path page must finish through a real guard. diff --git a/tests/test_campaign_a_validation.py b/tests/test_campaign_a_validation.py new file mode 100644 index 000000000..7e797f167 --- /dev/null +++ b/tests/test_campaign_a_validation.py @@ -0,0 +1,444 @@ +"""Wire-level regressions for HTTP and health probes; all servers are loopback fixtures.""" + +from __future__ import annotations + +import asyncio +import os +import secrets +import shlex +import shutil +import socket +import subprocess +import sys +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +import pytest + +from src.tools.http_probe_ops import MAX_BODY_SIZE, build_http_probe_command +from src.tools.post_validation import Check, _build_command, _evaluate, run_bundle +from src.tools.risk_classifier import CommandGovernor + + +@pytest.fixture +def http_server(): + requests = [] + + class Handler(BaseHTTPRequestHandler): + def _receive(self): + length = int(self.headers.get("Content-Length") or 0) + requests.append((self.command, self.rfile.read(length))) + self.send_response(200) + self.send_header("Content-Length", "0") + self.end_headers() + + def do_POST(self): # noqa: N802 - stdlib handler contract + self._receive() + + def do_PUT(self): # noqa: N802 - stdlib handler contract + self._receive() + + def do_PATCH(self): # noqa: N802 - stdlib handler contract + self._receive() + + def do_GET(self): # noqa: N802 - stdlib handler contract + if self.path == "/redirect": + self.send_response(302) + self.send_header("Location", "/404") + self.send_header("Content-Length", "0") + self.end_headers() + return + if self.path == "/stall": + self.send_response(404) + self.send_header("Content-Length", "1024") + self.end_headers() + self.wfile.write(b"x") + self.wfile.flush() + time.sleep(2) + return + code = int(self.path.strip("/")) + self.send_response(code) + self.send_header("Content-Length", "0") + self.end_headers() + + def log_message(self, *args): + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_address[1]}", requests + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + +@pytest.mark.skipif(not shutil.which("curl"), reason="needs curl") +@pytest.mark.parametrize("method", ["POST", "PUT", "PATCH"]) +@pytest.mark.parametrize("kind", ["file", "missing", "stdin", "mention", "bare", "oversized"]) +def test_at_leading_body_sent_literally(http_server, tmp_path, method, kind): + url, received = http_server + file = tmp_path / "data.txt" + file.write_bytes(b"private fixture contents\n") + huge = tmp_path / "huge.txt" + huge.write_bytes(b"x" * (MAX_BODY_SIZE * 4)) + body = { + "file": f"@{file}", + "missing": f"@{tmp_path / 'missing'}", + "stdin": "@-", + "mention": "@here deploy done", + "bare": "@", + "oversized": f"@{huge}", + }[kind] + command = build_http_probe_command({"url": url, "method": method, "body": body}) + subprocess.run( + ["/bin/sh", "-c", command], + stdin=subprocess.DEVNULL, + capture_output=True, + check=True, + timeout=15, + ) + assert received == [(method, body.encode())] + + +@pytest.mark.skipif(not shutil.which("curl"), reason="needs curl") +@pytest.mark.parametrize( + "body", + ["line1\nline2\n", "héllo ✓", "a=b@c", "x" * MAX_BODY_SIZE], + ids=["multiline", "utf8", "inner-at", "at-limit"], +) +def test_other_probe_bodies_unchanged(http_server, body): + url, received = http_server + command = build_http_probe_command({"url": url, "method": "POST", "body": body}) + assert "-d" in shlex.split(command) + subprocess.run(["/bin/sh", "-c", command], capture_output=True, check=True, timeout=15) + assert received == [("POST", body.encode())] + + +def test_header_file_interpretation_rejected(): + with pytest.raises(ValueError, match="header name"): + build_http_probe_command({"url": "https://example.test", "headers": {"@/tmp/a": "v"}}) + + +def test_fixed_shape_process_and_log_probes_keep_governor_approval(): + governor = CommandGovernor() + for check in ( + Check(type="process", target="fixture"), + Check(type="process", target="-fixture"), + Check(type="log_absent", target="unit=fixture.service:ERROR"), + Check(type="log_present", target="-fixture"), + ): + assert governor.check(_build_command(check)).allowed + + +def _shell_check(check: Check, *, env=None): + proc = subprocess.run( + ["/bin/sh", "-c", _build_command(check)], + capture_output=True, + text=True, + env=env, + timeout=15, + ) + return _evaluate(check, proc.returncode, proc.stdout + proc.stderr) + + +@pytest.mark.skipif(not shutil.which("pgrep"), reason="needs pgrep") +def test_absent_process_is_not_its_own_probe(): + token = "odin-absent-" + secrets.token_hex(12) + assert _shell_check(Check(type="process", target=token))[0] == "fail" + + +@pytest.mark.skipif(not shutil.which("pgrep"), reason="needs pgrep") +def test_running_process_and_bad_pattern(tmp_path): + token = str(secrets.randbelow(10**12)).zfill(12) + fixture = subprocess.Popen(["sleep", f"30.{token}"], stdin=subprocess.DEVNULL) + try: + assert _shell_check(Check(type="process", target=token))[0] == "pass" + finally: + fixture.kill() + fixture.wait() + assert _shell_check(Check(type="process", target="odin("))[0] == "error" + for name in ("sh", "grep"): + (tmp_path / name).symlink_to(shutil.which(name)) + assert ( + _shell_check(Check(type="process", target=token), env={"PATH": str(tmp_path)})[0] == "error" + ) + + +@pytest.mark.skipif(not shutil.which("pgrep"), reason="needs pgrep") +def test_process_pattern_cannot_escape_the_shell(tmp_path): + marker = tmp_path / "executed" + check = Check(type="process", target=f"x'; touch {marker}; echo 'odin-absent") + assert _shell_check(check)[0] == "fail" + assert not marker.exists() + + +@pytest.mark.skipif(not shutil.which("pgrep"), reason="needs pgrep") +def test_matching_ancestor_remains_visible(): + token = "odin-ancestor-" + secrets.token_hex(10) + check = Check(type="process", target=token) + # The command is in the environment, not the ancestor's argv. + runner = ( + "import os, subprocess; " + "proc = subprocess.run(['/bin/sh', '-c', os.environ['ODIN_CHECK_CMD']], " + "capture_output=True, text=True); print(proc.stdout + proc.stderr)" + ) + proc = subprocess.run( + [sys.executable, "-c", runner, token], + env={**os.environ, "ODIN_CHECK_CMD": _build_command(check)}, + capture_output=True, + text=True, + timeout=15, + ) + assert _evaluate(check, proc.returncode, proc.stdout)[0] == "pass" + + +@pytest.mark.skipif(not shutil.which("pgrep"), reason="needs pgrep") +async def test_sibling_cannot_make_absent_process_pass(): + token = "odin-absent-" + secrets.token_hex(12) + + async def exec_real(addr, command, user, *, timeout, use_workspace=False): + proc = await asyncio.create_subprocess_exec( + "/bin/sh", + "-c", + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + output, _ = await proc.communicate() + return proc.returncode, output.decode() + + report = await run_bundle( + [ + {"type": "command", "target": f"sleep 1; echo {token}"}, + {"type": "process", "target": token}, + ], + bundle_name="fixture", + default_host="localhost", + resolve_host=lambda _: ("127.0.0.1", "fixture", "linux"), + exec_command=exec_real, + ) + assert [c.status for c in report.checks] == ["pass", "fail"] + + +async def test_process_checks_run_separately_from_other_checks(): + inflight = {"process": 0, "other": 0} + overlaps = [] + + async def fake_exec(addr, command, user, *, timeout, use_workspace=False): + kind = "process" if "pgrep" in command else "other" + other = "other" if kind == "process" else "process" + inflight[kind] += 1 + if inflight[other]: + overlaps.append(kind) + await asyncio.sleep(0.02) + inflight[kind] -= 1 + return 0, "PRESENT" if kind == "process" else "200" + + report = await run_bundle( + [ + {"type": "http", "target": "https://fixture.test/a"}, + {"type": "process", "target": "fixture"}, + {"type": "http", "target": "https://fixture.test/b"}, + ], + bundle_name="fixture", + default_host="localhost", + resolve_host=lambda _: ("127.0.0.1", "fixture", "linux"), + exec_command=fake_exec, + ) + assert overlaps == [] + assert [c.type for c in report.checks] == ["http", "process", "http"] + assert report.verdict == "pass" + + +@pytest.mark.parametrize( + "output,status", + [ + ("PRESENT", "pass"), + ("ABSENT", "fail"), + ("PROCESS_CHECK_ERROR pgrep exit 2\nABSENT", "error"), + ("ssh: connection refused", "error"), + ("", "error"), + ], +) +def test_process_requires_exact_result_line(output, status): + assert _evaluate(Check(type="process", target="nginx"), 0, output)[0] == status + + +def _fake_journal(tmp_path, *, notice="", body="", code=0): + binary = tmp_path / "journalctl" + binary.write_text( + "#!/bin/sh\n" + 'case " $* " in *" -q "*) q=1;; *) q=;; esac\n' + f"[ -n \"$q\" ] || printf '%s\\n' {shlex.quote(notice)} >&2\n" + f"[ -n \"$q\" ] || [ -n {shlex.quote(body)} ] || echo '-- No entries --'\n" + f"[ -z {shlex.quote(body)} ] || printf '%s\\n' {shlex.quote(body)}\n" + f"exit {code}\n" + ) + binary.chmod(0o755) + return {**os.environ, "PATH": f"{tmp_path}:{os.environ['PATH']}"} + + +@pytest.mark.parametrize("target", ["unit=fixture.service:FIXTUREERROR", "FIXTUREERROR"]) +@pytest.mark.parametrize("ctype", ["log_absent", "log_present"]) +@pytest.mark.parametrize( + "notice,code", + [ + ("no journal access", 73), + ("No journal files were opened due to insufficient permissions.", 1), + ("Hint: You are currently not seeing messages from other users and the system.", 0), + ("No journal files were found.", 0), + ], +) +def test_unread_journal_is_error(tmp_path, target, ctype, notice, code): + env = _fake_journal(tmp_path, notice=notice, code=code) + assert _shell_check(Check(type=ctype, target=target), env=env)[0] == "error" + + +def test_invalid_log_pattern_and_journal_metadata(tmp_path): + env = _fake_journal(tmp_path, body="fixture: FIXTUREERROR") + for ctype in ("log_absent", "log_present"): + assert _shell_check(Check(type=ctype, target="FIXTURE(ERROR"), env=env)[0] == "error" + (tmp_path / "journalctl").unlink() + empty_env = _fake_journal(tmp_path) + assert _shell_check(Check(type="log_present", target="entries"), env=empty_env)[0] == "fail" + assert _shell_check(Check(type="log_absent", target="entries"), env=empty_env)[0] == "pass" + + +def test_missing_journalctl_is_error(tmp_path): + for tool in ("sh", "grep", "head", "tail"): + (tmp_path / tool).symlink_to(shutil.which(tool)) + for ctype in ("log_absent", "log_present"): + assert ( + _shell_check(Check(type=ctype, target="fixture"), env={"PATH": str(tmp_path)})[0] + == "error" + ) + + +def test_partial_visibility_keeps_positive_evidence_but_never_proves_absence(tmp_path): + notice = "Hint: You are currently not seeing messages from other users and the system." + env = _fake_journal(tmp_path, notice=notice, body="fixture: FIXTUREERROR") + assert _shell_check(Check(type="log_present", target="FIXTUREERROR"), env=env)[0] == "pass" + assert _shell_check(Check(type="log_absent", target="FIXTUREERROR"), env=env)[0] == "fail" + assert _shell_check(Check(type="log_absent", target="MISSING"), env=env)[0] == "error" + + +async def test_log_observed_text_contains_only_matched_lines(tmp_path): + env = _fake_journal(tmp_path, body="fixture: FIXTUREERROR") + + async def exec_real(addr, command, user, *, timeout, use_workspace=False): + proc = await asyncio.create_subprocess_exec( + "/bin/sh", + "-c", + command, + env=env, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + output, _ = await proc.communicate() + return proc.returncode, output.decode() + + report = await run_bundle( + [{"type": "log_absent", "target": "FIXTUREERROR"}], + bundle_name="fixture", + default_host="localhost", + resolve_host=lambda _: ("127.0.0.1", "fixture", "linux"), + exec_command=exec_real, + ) + assert report.checks[0].status == "fail" + assert report.checks[0].observed == "fixture: FIXTUREERROR" + + +@pytest.mark.parametrize("ctype,expected", [("log_absent", "fail"), ("log_present", "pass")]) +def test_readable_journal_still_matches(tmp_path, ctype, expected): + env = _fake_journal(tmp_path, body="fixture: FIXTUREERROR") + assert _shell_check(Check(type=ctype, target="FIXTUREERROR"), env=env)[0] == expected + + +@pytest.mark.parametrize( + "ctype,output,expected", + [ + ("log_absent", "", "error"), + ("log_present", "", "error"), + ("log_absent", "LOG_READ_OK", "pass"), + ("log_present", "LOG_READ_OK", "fail"), + ("log_absent", "LOG_READ_PARTIAL", "error"), + ("log_present", "match\nLOG_READ_PARTIAL", "pass"), + ("log_absent", "LOG_CHECK_ERROR journalctl exit 1\nLOG_READ_OK", "error"), + ], +) +def test_log_requires_visibility_sentinel(ctype, output, expected): + assert _evaluate(Check(type=ctype, target="match"), 0, output)[0] == expected + + +@pytest.mark.skipif(not shutil.which("curl"), reason="needs curl") +@pytest.mark.parametrize( + "path,expected", + [ + ("404", 404), + ("404", [404, 410]), + ("500", 500), + ("503", [503]), + ("redirect", 404), + ], +) +def test_expected_http_errors_pass(http_server, path, expected): + url, _ = http_server + assert _shell_check(Check(type="http", target=f"{url}/{path}", expected=expected))[0] == "pass" + + +@pytest.mark.skipif(not shutil.which("curl"), reason="needs curl") +def test_http_equals_error_status_and_default_success(http_server): + url, _ = http_server + assert ( + _shell_check(Check(type="http", target=f"{url}/404", expected=404, compare="equals"))[0] + == "pass" + ) + assert _shell_check(Check(type="http", target=f"{url}/200"))[0] == "pass" + + +@pytest.mark.skipif(not shutil.which("curl"), reason="needs curl") +@pytest.mark.parametrize("code", [404, 500]) +def test_unexpected_http_error_still_fails_with_code(http_server, code): + url, _ = http_server + status, detail = _shell_check(Check(type="http", target=f"{url}/{code}")) + assert status == "fail" and detail.endswith(f"got {code}") + + +@pytest.mark.skipif(not shutil.which("curl"), reason="needs curl") +def test_http_transport_failure_after_headers_never_passes(http_server): + url, _ = http_server + status, detail = _shell_check( + Check(type="http", target=f"{url}/stall", expected=404, timeout_seconds=1) + ) + assert status == "fail" and detail.startswith("curl failed (exit 28)") + + +@pytest.mark.skipif(not shutil.which("curl"), reason="needs curl") +def test_http_connection_refused_never_passes(): + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + status, detail = _shell_check( + Check(type="http", target=f"http://127.0.0.1:{port}/", expected=404) + ) + assert status == "fail" and detail.startswith("curl failed (exit 7)") + + +@pytest.mark.parametrize( + "output,status", + [ + ("404", "pass"), + ("FAILED_28", "fail"), + ("curl: (7) refused\n000FAILED_7", "fail"), + ("curl: (28) timeout\n404FAILED_28", "fail"), + ], +) +def test_http_failure_marker_wins_over_received_code(output, status): + got, detail = _evaluate(Check(type="http", target="fixture", expected=404), 0, output) + assert got == status + if "FAILED_" in output: + assert detail.startswith("curl failed (exit ") diff --git a/tests/test_campaign_discord_final_coverage.py b/tests/test_campaign_discord_final_coverage.py index 70e1d2b99..01c411250 100644 --- a/tests/test_campaign_discord_final_coverage.py +++ b/tests/test_campaign_discord_final_coverage.py @@ -174,6 +174,8 @@ async def test_application_startup_completes_services_despite_nonfatal_component bot.codex_quota_check = SimpleNamespace(start=AsyncMock()) bot.computer = SimpleNamespace(start=AsyncMock(side_effect=RuntimeError("desktop unavailable"))) bot.scheduler = SimpleNamespace(start=Mock()) + # object.__new__ bypasses service wiring; optional stores are absent here. + assert not hasattr(bot, "_knowledge_store") bot.scheduled_events = SimpleNamespace( _on_scheduled_task=AsyncMock(), _on_schedule_failure=AsyncMock(), diff --git a/tests/test_campaign_finish_boundaries.py b/tests/test_campaign_finish_boundaries.py index bb9af7096..f740fe92e 100644 --- a/tests/test_campaign_finish_boundaries.py +++ b/tests/test_campaign_finish_boundaries.py @@ -47,15 +47,23 @@ def test_legacy_env_writer_delegates_without_changing_content(tmp_path, monkeypa @pytest.mark.parametrize("count", [0, 3]) -async def test_archive_backfill_logs_zero_or_progress_and_backfills_knowledge(count): +async def test_archive_backfill_logs_zero_or_progress(count): bot = SimpleNamespace( _vector_store=SimpleNamespace(backfill=AsyncMock(return_value=count)), sessions=SimpleNamespace(persist_dir=Path("isolated")), _embedder=object(), - _knowledge_store=SimpleNamespace(backfill_fts=Mock(return_value=count)), - _fts_index=object(), ) await OdinBot._backfill_archives(bot) - bot._knowledge_store.backfill_fts.assert_called_once_with() + bot._vector_store.backfill.assert_awaited_once() + + +@pytest.mark.parametrize("count", [0, 3]) +async def test_knowledge_fts_reconcile_runs_locked_and_survives_failure(count): + bot = SimpleNamespace(_knowledge_store=SimpleNamespace( + backfill_fts_async=AsyncMock(return_value=count))) + await OdinBot._reconcile_knowledge_fts(bot) + bot._knowledge_store.backfill_fts_async.assert_awaited_once_with() + bot._knowledge_store.backfill_fts_async.side_effect = RuntimeError() + await OdinBot._reconcile_knowledge_fts(bot) async def test_archive_failure_does_not_escape_ready_background_work(): diff --git a/tests/test_cancelled_persistence_settles.py b/tests/test_cancelled_persistence_settles.py new file mode 100644 index 000000000..fe5dee771 --- /dev/null +++ b/tests/test_cancelled_persistence_settles.py @@ -0,0 +1,182 @@ +"""#397/#398: a cancelled persistence worker keeps its store lock until the worker +has physically finished, so it can never publish a stale snapshot over a later, +acknowledged write. Real stores in tmp_path; the only fake is a threading gate +that parks the first worker (models a slow disk).""" +from __future__ import annotations + +import asyncio +import json +import threading +from types import SimpleNamespace + +import pytest + +from src.knowledge.store import KnowledgeStore +from src.learning.reflector import ConversationReflector +from src.search.fts import FullTextIndex +from src.tools.executor import ToolExecutor + + +def _gate(obj, attr, monkeypatch, *, unbound=None): + """Park only the FIRST call of obj.attr until release is set.""" + entered, release = threading.Event(), threading.Event() + real = getattr(obj, attr) if unbound is None else unbound + calls = {"n": 0} + + def gated(*args, **kwargs): + calls["n"] += 1 + if calls["n"] == 1: + entered.set() + release.wait(5) + return real(*args, **kwargs) if unbound is None else real(obj, *args, **kwargs) + + monkeypatch.setattr(obj, attr, gated) + return entered, release + + +async def _cancel_while_parked(task, entered, lock): + """Cancel (twice) while the worker is parked; the lock must stay owned.""" + await asyncio.to_thread(entered.wait, 5) + task.cancel() + await asyncio.sleep(0) + task.cancel() + for _ in range(20): + await asyncio.sleep(0) + assert not task.done(), "cancelled caller returned while its worker was still running" + assert lock.locked(), "lock released while the admitted worker was still running" + + +@pytest.fixture +def kstore(tmp_path): + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts_index=fts) + yield store + store.close() + fts._conn.close() + + +def _rows(store, source): + return [r[0] for r in store._conn.execute( + "SELECT content FROM knowledge_chunks WHERE source=? ORDER BY chunk_index", (source,))] + + +async def test_cancelled_ingest_cannot_overwrite_later_stored_document(kstore, monkeypatch): + entered, release = _gate(kstore, "_write_chunks_sync", monkeypatch) + first = asyncio.create_task(kstore.ingest("first stale content", "runbook.md")) + await _cancel_while_parked(first, entered, kstore._write_lock) + second = asyncio.create_task(kstore.ingest("second current content", "runbook.md")) + release.set() + with pytest.raises(asyncio.CancelledError): + await first + outcome = await second + assert outcome.status == "stored" + assert _rows(kstore, "runbook.md") == ["second current content"] + assert kstore.get_source_snapshot("runbook.md") == "second current content" + + +async def test_cancelled_delete_cannot_remove_later_stored_document(kstore, monkeypatch): + await kstore.ingest("version one text", "doc.md") + entered, release = _gate(kstore, "delete_source", monkeypatch) + first = asyncio.create_task(kstore.delete_source_async("doc.md")) + await _cancel_while_parked(first, entered, kstore._write_lock) + second = asyncio.create_task(kstore.ingest("version two text", "doc.md")) + release.set() + with pytest.raises(asyncio.CancelledError): + await first + assert (await second).status == "stored" + assert _rows(kstore, "doc.md") == ["version two text"] + + +@pytest.fixture +def executor(tmp_path): + mem = tmp_path / "memory.json" + mem.write_text(json.dumps({"global": {"old": "X"}})) + return ToolExecutor(memory_path=str(mem)), mem + + +@pytest.mark.parametrize("first_input", [ + {"action": "save", "scope": "global", "key": "cancelled", "value": "A"}, + {"action": "delete", "key": "old"}, +]) +async def test_cancelled_memory_tool_write_cannot_erase_later_save( + executor, monkeypatch, first_input): + ex, mem = executor + entered, release = _gate( + ex, "_save_all_memory", monkeypatch, unbound=ToolExecutor._save_all_memory) + first = asyncio.create_task(ex.state_tools._handle_memory_manage(dict(first_input))) + await _cancel_while_parked(first, entered, ex._memory_lock) + second = asyncio.create_task(ex.state_tools._handle_memory_manage( + {"action": "save", "scope": "global", "key": "later", "value": "B"})) + release.set() + with pytest.raises(asyncio.CancelledError): + await first + assert await second == "Saved global note 'later'." + assert json.loads(mem.read_text())["global"]["later"] == "B" + + +async def test_cancelled_rest_memory_write_cannot_erase_later_save(executor, monkeypatch): + from aiohttp import web + + from src.web.api.knowledge_mem import register_memory_notes + + ex, mem = executor + routes = web.RouteTableDef() + register_memory_notes(routes, SimpleNamespace(tool_executor=ex)) + put = next(r.handler for r in routes if r.method == "PUT") + + def request(key, value): + async def body(): + return {"value": value} + return SimpleNamespace(match_info={"scope": "global", "key": key}, json=body, + _api_identity=None, query={}) + + entered, release = _gate( + ex, "_save_all_memory", monkeypatch, unbound=ToolExecutor._save_all_memory) + first = asyncio.create_task(put(request("cancelled", "A"))) + await _cancel_while_parked(first, entered, ex._memory_lock) + second = asyncio.create_task(put(request("later", "B"))) + release.set() + with pytest.raises(asyncio.CancelledError): + await first + assert (await second).status == 200 + assert json.loads(mem.read_text())["global"]["later"] == "B" + + +async def test_cancelled_list_write_cannot_erase_later_add(executor, monkeypatch): + ex, _mem = executor + st = ex.state_tools + entered, release = _gate(st, "_save_lists", monkeypatch) + first = asyncio.create_task(st._handle_manage_list( + {"action": "add", "list_name": "grocery", "items": ["cancelled-item"]}, user_id="u")) + await _cancel_while_parked(first, entered, ex._lists_lock) + second = asyncio.create_task(st._handle_manage_list( + {"action": "add", "list_name": "grocery", "items": ["later-item"]}, user_id="u")) + release.set() + with pytest.raises(asyncio.CancelledError): + await first + assert "later-item" in await second + names = [i["name"] for i in json.loads(st._lists_path().read_text())["grocery"]["items"]] + assert "later-item" in names + + +async def test_cancelled_learned_delete_cannot_revert_later_update(tmp_path, monkeypatch): + refl = ConversationReflector(learned_path=str(tmp_path / "learned.json"), enabled=True) + store = refl._empty_store() + stamp = "2026-01-01T00:00:00+00:00" + store["entries"] = [ + {"key": k, "category": "operational", "content": c, + "created_at": stamp, "updated_at": stamp} + for k, c in (("a", "Alpha lesson."), ("b", "Beta lesson.")) + ] + refl._save(store) + entered, release = _gate(refl, "_save", monkeypatch) + first = asyncio.create_task(refl.delete_entry_async("a")) + await _cancel_while_parked(first, entered, refl._lock) + second = asyncio.create_task(refl.update_entry_async("b", content="Beta lesson, updated.")) + release.set() + with pytest.raises(asyncio.CancelledError): + await first + assert (await second)["content"] == "Beta lesson, updated." + stored = json.loads((tmp_path / "learned.json").read_text())["entries"] + entries = {e["key"]: e["content"] for e in stored} + assert entries == {"b": "Beta lesson, updated."} diff --git a/tests/test_channel_logger_edge_regressions.py b/tests/test_channel_logger_edge_regressions.py new file mode 100644 index 000000000..aadbb83da --- /dev/null +++ b/tests/test_channel_logger_edge_regressions.py @@ -0,0 +1,71 @@ +"""Synthetic torn-file and reconciliation edge cases for channel indexing.""" + +import json + +from src.discord.channel_logger import ChannelLogger +from src.search.fts import FullTextIndex + + +def _record(identity, content): + return json.dumps({"log_identity": identity, "message_id": "", "content": content}) + + +def test_initial_index_does_not_consume_torn_final_record(tmp_path): + logger = ChannelLogger(tmp_path) + path = tmp_path / "42.jsonl" + path.write_text(_record("complete", "first") + "\n" + _record("torn", "unfinished"), + encoding="utf-8") + fts = FullTextIndex(":memory:") + + assert logger.index_to_fts(fts) == 1 + assert logger.index_to_fts(fts) == 0 + with path.open("a", encoding="utf-8") as stream: + stream.write("\n") + assert logger.index_to_fts(fts) == 1 + + +def test_legacy_record_gets_deterministic_identity_and_message_id(tmp_path): + logger = ChannelLogger(tmp_path) + path = tmp_path / "legacy.jsonl" + line = json.dumps({"content": "legacy body", "message_id": ""}) + path.write_text(line + "\n", encoding="utf-8") + first = logger._index_record(path, 0, line) + again = logger._index_record(path, 0, line) + + assert first == again + assert first["log_identity"].startswith("legacy:") + assert first["message_id"] == first["log_identity"] + assert logger._index_record(path, 0, "[]") is None + assert logger._index_record(path, 0, "{") is None + + +def test_search_user_filter_does_not_leak_into_unfiltered_search(tmp_path): + logger = ChannelLogger(tmp_path) + path = tmp_path / "42.jsonl" + records = [ + {"author_id": "7", "author": "alice", "content": "shared needle", + "channel_id": "42", "ts": 1.0}, + {"author_id": "8", "author": "bob", "content": "shared needle", + "channel_id": "42", "ts": 2.0}, + ] + path.write_text("".join(json.dumps(record) + "\n" for record in records), + encoding="utf-8") + + filtered = logger.search("needle", author_id="7") + unfiltered = logger.search("needle") + + assert [hit["author"] for hit in filtered] == ["alice"] + assert {hit["author"] for hit in unfiltered} == {"alice", "bob"} + + +def test_search_applies_reset_predicate_before_result_limit(tmp_path): + logger = ChannelLogger(tmp_path) + records = [ + {"author_id": "7", "content": "needle retained", "channel_id": "42", "ts": 3}, + {"author_id": "7", "content": "needle reset", "channel_id": "42", "ts": 1}, + ] + (tmp_path / "42.jsonl").write_text( + "".join(json.dumps(record) + "\n" for record in records), encoding="utf-8" + ) + hits = logger.search("needle", limit=1, author_id="7", accept=lambda row: row["ts"] > 2) + assert [hit["content"] for hit in hits] == ["needle retained"] diff --git a/tests/test_channel_state_recent_action_failure.py b/tests/test_channel_state_recent_action_failure.py new file mode 100644 index 000000000..12c283605 --- /dev/null +++ b/tests/test_channel_state_recent_action_failure.py @@ -0,0 +1,13 @@ +from src.discord.channel_state import ChannelStateRegistry + + +def test_track_action_marks_authoritative_failure_error(): + state = ChannelStateRegistry() + state.track_action("run_command", {}, "blocked", 1, channel_id="123", failed=True) + assert "→ ERROR" in state.recent_entries("123")[0] + + +def test_track_action_success_stays_ok(): + state = ChannelStateRegistry() + state.track_action("run_command", {}, "completed", 1, channel_id="123") + assert "→ OK" in state.recent_entries("123")[0] diff --git a/tests/test_compatible_output_reserve.py b/tests/test_compatible_output_reserve.py new file mode 100644 index 000000000..d390fc859 --- /dev/null +++ b/tests/test_compatible_output_reserve.py @@ -0,0 +1,255 @@ +"""Compatible request, admission, runtime and overflow reserve agreement (#386).""" + +from __future__ import annotations + +import logging +from types import SimpleNamespace + +import pytest + +from src.agents.manager import AgentInfo, _call_llm_with_recovery, _compatible_overflow_target_chars +from src.config.schema import ( + OpenAICodexConfig, + OpenAICompatibleConfig, + OpenAICompatibleModelProfile, + OpenRouterRoutingConfig, +) +from src.discord.llm_gateway import LLMServingIdentity +from src.discord.native_tools.agents_tasks import _generation_budget_snapshot +from src.discord.tool_loop import ToolLoopRunner +from src.llm.context_budget import ( + COMPATIBLE_RESCUE_MIN_USABLE_TOKENS, + FIXED_ENVELOPE_RESERVE_TOKENS, + compatible_agent_unavailable_reason, + snapshot_for_compatible_profile, +) +from src.llm.context_compressor import estimate_message_chars +from src.llm.errors import LLMContextLengthError +from src.llm.openai_compatible import OpenAICompatibleClient + + +def _config(model: str, total: int, output: int, *, openrouter: bool) -> OpenAICompatibleConfig: + profile = OpenAICompatibleModelProfile(total_window_tokens=total, max_output_tokens=output) + if openrouter: + return OpenAICompatibleConfig( + preset="openrouter", + base_url="https://openrouter.ai/api/v1", + model=model, + model_profiles={}, + openrouter=OpenRouterRoutingConfig(catalogue_profiles={model: profile}), + ) + return OpenAICompatibleConfig(model=model, model_profiles={model: profile}) + + +def _client(cfg: OpenAICompatibleConfig) -> OpenAICompatibleClient: + return OpenAICompatibleClient( + "test", + model=cfg.model, + base_url=cfg.base_url, + provider_name="compat", + max_tokens=cfg.max_tokens, + model_profiles=cfg.model_profiles, + openrouter_routing=cfg.openrouter if cfg.preset == "openrouter" else None, + ) + + +def _chat_snapshot(model: str, root): + runner = ToolLoopRunner.__new__(ToolLoopRunner) + runner._get_context_compressor = lambda: None + runner._window_observer = None + serving = LLMServingIdentity( + "compat", SimpleNamespace(model=model, provider_name="compat"), model, None + ) + return runner._capture_budget_snapshot(serving, root) + + +CASES = [ + ("deepseek-v4-flash", 1_048_576, 393_216, False), + ("vendor/output-equals-context", 163_840, 163_840, True), + ("vendor/small-output", 200_000, 8_192, False), + ("vendor/at-ceiling", 200_000, 32_768, True), + ("vendor/too-small", 70_000, 10_000, False), +] + + +@pytest.mark.parametrize(("model", "total", "output", "openrouter"), CASES) +def test_request_admission_runtime_and_rescue_reserve_the_same_output( + model, total, output, openrouter, caplog +): + cfg = _config(model, total, output, openrouter=openrouter) + root = SimpleNamespace(openai_codex=OpenAICodexConfig(), openai_compatible=cfg) + client = _client(cfg) + request_cap = client._request_max_tokens(model=model) + chat = _chat_snapshot(model, root) + agent = _generation_budget_snapshot(root, client, model, None, is_codex=False) + assert total - chat.base_budget == request_cap + assert total - agent.base_budget == request_cap + assert chat == agent + + admitted = compatible_agent_unavailable_reason(model, cfg) is None + assert admitted == (chat.working_budget >= COMPATIBLE_RESCUE_MIN_USABLE_TOKENS) + assert admitted == bool(chat.ladder) + + caplog.set_level(logging.WARNING) + plan = {"client": client, "model": model, "is_codex": False, "snapshot": agent} + exc = LLMContextLengthError( + "overflow", code="context_length_exceeded", context_window_tokens=total + ) + expected = max(1, (total - request_cap - FIXED_ENVELOPE_RESERVE_TOKENS) * 2500 // 1000) + assert _compatible_overflow_target_chars(exc, plan) == expected + assert "profile/request output mismatch" not in caplog.text + if output <= 32_768: + # The below-cap and at-cap cases also prove the transition to an + # over-cap profile. Otherwise these parameter rows pass on master. + next_cfg = _config(model, total, 32_769, openrouter=openrouter) + next_root = SimpleNamespace(openai_codex=OpenAICodexConfig(), openai_compatible=next_cfg) + next_cap = _client(next_cfg)._request_max_tokens(model=model) + assert next_cap == 32_768 + assert total - _chat_snapshot(model, next_root).base_budget == next_cap + assert ( + total + - _generation_budget_snapshot( + next_root, _client(next_cfg), model, None, is_codex=False + ).base_budget + == next_cap + ) + + +def test_admitted_profile_never_gets_a_zero_runtime_budget(): + model = "vendor/output-equals-context" + cfg = _config(model, 163_840, 163_840, openrouter=True) + root = SimpleNamespace(openai_codex=OpenAICodexConfig(), openai_compatible=cfg) + assert compatible_agent_unavailable_reason(model, cfg) is None + for snapshot in ( + _chat_snapshot(model, root), + _generation_budget_snapshot(root, _client(cfg), model, None, is_codex=False), + ): + assert snapshot.working_budget == 98_304 + assert snapshot.primary_chars == 140_760 + assert snapshot.ladder == (98_532,) + + +def test_request_override_and_admission_guards_alongside_runtime_regression(): + cfg = _config("vendor/big-output", 163_840, 163_840, openrouter=False) + client = _client(cfg) + # Request output is unchanged, including explicit overrides and fallback. + assert client._request_max_tokens() == 32_768 + assert client._request_max_tokens(1_234) == 1_234 + unknown = _config("vendor/missing", 163_840, 32_768, openrouter=False) + unknown.model_profiles.clear() + assert _client(unknown)._request_max_tokens() == unknown.max_tokens + # Admission's existing eligibility and reason-string behavior is pinned. + assert compatible_agent_unavailable_reason("vendor/big-output", cfg) is None + assert compatible_agent_unavailable_reason("vendor/missing", cfg) == ( + "no context profile configured" + ) + # These guards share an actual mismatch assertion and fail on master. + assert ( + snapshot_for_compatible_profile( + "vendor/big-output", cfg, max_context_chars=None + ).base_budget + == 131_072 + ) + + +async def test_normal_tool_chat_request_cap_matches_profile_without_changing_wire_body(): + model = "vendor/big-output" + cfg = _config(model, 163_840, 163_840, openrouter=False) + client = _client(cfg) + sent = [] + + async def capture(body): + sent.append(body) + return {"choices": [{"message": {"content": "done"}, "finish_reason": "stop"}]} + + client._request_with_retry = capture + await client.chat_with_tools([{"role": "user", "content": "hello"}], "system", []) + assert sent == [{ + "model": model, + "messages": [ + {"role": "system", "content": "system"}, + {"role": "user", "content": "hello"}, + ], + "tools": [], + "tool_choice": "auto", + "max_tokens": 32_768, + "stream": True, + "stream_options": {"include_usage": True}, + }] + assert cfg.model_profiles[model].total_window_tokens - ( + snapshot_for_compatible_profile(model, cfg, max_context_chars=None).base_budget + ) == sent[0]["max_tokens"] + + +async def test_bare_chat_explicit_output_override_is_not_a_tool_chat_request_change(): + model = "vendor/big-output" + cfg = _config(model, 163_840, 163_840, openrouter=False) + client = _client(cfg) + sent = [] + + async def capture(body): + sent.append(body) + return {"choices": [{"message": {"content": "done"}, "finish_reason": "stop"}]} + + client._request_with_retry = capture + await client.chat([{"role": "user", "content": "hello"}], "system", max_tokens=1_500) + assert sent[0]["max_tokens"] == 1_500 + # Bare chat is used by bounded auxiliary jobs; its explicit override is + # intentionally distinct from the profiled normal chat-with-tools cap. + assert client._request_max_tokens(model=model) == 32_768 + + +def _history(total_chars: int) -> list[dict]: + messages: list[dict] = [{"role": "user", "content": "TASK"}] + chunk = "x" * 50_000 + i = 0 + while estimate_message_chars(messages) < total_chars: + messages.extend( + [ + { + "role": "assistant", + "content": [ + {"type": "tool_use", "id": f"t{i}", "name": "read_file", "input": {"p": i}} + ], + }, + { + "role": "user", + "content": [{"type": "tool_result", "tool_use_id": f"t{i}", "content": chunk}], + }, + ] + ) + i += 1 + return messages + + +async def test_dense_overflow_below_typed_target_compacts_instead_of_resending(): + cfg = OpenAICompatibleConfig() + model = "deepseek-v4-flash" + client = OpenAICompatibleClient( + "fixture", model=model, provider_name="compat", model_profiles=cfg.model_profiles + ) + snapshot = snapshot_for_compatible_profile(model, cfg, max_context_chars=None) + agent = AgentInfo( + id="dense", label="d", goal="g", channel_id="c", requester_id="u", requester_name="u" + ) + agent.messages = _history(1_750_000) + agent.iteration_timeout = 30 + sent: list[int] = [] + + async def callback(messages, system_prompt, tools, generation_state=None): + generation_state.setdefault( + "plan", {"client": client, "model": model, "is_codex": False, "snapshot": snapshot} + ) + sent.append(estimate_message_chars(messages)) + if sent[-1] > 1_400_000: + raise LLMContextLengthError( + "overflow", code="context_length_exceeded", context_window_tokens=1_048_576 + ) + return {"text": "DONE", "tool_calls": [], "stop_reason": "end_turn"} + + result = await _call_llm_with_recovery( + agent, callback, "sys", [], rescue_ladder=snapshot.ladder, generation_state={} + ) + assert result is not None + assert len(sent) == 2 and sent[1] < sent[0] + assert agent.context_recoveries[0]["target_chars"] < sent[0] diff --git a/tests/test_computer_audit_reason_codes.py b/tests/test_computer_audit_reason_codes.py index 54cecf11f..e96582839 100644 --- a/tests/test_computer_audit_reason_codes.py +++ b/tests/test_computer_audit_reason_codes.py @@ -163,7 +163,7 @@ def test_approved_audit_vocabulary_is_not_silently_shrunk(): # Independent of the scanner: removing a code that is no longer emitted # must still fail. Update only after reviewing intentional vocabulary edits. assert hashlib.sha256("\n".join(sorted(_AUDIT_REASON_CODES)).encode()).hexdigest() == ( - "f4e49528648970c12035ed87046b60757499473ce6bcde16db81e166144ec0d1" + "18d362cc44a48affe030f8ee50b806bbae8d8a22c10ae73a4484d9666f116e4a" ) diff --git a/tests/test_computer_observe_session_boundary.py b/tests/test_computer_observe_session_boundary.py new file mode 100644 index 000000000..85e52cd4c --- /dev/null +++ b/tests/test_computer_observe_session_boundary.py @@ -0,0 +1,296 @@ +"""Observe/session refusals before any input carry not_dispatched evidence. + +Everything runs through ComputerIntegration._tool with the real controller and +store. The X11 adapter is the real X11AttachedBackend with fake worker I/O +(subprocess spawning is forbidden by the fixture); the Hyprland inventory uses +the real HyprlandRuntimeBackend with its discovery/transport phases mocked. +No display, input device or compositor is touched. +""" + +import json +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from src.computer.controller import ComputerController +from src.computer.integration import ComputerIntegration +from src.computer.models import ComputerError, RequestContext +from src.computer.runtime import hyprland_discovery as hd +from src.computer.store import ComputerStore +from tests.test_computer_attached_controller_r5 import Attached +from tests.test_computer_keyboard_grounding_r6 import fixture +from tests.test_computer_native_vision_r5 import client, serving +from tests.test_hyprland_inventory_diagnostics import rig # noqa: F401 - fixture + + +def facade(controller, context, monkeypatch): + bot = SimpleNamespace( + config=SimpleNamespace(computer=SimpleNamespace(enabled=True)), + host_access_manager=SimpleNamespace(is_host_allowed=lambda *_: True), + tool_executor=SimpleNamespace(check_permission=lambda *_: None), + ) + service = ComputerIntegration(bot, controller=controller) + monkeypatch.setattr(service, "_context", lambda _: context) + turn = SimpleNamespace( + user_id=context.owner_id, + message=SimpleNamespace(channel=SimpleNamespace(id=context.channel_id)), + _computer_serving=serving(client()), + ) + return service, turn + + +async def call(service, turn, name, values): + block = SimpleNamespace(id=f"{name}-call", name=name, input=values) + with service.foreground(turn, block): + return await service._tool(name, values) + + +def not_dispatched(delivered, reason): + payload = json.loads(delivered.output) + assert delivered.ok is False + assert delivered.error == "computer_rejected" + assert delivered.uncertain_outcome is False + assert payload["reason"] == reason + assert payload["input_outcome"] == "not_dispatched" + assert payload["terminal"] is False and payload["recoverable"] is True + assert payload["replay_permitted"] is False + assert "RELEASE-ALL" not in payload["instruction"] + assert delivered.audit_metadata["computer_reason_code"] == reason + assert delivered.audit_metadata["computer_input_outcome"] == "not_dispatched" + return payload + + +async def test_x11_ungranted_source_is_typed_refusal_and_session_survives(tmp_path, monkeypatch): + async with fixture(tmp_path, monkeypatch) as (controller, context, action, _, calls): + service, turn = facade(controller, context, monkeypatch) + sid = action["session_id"] + # The monitor label, not the opaque source_id from start/observe. + delivered = await call( + service, + turn, + "computer_observe", + {"session_id": sid, "generation": 1, "source_id": "fixture"}, + ) + not_dispatched(delivered, "capture_source_not_granted") + assert controller.store.get_session(sid).state == "active" + assert sid in controller._live + image = await call(service, turn, "computer_observe", {"session_id": sid, "generation": 1}) + assert isinstance(image, dict) and "__image_block__" in image + assert calls == [] + + +async def test_x11_export_refusal_is_typed_refusal_and_session_survives(tmp_path, monkeypatch): + async with fixture(tmp_path, monkeypatch) as (controller, context, action, _, calls): + service, turn = facade(controller, context, monkeypatch) + sid = action["session_id"] + delivered = await call( + service, + turn, + "computer_session", + { + "operation": "export", + "session_id": sid, + "generation": 1, + "name": "drawing.png", + }, + ) + not_dispatched(delivered, "existing_session_export_not_granted") + assert controller.store.get_session(sid).state == "active" + assert sid in controller._live + assert calls == [] + + +@pytest.mark.parametrize( + "reason", + [ + "hyprland_discovery_deadline", + "hyprland_discovery_runtime_untrusted", + "hyprland_discovery_policy_invalid", + "hyprland_discovery_runtime_unavailable", + "hyprland_discovery_candidate_limit", + "hyprland_discovery_hint_invalid", + "hyprland_discovery_not_found", + "hyprland_discovery_ambiguous", + "hyprland_discovery_unavailable", + ], +) +async def test_hyprland_discovery_refusal_is_typed_and_spares_the_live_session( + tmp_path, + monkeypatch, + rig, # noqa: F811 - pytest fixture + reason, +): + rig.phases["resolve"].side_effect = hd.HyprlandDiscoveryError(reason) + backends = iter([Attached(), rig.backend]) + store = ComputerStore(tmp_path / "db", tmp_path / "evidence") + controller = ComputerController( + store, lambda _app: next(backends), lambda _: True, enabled=True + ) + context = RequestContext("owner", "channel", "turn", "localhost") + try: + # An unrelated session of the same owner/channel is already running. + live = await controller.session(context, {"operation": "start"}) + service, turn = facade(controller, context, monkeypatch) + delivered = await call( + service, turn, "computer_session", {"operation": "inventory_targets"} + ) + payload = not_dispatched(delivered, reason) + assert payload["next_action"] == "retry_with_supported_operation" + assert store.get_session(live["session_id"]).state == "active" + assert live["session_id"] in controller._live + rig.phases["pin_connections"].assert_not_awaited() + finally: + await controller.close() + store.close() + + +@pytest.mark.parametrize( + "name,values,reason", + [ + ("computer_observe", {"session_id": "missing", "generation": 1}, "not_found"), + ("computer_observe", {"generation": 99}, "stale_generation"), + ("computer_observe", {"generation": 1, "source_id": 7}, "invalid_source_selection"), + ("computer_session", {"operation": "status", "session_id": "missing"}, "not_found"), + ], +) +async def test_observe_and_session_preflight_refusals_are_not_terminal( + tmp_path, monkeypatch, name, values, reason +): + async with fixture(tmp_path, monkeypatch) as (controller, context, action, _, calls): + service, turn = facade(controller, context, monkeypatch) + sid = action["session_id"] + values = {"session_id": sid, **values} + delivered = await call(service, turn, name, values) + not_dispatched(delivered, reason) + assert controller.store.get_session(sid).state == "active" + assert calls == [] + + +async def test_act_refusal_before_grant_is_not_terminal(tmp_path, monkeypatch): + # v4.6.0 already attaches evidence here; its "unstarted" state was terminal. + async with fixture(tmp_path, monkeypatch) as (controller, context, action, _, calls): + service, turn = facade(controller, context, monkeypatch) + request = {**action, "operation": "click", "x": 2, "y": 2, "unexpected": 1} + delivered = await call(service, turn, "computer_act", request) + payload = not_dispatched(delivered, "invalid_arguments") + assert payload["state"] == "unstarted" + assert controller.store.get_session(action["session_id"]).state == "active" + assert calls == [] + + +@pytest.mark.parametrize("recovery", [False, True]) +async def test_observe_refusal_after_focus_recovery_keeps_terminal_guidance( + tmp_path, monkeypatch, recovery +): + async with fixture(tmp_path, monkeypatch) as (controller, context, action, _, calls): + sid = action["session_id"] + backend = controller._live[sid].backend + + async def unfocused(**_): + raise ComputerError("input_focus_unavailable") + + monkeypatch.setattr(backend, "observe", unfocused) + recover = AsyncMock(return_value=True) + if recovery: + # Stand-in for the Hyprland-only native focus recovery seam. + monkeypatch.setattr(controller, "_recovery_enabled", lambda _live: True) + monkeypatch.setattr(controller, "_recover_focus", recover) + service, turn = facade(controller, context, monkeypatch) + delivered = await call( + service, turn, "computer_observe", {"session_id": sid, "generation": 1} + ) + payload = json.loads(delivered.output) + assert payload["reason"] == "input_focus_unavailable" + if recovery: + # A recovery attempt may have dispatched focus input: no claim. + recover.assert_awaited_once() + assert payload["input_outcome"] == "release_unknown" + assert payload["terminal"] is True + else: + not_dispatched(delivered, "input_focus_unavailable") + + +@pytest.mark.parametrize("operation", ["stop", "pause"]) +async def test_lifecycle_failure_keeps_unknown_release_guidance(tmp_path, monkeypatch, operation): + async with fixture(tmp_path, monkeypatch) as (controller, context, action, _, calls): + seam = "_stop" if operation == "stop" else "_pause" + attempted = AsyncMock(side_effect=ComputerError("cleanup_unavailable")) + service, turn = facade(controller, context, monkeypatch) + with monkeypatch.context() as patch: + patch.setattr(controller, seam, attempted) + delivered = await call( + service, + turn, + "computer_session", + {"operation": operation, "session_id": action["session_id"]}, + ) + attempted.assert_awaited_once() + payload = json.loads(delivered.output) + assert payload["reason"] == "cleanup_unavailable" + assert payload["input_outcome"] == "release_unknown" + assert payload["terminal"] is True + assert delivered.ok is False + + +async def test_observe_quarantine_failure_never_claims_not_dispatched(tmp_path, monkeypatch): + async with fixture(tmp_path, monkeypatch) as (controller, context, action, _, calls): + sid = action["session_id"] + + async def lost_continuity(**_): + raise ComputerError("stale_source_binding") + + quarantine = AsyncMock(side_effect=ComputerError("hyprland_quarantine_unavailable")) + service, turn = facade(controller, context, monkeypatch) + with monkeypatch.context() as patch: + patch.setattr(controller._live[sid].backend, "observe", lost_continuity) + patch.setattr(controller, "_hyprland_continuity_failure", lambda *_: True) + patch.setattr(controller, "_quarantine_hyprland", quarantine) + delivered = await call( + service, turn, "computer_observe", {"session_id": sid, "generation": 1} + ) + quarantine.assert_awaited_once() + payload = json.loads(delivered.output) + assert payload["reason"] == "hyprland_quarantine_unavailable" + assert payload["input_outcome"] == "release_unknown" + assert payload["terminal"] is True + + +async def test_successful_calls_are_not_audited_as_rejections(tmp_path, monkeypatch): + async with fixture(tmp_path, monkeypatch) as (controller, context, action, _, calls): + service, turn = facade(controller, context, monkeypatch) + sid = action["session_id"] + status = await call( + service, + turn, + "computer_session", + {"operation": "status", "session_id": sid, "generation": 1}, + ) + assert status.ok is True + assert status.audit_metadata["computer_reason_code"] == "computer_succeeded" + action.update(operation="key", key="Right") + image = await call(service, turn, "computer_act", action) + receipt = image["__computer_action_receipt__"] + assert receipt["status"] == "verified" and "reason" not in receipt + metadata = image["__computer_audit_metadata__"] + assert metadata["computer_reason_code"] == "verified" + assert metadata["computer_input_outcome"] == "released_verified" + assert len(calls) == 1 + + +def test_failure_audit_codes_are_unchanged(): + from src.computer.error_guidance import audit_outcome_code + + assert audit_outcome_code({"status": "not_satisfied"}, succeeded=False) == "computer_rejected" + assert audit_outcome_code({"status": "unknown"}, succeeded=False) == "computer_rejected" + assert ( + audit_outcome_code( + {"status": "not_satisfied", "verification": {"reason": "target_changed_observe_again"}}, + succeeded=False, + ) + == "target_changed_observe_again" + ) + assert audit_outcome_code({"status": "unsupported_operation"}, succeeded=True) == ( + "unsupported_operation" + ) + assert audit_outcome_code({"reason": "desktop text"}, succeeded=True) == "computer_succeeded" diff --git a/tests/test_computer_x11_attached_r10_extra.py b/tests/test_computer_x11_attached_r10_extra.py index 8823e147c..18ab34196 100644 --- a/tests/test_computer_x11_attached_r10_extra.py +++ b/tests/test_computer_x11_attached_r10_extra.py @@ -5,6 +5,7 @@ import pytest +from src.computer.models import ComputerError from src.computer.runtime import recovery from src.computer.runtime import x11_attached as attached @@ -191,7 +192,8 @@ async def test_inactive_and_revoked_routes(operation): "resume": lambda: b.resume(consent_generation=2), "export": lambda: b.export("fake"), }[operation] - with pytest.raises(attached.AttachedFailure): + refusal = ComputerError if operation in {"select", "export"} else attached.AttachedFailure + with pytest.raises(refusal): await call() diff --git a/tests/test_computer_x11_attached_r5.py b/tests/test_computer_x11_attached_r5.py index 5c7a14e69..3fc24ab0d 100644 --- a/tests/test_computer_x11_attached_r5.py +++ b/tests/test_computer_x11_attached_r5.py @@ -6,6 +6,7 @@ import pytest from src.computer.geometry import AffineTransform +from src.computer.models import ComputerError from src.computer.runtime import x11_attached as module from src.computer.runtime.x11_attached import ( INPUT_BLOCKER, @@ -134,7 +135,7 @@ async def read(operation, *, selected=None, crop=None): assert not frame.scope.input_sources and frame.source.input_region_id is None assert not frame.focused assert seen[-1]["name"] == source["label"] - with pytest.raises(AttachedFailure, match="not_granted"): + with pytest.raises(ComputerError, match="^capture_source_not_granted$"): await b.select_source("ungranted") diff --git a/tests/test_context_budget.py b/tests/test_context_budget.py index 4194b735b..f6b1cc570 100644 --- a/tests/test_context_budget.py +++ b/tests/test_context_budget.py @@ -268,7 +268,8 @@ def test_catalogue_and_alias_spellings_share_profile_budget(self, model): max_context_chars=None, ) assert snap.canonical_model == "deepseek-v4-flash" - assert snap.working_budget == 491_520 + # (1,048,576 - 32,768) × 75%. + assert snap.working_budget == 761_856 def test_total_window_minus_output_and_alias_are_derived(self): from types import SimpleNamespace @@ -314,8 +315,8 @@ def test_eligible_compatible_rescue_ladder_is_exact_and_descending(self): context_utilization=100, ) snap = snapshot_for_compatible_profile("large", cfg, max_context_chars=None) - # (100,000 - 42,000) * 2.5 = 145,000 primary; rescue begins at 70%. - assert (snap.primary_chars, snap.ladder) == (145_000, (101_500,)) + # (200,000 - 32,768 - 42,000) × 2.5; rescue begins at 70%. + assert (snap.primary_chars, snap.ladder) == (313_080, (219_156,)) def test_agent_eligibility_reserves_only_the_effective_request_output_cap(self): from types import SimpleNamespace @@ -374,6 +375,27 @@ def test_compatible_profile_legacy_shape_and_openrouter_catalogue_fallback(self) "no context profile configured" ) + def test_compatible_agent_rejects_profile_with_too_little_working_budget(self): + from types import SimpleNamespace + + from src.config.schema import OpenAICompatibleModelProfile + from src.llm.context_budget import compatible_agent_unavailable_reason + + cfg = SimpleNamespace( + model_profiles={ + "tiny": OpenAICompatibleModelProfile( + total_window_tokens=10_000, + max_output_tokens=1_000, + ) + }, + context_utilization=50, + ) + + assert compatible_agent_unavailable_reason("tiny", cfg) == ( + "post-utilization working budget is 4,500 tokens; " + "at least 63,000 are required" + ) + # --------------------------------------------------------------------------- # Configuration surface diff --git a/tests/test_context_budget_activation.py b/tests/test_context_budget_activation.py index 215056bab..dee5fac78 100644 --- a/tests/test_context_budget_activation.py +++ b/tests/test_context_budget_activation.py @@ -187,10 +187,10 @@ def test_compatible_chat_uses_profile_and_live_utilization_once(self): None, ) snapshot = runner._capture_budget_snapshot(serving, config) - assert snapshot.working_budget == 491_520 + assert snapshot.working_budget == 761_856 compatible.context_utilization = 60 - assert snapshot.working_budget == 491_520 - assert runner._capture_budget_snapshot(serving, config).working_budget == 393_216 + assert snapshot.working_budget == 761_856 + assert runner._capture_budget_snapshot(serving, config).working_budget == 609_484 def test_sol_headroom_no_compress_where_legacy_would_have(self): """850K chars: over the legacy 750K, comfortably under sol's 1.277M — diff --git a/tests/test_context_budget_legacy_profile_conversion.py b/tests/test_context_budget_legacy_profile_conversion.py new file mode 100644 index 000000000..496873b20 --- /dev/null +++ b/tests/test_context_budget_legacy_profile_conversion.py @@ -0,0 +1,21 @@ +"""Malformed legacy profile values fail closed without touching services.""" + +from types import SimpleNamespace + +from src.llm.context_budget import compatible_usable_input_tokens + + +def test_legacy_profile_value_with_value_error_returns_unknown(): + class InvalidLegacyValue: + def __int__(self): + raise ValueError("not an integer-like value") + + profile = SimpleNamespace(usable_input_tokens=InvalidLegacyValue()) + + assert compatible_usable_input_tokens(profile) is None + + +def test_total_window_with_unusable_output_reserve_returns_unknown(): + profile = SimpleNamespace(total_window_tokens=100_000, max_output_tokens=0) + + assert compatible_usable_input_tokens(profile) is None diff --git a/tests/test_context_compressor.py b/tests/test_context_compressor.py index 2b02fd5f7..9162eb7ca 100644 --- a/tests/test_context_compressor.py +++ b/tests/test_context_compressor.py @@ -302,6 +302,68 @@ def test_list_result_content(self): # summarize_iteration # ----------------------------------------------------------------------- class TestSummarizeIteration: + def test_failure_texts_are_labelled_with_reason(self): + cases = [ + ("Blocked [critical]: denied", "blocked"), + ("Unknown or disallowed host: x", "disallowed host"), + ("Command failed (exit 2)", "command failed"), + ("Script failed (exit 2)", "script failed"), + ("Tool run_command timed out: 300s", "timed out"), + ("Tool 'x' cancelled by /stop", "cancelled"), + ("Tool x input error: invalid", "input error"), + ("[Interrupted: outcome unknown]", "outcome unknown"), + ("Failed to fetch resource", "failed"), + ("Error (tool reported failure):\nno", "tool reported failure"), + ] + for text, reason in cases: + assert summarize_iteration(_iteration("run_command", result=text)) == ( + f"run_command→ERR ({reason})" + ) + legacy = [{"role": "user", "content": f"[Tool result: run_command]\n{text}"}] + assert summarize_iteration(legacy) == f"run_command→ERR ({reason})" + + def test_failed_retained_envelope_is_a_failure(self): + from src.tools.output_delivery import serialize + + text = serialize( + {"kind": "tool_output", "status": "failed", "head": "Command failed (exit 2)"} + ) + assert summarize_iteration(_iteration("run_command", result=text)) == ( + "run_command→ERR (command failed)" + ) + + def test_successes_keep_ok_text(self): + from src.tools.output_delivery import serialize + + for text in ( + "plain output", + serialize({"kind": "tool_output", "status": "succeeded", "head": "Failed to connect"}), + ): + assert summarize_iteration(_iteration("run_command", result=text)) == "run_command→OK" + block = { + "type": "tool_result", "tool_use_id": "tc1", + "content": "Failed to connect", "is_error": False, + } + assert summarize_iteration( + [_tool_use_msg("run_command"), {"role": "user", "content": [block]}] + ) == "run_command→OK" + + def test_agent_status_labels_unchanged(self): + for status in ("denied", "succeeded", "interrupted_effect_free"): + it = [ + _tool_use_msg("run_command"), + { + "role": "user", + "content": [ + { + "type": "tool_result", "tool_use_id": "tc1", + "content": "ignored", "status": status, + } + ], + }, + ] + assert summarize_iteration(it) == f"run_command→{status}" + def test_single_tool_ok(self): it = _iteration("run_command", result="success") s = summarize_iteration(it) @@ -404,6 +466,56 @@ def test_missing_outcome(self): s = summarize_iteration(it) assert "?" in s + def test_partial_structured_call_does_not_borrow_later_result(self): + it = [ + {"role": "assistant", "content": [ + {"type": "tool_use", "id": "first", "name": "read_file", "input": {}}, + {"type": "tool_use", "id": "second", "name": "run_command", "input": {}}, + ]}, + {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": "second", "content": "Error: exit 1"}, + ]}, + ] + assert summarize_iteration(it) == "read_file→?, run_command→ERR (error)" + + def test_structured_result_id_wins_over_result_order(self): + it = [ + {"role": "assistant", "content": [ + {"type": "tool_use", "id": "first", "name": "read_file", "input": {}}, + {"type": "tool_use", "id": "second", "name": "run_command", "input": {}}, + ]}, + {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": "second", "content": "Error: exit 1"}, + {"type": "tool_result", "tool_use_id": "first", "content": "file contents"}, + ]}, + ] + assert summarize_iteration(it) == "read_file→OK, run_command→ERR (error)" + + def test_unmatched_structured_result_does_not_claim_a_call(self): + it = [ + _tool_use_msg("read_file", "first"), + {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": "other", "content": "ok"}, + ]}, + ] + assert summarize_iteration(it) == "read_file→?" + + def test_soft_compaction_keeps_partial_call_unresolved(self): + messages = [ + _text_msg("user", "inspect"), + {"role": "assistant", "content": [ + {"type": "tool_use", "id": "first", "name": "read_file", "input": {}}, + {"type": "tool_use", "id": "second", "name": "run_command", "input": {}}, + ]}, + {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": "second", "content": "ok"}, + ]}, + *_iteration("read_file", "later", "ok"), + ] + out, count = compress_tool_context(messages, max_context_chars=1, keep_recent=1) + assert count == 1 + assert "read_file→?, run_command→OK" in out[1]["content"] + # ----------------------------------------------------------------------- # compress_tool_context — basic diff --git a/tests/test_email_client_paths.py b/tests/test_email_client_paths.py index edfccf317..c5ae54d46 100644 --- a/tests/test_email_client_paths.py +++ b/tests/test_email_client_paths.py @@ -56,6 +56,63 @@ def test_extract_body_truncation(self): out = ec._extract_body(big, 10) assert out.startswith("z" * 10) and "truncated at 10 chars" in out + def test_parameterised_text_attachment_before_body_is_not_the_body(self): + msg = email_lib.mime.multipart.MIMEMultipart() + attachment = email_lib.mime.text.MIMEText("ATTACHMENT TEXT", "plain") + attachment.add_header("Content-Disposition", "attachment", filename="notes.txt") + msg.attach(attachment) + msg.attach(email_lib.mime.text.MIMEText("REAL BODY", "plain")) + assert ec._extract_body(msg, 1000) == "REAL BODY" + + def test_html_only_body_with_text_attachment_returns_html(self): + msg = email_lib.mime.multipart.MIMEMultipart() + msg.attach(email_lib.mime.text.MIMEText("

REAL HTML

", "html")) + attachment = email_lib.mime.text.MIMEText("ATTACHMENT TEXT", "plain") + attachment.add_header("Content-Disposition", "attachment", filename="notes.txt") + msg.attach(attachment) + assert ec._extract_body(msg, 1000) == "[HTML content]\n

REAL HTML

" + + def test_forwarded_message_attachment_is_not_the_body(self): + msg = email_lib.mime.multipart.MIMEMultipart() + forwarded = email_lib.mime.base.MIMEBase("message", "rfc822") + inner = email_lib.mime.multipart.MIMEMultipart() + inner.attach(email_lib.mime.text.MIMEText("FORWARDED BODY", "plain")) + forwarded.set_payload([inner]) + forwarded.add_header("Content-Disposition", "attachment", filename="forwarded.eml") + msg.attach(forwarded) + msg.attach(email_lib.mime.text.MIMEText("REAL BODY", "plain")) + assert ec._extract_body(msg, 1000) == "REAL BODY" + + def test_capitalised_attachment_disposition_is_an_attachment(self): + msg = email_lib.mime.multipart.MIMEMultipart() + attachment = email_lib.mime.text.MIMEText("ATTACHMENT", "plain") + attachment["Content-Disposition"] = 'Attachment; filename="report.txt"' + msg.attach(attachment) + assert ec._extract_body(msg, 1000) == "[no text body found]" + assert ec._attachment_metadata(msg)[0]["filename"] == "report.txt" + + def test_attachment_list_uses_disposition_type_not_substring(self): + msg = email_lib.mime.multipart.MIMEMultipart() + inline = email_lib.mime.text.MIMEText("REAL BODY", "plain") + inline.add_header("Content-Disposition", "inline", filename="attachment-notes.txt") + msg.attach(inline) + attachment = email_lib.mime.text.MIMEText("ATTACHMENT", "plain") + attachment["Content-Disposition"] = 'Attachment; filename="report.txt"' + msg.attach(attachment) + assert ec._extract_body(msg, 1000) == "REAL BODY" + assert [item["filename"] for item in ec._attachment_metadata(msg)] == ["report.txt"] + + def test_has_attachments_uses_disposition_type(self): + msg = email_lib.mime.multipart.MIMEMultipart() + inline = email_lib.mime.text.MIMEText("REAL BODY", "plain") + inline.add_header("Content-Disposition", "inline", filename="attachment.txt") + msg.attach(inline) + assert ec._message_summary(msg)["has_attachments"] is False + attachment = email_lib.mime.text.MIMEText("ATTACHMENT", "plain") + attachment["Content-Disposition"] = 'Attachment; filename="report.txt"' + msg.attach(attachment) + assert ec._message_summary(msg)["has_attachments"] is True + class _FakeIMAP: """Minimal imaplib.IMAP4_SSL stand-in — no socket, records logout.""" @@ -179,6 +236,7 @@ def login(self, user, password): def sendmail(self, from_addr, recipients, body): self.sent = (from_addr, recipients, body) + return {} class TestSendEmail: diff --git a/tests/test_email_tools.py b/tests/test_email_tools.py index 7289103c8..134ef34f7 100644 --- a/tests/test_email_tools.py +++ b/tests/test_email_tools.py @@ -8,6 +8,7 @@ from __future__ import annotations import email as email_lib +import smtplib from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -51,6 +52,7 @@ class TestSendEmail: @patch("src.tools.email_client.smtplib.SMTP") def test_basic_send(self, mock_smtp_cls): mock_server = MagicMock() + mock_server.sendmail.return_value = {} mock_smtp_cls.return_value.__enter__ = MagicMock(return_value=mock_server) mock_smtp_cls.return_value.__exit__ = MagicMock(return_value=False) @@ -71,6 +73,7 @@ def test_basic_send(self, mock_smtp_cls): @patch("src.tools.email_client.smtplib.SMTP") def test_cc_bcc_recipients(self, mock_smtp_cls): mock_server = MagicMock() + mock_server.sendmail.return_value = {} mock_smtp_cls.return_value.__enter__ = MagicMock(return_value=mock_server) mock_smtp_cls.return_value.__exit__ = MagicMock(return_value=False) @@ -116,6 +119,7 @@ def test_attachments_require_allowed_dirs(self): @patch("src.tools.email_client.smtplib.SMTP") def test_attachment_valid_path(self, mock_smtp_cls, tmp_path): mock_server = MagicMock() + mock_server.sendmail.return_value = {} mock_smtp_cls.return_value.__enter__ = MagicMock(return_value=mock_server) mock_smtp_cls.return_value.__exit__ = MagicMock(return_value=False) f = tmp_path / "report.txt" @@ -144,6 +148,145 @@ def test_attachment_too_large(self, mock_smtp_cls, tmp_path): ) +class TestPartialRefusal: + @staticmethod + def _send(refused, **kwargs): + server = MagicMock() + server.sendmail.return_value = refused + with patch("src.tools.email_client.smtplib.SMTP") as smtp: + smtp.return_value.__enter__.return_value = server + return send_email( + smtp_host="fixture", smtp_port=587, username="sender", + password="synthetic-secret", from_address="sender@example.test", + to=["good@example.test", "bad@example.test"], + subject="Subject", body="Body", **kwargs, + ) + + def test_refused_to_is_reported_not_listed_as_sent(self): + result = self._send({"bad@example.test": (550, b"5.1.1 No such user")}) + assert result["status"] == "partial" + assert result["accepted_count"] == 1 + assert result["refused"] == [{ + "address": "bad@example.test", "field": "To", "code": 550, + "reason": "5.1.1 No such user", + }] + + @pytest.mark.parametrize("field", ["CC", "BCC"]) + def test_refused_cc_or_bcc_is_labelled(self, field): + kwargs = {field.lower(): ["hidden@example.test"]} + result = self._send( + {"hidden@example.test": (550, b"5.1.1 No such user")}, **kwargs + ) + assert result["status"] == "partial" + assert result["accepted_count"] == 2 + assert result["refused"][0] == { + "address": "hidden@example.test", "field": field, "code": 550, + "reason": "5.1.1 No such user", + } + + @pytest.mark.asyncio + @pytest.mark.parametrize("field", ["To", "CC", "BCC"]) + async def test_handler_reports_refusal_without_mislisting_accepted(self, field): + from src.config.schema import EmailConfig + from src.tools.executor import ToolExecutor + + executor = ToolExecutor(email_config=EmailConfig( + enabled=True, smtp_host="fixture", username="sender", + from_address="sender@example.test", + )) + refused = [{"address": "bad@example.test", "field": field, "code": 550, + "reason": "5.1.1 No such user"}] + result = {"message_id": "", "to": ["good@example.test"], + "cc": [], "subject": "Subject", "attachments": [], + "accepted_count": 1, "refused": refused} + if field == "To": + result["to"].append("bad@example.test") + elif field == "CC": + result["cc"] = ["bad@example.test"] + fake_thread = AsyncMock(return_value=result) + with patch("src.tools.handlers.comms.asyncio.to_thread", new=fake_thread): + text = await executor.comms_tools._handle_email_send({ + "to": ["good@example.test"], "subject": "Subject", "body": "Body", + }) + assert text.startswith("Email partially sent:") + assert "To: good@example.test\n" in text + assert "To: good@example.test, bad@example.test" not in text + assert "CC: bad@example.test" not in text + assert f"Refused: bad@example.test ({field}): 550 5.1.1 No such user" in text + + @pytest.mark.asyncio + async def test_partial_send_without_accepted_to_keeps_cc_and_attachments(self): + from src.config.schema import EmailConfig + from src.tools.executor import ToolExecutor + + executor = ToolExecutor(email_config=EmailConfig( + enabled=True, smtp_host="fixture", username="sender", + from_address="sender@example.test", + )) + result = { + "message_id": "", "to": ["bad@example.test"], + "cc": ["good@example.test"], "subject": "Subject", + "attachments": ["report.txt"], "accepted_count": 1, + "refused": [{"address": "bad@example.test", "field": "To", "code": 550, + "reason": "5.1.1 No such user"}], + } + with patch( + "src.tools.handlers.comms.asyncio.to_thread", + new=AsyncMock(return_value=result), + ): + text = await executor.comms_tools._handle_email_send({ + "to": ["bad@example.test"], "subject": "Subject", "body": "Body", + }) + assert "\nTo:" not in text + assert "\nCC: good@example.test\n" in text + assert "\nAttachments: report.txt\n" in text + + @pytest.mark.asyncio + async def test_partial_send_is_not_an_error_result(self): + from src.config.schema import EmailConfig + from src.tools.executor import ToolExecutor + + executor = ToolExecutor(email_config=EmailConfig( + enabled=True, smtp_host="fixture", username="sender", + from_address="sender@example.test", + )) + result = {"message_id": "", "to": ["good@example.test"], + "cc": [], "subject": "Subject", "attachments": [], "accepted_count": 1, + "refused": [{"address": "bad@example.test", "field": "BCC", "code": 550, + "reason": "5.1.1 No such user"}]} + fake_thread = AsyncMock(return_value=result) + with patch("src.tools.handlers.comms.asyncio.to_thread", new=fake_thread): + response = await executor.execute("email_send", { + "to": ["good@example.test"], "subject": "Subject", "body": "Body", + }) + assert response.ok is True + assert "Email partially sent:" in response.output + + def test_all_refused_raises_with_labelled_refusals(self): + with patch("src.tools.email_client.smtplib.SMTP") as smtp: + smtp.return_value.__enter__.return_value.sendmail.side_effect = ( + smtplib.SMTPRecipientsRefused({ + "bad@example.test": (550, b"No such user"), + "hidden@example.test": (550, b"No BCC mailbox"), + }) + ) + with pytest.raises( + RuntimeError, match="no message was sent.*bad@example.test \\(To\\)" + ): + send_email(smtp_host="fixture", smtp_port=587, username="sender", + password="synthetic-secret", from_address="sender@example.test", + to=["bad@example.test"], bcc=["hidden@example.test"], + subject="Subject", body="Body") + + def test_refusal_reason_is_sanitised(self): + reason = ("one\r\ntwo synthetic-secret " + "longword " * 40).encode() + result = self._send({"bad@example.test": (550, reason)}) + text = result["refused"][0]["reason"] + assert "\n" not in text and "\r" not in text + assert "[REDACTED]" in text and "synthetic-secret" not in text + assert len(text) <= 200 + + # --------------------------------------------------------------------------- # search_email # --------------------------------------------------------------------------- diff --git a/tests/test_fts_inventory_boundaries.py b/tests/test_fts_inventory_boundaries.py new file mode 100644 index 000000000..31500c73d --- /dev/null +++ b/tests/test_fts_inventory_boundaries.py @@ -0,0 +1,25 @@ +"""Inventory failures must not be mistaken for an empty FTS index.""" + +from src.search.fts import FullTextIndex + + +def test_knowledge_inventory_distinguishes_absent_store_from_empty_store(tmp_path): + index = FullTextIndex(str(tmp_path / "fts.db")) + assert index.knowledge_chunk_sources() == [] + assert index.index_knowledge_chunk("id", "searchable text", "source", 0) + assert index.knowledge_chunk_sources() == [("id", "source")] + index._conn = None + assert index.knowledge_chunk_sources() is None + + +def test_knowledge_inventory_read_failure_is_not_reported_as_empty(tmp_path): + index = FullTextIndex(str(tmp_path / "fts.db")) + assert index.index_knowledge_chunk("id", "searchable text", "source", 0) + index._conn.close() + assert index.knowledge_chunk_sources() is None + + +def test_replace_knowledge_source_refuses_unavailable_store(tmp_path): + index = FullTextIndex(str(tmp_path / "fts.db")) + index._conn = None + assert index.replace_knowledge_source("source", [("id", "text", 0)]) is False diff --git a/tests/test_grafana_alerts.py b/tests/test_grafana_alerts.py index e77272cbc..d6b82ffaa 100644 --- a/tests/test_grafana_alerts.py +++ b/tests/test_grafana_alerts.py @@ -1106,6 +1106,26 @@ async def test_webhook_enriched_event_data(self): assert event_data["resolved_count"] == 1 assert event_data["severity"] == "critical" + async def test_webhook_event_data_lists_every_alert_name(self): + server = _make_grafana_server() + server.set_send_message(AsyncMock()) + trigger_cb = AsyncMock(return_value=0) + server.set_trigger_callback(trigger_cb) + + payload = _unified_payload( + _firing_alert("FirstAlert"), _resolved_alert("SecondAlert"), + ) + async with TestClient(TestServer(server._app)) as client: + resp = await client.post( + "/webhook/grafana", json=payload, + headers=_TEST_WEBHOOK_HEADERS, + ) + assert resp.status == 200 + event_data = trigger_cb.call_args[0][1] + assert event_data["alert_name"] == "FirstAlert" + assert event_data["alert_names"] == ["FirstAlert", "SecondAlert"] + assert event_data["severity"] == "critical" + # --------------------------------------------------------------------------- # REST API endpoints diff --git a/tests/test_knowledge_dedup.py b/tests/test_knowledge_dedup.py index da7a53606..ebba13c26 100644 --- a/tests/test_knowledge_dedup.py +++ b/tests/test_knowledge_dedup.py @@ -276,7 +276,7 @@ async def test_failed_fts_write_returns_zero_and_does_not_record_version(self, t fts = FullTextIndex(str(tmp_path / "fts.db")) store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts_index=fts) try: - with patch.object(fts, "index_knowledge_chunk", return_value=False): + with patch.object(fts, "replace_knowledge_source", return_value=False): indexed = await store.ingest(SHORT_DOC, "doc.md", dedup=False) assert indexed == 0 assert store.get_source_content("doc.md") is None @@ -298,10 +298,11 @@ async def test_failed_update_fts_write_preserves_old_searchable_copy(self, tmp_p old = "old searchable knowledge body" new = "new replacement knowledge body" assert await store.ingest(old, "doc.md", dedup=False) == 1 - with patch.object(fts, "index_knowledge_chunk", return_value=False): + with patch.object(fts, "replace_knowledge_source", return_value=False): assert await store.ingest(new, "doc.md", dedup=False) == 0 assert any(r["source"] == "doc.md" for r in fts.search_knowledge("old")) - assert store.get_source_content("doc.md") in {old, f"{old}\n\n{new}"} + assert not fts.search_knowledge("replacement") + assert store.get_source_content("doc.md") == old finally: store.close() if fts._conn is not None: @@ -310,8 +311,6 @@ async def test_failed_update_fts_write_preserves_old_searchable_copy(self, tmp_p async def test_partial_multichunk_fts_failure_preserves_old_searchable_copy( self, tmp_path, ): - from unittest.mock import patch - from src.search.fts import FullTextIndex fts = FullTextIndex(str(tmp_path / "fts.db")) @@ -321,19 +320,30 @@ async def test_partial_multichunk_fts_failure_preserves_old_searchable_copy( new = " ".join(f"newtoken{i}" for i in range(900)) assert len(store._chunk_text(new)) > 1 assert await store.ingest(old, "doc.md", dedup=False) == 1 - real_index = fts.index_knowledge_chunk - calls = 0 + import sqlite3 + + class _FailSecondFtsInsert: + def __init__(self, conn): + self.conn, self.seen = conn, 0 + + def __getattr__(self, name): + return getattr(self.conn, name) - def fail_second(chunk_id, content, source, chunk_index): - nonlocal calls - calls += 1 - if calls == 2: - return False - return real_index(chunk_id, content, source, chunk_index) + def execute(self, sql, parameters=()): + if "INSERT INTO knowledge_fts" in sql: + self.seen += 1 + if self.seen == 2: + raise sqlite3.OperationalError("injected FTS write failure") + return self.conn.execute(sql, parameters) - with patch.object(fts, "index_knowledge_chunk", side_effect=fail_second): + real_conn = fts._conn + fts._conn = _FailSecondFtsInsert(real_conn) + try: assert await store.ingest(new, "doc.md", dedup=False) == 0 + finally: + fts._conn = real_conn assert any(r["source"] == "doc.md" for r in fts.search_knowledge("sentinel")) + assert not fts.search_knowledge("newtoken5") assert store.get_source_content("doc.md") == old finally: store.close() diff --git a/tests/test_knowledge_durability_failure_paths.py b/tests/test_knowledge_durability_failure_paths.py index 41c183cc3..62d2d2f9f 100644 --- a/tests/test_knowledge_durability_failure_paths.py +++ b/tests/test_knowledge_durability_failure_paths.py @@ -218,11 +218,13 @@ async def test_obsolete_fts_delete_mismatch_refuses_update(self, tmp_path): old = _long_document("old") assert await store.ingest(old, "doc.md", dedup=False) > 1 initial_versions = len(store.get_versions("doc.md")) - with patch.object(fts, "delete_knowledge_chunks", return_value=0) as retire: + with patch.object(fts, "replace_knowledge_source", return_value=False) as publish: assert await store.ingest("short replacement", "doc.md", dedup=False) == 0 - retire.assert_called_once() + publish.assert_called_once() assert len(store.get_versions("doc.md")) == initial_versions assert fts.search_knowledge("old") + assert not fts.search_knowledge("replacement") + assert store.get_source_snapshot("doc.md") == old finally: _close(store, fts) @@ -242,8 +244,9 @@ async def test_obsolete_db_delete_exception_refuses_update(self, tmp_path): store._conn.commit() assert await store.ingest("short replacement", "doc.md", dedup=False) == 0 - assert old_ids.issubset({row["chunk_id"] for row in store.get_source_chunks("doc.md")}) - assert fts.search_knowledge("replacement") + assert old_ids == {row["chunk_id"] for row in store.get_source_chunks("doc.md")} + assert not fts.search_knowledge("replacement") + assert store.source_is_durable("doc.md", expected_chunks=len(old_ids)) finally: _close(store, fts) @@ -257,21 +260,24 @@ async def test_final_durability_probe_refuses_lost_desired_fts_row(self, tmp_pat f"{hashlib.md5(b'doc.md').hexdigest()[:8]}_0_" f"{KnowledgeStore._content_hash(new)[:12]}" ) - real_delete = fts.delete_knowledge_chunks + real_replace = fts.replace_knowledge_source + calls = [] - def retire_old_and_drop_desired(chunk_ids): - removed = real_delete(chunk_ids) - assert real_delete({desired_id}) == 1 - return removed + def publish_then_drop_desired(source, rows): + ok = real_replace(source, rows) + if not calls: + assert fts.delete_knowledge_chunks({desired_id}) == 1 + calls.append(source) + return ok with patch.object( - fts, "delete_knowledge_chunks", side_effect=retire_old_and_drop_desired - ) as retire: + fts, "replace_knowledge_source", side_effect=publish_then_drop_desired + ) as publish: assert await store.ingest(new, "doc.md", dedup=False) == 0 - assert retire.call_count == 1 - assert store.get_source_content("doc.md") == new + assert publish.call_count == 2 + assert store.get_source_snapshot("doc.md") == old assert not fts.has_knowledge_chunk(desired_id) - assert not store.source_is_durable("doc.md") + assert store.source_is_durable("doc.md") finally: _close(store, fts) @@ -290,7 +296,8 @@ async def test_vector_verification_mismatch_refuses_ingest(self, tmp_path): == 0 ) assert store.get_versions("vector.md") == [] - assert store.source_is_durable("vector.md", expected_chunks=1) + assert store.get_source_content("vector.md") is None + assert not fts.has_knowledge_source("vector.md") finally: store._conn = real_conn _close(store, fts) diff --git a/tests/test_knowledge_import.py b/tests/test_knowledge_import.py index ed329d5f7..0b62d014c 100644 --- a/tests/test_knowledge_import.py +++ b/tests/test_knowledge_import.py @@ -557,15 +557,15 @@ async def test_failed_canonical_fts_create_keeps_searchable_legacy_copy(self): legacy = "docs/doc.md" canonical = path.resolve().as_uri() assert await store.ingest(content, legacy, dedup=False) == 1 - real_index = fts.index_knowledge_chunk + real_replace = fts.replace_knowledge_source - def fail_canonical_index(chunk_id, chunk, source, chunk_index): + def fail_canonical_index(source, rows): if source == canonical: return False - return real_index(chunk_id, chunk, source, chunk_index) + return real_replace(source, rows) with patch.object( - fts, "index_knowledge_chunk", side_effect=fail_canonical_index, + fts, "replace_knowledge_source", side_effect=fail_canonical_index, ): result = await importer.import_file(str(path)) diff --git a/tests/test_knowledge_ingest_outcomes.py b/tests/test_knowledge_ingest_outcomes.py index 71a32a65e..02002af68 100644 --- a/tests/test_knowledge_ingest_outcomes.py +++ b/tests/test_knowledge_ingest_outcomes.py @@ -181,7 +181,7 @@ async def test_durability_failure_is_still_failure(self, tmp_path): """A failed FTS write is a real failure, not a skip.""" store = _store(tmp_path) try: - with patch.object(store._fts, "index_knowledge_chunk", return_value=False): + with patch.object(store._fts, "replace_knowledge_source", return_value=False): outcome = await store.ingest(SHORT_DOC, "doc.md", dedup=False) assert outcome.status == INGEST_FAILURE assert int(outcome) == 0 @@ -190,41 +190,41 @@ async def test_durability_failure_is_still_failure(self, tmp_path): _close(store) async def test_stored_implies_the_fts_copy_is_verified(self, tmp_path): - """`stored` is a durability claim, and the FTS store is part of it. - - `ingest` returns `indexed == len(chunks)`, and that count is only - reachable past `_write_chunks_sync`'s own gates: every chunk write - succeeded (`all_writes_ok`), `_source_contains_rows` confirmed the - chunk rows in BOTH the DB and the configured FTS table, obsolete rows - were retired, and the final `source_is_durable` re-check passed. This - test holds that line: a partial, absent, raising, or silently lying - FTS write must never be reported as `stored`. - """ - # A partial multi-chunk failure: chunk 2 of 9 never lands in FTS. + """A part-way FTS insert failure leaves neither copy published.""" + import sqlite3 + fts = FullTextIndex(str(tmp_path / "fts.db")) store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts_index=fts) + + class _FailSecondFtsInsert: + def __init__(self, conn): + self.conn, self.seen = conn, 0 + + def __getattr__(self, name): + return getattr(self.conn, name) + + def execute(self, sql, parameters=()): + if "INSERT INTO knowledge_fts" in sql: + self.seen += 1 + if self.seen == 2: + raise sqlite3.OperationalError("injected FTS write failure") + return self.conn.execute(sql, parameters) + try: doc = " ".join(f"newtoken{i}" for i in range(900)) chunk_count = len(store._chunk_text(doc)) assert chunk_count > 1 - real_index = fts.index_knowledge_chunk - calls = 0 - - def fail_second(chunk_id, content, source, chunk_index): - nonlocal calls - calls += 1 - if calls == 2: - return False - return real_index(chunk_id, content, source, chunk_index) - - with patch.object(fts, "index_knowledge_chunk", side_effect=fail_second): + real_conn = fts._conn + fts._conn = _FailSecondFtsInsert(real_conn) + try: outcome = await store.ingest(doc, "big.md", dedup=False) + finally: + fts._conn = real_conn assert outcome.status == INGEST_FAILURE assert int(outcome) == 0 - # The FTS copy really is incomplete, which is why `stored` was - # withheld: 8 of 9 chunks landed and the durability check refused. - assert fts.count_knowledge_source("big.md") == chunk_count - 1 + assert fts.count_knowledge_source("big.md") == 0 + assert store.get_source_content("big.md") is None assert store.source_is_durable("big.md") is False finally: store.close() @@ -235,7 +235,7 @@ async def test_a_lying_fts_write_is_caught_by_verification(self, tmp_path): """An FTS write that reports success but stores nothing is a failure.""" store = _store(tmp_path) try: - with patch.object(store._fts, "index_knowledge_chunk", return_value=True): + with patch.object(store._fts, "replace_knowledge_source", return_value=True): outcome = await store.ingest(SHORT_DOC, "doc.md", dedup=False) assert outcome.status == INGEST_FAILURE assert int(outcome) == 0 @@ -247,7 +247,7 @@ async def test_a_raising_fts_write_is_a_failure(self, tmp_path): store = _store(tmp_path) try: with patch.object( - store._fts, "index_knowledge_chunk", side_effect=RuntimeError("boom"), + store._fts, "replace_knowledge_source", side_effect=RuntimeError("boom"), ): outcome = await store.ingest(SHORT_DOC, "doc.md", dedup=False) assert outcome.status == INGEST_FAILURE diff --git a/tests/test_knowledge_publication_atomicity.py b/tests/test_knowledge_publication_atomicity.py new file mode 100644 index 000000000..56caaa360 --- /dev/null +++ b/tests/test_knowledge_publication_atomicity.py @@ -0,0 +1,226 @@ +"""#402/#403: a knowledge ingest publishes DB rows, vectors, FTS rows and the +version snapshot together, or nothing. Faults are injected at the SQLite +connection boundary (the same boundary before and after the fix).""" +from __future__ import annotations + +import asyncio +import sqlite3 +import threading + +import pytest + +from src.knowledge.store import CHUNK_SIZE, VECTOR_DIM, KnowledgeStore +from src.search.fts import FullTextIndex + + +class _Proxy: + def __init__(self, conn): + self.conn = conn + + def __getattr__(self, name): + return getattr(self.conn, name) + + +class _FailNthExecute(_Proxy): + """Raise on the n-th execute whose SQL contains needle (1-based).""" + + def __init__(self, conn, needle, n): + super().__init__(conn) + self.needle, self.n, self.seen = needle, n, 0 + + def execute(self, sql, parameters=()): + if self.needle in sql: + self.seen += 1 + if self.seen == self.n: + raise sqlite3.OperationalError("injected FTS write failure") + return self.conn.execute(sql, parameters) + + +class _FailCommitOnce(_Proxy): + failed = False + + def commit(self): + if not self.failed: + self.failed = True + raise sqlite3.OperationalError("injected commit failure") + return self.conn.commit() + + +def _doc(tag: str, parts: int = 3) -> str: + """A document that chunks into `parts` chunks, each carrying a unique tag word.""" + body = [] + for i in range(parts): + words = " ".join(f"{tag}{i}w{j}" for j in range(CHUNK_SIZE // 12)) + body.append(f"{tag} part{i} {words}"[: CHUNK_SIZE - 20]) + return "\n\n".join(body) + + +class _Embedder: + async def embed(self, _content): + return [0.1] * VECTOR_DIM + + +@pytest.fixture +def stores(tmp_path): + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts_index=fts) + yield store, fts + store.close() + fts._conn.close() + + +def _fail_fts_insert(fts, n=2): + real = fts._conn + fts._conn = _FailNthExecute(real, "INSERT INTO knowledge_fts", n) + return lambda: setattr(fts, "_conn", real) + + +async def _sources(store, query): + return [hit["source"] for hit in await store.search_hybrid(query, None, limit=10)] + + +async def test_failed_initial_ingest_leaves_nothing_searchable(stores): + store, fts = stores + doc = _doc("orphanword") + assert len(store._chunk_text(doc)) == 3 + restore = _fail_fts_insert(fts) + outcome = await store.ingest(doc, "orphan.md") + restore() + assert outcome.status == "failure" + assert store.list_sources() == [] + assert fts.count_knowledge_source("orphan.md") == 0 + assert await _sources(store, "orphanword") == [] + # the name is not jammed: a retry stores it and delete removes it + assert (await store.ingest(doc, "orphan.md")).status == "stored" + assert await store.delete_source_async("orphan.md") == 3 + assert await _sources(store, "orphanword") == [] + + +async def test_failed_replacement_keeps_old_document_whole(stores): + store, fts = stores + v1, v2 = _doc("versionone"), _doc("versiontwo") + assert (await store.ingest(v1, "doc.md")).status == "stored" + restore = _fail_fts_insert(fts) + outcome = await store.ingest(v2, "doc.md") + restore() + assert outcome.status == "failure" + assert await _sources(store, "versiontwo") == [] + assert set(await _sources(store, "versionone")) == {"doc.md"} + assert store.source_is_durable("doc.md", expected_chunks=3) + assert store.get_source_snapshot("doc.md") == v1 + assert (await store.ingest(v1, "doc.md")).status == "unchanged" + assert await store.delete_source_async("doc.md") == 3 + + +async def test_replacement_without_embeddings_retires_old_vectors(stores): + store, _fts = stores + if not store._has_vec: + pytest.skip("sqlite-vec unavailable") + assert (await store.ingest("vector version one", "vectors.md", _Embedder())).status == "stored" + before = store._conn.execute("SELECT count(*) FROM knowledge_vec").fetchone()[0] + assert before == 1 + assert (await store.ingest("vector version two", "vectors.md")).status == "stored" + assert store._conn.execute("SELECT count(*) FROM knowledge_vec").fetchone()[0] == 0 + assert store.source_is_durable("vectors.md", expected_chunks=1) + + +async def test_db_commit_failure_after_fts_publish_is_compensated(stores): + store, fts = stores + real = store._conn + store._conn = _FailCommitOnce(real) + try: + outcome = await store.ingest(_doc("commitword"), "c.md") + finally: + store._conn = real + assert outcome.status == "failure" + assert fts.count_knowledge_source("c.md") == 0 + assert await _sources(store, "commitword") == [] + + +async def test_existing_orphan_rows_are_removed_by_delete_ingest_and_startup(stores): + """Installs that already carry leftovers from a failed ingest (pre-fix).""" + store, fts = stores + # leftovers of three past failures: no DB rows own these FTS rows + for i, name in enumerate(("gone.md", "reused.md", "untouched.md")): + assert fts.index_knowledge_chunk(f"stale{i}_0_x", f"leftoverword {name}", name, 0) + # (1) deleting the name removes its leftovers (master: "No document found") + assert await store.delete_source_async("gone.md") == 1 + assert fts.count_knowledge_source("gone.md") == 0 + # (2) a new ingest of the name replaces them (master: failure, name jammed) + assert (await store.ingest("fresh reused body", "reused.md")).status == "stored" + assert fts.count_knowledge_source("reused.md") == 1 + # (3) the startup reconciliation removes leftovers of names nobody touches + await store.backfill_fts_async() + assert fts.count_knowledge_source("untouched.md") == 0 + assert await _sources(store, "leftoverword") == [] + assert store.source_is_durable("reused.md", expected_chunks=1) + + +async def test_version_failure_fails_the_ingest_and_retry_repairs(stores): + store, _fts = stores + assert (await store.ingest("original text body", "notes.md")).status == "stored" + store._conn.execute( + "CREATE TRIGGER block_versions BEFORE INSERT ON knowledge_versions " + "WHEN NEW.source = 'notes.md' BEGIN SELECT RAISE(ABORT, 'injected version fault'); END" + ) + store._conn.commit() + outcome = await store.ingest("replacement text body", "notes.md") + assert outcome.status == "failure" + assert store.get_source_content("notes.md") == "original text body" + assert store.get_source_snapshot("notes.md") == "original text body" + store._conn.execute("DROP TRIGGER block_versions") + store._conn.commit() + assert (await store.ingest("replacement text body", "notes.md")).status == "stored" + assert store.get_source_snapshot("notes.md") == "replacement text body" + + +async def test_missing_snapshot_is_repaired_by_same_content_ingest(stores): + """A store damaged before the fix: current chunks, no current version row.""" + store, _fts = stores + assert (await store.ingest("original text body", "notes.md")).status == "stored" + store._conn.execute("DELETE FROM knowledge_versions WHERE source = 'notes.md'") + store._conn.commit() + assert store.get_source_snapshot("notes.md") is None + assert (await store.ingest("original text body", "notes.md")).status == "stored" + assert store.get_source_snapshot("notes.md") == "original text body" + assert (await store.ingest("original text body", "notes.md")).status == "unchanged" + + +async def test_cancelled_ingest_still_records_its_version(stores, monkeypatch): + store, _fts = stores + assert (await store.ingest("original text body", "notes.md")).status == "stored" + entered, release = threading.Event(), threading.Event() + real = store._write_chunks_sync + + def gated(*args, **kwargs): + entered.set() + release.wait(5) + return real(*args, **kwargs) + + monkeypatch.setattr(store, "_write_chunks_sync", gated) + task = asyncio.create_task(store.ingest("replacement text body", "notes.md")) + await asyncio.to_thread(entered.wait, 5) + task.cancel() + release.set() + with pytest.raises(asyncio.CancelledError): + await task + for _ in range(100): # let an orphaned worker (pre-fix) finish too + if not store._write_lock.locked(): + break + await asyncio.sleep(0.01) + await asyncio.sleep(0.2) + assert store.get_source_snapshot("notes.md") in {"original text body", "replacement text body"} + assert store.get_source_snapshot("notes.md") == store.get_source_content("notes.md") + + +def test_record_version_failure_leaves_no_pending_row(stores): + store, _fts = stores + real = store._conn + store._conn = _FailCommitOnce(real) + try: + assert store._record_version("a.md", "h", "body", 1, "u", "delete", "deleted") == 0 + finally: + store._conn = real + assert not store._conn.in_transaction + assert store._record_version("b.md", "h2", "b", 1, "u", "create") == 1 + assert store.get_versions("a.md") == [] diff --git a/tests/test_knowledge_reconciliation_boundaries.py b/tests/test_knowledge_reconciliation_boundaries.py new file mode 100644 index 000000000..6b8bca677 --- /dev/null +++ b/tests/test_knowledge_reconciliation_boundaries.py @@ -0,0 +1,90 @@ +"""Knowledge backfill must not delete FTS rows when inventory is unreadable.""" + +import pytest + +from src.knowledge.store import KnowledgeStore +from src.search.errors import SearchExecutionError +from src.search.fts import FullTextIndex + + +def test_backfill_does_not_delete_rows_when_inventory_fails(tmp_path, monkeypatch): + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts) + assert fts.index_knowledge_chunk("unowned", "keep until inventory works", "old", 0) + monkeypatch.setattr(fts, "knowledge_chunk_sources", lambda: None) + monkeypatch.setattr( + fts, "delete_knowledge_chunks", lambda *_: pytest.fail("deleted without inventory") + ) + assert store.backfill_fts() == 0 + assert fts.has_knowledge_chunk("unowned") + + +def test_backfill_unavailable_and_unreadable_metadata_leave_fts_intact(tmp_path): + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts) + assert fts.index_knowledge_chunk("orphan", "still present", "source", 0) + store._conn.close() + assert store.backfill_fts() == 0 + assert fts.has_knowledge_chunk("orphan") + store._conn = None + assert store.backfill_fts() == 0 + assert fts.has_knowledge_chunk("orphan") + + +class _FailedVersionConnection: + def __init__(self, real, *, rollback_fails=False): + self.real = real + self.rolled_back = False + self.rollback_fails = rollback_fails + + def execute(self, *args, **kwargs): + raise OSError("synthetic version write failure") + + def rollback(self): + self.rolled_back = True + if self.rollback_fails: + raise OSError("synthetic rollback failure") + self.real.rollback() + + +def test_failed_version_write_rolls_back_and_leaves_history_empty(tmp_path): + store = KnowledgeStore(str(tmp_path / "knowledge.db")) + connection = store._conn + fake = _FailedVersionConnection(connection) + store._conn = fake + assert store._record_version("source", "hash", "content", 1, "test", "ingest") == 0 + assert fake.rolled_back + store._conn = connection + assert store.get_versions("source") == [] + + +def test_version_write_failure_preserves_original_error_if_rollback_fails(tmp_path): + store = KnowledgeStore(str(tmp_path / "knowledge.db")) + connection = store._conn + fake = _FailedVersionConnection(connection, rollback_fails=True) + store._conn = fake + assert store._record_version("source", "hash", "content", 1, "test", "ingest") == 0 + assert fake.rolled_back + store._conn = connection + assert store.get_versions("source") == [] + + +@pytest.mark.asyncio +async def test_hybrid_search_refuses_unavailable_store_even_with_fts(tmp_path): + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts) + store.close() + with pytest.raises(SearchExecutionError, match="knowledge store is unavailable"): + await store.search_hybrid("searchable") + + +@pytest.mark.asyncio +async def test_ingest_refuses_unavailable_fts_without_publishing(tmp_path): + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts) + fts._conn.close() + fts._conn = None + outcome = await store.ingest("new document text", "new.md") + assert outcome.status != "stored" + assert store.get_source_content("new.md") is None + store.close() diff --git a/tests/test_knowledge_snapshot_campaign.py b/tests/test_knowledge_snapshot_campaign.py index 89395415b..962dec187 100644 --- a/tests/test_knowledge_snapshot_campaign.py +++ b/tests/test_knowledge_snapshot_campaign.py @@ -32,7 +32,7 @@ async def test_real_reingest_preserves_full_snapshot(tmp_path, body): @pytest.mark.asyncio -@pytest.mark.parametrize("failure", ["missing", "stale", "failed_snapshot"]) +@pytest.mark.parametrize("failure", ["missing", "stale"]) async def test_route_refuses_missing_or_stale_snapshot_without_changes( tmp_path, failure, monkeypatch, ): @@ -41,12 +41,9 @@ async def test_route_refuses_missing_or_stale_snapshot_without_changes( if failure == "missing": store._conn.execute("DELETE FROM knowledge_versions") store._conn.commit() - elif failure == "stale": + else: store._conn.execute("UPDATE knowledge_versions SET content='stale'") store._conn.commit() - else: - monkeypatch.setattr(store, "_record_version", lambda *args, **kwargs: 0) - await store.ingest("replacement", "document") before = store._conn.execute("SELECT * FROM knowledge_chunks").fetchall() routes = web.RouteTableDef() register_knowledge(routes, SimpleNamespace(knowledge=store, embedder=None)) diff --git a/tests/test_knowledge_store_durability_preconditions.py b/tests/test_knowledge_store_durability_preconditions.py new file mode 100644 index 000000000..0f99bdd47 --- /dev/null +++ b/tests/test_knowledge_store_durability_preconditions.py @@ -0,0 +1,34 @@ +"""Focused regressions for KnowledgeStore durability precondition failures.""" +from __future__ import annotations + +import hashlib + +from src.knowledge.store import KnowledgeStore +from src.search.fts import FullTextIndex + + +async def test_durability_rejects_incomplete_or_stale_source(tmp_path): + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = KnowledgeStore(str(tmp_path / "knowledge.db"), fts_index=fts) + content = "A synthetic document with enough content to persist." + try: + outcome = await store.ingest(content, "synthetic.md") + assert outcome.status == "stored" + + # The source exists, but its complete chunk set and content provenance + # are part of the durability contract, not optional metadata. + assert not store.source_is_durable("synthetic.md", expected_chunks=2) + assert not store.source_is_durable( + "synthetic.md", expected_content_hash=hashlib.sha256(b"stale").hexdigest() + ) + + fts_conn = fts._conn + fts._conn = None + try: + assert not store.source_is_durable("synthetic.md") + finally: + fts._conn = fts_conn + finally: + store.close() + if fts._conn is not None: + fts._conn.close() diff --git a/tests/test_loop_error_sanitization.py b/tests/test_loop_error_sanitization.py index b805fcdcb..9c0e7b850 100644 --- a/tests/test_loop_error_sanitization.py +++ b/tests/test_loop_error_sanitization.py @@ -60,3 +60,45 @@ async def exploding_iteration(prompt, ch, prev_context, cancel_event): assert "@everyone" not in joined # neutralized by the formatter assert any("RuntimeError" in h for h in history) assert any("stopped after" in s for s in channel.sent) + + +class TestLoopPostTruncation: + async def test_truncated_response_closes_cut_code_block(self): + import src.tools.autonomous_loop as al + + manager = al.LoopManager() + channel = _FakeChannel() + info = al.LoopInfo( + id="test-loop", + goal="g", + mode="notify", + interval_seconds=1, + stop_condition=None, + max_iterations=1, + channel_id="c", + requester_id="u1", + requester_name="user", + ) + response = "```python\n" + "x" * 1980 + "\nrest of code\n```" + await manager._post_response(info, channel, response) + assert channel.sent == ["```python\n" + "x" * 1940 + "\n```\n... (truncated)"] + + async def test_short_response_is_posted_verbatim(self): + import src.tools.autonomous_loop as al + + manager = al.LoopManager() + channel = _FakeChannel() + info = al.LoopInfo( + id="test-loop", + goal="g", + mode="notify", + interval_seconds=1, + stop_condition=None, + max_iterations=1, + channel_id="c", + requester_id="u1", + requester_name="user", + ) + response = "```python\nprint('kept exactly')\n```" + await manager._post_response(info, channel, response) + assert channel.sent == [response] diff --git a/tests/test_monitor_script.py b/tests/test_monitor_script.py new file mode 100644 index 000000000..15e0db8da --- /dev/null +++ b/tests/test_monitor_script.py @@ -0,0 +1,68 @@ +"""Monitor script reads the actual application log destination without real services.""" + +import subprocess +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "monitor.sh" + + +def _stub(bin_dir: Path, name: str, body: str): + path = bin_dir / name + path.write_text("#!/bin/sh\n" + body) + path.chmod(0o755) + + +def _run(tmp_path: Path, *, unit_exists: bool, env_extra: dict | None = None): + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + calls = tmp_path / "calls.log" + _stub(bin_dir, "journalctl", f'echo "journalctl $*" >> "{calls}"\necho "journal line"\n') + _stub( + bin_dir, "systemctl", f'echo "systemctl $*" >> "{calls}"\nexit {0 if unit_exists else 1}\n' + ) + _stub(bin_dir, "incus", f'echo "incus $*" >> "{calls}"\necho "incus journal line"\n') + env = { + "PATH": f"{bin_dir}:/usr/bin:/bin", + "HOME": str(tmp_path), + "ODIN_DEPLOY": "local", + **(env_extra or {}), + } + result = subprocess.run( + ["bash", str(SCRIPT), "logs", "5"], + env=env, + capture_output=True, + text=True, + timeout=30, + ) + return result, calls.read_text() if calls.exists() else "" + + +def test_service_install_reads_the_journal(tmp_path): + result, calls = _run(tmp_path, unit_exists=True) + assert result.returncode == 0, result.stderr + assert "journalctl -u odin -n 5 --no-pager" in calls + assert "journal line" in result.stdout + assert "Log file not found" not in result.stdout + + +def test_source_run_explains_stdout_instead_of_a_missing_file(tmp_path): + result, calls = _run(tmp_path, unit_exists=False) + assert result.returncode == 0, result.stderr + assert "journalctl" not in calls + assert "standard output/error, not to a file" in result.stdout + assert "ODIN_LOG_FILE" in result.stdout + + +def test_explicit_log_file_override_is_preserved(tmp_path): + log_file = tmp_path / "odin.out" + log_file.write_text("".join(f"line {i}\n" for i in range(20))) + result, calls = _run(tmp_path, unit_exists=True, env_extra={"ODIN_LOG_FILE": str(log_file)}) + assert result.returncode == 0, result.stderr + assert result.stdout.rstrip().splitlines()[-5:] == [f"line {i}" for i in range(15, 20)] + assert "journalctl" not in calls + + +def test_incus_reads_the_instance_journal_directly(tmp_path): + result, calls = _run(tmp_path, unit_exists=True, env_extra={"ODIN_DEPLOY": "incus"}) + assert result.returncode == 0, result.stderr + assert calls.splitlines() == ["incus exec odin -- journalctl -u odin -n 5 --no-pager"] diff --git a/tests/test_post_validation.py b/tests/test_post_validation.py index 75d65ce97..e44067761 100644 --- a/tests/test_post_validation.py +++ b/tests/test_post_validation.py @@ -3,6 +3,7 @@ import asyncio import json +import shlex import subprocess from pathlib import Path @@ -139,8 +140,9 @@ def test_service(self): def test_log_absent_unit(self): cmd = _build_command(Check(type="log_absent", target="unit=nginx:ERROR")) - assert "journalctl -u" in cmd + assert "journalctl" in cmd assert "grep" in cmd + assert shlex.split(cmd)[-3:] == ["nginx", "120", "ERROR"] def test_log_absent_bare(self): cmd = _build_command(Check(type="log_absent", target="kernel panic")) @@ -254,15 +256,17 @@ def test_service_multi_ok(self): assert status == "pass" def test_log_absent_with_output_fails(self): - status, _ = _evaluate(Check(type="log_absent", target="ERROR"), 0, "ERROR found") + status, _ = _evaluate( + Check(type="log_absent", target="ERROR"), 0, "ERROR found\nLOG_READ_OK" + ) assert status == "fail" def test_log_absent_empty_passes(self): - status, _ = _evaluate(Check(type="log_absent", target="ERROR"), 0, "") + status, _ = _evaluate(Check(type="log_absent", target="ERROR"), 0, "LOG_READ_OK") assert status == "pass" def test_log_present_empty_fails(self): - status, _ = _evaluate(Check(type="log_present", target="OK"), 0, "") + status, _ = _evaluate(Check(type="log_present", target="OK"), 0, "LOG_READ_OK") assert status == "fail" def test_command_exit_zero(self): diff --git a/tests/test_preserved_reasoning_accounting.py b/tests/test_preserved_reasoning_accounting.py new file mode 100644 index 000000000..7306812aa --- /dev/null +++ b/tests/test_preserved_reasoning_accounting.py @@ -0,0 +1,238 @@ +"""Regression coverage for GLM preserved reasoning in context sizing (#385).""" + +from __future__ import annotations + +import json +from types import SimpleNamespace + +from src.agents.manager import AgentInfo, _call_llm_with_recovery +from src.config.schema import ContextCompressionConfig +from src.discord.tool_loop import ToolLoopRunner, build_assistant_content +from src.llm.context_compressor import ( + SurfaceBoundary, + compress_tool_context, + emergency_compress_for_window, + estimate_message_chars, +) +from src.llm.errors import LLMContextLengthError +from src.llm.openai_compatible import OpenAICompatibleClient + + +def _client(*, preserve: bool = True) -> OpenAICompatibleClient: + return OpenAICompatibleClient( + "test", + model="glm-fixture", + base_url="https://example.invalid/v4", + reasoning_dialect="glm_thinking", + glm_clear_thinking=not preserve, + reasoning_content_feedback_policy="preserve" if preserve else "do_not_echo", + ) + + +def _transcript(n: int, reasoning_chars: int, result_chars: int = 2) -> list[dict]: + messages: list[dict] = [{"role": "user", "content": "TASK"}] + for i in range(n): + content = [] + if reasoning_chars: + content.append( + {"type": "reasoning_content", "reasoning_content": "r" * reasoning_chars} + ) + content.append( + {"type": "tool_use", "id": f"tu_{i}", "name": "read_file", "input": {"path": f"f{i}"}} + ) + messages.extend( + [ + {"role": "assistant", "content": content}, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": f"tu_{i}", + "content": "x" * result_chars, + } + ], + }, + ] + ) + return messages + + +def _wire_reasoning_chars(messages: list[dict]) -> int: + wire = _client()._convert_messages(messages, "") + return sum(len(entry.get("reasoning_content") or "") for entry in wire) + + +def _blocks(messages: list[dict], block_type: str) -> list[dict]: + return [ + block + for msg in messages + if isinstance(msg.get("content"), list) + for block in msg["content"] + if isinstance(block, dict) and block.get("type") == block_type + ] + + +def test_estimate_counts_every_replayed_reasoning_char(): + messages = _transcript(3, 10_000) + assert _wire_reasoning_chars(messages) == 30_000 + # Guard: the default mode still never puts reasoning in history. + raw = { + "choices": [ + { + "message": { + "content": "", + "reasoning_content": "r" * 500, + "tool_calls": [ + {"id": "c1", "function": {"name": "read_file", "arguments": "{}"}} + ], + } + } + ] + } + response = _client(preserve=False)._parse_response(raw) + assert response.reasoning_content is None + assert [b["type"] for b in build_assistant_content(response)] == ["tool_use"] + assert estimate_message_chars(messages) - estimate_message_chars(_transcript(3, 0)) == 30_000 + + +def test_estimate_charges_list_tool_results_as_openai_wire_json_without_mutating(): + structured_result = [ + {"type": "text", "text": "payload"}, + {"type": "resource", "uri": "file://large", "metadata": {"size": 12345}}, + ] + messages = [ + {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "c1", + "content": structured_result}]} + ] + wire = _client()._convert_messages(messages, "") + expected = len(json.dumps(structured_result)) + + assert wire == [ + {"role": "tool", "tool_call_id": "c1", "content": json.dumps(structured_result)} + ] + assert estimate_message_chars(messages) == len("user") + expected + assert messages[0]["content"][0]["content"] is structured_result + assert messages[0]["content"][0]["content"] == structured_result + + +def test_estimate_preserves_string_tool_result_accounting(): + messages = [{"role": "user", "content": [{"type": "tool_result", "content": "plain"}]}] + assert estimate_message_chars(messages) == len("user") + len("plain") + + +def test_estimate_preserves_dict_block_accounting(): + value = {"path": "file.txt", "nested": {"size": 12}} + messages = [{"role": "assistant", "content": [{"type": "tool_use", "input": value}]}] + assert estimate_message_chars(messages) == len("assistant") + len(json.dumps(value)) + + +def test_soft_compaction_triggers_on_reasoning_growth(): + out, compressed = compress_tool_context( + _transcript(6, 10_000), max_context_chars=5_000, keep_recent=2 + ) + assert compressed == 4 + assert [b["reasoning_content"] for b in _blocks(out, "reasoning_content")] == ["r" * 10_000] * 2 + + +def test_emergency_refuses_fit_while_newest_reasoning_exceeds_target(): + messages = _transcript(1, 10_000) + out, report = emergency_compress_for_window(messages, target_chars=500) + assert report["fits"] is False + assert out is messages + assert report["compressed_chars"] >= _wire_reasoning_chars(messages) + + +def test_emergency_counts_kept_reasoning_and_never_edits_it(): + messages = _transcript(3, 10_000, result_chars=30_000) + out, report = emergency_compress_for_window(messages, target_chars=25_000) + assert report["fits"] is True + assert _wire_reasoning_chars(out) <= report["compressed_chars"] <= 25_000 + assert report["compressed_chars"] == estimate_message_chars(out) + assert all(b["reasoning_content"] == "r" * 10_000 for b in _blocks(out, "reasoning_content")) + assert len(_blocks(out, "tool_result")[-1]["content"]) > 0 + # Guard: fair-share truncation without reasoning remains byte-identical. + out, report = emergency_compress_for_window( + _transcript(3, 0, result_chars=30_000), target_chars=25_000 + ) + assert (report["fits"], report["compressed_chars"], report["iterations_kept"]) == ( + True, + 24_998, + 3, + ) + assert [len(b["content"]) for b in _blocks(out, "tool_result")] == [8_303] * 3 + + +def test_boundary_mode_counts_reasoning(): + messages = [{"role": "user", "content": "old history"}, {"role": "user", "content": "REQ"}] + messages += _transcript(3, 10_000, result_chars=30_000)[1:] + out, report = emergency_compress_for_window( + messages, target_chars=25_000, boundary=SurfaceBoundary(request_start=1, envelope_len=1) + ) + assert report["fits"] is True + assert _wire_reasoning_chars(out) <= report["compressed_chars"] <= 25_000 + + +def test_chat_soft_pass_compacts_on_preserved_reasoning_growth(): + runner = ToolLoopRunner.__new__(ToolLoopRunner) + runner._get_context_compressor = lambda: ContextCompressionConfig(keep_recent_iterations=2) + runner._get_compression_stats = lambda: None + runner._window_observer = None + st = SimpleNamespace(iteration=6, messages=_transcript(6, 10_000), _boundary_envelope_len=1) + assert runner._maybe_compress(st, budget_snapshot=SimpleNamespace(primary_chars=5_000)) + assert len(_blocks(st.messages, "reasoning_content")) == 2 + + +def _agent(messages: list[dict]) -> AgentInfo: + agent = AgentInfo( + id="reasoning", + label="t", + goal="g", + channel_id="c", + requester_id="u", + requester_name="user", + ) + agent.messages = messages + agent.iteration_timeout = 30 + return agent + + +async def test_preserved_reasoning_overflow_is_not_resent_as_fitting(): + agent = _agent(_transcript(1, 10_000)) + sent = [] + + async def callback(messages, system_prompt, tools, generation_state=None): + sent.append(_wire_reasoning_chars(messages)) + raise LLMContextLengthError("overflow", provider="compat", code="context_length_exceeded") + + assert ( + await _call_llm_with_recovery( + agent, callback, "sys", [], rescue_ladder=(500, 250), generation_state={} + ) + is None + ) + assert sent == [10_000] + assert [r["fits"] for r in agent.context_recoveries] == [False] + + +async def test_preserved_reasoning_rescue_drops_older_iterations_and_keeps_newest(): + agent = _agent(_transcript(3, 10_000)) + sent = [] + + async def callback(messages, system_prompt, tools, generation_state=None): + sent.append((estimate_message_chars(messages), _wire_reasoning_chars(messages))) + if len(sent) == 1: + raise LLMContextLengthError( + "overflow", provider="compat", code="context_length_exceeded" + ) + return {"text": "DONE", "tool_calls": [], "stop_reason": "end_turn"} + + assert ( + await _call_llm_with_recovery( + agent, callback, "sys", [], rescue_ladder=(15_000,), generation_state={} + ) + is not None + ) + assert len(sent) == 2 + assert sent[1][1] == 10_000 + assert sent[1][0] <= 15_000 diff --git a/tests/test_release_workflow.py b/tests/test_release_workflow.py new file mode 100644 index 000000000..975db2531 --- /dev/null +++ b/tests/test_release_workflow.py @@ -0,0 +1,81 @@ +"""Release tags are validated before version metadata can be changed.""" + +import os +import subprocess +from pathlib import Path + +import pytest +import yaml + +WORKFLOWS = Path(__file__).resolve().parents[1] / ".github" / "workflows" + + +def _steps(): + workflow = yaml.safe_load((WORKFLOWS / "release.yml").read_text()) + return { + step.get("id", step.get("name")): step for step in workflow["jobs"]["build-deb"]["steps"] + } + + +def _bash(source, cwd, **env): + return subprocess.run( + ["bash", "-e", "-c", source], + cwd=cwd, + env={"PATH": os.environ["PATH"], **env}, + capture_output=True, + text=True, + timeout=30, + ) + + +def _release_version(tmp_path, tag): + steps = _steps() + project = tmp_path / "pyproject.toml" + project.write_text('[project]\nname = "fixture"\nversion = "0.0.0"\n') + output = tmp_path / "github_output" + output.write_text("") + extracted = _bash( + steps["version"]["run"], tmp_path, GITHUB_REF_NAME=tag, GITHUB_OUTPUT=str(output) + ) + if extracted.returncode: + return extracted.returncode, project.read_text() + version = dict(line.split("=", 1) for line in output.read_text().splitlines())["version"] + set_version = steps["Set version in pyproject.toml"] + assert set_version["env"] == {"VERSION": "${{ steps.version.outputs.version }}"} + changed = _bash(set_version["run"], tmp_path, VERSION=version) + return changed.returncode, project.read_text() + + +def test_valid_release_tag_sets_package_version(tmp_path): + result, project = _release_version(tmp_path, "v4.7.0") + assert result == 0 + assert 'version = "4.7.0"' in project + + +@pytest.mark.parametrize( + "tag", + [ + "v1.2.3$(printf${IFS}INJECTED)", + "v1.2.3`printf${IFS}INJECTED`", + 'v1.2.3";printf${IFS}INJECTED;"', + "v1.2.3&INJECTED", + "v1.2/3", + "vnext", + ], +) +def test_hostile_or_malformed_tag_rejected_before_use(tmp_path, tag): + result, project = _release_version(tmp_path, tag) + assert result != 0 + assert 'version = "0.0.0"' in project + assert "INJECTED" not in project + + +def test_release_workflow_never_substitutes_expressions_into_shell_source(): + offenders = [] + for path in [WORKFLOWS / "release.yml"]: + workflow = yaml.safe_load(path.read_text()) + for job in workflow.get("jobs", {}).values(): + for step in job.get("steps", []): + if "${{" in str(step.get("run", "")): + offenders.append((path.name, step.get("name"))) + assert offenders == [] diff --git a/tests/test_resume_admission.py b/tests/test_resume_admission.py index 669fa8b88..916de393d 100644 --- a/tests/test_resume_admission.py +++ b/tests/test_resume_admission.py @@ -350,6 +350,18 @@ class TestAutoResume: async def test_auto_resume_fires_when_capacity_returns(self, tmp_path, monkeypatch): monkeypatch.setattr(tr, "_AUTO_POLL_SECONDS", 0.02) h, original = await suspend_turn(tmp_path) + delivery_started = asyncio.Event() + allow_delivery = asyncio.Event() + send_chunked = h.bot.delivery.send_chunked + + async def gated_delivery(message, text): + delivery_started.set() + await allow_delivery.wait() + await send_chunked(message, text) + + # The terminal state is persisted before delivery. Hold that boundary + # open so the test proves terminal state alone is not delivery proof. + monkeypatch.setattr(h.bot.delivery, "send_chunked", gated_delivery) # Heal capacity + re-register the waiter (suspend_turn cancelled it). heal_capacity(h, text_response("Auto-finished.")) rows = h.store.list_suspended_sync("discord") @@ -357,13 +369,20 @@ async def test_auto_resume_fires_when_capacity_returns(self, tmp_path, monkeypat key = TurnKey("discord", rows[0]["channel_id"], rows[0]["message_id"]) h.manager.on_turn_suspended(key, rows[0]["generation"]) - for _ in range(600): - await asyncio.sleep(0.02) - if h.row()[0] in TurnStatus.TERMINAL: - break + try: + await asyncio.wait_for(delivery_started.wait(), timeout=12) + assert h.row()[0] == TurnStatus.TERMINAL_COMPLETED + assert original.replies == [] # held at the delivery boundary + finally: + allow_delivery.set() + + async def reply_landed(): + while not any("Auto-finished." in (r["content"] or "") for r in original.replies): + await asyncio.sleep(0.01) + + # Check the externally observable contract, not merely terminal state. + await asyncio.wait_for(reply_landed(), timeout=2) assert h.row()[0] == TurnStatus.TERMINAL_COMPLETED - # The reply landed against the ORIGINAL message. - assert any("Auto-finished." in (r["content"] or "") for r in original.replies) async def test_auto_resume_stands_down_when_session_advances( self, tmp_path, monkeypatch @@ -409,7 +428,7 @@ def test_repair_synthesizes_truthful_results(self): "tool_name": "run_command", "generation_seq": 1}, # "c" has no ledger row: intent was never recorded → never ran. ] - TurnResumeManager._repair_unmatched_tool_use(messages, operations) + TurnResumeManager._repair_unmatched_tool_use(messages, operations, generation_seq=1) assert messages[-1]["role"] == "user" by_id = {b["tool_use_id"]: b["content"] for b in messages[-1]["content"]} assert by_id["a"] == "real output" @@ -426,10 +445,159 @@ def test_matched_transcript_is_untouched(self): ]}, ] before = json.loads(json.dumps(messages)) - TurnResumeManager._repair_unmatched_tool_use(messages, []) + TurnResumeManager._repair_unmatched_tool_use(messages, [], generation_seq=1) assert messages == before +class TestReusedToolCallIdRepair: + def test_duplicate_operation_identity_is_refused(self): + messages = [{"role": "assistant", "content": [ + {"type": "tool_use", "id": "X"} + ]}] + operations = [ + {"generation_seq": 2, "tool_call_id": "X", "state": OpState.APPLIED, + "result": "first result"}, + {"generation_seq": 2, "tool_call_id": "X", "state": OpState.APPLIED, + "result": "second result"}, + ] + + with pytest.raises(ValueError, match="duplicate operation identity"): + TurnResumeManager._repair_unmatched_tool_use( + messages, operations, generation_seq=2 + ) + + @pytest.mark.parametrize( + ("generation_two", "expected"), + [ + (dict(state=OpState.APPLIED, result="second output"), "second output"), + (dict(state=OpState.DEFINITELY_FAILED, result=None), "outcome unknown"), + (dict(state=OpState.DEFINITELY_FAILED, result=None, + effect_class="EFFECT_FREE_OBSERVATION"), "Interrupted observation"), + (None, "never ran"), + ], + ) + def test_reused_id_is_answered_from_its_own_generation(self, generation_two, expected): + messages = [ + {"role": "assistant", "content": [{"type": "tool_use", "id": "X"}]}, + {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "X", + "content": "first output"}]}, + {"role": "assistant", "content": [{"type": "tool_use", "id": "X"}]}, + ] + operations = [{"generation_seq": 1, "tool_call_id": "X", "state": OpState.APPLIED, + "result": "first output"}] + if generation_two is not None: + operations.append({"generation_seq": 2, "tool_call_id": "X", **generation_two}) + TurnResumeManager._repair_unmatched_tool_use(messages, operations, generation_seq=2) + assert len(messages) == 4 + (result,) = messages[-1]["content"] + assert result["tool_use_id"] == "X" + assert expected in result["content"] + assert "first output" not in result["content"] + + def test_duplicate_ids_in_final_message_each_receive_one_result(self): + messages = [{"role": "assistant", "content": [ + {"type": "tool_use", "id": "X"}, {"type": "tool_use", "id": "X"} + ]}] + TurnResumeManager._repair_unmatched_tool_use(messages, [], generation_seq=2) + assert [block["tool_use_id"] for block in messages[-1]["content"]] == ["X", "X"] + + def test_unmatched_use_outside_final_message_is_rejected(self): + messages = [ + {"role": "assistant", "content": [{"type": "tool_use", "id": "X"}]}, + {"role": "user", "content": "unrelated"}, + ] + with pytest.raises(ValueError, match="outside the checkpoint"): + TurnResumeManager._repair_unmatched_tool_use(messages, [], generation_seq=1) + + async def test_crash_with_reused_id_resumes_with_every_call_answered(self, tmp_path): + import shutil + import sqlite3 + + from src.llm.types import LLMResponse, ToolCall + from src.tools.result_validator import ToolResult + + def calls(*specs): + return LLMResponse( + text="", + tool_calls=[ToolCall(id=i, name=n, input=a) for i, n, a in specs], + stop_reason="tool_use", + input_tokens=10, + output_tokens=5, + ) + + h = Harness( + [ + calls(("X", "run_command", {"command": "first"})), + calls( + ("X", "run_command", {"command": "second"}), + ("W", "wait_for_agents", {"agent_ids": ["a"]}), + ), + ], + tmp_path, + ) + executed: list[str] = [] + blocked = asyncio.Event() + + async def execute(tool_name, tool_input, *, user_id=None): + executed.append(tool_input["command"]) + return ToolResult(output=f"applied {tool_input['command']}", tool_name=tool_name) + + async def blocking_wait(*_args, **_kwargs): + blocked.set() + await asyncio.sleep(3600) + + h.bot.tool_executor.execute = execute + h.bot.native_tools.dispatch = blocking_wait + original = FakeMessage("do the long thing") + h.register(original) + task = asyncio.create_task( + h.bot.tool_loop.run(original, [{"role": "user", "content": original.content}]) + ) + await asyncio.wait_for(blocked.wait(), timeout=5) + for _ in range(200): + settled = h.store._conn.execute( + "SELECT state FROM operations WHERE generation_seq=2 AND tool_call_id='X'" + ).fetchone() + if settled and settled[0] == OpState.APPLIED: + break + await asyncio.sleep(0.01) + assert settled and settled[0] == OpState.APPLIED + crash_dir = tmp_path / "crash" + crash_dir.mkdir() + target = sqlite3.connect(crash_dir / "turns.sqlite3") + h.store._conn.backup(target) + target.close() + shutil.copytree(tmp_path / "ts" / "blobs", crash_dir / "blobs") + task.cancel() + await asyncio.gather(task, return_exceptions=True) + restarted = TurnStateStore(crash_dir / "turns.sqlite3") + h.store = restarted + h.manager._store = restarted + assert h.row()[0] == TurnStatus.SUSPENDED + + heal_capacity(h, text_response("resumed and done")) + result = await h.manager.try_explicit_resume(resume_msg(original)) + assert result is not None and result[0] == "resumed and done" + assert executed == ["first", "second"] + + sent = h.fake.calls[-1]["messages"] + open_uses: list[str] = [] + answers: list[tuple[str, str]] = [] + for msg in sent: + content = msg.get("content") + if not isinstance(content, list): + continue + for block in content: + if block.get("type") == "tool_use": + open_uses.append(block["id"]) + elif block.get("type") == "tool_result": + open_uses.remove(block["tool_use_id"]) + answers.append((block["tool_use_id"], str(block["content"]))) + assert open_uses == [] + assert answers[1] == ("X", "applied second") + assert answers[2][0] == "W" and "Interrupted observation" in answers[2][1] + + class TestPostAcquireSafety: async def test_corrupt_checkpoint_rejects_before_acquiring(self, tmp_path): """Review blocker #5 (PR #242): reconstruction runs BEFORE the diff --git a/tests/test_scheduled_digest_identity.py b/tests/test_scheduled_digest_identity.py new file mode 100644 index 000000000..2517a850d --- /dev/null +++ b/tests/test_scheduled_digest_identity.py @@ -0,0 +1,137 @@ +"""Scheduled digest host probes retain schedule identity and failures.""" + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from src.tools import ToolResult +from tests.fakes import make_bot + + +@pytest.fixture +def bot(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + return make_bot() + + +def _wire_domain_stubs(bot, aliases, dispatch): + events = bot.scheduled_events + events._host_registry = SimpleNamespace(active_aliases=lambda: aliases) + events._get_channel = lambda _channel_id: SimpleNamespace(id="55", send=AsyncMock()) + events._tool_executor.check_permission = lambda *_args: "" + events._tool_loop.dispatch_loop_tool_inner = dispatch + events._audit.log_execution = AsyncMock() + return events + + +@pytest.mark.asyncio +async def test_system_digest_uses_scheduler_identity_and_reports_denied_hosts(bot): + identities = [] + + async def dispatch(name, tool_input, _proxy, user_id): + identities.append(user_id) + return ToolResult( + output="Unknown or disallowed host", + ok=False, + error="permission_denied", + tool_name=name, + ) + + events = _wire_domain_stubs(bot, ("srv",), dispatch) + raw, failed, total = await events._format_digest_raw( + {"requester_id": None}, events._get_channel("55") + ) + + assert identities == ["scheduler", "scheduler"] + assert failed == ["Disk (srv)", "Memory (srv)"] + assert total == 2 + assert "Collection failed: Unknown or disallowed host" in raw + + +@pytest.mark.asyncio +async def test_user_digest_runs_as_its_requester(bot): + identities = [] + + async def dispatch(name, _tool_input, _proxy, user_id): + identities.append(user_id) + return ToolResult(output="ok", ok=True, tool_name=name) + + events = _wire_domain_stubs(bot, ("srv",), dispatch) + raw, failed, total = await events._format_digest_raw( + {"requester_id": "1234"}, events._get_channel("55") + ) + + assert identities == ["1234", "1234"] + assert failed == [] + assert total == 2 + assert "ok" in raw + + +@pytest.mark.asyncio +async def test_digest_with_no_reachable_host_is_a_failed_run(bot): + channel = SimpleNamespace(id="55", send=AsyncMock()) + + async def dispatch(name, _tool_input, _proxy, _user_id): + return ToolResult( + output="SSH connection refused", ok=False, error="execution_error", tool_name=name + ) + + events = _wire_domain_stubs(bot, ("offline",), dispatch) + events._get_channel = lambda _channel_id: channel + with pytest.raises(RuntimeError, match="Digest collected no data: all 2 checks failed"): + await events._on_scheduled_digest({"id": "D1", "channel_id": "55"}) + + notice = channel.send.await_args.args[0] + assert "Collection failed for every check (2 of 2)" in notice + assert "Disk (offline)" in notice + + +@pytest.mark.asyncio +async def test_digest_with_no_configured_hosts_is_not_reported_healthy(bot): + channel = SimpleNamespace(id="55", send=AsyncMock()) + events = _wire_domain_stubs(bot, (), AsyncMock()) + events._get_channel = lambda _channel_id: channel + + with pytest.raises(RuntimeError, match="No configured hosts"): + await events._on_scheduled_digest({"id": "D2", "channel_id": "55"}) + + assert channel.send.await_count == 1 + assert "Failed to collect data" in channel.send.await_args.args[0] + + +@pytest.mark.asyncio +async def test_cancelled_child_probe_is_reported_as_collection_failure(bot): + events = _wire_domain_stubs(bot, ("srv",), AsyncMock()) + events._tool_loop.dispatch_loop_tool_inner = AsyncMock( + side_effect=[asyncio.CancelledError(), ToolResult(output="mem ok", ok=True)] + ) + + raw, failed, total = await events._format_digest_raw( + {"requester_id": None}, events._get_channel("55") + ) + + assert failed == ["Disk (srv)"] + assert total == 2 + assert "Collection failed:" in raw + assert "mem ok" in raw + + +@pytest.mark.asyncio +async def test_audit_failure_after_delivery_does_not_retry_digest(bot): + channel = SimpleNamespace(id="55", send=AsyncMock()) + events = _wire_domain_stubs(bot, ("srv",), AsyncMock()) + events._get_channel = lambda _channel_id: channel + events._tool_loop.dispatch_loop_tool_inner = AsyncMock( + return_value=ToolResult(output="healthy", ok=True) + ) + events._audit.log_execution = AsyncMock(side_effect=RuntimeError("audit unavailable")) + + await events._on_scheduled_digest({"id": "D3", "channel_id": "55"}) + + assert channel.send.await_count == 1 + assert "healthy" in channel.send.await_args.args[0] + events._audit.log_execution.assert_awaited_once() diff --git a/tests/test_scheduled_events.py b/tests/test_scheduled_events.py index 3744acd79..7287dddab 100644 --- a/tests/test_scheduled_events.py +++ b/tests/test_scheduled_events.py @@ -114,12 +114,46 @@ async def test_collect_failure_and_notice_delivery_failure_propagate(self): class TestFormatDigestRaw: - async def test_gather_with_exception(self): + async def test_no_configured_hosts_fails_instead_of_reporting_empty_digest(self): + h = _handlers(get_config=lambda: SimpleNamespace(tools=SimpleNamespace(hosts={}))) + + with pytest.raises(RuntimeError, match="No configured hosts available"): + await h._format_digest_raw({}, _channel()) + + async def test_failed_probe_is_a_collection_failure(self): ex = MagicMock() - ex.execute = AsyncMock(side_effect=[RuntimeError("disk err"), "mem ok"]) + channel = _channel() + tool_loop = MagicMock() + failure = ToolResult( + output="disk err", ok=False, error="execution_error", tool_name="run_command" + ) + success = ToolResult(output="mem ok", ok=True, tool_name="run_command") + tool_loop.dispatch_loop_tool_inner = AsyncMock(side_effect=[failure, success]) h = _handlers(tool_executor=ex) - out = await h._format_digest_raw() - assert "ERROR" in out and "mem ok" in out + h._tool_loop = tool_loop + out, failed, total = await h._format_digest_raw({"requester_id": None}, channel) + assert "Collection failed: disk err" in out and "mem ok" in out + assert failed == ["Disk (srv)"] + assert total == 2 + assert all( + call.args[3] == "scheduler" + for call in tool_loop.dispatch_loop_tool_inner.await_args_list + ) + + async def test_dispatch_exception_is_reported_as_collection_failure(self): + tool_loop = MagicMock( + dispatch_loop_tool_inner=AsyncMock( + side_effect=[RuntimeError("host unreachable"), "mem ok"] + ) + ) + h = _handlers(tool_loop=tool_loop) + + raw, failed, total = await h._format_digest_raw({}, _channel()) + + assert failed == ["Disk (srv)"] + assert total == 2 + assert "Collection failed: Error executing run_command: host unreachable" in raw + assert "### Memory (srv)\nmem ok" in raw class TestResolveMentions: diff --git a/tests/test_scheduled_events_digest_failure_summary.py b/tests/test_scheduled_events_digest_failure_summary.py new file mode 100644 index 000000000..e30991f64 --- /dev/null +++ b/tests/test_scheduled_events_digest_failure_summary.py @@ -0,0 +1,50 @@ +"""Exercise the digest's many-failed-check summary using fake-only deps.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from src.discord.scheduled_events import ScheduledEventHandlers, ScheduledEventsDeps +from src.tools import ToolResult + + +@pytest.mark.asyncio +async def test_digest_summarizes_many_failures_with_bounded_label_list(): + channel = SimpleNamespace(send=AsyncMock()) + aliases = tuple(f"host-{index}" for index in range(6)) + failure = ToolResult( + output="unavailable", ok=False, error="execution_error", tool_name="run_command" + ) + tool_loop = MagicMock( + dispatch_loop_tool_inner=AsyncMock( + side_effect=[failure] * 11 + [ToolResult(output="healthy", ok=True)] + ) + ) + deps = ScheduledEventsDeps( + get_config=lambda: SimpleNamespace(tools=SimpleNamespace(hosts={})), + get_channel=lambda _channel_id: channel, + get_guilds=lambda: [], + tool_executor=MagicMock(check_permission=MagicMock(return_value="")), + audit=MagicMock(log_execution=AsyncMock(), log_event=AsyncMock()), + llm_gateway=SimpleNamespace( + active_client=SimpleNamespace(chat=AsyncMock(return_value="partial summary")) + ), + tool_loop=tool_loop, + agent_task_tools=MagicMock(), + host_registry=SimpleNamespace(active_aliases=lambda: aliases), + ) + handlers = ScheduledEventHandlers(deps) + + await handlers._on_scheduled_digest({"id": "D-many", "channel_id": "42"}) + + assert tool_loop.dispatch_loop_tool_inner.await_count == 12 + notice = channel.send.await_args.args[0] + assert "partial summary" in notice + assert "Collection failed for 11 of 12 checks:" in notice + assert "Disk (host-0)" in notice + assert "Memory (host-4)" in notice + assert "…" in notice + assert "Disk (host-5)" not in notice diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 1e281c41c..3219e63df 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -370,6 +370,49 @@ async def test_tick_callback_error_does_not_crash(self, tmp_path): # --------------------------------------------------------------------------- class TestSchedulerFireTriggers: + async def test_named_grafana_trigger_matches_any_alert_in_batch(self, tmp_path): + s = _make_scheduler(tmp_path) + cb = AsyncMock() + s._callback = cb + + await s.add( + "second alert", "reminder", "chan1", + trigger={"source": "grafana", "event": "alert", "alert_name": "second"}, + ) + await s.add( + "catch all", "reminder", "chan1", + trigger={"source": "grafana", "event": "alert"}, + ) + await s.add( + "wrong event", "reminder", "chan1", + trigger={"source": "grafana", "event": "resolved", "alert_name": "second"}, + ) + await s.add( + "absent", "reminder", "chan1", + trigger={"source": "grafana", "event": "alert", "alert_name": "missing"}, + ) + + fired = await s.fire_triggers("grafana", { + "event": "alert", "alert_name": "FIRST", + "alert_names": ["FIRST", "Second CPU"], + }) + assert fired == 2 + assert cb.await_count == 2 + + async def test_single_name_event_data_still_matches(self, tmp_path): + s = _make_scheduler(tmp_path) + cb = AsyncMock() + s._callback = cb + await s.add( + "legacy", "reminder", "chan1", + trigger={"source": "grafana", "event": "alert", "alert_name": "highcpu"}, + ) + + assert await s.fire_triggers("grafana", { + "event": "alert", "alert_name": "HighCPU", + }) == 1 + cb.assert_awaited_once() + async def test_fire_triggers_matching(self, tmp_path): s = _make_scheduler(tmp_path) cb = AsyncMock() @@ -1924,3 +1967,222 @@ async def test_failed_one_time_is_removed_after_later_success(self, tmp_path): assert s.list_all() == [] history = await s.history.query(sched["id"]) assert [entry["status"] for entry in reversed(history)] == ["failure", "success"] + + +_SCHEDULE_STEPS = [ + {"tool_name": "run_command", "tool_input": {"host": "synthetic", "command": "deploy"}} +] + + +async def _side_effecting_once(s: Scheduler, action: str, when: str) -> dict: + kwargs = {"run_at": when} + if action == "workflow": + kwargs["steps"] = _SCHEDULE_STEPS + elif action == "check": + kwargs.update( + tool_name="run_command", tool_input={"host": "synthetic", "command": "deploy"} + ) + elif action == "webhook": + kwargs["webhook_config"] = {"url": "http://192.0.2.1/hook", "method": "POST"} + return await s.add(action, action, "chan1", **kwargs) + + +class TestInterruptedOneTimeRuns: + @pytest.mark.parametrize("action", ["workflow", "check", "webhook"]) + async def test_interrupted_side_effecting_run_is_quarantined_not_replayed( + self, tmp_path, action + ): + s = _make_scheduler(tmp_path) + entered = asyncio.Event() + + async def blocking(*_args): + entered.set() + await asyncio.sleep(3600) + + s._callback = blocking + s._execute_webhook = blocking + sched = await _side_effecting_once( + s, action, (datetime.now(UTC) - timedelta(seconds=5)).isoformat() + ) + s.start(blocking) + await asyncio.wait_for(entered.wait(), timeout=5) + await s.stop() + + restarted = _make_scheduler(tmp_path) + (record,) = restarted.list_all() + assert record["paused"] and "started at" in record["inert_reason"] + assert "run_started_at" not in record + replay = AsyncMock() + restarted._callback = replay + restarted._execute_webhook = replay + await restarted._tick() + replay.assert_not_awaited() + with pytest.raises(ValueError, match="started at"): + await restarted.run_now(sched["id"]) + with pytest.raises(ValueError, match="started at"): + await restarted.update(sched["id"], paused=False) + rearmed = await restarted.update( + sched["id"], run_at=(datetime.now(UTC) + timedelta(hours=1)).isoformat() + ) + assert not rearmed["paused"] and "inert_reason" not in rearmed + + async def test_started_but_unsaved_completion_quarantines_in_process(self, tmp_path): + s = _make_scheduler(tmp_path) + effects = [] + + async def effect(*_args): + effects.append("sent") + + s._callback = effect + await _side_effecting_once( + s, "workflow", (datetime.now(UTC) - timedelta(seconds=5)).isoformat() + ) + publish = s._publish + + async def fail_completion(candidate): + # The start marker publishes successfully. Fail the write that + # would remove it, after the callback has made its effect. + if effects: + raise OSError("synthetic persistence failure") + await publish(candidate) + + s._publish = fail_completion + with pytest.raises(OSError, match="synthetic persistence"): + await s._tick() + assert effects == ["sent"] + s._publish = publish + await s._tick() + assert effects == ["sent"] + assert s.list_all()[0]["paused"] + assert "completion was never recorded" in s.list_all()[0]["inert_reason"] + + async def test_failed_job_clears_marker_and_retry_runs(self, tmp_path): + s = _make_scheduler(tmp_path) + attempted = [] + + async def fail_then_succeed(*_args): + attempted.append("attempt") + if len(attempted) == 1: + raise RuntimeError("synthetic failure") + + s._callback = fail_then_succeed + sched = await s.add( + "retry", "workflow", "chan1", + run_at=(datetime.now(UTC) - timedelta(seconds=5)).isoformat(), + steps=_SCHEDULE_STEPS, max_retries=1, retry_backoff_seconds=1, + ) + await s._tick() + (record,) = _make_scheduler(tmp_path).list_all() + assert record["id"] == sched["id"] and not record.get("paused") + assert record["last_error"] == "synthetic failure" + assert "run_started_at" not in record + async with s._lock: + s._schedules[0]["retry_at"] = (datetime.now(UTC) - timedelta(seconds=1)).isoformat() + s._save() + await s._tick() + assert attempted == ["attempt", "attempt"] + assert s.list_all() == [] + + async def test_recurring_and_digest_are_not_quarantined(self, tmp_path): + s = _make_scheduler(tmp_path) + entered = asyncio.Event() + + async def blocking(*_args): + entered.set() + await asyncio.sleep(3600) + + s._callback = blocking + sched = await s.add( + "recurring", "workflow", "chan1", cron="* * * * *", steps=_SCHEDULE_STEPS + ) + async with s._lock: + s._schedules[0]["next_run"] = (datetime.now(UTC) - timedelta(seconds=1)).isoformat() + s.start(blocking) + await asyncio.wait_for(entered.wait(), timeout=5) + await s.stop() + (record,) = _make_scheduler(tmp_path).list_all() + assert record["id"] == sched["id"] and not record.get("paused") + assert "run_started_at" not in record + + digest = _make_scheduler(tmp_path / "digest") + await digest.add( + "digest", "digest", "chan1", + run_at=(datetime.now(UTC) - timedelta(seconds=5)).isoformat(), + ) + digest._callback = blocking + entered.clear() + digest.start(blocking) + await asyncio.wait_for(entered.wait(), timeout=5) + await digest.stop() + (record,) = _make_scheduler(tmp_path / "digest").list_all() + assert not record.get("paused") and "run_started_at" not in record + + async def test_queued_one_time_that_never_started_runs_after_restart(self, tmp_path): + s = _make_scheduler(tmp_path) + for index in range(2): + await s.add( + f"job {index}", "workflow", "chan1", + run_at=(datetime.now(UTC) - timedelta(seconds=5)).isoformat(), + steps=_SCHEDULE_STEPS, + ) + entered = asyncio.Event() + + async def blocking(*_args): + entered.set() + await asyncio.sleep(3600) + + s.start(blocking) + await asyncio.wait_for(entered.wait(), timeout=5) + await s.stop() + restarted = _make_scheduler(tmp_path) + assert len(restarted.list_all()) == 2 + started = [record for record in restarted.list_all() if record.get("paused")] + assert len(started) == 1 + replay = AsyncMock() + restarted._callback = replay + await restarted._tick() + replay.assert_awaited_once() + assert replay.await_args.args[0]["id"] != started[0]["id"] + + async def test_interrupted_run_is_quarantined_after_reload(self, tmp_path): + s = _make_scheduler(tmp_path) + entered = asyncio.Event() + release = asyncio.Event() + + async def callback(_schedule): + entered.set() + await release.wait() + + s._callback = callback + past = (datetime.now(UTC) - timedelta(minutes=1)).isoformat() + sched = await s.add( + "once", "workflow", "chan1", run_at=past, + steps=[{"tool_name": "run_command", "tool_input": {"command": "true"}}], + ) + task = asyncio.create_task(s._tick()) + await asyncio.wait_for(entered.wait(), timeout=2) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + + reloaded = _make_scheduler(tmp_path) + record = reloaded.list_all()[0] + assert record["paused"] is True + assert "completion was never recorded" in record["inert_reason"] + assert "run_started_at" not in record + assert "run_started_at" in sched or "run_started_at" in s.list_all()[0] + with pytest.raises(ValueError, match="completion was never recorded"): + await reloaded.run_now(sched["id"]) + rearmed = await reloaded.update( + sched["id"], run_at=(datetime.now(UTC) + timedelta(hours=1)).isoformat() + ) + assert rearmed["paused"] is False + assert "inert_reason" not in rearmed + + async def test_reminder_does_not_persist_run_start_marker(self, tmp_path): + s = _make_scheduler(tmp_path) + s._callback = AsyncMock() + past = (datetime.now(UTC) - timedelta(minutes=1)).isoformat() + await s.add("remind", "reminder", "chan1", run_at=past) + await s._tick() + assert "run_started_at" not in s.data_path.read_text() diff --git a/tests/test_search_history_source_priority.py b/tests/test_search_history_source_priority.py index 2c25d13fb..64a3bc59e 100644 --- a/tests/test_search_history_source_priority.py +++ b/tests/test_search_history_source_priority.py @@ -1,6 +1,7 @@ """B9: exact channel history cannot be crowded out by semantic session hits.""" from __future__ import annotations +import json import time import uuid from datetime import UTC, datetime @@ -136,7 +137,60 @@ async def test_current_and_archive_user_filters_are_unchanged(history): results = await manager.search_history("needle", limit=4, user_id="alice") assert [r["user_id"] for r in results[:2]] == ["alice", "alice"] - assert [r["type"] for r in results[2:]] == ["channel", "semantic"] + assert results[2:] == [] + manager._vector_store.search_hybrid.assert_not_awaited() + + +async def test_user_filter_applies_to_every_source(history): + manager, logger, index = history + now = time.time() + live = manager.get_or_create("live") + live.summary = "needle legacy live" + live.summary_segments = [ + {"summary": "needle alice segment", "participants": ["alice"], + "start_ts": now - 2, "end_ts": now}, + {"summary": "needle bob segment", "participants": ["bob"], + "start_ts": now - 3, "end_ts": now - 1}, + {"summary": "needle unclaimed", "participants": [], + "start_ts": now - 4, "end_ts": now - 2}, + ] + archive_dir = manager.persist_dir / "archive" + archive_dir.mkdir(exist_ok=True) + (archive_dir / "history.json").write_text(json.dumps({ + "channel_id": "archive", "summary": "needle legacy archived", + "summary_segments": [ + {"summary": "needle alice archive", "participants": ["alice"], + "start_ts": now - 5, "end_ts": now - 4}, + {"summary": "needle bob archive", "participants": ["bob"], + "start_ts": now - 6, "end_ts": now - 5}, + ], "messages": [{"role": "user", "content": "needle alice raw", + "timestamp": now, "user_id": "alice"}], + })) + with (logger._log_dir / "logs.jsonl").open("a") as stream: + for author in ("alice", "bob"): + stream.write(json.dumps({"channel_id": "logs", "author_id": author, + "author": author, "ts": now + 1, + "content": f"needle {author} log"}) + "\n") + _index(index, "needle unattributed index") + manager._vector_store.search_hybrid.return_value = [_semantic("hybrid", now)] + results = await manager.search_history("needle", user_id="alice", limit=20) + assert [r["content"] for r in results] == [ + "needle alice segment", "needle alice archive", "needle alice raw", + "needle alice log", + ] + manager._vector_store.search_hybrid.assert_not_awaited() + + +async def test_user_filtered_channel_scan_counts_only_matching_authors(history): + manager, logger, _ = history + with (logger._log_dir / "target.jsonl").open("a") as stream: + for author in ["bob"] * 8 + ["alice"] + ["bob"] * 8: + stream.write(json.dumps({"channel_id": "target", "author_id": author, + "author": author, "ts": 100, + "content": f"needle {author}"}) + "\n") + assert [r["content"] for r in await manager.search_history( + "needle", limit=1, user_id="alice", channel_id="target", + )] == ["needle alice"] @pytest.mark.parametrize("limit", [1, 3]) diff --git a/tests/test_session_archive_failure_boundaries.py b/tests/test_session_archive_failure_boundaries.py new file mode 100644 index 000000000..a17899042 --- /dev/null +++ b/tests/test_session_archive_failure_boundaries.py @@ -0,0 +1,74 @@ +"""Archive restoration/search must survive malformed names and files.""" + +import json +from unittest.mock import AsyncMock + +import pytest + +from src.sessions.manager import SessionManager + + +def _manager(tmp_path, **kwargs): + return SessionManager(max_history=8, max_age_hours=24, + persist_dir=str(tmp_path), **kwargs) + + +def test_restore_falls_back_from_bad_name_and_corrupt_newest(tmp_path): + archives = tmp_path / "archive" + archives.mkdir() + (archives / "room_1.json").write_text(json.dumps({ + "channel_id": "room", "messages": [ + {"role": "user", "content": "preserved", "timestamp": 1}, + ], + })) + (archives / "room_9999999999.json").write_text("{truncated") + malformed = archives / "room_invalid.json" + malformed.write_text("{truncated") + assert _manager(tmp_path).get_or_create("room").messages[0].content == "preserved" + + +def test_restore_invalid_timestamp_uses_file_mtime(tmp_path): + archives = tmp_path / "archive" + archives.mkdir() + (archives / "room_invalid.json").write_text(json.dumps({ + "channel_id": "room", "summary": "old context", + })) + assert _manager(tmp_path).get_or_create("room").summary == "old context" + + +@pytest.mark.asyncio +async def test_archive_search_skips_corrupt_file_and_keeps_good_matches(tmp_path): + archives = tmp_path / "archive" + archives.mkdir() + (archives / "room_2.json").write_text("{truncated") + (archives / "room_1.json").write_text(json.dumps({ + "channel_id": "room", "messages": [ + {"role": "user", "content": "needle survives", "timestamp": 1}, + ], + })) + results = await _manager(tmp_path).search_history("needle", channel_id="room") + assert [r["content"] for r in results] == ["needle survives"] + + +def test_archive_search_early_limit_and_corrupt_file_fallback(tmp_path): + archives = tmp_path / "archive" + archives.mkdir() + (archives / "room_2.json").write_text("{truncated") + (archives / "room_1.json").write_text(json.dumps({ + "channel_id": "room", "messages": [ + {"role": "user", "content": "needle survives", "timestamp": 1}, + {"role": "user", "content": "needle newest", "timestamp": 2}, + ], + })) + result = _manager(tmp_path)._search_archives("needle", 1, channel_id="room") + assert len(result) == 1 + assert result[0]["content"] == "needle newest" + + +@pytest.mark.asyncio +async def test_safe_index_catches_failed_vector_io(tmp_path): + vector = AsyncMock() + vector.index_session.side_effect = OSError("synthetic vector disk failure") + manager = _manager(tmp_path, vector_store=vector) + await manager._safe_index(tmp_path / "archive" / "not-written.json") + vector.index_session.assert_awaited_once() diff --git a/tests/test_session_persistence_reliability.py b/tests/test_session_persistence_reliability.py index 98132ca3b..8d278785e 100644 --- a/tests/test_session_persistence_reliability.py +++ b/tests/test_session_persistence_reliability.py @@ -253,6 +253,23 @@ def search(self, query, limit): assert not any("banana" in h.get("content", "") for h in hits) +async def test_channel_log_fallback_preserves_legacy_search_signature(tmp_path): + """Unfiltered history calls retain the logger's search(query, limit) contract.""" + mgr = _mgr(tmp_path) + + class _FakeLogger: + def search(self, query, limit): + assert query == "banana" + return [{ + "type": "user", "content": "banana scoped log", + "timestamp": 200.0, "channel_id": "c9", "user_id": "u", + }] + + mgr._channel_logger = _FakeLogger() + hits = await mgr.search_history("banana", channel_id="c9") + assert any(hit["content"] == "banana scoped log" for hit in hits) + + # --------------------------------------------------------------------------- # Compaction source length # --------------------------------------------------------------------------- diff --git a/tests/test_session_search.py b/tests/test_session_search.py index 50c826170..b6007a417 100644 --- a/tests/test_session_search.py +++ b/tests/test_session_search.py @@ -711,12 +711,11 @@ def test_archive_no_archive_dir(self, tmp_path): results = mgr._search_archives("test", 10) assert results == [] - async def test_summary_not_filtered_by_user_id(self, tmp_path): + async def test_summary_without_provenance_filtered_by_user_id(self, tmp_path): mgr = _manager(tmp_path) mgr._sessions["ch1"] = _session("ch1", summary="deployment plan") results = await mgr.search_history("deployment", user_id="alice") - assert len(results) == 1 - assert results[0]["type"] == "summary" + assert results == [] async def test_search_history_deduplication(self, tmp_path): mgr = _manager(tmp_path) diff --git a/tests/test_session_segment_index.py b/tests/test_session_segment_index.py new file mode 100644 index 000000000..4f1e1444c --- /dev/null +++ b/tests/test_session_segment_index.py @@ -0,0 +1,96 @@ +"""Archived summary segments retain independent, recoverable search identities.""" +from __future__ import annotations + +import json + +from src.search.fts import FullTextIndex +from src.search.vectorstore import SessionVectorStore, summary_segment_doc_id +from src.sessions.manager import SessionManager + + +def _archive(path, segments, *, messages=None, summary=""): + path.write_text(json.dumps({"channel_id": "room", "summary": summary, + "messages": messages or [], "summary_segments": segments, + "last_active": 110})) + + +async def test_segment_only_backfill_is_idempotent_and_hybrid_finds_canonical_id(tmp_path): + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + segment = {"summary": "uniquearchivefact about deployment", "start_ts": 40, + "end_ts": 50, "source_count": 3, "participants": ["alice"]} + _archive(archive_dir / "first.json", [segment]) + _archive(archive_dir / "second.json", [segment]) + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = SessionVectorStore(str(tmp_path / "sessions.db"), fts_index=fts) + assert await store.backfill_segments(archive_dir, None) == 2 + assert await store.backfill_segments(archive_dir, None) == 0 + assert await store.index_session(archive_dir / "first.json", None) + canonical = summary_segment_doc_id("room", segment) + results = await store.search_hybrid("uniquearchivefact", None) + assert [r["doc_id"] for r in results] == [canonical] + assert store._conn.execute("SELECT COUNT(*) FROM session_archives WHERE doc_id = ?", + (canonical,)).fetchone()[0] == 1 + assert store._conn.execute("SELECT message_count FROM session_archives WHERE doc_id = ?", + (canonical,)).fetchone()[0] == 3 + fts._conn.execute("DELETE FROM session_fts WHERE doc_id = ?", (canonical,)) + fts._conn.commit() + assert await store.backfill_segments(archive_dir, None) == 0 + assert fts.has_session(canonical) + fts._conn.close() + store._conn.close() + + +async def test_unacknowledged_fts_write_retries_on_next_backfill(tmp_path): + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + segment = {"summary": "recoverablefact", "start_ts": 1, "end_ts": 2} + _archive(archive_dir / "one.json", [segment]) + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = SessionVectorStore(str(tmp_path / "sessions.db"), fts_index=fts) + original = fts.index_session + fts.index_session = lambda *args: False + assert await store.backfill_segments(archive_dir, None) == 0 + assert store._conn.execute("SELECT COUNT(*) FROM segment_index_state").fetchone()[0] == 0 + fts.index_session = original + assert await store.backfill_segments(archive_dir, None) == 1 + assert fts.has_session(summary_segment_doc_id("room", segment)) + fts._conn.close() + store._conn.close() + + +async def test_fts_ack_before_metadata_commit_retries_missing_segment(tmp_path): + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + segment = {"summary": "interruptedfact", "start_ts": 1, "end_ts": 2} + _archive(archive_dir / "one.json", [segment]) + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = SessionVectorStore(str(tmp_path / "sessions.db"), fts_index=fts) + store._conn.execute("CREATE TRIGGER abort_segment_state BEFORE INSERT ON " + "segment_index_state BEGIN SELECT RAISE(ABORT, 'interrupted'); END") + store._conn.commit() + assert await store.backfill_segments(archive_dir, None) == 0 + canonical = summary_segment_doc_id("room", segment) + assert fts.has_session(canonical) + assert not store._segment_exists_sync(canonical) + store._conn.execute("DROP TRIGGER abort_segment_state") + store._conn.commit() + assert await store.backfill_segments(archive_dir, None) == 1 + assert store._segment_exists_sync(canonical) + fts._conn.close() + store._conn.close() + + +async def test_archive_search_dedupes_before_limit_and_respects_participants_and_reset(tmp_path): + manager = SessionManager(max_history=50, max_age_hours=24, persist_dir=str(tmp_path)) + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + segment = {"summary": "rarearchivedfact", "start_ts": 5, "end_ts": 10, + "participants": ["alice", "bob"]} + for name in ("one", "two"): + _archive(archive_dir / f"{name}.json", [segment]) + assert len(await manager.search_history("rarearchivedfact", limit=2)) == 1 + assert len(await manager.search_history("rarearchivedfact", user_id="alice")) == 1 + assert await manager.search_history("rarearchivedfact", user_id="other") == [] + manager._reset_epochs["room"] = 10 + assert await manager.search_history("rarearchivedfact") == [] diff --git a/tests/test_time_parser.py b/tests/test_time_parser.py index 39c0c28ca..3dd1f9918 100644 --- a/tests/test_time_parser.py +++ b/tests/test_time_parser.py @@ -5,7 +5,7 @@ """ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime, timedelta from zoneinfo import ZoneInfo import pytest @@ -13,7 +13,7 @@ from src.tools import time_parser from src.tools.time_parser import ( _next_weekday, - _parse_time_of_day, + _split_time_of_day, parse_time, set_default_timezone, ) @@ -24,17 +24,17 @@ class TestParseTimeOfDay: def test_12_hour(self): - assert _parse_time_of_day("9am") == (9, 0) - assert _parse_time_of_day("9:30pm") == (21, 30) - assert _parse_time_of_day("12am") == (0, 0) # midnight - assert _parse_time_of_day("12pm") == (12, 0) # noon + assert _split_time_of_day("9am")[0] == (9, 0) + assert _split_time_of_day("9:30pm")[0] == (21, 30) + assert _split_time_of_day("12am")[0] == (0, 0) # midnight + assert _split_time_of_day("12pm")[0] == (12, 0) # noon def test_24_hour(self): - assert _parse_time_of_day("17:00") == (17, 0) - assert _parse_time_of_day("09:30") == (9, 30) + assert _split_time_of_day("17:00")[0] == (17, 0) + assert _split_time_of_day("09:30")[0] == (9, 30) def test_bare_hour_is_none(self): - assert _parse_time_of_day("9") is None + assert _split_time_of_day("9") is None class TestNextWeekday: @@ -130,3 +130,98 @@ def test_set_default_timezone(self): assert time_parser._default_tz.key == "America/New_York" finally: set_default_timezone("UTC") # restore + + +NY = ZoneInfo("America/New_York") + + +def _elapsed(expression: str, now: datetime) -> timedelta: + result = datetime.fromisoformat(parse_time(expression, now)) + return result.astimezone(UTC) - now.astimezone(UTC) + + +class TestCompoundDurations: + @pytest.mark.parametrize("expression", [ + "in 1 hour 30 minutes", "in 1 hour and 30 minutes", "in 1 hour, 30 minutes" + ]) + def test_compound_hours_and_minutes(self, expression): + assert parse_time(expression, NOW).startswith("2026-03-18T13:30") + + def test_days_weeks_and_elapsed_units(self): + assert parse_time("in 2 days 3 hours", NOW).startswith("2026-03-20T15:00") + assert parse_time("in 1 week 2 days", NOW).startswith("2026-03-27T12:00") + + @pytest.mark.parametrize("expression", ["in 2 days at 9am", "in 2 days 9am"]) + def test_days_then_clock(self, expression): + assert parse_time(expression, NOW).startswith("2026-03-20T09:00") + + def test_unknown_later_unit(self): + with pytest.raises(ValueError, match="Unknown time unit: fortnights"): + parse_time("in 1 hour 30 fortnights", NOW) + + @pytest.mark.parametrize("expression", [ + "in 1 hour 30m", "in 1 hour and a half", "in 2 hours tomorrow", + "in 2 hours at 5pm", "in 2 days on friday", "in 0 hours at 5pm", + ]) + def test_rejects_unconsumed_time_words(self, expression): + with pytest.raises(ValueError, match="Cannot parse time expression"): + parse_time(expression, NOW) + + @pytest.mark.parametrize("tail", [ + " please", " from now", " or so", " at the latest", " to check the 3 servers", + " tonight", " today", " est", " utc", + ]) + def test_harmless_tails(self, tail): + assert parse_time("in 2 hours" + tail, NOW) == parse_time("in 2 hours", NOW) + assert parse_time("at 5pm" + tail, NOW) == parse_time("at 5pm", NOW) + + +class TestClockTimeWithDay: + @pytest.mark.parametrize("expression", ["at 5pm tomorrow", "5pm tomorrow"]) + def test_tomorrow_after_time(self, expression): + assert parse_time(expression, NOW).startswith("2026-03-19T17:00") + + @pytest.mark.parametrize("expression, expected", [ + ("at 9am on monday", "2026-03-23T09:00"), + ("3pm friday", "2026-03-20T15:00"), + ("friday 3pm", "2026-03-20T15:00"), + ("next friday 3pm", "2026-03-20T15:00"), + ("sat at 3pm", "2026-03-21T15:00"), + ]) + def test_day_and_time_orders(self, expression, expected): + assert parse_time(expression, NOW).startswith(expected) + + +class TestDaylightSaving: + def test_elapsed_hours_across_spring_forward(self): + assert _elapsed("in 24 hours", datetime(2026, 3, 7, 12, tzinfo=NY)) == timedelta(hours=24) + assert _elapsed("in 2 hours", datetime(2026, 3, 8, 1, 30, tzinfo=NY)) == timedelta(hours=2) + + def test_elapsed_hours_across_fall_back(self): + now = datetime(2026, 11, 1, 1, 30, tzinfo=NY) + assert parse_time("in 1 hour", now) == "2026-11-01T01:30:00-05:00" + assert _elapsed("in 24 hours", datetime(2026, 10, 31, 12, tzinfo=NY)) == timedelta(hours=24) + + def test_days_keep_wall_clock(self): + now = datetime(2026, 3, 7, 12, tzinfo=NY) + assert parse_time("in 1 day", now) == "2026-03-08T12:00:00-04:00" + assert parse_time("tomorrow at 9am", now) == "2026-03-08T09:00:00-04:00" + + def test_skipped_and_repeated_clock_times(self): + assert parse_time("tomorrow at 2:30am", datetime(2026, 3, 7, 12, tzinfo=NY)) == ( + "2026-03-08T03:30:00-04:00" + ) + assert parse_time("tomorrow at 1:30am", datetime(2026, 10, 31, 12, tzinfo=NY)) == ( + "2026-11-01T01:30:00-04:00" + ) + + def test_configured_zone_when_now_omitted(self, monkeypatch): + class FixedClock(datetime): + @classmethod + def now(cls, tz=None): + return datetime(2026, 3, 8, 6, 30, tzinfo=UTC).astimezone(tz) + + monkeypatch.setattr(time_parser, "_default_tz", NY) + monkeypatch.setattr(time_parser, "datetime", FixedClock) + result = datetime.fromisoformat(parse_time("in 2 hours")) + assert result.astimezone(UTC) == datetime(2026, 3, 8, 8, 30, tzinfo=UTC) diff --git a/tests/test_tool_loop_provenance.py b/tests/test_tool_loop_provenance.py index f62c3049e..d19ad967f 100644 --- a/tests/test_tool_loop_provenance.py +++ b/tests/test_tool_loop_provenance.py @@ -507,3 +507,42 @@ async def block(*_args): assert str(exc) == "ledger down" else: raise AssertionError("ledger failure did not propagate") + + @pytest.mark.parametrize( + ("tool_name", "tool_input"), + [ + ("run_command", {"command": "x"}), + ("wait_for_agents", {"agent_ids": ["a"], "timeout": "invalid"}), + ], + ) + async def test_timed_out_ledger_failure_propagates_after_audit(self, tool_name, tool_input): + from unittest.mock import AsyncMock + + runner = ToolLoopRunner.__new__(ToolLoopRunner) + cancelled = asyncio.Event() + + async def block(*_args): + try: + await asyncio.sleep(3600) + except asyncio.CancelledError: + cancelled.set() + raise + + runner._run_one_tool = block + runner._audit = SimpleNamespace(log_execution=AsyncMock()) + durability = SimpleNamespace( + after_tool_interrupted=AsyncMock(side_effect=RuntimeError("ledger down")) + ) + st = SimpleNamespace( + iteration=0, + _cancel=asyncio.Event(), + durability=durability, + message=SimpleNamespace(author=SimpleNamespace(id=1), channel=SimpleNamespace(id=2)), + ) + block_info = SimpleNamespace(name=tool_name, input=tool_input, id="call") + + with pytest.raises(RuntimeError, match="ledger down"): + await runner._run_one_tool_with_timeout(st, block_info, 0.01) + assert cancelled.is_set() + durability.after_tool_interrupted.assert_awaited_once() + runner._audit.log_execution.assert_awaited_once() diff --git a/tests/test_tool_text_regressions.py b/tests/test_tool_text_regressions.py new file mode 100644 index 000000000..db73c5381 --- /dev/null +++ b/tests/test_tool_text_regressions.py @@ -0,0 +1,56 @@ +"""Regression coverage for tool-result failure labels and truncation boundaries.""" + +import json + +import pytest + +from src.tools.result_capture import result_capture +from src.tools.tool_text import _truncate_lines, failure_reason + + +@pytest.mark.parametrize(("text", "reason"), [ + (None, None), + (" Command failed (exit 2)", "command failed"), + ("Error: tool 'browser' timed out", "timed out"), + ("Tool 'browser' cancelled", "cancelled"), + ("Unknown tool: missing", "unknown tool"), + ("[Interrupted before execution]", "not executed"), + ("[Interrupted: outcome unknown]", "outcome unknown"), + ("[Interrupted observation]", "interrupted"), + ("Error (tool reported failure): nope", "tool reported failure"), + ("ordinary output", None), +]) +def test_failure_reason_plain_text_edges(text, reason): + assert failure_reason(text) == reason + + +@pytest.mark.parametrize(("status", "head", "expected"), [ + ("succeeded", "Command failed", None), + ("failed", "Permission denied: /fixture", "permission denied"), + ("failed", "ordinary detail", "failed"), +]) +def test_failure_reason_tool_output_envelope(status, head, expected): + payload = json.dumps( + {"kind": "tool_output", "status": status, "head": head}, + separators=(",", ":"), + ) + assert failure_reason(payload) == expected + + +def test_failure_reason_rejects_malformed_or_non_string_envelopes(): + assert failure_reason('{"kind":"tool_output","status":"failed"') is None + assert failure_reason('{"kind":"tool_output","status":7}') is None + + +def test_truncate_lines_small_custom_boundary_and_capture_bypass(): + assert _truncate_lines("a\nb", max_lines=2) == "a\nb" + truncated = _truncate_lines("\n".join(f"row-{i}" for i in range(7)), max_lines=4) + assert truncated.splitlines() == [ + "row-0", + "row-1", + "[... 3 lines omitted — pipe through head/tail/grep for specific output ...]", + "row-5", "row-6", + ] + full = "\n".join(f"row-{i}" for i in range(8)) + with result_capture(): + assert _truncate_lines(full, max_lines=2) == full diff --git a/tests/test_vector_archive_failure_boundaries.py b/tests/test_vector_archive_failure_boundaries.py new file mode 100644 index 000000000..1ae4b6254 --- /dev/null +++ b/tests/test_vector_archive_failure_boundaries.py @@ -0,0 +1,109 @@ +"""Isolated archive failures do not prevent indexing remaining archives.""" + +import json + +import pytest + +from src.search.errors import SearchExecutionError +from src.search.fts import FullTextIndex +from src.search.vectorstore import SessionVectorStore + + +@pytest.mark.asyncio +async def test_backfill_segments_skips_bad_archive_and_commits_good_one(tmp_path): + archives = tmp_path / "archive" + archives.mkdir() + (archives / "a.json").write_text("{broken") + (archives / "b.json").write_text(json.dumps({ + "channel_id": "room", "summary_segments": [ + {"summary": "retained segment", "start_ts": 1, "end_ts": 2, "source_count": 3}, + ], + })) + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = SessionVectorStore(str(tmp_path / "sessions.db"), fts) + assert await store.backfill_segments(archives, None) == 1 + assert store._get_segment_state_sync() == {"b"} + assert len(fts.search_sessions("retained")) == 1 + # A second run repairs nothing and does not count the acknowledged archive twice. + assert await store.backfill_segments(archives, None) == 0 + + +@pytest.mark.asyncio +async def test_index_session_rejects_unreadable_archive_without_writing(tmp_path): + store = SessionVectorStore(str(tmp_path / "sessions.db")) + assert not await store.index_session(tmp_path / "missing.json", None) + assert store._get_indexed_ids_sync() == set() + + +def test_fts_repair_rejects_unacknowledged_segment(tmp_path, monkeypatch): + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = SessionVectorStore(str(tmp_path / "sessions.db"), fts) + store._conn.execute( + "INSERT INTO session_archives (doc_id, content, channel_id, last_active) " + "VALUES (?, ?, ?, ?)", ("seg:missing", "repair me", "room", 2), + ) + store._conn.commit() + monkeypatch.setattr(fts, "index_session", lambda *_: False) + with pytest.raises(RuntimeError, match="FTS repair did not acknowledge"): + store._repair_segment_fts_sync() + + +@pytest.mark.asyncio +async def test_search_hybrid_no_backend_and_empty_backend(tmp_path): + store = SessionVectorStore(str(tmp_path / "sessions.db")) + store._has_vec = False + with pytest.raises(SearchExecutionError, match="no session search backend"): + await store.search_hybrid("needle", None) + store._fts = FullTextIndex(str(tmp_path / "fts.db")) + assert await store.search_hybrid("needle", None) == [] + + +@pytest.mark.asyncio +async def test_backfill_legacy_archives_skips_invalid_json_and_indexes_valid(tmp_path): + archives = tmp_path / "archive" + archives.mkdir() + (archives / "a.json").write_text("{truncated") + (archives / "b.json").write_text(json.dumps({ + "channel_id": "room", "last_active": 3, + "messages": [{"role": "user", "content": "searchable archive"}], + })) + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = SessionVectorStore(str(tmp_path / "sessions.db"), fts) + # The first archive is unreadable; backfill must still write and index the second. + assert await store.backfill(archives, None) == 1 + assert store._get_indexed_ids_sync() == {"b"} + assert [r["doc_id"] for r in fts.search_sessions("searchable")] == ["b"] + + +@pytest.mark.asyncio +async def test_segment_only_archive_without_summary_is_acknowledged(tmp_path): + archive = tmp_path / "segment_only.json" + archive.write_text(json.dumps({ + "channel_id": "room", "summary_segments": [ + {"summary": "", "start_ts": 1, "end_ts": 2}, + {"summary": "recoverable segment", "start_ts": 1, "end_ts": 3}, + ], + })) + store = SessionVectorStore(str(tmp_path / "sessions.db"), + FullTextIndex(str(tmp_path / "fts.db"))) + assert await store.index_session(archive, None) + assert store._get_segment_state_sync() == {"segment_only"} + assert len(store._fts.search_sessions("recoverable")) == 1 + + +def test_fts_backfill_skips_bad_archive_and_missing_text(tmp_path): + archives = tmp_path / "archive" + archives.mkdir() + (archives / "a.json").write_text("{truncated") + (archives / "b.json").write_text(json.dumps({"channel_id": "room"})) + (archives / "c.json").write_text(json.dumps({ + "channel_id": "room", "messages": [ + {"role": "user", "content": "restored searchable text"}, + ], + })) + fts = FullTextIndex(str(tmp_path / "fts.db")) + store = SessionVectorStore(str(tmp_path / "sessions.db"), fts) + store._backfill_fts_sync(archives) + assert [row["doc_id"] for row in fts.search_sessions("restored")] == ["c"] + assert not fts.has_session("a") + assert not fts.has_session("b") diff --git a/tests/test_web_api_knowledge_mem.py b/tests/test_web_api_knowledge_mem.py index 9891e7d80..03949a8e8 100644 --- a/tests/test_web_api_knowledge_mem.py +++ b/tests/test_web_api_knowledge_mem.py @@ -335,7 +335,7 @@ async def test_restore_reports_durability_failure(self, kbot, tmp_path): initial_versions = len(kbot.knowledge.get_versions("v.md")) kbot.knowledge._fts = fts - with patch.object(fts, "index_knowledge_chunk", return_value=False) as failed: + with patch.object(fts, "replace_knowledge_source", return_value=False) as failed: response = await c.post("/api/knowledge/v.md/versions/1/restore") failed.assert_called_once() diff --git a/tests/test_write_invariant_integration.py b/tests/test_write_invariant_integration.py index fb25f5c9d..504f2c249 100644 --- a/tests/test_write_invariant_integration.py +++ b/tests/test_write_invariant_integration.py @@ -268,6 +268,58 @@ async def kill_durability(tool_name, tool_input, *, user_id=None): # The second batch never started: fail closed, not fail quiet. assert executed == ["one"] + async def test_outer_timeout_settle_failure_halts_before_next_generation(self, tmp_path): + bot, fake, store = build_with_store( + [tool_call_response(("run_command", {"command": "x"})), text_response("continued")], + tmp_path, + ) + started = asyncio.Event() + + async def blocking(tool_name, tool_input, *, user_id=None): + started.set() + await asyncio.sleep(3600) + + bot.tool_executor.execute = blocking + bot.tool_loop._outer_tool_timeout = lambda name, inp: 0.2 + real_settle = store.settle_interrupted_sync + failures = [] + + def fail_once(*args, **kwargs): + if not failures: + failures.append(1) + raise TurnStateUnavailableError("injected: ledger write failed") + return real_settle(*args, **kwargs) + + store.settle_interrupted_sync = fail_once + with pytest.raises(TurnStateUnavailableError): + await run_loop(bot, FakeMessage("go")) + assert started.is_set() and failures == [1] + assert len(fake.calls) == 1 + (status,) = store._conn.execute("SELECT status FROM turns").fetchone() + assert status == TurnStatus.TERMINAL_FAILED + ((state,),) = store._conn.execute("SELECT state FROM operations").fetchall() + assert state in {OpState.RUNNING, OpState.OUTCOME_UNKNOWN} + + async def test_outer_timeout_settle_success_still_continues(self, tmp_path): + bot, fake, store = build_with_store( + [tool_call_response(("run_command", {"command": "x"})), text_response("continued")], + tmp_path, + ) + + async def blocking(tool_name, tool_input, *, user_id=None): + await asyncio.sleep(3600) + + bot.tool_executor.execute = blocking + bot.tool_loop._outer_tool_timeout = lambda name, inp: 0.2 + text, _, is_error, *_ = await run_loop(bot, FakeMessage("go")) + assert (text, is_error) == ("continued", False) + assert len(fake.calls) == 2 + assert "timed out after 0.2s" in str(fake.calls[1]["messages"]) + ((state,),) = store._conn.execute("SELECT state FROM operations").fetchall() + assert state == OpState.OUTCOME_UNKNOWN + (status,) = store._conn.execute("SELECT status FROM turns").fetchone() + assert status == TurnStatus.TERMINAL_COMPLETED + class TestCancellationBranches: async def test_stop_before_first_iteration_is_terminal_cancelled(self, tmp_path): diff --git a/ui/dist/assets/index-BoZFf8ax.js b/ui/dist/assets/index-Dn5vs1FT.js similarity index 59% rename from ui/dist/assets/index-BoZFf8ax.js rename to ui/dist/assets/index-Dn5vs1FT.js index 270b3a25d..784351d9e 100644 --- a/ui/dist/assets/index-BoZFf8ax.js +++ b/ui/dist/assets/index-Dn5vs1FT.js @@ -1,40 +1,40 @@ -var Bb=Object.defineProperty;var Ub=(e,t,s)=>t in e?Bb(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s;var Tt=(e,t,s)=>Ub(e,typeof t!="symbol"?t+"":t,s);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))a(n);new MutationObserver(n=>{for(const i of n)if(i.type==="childList")for(const l of i.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&a(l)}).observe(document,{childList:!0,subtree:!0});function s(n){const i={};return n.integrity&&(i.integrity=n.integrity),n.referrerPolicy&&(i.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?i.credentials="include":n.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function a(n){if(n.ep)return;n.ep=!0;const i=s(n);fetch(n.href,i)}})();class zb{constructor(){this._persist=localStorage.getItem("odin_persist")==="1",this._token=this._persist?localStorage.getItem("odin_token")||"":sessionStorage.getItem("odin_token")||"";const t=this._persist?localStorage:sessionStorage;this._sessionTimeout=parseInt(t.getItem("odin_session_timeout")||"0",10),this._lastActivity=Date.now(),this._activityTimer=null,this.onSessionExpired=null,this._token&&this._sessionTimeout>0&&this._startActivityMonitor()}get token(){return this._token}get sessionTimeout(){return this._sessionTimeout}setToken(t,s=0){if(this._token=t,this._sessionTimeout=s,this._lastActivity=Date.now(),t){const a=this._persist?localStorage:sessionStorage;a.setItem("odin_token",t),this._persist&&localStorage.setItem("odin_persist","1"),s>0?a.setItem("odin_session_timeout",String(s)):a.removeItem("odin_session_timeout"),this._startActivityMonitor()}else sessionStorage.removeItem("odin_token"),sessionStorage.removeItem("odin_session_timeout"),localStorage.removeItem("odin_token"),localStorage.removeItem("odin_persist"),localStorage.removeItem("odin_session_timeout"),this._stopActivityMonitor()}setPersist(t){this._persist=t}_startActivityMonitor(){this._stopActivityMonitor(),!(this._sessionTimeout<=0)&&(this._activityTimer=setInterval(()=>{(Date.now()-this._lastActivity)/1e3>=this._sessionTimeout&&(this._stopActivityMonitor(),this.onSessionExpired&&this.onSessionExpired())},1e4))}_stopActivityMonitor(){this._activityTimer&&(clearInterval(this._activityTimer),this._activityTimer=null)}_headers(t={}){const s={"Content-Type":"application/json",...t};return this._token&&(s.Authorization=`Bearer ${this._token}`),s}async _request(t,s,a=null,{signal:n}={}){this._lastActivity=Date.now();const i={method:t,headers:this._headers(),signal:n};a!==null&&(i.body=JSON.stringify(a));const l=await fetch(s,i);if(l.status===401)throw new bo("Unauthorized");const o=await l.json().catch(()=>null);if(!l.ok){const r=(o==null?void 0:o.error)||`HTTP ${l.status}`;throw new yc(r,l.status,o)}return o}get(t,s={}){return this._request("GET",t,null,s)}async getBlob(t){this._lastActivity=Date.now();const s=await fetch(t,{method:"GET",headers:this._headers()});if(s.status===401)throw new bo("Unauthorized");if(!s.ok){const a=await s.json().catch(()=>null);throw new yc((a==null?void 0:a.error)||`HTTP ${s.status}`,s.status,a)}return s.blob()}post(t,s){return this._request("POST",t,s)}async setListenerExposure(t,s){const a=await fetch("/api/setup/listener",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({expose_beyond_loopback:s})}),n=await a.json().catch(()=>null);if(!a.ok)throw new yc((n==null?void 0:n.error)||"Listener reauthentication failed",a.status,n);return n}put(t,s){return this._request("PUT",t,s)}del(t){return this._request("DELETE",t)}async login(t){const s=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t})}),a=await s.json().catch(()=>null);if(!s.ok)throw new bo((a==null?void 0:a.error)||"Login failed");return this.setToken(a.session_id,a.timeout_seconds||0),a}async logout(){const t=this.post("/api/auth/logout",{});this.setToken("");try{await t}catch{}}async check(){try{return await this.get("/api/status"),{ok:!0,needsAuth:!1}}catch(t){return t instanceof bo?{ok:!1,needsAuth:!0}:{ok:!1,needsAuth:!1,error:t.message}}}}class bo extends Error{constructor(t){super(t),this.name="AuthError"}}class yc extends Error{constructor(t,s,a){super(t),this.name="ApiError",this.status=s,this.data=a}}class Hb{constructor(t){this._api=t,this._ws=null,this._handlers={logs:[],events:[],chat:[]},this._reconnectDelay=1e3,this._maxReconnectDelay=3e4,this._shouldConnect=!1,this._subscriptions=new Set,this._reconnectAttempt=0,this._reconnectTimer=null,this._lastPongTime=0,this._pingInterval=null,this._forcedRetireTimer=null,this._subscriptionAckTimer=null,this._pendingReconnect=null,this._latency=-1,this._chatPending=!1,this._state="disconnected",this._lifecycle={status:new Set,state:new Set,latency:new Set,reconnected:new Set},this._everConnected=!1,this._reconnectEpoch=0}onStatus(t){return this._addLifecycle("status",t)}onState(t){return this._addLifecycle("state",t)}onLatencyChange(t){return this._addLifecycle("latency",t)}onReconnected(t){return this._addLifecycle("reconnected",t)}_addLifecycle(t,s){return this._lifecycle[t].add(s),()=>{this._lifecycle[t].delete(s)}}_emitLifecycle(t,...s){for(const a of[...this._lifecycle[t]])try{a(...s)}catch{}}get connected(){var t;return((t=this._ws)==null?void 0:t.readyState)===WebSocket.OPEN}get state(){return this._state}get reconnectAttempt(){return this._reconnectAttempt}get latency(){return this._latency}get reconnectEpoch(){return this._reconnectEpoch}_resetLatency(){this._latency=-1,this._emitLifecycle("latency",-1)}connect(){this._shouldConnect=!0,this._setState("connecting"),this._open()}disconnect(){this._shouldConnect=!1,this._everConnected=!1,this._reconnectTimer&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._forcedRetireTimer&&(clearTimeout(this._forcedRetireTimer),this._forcedRetireTimer=null),this._subscriptionAckTimer&&(clearTimeout(this._subscriptionAckTimer),this._subscriptionAckTimer=null),this._pendingReconnect=null,this._reconnectAttempt=0,this._resetLatency(),this._stopPing(),this._ws&&(this._ws.close(),this._ws=null),this._setState("disconnected")}_setState(t){this._state!==t&&(this._state=t,this._emitLifecycle("state",t,{attempt:this._reconnectAttempt,latency:this._latency}))}_startPing(t){this._stopPing(),this._lastPongTime=Date.now(),this._pingInterval=setInterval(()=>{if(!(this._ws!==t||t.readyState!==WebSocket.OPEN)){if(this._lastPongTime&&Date.now()-this._lastPongTime>47e3){this._beginForcedRetirement(t,"pong timeout");return}try{t.send(JSON.stringify({type:"ping",ts:Date.now()}))}catch{}}},15e3)}_beginForcedRetirement(t,s){if(!(this._ws!==t||this._forcedRetireTimer)){this._stopPing(),this._reconnectAttempt++,this._setState("reconnecting"),this._emitLifecycle("status",!1),this._forcedRetireTimer=setTimeout(()=>{this._forcedRetireTimer=null,this._retireSocket(t,!0,!0)},1e3);try{t.close(4e3,s)}catch{}}}_scheduleReconnect(t=!0){!this._shouldConnect||this._reconnectTimer||(t&&this._reconnectAttempt++,this._setState("reconnecting"),this._reconnectTimer=setTimeout(()=>{this._reconnectTimer=null,this._open()},this._reconnectDelay),this._reconnectDelay=Math.min(this._reconnectDelay*2,this._maxReconnectDelay))}_retireSocket(t,s=!1,a=!1){if(this._ws===t){if(this._forcedRetireTimer&&(clearTimeout(this._forcedRetireTimer),this._forcedRetireTimer=null),this._subscriptionAckTimer&&(clearTimeout(this._subscriptionAckTimer),this._subscriptionAckTimer=null),this._pendingReconnect=null,this._ws=null,this._stopPing(),this._resetLatency(),this._chatPending){this._chatPending=!1;const n={type:"chat_error",error:"Connection lost — the response may still complete; check session history."};for(const i of this._handlers.chat||[])i(n)}s||this._emitLifecycle("status",!1),this._shouldConnect?this._scheduleReconnect(!a):this._setState("disconnected")}}_beginReconnectBarrier(t,s){if(!s)return;const a=new Set(this._subscriptions);if(a.size===0){this._reconnectEpoch+=1,this._emitLifecycle("reconnected",this._reconnectEpoch);return}this._pendingReconnect={socket:t,channels:a},this._subscriptionAckTimer=setTimeout(()=>{var n;((n=this._pendingReconnect)==null?void 0:n.socket)===t&&this._beginForcedRetirement(t,"subscription acknowledgement timeout")},5e3)}_ackSubscription(t,s){const a=this._pendingReconnect;!a||a.socket!==t||!a.channels.has(s)||(a.channels.delete(s),!(a.channels.size>0)&&(this._pendingReconnect=null,this._subscriptionAckTimer&&(clearTimeout(this._subscriptionAckTimer),this._subscriptionAckTimer=null),this._reconnectEpoch+=1,this._emitLifecycle("reconnected",this._reconnectEpoch)))}_stopPing(){this._pingInterval&&(clearInterval(this._pingInterval),this._pingInterval=null)}subscribe(t,s){var a;if(this._handlers[t]||(this._handlers[t]=[]),this._handlers[t].push(s),t!=="chat"&&(this._subscriptions.add(t),this.connected)){const n=this._ws;((a=this._pendingReconnect)==null?void 0:a.socket)===n&&this._pendingReconnect.channels.add(t),n.send(JSON.stringify({subscribe:t}))}}unsubscribe(t,s){const a=this._handlers[t];if(a){const n=a.indexOf(s);if(n>=0&&a.splice(n,1),a.length===0&&t!=="chat"&&(this._subscriptions.delete(t),this.connected)){const i=this._ws;i.send(JSON.stringify({unsubscribe:t})),this._ackSubscription(i,t)}}}on(t,s){return this.subscribe(t,s)}off(t,s){return this.unsubscribe(t,s)}sendChat(t,{channelId:s,userId:a,username:n}={}){return this.connected?(this._ws.send(JSON.stringify({type:"chat",content:t,channel_id:s||"web-default",user_id:a||void 0,username:n||void 0})),this._chatPending=!0,!0):!1}_open(){if(this._ws||!this._shouldConnect)return;const s=`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/api/ws`,a=this._api.token?["odin.bearer."+btoa(this._api.token).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")]:void 0,n=a?new WebSocket(s,a):new WebSocket(s);this._ws=n;const i=()=>this._ws===n;n.onopen=()=>{if(!i())return;const l=this._everConnected;this._everConnected=!0,this._reconnectDelay=1e3,this._reconnectAttempt=0;for(const o of this._subscriptions)n.send(JSON.stringify({subscribe:o}));this._startPing(n),this._setState("connected"),this._emitLifecycle("status",!0),this._beginReconnectBarrier(n,l)},n.onmessage=l=>{if(!i())return;let o;try{o=JSON.parse(l.data)}catch{return}const r=o.type;if(r==="pong"){o.ts&&(this._latency=Date.now()-o.ts,this._lastPongTime=Date.now(),this._emitLifecycle("latency",this._latency));return}if(r==="subscribed"){this._ackSubscription(n,o.channel);return}if(r==="log")for(const c of this._handlers.logs||[])c(o);else if(r==="event")for(const c of this._handlers.events||[])c(o);else if(r==="chat_response"||r==="chat_error"){this._chatPending=!1;for(const c of this._handlers.chat||[])c(o)}},n.onclose=()=>{const l=!!this._forcedRetireTimer;this._retireSocket(n,l,l)},n.onerror=()=>{}}}const z=new zb,ut=new Hb(z);/** +var ly=Object.defineProperty;var oy=(e,t,s)=>t in e?ly(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s;var Et=(e,t,s)=>oy(e,typeof t!="symbol"?t+"":t,s);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))a(n);new MutationObserver(n=>{for(const i of n)if(i.type==="childList")for(const l of i.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&a(l)}).observe(document,{childList:!0,subtree:!0});function s(n){const i={};return n.integrity&&(i.integrity=n.integrity),n.referrerPolicy&&(i.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?i.credentials="include":n.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function a(n){if(n.ep)return;n.ep=!0;const i=s(n);fetch(n.href,i)}})();class ry{constructor(){this._persist=localStorage.getItem("odin_persist")==="1",this._token=this._persist?localStorage.getItem("odin_token")||"":sessionStorage.getItem("odin_token")||"";const t=this._persist?localStorage:sessionStorage;this._sessionTimeout=parseInt(t.getItem("odin_session_timeout")||"0",10),this._lastActivity=Date.now(),this._activityTimer=null,this.onSessionExpired=null,this._token&&this._sessionTimeout>0&&this._startActivityMonitor()}get token(){return this._token}get sessionTimeout(){return this._sessionTimeout}setToken(t,s=0){this._token=t,this._sessionTimeout=s,this._lastActivity=Date.now(),t?(this._persist?(localStorage.setItem("odin_token",t),s>0?localStorage.setItem("odin_session_timeout",String(s)):localStorage.removeItem("odin_session_timeout"),localStorage.setItem("odin_persist","1"),sessionStorage.removeItem("odin_token"),sessionStorage.removeItem("odin_session_timeout")):(localStorage.removeItem("odin_persist"),localStorage.removeItem("odin_token"),localStorage.removeItem("odin_session_timeout"),sessionStorage.setItem("odin_token",t),s>0?sessionStorage.setItem("odin_session_timeout",String(s)):sessionStorage.removeItem("odin_session_timeout")),this._startActivityMonitor()):(localStorage.removeItem("odin_persist"),sessionStorage.removeItem("odin_token"),sessionStorage.removeItem("odin_session_timeout"),localStorage.removeItem("odin_token"),localStorage.removeItem("odin_session_timeout"),this._stopActivityMonitor())}setPersist(t){this._persist=t}_startActivityMonitor(){this._stopActivityMonitor(),!(this._sessionTimeout<=0)&&(this._activityTimer=setInterval(()=>{(Date.now()-this._lastActivity)/1e3>=this._sessionTimeout&&(this._stopActivityMonitor(),this.onSessionExpired&&this.onSessionExpired())},1e4))}_stopActivityMonitor(){this._activityTimer&&(clearInterval(this._activityTimer),this._activityTimer=null)}_headers(t={}){const s={"Content-Type":"application/json",...t};return this._token&&(s.Authorization=`Bearer ${this._token}`),s}async _request(t,s,a=null,{signal:n}={}){this._lastActivity=Date.now();const i={method:t,headers:this._headers(),signal:n};a!==null&&(i.body=JSON.stringify(a));const l=await fetch(s,i);if(l.status===401)throw new Eo("Unauthorized");const o=await l.json().catch(()=>null);if(!l.ok){const r=(o==null?void 0:o.error)||`HTTP ${l.status}`;throw new Rc(r,l.status,o)}return o}get(t,s={}){return this._request("GET",t,null,s)}async getBlob(t){this._lastActivity=Date.now();const s=await fetch(t,{method:"GET",headers:this._headers()});if(s.status===401)throw new Eo("Unauthorized");if(!s.ok){const a=await s.json().catch(()=>null);throw new Rc((a==null?void 0:a.error)||`HTTP ${s.status}`,s.status,a)}return s.blob()}post(t,s){return this._request("POST",t,s)}async setListenerExposure(t,s){const a=await fetch("/api/setup/listener",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({expose_beyond_loopback:s})}),n=await a.json().catch(()=>null);if(!a.ok)throw new Rc((n==null?void 0:n.error)||"Listener reauthentication failed",a.status,n);return n}put(t,s){return this._request("PUT",t,s)}del(t){return this._request("DELETE",t)}async login(t){const s=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t})}),a=await s.json().catch(()=>null);if(!s.ok)throw new Eo((a==null?void 0:a.error)||"Login failed");return this.setToken(a.session_id,a.timeout_seconds||0),a}async logout(){const t=this.post("/api/auth/logout",{});this.setToken("");try{await t}catch{}}async check(){try{return await this.get("/api/status"),{ok:!0,needsAuth:!1}}catch(t){return t instanceof Eo?{ok:!1,needsAuth:!0}:{ok:!1,needsAuth:!1,error:t.message}}}}class Eo extends Error{constructor(t){super(t),this.name="AuthError"}}class Rc extends Error{constructor(t,s,a){super(t),this.name="ApiError",this.status=s,this.data=a}}class cy{constructor(t){this._api=t,this._ws=null,this._handlers={logs:[],events:[],chat:[]},this._reconnectDelay=1e3,this._maxReconnectDelay=3e4,this._shouldConnect=!1,this._subscriptions=new Set,this._reconnectAttempt=0,this._reconnectTimer=null,this._lastPongTime=0,this._pingInterval=null,this._forcedRetireTimer=null,this._subscriptionAckTimer=null,this._pendingReconnect=null,this._latency=-1,this._chatPending=!1,this._state="disconnected",this._lifecycle={status:new Set,state:new Set,latency:new Set,reconnected:new Set},this._everConnected=!1,this._reconnectEpoch=0}onStatus(t){return this._addLifecycle("status",t)}onState(t){return this._addLifecycle("state",t)}onLatencyChange(t){return this._addLifecycle("latency",t)}onReconnected(t){return this._addLifecycle("reconnected",t)}_addLifecycle(t,s){return this._lifecycle[t].add(s),()=>{this._lifecycle[t].delete(s)}}_emitLifecycle(t,...s){for(const a of[...this._lifecycle[t]])try{a(...s)}catch{}}get connected(){var t;return((t=this._ws)==null?void 0:t.readyState)===WebSocket.OPEN}get state(){return this._state}get reconnectAttempt(){return this._reconnectAttempt}get latency(){return this._latency}get reconnectEpoch(){return this._reconnectEpoch}_resetLatency(){this._latency=-1,this._emitLifecycle("latency",-1)}connect(){this._shouldConnect=!0,this._setState("connecting"),this._open()}disconnect(){this._shouldConnect=!1,this._everConnected=!1,this._reconnectTimer&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._forcedRetireTimer&&(clearTimeout(this._forcedRetireTimer),this._forcedRetireTimer=null),this._subscriptionAckTimer&&(clearTimeout(this._subscriptionAckTimer),this._subscriptionAckTimer=null),this._pendingReconnect=null,this._reconnectAttempt=0,this._resetLatency(),this._stopPing(),this._ws&&(this._ws.close(),this._ws=null),this._setState("disconnected")}_setState(t){this._state!==t&&(this._state=t,this._emitLifecycle("state",t,{attempt:this._reconnectAttempt,latency:this._latency}))}_startPing(t){this._stopPing(),this._lastPongTime=Date.now(),this._pingInterval=setInterval(()=>{if(!(this._ws!==t||t.readyState!==WebSocket.OPEN)){if(this._lastPongTime&&Date.now()-this._lastPongTime>47e3){this._beginForcedRetirement(t,"pong timeout");return}try{t.send(JSON.stringify({type:"ping",ts:Date.now()}))}catch{}}},15e3)}_beginForcedRetirement(t,s){if(!(this._ws!==t||this._forcedRetireTimer)){this._stopPing(),this._reconnectAttempt++,this._setState("reconnecting"),this._emitLifecycle("status",!1),this._forcedRetireTimer=setTimeout(()=>{this._forcedRetireTimer=null,this._retireSocket(t,!0,!0)},1e3);try{t.close(4e3,s)}catch{}}}_scheduleReconnect(t=!0){!this._shouldConnect||this._reconnectTimer||(t&&this._reconnectAttempt++,this._setState("reconnecting"),this._reconnectTimer=setTimeout(()=>{this._reconnectTimer=null,this._open()},this._reconnectDelay),this._reconnectDelay=Math.min(this._reconnectDelay*2,this._maxReconnectDelay))}_retireSocket(t,s=!1,a=!1){if(this._ws===t){if(this._forcedRetireTimer&&(clearTimeout(this._forcedRetireTimer),this._forcedRetireTimer=null),this._subscriptionAckTimer&&(clearTimeout(this._subscriptionAckTimer),this._subscriptionAckTimer=null),this._pendingReconnect=null,this._ws=null,this._stopPing(),this._resetLatency(),this._chatPending){this._chatPending=!1;const n={type:"chat_error",error:"Connection lost — the response may still complete; check session history."};for(const i of this._handlers.chat||[])i(n)}s||this._emitLifecycle("status",!1),this._shouldConnect?this._scheduleReconnect(!a):this._setState("disconnected")}}_beginReconnectBarrier(t,s){if(!s)return;const a=new Set(this._subscriptions);if(a.size===0){this._reconnectEpoch+=1,this._emitLifecycle("reconnected",this._reconnectEpoch);return}this._pendingReconnect={socket:t,channels:a},this._subscriptionAckTimer=setTimeout(()=>{var n;((n=this._pendingReconnect)==null?void 0:n.socket)===t&&this._beginForcedRetirement(t,"subscription acknowledgement timeout")},5e3)}_ackSubscription(t,s){const a=this._pendingReconnect;!a||a.socket!==t||!a.channels.has(s)||(a.channels.delete(s),!(a.channels.size>0)&&(this._pendingReconnect=null,this._subscriptionAckTimer&&(clearTimeout(this._subscriptionAckTimer),this._subscriptionAckTimer=null),this._reconnectEpoch+=1,this._emitLifecycle("reconnected",this._reconnectEpoch)))}_stopPing(){this._pingInterval&&(clearInterval(this._pingInterval),this._pingInterval=null)}subscribe(t,s){var a;if(this._handlers[t]||(this._handlers[t]=[]),this._handlers[t].push(s),t!=="chat"&&(this._subscriptions.add(t),this.connected)){const n=this._ws;((a=this._pendingReconnect)==null?void 0:a.socket)===n&&this._pendingReconnect.channels.add(t),n.send(JSON.stringify({subscribe:t}))}}unsubscribe(t,s){const a=this._handlers[t];if(a){const n=a.indexOf(s);if(n>=0&&a.splice(n,1),a.length===0&&t!=="chat"&&(this._subscriptions.delete(t),this.connected)){const i=this._ws;i.send(JSON.stringify({unsubscribe:t})),this._ackSubscription(i,t)}}}on(t,s){return this.subscribe(t,s)}off(t,s){return this.unsubscribe(t,s)}sendChat(t,{channelId:s,userId:a,username:n}={}){return this.connected?(this._ws.send(JSON.stringify({type:"chat",content:t,channel_id:s||"web-default",user_id:a||void 0,username:n||void 0})),this._chatPending=!0,!0):!1}_open(){if(this._ws||!this._shouldConnect)return;const s=`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/api/ws`,a=this._api.token?["odin.bearer."+btoa(this._api.token).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")]:void 0,n=a?new WebSocket(s,a):new WebSocket(s);this._ws=n;const i=()=>this._ws===n;n.onopen=()=>{if(!i())return;const l=this._everConnected;this._everConnected=!0,this._reconnectDelay=1e3,this._reconnectAttempt=0;for(const o of this._subscriptions)n.send(JSON.stringify({subscribe:o}));this._startPing(n),this._setState("connected"),this._emitLifecycle("status",!0),this._beginReconnectBarrier(n,l)},n.onmessage=l=>{if(!i())return;let o;try{o=JSON.parse(l.data)}catch{return}const r=o.type;if(r==="pong"){o.ts&&(this._latency=Date.now()-o.ts,this._lastPongTime=Date.now(),this._emitLifecycle("latency",this._latency));return}if(r==="subscribed"){this._ackSubscription(n,o.channel);return}if(r==="log")for(const c of this._handlers.logs||[])c(o);else if(r==="event")for(const c of this._handlers.events||[])c(o);else if(r==="chat_response"||r==="chat_error"){this._chatPending=!1;for(const c of this._handlers.chat||[])c(o)}},n.onclose=()=>{const l=!!this._forcedRetireTimer;this._retireSocket(n,l,l)},n.onerror=()=>{}}}const z=new ry,mt=new cy(z);/** * @vue/shared v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/function Qs(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const it={},Ri=[],hs=()=>{},Ti=()=>!1,ti=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Lr=e=>e.startsWith("onUpdate:"),nt=Object.assign,Fd=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},jb=Object.prototype.hasOwnProperty,ht=(e,t)=>jb.call(e,t),Le=Array.isArray,Ii=e=>Qi(e)==="[object Map]",si=e=>Qi(e)==="[object Set]",ip=e=>Qi(e)==="[object Date]",qb=e=>Qi(e)==="[object RegExp]",je=e=>typeof e=="function",Qe=e=>typeof e=="string",Cs=e=>typeof e=="symbol",mt=e=>e!==null&&typeof e=="object",Bd=e=>(mt(e)||je(e))&&je(e.then)&&je(e.catch),gm=Object.prototype.toString,Qi=e=>gm.call(e),Vb=e=>Qi(e).slice(8,-1),Nr=e=>Qi(e)==="[object Object]",Mr=e=>Qe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Za=Qs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Gb=Qs("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),Dr=e=>{const t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},Wb=/-\w/g,wt=Dr(e=>e.replace(Wb,t=>t.slice(1).toUpperCase())),Kb=/\B([A-Z])/g,Us=Dr(e=>e.replace(Kb,"-$1").toLowerCase()),ai=Dr(e=>e.charAt(0).toUpperCase()+e.slice(1)),Oi=Dr(e=>e?`on${ai(e)}`:""),is=(e,t)=>!Object.is(e,t),Li=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:a,value:s})},Pr=e=>{const t=parseFloat(e);return isNaN(t)?e:t},tr=e=>{const t=Qe(e)?Number(e):NaN;return isNaN(t)?e:t};let lp;const $r=()=>lp||(lp=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Jb(e,t){return e+JSON.stringify(t,(s,a)=>typeof a=="function"?a.toString():a)}const Zb="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol",Qb=Qs(Zb);function so(e){if(Le(e)){const t={};for(let s=0;s{if(s){const a=s.split(Xb);a.length>1&&(t[a[0].trim()]=a[1].trim())}}),t}function ao(e){let t="";if(Qe(e))t=e;else if(Le(e))for(let s=0;sen(s,t))}const _m=e=>!!(e&&e.__v_isRef===!0),wm=e=>Qe(e)?e:e==null?"":Le(e)||mt(e)&&(e.toString===gm||!je(e.toString))?_m(e)?wm(e.value):JSON.stringify(e,km,2):String(e),km=(e,t)=>_m(t)?km(e,t.value):Ii(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[a,n],i)=>(s[xc(a,i)+" =>"]=n,s),{})}:si(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>xc(s))}:Cs(t)?xc(t):mt(t)&&!Le(t)&&!Nr(t)?String(t):t,xc=(e,t="")=>{var s;return Cs(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};function fy(e){return e==null?"initial":typeof e=="string"?e===""?" ":e:String(e)}/** +**/function Xs(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const ct={},Li=[],vs=()=>{},Ri=()=>!1,li=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Br=e=>e.startsWith("onUpdate:"),lt=Object.assign,Gd=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},dy=Object.prototype.hasOwnProperty,bt=(e,t)=>dy.call(e,t),Me=Array.isArray,Ni=e=>el(e)==="[object Map]",oi=e=>el(e)==="[object Set]",vp=e=>el(e)==="[object Date]",uy=e=>el(e)==="[object RegExp]",Ve=e=>typeof e=="function",Ze=e=>typeof e=="string",Cs=e=>typeof e=="symbol",ht=e=>e!==null&&typeof e=="object",Wd=e=>(ht(e)||Ve(e))&&Ve(e.then)&&Ve(e.catch),Em=Object.prototype.toString,el=e=>Em.call(e),py=e=>el(e).slice(8,-1),Ur=e=>el(e)==="[object Object]",zr=e=>Ze(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,en=Xs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),fy=Xs("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),Hr=e=>{const t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},my=/-\w/g,Tt=Hr(e=>e.replace(my,t=>t.slice(1).toUpperCase())),vy=/\B([A-Z])/g,Hs=Hr(e=>e.replace(vy,"-$1").toLowerCase()),ri=Hr(e=>e.charAt(0).toUpperCase()+e.slice(1)),Mi=Hr(e=>e?`on${ri(e)}`:""),os=(e,t)=>!Object.is(e,t),Di=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:a,value:s})},jr=e=>{const t=parseFloat(e);return isNaN(t)?e:t},rr=e=>{const t=Ze(e)?Number(e):NaN;return isNaN(t)?e:t};let hp;const qr=()=>hp||(hp=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function hy(e,t){return e+JSON.stringify(t,(s,a)=>typeof a=="function"?a.toString():a)}const gy="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol",by=Xs(gy);function ro(e){if(Me(e)){const t={};for(let s=0;s{if(s){const a=s.split(xy);a.length>1&&(t[a[0].trim()]=a[1].trim())}}),t}function co(e){let t="";if(Ze(e))t=e;else if(Me(e))for(let s=0;snn(s,t))}const Om=e=>!!(e&&e.__v_isRef===!0),Lm=e=>Ze(e)?e:e==null?"":Me(e)||ht(e)&&(e.toString===Em||!Ve(e.toString))?Om(e)?Lm(e.value):JSON.stringify(e,Nm,2):String(e),Nm=(e,t)=>Om(t)?Nm(e,t.value):Ni(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[a,n],i)=>(s[Ic(a,i)+" =>"]=n,s),{})}:oi(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>Ic(s))}:Cs(t)?Ic(t):ht(t)&&!Me(t)&&!Ur(t)?String(t):t,Ic=(e,t="")=>{var s;return Cs(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};function My(e){return e==null?"initial":typeof e=="string"?e===""?" ":e:String(e)}/** * @vue/reactivity v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/let ts;class Ud{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&ts&&(ts.active?(this.parent=ts,this.index=(ts.scopes||(ts.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,s;if(this.scopes)for(t=0,s=this.scopes.length;t0&&--this._on===0){if(ts===this)ts=this.prevScope;else{let t=ts;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let s,a;for(s=0,a=this.effects.length;s0)return;if(Tl){let t=Tl;for(Tl=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;Cl;){let t=Cl;for(Cl=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(a){e||(e=a)}t=s}}if(e)throw e}function Em(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Am(e){let t,s=e.depsTail,a=s;for(;a;){const n=a.prevDep;a.version===-1?(a===s&&(s=n),jd(a),vy(a)):t=a,a.dep.activeLink=a.prevActiveLink,a.prevActiveLink=void 0,a=n}e.deps=t,e.depsTail=s}function Qc(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Rm(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Rm(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Pl)||(e.globalVersion=Pl,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Qc(e))))return;e.flags|=2;const t=e.dep,s=Rt,a=ma;Rt=e,ma=!0;try{Em(e);const n=e.fn(e._value);(t.version===0||is(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(n){throw t.version++,n}finally{Rt=s,ma=a,Am(e),e.flags&=-3}}function jd(e,t=!1){const{dep:s,prevSub:a,nextSub:n}=e;if(a&&(a.nextSub=n,e.prevSub=void 0),n&&(n.prevSub=a,e.nextSub=void 0),s.subs===e&&(s.subs=a,!a&&s.computed)){s.computed.flags&=-5;for(let i=s.computed.deps;i;i=i.nextDep)jd(i,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function vy(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}function gy(e,t){e.effect instanceof Dl&&(e=e.effect.fn);const s=new Dl(e);t&&nt(s,t);try{s.run()}catch(n){throw s.stop(),n}const a=s.run.bind(s);return a.effect=s,a}function by(e){e.effect.stop()}let ma=!0;const Im=[];function tn(){Im.push(ma),ma=!1}function sn(){const e=Im.pop();ma=e===void 0?!0:e}function op(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=Rt;Rt=void 0;try{t()}finally{Rt=s}}}let Pl=0;class yy{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Br{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Rt||!ma||Rt===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==Rt)s=this.activeLink=new yy(Rt,this),Rt.deps?(s.prevDep=Rt.depsTail,Rt.depsTail.nextDep=s,Rt.depsTail=s):Rt.deps=Rt.depsTail=s,Om(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const a=s.nextDep;a.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=a),s.prevDep=Rt.depsTail,s.nextDep=void 0,Rt.depsTail.nextDep=s,Rt.depsTail=s,Rt.deps===s&&(Rt.deps=a)}return s}trigger(t){this.version++,Pl++,this.notify(t)}notify(t){zd();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{Hd()}}}function Om(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let a=t.deps;a;a=a.nextDep)Om(a)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const sr=new WeakMap,Gn=Symbol(""),Yc=Symbol(""),$l=Symbol("");function ws(e,t,s){if(ma&&Rt){let a=sr.get(e);a||sr.set(e,a=new Map);let n=a.get(s);n||(a.set(s,n=new Br),n.map=a,n.key=s),n.track()}}function Va(e,t,s,a,n,i){const l=sr.get(e);if(!l){Pl++;return}const o=r=>{r&&r.trigger()};if(zd(),t==="clear")l.forEach(o);else{const r=Le(e),c=r&&Mr(s);if(r&&s==="length"){const d=Number(a);l.forEach((u,f)=>{(f==="length"||f===$l||!Cs(f)&&f>=d)&&o(u)})}else switch((s!==void 0||l.has(void 0))&&o(l.get(s)),c&&o(l.get($l)),t){case"add":r?c&&o(l.get("length")):(o(l.get(Gn)),Ii(e)&&o(l.get(Yc)));break;case"delete":r||(o(l.get(Gn)),Ii(e)&&o(l.get(Yc)));break;case"set":Ii(e)&&o(l.get(Gn));break}}Hd()}function xy(e,t){const s=sr.get(e);return s&&s.get(t)}function vi(e){const t=dt(e);return t===e?t:(ws(t,"iterate",$l),Hs(e)?t:t.map(va))}function Ur(e){return ws(e=dt(e),"iterate",$l),e}function Aa(e,t){return Ia(e)?Bi(Qa(e)?va(t):t):va(t)}const _y={__proto__:null,[Symbol.iterator](){return wc(this,Symbol.iterator,e=>Aa(this,e))},concat(...e){return vi(this).concat(...e.map(t=>Le(t)?vi(t):t))},entries(){return wc(this,"entries",e=>(e[1]=Aa(this,e[1]),e))},every(e,t){return $a(this,"every",e,t,void 0,arguments)},filter(e,t){return $a(this,"filter",e,t,s=>s.map(a=>Aa(this,a)),arguments)},find(e,t){return $a(this,"find",e,t,s=>Aa(this,s),arguments)},findIndex(e,t){return $a(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return $a(this,"findLast",e,t,s=>Aa(this,s),arguments)},findLastIndex(e,t){return $a(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return $a(this,"forEach",e,t,void 0,arguments)},includes(...e){return kc(this,"includes",e)},indexOf(...e){return kc(this,"indexOf",e)},join(e){return vi(this).join(e)},lastIndexOf(...e){return kc(this,"lastIndexOf",e)},map(e,t){return $a(this,"map",e,t,void 0,arguments)},pop(){return cl(this,"pop")},push(...e){return cl(this,"push",e)},reduce(e,...t){return rp(this,"reduce",e,t)},reduceRight(e,...t){return rp(this,"reduceRight",e,t)},shift(){return cl(this,"shift")},some(e,t){return $a(this,"some",e,t,void 0,arguments)},splice(...e){return cl(this,"splice",e)},toReversed(){return vi(this).toReversed()},toSorted(e){return vi(this).toSorted(e)},toSpliced(...e){return vi(this).toSpliced(...e)},unshift(...e){return cl(this,"unshift",e)},values(){return wc(this,"values",e=>Aa(this,e))}};function wc(e,t,s){const a=Ur(e),n=a[t]();return a!==e&&!Hs(e)&&(n._next=n.next,n.next=()=>{const i=n._next();return i.done||(i.value=s(i.value)),i}),n}const wy=Array.prototype;function $a(e,t,s,a,n,i){const l=Ur(e),o=l!==e&&!Hs(e),r=l[t];if(r!==wy[t]){const u=r.apply(e,i);return o?va(u):u}let c=s;l!==e&&(o?c=function(u,f){return s.call(this,Aa(e,u),f,e)}:s.length>2&&(c=function(u,f){return s.call(this,u,f,e)}));const d=r.call(l,c,a);return o&&n?n(d):d}function rp(e,t,s,a){const n=Ur(e),i=n!==e&&!Hs(e);let l=s,o=!1;n!==e&&(i?(o=a.length===0,l=function(c,d,u){return o&&(o=!1,c=Aa(e,c)),s.call(this,c,Aa(e,d),u,e)}):s.length>3&&(l=function(c,d,u){return s.call(this,c,d,u,e)}));const r=n[t](l,...a);return o?Aa(e,r):r}function kc(e,t,s){const a=dt(e);ws(a,"iterate",$l);const n=a[t](...s);return(n===-1||n===!1)&&no(s[0])?(s[0]=dt(s[0]),a[t](...s)):n}function cl(e,t,s=[]){tn(),zd();const a=dt(e)[t].apply(e,s);return Hd(),sn(),a}const ky=Qs("__proto__,__v_isRef,__isVue"),Lm=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Cs));function Sy(e){Cs(e)||(e=String(e));const t=dt(this);return ws(t,"has",e),t.hasOwnProperty(e)}class Nm{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,a){if(s==="__v_skip")return t.__v_skip;const n=this._isReadonly,i=this._isShallow;if(s==="__v_isReactive")return!n;if(s==="__v_isReadonly")return n;if(s==="__v_isShallow")return i;if(s==="__v_raw")return a===(n?i?Bm:Fm:i?$m:Pm).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(a)?t:void 0;const l=Le(t);if(!n){let r;if(l&&(r=_y[s]))return r;if(s==="hasOwnProperty")return Sy}const o=Reflect.get(t,s,Wt(t)?t:a);if((Cs(s)?Lm.has(s):ky(s))||(n||ws(t,"get",s),i))return o;if(Wt(o)){const r=l&&Mr(s)?o:o.value;return n&&mt(r)?ar(r):r}return mt(o)?n?ar(o):Tn(o):o}}class Mm extends Nm{constructor(t=!1){super(!1,t)}set(t,s,a,n){let i=t[s];const l=Le(t)&&Mr(s);if(!this._isShallow){const c=Ia(i);if(!Hs(a)&&!Ia(a)&&(i=dt(i),a=dt(a)),!l&&Wt(i)&&!Wt(a))return c||(i.value=a),!0}const o=l?Number(s)e,yo=e=>Reflect.getPrototypeOf(e);function Ry(e,t,s){return function(...a){const n=this.__v_raw,i=dt(n),l=Ii(i),o=e==="entries"||e===Symbol.iterator&&l,r=e==="keys"&&l,c=n[e](...a),d=s?Xc:t?Bi:va;return!t&&ws(i,"iterate",r?Yc:Gn),nt(Object.create(c),{next(){const{value:u,done:f}=c.next();return f?{value:u,done:f}:{value:o?[d(u[0]),d(u[1])]:d(u),done:f}}})}}function xo(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Iy(e,t){const s={get(n){const i=this.__v_raw,l=dt(i),o=dt(n);e||(is(n,o)&&ws(l,"get",n),ws(l,"get",o));const{has:r}=yo(l),c=t?Xc:e?Bi:va;if(r.call(l,n))return c(i.get(n));if(r.call(l,o))return c(i.get(o));i!==l&&i.get(n)},get size(){const n=this.__v_raw;return!e&&ws(dt(n),"iterate",Gn),n.size},has(n){const i=this.__v_raw,l=dt(i),o=dt(n);return e||(is(n,o)&&ws(l,"has",n),ws(l,"has",o)),n===o?i.has(n):i.has(n)||i.has(o)},forEach(n,i){const l=this,o=l.__v_raw,r=dt(o),c=t?Xc:e?Bi:va;return!e&&ws(r,"iterate",Gn),o.forEach((d,u)=>n.call(i,c(d),c(u),l))}};return nt(s,e?{add:xo("add"),set:xo("set"),delete:xo("delete"),clear:xo("clear")}:{add(n){const i=dt(this),l=yo(i),o=dt(n),r=!t&&!Hs(n)&&!Ia(n)?o:n;return l.has.call(i,r)||is(n,r)&&l.has.call(i,n)||is(o,r)&&l.has.call(i,o)||(i.add(r),Va(i,"add",r,r)),this},set(n,i){!t&&!Hs(i)&&!Ia(i)&&(i=dt(i));const l=dt(this),{has:o,get:r}=yo(l);let c=o.call(l,n);c||(n=dt(n),c=o.call(l,n));const d=r.call(l,n);return l.set(n,i),c?is(i,d)&&Va(l,"set",n,i):Va(l,"add",n,i),this},delete(n){const i=dt(this),{has:l,get:o}=yo(i);let r=l.call(i,n);r||(n=dt(n),r=l.call(i,n)),o&&o.call(i,n);const c=i.delete(n);return r&&Va(i,"delete",n,void 0),c},clear(){const n=dt(this),i=n.size!==0,l=n.clear();return i&&Va(n,"clear",void 0,void 0),l}}),["keys","values","entries",Symbol.iterator].forEach(n=>{s[n]=Ry(n,e,t)}),s}function zr(e,t){const s=Iy(e,t);return(a,n,i)=>n==="__v_isReactive"?!e:n==="__v_isReadonly"?e:n==="__v_raw"?a:Reflect.get(ht(s,n)&&n in a?s:a,n,i)}const Oy={get:zr(!1,!1)},Ly={get:zr(!1,!0)},Ny={get:zr(!0,!1)},My={get:zr(!0,!0)},Pm=new WeakMap,$m=new WeakMap,Fm=new WeakMap,Bm=new WeakMap;function Dy(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Tn(e){return Ia(e)?e:Hr(e,!1,Cy,Oy,Pm)}function qd(e){return Hr(e,!1,Ey,Ly,$m)}function ar(e){return Hr(e,!0,Ty,Ny,Fm)}function Py(e){return Hr(e,!0,Ay,My,Bm)}function Hr(e,t,s,a,n){if(!mt(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const i=n.get(e);if(i)return i;const l=Dy(Vb(e));if(l===0)return e;const o=new Proxy(e,l===2?a:s);return n.set(e,o),o}function Qa(e){return Ia(e)?Qa(e.__v_raw):!!(e&&e.__v_isReactive)}function Ia(e){return!!(e&&e.__v_isReadonly)}function Hs(e){return!!(e&&e.__v_isShallow)}function no(e){return e?!!e.__v_raw:!1}function dt(e){const t=e&&e.__v_raw;return t?dt(t):e}function Um(e){return!ht(e,"__v_skip")&&Object.isExtensible(e)&&bm(e,"__v_skip",!0),e}const va=e=>mt(e)?Tn(e):e,Bi=e=>mt(e)?ar(e):e;function Wt(e){return e?e.__v_isRef===!0:!1}function p(e){return zm(e,!1)}function Vd(e){return zm(e,!0)}function zm(e,t){return Wt(e)?e:new $y(e,t)}class $y{constructor(t,s){this.dep=new Br,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=s?t:dt(t),this._value=s?t:va(t),this.__v_isShallow=s}get value(){return this.dep.track(),this._value}set value(t){const s=this._rawValue,a=this.__v_isShallow||Hs(t)||Ia(t);t=a?t:dt(t),is(t,s)&&(this._rawValue=t,this._value=a?t:va(t),this.dep.trigger())}}function Fy(e){e.dep&&e.dep.trigger()}function Ra(e){return Wt(e)?e.value:e}function By(e){return je(e)?e():Ra(e)}const Uy={get:(e,t,s)=>t==="__v_raw"?e:Ra(Reflect.get(e,t,s)),set:(e,t,s,a)=>{const n=e[t];return Wt(n)&&!Wt(s)?(n.value=s,!0):Reflect.set(e,t,s,a)}};function Gd(e){return Qa(e)?e:new Proxy(e,Uy)}class zy{constructor(t){this.__v_isRef=!0,this._value=void 0;const s=this.dep=new Br,{get:a,set:n}=t(s.track.bind(s),s.trigger.bind(s));this._get=a,this._set=n}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Hm(e){return new zy(e)}function Hy(e){const t=Le(e)?new Array(e.length):{};for(const s in e)t[s]=jm(e,s);return t}class jy{constructor(t,s,a){this._object=t,this._defaultValue=a,this.__v_isRef=!0,this._value=void 0,this._key=Cs(s)?s:String(s),this._raw=dt(t);let n=!0,i=t;if(!Le(t)||Cs(this._key)||!Mr(this._key))do n=!no(i)||Hs(i);while(n&&(i=i.__v_raw));this._shallow=n}get value(){let t=this._object[this._key];return this._shallow&&(t=Ra(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&Wt(this._raw[this._key])){const s=this._object[this._key];if(Wt(s)){s.value=t;return}}this._object[this._key]=t}get dep(){return xy(this._raw,this._key)}}class qy{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function Vy(e,t,s){return Wt(e)?e:je(e)?new qy(e):mt(e)&&arguments.length>1?jm(e,t,s):p(e)}function jm(e,t,s){return new jy(e,t,s)}class Gy{constructor(t,s,a){this.fn=t,this.setter=s,this._value=void 0,this.dep=new Br(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Pl-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=a}notify(){if(this.flags|=16,!(this.flags&8)&&Rt!==this)return Tm(this,!0),!0}get value(){const t=this.dep.track();return Rm(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Wy(e,t,s=!1){let a,n;return je(e)?a=e:(a=e.get,n=e.set),new Gy(a,n,s)}const Ky={GET:"get",HAS:"has",ITERATE:"iterate"},Jy={SET:"set",ADD:"add",DELETE:"delete",CLEAR:"clear"},_o={},nr=new WeakMap;let bn;function Zy(){return bn}function qm(e,t=!1,s=bn){if(s){let a=nr.get(s);a||nr.set(s,a=[]),a.push(e)}}function Qy(e,t,s=it){const{immediate:a,deep:n,once:i,scheduler:l,augmentJob:o,call:r}=s,c=x=>n?x:Hs(x)||n===!1||n===0?Ga(x,1):Ga(x);let d,u,f,m,h=!1,g=!1;if(Wt(e)?(u=()=>e.value,h=Hs(e)):Qa(e)?(u=()=>c(e),h=!0):Le(e)?(g=!0,h=e.some(x=>Qa(x)||Hs(x)),u=()=>e.map(x=>{if(Wt(x))return x.value;if(Qa(x))return c(x);if(je(x))return r?r(x,2):x()})):je(e)?t?u=r?()=>r(e,2):e:u=()=>{if(f){tn();try{f()}finally{sn()}}const x=bn;bn=d;try{return r?r(e,3,[m]):e(m)}finally{bn=x}}:u=hs,t&&n){const x=u,S=n===!0?1/0:n;u=()=>Ga(x(),S)}const A=Sm(),w=()=>{d.stop(),A&&A.active&&Fd(A.effects,d)};if(i&&t){const x=t;t=(...S)=>{const _=x(...S);return w(),_}}let y=g?new Array(e.length).fill(_o):_o;const b=x=>{if(!(!(d.flags&1)||!d.dirty&&!x))if(t){const S=d.run();if(x||n||h||(g?S.some((_,T)=>is(_,y[T])):is(S,y))){f&&f();const _=bn;bn=d;try{const T=[S,y===_o?void 0:g&&y[0]===_o?[]:y,m];y=S,r?r(t,3,T):t(...T)}finally{bn=_}}}else d.run()};return o&&o(b),d=new Dl(u),d.scheduler=l?()=>l(b,!1):b,m=x=>qm(x,!1,d),f=d.onStop=()=>{const x=nr.get(d);if(x){if(r)r(x,4);else for(const S of x)S();nr.delete(d)}},t?a?b(!0):y=d.run():l?l(b.bind(null,!0),!0):d.run(),w.pause=d.pause.bind(d),w.resume=d.resume.bind(d),w.stop=w,w}function Ga(e,t=1/0,s){if(t<=0||!mt(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,Wt(e))Ga(e.value,t,s);else if(Le(e))for(let a=0;a{Ga(a,t,s)});else if(Nr(e)){for(const a in e)Ga(e[a],t,s);for(const a of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,a)&&Ga(e[a],t,s)}return e}/** +**/let as;class Kd{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&as&&(as.active?(this.parent=as,this.index=(as.scopes||(as.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,s;if(this.scopes)for(t=0,s=this.scopes.length;t0&&--this._on===0){if(as===this)as=this.prevScope;else{let t=as;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let s,a;for(s=0,a=this.effects.length;s0)return;if(Ll){let t=Ll;for(Ll=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;Ol;){let t=Ol;for(Ol=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(a){e||(e=a)}t=s}}if(e)throw e}function $m(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Fm(e){let t,s=e.depsTail,a=s;for(;a;){const n=a.prevDep;a.version===-1?(a===s&&(s=n),Qd(a),$y(a)):t=a,a.dep.activeLink=a.prevActiveLink,a.prevActiveLink=void 0,a=n}e.deps=t,e.depsTail=s}function id(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Bm(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Bm(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Hl)||(e.globalVersion=Hl,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!id(e))))return;e.flags|=2;const t=e.dep,s=At,a=ma;At=e,ma=!0;try{$m(e);const n=e.fn(e._value);(t.version===0||os(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(n){throw t.version++,n}finally{At=s,ma=a,Fm(e),e.flags&=-3}}function Qd(e,t=!1){const{dep:s,prevSub:a,nextSub:n}=e;if(a&&(a.nextSub=n,e.prevSub=void 0),n&&(n.prevSub=a,e.nextSub=void 0),s.subs===e&&(s.subs=a,!a&&s.computed)){s.computed.flags&=-5;for(let i=s.computed.deps;i;i=i.nextDep)Qd(i,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function $y(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}function Fy(e,t){e.effect instanceof zl&&(e=e.effect.fn);const s=new zl(e);t&<(s,t);try{s.run()}catch(n){throw s.stop(),n}const a=s.run.bind(s);return a.effect=s,a}function By(e){e.effect.stop()}let ma=!0;const Um=[];function ln(){Um.push(ma),ma=!1}function on(){const e=Um.pop();ma=e===void 0?!0:e}function gp(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=At;At=void 0;try{t()}finally{At=s}}}let Hl=0;class Uy{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Gr{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!At||!ma||At===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==At)s=this.activeLink=new Uy(At,this),At.deps?(s.prevDep=At.depsTail,At.depsTail.nextDep=s,At.depsTail=s):At.deps=At.depsTail=s,zm(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const a=s.nextDep;a.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=a),s.prevDep=At.depsTail,s.nextDep=void 0,At.depsTail.nextDep=s,At.depsTail=s,At.deps===s&&(At.deps=a)}return s}trigger(t){this.version++,Hl++,this.notify(t)}notify(t){Jd();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{Zd()}}}function zm(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let a=t.deps;a;a=a.nextDep)zm(a)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const cr=new WeakMap,Qn=Symbol(""),ld=Symbol(""),jl=Symbol("");function ws(e,t,s){if(ma&&At){let a=cr.get(e);a||cr.set(e,a=new Map);let n=a.get(s);n||(a.set(s,n=new Gr),n.map=a,n.key=s),n.track()}}function Ja(e,t,s,a,n,i){const l=cr.get(e);if(!l){Hl++;return}const o=r=>{r&&r.trigger()};if(Jd(),t==="clear")l.forEach(o);else{const r=Me(e),c=r&&zr(s);if(r&&s==="length"){const d=Number(a);l.forEach((u,p)=>{(p==="length"||p===jl||!Cs(p)&&p>=d)&&o(u)})}else switch((s!==void 0||l.has(void 0))&&o(l.get(s)),c&&o(l.get(jl)),t){case"add":r?c&&o(l.get("length")):(o(l.get(Qn)),Ni(e)&&o(l.get(ld)));break;case"delete":r||(o(l.get(Qn)),Ni(e)&&o(l.get(ld)));break;case"set":Ni(e)&&o(l.get(Qn));break}}Zd()}function zy(e,t){const s=cr.get(e);return s&&s.get(t)}function yi(e){const t=ft(e);return t===e?t:(ws(t,"iterate",jl),qs(e)?t:t.map(ha))}function Wr(e){return ws(e=ft(e),"iterate",jl),e}function Oa(e,t){return Na(e)?Hi(tn(e)?ha(t):t):ha(t)}const Hy={__proto__:null,[Symbol.iterator](){return Lc(this,Symbol.iterator,e=>Oa(this,e))},concat(...e){return yi(this).concat(...e.map(t=>Me(t)?yi(t):t))},entries(){return Lc(this,"entries",e=>(e[1]=Oa(this,e[1]),e))},every(e,t){return za(this,"every",e,t,void 0,arguments)},filter(e,t){return za(this,"filter",e,t,s=>s.map(a=>Oa(this,a)),arguments)},find(e,t){return za(this,"find",e,t,s=>Oa(this,s),arguments)},findIndex(e,t){return za(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return za(this,"findLast",e,t,s=>Oa(this,s),arguments)},findLastIndex(e,t){return za(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return za(this,"forEach",e,t,void 0,arguments)},includes(...e){return Nc(this,"includes",e)},indexOf(...e){return Nc(this,"indexOf",e)},join(e){return yi(this).join(e)},lastIndexOf(...e){return Nc(this,"lastIndexOf",e)},map(e,t){return za(this,"map",e,t,void 0,arguments)},pop(){return ul(this,"pop")},push(...e){return ul(this,"push",e)},reduce(e,...t){return bp(this,"reduce",e,t)},reduceRight(e,...t){return bp(this,"reduceRight",e,t)},shift(){return ul(this,"shift")},some(e,t){return za(this,"some",e,t,void 0,arguments)},splice(...e){return ul(this,"splice",e)},toReversed(){return yi(this).toReversed()},toSorted(e){return yi(this).toSorted(e)},toSpliced(...e){return yi(this).toSpliced(...e)},unshift(...e){return ul(this,"unshift",e)},values(){return Lc(this,"values",e=>Oa(this,e))}};function Lc(e,t,s){const a=Wr(e),n=a[t]();return a!==e&&!qs(e)&&(n._next=n.next,n.next=()=>{const i=n._next();return i.done||(i.value=s(i.value)),i}),n}const jy=Array.prototype;function za(e,t,s,a,n,i){const l=Wr(e),o=l!==e&&!qs(e),r=l[t];if(r!==jy[t]){const u=r.apply(e,i);return o?ha(u):u}let c=s;l!==e&&(o?c=function(u,p){return s.call(this,Oa(e,u),p,e)}:s.length>2&&(c=function(u,p){return s.call(this,u,p,e)}));const d=r.call(l,c,a);return o&&n?n(d):d}function bp(e,t,s,a){const n=Wr(e),i=n!==e&&!qs(e);let l=s,o=!1;n!==e&&(i?(o=a.length===0,l=function(c,d,u){return o&&(o=!1,c=Oa(e,c)),s.call(this,c,Oa(e,d),u,e)}):s.length>3&&(l=function(c,d,u){return s.call(this,c,d,u,e)}));const r=n[t](l,...a);return o?Oa(e,r):r}function Nc(e,t,s){const a=ft(e);ws(a,"iterate",jl);const n=a[t](...s);return(n===-1||n===!1)&&uo(s[0])?(s[0]=ft(s[0]),a[t](...s)):n}function ul(e,t,s=[]){ln(),Jd();const a=ft(e)[t].apply(e,s);return Zd(),on(),a}const qy=Xs("__proto__,__v_isRef,__isVue"),Hm=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Cs));function Vy(e){Cs(e)||(e=String(e));const t=ft(this);return ws(t,"has",e),t.hasOwnProperty(e)}class jm{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,a){if(s==="__v_skip")return t.__v_skip;const n=this._isReadonly,i=this._isShallow;if(s==="__v_isReactive")return!n;if(s==="__v_isReadonly")return n;if(s==="__v_isShallow")return i;if(s==="__v_raw")return a===(n?i?Jm:Km:i?Wm:Gm).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(a)?t:void 0;const l=Me(t);if(!n){let r;if(l&&(r=Hy[s]))return r;if(s==="hasOwnProperty")return Vy}const o=Reflect.get(t,s,Xt(t)?t:a);if((Cs(s)?Hm.has(s):qy(s))||(n||ws(t,"get",s),i))return o;if(Xt(o)){const r=l&&zr(s)?o:o.value;return n&&ht(r)?dr(r):r}return ht(o)?n?dr(o):Mn(o):o}}class qm extends jm{constructor(t=!1){super(!1,t)}set(t,s,a,n){let i=t[s];const l=Me(t)&&zr(s);if(!this._isShallow){const c=Na(i);if(!qs(a)&&!Na(a)&&(i=ft(i),a=ft(a)),!l&&Xt(i)&&!Xt(a))return c||(i.value=a),!0}const o=l?Number(s)e,Ao=e=>Reflect.getPrototypeOf(e);function Zy(e,t,s){return function(...a){const n=this.__v_raw,i=ft(n),l=Ni(i),o=e==="entries"||e===Symbol.iterator&&l,r=e==="keys"&&l,c=n[e](...a),d=s?od:t?Hi:ha;return!t&&ws(i,"iterate",r?ld:Qn),lt(Object.create(c),{next(){const{value:u,done:p}=c.next();return p?{value:u,done:p}:{value:o?[d(u[0]),d(u[1])]:d(u),done:p}}})}}function Ro(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Qy(e,t){const s={get(n){const i=this.__v_raw,l=ft(i),o=ft(n);e||(os(n,o)&&ws(l,"get",n),ws(l,"get",o));const{has:r}=Ao(l),c=t?od:e?Hi:ha;if(r.call(l,n))return c(i.get(n));if(r.call(l,o))return c(i.get(o));i!==l&&i.get(n)},get size(){const n=this.__v_raw;return!e&&ws(ft(n),"iterate",Qn),n.size},has(n){const i=this.__v_raw,l=ft(i),o=ft(n);return e||(os(n,o)&&ws(l,"has",n),ws(l,"has",o)),n===o?i.has(n):i.has(n)||i.has(o)},forEach(n,i){const l=this,o=l.__v_raw,r=ft(o),c=t?od:e?Hi:ha;return!e&&ws(r,"iterate",Qn),o.forEach((d,u)=>n.call(i,c(d),c(u),l))}};return lt(s,e?{add:Ro("add"),set:Ro("set"),delete:Ro("delete"),clear:Ro("clear")}:{add(n){const i=ft(this),l=Ao(i),o=ft(n),r=!t&&!qs(n)&&!Na(n)?o:n;return l.has.call(i,r)||os(n,r)&&l.has.call(i,n)||os(o,r)&&l.has.call(i,o)||(i.add(r),Ja(i,"add",r,r)),this},set(n,i){!t&&!qs(i)&&!Na(i)&&(i=ft(i));const l=ft(this),{has:o,get:r}=Ao(l);let c=o.call(l,n);c||(n=ft(n),c=o.call(l,n));const d=r.call(l,n);return l.set(n,i),c?os(i,d)&&Ja(l,"set",n,i):Ja(l,"add",n,i),this},delete(n){const i=ft(this),{has:l,get:o}=Ao(i);let r=l.call(i,n);r||(n=ft(n),r=l.call(i,n)),o&&o.call(i,n);const c=i.delete(n);return r&&Ja(i,"delete",n,void 0),c},clear(){const n=ft(this),i=n.size!==0,l=n.clear();return i&&Ja(n,"clear",void 0,void 0),l}}),["keys","values","entries",Symbol.iterator].forEach(n=>{s[n]=Zy(n,e,t)}),s}function Kr(e,t){const s=Qy(e,t);return(a,n,i)=>n==="__v_isReactive"?!e:n==="__v_isReadonly"?e:n==="__v_raw"?a:Reflect.get(bt(s,n)&&n in a?s:a,n,i)}const Yy={get:Kr(!1,!1)},Xy={get:Kr(!1,!0)},ex={get:Kr(!0,!1)},tx={get:Kr(!0,!0)},Gm=new WeakMap,Wm=new WeakMap,Km=new WeakMap,Jm=new WeakMap;function sx(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Mn(e){return Na(e)?e:Jr(e,!1,Gy,Yy,Gm)}function Yd(e){return Jr(e,!1,Ky,Xy,Wm)}function dr(e){return Jr(e,!0,Wy,ex,Km)}function ax(e){return Jr(e,!0,Jy,tx,Jm)}function Jr(e,t,s,a,n){if(!ht(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const i=n.get(e);if(i)return i;const l=sx(py(e));if(l===0)return e;const o=new Proxy(e,l===2?a:s);return n.set(e,o),o}function tn(e){return Na(e)?tn(e.__v_raw):!!(e&&e.__v_isReactive)}function Na(e){return!!(e&&e.__v_isReadonly)}function qs(e){return!!(e&&e.__v_isShallow)}function uo(e){return e?!!e.__v_raw:!1}function ft(e){const t=e&&e.__v_raw;return t?ft(t):e}function Zm(e){return!bt(e,"__v_skip")&&Object.isExtensible(e)&&Am(e,"__v_skip",!0),e}const ha=e=>ht(e)?Mn(e):e,Hi=e=>ht(e)?dr(e):e;function Xt(e){return e?e.__v_isRef===!0:!1}function f(e){return Qm(e,!1)}function Xd(e){return Qm(e,!0)}function Qm(e,t){return Xt(e)?e:new nx(e,t)}class nx{constructor(t,s){this.dep=new Gr,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=s?t:ft(t),this._value=s?t:ha(t),this.__v_isShallow=s}get value(){return this.dep.track(),this._value}set value(t){const s=this._rawValue,a=this.__v_isShallow||qs(t)||Na(t);t=a?t:ft(t),os(t,s)&&(this._rawValue=t,this._value=a?t:ha(t),this.dep.trigger())}}function ix(e){e.dep&&e.dep.trigger()}function La(e){return Xt(e)?e.value:e}function lx(e){return Ve(e)?e():La(e)}const ox={get:(e,t,s)=>t==="__v_raw"?e:La(Reflect.get(e,t,s)),set:(e,t,s,a)=>{const n=e[t];return Xt(n)&&!Xt(s)?(n.value=s,!0):Reflect.set(e,t,s,a)}};function eu(e){return tn(e)?e:new Proxy(e,ox)}class rx{constructor(t){this.__v_isRef=!0,this._value=void 0;const s=this.dep=new Gr,{get:a,set:n}=t(s.track.bind(s),s.trigger.bind(s));this._get=a,this._set=n}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Ym(e){return new rx(e)}function cx(e){const t=Me(e)?new Array(e.length):{};for(const s in e)t[s]=Xm(e,s);return t}class dx{constructor(t,s,a){this._object=t,this._defaultValue=a,this.__v_isRef=!0,this._value=void 0,this._key=Cs(s)?s:String(s),this._raw=ft(t);let n=!0,i=t;if(!Me(t)||Cs(this._key)||!zr(this._key))do n=!uo(i)||qs(i);while(n&&(i=i.__v_raw));this._shallow=n}get value(){let t=this._object[this._key];return this._shallow&&(t=La(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&Xt(this._raw[this._key])){const s=this._object[this._key];if(Xt(s)){s.value=t;return}}this._object[this._key]=t}get dep(){return zy(this._raw,this._key)}}class ux{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function px(e,t,s){return Xt(e)?e:Ve(e)?new ux(e):ht(e)&&arguments.length>1?Xm(e,t,s):f(e)}function Xm(e,t,s){return new dx(e,t,s)}class fx{constructor(t,s,a){this.fn=t,this.setter=s,this._value=void 0,this.dep=new Gr(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Hl-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=a}notify(){if(this.flags|=16,!(this.flags&8)&&At!==this)return Pm(this,!0),!0}get value(){const t=this.dep.track();return Bm(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function mx(e,t,s=!1){let a,n;return Ve(e)?a=e:(a=e.get,n=e.set),new fx(a,n,s)}const vx={GET:"get",HAS:"has",ITERATE:"iterate"},hx={SET:"set",ADD:"add",DELETE:"delete",CLEAR:"clear"},Io={},ur=new WeakMap;let Tn;function gx(){return Tn}function ev(e,t=!1,s=Tn){if(s){let a=ur.get(s);a||ur.set(s,a=[]),a.push(e)}}function bx(e,t,s=ct){const{immediate:a,deep:n,once:i,scheduler:l,augmentJob:o,call:r}=s,c=x=>n?x:qs(x)||n===!1||n===0?Za(x,1):Za(x);let d,u,p,m,h=!1,y=!1;if(Xt(e)?(u=()=>e.value,h=qs(e)):tn(e)?(u=()=>c(e),h=!0):Me(e)?(y=!0,h=e.some(x=>tn(x)||qs(x)),u=()=>e.map(x=>{if(Xt(x))return x.value;if(tn(x))return c(x);if(Ve(x))return r?r(x,2):x()})):Ve(e)?t?u=r?()=>r(e,2):e:u=()=>{if(p){ln();try{p()}finally{on()}}const x=Tn;Tn=d;try{return r?r(e,3,[m]):e(m)}finally{Tn=x}}:u=vs,t&&n){const x=u,w=n===!0?1/0:n;u=()=>Za(x(),w)}const k=Mm(),A=()=>{d.stop(),k&&k.active&&Gd(k.effects,d)};if(i&&t){const x=t;t=(...w)=>{const _=x(...w);return A(),_}}let g=y?new Array(e.length).fill(Io):Io;const b=x=>{if(!(!(d.flags&1)||!d.dirty&&!x))if(t){const w=d.run();if(x||n||h||(y?w.some((_,T)=>os(_,g[T])):os(w,g))){p&&p();const _=Tn;Tn=d;try{const T=[w,g===Io?void 0:y&&g[0]===Io?[]:g,m];g=w,r?r(t,3,T):t(...T)}finally{Tn=_}}}else d.run()};return o&&o(b),d=new zl(u),d.scheduler=l?()=>l(b,!1):b,m=x=>ev(x,!1,d),p=d.onStop=()=>{const x=ur.get(d);if(x){if(r)r(x,4);else for(const w of x)w();ur.delete(d)}},t?a?b(!0):g=d.run():l?l(b.bind(null,!0),!0):d.run(),A.pause=d.pause.bind(d),A.resume=d.resume.bind(d),A.stop=A,A}function Za(e,t=1/0,s){if(t<=0||!ht(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,Xt(e))Za(e.value,t,s);else if(Me(e))for(let a=0;a{Za(a,t,s)});else if(Ur(e)){for(const a in e)Za(e[a],t,s);for(const a of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,a)&&Za(e[a],t,s)}return e}/** * @vue/runtime-core v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const Vm=[];function Yy(e){Vm.push(e)}function Xy(){Vm.pop()}function ex(e,t){}const tx={SETUP_FUNCTION:0,0:"SETUP_FUNCTION",RENDER_FUNCTION:1,1:"RENDER_FUNCTION",NATIVE_EVENT_HANDLER:5,5:"NATIVE_EVENT_HANDLER",COMPONENT_EVENT_HANDLER:6,6:"COMPONENT_EVENT_HANDLER",VNODE_HOOK:7,7:"VNODE_HOOK",DIRECTIVE_HOOK:8,8:"DIRECTIVE_HOOK",TRANSITION_HOOK:9,9:"TRANSITION_HOOK",APP_ERROR_HANDLER:10,10:"APP_ERROR_HANDLER",APP_WARN_HANDLER:11,11:"APP_WARN_HANDLER",FUNCTION_REF:12,12:"FUNCTION_REF",ASYNC_COMPONENT_LOADER:13,13:"ASYNC_COMPONENT_LOADER",SCHEDULER:14,14:"SCHEDULER",COMPONENT_UPDATE:15,15:"COMPONENT_UPDATE",APP_UNMOUNT_CLEANUP:16,16:"APP_UNMOUNT_CLEANUP"},sx={sp:"serverPrefetch hook",bc:"beforeCreate hook",c:"created hook",bm:"beforeMount hook",m:"mounted hook",bu:"beforeUpdate hook",u:"updated",bum:"beforeUnmount hook",um:"unmounted hook",a:"activated hook",da:"deactivated hook",ec:"errorCaptured hook",rtc:"renderTracked hook",rtg:"renderTriggered hook",0:"setup function",1:"render function",2:"watcher getter",3:"watcher callback",4:"watcher cleanup function",5:"native event handler",6:"component event handler",7:"vnode hook",8:"directive hook",9:"transition hook",10:"app errorHandler",11:"app warnHandler",12:"ref function",13:"async component loader",14:"scheduler flush",15:"component update",16:"app unmount cleanup function"};function Yi(e,t,s,a){try{return a?e(...a):e()}catch(n){ni(n,t,s)}}function Zs(e,t,s,a){if(je(e)){const n=Yi(e,t,s,a);return n&&Bd(n)&&n.catch(i=>{ni(i,t,s)}),n}if(Le(e)){const n=[];for(let i=0;i>>1,n=Is[a],i=Bl(n);i=Bl(s)?Is.push(e):Is.splice(nx(t),0,e),e.flags|=1,Wm()}}function Wm(){ir||(ir=Gm.then(Km))}function Fl(e){Le(e)?Ni.push(...e):yn&&e.id===-1?yn.splice(wi+1,0,e):e.flags&1||(Ni.push(e),e.flags|=1),Wm()}function cp(e,t,s=Ta+1){for(;sBl(s)-Bl(a));if(Ni.length=0,yn){yn.push(...t);return}for(yn=t,wi=0;wie.id==null?e.flags&2?-1:1/0:e.id;function Km(e){try{for(Ta=0;Taki.emit(n,...i)),wo=[]):typeof window<"u"&&window.HTMLElement&&!((a=(s=window.navigator)==null?void 0:s.userAgent)!=null&&a.includes("jsdom"))?((t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push(i=>{Jm(i,t)}),setTimeout(()=>{ki||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,wo=[])},3e3)):wo=[]}let ms=null,jr=null;function Ul(e){const t=ms;return ms=e,jr=e&&e.type.__scopeId||null,t}function ix(e){jr=e}function lx(){jr=null}const ox=e=>Kd;function Kd(e,t=ms,s){if(!t||e._n)return e;const a=(...n)=>{a._d&&ql(-1);const i=Ul(t);let l;try{l=e(...n)}finally{Ul(i),a._d&&ql(1)}return l};return a._n=!0,a._c=!0,a._d=!0,a}function rx(e,t){if(ms===null)return e;const s=ro(ms),a=e.dirs||(e.dirs=[]);for(let n=0;n1)return s&&je(t)?t.call(a&&a.proxy):t}}function cx(){return!!(Ls()||Wn)}const Zm=Symbol.for("v-scx"),Qm=()=>na(Zm);function dx(e,t){return io(e,null,t)}function ux(e,t){return io(e,null,{flush:"post"})}function Ym(e,t){return io(e,null,{flush:"sync"})}function jt(e,t,s){return io(e,t,s)}function io(e,t,s=it){const{immediate:a,deep:n,flush:i,once:l}=s,o=nt({},s),r=t&&a||!t&&i!=="post";let c;if(Yn){if(i==="sync"){const m=Qm();c=m.__watcherHandles||(m.__watcherHandles=[])}else if(!r){const m=()=>{};return m.stop=hs,m.resume=hs,m.pause=hs,m}}const d=fs;o.call=(m,h,g)=>Zs(m,d,h,g);let u=!1;i==="post"?o.scheduler=m=>{Vt(m,d&&d.suspense)}:i!=="sync"&&(u=!0,o.scheduler=(m,h)=>{h?m():Wd(m)}),o.augmentJob=m=>{t&&(m.flags|=4),u&&(m.flags|=2,d&&(m.id=d.uid,m.i=d))};const f=Qy(e,t,o);return Yn&&(c?c.push(f):r&&f()),f}function px(e,t,s){const a=this.proxy,n=Qe(e)?e.includes(".")?Xm(a,e):()=>a[e]:e.bind(a,a);let i;je(t)?i=t:(i=t.handler,s=t);const l=Xi(this),o=io(n,i.bind(a),s);return l(),o}function Xm(e,t){const s=t.split(".");return()=>{let a=e;for(let n=0;ne.__isTeleport,Hn=e=>e&&(e.disabled||e.disabled===""),fx=e=>e&&(e.defer||e.defer===""),dp=e=>typeof SVGElement<"u"&&e instanceof SVGElement,up=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,ed=(e,t)=>{const s=e&&e.to;return Qe(s)?t?t(s):null:s},mx={name:"Teleport",__isTeleport:!0,process(e,t,s,a,n,i,l,o,r,c){const{mc:d,pc:u,pbc:f,o:{insert:m,querySelector:h,createText:g,createComment:A,parentNode:w}}=c,y=Hn(t.props);let{dynamicChildren:b}=t;const x=(T,L,E)=>{T.shapeFlag&16&&d(T.children,L,E,n,i,l,o,r)},S=(T=t)=>{const L=Hn(T.props),E=T.target=ed(T.props,h),P=td(E,T,g,m);E&&(l!=="svg"&&dp(E)?l="svg":l!=="mathml"&&up(E)&&(l="mathml"),n&&n.isCE&&(n.ce._teleportTargets||(n.ce._teleportTargets=new Set)).add(E),L||(x(T,E,P),xl(T,!1)))},_=T=>{const L=()=>{if(vn.get(T)===L){if(vn.delete(T),Hn(T.props)){const E=w(T.el)||s;x(T,E,T.anchor),xl(T,!0)}S(T)}};vn.set(T,L),Vt(L,i)};if(e==null){const T=t.el=g(""),L=t.anchor=g("");if(m(T,s,a),m(L,s,a),fx(t.props)||i&&i.pendingBranch){_(t);return}y&&(x(t,s,L),xl(t,!0)),S()}else{t.el=e.el;const T=t.anchor=e.anchor,L=vn.get(e);if(L){L.flags|=8,vn.delete(e),_(t);return}t.targetStart=e.targetStart;const E=t.target=e.target,P=t.targetAnchor=e.targetAnchor,R=Hn(e.props),I=R?s:E,H=R?T:P;if(l==="svg"||dp(E)?l="svg":(l==="mathml"||up(E))&&(l="mathml"),b?(f(e.dynamicChildren,b,I,n,i,l,o),iu(e,t,!0)):r||u(e,t,I,H,n,i,l,o,!1),y)R?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):ko(t,s,T,c,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const X=t.target=ed(t.props,h);X&&ko(t,X,null,c,0)}else R&&ko(t,E,P,c,1);xl(t,y)}},remove(e,t,s,{um:a,o:{remove:n}},i){const{shapeFlag:l,children:o,anchor:r,targetStart:c,targetAnchor:d,target:u,props:f}=e,m=i||!Hn(f),h=vn.get(e);if(h&&(h.flags|=8,vn.delete(e)),u&&(n(c),n(d)),i&&n(r),!h&&l&16)for(let g=0;g{e.isMounted=!0}),Wr(()=>{e.isUnmounting=!0}),e}const Xs=[Function,Array],Zd={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Xs,onEnter:Xs,onAfterEnter:Xs,onEnterCancelled:Xs,onBeforeLeave:Xs,onLeave:Xs,onAfterLeave:Xs,onLeaveCancelled:Xs,onBeforeAppear:Xs,onAppear:Xs,onAfterAppear:Xs,onAppearCancelled:Xs},sh=e=>{const t=e.subTree;return t.component?sh(t.component):t},gx={name:"BaseTransition",props:Zd,setup(e,{slots:t}){const s=Ls(),a=Jd();return()=>{const n=t.default&&qr(t.default(),!0),i=n&&n.length?ah(n):s.subTree?Bh():void 0;if(!i)return;const l=dt(e),{mode:o}=l;if(a.isLeaving)return Sc(i);const r=pp(i);if(!r)return Sc(i);let c=Ui(r,l,a,s,u=>c=u);r.type!==Ht&&an(r,c);let d=s.subTree&&pp(s.subTree);if(d&&d.type!==Ht&&!fa(d,r)&&sh(s).type!==Ht){let u=Ui(d,l,a,s);if(an(d,u),o==="out-in"&&r.type!==Ht)return a.isLeaving=!0,u.afterLeave=()=>{a.isLeaving=!1,s.job.flags&8||s.update(),delete u.afterLeave,d=void 0},Sc(i);o==="in-out"&&r.type!==Ht?u.delayLeave=(f,m,h)=>{const g=ih(a,d);g[String(d.key)]=d,f[ea]=()=>{m(),f[ea]=void 0,delete c.delayedLeave,d=void 0},c.delayedLeave=()=>{h(),delete c.delayedLeave,d=void 0}}:d=void 0}else d&&(d=void 0);return i}}};function ah(e){let t=e[0];if(e.length>1){for(const s of e)if(s.type!==Ht){t=s;break}}return t}const nh=gx;function ih(e,t){const{leavingVNodes:s}=e;let a=s.get(t.type);return a||(a=Object.create(null),s.set(t.type,a)),a}function Ui(e,t,s,a,n){const{appear:i,mode:l,persisted:o=!1,onBeforeEnter:r,onEnter:c,onAfterEnter:d,onEnterCancelled:u,onBeforeLeave:f,onLeave:m,onAfterLeave:h,onLeaveCancelled:g,onBeforeAppear:A,onAppear:w,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=ih(s,e),_=(E,P)=>{E&&Zs(E,a,9,P)},T=(E,P)=>{const R=P[1];_(E,P),Le(E)?E.every(I=>I.length<=1)&&R():E.length<=1&&R()},L={mode:l,persisted:o,beforeEnter(E){let P=r;if(!s.isMounted)if(i)P=A||r;else return;E[ea]&&E[ea](!0);const R=S[x];R&&fa(e,R)&&R.el[ea]&&R.el[ea](),_(P,[E])},enter(E){if(S[x]===e)return;let P=c,R=d,I=u;if(!s.isMounted)if(i)P=w||c,R=y||d,I=b||u;else return;let H=!1;E[dl]=Q=>{H||(H=!0,Q?_(I,[E]):_(R,[E]),L.delayedLeave&&L.delayedLeave(),E[dl]=void 0)};const X=E[dl].bind(null,!1);P?T(P,[E,X]):X()},leave(E,P){const R=String(e.key);if(E[dl]&&E[dl](!0),s.isUnmounting)return P();_(f,[E]);let I=!1;E[ea]=X=>{I||(I=!0,P(),X?_(g,[E]):_(h,[E]),E[ea]=void 0,S[R]===e&&delete S[R])};const H=E[ea].bind(null,!1);S[R]=e,m?T(m,[E,H]):H()},clone(E){const P=Ui(E,t,s,a,n);return n&&n(P),P}};return L}function Sc(e){if(oo(e))return e=Oa(e),e.children=null,e}function pp(e){if(!oo(e))return th(e.type)&&e.children?ah(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:s}=e;if(s){if(t&16)return s[0];if(t&32&&je(s.default))return s.default()}}function an(e,t){e.shapeFlag&6&&e.component?(e.transition=t,an(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function qr(e,t=!1,s){let a=[],n=0;for(let i=0;i1)for(let i=0;is.value,set:i=>s.value=i})}return s}function fp(e,t){let s;return!!((s=Object.getOwnPropertyDescriptor(e,t))&&!s.configurable)}const or=new WeakMap;function Mi(e,t,s,a,n=!1){if(Le(e)){e.forEach((g,A)=>Mi(g,t&&(Le(t)?t[A]:t),s,a,n));return}if(Ya(a)&&!n){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&Mi(e,t,s,a.component.subTree);return}const i=a.shapeFlag&4?ro(a.component):a.el,l=n?null:i,{i:o,r}=e,c=t&&t.r,d=o.refs===it?o.refs={}:o.refs,u=o.setupState,f=dt(u),m=u===it?Ti:g=>fp(d,g)?!1:ht(f,g),h=(g,A)=>!(A&&fp(d,A));if(c!=null&&c!==r){if(mp(t),Qe(c))d[c]=null,m(c)&&(u[c]=null);else if(Wt(c)){const g=t;h(c,g.k)&&(c.value=null),g.k&&(d[g.k]=null)}}if(je(r))Yi(r,o,12,[l,d]);else{const g=Qe(r),A=Wt(r);if(g||A){const w=()=>{if(e.f){const y=g?m(r)?u[r]:d[r]:h()||!e.k?r.value:d[e.k];if(n)Le(y)&&Fd(y,i);else if(Le(y))y.includes(i)||y.push(i);else if(g)d[r]=[i],m(r)&&(u[r]=d[r]);else{const b=[i];h(r,e.k)&&(r.value=b),e.k&&(d[e.k]=b)}}else g?(d[r]=l,m(r)&&(u[r]=l)):A&&(h(r,e.k)&&(r.value=l),e.k&&(d[e.k]=l))};if(l){const y=()=>{w(),or.delete(e)};y.id=-1,or.set(e,y),Vt(y,s)}else mp(e),w()}}}function mp(e){const t=or.get(e);t&&(t.flags|=8,or.delete(e))}let hp=!1;const gi=()=>{hp||(console.error("Hydration completed but contains mismatches."),hp=!0)},xx=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",_x=e=>e.namespaceURI.includes("MathML"),So=e=>{if(e.nodeType===1){if(xx(e))return"svg";if(_x(e))return"mathml"}},Ei=e=>e.nodeType===8;function wx(e){const{mt:t,p:s,o:{patchProp:a,createText:n,nextSibling:i,parentNode:l,remove:o,insert:r,createComment:c}}=e,d=(b,x)=>{if(!x.hasChildNodes()){s(null,b,x),lr(),x._vnode=b;return}u(x.firstChild,b,null,null,null),lr(),x._vnode=b},u=(b,x,S,_,T,L=!1)=>{L=L||!!x.dynamicChildren;const E=Ei(b)&&b.data==="[",P=()=>g(b,x,S,_,T,E),{type:R,ref:I,shapeFlag:H,patchFlag:X}=x;let Q=b.nodeType;x.el=b,X===-2&&(L=!1,x.dynamicChildren=null);let N=null;switch(R){case kn:Q!==3?x.children===""?(r(x.el=n(""),l(b),b),N=b):N=P():(b.data!==x.children&&(gi(),b.data=x.children),N=i(b));break;case Ht:y(b)?(N=i(b),w(x.el=b.content.firstChild,b,S)):Q!==8||E?N=P():N=i(b);break;case Kn:if(E&&(b=i(b),Q=b.nodeType),Q===1||Q===3){N=b;const O=!x.children.length;for(let $=0;${L=L||!!x.dynamicChildren;const{type:E,props:P,patchFlag:R,shapeFlag:I,dirs:H,transition:X}=x,Q=E==="input"||E==="option";if(Q||R!==-1){H&&Ea(x,null,S,"created");let N=!1;if(y(b)){N=Ih(null,X)&&S&&S.vnode.props&&S.vnode.props.appear;const $=b.content.firstChild;if(N){const ce=$.getAttribute("class");ce&&($.$cls=ce),X.beforeEnter($)}w($,b,S),x.el=b=$}if(I&16&&!(P&&(P.innerHTML||P.textContent))){let $=m(b.firstChild,x,b,S,_,T,L);for($&&!Co(b,1)&&gi();$;){const ce=$;$=$.nextSibling,o(ce)}}else if(I&8){let $=x.children;$[0]===` -`&&(b.tagName==="PRE"||b.tagName==="TEXTAREA")&&($=$.slice(1));const{textContent:ce}=b;ce!==$&&ce!==$.replace(/\r\n|\r/g,` -`)&&(Co(b,0)||gi(),b.textContent=x.children)}if(P){if(Q||!L||R&48){const $=b.tagName.includes("-");for(const ce in P)(Q&&(ce.endsWith("value")||ce==="indeterminate")||ti(ce)&&!Za(ce)||ce[0]==="."||$&&!Za(ce))&&a(b,ce,null,P[ce],void 0,S)}else if(P.onClick)a(b,"onClick",null,P.onClick,void 0,S);else if(R&4&&Qa(P.style))for(const $ in P.style)P.style[$]}let O;(O=P&&P.onVnodeBeforeMount)&&$s(O,S,x),H&&Ea(x,null,S,"beforeMount"),((O=P&&P.onVnodeMounted)||H||N)&&Mh(()=>{O&&$s(O,S,x),N&&X.enter(b),H&&Ea(x,null,S,"mounted")},_)}return b.nextSibling},m=(b,x,S,_,T,L,E)=>{E=E||!!x.dynamicChildren;const P=x.children,R=P.length;let I=!1;for(let H=0;H{const{slotScopeIds:E}=x;E&&(T=T?T.concat(E):E);const P=l(b),R=m(i(b),x,P,S,_,T,L);return R&&Ei(R)&&R.data==="]"?i(x.anchor=R):(gi(),r(x.anchor=c("]"),P,R),R)},g=(b,x,S,_,T,L)=>{if(Co(b.parentElement,1)||gi(),x.el=null,L){const R=A(b);for(;;){const I=i(b);if(I&&I!==R)o(I);else break}}const E=i(b),P=l(b);return o(b),s(null,x,P,E,S,_,So(P),T),S&&(S.vnode.el=x.el,Jr(S,x.el)),E},A=(b,x="[",S="]")=>{let _=0;for(;b;)if(b=i(b),b&&Ei(b)&&(b.data===x&&_++,b.data===S)){if(_===0)return i(b);_--}return b},w=(b,x,S)=>{const _=x.parentNode;_&&_.replaceChild(b,x);let T=S;for(;T;)T.vnode.el===x&&(T.vnode.el=T.subTree.el=b),T=T.parent},y=b=>b.nodeType===1&&b.tagName==="TEMPLATE";return[d,u]}const vp="data-allow-mismatch",kx={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function Co(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(vp);)e=e.parentElement;const s=e&&e.getAttribute(vp);if(s==null)return!1;if(s==="")return!0;{const a=s.split(",");return t===0&&a.includes("children")?!0:a.includes(kx[t])}}const Sx=$r().requestIdleCallback||(e=>setTimeout(e,1)),Cx=$r().cancelIdleCallback||(e=>clearTimeout(e)),Tx=(e=1e4)=>t=>{const s=Sx(t,{timeout:e});return()=>Cx(s)};function Ex(e){const{top:t,left:s,bottom:a,right:n}=e.getBoundingClientRect(),{innerHeight:i,innerWidth:l}=window;return(t>0&&t0&&a0&&s0&&n(t,s)=>{const a=new IntersectionObserver(n=>{for(const i of n)if(i.isIntersecting){a.disconnect(),t();break}},e);return s(n=>{if(n instanceof Element){if(Ex(n))return t(),a.disconnect(),!1;a.observe(n)}}),()=>a.disconnect()},Rx=e=>t=>{if(e){const s=matchMedia(e);if(s.matches)t();else return s.addEventListener("change",t,{once:!0}),()=>s.removeEventListener("change",t)}},Ix=(e=[])=>(t,s)=>{Qe(e)&&(e=[e]);let a=!1;const n=l=>{a||(a=!0,i(),t(),l.target.dispatchEvent(new l.constructor(l.type,l)))},i=()=>{s(l=>{for(const o of e)l.removeEventListener(o,n)})};return s(l=>{for(const o of e)l.addEventListener(o,n,{once:!0})}),i};function Ox(e,t){if(Ei(e)&&e.data==="["){let s=1,a=e.nextSibling;for(;a;){if(a.nodeType===1){if(t(a)===!1)break}else if(Ei(a))if(a.data==="]"){if(--s===0)break}else a.data==="["&&s++;a=a.nextSibling}}else t(e)}const Ya=e=>!!e.type.__asyncLoader;function Lx(e){je(e)&&(e={loader:e});const{loader:t,loadingComponent:s,errorComponent:a,delay:n=200,hydrate:i,timeout:l,suspensible:o=!0,onError:r}=e;let c=null,d,u=0;const f=()=>(u++,c=null,m()),m=()=>{let h;return c||(h=c=t().catch(g=>{if(g=g instanceof Error?g:new Error(String(g)),r)return new Promise((A,w)=>{r(g,()=>A(f()),()=>w(g),u+1)});throw g}).then(g=>h!==c&&c?c:(g&&(g.__esModule||g[Symbol.toStringTag]==="Module")&&(g=g.default),d=g,g)))};return lo({name:"AsyncComponentWrapper",__asyncLoader:m,__asyncHydrate(h,g,A){let w=!1;(g.bu||(g.bu=[])).push(()=>w=!0);const y=()=>{w||A()},b=i?()=>{const x=i(y,S=>Ox(h,S));x&&(g.bum||(g.bum=[])).push(x)}:y;d?b():m().then(()=>!g.isUnmounted&&b())},get __asyncResolved(){return d},setup(){const h=fs;if(Qd(h),d)return()=>To(d,h);const g=S=>{c=null,ni(S,h,13,!a)};if(o&&h.suspense||Yn)return m().then(S=>()=>To(S,h)).catch(S=>(g(S),()=>a?Lt(a,{error:S}):null));const A=p(!1),w=p(),y=p(!!n);let b,x;return xt(()=>{b!=null&&clearTimeout(b),x!=null&&clearTimeout(x)}),n&&(x=setTimeout(()=>{h.isUnmounted||(y.value=!1)},n)),l!=null&&(b=setTimeout(()=>{if(!h.isUnmounted&&!A.value&&!w.value){const S=new Error(`Async component timed out after ${l}ms.`);g(S),w.value=S}},l)),m().then(()=>{h.isUnmounted||(A.value=!0,h.parent&&oo(h.parent.vnode)&&h.parent.update())}).catch(S=>{if(h.isUnmounted){c=null;return}g(S),w.value=S}),()=>{if(A.value&&d)return To(d,h);if(w.value&&a)return Lt(a,{error:w.value});if(s&&!y.value)return To(s,h)}}})}function To(e,t){const{ref:s,props:a,children:n,ce:i}=t.vnode,l=Lt(e,a,n);return l.ref=s,l.ce=i,delete t.vnode.ce,l}const oo=e=>e.type.__isKeepAlive,Nx={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){const s=Ls(),a=s.ctx;if(!a.renderer)return()=>{const y=t.default&&t.default();return y&&y.length===1?y[0]:y};const n=new Map,i=new Set;let l=null;const o=s.suspense,{renderer:{p:r,m:c,um:d,o:{createElement:u}}}=a,f=u("div");a.activate=(y,b,x,S,_)=>{const T=y.component;c(y,b,x,0,o),r(T.vnode,y,b,x,T,o,S,y.slotScopeIds,_),Vt(()=>{T.isDeactivated=!1,T.a&&Li(T.a);const L=y.props&&y.props.onVnodeMounted;L&&$s(L,T.parent,y)},o)},a.deactivate=y=>{const b=y.component;cr(b.m),cr(b.a),c(y,f,null,1,o),Vt(()=>{b.da&&Li(b.da);const x=y.props&&y.props.onVnodeUnmounted;x&&$s(x,b.parent,y),b.isDeactivated=!0},o)};function m(y){Cc(y),d(y,s,o,!0)}function h(y){n.forEach((b,x)=>{const S=dd(Ya(b)?b.type.__asyncResolved||{}:b.type);S&&!y(S)&&g(x)})}function g(y){const b=n.get(y);b&&(!l||!fa(b,l))?m(b):l&&Cc(l),n.delete(y),i.delete(y)}jt(()=>[e.include,e.exclude],([y,b])=>{y&&h(x=>_l(y,x)),b&&h(x=>!_l(b,x))},{flush:"post",deep:!0});let A=null;const w=()=>{A!=null&&(dr(s.subTree.type)?Vt(()=>{n.set(A,Eo(s.subTree))},s.subTree.suspense):n.set(A,Eo(s.subTree)))};return tt(w),Gr(w),Wr(()=>{n.forEach(y=>{const{subTree:b,suspense:x}=s,S=Eo(b);if(y.type===S.type&&y.key===S.key){Cc(S);const _=S.component.da;_&&Vt(_,x);return}m(y)})}),()=>{if(A=null,!t.default)return l=null;const y=t.default(),b=y[0];if(y.length>1)return l=null,y;if(!nn(b)||!(b.shapeFlag&4)&&!(b.shapeFlag&128))return l=null,b;let x=Eo(b);if(x.type===Ht)return l=null,x;const S=x.type,_=dd(Ya(x)?x.type.__asyncResolved||{}:S),{include:T,exclude:L,max:E}=e;if(T&&(!_||!_l(T,_))||L&&_&&_l(L,_))return x.shapeFlag&=-257,l=x,b;const P=x.key==null?S:x.key,R=n.get(P);return x.el&&(x=Oa(x),b.shapeFlag&128&&(b.ssContent=x)),A=P,R?(x.el=R.el,x.component=R.component,x.transition&&an(x,x.transition),x.shapeFlag|=512,i.delete(P),i.add(P)):(i.add(P),E&&i.size>parseInt(E,10)&&g(i.values().next().value)),x.shapeFlag|=256,l=x,dr(b.type)?b:x}}},Mx=Nx;function _l(e,t){return Le(e)?e.some(s=>_l(s,t)):Qe(e)?e.split(",").includes(t):qb(e)?(e.lastIndex=0,e.test(t)):!1}function rs(e,t){lh(e,"a",t)}function Kt(e,t){lh(e,"da",t)}function lh(e,t,s=fs){const a=e.__wdc||(e.__wdc=()=>{let n=s;for(;n;){if(n.isDeactivated)return;n=n.parent}return e()});if(Vr(t,a,s),s){let n=s.parent;for(;n&&n.parent;)oo(n.parent.vnode)&&Dx(a,t,s,n),n=n.parent}}function Dx(e,t,s,a){const n=Vr(t,e,a,!0);xt(()=>{Fd(a[t],n)},s)}function Cc(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Eo(e){return e.shapeFlag&128?e.ssContent:e}function Vr(e,t,s=fs,a=!1){if(s){const n=s[e]||(s[e]=[]),i=t.__weh||(t.__weh=(...l)=>{tn();const o=Xi(s),r=Zs(t,s,e,l);return o(),sn(),r});return a?n.unshift(i):n.push(i),i}}const ln=e=>(t,s=fs)=>{(!Yn||e==="sp")&&Vr(e,(...a)=>t(...a),s)},oh=ln("bm"),tt=ln("m"),Yd=ln("bu"),Gr=ln("u"),Wr=ln("bum"),xt=ln("um"),rh=ln("sp"),ch=ln("rtg"),dh=ln("rtc");function uh(e,t=fs){Vr("ec",e,t)}const Xd="components",Px="directives";function $x(e,t){return eu(Xd,e,!0,t)||e}const ph=Symbol.for("v-ndc");function Fx(e){return Qe(e)?eu(Xd,e,!1)||e:e||ph}function Bx(e){return eu(Px,e)}function eu(e,t,s=!0,a=!1){const n=ms||fs;if(n){const i=n.type;if(e===Xd){const o=dd(i,!1);if(o&&(o===t||o===wt(t)||o===ai(wt(t))))return i}const l=gp(n[e]||i[e],t)||gp(n.appContext[e],t);return!l&&a?i:l}}function gp(e,t){return e&&(e[t]||e[wt(t)]||e[ai(wt(t))])}function Ux(e,t,s,a){let n;const i=s&&s[a],l=Le(e);if(l||Qe(e)){const o=l&&Qa(e);let r=!1,c=!1;o&&(r=!Hs(e),c=Ia(e),e=Ur(e)),n=new Array(e.length);for(let d=0,u=e.length;dt(o,r,void 0,i&&i[r]));else{const o=Object.keys(e);n=new Array(o.length);for(let r=0,c=o.length;r{const i=a.fn(...n);return i&&(i.key=a.key),i}:a.fn)}return e}function Hx(e,t,s={},a,n){if(ms.ce||ms.parent&&Ya(ms.parent)&&ms.parent.ce){const c=Object.keys(s).length>0;return t!=="default"&&(s.name=t),jl(),ur(ls,null,[Lt("slot",s,a&&a())],c?-2:64)}let i=e[t];i&&i._c&&(i._d=!1),jl();const l=i&&tu(i(s)),o=s.key||l&&l.key,r=ur(ls,{key:(o&&!Cs(o)?o:`_${t}`)+(!l&&a?"_fb":"")},l||(a?a():[]),l&&e._===1?64:-2);return!n&&r.scopeId&&(r.slotScopeIds=[r.scopeId+"-s"]),i&&i._c&&(i._d=!0),r}function tu(e){return e.some(t=>nn(t)?!(t.type===Ht||t.type===ls&&!tu(t.children)):!0)?e:null}function jx(e,t){const s={};for(const a in e)s[t&&/[A-Z]/.test(a)?`on:${a}`:Oi(a)]=e[a];return s}const sd=e=>e?Hh(e)?ro(e):sd(e.parent):null,Al=nt(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>sd(e.parent),$root:e=>sd(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>su(e),$forceUpdate:e=>e.f||(e.f=()=>{Wd(e.update)}),$nextTick:e=>e.n||(e.n=Mt.bind(e.proxy)),$watch:e=>px.bind(e)}),Tc=(e,t)=>e!==it&&!e.__isScriptSetup&&ht(e,t),ad={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:a,data:n,props:i,accessCache:l,type:o,appContext:r}=e;if(t[0]!=="$"){const f=l[t];if(f!==void 0)switch(f){case 1:return a[t];case 2:return n[t];case 4:return s[t];case 3:return i[t]}else{if(Tc(a,t))return l[t]=1,a[t];if(n!==it&&ht(n,t))return l[t]=2,n[t];if(ht(i,t))return l[t]=3,i[t];if(s!==it&&ht(s,t))return l[t]=4,s[t];nd&&(l[t]=0)}}const c=Al[t];let d,u;if(c)return t==="$attrs"&&ws(e.attrs,"get",""),c(e);if((d=o.__cssModules)&&(d=d[t]))return d;if(s!==it&&ht(s,t))return l[t]=4,s[t];if(u=r.config.globalProperties,ht(u,t))return u[t]},set({_:e},t,s){const{data:a,setupState:n,ctx:i}=e;return Tc(n,t)?(n[t]=s,!0):a!==it&&ht(a,t)?(a[t]=s,!0):ht(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:a,appContext:n,props:i,type:l}},o){let r;return!!(s[o]||e!==it&&o[0]!=="$"&&ht(e,o)||Tc(t,o)||ht(i,o)||ht(a,o)||ht(Al,o)||ht(n.config.globalProperties,o)||(r=l.__cssModules)&&r[o])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:ht(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}},qx=nt({},ad,{get(e,t){if(t!==Symbol.unscopables)return ad.get(e,t,e)},has(e,t){return t[0]!=="_"&&!Qb(t)}});function Vx(){return null}function Gx(){return null}function Wx(e){}function Kx(e){}function Jx(){return null}function Zx(){}function Qx(e,t){return null}function Yx(){return fh().slots}function Xx(){return fh().attrs}function fh(e){const t=Ls();return t.setupContext||(t.setupContext=Gh(t))}function zl(e){return Le(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}function e0(e,t){const s=zl(e);for(const a in t){if(a.startsWith("__skip"))continue;let n=s[a];n?Le(n)||je(n)?n=s[a]={type:n,default:t[a]}:n.default=t[a]:n===null&&(n=s[a]={default:t[a]}),n&&t[`__skip_${a}`]&&(n.skipFactory=!0)}return s}function t0(e,t){return!e||!t?e||t:Le(e)&&Le(t)?e.concat(t):nt({},zl(e),zl(t))}function s0(e,t){const s={};for(const a in e)t.includes(a)||Object.defineProperty(s,a,{enumerable:!0,get:()=>e[a]});return s}function a0(e){const t=Ls(),s=Yn;let a=e();Vl(),s&&Pi(!1);const n=()=>{Xi(t),s&&Pi(!0)},i=()=>{Ls()!==t&&t.scope.off(),Vl(),s&&Pi(!1)};return Bd(a)&&(a=a.catch(l=>{throw n(),Promise.resolve().then(()=>Promise.resolve().then(i)),l})),[a,()=>{n(),Promise.resolve().then(i)}]}let nd=!0;function n0(e){const t=su(e),s=e.proxy,a=e.ctx;nd=!1,t.beforeCreate&&bp(t.beforeCreate,e,"bc");const{data:n,computed:i,methods:l,watch:o,provide:r,inject:c,created:d,beforeMount:u,mounted:f,beforeUpdate:m,updated:h,activated:g,deactivated:A,beforeDestroy:w,beforeUnmount:y,destroyed:b,unmounted:x,render:S,renderTracked:_,renderTriggered:T,errorCaptured:L,serverPrefetch:E,expose:P,inheritAttrs:R,components:I,directives:H,filters:X}=t;if(c&&i0(c,a,null),l)for(const O in l){const $=l[O];je($)&&(a[O]=$.bind(s))}if(n){const O=n.call(s,s);mt(O)&&(e.data=Tn(O))}if(nd=!0,i)for(const O in i){const $=i[O],ce=je($)?$.bind(s,s):je($.get)?$.get.bind(s,s):hs,ue=!je($)&&je($.set)?$.set.bind(s):hs,Z=U({get:ce,set:ue});Object.defineProperty(a,O,{enumerable:!0,configurable:!0,get:()=>Z.value,set:J=>Z.value=J})}if(o)for(const O in o)mh(o[O],a,s,O);if(r){const O=je(r)?r.call(s):r;Reflect.ownKeys(O).forEach($=>{El($,O[$])})}d&&bp(d,e,"c");function N(O,$){Le($)?$.forEach(ce=>O(ce.bind(s))):$&&O($.bind(s))}if(N(oh,u),N(tt,f),N(Yd,m),N(Gr,h),N(rs,g),N(Kt,A),N(uh,L),N(dh,_),N(ch,T),N(Wr,y),N(xt,x),N(rh,E),Le(P))if(P.length){const O=e.exposed||(e.exposed={});P.forEach($=>{Object.defineProperty(O,$,{get:()=>s[$],set:ce=>s[$]=ce,enumerable:!0})})}else e.exposed||(e.exposed={});S&&e.render===hs&&(e.render=S),R!=null&&(e.inheritAttrs=R),I&&(e.components=I),H&&(e.directives=H),E&&Qd(e)}function i0(e,t,s=hs){Le(e)&&(e=id(e));for(const a in e){const n=e[a];let i;mt(n)?"default"in n?i=na(n.from||a,n.default,!0):i=na(n.from||a):i=na(n),Wt(i)?Object.defineProperty(t,a,{enumerable:!0,configurable:!0,get:()=>i.value,set:l=>i.value=l}):t[a]=i}}function bp(e,t,s){Zs(Le(e)?e.map(a=>a.bind(t.proxy)):e.bind(t.proxy),t,s)}function mh(e,t,s,a){let n=a.includes(".")?Xm(s,a):()=>s[a];if(Qe(e)){const i=t[e];je(i)&&jt(n,i)}else if(je(e))jt(n,e.bind(s));else if(mt(e))if(Le(e))e.forEach(i=>mh(i,t,s,a));else{const i=je(e.handler)?e.handler.bind(s):t[e.handler];je(i)&&jt(n,i,e)}}function su(e){const t=e.type,{mixins:s,extends:a}=t,{mixins:n,optionsCache:i,config:{optionMergeStrategies:l}}=e.appContext,o=i.get(t);let r;return o?r=o:!n.length&&!s&&!a?r=t:(r={},n.length&&n.forEach(c=>rr(r,c,l,!0)),rr(r,t,l)),mt(t)&&i.set(t,r),r}function rr(e,t,s,a=!1){const{mixins:n,extends:i}=t;i&&rr(e,i,s,!0),n&&n.forEach(l=>rr(e,l,s,!0));for(const l in t)if(!(a&&l==="expose")){const o=l0[l]||s&&s[l];e[l]=o?o(e[l],t[l]):t[l]}return e}const l0={data:yp,props:xp,emits:xp,methods:wl,computed:wl,beforeCreate:Es,created:Es,beforeMount:Es,mounted:Es,beforeUpdate:Es,updated:Es,beforeDestroy:Es,beforeUnmount:Es,destroyed:Es,unmounted:Es,activated:Es,deactivated:Es,errorCaptured:Es,serverPrefetch:Es,components:wl,directives:wl,watch:r0,provide:yp,inject:o0};function yp(e,t){return t?e?function(){return nt(je(e)?e.call(this,this):e,je(t)?t.call(this,this):t)}:t:e}function o0(e,t){return wl(id(e),id(t))}function id(e){if(Le(e)){const t={};for(let s=0;s{let d,u=it,f;return Ym(()=>{const m=e[n];is(d,m)&&(d=m,c())}),{get(){return r(),s.get?s.get(d):d},set(m){const h=s.set?s.set(m):m;if(!is(h,d)&&!(u!==it&&is(m,u)))return;const g=a.vnode.props,A=!!(g&&(t in g||n in g||i in g)&&(`onUpdate:${t}`in g||`onUpdate:${n}`in g||`onUpdate:${i}`in g));A||(d=m,c()),a.emit(`update:${t}`,h),is(m,u)&&(is(m,h)&&!is(h,f)||A&&u!==it&&!is(h,d))&&c(),u=m,f=h}}});return o[Symbol.iterator]=()=>{let r=0;return{next(){return r<2?{value:r++?l||it:o,done:!1}:{done:!0}}}},o}const vh=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${wt(t)}Modifiers`]||e[`${Us(t)}Modifiers`];function p0(e,t,...s){if(e.isUnmounted)return;const a=e.vnode.props||it;let n=s;const i=t.startsWith("update:"),l=i&&vh(a,t.slice(7));l&&(l.trim&&(n=s.map(d=>Qe(d)?d.trim():d)),l.number&&(n=s.map(Pr)));let o,r=a[o=Oi(t)]||a[o=Oi(wt(t))];!r&&i&&(r=a[o=Oi(Us(t))]),r&&Zs(r,e,6,n);const c=a[o+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[o])return;e.emitted[o]=!0,Zs(c,e,6,n)}}const f0=new WeakMap;function gh(e,t,s=!1){const a=s?f0:t.emitsCache,n=a.get(e);if(n!==void 0)return n;const i=e.emits;let l={},o=!1;if(!je(e)){const r=c=>{const d=gh(c,t,!0);d&&(o=!0,nt(l,d))};!s&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!i&&!o?(mt(e)&&a.set(e,null),null):(Le(i)?i.forEach(r=>l[r]=null):nt(l,i),mt(e)&&a.set(e,l),l)}function Kr(e,t){return!e||!ti(t)?!1:(t=t.slice(2).replace(/Once$/,""),ht(e,t[0].toLowerCase()+t.slice(1))||ht(e,Us(t))||ht(e,t))}function Wo(e){const{type:t,vnode:s,proxy:a,withProxy:n,propsOptions:[i],slots:l,attrs:o,emit:r,render:c,renderCache:d,props:u,data:f,setupState:m,ctx:h,inheritAttrs:g}=e,A=Ul(e);let w,y;try{if(s.shapeFlag&4){const x=n||a,S=x;w=Bs(c.call(S,x,d,u,m,f,h)),y=o}else{const x=t;w=Bs(x.length>1?x(u,{attrs:o,slots:l,emit:r}):x(u,null)),y=t.props?o:h0(o)}}catch(x){Rl.length=0,ni(x,e,1),w=Lt(Ht)}let b=w;if(y&&g!==!1){const x=Object.keys(y),{shapeFlag:S}=b;x.length&&S&7&&(i&&x.some(Lr)&&(y=v0(y,i)),b=Oa(b,y,!1,!0))}return s.dirs&&(b=Oa(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(s.dirs):s.dirs),s.transition&&an(b,s.transition),w=b,Ul(A),w}function m0(e,t=!0){let s;for(let a=0;a{let t;for(const s in e)(s==="class"||s==="style"||ti(s))&&((t||(t={}))[s]=e[s]);return t},v0=(e,t)=>{const s={};for(const a in e)(!Lr(a)||!(a.slice(9)in t))&&(s[a]=e[a]);return s};function g0(e,t,s){const{props:a,children:n,component:i}=e,{props:l,children:o,patchFlag:r}=t,c=i.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&r>=0){if(r&1024)return!0;if(r&16)return a?_p(a,l,c):!!l;if(r&8){const d=t.dynamicProps;for(let u=0;uObject.create(yh),_h=e=>Object.getPrototypeOf(e)===yh;function b0(e,t,s,a=!1){const n={},i=xh();e.propsDefaults=Object.create(null),wh(e,t,n,i);for(const l in e.propsOptions[0])l in n||(n[l]=void 0);s?e.props=a?n:qd(n):e.type.props?e.props=n:e.props=i,e.attrs=i}function y0(e,t,s,a){const{props:n,attrs:i,vnode:{patchFlag:l}}=e,o=dt(n),[r]=e.propsOptions;let c=!1;if((a||l>0)&&!(l&16)){if(l&8){const d=e.vnode.dynamicProps;for(let u=0;u{r=!0;const[f,m]=kh(u,t,!0);nt(l,f),m&&o.push(...m)};!s&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}if(!i&&!r)return mt(e)&&a.set(e,Ri),Ri;if(Le(i))for(let d=0;de==="_"||e==="_ctx"||e==="$stable",nu=e=>Le(e)?e.map(Bs):[Bs(e)],_0=(e,t,s)=>{if(t._n)return t;const a=Kd((...n)=>nu(t(...n)),s);return a._c=!1,a},Sh=(e,t,s)=>{const a=e._ctx;for(const n in e){if(au(n))continue;const i=e[n];if(je(i))t[n]=_0(n,i,a);else if(i!=null){const l=nu(i);t[n]=()=>l}}},Ch=(e,t)=>{const s=nu(t);e.slots.default=()=>s},Th=(e,t,s)=>{for(const a in t)(s||!au(a))&&(e[a]=t[a])},w0=(e,t,s)=>{const a=e.slots=xh();if(e.vnode.shapeFlag&32){const n=t._;n?(Th(a,t,s),s&&bm(a,"_",n,!0)):Sh(t,a)}else t&&Ch(e,t)},k0=(e,t,s)=>{const{vnode:a,slots:n}=e;let i=!0,l=it;if(a.shapeFlag&32){const o=t._;o?s&&o===1?i=!1:Th(n,t,s):(i=!t.$stable,Sh(t,n)),l=t}else t&&(Ch(e,t),l={default:1});if(i)for(const o in n)!au(o)&&l[o]==null&&delete n[o]},Vt=Mh;function Eh(e){return Rh(e)}function Ah(e){return Rh(e,wx)}function Rh(e,t){const s=$r();s.__VUE__=!0;const{insert:a,remove:n,patchProp:i,createElement:l,createText:o,createComment:r,setText:c,setElementText:d,parentNode:u,nextSibling:f,setScopeId:m=hs,insertStaticContent:h}=e,g=(k,D,W,ge=null,pe=null,he=null,we=void 0,K=null,ne=!!D.dynamicChildren)=>{if(k===D)return;k&&!fa(k,D)&&(ge=le(k),J(k,pe,he,!0),k=null),D.patchFlag===-2&&(ne=!1,D.dynamicChildren=null);const{type:te,ref:_e,shapeFlag:B}=D;switch(te){case kn:A(k,D,W,ge);break;case Ht:w(k,D,W,ge);break;case Kn:k==null&&y(D,W,ge,we);break;case ls:I(k,D,W,ge,pe,he,we,K,ne);break;default:B&1?S(k,D,W,ge,pe,he,we,K,ne):B&6?H(k,D,W,ge,pe,he,we,K,ne):(B&64||B&128)&&te.process(k,D,W,ge,pe,he,we,K,ne,j)}_e!=null&&pe?Mi(_e,k&&k.ref,he,D||k,!D):_e==null&&k&&k.ref!=null&&Mi(k.ref,null,he,k,!0)},A=(k,D,W,ge)=>{if(k==null)a(D.el=o(D.children),W,ge);else{const pe=D.el=k.el;D.children!==k.children&&c(pe,D.children)}},w=(k,D,W,ge)=>{k==null?a(D.el=r(D.children||""),W,ge):D.el=k.el},y=(k,D,W,ge)=>{[k.el,k.anchor]=h(k.children,D,W,ge,k.el,k.anchor)},b=({el:k,anchor:D},W,ge)=>{let pe;for(;k&&k!==D;)pe=f(k),a(k,W,ge),k=pe;a(D,W,ge)},x=({el:k,anchor:D})=>{let W;for(;k&&k!==D;)W=f(k),n(k),k=W;n(D)},S=(k,D,W,ge,pe,he,we,K,ne)=>{if(D.type==="svg"?we="svg":D.type==="math"&&(we="mathml"),k==null)_(D,W,ge,pe,he,we,K,ne);else{const te=k.el&&k.el._isVueCE?k.el:null;try{te&&te._beginPatch(),E(k,D,pe,he,we,K,ne)}finally{te&&te._endPatch()}}},_=(k,D,W,ge,pe,he,we,K)=>{let ne,te;const{props:_e,shapeFlag:B,transition:fe,dirs:de}=k;if(ne=k.el=l(k.type,he,_e&&_e.is,_e),B&8?d(ne,k.children):B&16&&L(k.children,ne,null,ge,pe,Ec(k,he),we,K),de&&Ea(k,null,ge,"created"),T(ne,k,k.scopeId,we,ge),_e){for(const Fe in _e)Fe!=="value"&&!Za(Fe)&&i(ne,Fe,null,_e[Fe],he,ge);"value"in _e&&i(ne,"value",null,_e.value,he),(te=_e.onVnodeBeforeMount)&&$s(te,ge,k)}de&&Ea(k,null,ge,"beforeMount");const be=Ih(pe,fe);be&&fe.beforeEnter(ne),a(ne,D,W),((te=_e&&_e.onVnodeMounted)||be||de)&&Vt(()=>{try{te&&$s(te,ge,k),be&&fe.enter(ne),de&&Ea(k,null,ge,"mounted")}finally{}},pe)},T=(k,D,W,ge,pe)=>{if(W&&m(k,W),ge)for(let he=0;he{for(let te=ne;te{const K=D.el=k.el;let{patchFlag:ne,dynamicChildren:te,dirs:_e}=D;ne|=k.patchFlag&16;const B=k.props||it,fe=D.props||it;let de;if(W&&$n(W,!1),(de=fe.onVnodeBeforeUpdate)&&$s(de,W,D,k),_e&&Ea(D,k,W,"beforeUpdate"),W&&$n(W,!0),(B.innerHTML&&fe.innerHTML==null||B.textContent&&fe.textContent==null)&&d(K,""),te?P(k.dynamicChildren,te,K,W,ge,Ec(D,pe),he):we||$(k,D,K,null,W,ge,Ec(D,pe),he,!1),ne>0){if(ne&16)R(K,B,fe,W,pe);else if(ne&2&&B.class!==fe.class&&i(K,"class",null,fe.class,pe),ne&4&&i(K,"style",B.style,fe.style,pe),ne&8){const be=D.dynamicProps;for(let Fe=0;Fe{de&&$s(de,W,D,k),_e&&Ea(D,k,W,"updated")},ge)},P=(k,D,W,ge,pe,he,we)=>{for(let K=0;K{if(D!==W){if(D!==it)for(const he in D)!Za(he)&&!(he in W)&&i(k,he,D[he],null,pe,ge);for(const he in W){if(Za(he))continue;const we=W[he],K=D[he];we!==K&&he!=="value"&&i(k,he,K,we,pe,ge)}"value"in W&&i(k,"value",D.value,W.value,pe)}},I=(k,D,W,ge,pe,he,we,K,ne)=>{const te=D.el=k?k.el:o(""),_e=D.anchor=k?k.anchor:o("");let{patchFlag:B,dynamicChildren:fe,slotScopeIds:de}=D;de&&(K=K?K.concat(de):de),k==null?(a(te,W,ge),a(_e,W,ge),L(D.children||[],W,_e,pe,he,we,K,ne)):B>0&&B&64&&fe&&k.dynamicChildren&&k.dynamicChildren.length===fe.length?(P(k.dynamicChildren,fe,W,pe,he,we,K),(D.key!=null||pe&&D===pe.subTree)&&iu(k,D,!0)):$(k,D,W,_e,pe,he,we,K,ne)},H=(k,D,W,ge,pe,he,we,K,ne)=>{D.slotScopeIds=K,k==null?D.shapeFlag&512?pe.ctx.activate(D,W,ge,we,ne):X(D,W,ge,pe,he,we,ne):Q(k,D,ne)},X=(k,D,W,ge,pe,he,we)=>{const K=k.component=zh(k,ge,pe);if(oo(k)&&(K.ctx.renderer=j),jh(K,!1,we),K.asyncDep){if(pe&&pe.registerDep(K,N,we),!k.el){const ne=K.subTree=Lt(Ht);w(null,ne,D,W),k.placeholder=ne.el}}else N(K,k,D,W,pe,he,we)},Q=(k,D,W)=>{const ge=D.component=k.component;if(g0(k,D,W))if(ge.asyncDep&&!ge.asyncResolved){O(ge,D,W);return}else ge.next=D,ge.update();else D.el=k.el,ge.vnode=D},N=(k,D,W,ge,pe,he,we)=>{const K=()=>{if(k.isMounted){let{next:B,bu:fe,u:de,parent:be,vnode:Fe}=k;{const Xe=Oh(k);if(Xe){B&&(B.el=Fe.el,O(k,B,we)),Xe.asyncDep.then(()=>{Vt(()=>{k.isUnmounted||te()},pe)});return}}let qe=B,$e;$n(k,!1),B?(B.el=Fe.el,O(k,B,we)):B=Fe,fe&&Li(fe),($e=B.props&&B.props.onVnodeBeforeUpdate)&&$s($e,be,B,Fe),$n(k,!0);const Ke=Wo(k),at=k.subTree;k.subTree=Ke,g(at,Ke,u(at.el),le(at),k,pe,he),B.el=Ke.el,qe===null&&Jr(k,Ke.el),de&&Vt(de,pe),($e=B.props&&B.props.onVnodeUpdated)&&Vt(()=>$s($e,be,B,Fe),pe)}else{let B;const{el:fe,props:de}=D,{bm:be,m:Fe,parent:qe,root:$e,type:Ke}=k,at=Ya(D);if($n(k,!1),be&&Li(be),!at&&(B=de&&de.onVnodeBeforeMount)&&$s(B,qe,D),$n(k,!0),fe&&xe){const Xe=()=>{k.subTree=Wo(k),xe(fe,k.subTree,k,pe,null)};at&&Ke.__asyncHydrate?Ke.__asyncHydrate(fe,k,Xe):Xe()}else{$e.ce&&$e.ce._hasShadowRoot()&&$e.ce._injectChildStyle(Ke,k.parent?k.parent.type:void 0);const Xe=k.subTree=Wo(k);g(null,Xe,W,ge,k,pe,he),D.el=Xe.el}if(Fe&&Vt(Fe,pe),!at&&(B=de&&de.onVnodeMounted)){const Xe=D;Vt(()=>$s(B,qe,Xe),pe)}(D.shapeFlag&256||qe&&Ya(qe.vnode)&&qe.vnode.shapeFlag&256)&&k.a&&Vt(k.a,pe),k.isMounted=!0,D=W=ge=null}};k.scope.on();const ne=k.effect=new Dl(K);k.scope.off();const te=k.update=ne.run.bind(ne),_e=k.job=ne.runIfDirty.bind(ne);_e.i=k,_e.id=k.uid,ne.scheduler=()=>Wd(_e),$n(k,!0),te()},O=(k,D,W)=>{D.component=k;const ge=k.vnode.props;k.vnode=D,k.next=null,y0(k,D.props,ge,W),k0(k,D.children,W),tn(),cp(k),sn()},$=(k,D,W,ge,pe,he,we,K,ne=!1)=>{const te=k&&k.children,_e=k?k.shapeFlag:0,B=D.children,{patchFlag:fe,shapeFlag:de}=D;if(fe>0){if(fe&128){ue(te,B,W,ge,pe,he,we,K,ne);return}else if(fe&256){ce(te,B,W,ge,pe,he,we,K,ne);return}}de&8?(_e&16&&Oe(te,pe,he),B!==te&&d(W,B)):_e&16?de&16?ue(te,B,W,ge,pe,he,we,K,ne):Oe(te,pe,he,!0):(_e&8&&d(W,""),de&16&&L(B,W,ge,pe,he,we,K,ne))},ce=(k,D,W,ge,pe,he,we,K,ne)=>{k=k||Ri,D=D||Ri;const te=k.length,_e=D.length,B=Math.min(te,_e);let fe;for(fe=0;fe_e?Oe(k,pe,he,!0,!1,B):L(D,W,ge,pe,he,we,K,ne,B)},ue=(k,D,W,ge,pe,he,we,K,ne)=>{let te=0;const _e=D.length;let B=k.length-1,fe=_e-1;for(;te<=B&&te<=fe;){const de=k[te],be=D[te]=ne?ja(D[te]):Bs(D[te]);if(fa(de,be))g(de,be,W,null,pe,he,we,K,ne);else break;te++}for(;te<=B&&te<=fe;){const de=k[B],be=D[fe]=ne?ja(D[fe]):Bs(D[fe]);if(fa(de,be))g(de,be,W,null,pe,he,we,K,ne);else break;B--,fe--}if(te>B){if(te<=fe){const de=fe+1,be=de<_e?D[de].el:ge;for(;te<=fe;)g(null,D[te]=ne?ja(D[te]):Bs(D[te]),W,be,pe,he,we,K,ne),te++}}else if(te>fe)for(;te<=B;)J(k[te],pe,he,!0),te++;else{const de=te,be=te,Fe=new Map;for(te=be;te<=fe;te++){const Re=D[te]=ne?ja(D[te]):Bs(D[te]);Re.key!=null&&Fe.set(Re.key,te)}let qe,$e=0;const Ke=fe-be+1;let at=!1,Xe=0;const Y=new Array(Ke);for(te=0;te=Ke){J(Re,pe,he,!0);continue}let Me;if(Re.key!=null)Me=Fe.get(Re.key);else for(qe=be;qe<=fe;qe++)if(Y[qe-be]===0&&fa(Re,D[qe])){Me=qe;break}Me===void 0?J(Re,pe,he,!0):(Y[Me-be]=te+1,Me>=Xe?Xe=Me:at=!0,g(Re,D[Me],W,null,pe,he,we,K,ne),$e++)}const Ae=at?S0(Y):Ri;for(qe=Ae.length-1,te=Ke-1;te>=0;te--){const Re=be+te,Me=D[Re],ae=D[Re+1],Ne=Re+1<_e?ae.el||Lh(ae):ge;Y[te]===0?g(null,Me,W,Ne,pe,he,we,K,ne):at&&(qe<0||te!==Ae[qe]?Z(Me,W,Ne,2):qe--)}}},Z=(k,D,W,ge,pe=null)=>{const{el:he,type:we,transition:K,children:ne,shapeFlag:te}=k;if(te&6){Z(k.component.subTree,D,W,ge);return}if(te&128){k.suspense.move(D,W,ge);return}if(te&64){we.move(k,D,W,j);return}if(we===ls){a(he,D,W);for(let B=0;BK.enter(he),pe));else{const{leave:B,delayLeave:fe,afterLeave:de}=K,be=()=>{k.ctx.isUnmounted?n(he):a(he,D,W)},Fe=()=>{const qe=he._isLeaving||!!he[ea];he._isLeaving&&he[ea](!0),K.persisted&&!qe?be():B(he,()=>{be(),de&&de()})};fe?fe(he,be,Fe):Fe()}else a(he,D,W)},J=(k,D,W,ge=!1,pe=!1)=>{const{type:he,props:we,ref:K,children:ne,dynamicChildren:te,shapeFlag:_e,patchFlag:B,dirs:fe,cacheIndex:de,memo:be}=k;if(B===-2&&(pe=!1),K!=null&&(tn(),Mi(K,null,W,k,!0),sn()),de!=null&&(D.renderCache[de]=void 0),_e&256){D.ctx.deactivate(k);return}const Fe=_e&1&&fe,qe=!Ya(k);let $e;if(qe&&($e=we&&we.onVnodeBeforeUnmount)&&$s($e,D,k),_e&6)ye(k.component,W,ge);else{if(_e&128){k.suspense.unmount(W,ge);return}Fe&&Ea(k,null,D,"beforeUnmount"),_e&64?k.type.remove(k,D,W,j,ge):te&&!te.hasOnce&&(he!==ls||B>0&&B&64)?Oe(te,D,W,!1,!0):(he===ls&&B&384||!pe&&_e&16)&&Oe(ne,D,W),ge&&se(k)}const Ke=be!=null&&de==null;(qe&&($e=we&&we.onVnodeUnmounted)||Fe||Ke)&&Vt(()=>{$e&&$s($e,D,k),Fe&&Ea(k,null,D,"unmounted"),Ke&&(k.el=null)},W)},se=k=>{const{type:D,el:W,anchor:ge,transition:pe}=k;if(D===ls){q(W,ge);return}if(D===Kn){x(k);return}const he=()=>{n(W),pe&&!pe.persisted&&pe.afterLeave&&pe.afterLeave()};if(k.shapeFlag&1&&pe&&!pe.persisted){const{leave:we,delayLeave:K}=pe,ne=()=>we(W,he);K?K(k.el,he,ne):ne()}else he()},q=(k,D)=>{let W;for(;k!==D;)W=f(k),n(k),k=W;n(D)},ye=(k,D,W)=>{const{bum:ge,scope:pe,job:he,subTree:we,um:K,m:ne,a:te}=k;cr(ne),cr(te),ge&&Li(ge),pe.stop(),he&&(he.flags|=8,J(we,k,D,W)),K&&Vt(K,D),Vt(()=>{k.isUnmounted=!0},D)},Oe=(k,D,W,ge=!1,pe=!1,he=0)=>{for(let we=he;we{if(k.shapeFlag&6)return le(k.component.subTree);if(k.shapeFlag&128)return k.suspense.next();const D=f(k.anchor||k.el),W=D&&D[eh];return W?f(W):D};let oe=!1;const ve=(k,D,W)=>{let ge;k==null?D._vnode&&(J(D._vnode,null,null,!0),ge=D._vnode.component):g(D._vnode||null,k,D,null,null,null,W),D._vnode=k,oe||(oe=!0,cp(ge),lr(),oe=!1)},j={p:g,um:J,m:Z,r:se,mt:X,mc:L,pc:$,pbc:P,n:le,o:e};let V,xe;return t&&([V,xe]=t(j)),{render:ve,hydrate:V,createApp:d0(ve,V)}}function Ec({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function $n({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Ih(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function iu(e,t,s=!1){const a=e.children,n=t.children;if(Le(a)&&Le(n))for(let i=0;i>1,e[s[o]]0&&(t[a]=s[i-1]),s[i]=a)}}for(i=s.length,l=s[i-1];i-- >0;)s[i]=l,l=t[l];return s}function Oh(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Oh(t)}function cr(e){if(e)for(let t=0;te.__isSuspense;let od=0;const C0={name:"Suspense",__isSuspense:!0,process(e,t,s,a,n,i,l,o,r,c){if(e==null)E0(t,s,a,n,i,l,o,r,c);else{if(i&&i.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}A0(e,t,s,a,n,l,o,r,c)}},hydrate:R0,normalize:I0},T0=C0;function Hl(e,t){const s=e.props&&e.props[t];je(s)&&s()}function E0(e,t,s,a,n,i,l,o,r){const{p:c,o:{createElement:d}}=r,u=d("div"),f=e.suspense=Nh(e,n,a,t,u,s,i,l,o,r);c(null,f.pendingBranch=e.ssContent,u,null,a,f,i,l),f.deps>0?(Hl(e,"onPending"),Hl(e,"onFallback"),c(null,e.ssFallback,t,s,a,null,i,l),Di(f,e.ssFallback)):f.resolve(!1,!0)}function A0(e,t,s,a,n,i,l,o,{p:r,um:c,o:{createElement:d}}){const u=t.suspense=e.suspense;u.vnode=t,t.el=e.el;const f=t.ssContent,m=t.ssFallback,{activeBranch:h,pendingBranch:g,isInFallback:A,isHydrating:w}=u;if(g)u.pendingBranch=f,fa(g,f)?(r(g,f,u.hiddenContainer,null,n,u,i,l,o),u.deps<=0?u.resolve():A&&(w||(r(h,m,s,a,n,null,i,l,o),Di(u,m)))):(u.pendingId=od++,w?(u.isHydrating=!1,u.activeBranch=g):c(g,n,u),u.deps=0,u.effects.length=0,u.hiddenContainer=d("div"),A?(r(null,f,u.hiddenContainer,null,n,u,i,l,o),u.deps<=0?u.resolve():(r(h,m,s,a,n,null,i,l,o),Di(u,m))):h&&fa(h,f)?(r(h,f,s,a,n,u,i,l,o),u.resolve(!0)):(r(null,f,u.hiddenContainer,null,n,u,i,l,o),u.deps<=0&&u.resolve()));else if(h&&fa(h,f))r(h,f,s,a,n,u,i,l,o),Di(u,f);else if(Hl(t,"onPending"),u.pendingBranch=f,f.shapeFlag&512?u.pendingId=f.component.suspenseId:u.pendingId=od++,r(null,f,u.hiddenContainer,null,n,u,i,l,o),u.deps<=0)u.resolve();else{const{timeout:y,pendingId:b}=u;y>0?setTimeout(()=>{u.pendingId===b&&u.fallback(m)},y):y===0&&u.fallback(m)}}function Nh(e,t,s,a,n,i,l,o,r,c,d=!1){const{p:u,m:f,um:m,n:h,o:{parentNode:g,remove:A}}=c;let w;const y=O0(e);y&&t&&t.pendingBranch&&(w=t.pendingId,t.deps++);const b=e.props?tr(e.props.timeout):void 0,x=i,S={vnode:e,parent:t,parentComponent:s,namespace:l,container:a,hiddenContainer:n,deps:0,pendingId:od++,timeout:typeof b=="number"?b:-1,activeBranch:null,isFallbackMountPending:!1,pendingBranch:null,isInFallback:!d,isHydrating:d,isUnmounted:!1,effects:[],resolve(_=!1,T=!1){const{vnode:L,activeBranch:E,pendingBranch:P,pendingId:R,effects:I,parentComponent:H,container:X,isInFallback:Q}=S;let N=!1;if(S.isHydrating)S.isHydrating=!1;else if(!_){N=E&&P.transition&&P.transition.mode==="out-in";let ce=!1;N&&(E.transition.afterLeave=()=>{R===S.pendingId&&(f(P,X,i===x&&!ce?h(E):i,0),Fl(I),Q&&L.ssFallback&&(L.ssFallback.el=null))}),E&&!S.isFallbackMountPending&&(g(E.el)===X&&(i=h(E),ce=!0),m(E,H,S,!0),!N&&Q&&L.ssFallback&&Vt(()=>L.ssFallback.el=null,S)),N||f(P,X,i,0)}S.isFallbackMountPending=!1,Di(S,P),S.pendingBranch=null,S.isInFallback=!1;let O=S.parent,$=!1;for(;O;){if(O.pendingBranch){O.effects.push(...I),$=!0;break}O=O.parent}!$&&!N&&Fl(I),S.effects=[],y&&t&&t.pendingBranch&&w===t.pendingId&&(t.deps--,t.deps===0&&!T&&t.resolve()),Hl(L,"onResolve")},fallback(_){if(!S.pendingBranch)return;const{vnode:T,activeBranch:L,parentComponent:E,container:P,namespace:R}=S;Hl(T,"onFallback");const I=h(L),H=()=>{S.isFallbackMountPending=!1,S.isInFallback&&(u(null,_,P,I,E,null,R,o,r),Di(S,_))},X=_.transition&&_.transition.mode==="out-in";X&&(S.isFallbackMountPending=!0,L.transition.afterLeave=H),S.isInFallback=!0,m(L,E,null,!0),X||H()},move(_,T,L){S.activeBranch&&f(S.activeBranch,_,T,L),S.container=_},next(){return S.activeBranch&&h(S.activeBranch)},registerDep(_,T,L){const E=!!S.pendingBranch;E&&S.deps++;const P=_.vnode.el;_.asyncDep.catch(R=>{ni(R,_,0)}).then(R=>{if(_.isUnmounted||S.isUnmounted||S.pendingId!==_.suspenseId)return;Vl(),_.asyncResolved=!0;const{vnode:I}=_;rd(_,R,!1),P&&(I.el=P);const H=!P&&_.subTree.el;T(_,I,g(P||_.subTree.el),P?null:h(_.subTree),S,l,L),H&&(I.placeholder=null,A(H)),Jr(_,I.el),E&&--S.deps===0&&S.resolve()})},unmount(_,T){S.isUnmounted=!0,S.activeBranch&&m(S.activeBranch,s,_,T),S.pendingBranch&&m(S.pendingBranch,s,_,T)}};return S}function R0(e,t,s,a,n,i,l,o,r){const c=t.suspense=Nh(t,a,s,e.parentNode,document.createElement("div"),null,n,i,l,o,!0),d=r(e,c.pendingBranch=t.ssContent,s,c,i,l);return c.deps===0&&c.resolve(!1,!0),d}function I0(e){const{shapeFlag:t,children:s}=e,a=t&32;e.ssContent=kp(a?s.default:s),e.ssFallback=a?kp(s.fallback):Lt(Ht)}function kp(e){let t;if(je(e)){const s=Qn&&e._c;s&&(e._d=!1,jl()),e=e(),s&&(e._d=!0,t=ks,Dh())}return Le(e)&&(e=m0(e)),e=Bs(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter(s=>s!==e)),e}function Mh(e,t){t&&t.pendingBranch?Le(e)?t.effects.push(...e):t.effects.push(e):Fl(e)}function Di(e,t){e.activeBranch=t;const{vnode:s,parentComponent:a}=e;let n=t.el;for(;!n&&t.component;)t=t.component.subTree,n=t.el;s.el=n,a&&a.subTree===s&&(a.vnode.el=n,Jr(a,n))}function O0(e){const t=e.props&&e.props.suspensible;return t!=null&&t!==!1}const ls=Symbol.for("v-fgt"),kn=Symbol.for("v-txt"),Ht=Symbol.for("v-cmt"),Kn=Symbol.for("v-stc"),Rl=[];let ks=null;function jl(e=!1){Rl.push(ks=e?null:[])}function Dh(){Rl.pop(),ks=Rl[Rl.length-1]||null}let Qn=1;function ql(e,t=!1){Qn+=e,e<0&&ks&&t&&(ks.hasOnce=!0)}function Ph(e){return e.dynamicChildren=Qn>0?ks||Ri:null,Dh(),Qn>0&&ks&&ks.push(e),e}function L0(e,t,s,a,n,i){return Ph(lu(e,t,s,a,n,i,!0))}function ur(e,t,s,a,n){return Ph(Lt(e,t,s,a,n,!0))}function nn(e){return e?e.__v_isVNode===!0:!1}function fa(e,t){return e.type===t.type&&e.key===t.key}function N0(e){}const $h=({key:e})=>e??null,Ko=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?Qe(e)||Wt(e)||je(e)?{i:ms,r:e,k:t,f:!!s}:e:null);function lu(e,t=null,s=null,a=0,n=null,i=e===ls?0:1,l=!1,o=!1){const r={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&$h(t),ref:t&&Ko(t),scopeId:jr,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:a,dynamicProps:n,dynamicChildren:null,appContext:null,ctx:ms};return o?(ru(r,s),i&128&&e.normalize(r)):s&&(r.shapeFlag|=Qe(s)?8:16),Qn>0&&!l&&ks&&(r.patchFlag>0||i&6)&&r.patchFlag!==32&&ks.push(r),r}const Lt=M0;function M0(e,t=null,s=null,a=0,n=null,i=!1){if((!e||e===ph)&&(e=Ht),nn(e)){const o=Oa(e,t,!0);return s&&ru(o,s),Qn>0&&!i&&ks&&(o.shapeFlag&6?ks[ks.indexOf(e)]=o:ks.push(o)),o.patchFlag=-2,o}if(z0(e)&&(e=e.__vccOpts),t){t=Fh(t);let{class:o,style:r}=t;o&&!Qe(o)&&(t.class=ao(o)),mt(r)&&(no(r)&&!Le(r)&&(r=nt({},r)),t.style=so(r))}const l=Qe(e)?1:dr(e)?128:th(e)?64:mt(e)?4:je(e)?2:0;return lu(e,t,s,a,n,l,i,!0)}function Fh(e){return e?no(e)||_h(e)?nt({},e):e:null}function Oa(e,t,s=!1,a=!1){const{props:n,ref:i,patchFlag:l,children:o,transition:r}=e,c=t?Uh(n||{},t):n,d={__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&$h(c),ref:t&&t.ref?s&&i?Le(i)?i.concat(Ko(t)):[i,Ko(t)]:Ko(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ls?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:r,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Oa(e.ssContent),ssFallback:e.ssFallback&&Oa(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return r&&a&&an(d,r.clone(d)),d}function ou(e=" ",t=0){return Lt(kn,null,e,t)}function D0(e,t){const s=Lt(Kn,null,e);return s.staticCount=t,s}function Bh(e="",t=!1){return t?(jl(),ur(Ht,null,e)):Lt(Ht,null,e)}function Bs(e){return e==null||typeof e=="boolean"?Lt(Ht):Le(e)?Lt(ls,null,e.slice()):nn(e)?ja(e):Lt(kn,null,String(e))}function ja(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Oa(e)}function ru(e,t){let s=0;const{shapeFlag:a}=e;if(t==null)t=null;else if(Le(t))s=16;else if(typeof t=="object")if(a&65){const n=t.default;n&&(n._c&&(n._d=!1),ru(e,n()),n._c&&(n._d=!0));return}else{s=32;const n=t._;!n&&!_h(t)?t._ctx=ms:n===3&&ms&&(ms.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else je(t)?(t={default:t,_ctx:ms},s=32):(t=String(t),a&64?(s=16,t=[ou(t)]):s=8);e.children=t,e.shapeFlag|=s}function Uh(...e){const t={};for(let s=0;sfs||ms;let pr,Pi;{const e=$r(),t=(s,a)=>{let n;return(n=e[s])||(n=e[s]=[]),n.push(a),i=>{n.length>1?n.forEach(l=>l(i)):n[0](i)}};pr=t("__VUE_INSTANCE_SETTERS__",s=>fs=s),Pi=t("__VUE_SSR_SETTERS__",s=>Yn=s)}const Xi=e=>{const t=fs;return pr(e),e.scope.on(),()=>{e.scope.off(),pr(t)}},Vl=()=>{fs&&fs.scope.off(),pr(null)};function Hh(e){return e.vnode.shapeFlag&4}let Yn=!1;function jh(e,t=!1,s=!1){t&&Pi(t);const{props:a,children:n}=e.vnode,i=Hh(e);b0(e,a,i,t),w0(e,n,s||t);const l=i?F0(e,t):void 0;return t&&Pi(!1),l}function F0(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,ad);const{setup:a}=s;if(a){tn();const n=e.setupContext=a.length>1?Gh(e):null,i=Xi(e),l=Yi(a,e,0,[e.props,n]),o=Bd(l);if(sn(),i(),(o||e.sp)&&!Ya(e)&&Qd(e),o){if(l.then(Vl,Vl),t)return l.then(r=>{rd(e,r,t)}).catch(r=>{ni(r,e,0)});e.asyncDep=l}else rd(e,l,t)}else Vh(e,t)}function rd(e,t,s){je(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:mt(t)&&(e.setupState=Gd(t)),Vh(e,s)}let fr,cd;function qh(e){fr=e,cd=t=>{t.render._rc&&(t.withProxy=new Proxy(t.ctx,qx))}}const B0=()=>!fr;function Vh(e,t,s){const a=e.type;if(!e.render){if(!t&&fr&&!a.render){const n=a.template||su(e).template;if(n){const{isCustomElement:i,compilerOptions:l}=e.appContext.config,{delimiters:o,compilerOptions:r}=a,c=nt(nt({isCustomElement:i,delimiters:o},l),r);a.render=fr(n,c)}}e.render=a.render||hs,cd&&cd(e)}{const n=Xi(e);tn();try{n0(e)}finally{sn(),n()}}}const U0={get(e,t){return ws(e,"get",""),e[t]}};function Gh(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,U0),slots:e.slots,emit:e.emit,expose:t}}function ro(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Gd(Um(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in Al)return Al[s](e)},has(t,s){return s in t||s in Al}})):e.proxy}function dd(e,t=!0){return je(e)?e.displayName||e.name:e.name||t&&e.__name}function z0(e){return je(e)&&"__vccOpts"in e}const U=(e,t)=>Wy(e,t,Yn);function zi(e,t,s){try{ql(-1);const a=arguments.length;return a===2?mt(t)&&!Le(t)?nn(t)?Lt(e,null,[t]):Lt(e,t):Lt(e,null,t):(a>3?s=Array.prototype.slice.call(arguments,2):a===3&&nn(s)&&(s=[s]),Lt(e,t,s))}finally{ql(1)}}function H0(){}function j0(e,t,s,a){const n=s[a];if(n&&Wh(n,e))return n;const i=t();return i.memo=e.slice(),i.cacheIndex=a,s[a]=i}function Wh(e,t){const s=e.memo;if(s.length!=t.length)return!1;for(let a=0;a0&&ks&&ks.push(e),!0}const Kh="3.5.38",q0=hs,V0=sx,G0=ki,W0=Jm,K0={createComponentInstance:zh,setupComponent:jh,renderComponentRoot:Wo,setCurrentRenderingInstance:Ul,isVNode:nn,normalizeVNode:Bs,getComponentPublicInstance:ro,ensureValidVNode:tu,pushWarningContext:Yy,popWarningContext:Xy},J0=K0,Z0=null,Q0=null,Y0=null;/** +**/const tv=[];function yx(e){tv.push(e)}function xx(){tv.pop()}function _x(e,t){}const wx={SETUP_FUNCTION:0,0:"SETUP_FUNCTION",RENDER_FUNCTION:1,1:"RENDER_FUNCTION",NATIVE_EVENT_HANDLER:5,5:"NATIVE_EVENT_HANDLER",COMPONENT_EVENT_HANDLER:6,6:"COMPONENT_EVENT_HANDLER",VNODE_HOOK:7,7:"VNODE_HOOK",DIRECTIVE_HOOK:8,8:"DIRECTIVE_HOOK",TRANSITION_HOOK:9,9:"TRANSITION_HOOK",APP_ERROR_HANDLER:10,10:"APP_ERROR_HANDLER",APP_WARN_HANDLER:11,11:"APP_WARN_HANDLER",FUNCTION_REF:12,12:"FUNCTION_REF",ASYNC_COMPONENT_LOADER:13,13:"ASYNC_COMPONENT_LOADER",SCHEDULER:14,14:"SCHEDULER",COMPONENT_UPDATE:15,15:"COMPONENT_UPDATE",APP_UNMOUNT_CLEANUP:16,16:"APP_UNMOUNT_CLEANUP"},kx={sp:"serverPrefetch hook",bc:"beforeCreate hook",c:"created hook",bm:"beforeMount hook",m:"mounted hook",bu:"beforeUpdate hook",u:"updated",bum:"beforeUnmount hook",um:"unmounted hook",a:"activated hook",da:"deactivated hook",ec:"errorCaptured hook",rtc:"renderTracked hook",rtg:"renderTriggered hook",0:"setup function",1:"render function",2:"watcher getter",3:"watcher callback",4:"watcher cleanup function",5:"native event handler",6:"component event handler",7:"vnode hook",8:"directive hook",9:"transition hook",10:"app errorHandler",11:"app warnHandler",12:"ref function",13:"async component loader",14:"scheduler flush",15:"component update",16:"app unmount cleanup function"};function tl(e,t,s,a){try{return a?e(...a):e()}catch(n){ci(n,t,s)}}function Ys(e,t,s,a){if(Ve(e)){const n=tl(e,t,s,a);return n&&Wd(n)&&n.catch(i=>{ci(i,t,s)}),n}if(Me(e)){const n=[];for(let i=0;i>>1,n=Ns[a],i=Vl(n);i=Vl(s)?Ns.push(e):Ns.splice(Cx(t),0,e),e.flags|=1,av()}}function av(){pr||(pr=sv.then(nv))}function ql(e){Me(e)?Pi.push(...e):En&&e.id===-1?En.splice(Ci+1,0,e):e.flags&1||(Pi.push(e),e.flags|=1),av()}function yp(e,t,s=Ra+1){for(;sVl(s)-Vl(a));if(Pi.length=0,En){En.push(...t);return}for(En=t,Ci=0;Cie.id==null?e.flags&2?-1:1/0:e.id;function nv(e){try{for(Ra=0;RaTi.emit(n,...i)),Oo=[]):typeof window<"u"&&window.HTMLElement&&!((a=(s=window.navigator)==null?void 0:s.userAgent)!=null&&a.includes("jsdom"))?((t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push(i=>{iv(i,t)}),setTimeout(()=>{Ti||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,Oo=[])},3e3)):Oo=[]}let ms=null,Zr=null;function Gl(e){const t=ms;return ms=e,Zr=e&&e.type.__scopeId||null,t}function Tx(e){Zr=e}function Ex(){Zr=null}const Ax=e=>su;function su(e,t=ms,s){if(!t||e._n)return e;const a=(...n)=>{a._d&&Zl(-1);const i=Gl(t);let l;try{l=e(...n)}finally{Gl(i),a._d&&Zl(1)}return l};return a._n=!0,a._c=!0,a._d=!0,a}function Rx(e,t){if(ms===null)return e;const s=vo(ms),a=e.dirs||(e.dirs=[]);for(let n=0;n1)return s&&Ve(t)?t.call(a&&a.proxy):t}}function Ix(){return!!(Ds()||Yn)}const lv=Symbol.for("v-scx"),ov=()=>ra(lv);function Ox(e,t){return po(e,null,t)}function Lx(e,t){return po(e,null,{flush:"post"})}function rv(e,t){return po(e,null,{flush:"sync"})}function Gt(e,t,s){return po(e,t,s)}function po(e,t,s=ct){const{immediate:a,deep:n,flush:i,once:l}=s,o=lt({},s),r=t&&a||!t&&i!=="post";let c;if(ai){if(i==="sync"){const m=ov();c=m.__watcherHandles||(m.__watcherHandles=[])}else if(!r){const m=()=>{};return m.stop=vs,m.resume=vs,m.pause=vs,m}}const d=fs;o.call=(m,h,y)=>Ys(m,d,h,y);let u=!1;i==="post"?o.scheduler=m=>{Qt(m,d&&d.suspense)}:i!=="sync"&&(u=!0,o.scheduler=(m,h)=>{h?m():tu(m)}),o.augmentJob=m=>{t&&(m.flags|=4),u&&(m.flags|=2,d&&(m.id=d.uid,m.i=d))};const p=bx(e,t,o);return ai&&(c?c.push(p):r&&p()),p}function Nx(e,t,s){const a=this.proxy,n=Ze(e)?e.includes(".")?cv(a,e):()=>a[e]:e.bind(a,a);let i;Ve(t)?i=t:(i=t.handler,s=t);const l=sl(this),o=po(n,i.bind(a),s);return l(),o}function cv(e,t){const s=t.split(".");return()=>{let a=e;for(let n=0;ne.__isTeleport,Wn=e=>e&&(e.disabled||e.disabled===""),Mx=e=>e&&(e.defer||e.defer===""),xp=e=>typeof SVGElement<"u"&&e instanceof SVGElement,_p=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,rd=(e,t)=>{const s=e&&e.to;return Ze(s)?t?t(s):null:s},Dx={name:"Teleport",__isTeleport:!0,process(e,t,s,a,n,i,l,o,r,c){const{mc:d,pc:u,pbc:p,o:{insert:m,querySelector:h,createText:y,createComment:k,parentNode:A}}=c,g=Wn(t.props);let{dynamicChildren:b}=t;const x=(T,L,E)=>{T.shapeFlag&16&&d(T.children,L,E,n,i,l,o,r)},w=(T=t)=>{const L=Wn(T.props),E=T.target=rd(T.props,h),F=cd(E,T,y,m);E&&(l!=="svg"&&xp(E)?l="svg":l!=="mathml"&&_p(E)&&(l="mathml"),n&&n.isCE&&(n.ce._teleportTargets||(n.ce._teleportTargets=new Set)).add(E),L||(x(T,E,F),Tl(T,!1)))},_=T=>{const L=()=>{if(Sn.get(T)===L){if(Sn.delete(T),Wn(T.props)){const E=A(T.el)||s;x(T,E,T.anchor),Tl(T,!0)}w(T)}};Sn.set(T,L),Qt(L,i)};if(e==null){const T=t.el=y(""),L=t.anchor=y("");if(m(T,s,a),m(L,s,a),Mx(t.props)||i&&i.pendingBranch){_(t);return}g&&(x(t,s,L),Tl(t,!0)),w()}else{t.el=e.el;const T=t.anchor=e.anchor,L=Sn.get(e);if(L){L.flags|=8,Sn.delete(e),_(t);return}t.targetStart=e.targetStart;const E=t.target=e.target,F=t.targetAnchor=e.targetAnchor,O=Wn(e.props),I=O?s:E,H=O?T:F;if(l==="svg"||xp(E)?l="svg":(l==="mathml"||_p(E))&&(l="mathml"),b?(p(e.dynamicChildren,b,I,n,i,l,o),fu(e,t,!0)):r||u(e,t,I,H,n,i,l,o,!1),g)O?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Lo(t,s,T,c,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const te=t.target=rd(t.props,h);te&&Lo(t,te,null,c,0)}else O&&Lo(t,E,F,c,1);Tl(t,g)}},remove(e,t,s,{um:a,o:{remove:n}},i){const{shapeFlag:l,children:o,anchor:r,targetStart:c,targetAnchor:d,target:u,props:p}=e,m=i||!Wn(p),h=Sn.get(e);if(h&&(h.flags|=8,Sn.delete(e)),u&&(n(c),n(d)),i&&n(r),!h&&l&16)for(let y=0;y{e.isMounted=!0}),ec(()=>{e.isUnmounting=!0}),e}const aa=[Function,Array],nu={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:aa,onEnter:aa,onAfterEnter:aa,onEnterCancelled:aa,onBeforeLeave:aa,onLeave:aa,onAfterLeave:aa,onLeaveCancelled:aa,onBeforeAppear:aa,onAppear:aa,onAfterAppear:aa,onAppearCancelled:aa},pv=e=>{const t=e.subTree;return t.component?pv(t.component):t},Fx={name:"BaseTransition",props:nu,setup(e,{slots:t}){const s=Ds(),a=au();return()=>{const n=t.default&&Qr(t.default(),!0),i=n&&n.length?fv(n):s.subTree?Jv():void 0;if(!i)return;const l=ft(e),{mode:o}=l;if(a.isLeaving)return Mc(i);const r=wp(i);if(!r)return Mc(i);let c=ji(r,l,a,s,u=>c=u);r.type!==Zt&&rn(r,c);let d=s.subTree&&wp(s.subTree);if(d&&d.type!==Zt&&!fa(d,r)&&pv(s).type!==Zt){let u=ji(d,l,a,s);if(rn(d,u),o==="out-in"&&r.type!==Zt)return a.isLeaving=!0,u.afterLeave=()=>{a.isLeaving=!1,s.job.flags&8||s.update(),delete u.afterLeave,d=void 0},Mc(i);o==="in-out"&&r.type!==Zt?u.delayLeave=(p,m,h)=>{const y=vv(a,d);y[String(d.key)]=d,p[na]=()=>{m(),p[na]=void 0,delete c.delayedLeave,d=void 0},c.delayedLeave=()=>{h(),delete c.delayedLeave,d=void 0}}:d=void 0}else d&&(d=void 0);return i}}};function fv(e){let t=e[0];if(e.length>1){for(const s of e)if(s.type!==Zt){t=s;break}}return t}const mv=Fx;function vv(e,t){const{leavingVNodes:s}=e;let a=s.get(t.type);return a||(a=Object.create(null),s.set(t.type,a)),a}function ji(e,t,s,a,n){const{appear:i,mode:l,persisted:o=!1,onBeforeEnter:r,onEnter:c,onAfterEnter:d,onEnterCancelled:u,onBeforeLeave:p,onLeave:m,onAfterLeave:h,onLeaveCancelled:y,onBeforeAppear:k,onAppear:A,onAfterAppear:g,onAppearCancelled:b}=t,x=String(e.key),w=vv(s,e),_=(E,F)=>{E&&Ys(E,a,9,F)},T=(E,F)=>{const O=F[1];_(E,F),Me(E)?E.every(I=>I.length<=1)&&O():E.length<=1&&O()},L={mode:l,persisted:o,beforeEnter(E){let F=r;if(!s.isMounted)if(i)F=k||r;else return;E[na]&&E[na](!0);const O=w[x];O&&fa(e,O)&&O.el[na]&&O.el[na](),_(F,[E])},enter(E){if(w[x]===e)return;let F=c,O=d,I=u;if(!s.isMounted)if(i)F=A||c,O=g||d,I=b||u;else return;let H=!1;E[pl]=ee=>{H||(H=!0,ee?_(I,[E]):_(O,[E]),L.delayedLeave&&L.delayedLeave(),E[pl]=void 0)};const te=E[pl].bind(null,!1);F?T(F,[E,te]):te()},leave(E,F){const O=String(e.key);if(E[pl]&&E[pl](!0),s.isUnmounting)return F();_(p,[E]);let I=!1;E[na]=te=>{I||(I=!0,F(),te?_(y,[E]):_(h,[E]),E[na]=void 0,w[O]===e&&delete w[O])};const H=E[na].bind(null,!1);w[O]=e,m?T(m,[E,H]):H()},clone(E){const F=ji(E,t,s,a,n);return n&&n(F),F}};return L}function Mc(e){if(mo(e))return e=Ma(e),e.children=null,e}function wp(e){if(!mo(e))return uv(e.type)&&e.children?fv(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:s}=e;if(s){if(t&16)return s[0];if(t&32&&Ve(s.default))return s.default()}}function rn(e,t){e.shapeFlag&6&&e.component?(e.transition=t,rn(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Qr(e,t=!1,s){let a=[],n=0;for(let i=0;i1)for(let i=0;is.value,set:i=>s.value=i})}return s}function kp(e,t){let s;return!!((s=Object.getOwnPropertyDescriptor(e,t))&&!s.configurable)}const mr=new WeakMap;function $i(e,t,s,a,n=!1){if(Me(e)){e.forEach((y,k)=>$i(y,t&&(Me(t)?t[k]:t),s,a,n));return}if(sn(a)&&!n){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&$i(e,t,s,a.component.subTree);return}const i=a.shapeFlag&4?vo(a.component):a.el,l=n?null:i,{i:o,r}=e,c=t&&t.r,d=o.refs===ct?o.refs={}:o.refs,u=o.setupState,p=ft(u),m=u===ct?Ri:y=>kp(d,y)?!1:bt(p,y),h=(y,k)=>!(k&&kp(d,k));if(c!=null&&c!==r){if(Sp(t),Ze(c))d[c]=null,m(c)&&(u[c]=null);else if(Xt(c)){const y=t;h(c,y.k)&&(c.value=null),y.k&&(d[y.k]=null)}}if(Ve(r))tl(r,o,12,[l,d]);else{const y=Ze(r),k=Xt(r);if(y||k){const A=()=>{if(e.f){const g=y?m(r)?u[r]:d[r]:h()||!e.k?r.value:d[e.k];if(n)Me(g)&&Gd(g,i);else if(Me(g))g.includes(i)||g.push(i);else if(y)d[r]=[i],m(r)&&(u[r]=d[r]);else{const b=[i];h(r,e.k)&&(r.value=b),e.k&&(d[e.k]=b)}}else y?(d[r]=l,m(r)&&(u[r]=l)):k&&(h(r,e.k)&&(r.value=l),e.k&&(d[e.k]=l))};if(l){const g=()=>{A(),mr.delete(e)};g.id=-1,mr.set(e,g),Qt(g,s)}else Sp(e),A()}}}function Sp(e){const t=mr.get(e);t&&(t.flags|=8,mr.delete(e))}let Cp=!1;const xi=()=>{Cp||(console.error("Hydration completed but contains mismatches."),Cp=!0)},zx=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",Hx=e=>e.namespaceURI.includes("MathML"),No=e=>{if(e.nodeType===1){if(zx(e))return"svg";if(Hx(e))return"mathml"}},Ii=e=>e.nodeType===8;function jx(e){const{mt:t,p:s,o:{patchProp:a,createText:n,nextSibling:i,parentNode:l,remove:o,insert:r,createComment:c}}=e,d=(b,x)=>{if(!x.hasChildNodes()){s(null,b,x),fr(),x._vnode=b;return}u(x.firstChild,b,null,null,null),fr(),x._vnode=b},u=(b,x,w,_,T,L=!1)=>{L=L||!!x.dynamicChildren;const E=Ii(b)&&b.data==="[",F=()=>y(b,x,w,_,T,E),{type:O,ref:I,shapeFlag:H,patchFlag:te}=x;let ee=b.nodeType;x.el=b,te===-2&&(L=!1,x.dynamicChildren=null);let N=null;switch(O){case On:ee!==3?x.children===""?(r(x.el=n(""),l(b),b),N=b):N=F():(b.data!==x.children&&(xi(),b.data=x.children),N=i(b));break;case Zt:g(b)?(N=i(b),A(x.el=b.content.firstChild,b,w)):ee!==8||E?N=F():N=i(b);break;case Xn:if(E&&(b=i(b),ee=b.nodeType),ee===1||ee===3){N=b;const M=!x.children.length;for(let $=0;${L=L||!!x.dynamicChildren;const{type:E,props:F,patchFlag:O,shapeFlag:I,dirs:H,transition:te}=x,ee=E==="input"||E==="option";if(ee||O!==-1){H&&Ia(x,null,w,"created");let N=!1;if(g(b)){N=Uv(null,te)&&w&&w.vnode.props&&w.vnode.props.appear;const $=b.content.firstChild;if(N){const re=$.getAttribute("class");re&&($.$cls=re),te.beforeEnter($)}A($,b,w),x.el=b=$}if(I&16&&!(F&&(F.innerHTML||F.textContent))){let $=m(b.firstChild,x,b,w,_,T,L);for($&&!Mo(b,1)&&xi();$;){const re=$;$=$.nextSibling,o(re)}}else if(I&8){let $=x.children;$[0]===` +`&&(b.tagName==="PRE"||b.tagName==="TEXTAREA")&&($=$.slice(1));const{textContent:re}=b;re!==$&&re!==$.replace(/\r\n|\r/g,` +`)&&(Mo(b,0)||xi(),b.textContent=x.children)}if(F){if(ee||!L||O&48){const $=b.tagName.includes("-");for(const re in F)(ee&&(re.endsWith("value")||re==="indeterminate")||li(re)&&!en(re)||re[0]==="."||$&&!en(re))&&a(b,re,null,F[re],void 0,w)}else if(F.onClick)a(b,"onClick",null,F.onClick,void 0,w);else if(O&4&&tn(F.style))for(const $ in F.style)F.style[$]}let M;(M=F&&F.onVnodeBeforeMount)&&Bs(M,w,x),H&&Ia(x,null,w,"beforeMount"),((M=F&&F.onVnodeMounted)||H||N)&&qv(()=>{M&&Bs(M,w,x),N&&te.enter(b),H&&Ia(x,null,w,"mounted")},_)}return b.nextSibling},m=(b,x,w,_,T,L,E)=>{E=E||!!x.dynamicChildren;const F=x.children,O=F.length;let I=!1;for(let H=0;H{const{slotScopeIds:E}=x;E&&(T=T?T.concat(E):E);const F=l(b),O=m(i(b),x,F,w,_,T,L);return O&&Ii(O)&&O.data==="]"?i(x.anchor=O):(xi(),r(x.anchor=c("]"),F,O),O)},y=(b,x,w,_,T,L)=>{if(Mo(b.parentElement,1)||xi(),x.el=null,L){const O=k(b);for(;;){const I=i(b);if(I&&I!==O)o(I);else break}}const E=i(b),F=l(b);return o(b),s(null,x,F,E,w,_,No(F),T),w&&(w.vnode.el=x.el,sc(w,x.el)),E},k=(b,x="[",w="]")=>{let _=0;for(;b;)if(b=i(b),b&&Ii(b)&&(b.data===x&&_++,b.data===w)){if(_===0)return i(b);_--}return b},A=(b,x,w)=>{const _=x.parentNode;_&&_.replaceChild(b,x);let T=w;for(;T;)T.vnode.el===x&&(T.vnode.el=T.subTree.el=b),T=T.parent},g=b=>b.nodeType===1&&b.tagName==="TEMPLATE";return[d,u]}const Tp="data-allow-mismatch",qx={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function Mo(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(Tp);)e=e.parentElement;const s=e&&e.getAttribute(Tp);if(s==null)return!1;if(s==="")return!0;{const a=s.split(",");return t===0&&a.includes("children")?!0:a.includes(qx[t])}}const Vx=qr().requestIdleCallback||(e=>setTimeout(e,1)),Gx=qr().cancelIdleCallback||(e=>clearTimeout(e)),Wx=(e=1e4)=>t=>{const s=Vx(t,{timeout:e});return()=>Gx(s)};function Kx(e){const{top:t,left:s,bottom:a,right:n}=e.getBoundingClientRect(),{innerHeight:i,innerWidth:l}=window;return(t>0&&t0&&a0&&s0&&n(t,s)=>{const a=new IntersectionObserver(n=>{for(const i of n)if(i.isIntersecting){a.disconnect(),t();break}},e);return s(n=>{if(n instanceof Element){if(Kx(n))return t(),a.disconnect(),!1;a.observe(n)}}),()=>a.disconnect()},Zx=e=>t=>{if(e){const s=matchMedia(e);if(s.matches)t();else return s.addEventListener("change",t,{once:!0}),()=>s.removeEventListener("change",t)}},Qx=(e=[])=>(t,s)=>{Ze(e)&&(e=[e]);let a=!1;const n=l=>{a||(a=!0,i(),t(),l.target.dispatchEvent(new l.constructor(l.type,l)))},i=()=>{s(l=>{for(const o of e)l.removeEventListener(o,n)})};return s(l=>{for(const o of e)l.addEventListener(o,n,{once:!0})}),i};function Yx(e,t){if(Ii(e)&&e.data==="["){let s=1,a=e.nextSibling;for(;a;){if(a.nodeType===1){if(t(a)===!1)break}else if(Ii(a))if(a.data==="]"){if(--s===0)break}else a.data==="["&&s++;a=a.nextSibling}}else t(e)}const sn=e=>!!e.type.__asyncLoader;function Xx(e){Ve(e)&&(e={loader:e});const{loader:t,loadingComponent:s,errorComponent:a,delay:n=200,hydrate:i,timeout:l,suspensible:o=!0,onError:r}=e;let c=null,d,u=0;const p=()=>(u++,c=null,m()),m=()=>{let h;return c||(h=c=t().catch(y=>{if(y=y instanceof Error?y:new Error(String(y)),r)return new Promise((k,A)=>{r(y,()=>k(p()),()=>A(y),u+1)});throw y}).then(y=>h!==c&&c?c:(y&&(y.__esModule||y[Symbol.toStringTag]==="Module")&&(y=y.default),d=y,y)))};return fo({name:"AsyncComponentWrapper",__asyncLoader:m,__asyncHydrate(h,y,k){let A=!1;(y.bu||(y.bu=[])).push(()=>A=!0);const g=()=>{A||k()},b=i?()=>{const x=i(g,w=>Yx(h,w));x&&(y.bum||(y.bum=[])).push(x)}:g;d?b():m().then(()=>!y.isUnmounted&&b())},get __asyncResolved(){return d},setup(){const h=fs;if(iu(h),d)return()=>Do(d,h);const y=w=>{c=null,ci(w,h,13,!a)};if(o&&h.suspense||ai)return m().then(w=>()=>Do(w,h)).catch(w=>(y(w),()=>a?Bt(a,{error:w}):null));const k=f(!1),A=f(),g=f(!!n);let b,x;return wt(()=>{b!=null&&clearTimeout(b),x!=null&&clearTimeout(x)}),n&&(x=setTimeout(()=>{h.isUnmounted||(g.value=!1)},n)),l!=null&&(b=setTimeout(()=>{if(!h.isUnmounted&&!k.value&&!A.value){const w=new Error(`Async component timed out after ${l}ms.`);y(w),A.value=w}},l)),m().then(()=>{h.isUnmounted||(k.value=!0,h.parent&&mo(h.parent.vnode)&&h.parent.update())}).catch(w=>{if(h.isUnmounted){c=null;return}y(w),A.value=w}),()=>{if(k.value&&d)return Do(d,h);if(A.value&&a)return Bt(a,{error:A.value});if(s&&!g.value)return Do(s,h)}}})}function Do(e,t){const{ref:s,props:a,children:n,ce:i}=t.vnode,l=Bt(e,a,n);return l.ref=s,l.ce=i,delete t.vnode.ce,l}const mo=e=>e.type.__isKeepAlive,e0={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){const s=Ds(),a=s.ctx;if(!a.renderer)return()=>{const g=t.default&&t.default();return g&&g.length===1?g[0]:g};const n=new Map,i=new Set;let l=null;const o=s.suspense,{renderer:{p:r,m:c,um:d,o:{createElement:u}}}=a,p=u("div");a.activate=(g,b,x,w,_)=>{const T=g.component;c(g,b,x,0,o),r(T.vnode,g,b,x,T,o,w,g.slotScopeIds,_),Qt(()=>{T.isDeactivated=!1,T.a&&Di(T.a);const L=g.props&&g.props.onVnodeMounted;L&&Bs(L,T.parent,g)},o)},a.deactivate=g=>{const b=g.component;hr(b.m),hr(b.a),c(g,p,null,1,o),Qt(()=>{b.da&&Di(b.da);const x=g.props&&g.props.onVnodeUnmounted;x&&Bs(x,b.parent,g),b.isDeactivated=!0},o)};function m(g){Dc(g),d(g,s,o,!0)}function h(g){n.forEach((b,x)=>{const w=bd(sn(b)?b.type.__asyncResolved||{}:b.type);w&&!g(w)&&y(x)})}function y(g){const b=n.get(g);b&&(!l||!fa(b,l))?m(b):l&&Dc(l),n.delete(g),i.delete(g)}Gt(()=>[e.include,e.exclude],([g,b])=>{g&&h(x=>El(g,x)),b&&h(x=>!El(b,x))},{flush:"post",deep:!0});let k=null;const A=()=>{k!=null&&(gr(s.subTree.type)?Qt(()=>{n.set(k,Po(s.subTree))},s.subTree.suspense):n.set(k,Po(s.subTree)))};return st(A),Xr(A),ec(()=>{n.forEach(g=>{const{subTree:b,suspense:x}=s,w=Po(b);if(g.type===w.type&&g.key===w.key){Dc(w);const _=w.component.da;_&&Qt(_,x);return}m(g)})}),()=>{if(k=null,!t.default)return l=null;const g=t.default(),b=g[0];if(g.length>1)return l=null,g;if(!cn(b)||!(b.shapeFlag&4)&&!(b.shapeFlag&128))return l=null,b;let x=Po(b);if(x.type===Zt)return l=null,x;const w=x.type,_=bd(sn(x)?x.type.__asyncResolved||{}:w),{include:T,exclude:L,max:E}=e;if(T&&(!_||!El(T,_))||L&&_&&El(L,_))return x.shapeFlag&=-257,l=x,b;const F=x.key==null?w:x.key,O=n.get(F);return x.el&&(x=Ma(x),b.shapeFlag&128&&(b.ssContent=x)),k=F,O?(x.el=O.el,x.component=O.component,x.transition&&rn(x,x.transition),x.shapeFlag|=512,i.delete(F),i.add(F)):(i.add(F),E&&i.size>parseInt(E,10)&&y(i.values().next().value)),x.shapeFlag|=256,l=x,gr(b.type)?b:x}}},t0=e0;function El(e,t){return Me(e)?e.some(s=>El(s,t)):Ze(e)?e.split(",").includes(t):uy(e)?(e.lastIndex=0,e.test(t)):!1}function cs(e,t){hv(e,"a",t)}function es(e,t){hv(e,"da",t)}function hv(e,t,s=fs){const a=e.__wdc||(e.__wdc=()=>{let n=s;for(;n;){if(n.isDeactivated)return;n=n.parent}return e()});if(Yr(t,a,s),s){let n=s.parent;for(;n&&n.parent;)mo(n.parent.vnode)&&s0(a,t,s,n),n=n.parent}}function s0(e,t,s,a){const n=Yr(t,e,a,!0);wt(()=>{Gd(a[t],n)},s)}function Dc(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Po(e){return e.shapeFlag&128?e.ssContent:e}function Yr(e,t,s=fs,a=!1){if(s){const n=s[e]||(s[e]=[]),i=t.__weh||(t.__weh=(...l)=>{ln();const o=sl(s),r=Ys(t,s,e,l);return o(),on(),r});return a?n.unshift(i):n.push(i),i}}const dn=e=>(t,s=fs)=>{(!ai||e==="sp")&&Yr(e,(...a)=>t(...a),s)},gv=dn("bm"),st=dn("m"),lu=dn("bu"),Xr=dn("u"),ec=dn("bum"),wt=dn("um"),bv=dn("sp"),yv=dn("rtg"),xv=dn("rtc");function _v(e,t=fs){Yr("ec",e,t)}const ou="components",a0="directives";function n0(e,t){return ru(ou,e,!0,t)||e}const wv=Symbol.for("v-ndc");function i0(e){return Ze(e)?ru(ou,e,!1)||e:e||wv}function l0(e){return ru(a0,e)}function ru(e,t,s=!0,a=!1){const n=ms||fs;if(n){const i=n.type;if(e===ou){const o=bd(i,!1);if(o&&(o===t||o===Tt(t)||o===ri(Tt(t))))return i}const l=Ep(n[e]||i[e],t)||Ep(n.appContext[e],t);return!l&&a?i:l}}function Ep(e,t){return e&&(e[t]||e[Tt(t)]||e[ri(Tt(t))])}function o0(e,t,s,a){let n;const i=s&&s[a],l=Me(e);if(l||Ze(e)){const o=l&&tn(e);let r=!1,c=!1;o&&(r=!qs(e),c=Na(e),e=Wr(e)),n=new Array(e.length);for(let d=0,u=e.length;dt(o,r,void 0,i&&i[r]));else{const o=Object.keys(e);n=new Array(o.length);for(let r=0,c=o.length;r{const i=a.fn(...n);return i&&(i.key=a.key),i}:a.fn)}return e}function c0(e,t,s={},a,n){if(ms.ce||ms.parent&&sn(ms.parent)&&ms.parent.ce){const c=Object.keys(s).length>0;return t!=="default"&&(s.name=t),Jl(),br(rs,null,[Bt("slot",s,a&&a())],c?-2:64)}let i=e[t];i&&i._c&&(i._d=!1),Jl();const l=i&&cu(i(s)),o=s.key||l&&l.key,r=br(rs,{key:(o&&!Cs(o)?o:`_${t}`)+(!l&&a?"_fb":"")},l||(a?a():[]),l&&e._===1?64:-2);return!n&&r.scopeId&&(r.slotScopeIds=[r.scopeId+"-s"]),i&&i._c&&(i._d=!0),r}function cu(e){return e.some(t=>cn(t)?!(t.type===Zt||t.type===rs&&!cu(t.children)):!0)?e:null}function d0(e,t){const s={};for(const a in e)s[t&&/[A-Z]/.test(a)?`on:${a}`:Mi(a)]=e[a];return s}const dd=e=>e?Yv(e)?vo(e):dd(e.parent):null,Ml=lt(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>dd(e.parent),$root:e=>dd(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>du(e),$forceUpdate:e=>e.f||(e.f=()=>{tu(e.update)}),$nextTick:e=>e.n||(e.n=Ut.bind(e.proxy)),$watch:e=>Nx.bind(e)}),Pc=(e,t)=>e!==ct&&!e.__isScriptSetup&&bt(e,t),ud={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:a,data:n,props:i,accessCache:l,type:o,appContext:r}=e;if(t[0]!=="$"){const p=l[t];if(p!==void 0)switch(p){case 1:return a[t];case 2:return n[t];case 4:return s[t];case 3:return i[t]}else{if(Pc(a,t))return l[t]=1,a[t];if(n!==ct&&bt(n,t))return l[t]=2,n[t];if(bt(i,t))return l[t]=3,i[t];if(s!==ct&&bt(s,t))return l[t]=4,s[t];pd&&(l[t]=0)}}const c=Ml[t];let d,u;if(c)return t==="$attrs"&&ws(e.attrs,"get",""),c(e);if((d=o.__cssModules)&&(d=d[t]))return d;if(s!==ct&&bt(s,t))return l[t]=4,s[t];if(u=r.config.globalProperties,bt(u,t))return u[t]},set({_:e},t,s){const{data:a,setupState:n,ctx:i}=e;return Pc(n,t)?(n[t]=s,!0):a!==ct&&bt(a,t)?(a[t]=s,!0):bt(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:a,appContext:n,props:i,type:l}},o){let r;return!!(s[o]||e!==ct&&o[0]!=="$"&&bt(e,o)||Pc(t,o)||bt(i,o)||bt(a,o)||bt(Ml,o)||bt(n.config.globalProperties,o)||(r=l.__cssModules)&&r[o])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:bt(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}},u0=lt({},ud,{get(e,t){if(t!==Symbol.unscopables)return ud.get(e,t,e)},has(e,t){return t[0]!=="_"&&!by(t)}});function p0(){return null}function f0(){return null}function m0(e){}function v0(e){}function h0(){return null}function g0(){}function b0(e,t){return null}function y0(){return kv().slots}function x0(){return kv().attrs}function kv(e){const t=Ds();return t.setupContext||(t.setupContext=sh(t))}function Wl(e){return Me(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}function _0(e,t){const s=Wl(e);for(const a in t){if(a.startsWith("__skip"))continue;let n=s[a];n?Me(n)||Ve(n)?n=s[a]={type:n,default:t[a]}:n.default=t[a]:n===null&&(n=s[a]={default:t[a]}),n&&t[`__skip_${a}`]&&(n.skipFactory=!0)}return s}function w0(e,t){return!e||!t?e||t:Me(e)&&Me(t)?e.concat(t):lt({},Wl(e),Wl(t))}function k0(e,t){const s={};for(const a in e)t.includes(a)||Object.defineProperty(s,a,{enumerable:!0,get:()=>e[a]});return s}function S0(e){const t=Ds(),s=ai;let a=e();Ql(),s&&Bi(!1);const n=()=>{sl(t),s&&Bi(!0)},i=()=>{Ds()!==t&&t.scope.off(),Ql(),s&&Bi(!1)};return Wd(a)&&(a=a.catch(l=>{throw n(),Promise.resolve().then(()=>Promise.resolve().then(i)),l})),[a,()=>{n(),Promise.resolve().then(i)}]}let pd=!0;function C0(e){const t=du(e),s=e.proxy,a=e.ctx;pd=!1,t.beforeCreate&&Ap(t.beforeCreate,e,"bc");const{data:n,computed:i,methods:l,watch:o,provide:r,inject:c,created:d,beforeMount:u,mounted:p,beforeUpdate:m,updated:h,activated:y,deactivated:k,beforeDestroy:A,beforeUnmount:g,destroyed:b,unmounted:x,render:w,renderTracked:_,renderTriggered:T,errorCaptured:L,serverPrefetch:E,expose:F,inheritAttrs:O,components:I,directives:H,filters:te}=t;if(c&&T0(c,a,null),l)for(const M in l){const $=l[M];Ve($)&&(a[M]=$.bind(s))}if(n){const M=n.call(s,s);ht(M)&&(e.data=Mn(M))}if(pd=!0,i)for(const M in i){const $=i[M],re=Ve($)?$.bind(s,s):Ve($.get)?$.get.bind(s,s):vs,ce=!Ve($)&&Ve($.set)?$.set.bind(s):vs,Z=B({get:re,set:ce});Object.defineProperty(a,M,{enumerable:!0,configurable:!0,get:()=>Z.value,set:Q=>Z.value=Q})}if(o)for(const M in o)Sv(o[M],a,s,M);if(r){const M=Ve(r)?r.call(s):r;Reflect.ownKeys(M).forEach($=>{Nl($,M[$])})}d&&Ap(d,e,"c");function N(M,$){Me($)?$.forEach(re=>M(re.bind(s))):$&&M($.bind(s))}if(N(gv,u),N(st,p),N(lu,m),N(Xr,h),N(cs,y),N(es,k),N(_v,L),N(xv,_),N(yv,T),N(ec,g),N(wt,x),N(bv,E),Me(F))if(F.length){const M=e.exposed||(e.exposed={});F.forEach($=>{Object.defineProperty(M,$,{get:()=>s[$],set:re=>s[$]=re,enumerable:!0})})}else e.exposed||(e.exposed={});w&&e.render===vs&&(e.render=w),O!=null&&(e.inheritAttrs=O),I&&(e.components=I),H&&(e.directives=H),E&&iu(e)}function T0(e,t,s=vs){Me(e)&&(e=fd(e));for(const a in e){const n=e[a];let i;ht(n)?"default"in n?i=ra(n.from||a,n.default,!0):i=ra(n.from||a):i=ra(n),Xt(i)?Object.defineProperty(t,a,{enumerable:!0,configurable:!0,get:()=>i.value,set:l=>i.value=l}):t[a]=i}}function Ap(e,t,s){Ys(Me(e)?e.map(a=>a.bind(t.proxy)):e.bind(t.proxy),t,s)}function Sv(e,t,s,a){let n=a.includes(".")?cv(s,a):()=>s[a];if(Ze(e)){const i=t[e];Ve(i)&&Gt(n,i)}else if(Ve(e))Gt(n,e.bind(s));else if(ht(e))if(Me(e))e.forEach(i=>Sv(i,t,s,a));else{const i=Ve(e.handler)?e.handler.bind(s):t[e.handler];Ve(i)&&Gt(n,i,e)}}function du(e){const t=e.type,{mixins:s,extends:a}=t,{mixins:n,optionsCache:i,config:{optionMergeStrategies:l}}=e.appContext,o=i.get(t);let r;return o?r=o:!n.length&&!s&&!a?r=t:(r={},n.length&&n.forEach(c=>vr(r,c,l,!0)),vr(r,t,l)),ht(t)&&i.set(t,r),r}function vr(e,t,s,a=!1){const{mixins:n,extends:i}=t;i&&vr(e,i,s,!0),n&&n.forEach(l=>vr(e,l,s,!0));for(const l in t)if(!(a&&l==="expose")){const o=E0[l]||s&&s[l];e[l]=o?o(e[l],t[l]):t[l]}return e}const E0={data:Rp,props:Ip,emits:Ip,methods:Al,computed:Al,beforeCreate:Is,created:Is,beforeMount:Is,mounted:Is,beforeUpdate:Is,updated:Is,beforeDestroy:Is,beforeUnmount:Is,destroyed:Is,unmounted:Is,activated:Is,deactivated:Is,errorCaptured:Is,serverPrefetch:Is,components:Al,directives:Al,watch:R0,provide:Rp,inject:A0};function Rp(e,t){return t?e?function(){return lt(Ve(e)?e.call(this,this):e,Ve(t)?t.call(this,this):t)}:t:e}function A0(e,t){return Al(fd(e),fd(t))}function fd(e){if(Me(e)){const t={};for(let s=0;s{let d,u=ct,p;return rv(()=>{const m=e[n];os(d,m)&&(d=m,c())}),{get(){return r(),s.get?s.get(d):d},set(m){const h=s.set?s.set(m):m;if(!os(h,d)&&!(u!==ct&&os(m,u)))return;const y=a.vnode.props,k=!!(y&&(t in y||n in y||i in y)&&(`onUpdate:${t}`in y||`onUpdate:${n}`in y||`onUpdate:${i}`in y));k||(d=m,c()),a.emit(`update:${t}`,h),os(m,u)&&(os(m,h)&&!os(h,p)||k&&u!==ct&&!os(h,d))&&c(),u=m,p=h}}});return o[Symbol.iterator]=()=>{let r=0;return{next(){return r<2?{value:r++?l||ct:o,done:!1}:{done:!0}}}},o}const Tv=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Tt(t)}Modifiers`]||e[`${Hs(t)}Modifiers`];function N0(e,t,...s){if(e.isUnmounted)return;const a=e.vnode.props||ct;let n=s;const i=t.startsWith("update:"),l=i&&Tv(a,t.slice(7));l&&(l.trim&&(n=s.map(d=>Ze(d)?d.trim():d)),l.number&&(n=s.map(jr)));let o,r=a[o=Mi(t)]||a[o=Mi(Tt(t))];!r&&i&&(r=a[o=Mi(Hs(t))]),r&&Ys(r,e,6,n);const c=a[o+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[o])return;e.emitted[o]=!0,Ys(c,e,6,n)}}const M0=new WeakMap;function Ev(e,t,s=!1){const a=s?M0:t.emitsCache,n=a.get(e);if(n!==void 0)return n;const i=e.emits;let l={},o=!1;if(!Ve(e)){const r=c=>{const d=Ev(c,t,!0);d&&(o=!0,lt(l,d))};!s&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!i&&!o?(ht(e)&&a.set(e,null),null):(Me(i)?i.forEach(r=>l[r]=null):lt(l,i),ht(e)&&a.set(e,l),l)}function tc(e,t){return!e||!li(t)?!1:(t=t.slice(2).replace(/Once$/,""),bt(e,t[0].toLowerCase()+t.slice(1))||bt(e,Hs(t))||bt(e,t))}function er(e){const{type:t,vnode:s,proxy:a,withProxy:n,propsOptions:[i],slots:l,attrs:o,emit:r,render:c,renderCache:d,props:u,data:p,setupState:m,ctx:h,inheritAttrs:y}=e,k=Gl(e);let A,g;try{if(s.shapeFlag&4){const x=n||a,w=x;A=zs(c.call(w,x,d,u,m,p,h)),g=o}else{const x=t;A=zs(x.length>1?x(u,{attrs:o,slots:l,emit:r}):x(u,null)),g=t.props?o:P0(o)}}catch(x){Dl.length=0,ci(x,e,1),A=Bt(Zt)}let b=A;if(g&&y!==!1){const x=Object.keys(g),{shapeFlag:w}=b;x.length&&w&7&&(i&&x.some(Br)&&(g=$0(g,i)),b=Ma(b,g,!1,!0))}return s.dirs&&(b=Ma(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(s.dirs):s.dirs),s.transition&&rn(b,s.transition),A=b,Gl(k),A}function D0(e,t=!0){let s;for(let a=0;a{let t;for(const s in e)(s==="class"||s==="style"||li(s))&&((t||(t={}))[s]=e[s]);return t},$0=(e,t)=>{const s={};for(const a in e)(!Br(a)||!(a.slice(9)in t))&&(s[a]=e[a]);return s};function F0(e,t,s){const{props:a,children:n,component:i}=e,{props:l,children:o,patchFlag:r}=t,c=i.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&r>=0){if(r&1024)return!0;if(r&16)return a?Op(a,l,c):!!l;if(r&8){const d=t.dynamicProps;for(let u=0;uObject.create(Rv),Ov=e=>Object.getPrototypeOf(e)===Rv;function B0(e,t,s,a=!1){const n={},i=Iv();e.propsDefaults=Object.create(null),Lv(e,t,n,i);for(const l in e.propsOptions[0])l in n||(n[l]=void 0);s?e.props=a?n:Yd(n):e.type.props?e.props=n:e.props=i,e.attrs=i}function U0(e,t,s,a){const{props:n,attrs:i,vnode:{patchFlag:l}}=e,o=ft(n),[r]=e.propsOptions;let c=!1;if((a||l>0)&&!(l&16)){if(l&8){const d=e.vnode.dynamicProps;for(let u=0;u{r=!0;const[p,m]=Nv(u,t,!0);lt(l,p),m&&o.push(...m)};!s&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}if(!i&&!r)return ht(e)&&a.set(e,Li),Li;if(Me(i))for(let d=0;de==="_"||e==="_ctx"||e==="$stable",pu=e=>Me(e)?e.map(zs):[zs(e)],H0=(e,t,s)=>{if(t._n)return t;const a=su((...n)=>pu(t(...n)),s);return a._c=!1,a},Mv=(e,t,s)=>{const a=e._ctx;for(const n in e){if(uu(n))continue;const i=e[n];if(Ve(i))t[n]=H0(n,i,a);else if(i!=null){const l=pu(i);t[n]=()=>l}}},Dv=(e,t)=>{const s=pu(t);e.slots.default=()=>s},Pv=(e,t,s)=>{for(const a in t)(s||!uu(a))&&(e[a]=t[a])},j0=(e,t,s)=>{const a=e.slots=Iv();if(e.vnode.shapeFlag&32){const n=t._;n?(Pv(a,t,s),s&&Am(a,"_",n,!0)):Mv(t,a)}else t&&Dv(e,t)},q0=(e,t,s)=>{const{vnode:a,slots:n}=e;let i=!0,l=ct;if(a.shapeFlag&32){const o=t._;o?s&&o===1?i=!1:Pv(n,t,s):(i=!t.$stable,Mv(t,n)),l=t}else t&&(Dv(e,t),l={default:1});if(i)for(const o in n)!uu(o)&&l[o]==null&&delete n[o]},Qt=qv;function $v(e){return Bv(e)}function Fv(e){return Bv(e,jx)}function Bv(e,t){const s=qr();s.__VUE__=!0;const{insert:a,remove:n,patchProp:i,createElement:l,createText:o,createComment:r,setText:c,setElementText:d,parentNode:u,nextSibling:p,setScopeId:m=vs,insertStaticContent:h}=e,y=(S,P,G,he=null,fe=null,me=null,we=void 0,J=null,ie=!!P.dynamicChildren)=>{if(S===P)return;S&&!fa(S,P)&&(he=le(S),Q(S,fe,me,!0),S=null),P.patchFlag===-2&&(ie=!1,P.dynamicChildren=null);const{type:X,ref:_e,shapeFlag:U}=P;switch(X){case On:k(S,P,G,he);break;case Zt:A(S,P,G,he);break;case Xn:S==null&&g(P,G,he,we);break;case rs:I(S,P,G,he,fe,me,we,J,ie);break;default:U&1?w(S,P,G,he,fe,me,we,J,ie):U&6?H(S,P,G,he,fe,me,we,J,ie):(U&64||U&128)&&X.process(S,P,G,he,fe,me,we,J,ie,j)}_e!=null&&fe?$i(_e,S&&S.ref,me,P||S,!P):_e==null&&S&&S.ref!=null&&$i(S.ref,null,me,S,!0)},k=(S,P,G,he)=>{if(S==null)a(P.el=o(P.children),G,he);else{const fe=P.el=S.el;P.children!==S.children&&c(fe,P.children)}},A=(S,P,G,he)=>{S==null?a(P.el=r(P.children||""),G,he):P.el=S.el},g=(S,P,G,he)=>{[S.el,S.anchor]=h(S.children,P,G,he,S.el,S.anchor)},b=({el:S,anchor:P},G,he)=>{let fe;for(;S&&S!==P;)fe=p(S),a(S,G,he),S=fe;a(P,G,he)},x=({el:S,anchor:P})=>{let G;for(;S&&S!==P;)G=p(S),n(S),S=G;n(P)},w=(S,P,G,he,fe,me,we,J,ie)=>{if(P.type==="svg"?we="svg":P.type==="math"&&(we="mathml"),S==null)_(P,G,he,fe,me,we,J,ie);else{const X=S.el&&S.el._isVueCE?S.el:null;try{X&&X._beginPatch(),E(S,P,fe,me,we,J,ie)}finally{X&&X._endPatch()}}},_=(S,P,G,he,fe,me,we,J)=>{let ie,X;const{props:_e,shapeFlag:U,transition:de,dirs:ge}=S;if(ie=S.el=l(S.type,me,_e&&_e.is,_e),U&8?d(ie,S.children):U&16&&L(S.children,ie,null,he,fe,$c(S,me),we,J),ge&&Ia(S,null,he,"created"),T(ie,S,S.scopeId,we,he),_e){for(const $e in _e)$e!=="value"&&!en($e)&&i(ie,$e,null,_e[$e],me,he);"value"in _e&&i(ie,"value",null,_e.value,me),(X=_e.onVnodeBeforeMount)&&Bs(X,he,S)}ge&&Ia(S,null,he,"beforeMount");const Ae=Uv(fe,de);Ae&&de.beforeEnter(ie),a(ie,P,G),((X=_e&&_e.onVnodeMounted)||Ae||ge)&&Qt(()=>{try{X&&Bs(X,he,S),Ae&&de.enter(ie),ge&&Ia(S,null,he,"mounted")}finally{}},fe)},T=(S,P,G,he,fe)=>{if(G&&m(S,G),he)for(let me=0;me{for(let X=ie;X{const J=P.el=S.el;let{patchFlag:ie,dynamicChildren:X,dirs:_e}=P;ie|=S.patchFlag&16;const U=S.props||ct,de=P.props||ct;let ge;if(G&&Hn(G,!1),(ge=de.onVnodeBeforeUpdate)&&Bs(ge,G,P,S),_e&&Ia(P,S,G,"beforeUpdate"),G&&Hn(G,!0),(U.innerHTML&&de.innerHTML==null||U.textContent&&de.textContent==null)&&d(J,""),X?F(S.dynamicChildren,X,J,G,he,$c(P,fe),me):we||$(S,P,J,null,G,he,$c(P,fe),me,!1),ie>0){if(ie&16)O(J,U,de,G,fe);else if(ie&2&&U.class!==de.class&&i(J,"class",null,de.class,fe),ie&4&&i(J,"style",U.style,de.style,fe),ie&8){const Ae=P.dynamicProps;for(let $e=0;$e{ge&&Bs(ge,G,P,S),_e&&Ia(P,S,G,"updated")},he)},F=(S,P,G,he,fe,me,we)=>{for(let J=0;J{if(P!==G){if(P!==ct)for(const me in P)!en(me)&&!(me in G)&&i(S,me,P[me],null,fe,he);for(const me in G){if(en(me))continue;const we=G[me],J=P[me];we!==J&&me!=="value"&&i(S,me,J,we,fe,he)}"value"in G&&i(S,"value",P.value,G.value,fe)}},I=(S,P,G,he,fe,me,we,J,ie)=>{const X=P.el=S?S.el:o(""),_e=P.anchor=S?S.anchor:o("");let{patchFlag:U,dynamicChildren:de,slotScopeIds:ge}=P;ge&&(J=J?J.concat(ge):ge),S==null?(a(X,G,he),a(_e,G,he),L(P.children||[],G,_e,fe,me,we,J,ie)):U>0&&U&64&&de&&S.dynamicChildren&&S.dynamicChildren.length===de.length?(F(S.dynamicChildren,de,G,fe,me,we,J),(P.key!=null||fe&&P===fe.subTree)&&fu(S,P,!0)):$(S,P,G,_e,fe,me,we,J,ie)},H=(S,P,G,he,fe,me,we,J,ie)=>{P.slotScopeIds=J,S==null?P.shapeFlag&512?fe.ctx.activate(P,G,he,we,ie):te(P,G,he,fe,me,we,ie):ee(S,P,ie)},te=(S,P,G,he,fe,me,we)=>{const J=S.component=Qv(S,he,fe);if(mo(S)&&(J.ctx.renderer=j),Xv(J,!1,we),J.asyncDep){if(fe&&fe.registerDep(J,N,we),!S.el){const ie=J.subTree=Bt(Zt);A(null,ie,P,G),S.placeholder=ie.el}}else N(J,S,P,G,fe,me,we)},ee=(S,P,G)=>{const he=P.component=S.component;if(F0(S,P,G))if(he.asyncDep&&!he.asyncResolved){M(he,P,G);return}else he.next=P,he.update();else P.el=S.el,he.vnode=P},N=(S,P,G,he,fe,me,we)=>{const J=()=>{if(S.isMounted){let{next:U,bu:de,u:ge,parent:Ae,vnode:$e}=S;{const Qe=zv(S);if(Qe){U&&(U.el=$e.el,M(S,U,we)),Qe.asyncDep.then(()=>{Qt(()=>{S.isUnmounted||X()},fe)});return}}let We=U,je;Hn(S,!1),U?(U.el=$e.el,M(S,U,we)):U=$e,de&&Di(de),(je=U.props&&U.props.onVnodeBeforeUpdate)&&Bs(je,Ae,U,$e),Hn(S,!0);const Xe=er(S),it=S.subTree;S.subTree=Xe,y(it,Xe,u(it.el),le(it),S,fe,me),U.el=Xe.el,We===null&&sc(S,Xe.el),ge&&Qt(ge,fe),(je=U.props&&U.props.onVnodeUpdated)&&Qt(()=>Bs(je,Ae,U,$e),fe)}else{let U;const{el:de,props:ge}=P,{bm:Ae,m:$e,parent:We,root:je,type:Xe}=S,it=sn(P);if(Hn(S,!1),Ae&&Di(Ae),!it&&(U=ge&&ge.onVnodeBeforeMount)&&Bs(U,We,P),Hn(S,!0),de&&xe){const Qe=()=>{S.subTree=er(S),xe(de,S.subTree,S,fe,null)};it&&Xe.__asyncHydrate?Xe.__asyncHydrate(de,S,Qe):Qe()}else{je.ce&&je.ce._hasShadowRoot()&&je.ce._injectChildStyle(Xe,S.parent?S.parent.type:void 0);const Qe=S.subTree=er(S);y(null,Qe,G,he,S,fe,me),P.el=Qe.el}if($e&&Qt($e,fe),!it&&(U=ge&&ge.onVnodeMounted)){const Qe=P;Qt(()=>Bs(U,We,Qe),fe)}(P.shapeFlag&256||We&&sn(We.vnode)&&We.vnode.shapeFlag&256)&&S.a&&Qt(S.a,fe),S.isMounted=!0,P=G=he=null}};S.scope.on();const ie=S.effect=new zl(J);S.scope.off();const X=S.update=ie.run.bind(ie),_e=S.job=ie.runIfDirty.bind(ie);_e.i=S,_e.id=S.uid,ie.scheduler=()=>tu(_e),Hn(S,!0),X()},M=(S,P,G)=>{P.component=S;const he=S.vnode.props;S.vnode=P,S.next=null,U0(S,P.props,he,G),q0(S,P.children,G),ln(),yp(S),on()},$=(S,P,G,he,fe,me,we,J,ie=!1)=>{const X=S&&S.children,_e=S?S.shapeFlag:0,U=P.children,{patchFlag:de,shapeFlag:ge}=P;if(de>0){if(de&128){ce(X,U,G,he,fe,me,we,J,ie);return}else if(de&256){re(X,U,G,he,fe,me,we,J,ie);return}}ge&8?(_e&16&&Oe(X,fe,me),U!==X&&d(G,U)):_e&16?ge&16?ce(X,U,G,he,fe,me,we,J,ie):Oe(X,fe,me,!0):(_e&8&&d(G,""),ge&16&&L(U,G,he,fe,me,we,J,ie))},re=(S,P,G,he,fe,me,we,J,ie)=>{S=S||Li,P=P||Li;const X=S.length,_e=P.length,U=Math.min(X,_e);let de;for(de=0;de_e?Oe(S,fe,me,!0,!1,U):L(P,G,he,fe,me,we,J,ie,U)},ce=(S,P,G,he,fe,me,we,J,ie)=>{let X=0;const _e=P.length;let U=S.length-1,de=_e-1;for(;X<=U&&X<=de;){const ge=S[X],Ae=P[X]=ie?Wa(P[X]):zs(P[X]);if(fa(ge,Ae))y(ge,Ae,G,null,fe,me,we,J,ie);else break;X++}for(;X<=U&&X<=de;){const ge=S[U],Ae=P[de]=ie?Wa(P[de]):zs(P[de]);if(fa(ge,Ae))y(ge,Ae,G,null,fe,me,we,J,ie);else break;U--,de--}if(X>U){if(X<=de){const ge=de+1,Ae=ge<_e?P[ge].el:he;for(;X<=de;)y(null,P[X]=ie?Wa(P[X]):zs(P[X]),G,Ae,fe,me,we,J,ie),X++}}else if(X>de)for(;X<=U;)Q(S[X],fe,me,!0),X++;else{const ge=X,Ae=X,$e=new Map;for(X=Ae;X<=de;X++){const Le=P[X]=ie?Wa(P[X]):zs(P[X]);Le.key!=null&&$e.set(Le.key,X)}let We,je=0;const Xe=de-Ae+1;let it=!1,Qe=0;const se=new Array(Xe);for(X=0;X=Xe){Q(Le,fe,me,!0);continue}let Te;if(Le.key!=null)Te=$e.get(Le.key);else for(We=Ae;We<=de;We++)if(se[We-Ae]===0&&fa(Le,P[We])){Te=We;break}Te===void 0?Q(Le,fe,me,!0):(se[Te-Ae]=X+1,Te>=Qe?Qe=Te:it=!0,y(Le,P[Te],G,null,fe,me,we,J,ie),je++)}const Re=it?V0(se):Li;for(We=Re.length-1,X=Xe-1;X>=0;X--){const Le=Ae+X,Te=P[Le],W=P[Le+1],De=Le+1<_e?W.el||Hv(W):he;se[X]===0?y(null,Te,G,De,fe,me,we,J,ie):it&&(We<0||X!==Re[We]?Z(Te,G,De,2):We--)}}},Z=(S,P,G,he,fe=null)=>{const{el:me,type:we,transition:J,children:ie,shapeFlag:X}=S;if(X&6){Z(S.component.subTree,P,G,he);return}if(X&128){S.suspense.move(P,G,he);return}if(X&64){we.move(S,P,G,j);return}if(we===rs){a(me,P,G);for(let U=0;UJ.enter(me),fe));else{const{leave:U,delayLeave:de,afterLeave:ge}=J,Ae=()=>{S.ctx.isUnmounted?n(me):a(me,P,G)},$e=()=>{const We=me._isLeaving||!!me[na];me._isLeaving&&me[na](!0),J.persisted&&!We?Ae():U(me,()=>{Ae(),ge&&ge()})};de?de(me,Ae,$e):$e()}else a(me,P,G)},Q=(S,P,G,he=!1,fe=!1)=>{const{type:me,props:we,ref:J,children:ie,dynamicChildren:X,shapeFlag:_e,patchFlag:U,dirs:de,cacheIndex:ge,memo:Ae}=S;if(U===-2&&(fe=!1),J!=null&&(ln(),$i(J,null,G,S,!0),on()),ge!=null&&(P.renderCache[ge]=void 0),_e&256){P.ctx.deactivate(S);return}const $e=_e&1&&de,We=!sn(S);let je;if(We&&(je=we&&we.onVnodeBeforeUnmount)&&Bs(je,P,S),_e&6)be(S.component,G,he);else{if(_e&128){S.suspense.unmount(G,he);return}$e&&Ia(S,null,P,"beforeUnmount"),_e&64?S.type.remove(S,P,G,j,he):X&&!X.hasOnce&&(me!==rs||U>0&&U&64)?Oe(X,P,G,!1,!0):(me===rs&&U&384||!fe&&_e&16)&&Oe(ie,P,G),he&&ae(S)}const Xe=Ae!=null&&ge==null;(We&&(je=we&&we.onVnodeUnmounted)||$e||Xe)&&Qt(()=>{je&&Bs(je,P,S),$e&&Ia(S,null,P,"unmounted"),Xe&&(S.el=null)},G)},ae=S=>{const{type:P,el:G,anchor:he,transition:fe}=S;if(P===rs){V(G,he);return}if(P===Xn){x(S);return}const me=()=>{n(G),fe&&!fe.persisted&&fe.afterLeave&&fe.afterLeave()};if(S.shapeFlag&1&&fe&&!fe.persisted){const{leave:we,delayLeave:J}=fe,ie=()=>we(G,me);J?J(S.el,me,ie):ie()}else me()},V=(S,P)=>{let G;for(;S!==P;)G=p(S),n(S),S=G;n(P)},be=(S,P,G)=>{const{bum:he,scope:fe,job:me,subTree:we,um:J,m:ie,a:X}=S;hr(ie),hr(X),he&&Di(he),fe.stop(),me&&(me.flags|=8,Q(we,S,P,G)),J&&Qt(J,P),Qt(()=>{S.isUnmounted=!0},P)},Oe=(S,P,G,he=!1,fe=!1,me=0)=>{for(let we=me;we{if(S.shapeFlag&6)return le(S.component.subTree);if(S.shapeFlag&128)return S.suspense.next();const P=p(S.anchor||S.el),G=P&&P[dv];return G?p(G):P};let oe=!1;const ve=(S,P,G)=>{let he;S==null?P._vnode&&(Q(P._vnode,null,null,!0),he=P._vnode.component):y(P._vnode||null,S,P,null,null,null,G),P._vnode=S,oe||(oe=!0,yp(he),fr(),oe=!1)},j={p:y,um:Q,m:Z,r:ae,mt:te,mc:L,pc:$,pbc:F,n:le,o:e};let Y,xe;return t&&([Y,xe]=t(j)),{render:ve,hydrate:Y,createApp:O0(ve,Y)}}function $c({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function Hn({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Uv(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function fu(e,t,s=!1){const a=e.children,n=t.children;if(Me(a)&&Me(n))for(let i=0;i>1,e[s[o]]0&&(t[a]=s[i-1]),s[i]=a)}}for(i=s.length,l=s[i-1];i-- >0;)s[i]=l,l=t[l];return s}function zv(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:zv(t)}function hr(e){if(e)for(let t=0;te.__isSuspense;let vd=0;const G0={name:"Suspense",__isSuspense:!0,process(e,t,s,a,n,i,l,o,r,c){if(e==null)K0(t,s,a,n,i,l,o,r,c);else{if(i&&i.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}J0(e,t,s,a,n,l,o,r,c)}},hydrate:Z0,normalize:Q0},W0=G0;function Kl(e,t){const s=e.props&&e.props[t];Ve(s)&&s()}function K0(e,t,s,a,n,i,l,o,r){const{p:c,o:{createElement:d}}=r,u=d("div"),p=e.suspense=jv(e,n,a,t,u,s,i,l,o,r);c(null,p.pendingBranch=e.ssContent,u,null,a,p,i,l),p.deps>0?(Kl(e,"onPending"),Kl(e,"onFallback"),c(null,e.ssFallback,t,s,a,null,i,l),Fi(p,e.ssFallback)):p.resolve(!1,!0)}function J0(e,t,s,a,n,i,l,o,{p:r,um:c,o:{createElement:d}}){const u=t.suspense=e.suspense;u.vnode=t,t.el=e.el;const p=t.ssContent,m=t.ssFallback,{activeBranch:h,pendingBranch:y,isInFallback:k,isHydrating:A}=u;if(y)u.pendingBranch=p,fa(y,p)?(r(y,p,u.hiddenContainer,null,n,u,i,l,o),u.deps<=0?u.resolve():k&&(A||(r(h,m,s,a,n,null,i,l,o),Fi(u,m)))):(u.pendingId=vd++,A?(u.isHydrating=!1,u.activeBranch=y):c(y,n,u),u.deps=0,u.effects.length=0,u.hiddenContainer=d("div"),k?(r(null,p,u.hiddenContainer,null,n,u,i,l,o),u.deps<=0?u.resolve():(r(h,m,s,a,n,null,i,l,o),Fi(u,m))):h&&fa(h,p)?(r(h,p,s,a,n,u,i,l,o),u.resolve(!0)):(r(null,p,u.hiddenContainer,null,n,u,i,l,o),u.deps<=0&&u.resolve()));else if(h&&fa(h,p))r(h,p,s,a,n,u,i,l,o),Fi(u,p);else if(Kl(t,"onPending"),u.pendingBranch=p,p.shapeFlag&512?u.pendingId=p.component.suspenseId:u.pendingId=vd++,r(null,p,u.hiddenContainer,null,n,u,i,l,o),u.deps<=0)u.resolve();else{const{timeout:g,pendingId:b}=u;g>0?setTimeout(()=>{u.pendingId===b&&u.fallback(m)},g):g===0&&u.fallback(m)}}function jv(e,t,s,a,n,i,l,o,r,c,d=!1){const{p:u,m:p,um:m,n:h,o:{parentNode:y,remove:k}}=c;let A;const g=Y0(e);g&&t&&t.pendingBranch&&(A=t.pendingId,t.deps++);const b=e.props?rr(e.props.timeout):void 0,x=i,w={vnode:e,parent:t,parentComponent:s,namespace:l,container:a,hiddenContainer:n,deps:0,pendingId:vd++,timeout:typeof b=="number"?b:-1,activeBranch:null,isFallbackMountPending:!1,pendingBranch:null,isInFallback:!d,isHydrating:d,isUnmounted:!1,effects:[],resolve(_=!1,T=!1){const{vnode:L,activeBranch:E,pendingBranch:F,pendingId:O,effects:I,parentComponent:H,container:te,isInFallback:ee}=w;let N=!1;if(w.isHydrating)w.isHydrating=!1;else if(!_){N=E&&F.transition&&F.transition.mode==="out-in";let re=!1;N&&(E.transition.afterLeave=()=>{O===w.pendingId&&(p(F,te,i===x&&!re?h(E):i,0),ql(I),ee&&L.ssFallback&&(L.ssFallback.el=null))}),E&&!w.isFallbackMountPending&&(y(E.el)===te&&(i=h(E),re=!0),m(E,H,w,!0),!N&&ee&&L.ssFallback&&Qt(()=>L.ssFallback.el=null,w)),N||p(F,te,i,0)}w.isFallbackMountPending=!1,Fi(w,F),w.pendingBranch=null,w.isInFallback=!1;let M=w.parent,$=!1;for(;M;){if(M.pendingBranch){M.effects.push(...I),$=!0;break}M=M.parent}!$&&!N&&ql(I),w.effects=[],g&&t&&t.pendingBranch&&A===t.pendingId&&(t.deps--,t.deps===0&&!T&&t.resolve()),Kl(L,"onResolve")},fallback(_){if(!w.pendingBranch)return;const{vnode:T,activeBranch:L,parentComponent:E,container:F,namespace:O}=w;Kl(T,"onFallback");const I=h(L),H=()=>{w.isFallbackMountPending=!1,w.isInFallback&&(u(null,_,F,I,E,null,O,o,r),Fi(w,_))},te=_.transition&&_.transition.mode==="out-in";te&&(w.isFallbackMountPending=!0,L.transition.afterLeave=H),w.isInFallback=!0,m(L,E,null,!0),te||H()},move(_,T,L){w.activeBranch&&p(w.activeBranch,_,T,L),w.container=_},next(){return w.activeBranch&&h(w.activeBranch)},registerDep(_,T,L){const E=!!w.pendingBranch;E&&w.deps++;const F=_.vnode.el;_.asyncDep.catch(O=>{ci(O,_,0)}).then(O=>{if(_.isUnmounted||w.isUnmounted||w.pendingId!==_.suspenseId)return;Ql(),_.asyncResolved=!0;const{vnode:I}=_;hd(_,O,!1),F&&(I.el=F);const H=!F&&_.subTree.el;T(_,I,y(F||_.subTree.el),F?null:h(_.subTree),w,l,L),H&&(I.placeholder=null,k(H)),sc(_,I.el),E&&--w.deps===0&&w.resolve()})},unmount(_,T){w.isUnmounted=!0,w.activeBranch&&m(w.activeBranch,s,_,T),w.pendingBranch&&m(w.pendingBranch,s,_,T)}};return w}function Z0(e,t,s,a,n,i,l,o,r){const c=t.suspense=jv(t,a,s,e.parentNode,document.createElement("div"),null,n,i,l,o,!0),d=r(e,c.pendingBranch=t.ssContent,s,c,i,l);return c.deps===0&&c.resolve(!1,!0),d}function Q0(e){const{shapeFlag:t,children:s}=e,a=t&32;e.ssContent=Np(a?s.default:s),e.ssFallback=a?Np(s.fallback):Bt(Zt)}function Np(e){let t;if(Ve(e)){const s=si&&e._c;s&&(e._d=!1,Jl()),e=e(),s&&(e._d=!0,t=ks,Vv())}return Me(e)&&(e=D0(e)),e=zs(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter(s=>s!==e)),e}function qv(e,t){t&&t.pendingBranch?Me(e)?t.effects.push(...e):t.effects.push(e):ql(e)}function Fi(e,t){e.activeBranch=t;const{vnode:s,parentComponent:a}=e;let n=t.el;for(;!n&&t.component;)t=t.component.subTree,n=t.el;s.el=n,a&&a.subTree===s&&(a.vnode.el=n,sc(a,n))}function Y0(e){const t=e.props&&e.props.suspensible;return t!=null&&t!==!1}const rs=Symbol.for("v-fgt"),On=Symbol.for("v-txt"),Zt=Symbol.for("v-cmt"),Xn=Symbol.for("v-stc"),Dl=[];let ks=null;function Jl(e=!1){Dl.push(ks=e?null:[])}function Vv(){Dl.pop(),ks=Dl[Dl.length-1]||null}let si=1;function Zl(e,t=!1){si+=e,e<0&&ks&&t&&(ks.hasOnce=!0)}function Gv(e){return e.dynamicChildren=si>0?ks||Li:null,Vv(),si>0&&ks&&ks.push(e),e}function X0(e,t,s,a,n,i){return Gv(mu(e,t,s,a,n,i,!0))}function br(e,t,s,a,n){return Gv(Bt(e,t,s,a,n,!0))}function cn(e){return e?e.__v_isVNode===!0:!1}function fa(e,t){return e.type===t.type&&e.key===t.key}function e_(e){}const Wv=({key:e})=>e??null,tr=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?Ze(e)||Xt(e)||Ve(e)?{i:ms,r:e,k:t,f:!!s}:e:null);function mu(e,t=null,s=null,a=0,n=null,i=e===rs?0:1,l=!1,o=!1){const r={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Wv(t),ref:t&&tr(t),scopeId:Zr,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:a,dynamicProps:n,dynamicChildren:null,appContext:null,ctx:ms};return o?(hu(r,s),i&128&&e.normalize(r)):s&&(r.shapeFlag|=Ze(s)?8:16),si>0&&!l&&ks&&(r.patchFlag>0||i&6)&&r.patchFlag!==32&&ks.push(r),r}const Bt=t_;function t_(e,t=null,s=null,a=0,n=null,i=!1){if((!e||e===wv)&&(e=Zt),cn(e)){const o=Ma(e,t,!0);return s&&hu(o,s),si>0&&!i&&ks&&(o.shapeFlag&6?ks[ks.indexOf(e)]=o:ks.push(o)),o.patchFlag=-2,o}if(r_(e)&&(e=e.__vccOpts),t){t=Kv(t);let{class:o,style:r}=t;o&&!Ze(o)&&(t.class=co(o)),ht(r)&&(uo(r)&&!Me(r)&&(r=lt({},r)),t.style=ro(r))}const l=Ze(e)?1:gr(e)?128:uv(e)?64:ht(e)?4:Ve(e)?2:0;return mu(e,t,s,a,n,l,i,!0)}function Kv(e){return e?uo(e)||Ov(e)?lt({},e):e:null}function Ma(e,t,s=!1,a=!1){const{props:n,ref:i,patchFlag:l,children:o,transition:r}=e,c=t?Zv(n||{},t):n,d={__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&Wv(c),ref:t&&t.ref?s&&i?Me(i)?i.concat(tr(t)):[i,tr(t)]:tr(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==rs?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:r,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Ma(e.ssContent),ssFallback:e.ssFallback&&Ma(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return r&&a&&rn(d,r.clone(d)),d}function vu(e=" ",t=0){return Bt(On,null,e,t)}function s_(e,t){const s=Bt(Xn,null,e);return s.staticCount=t,s}function Jv(e="",t=!1){return t?(Jl(),br(Zt,null,e)):Bt(Zt,null,e)}function zs(e){return e==null||typeof e=="boolean"?Bt(Zt):Me(e)?Bt(rs,null,e.slice()):cn(e)?Wa(e):Bt(On,null,String(e))}function Wa(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Ma(e)}function hu(e,t){let s=0;const{shapeFlag:a}=e;if(t==null)t=null;else if(Me(t))s=16;else if(typeof t=="object")if(a&65){const n=t.default;n&&(n._c&&(n._d=!1),hu(e,n()),n._c&&(n._d=!0));return}else{s=32;const n=t._;!n&&!Ov(t)?t._ctx=ms:n===3&&ms&&(ms.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else Ve(t)?(t={default:t,_ctx:ms},s=32):(t=String(t),a&64?(s=16,t=[vu(t)]):s=8);e.children=t,e.shapeFlag|=s}function Zv(...e){const t={};for(let s=0;sfs||ms;let yr,Bi;{const e=qr(),t=(s,a)=>{let n;return(n=e[s])||(n=e[s]=[]),n.push(a),i=>{n.length>1?n.forEach(l=>l(i)):n[0](i)}};yr=t("__VUE_INSTANCE_SETTERS__",s=>fs=s),Bi=t("__VUE_SSR_SETTERS__",s=>ai=s)}const sl=e=>{const t=fs;return yr(e),e.scope.on(),()=>{e.scope.off(),yr(t)}},Ql=()=>{fs&&fs.scope.off(),yr(null)};function Yv(e){return e.vnode.shapeFlag&4}let ai=!1;function Xv(e,t=!1,s=!1){t&&Bi(t);const{props:a,children:n}=e.vnode,i=Yv(e);B0(e,a,i,t),j0(e,n,s||t);const l=i?i_(e,t):void 0;return t&&Bi(!1),l}function i_(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,ud);const{setup:a}=s;if(a){ln();const n=e.setupContext=a.length>1?sh(e):null,i=sl(e),l=tl(a,e,0,[e.props,n]),o=Wd(l);if(on(),i(),(o||e.sp)&&!sn(e)&&iu(e),o){if(l.then(Ql,Ql),t)return l.then(r=>{hd(e,r,t)}).catch(r=>{ci(r,e,0)});e.asyncDep=l}else hd(e,l,t)}else th(e,t)}function hd(e,t,s){Ve(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ht(t)&&(e.setupState=eu(t)),th(e,s)}let xr,gd;function eh(e){xr=e,gd=t=>{t.render._rc&&(t.withProxy=new Proxy(t.ctx,u0))}}const l_=()=>!xr;function th(e,t,s){const a=e.type;if(!e.render){if(!t&&xr&&!a.render){const n=a.template||du(e).template;if(n){const{isCustomElement:i,compilerOptions:l}=e.appContext.config,{delimiters:o,compilerOptions:r}=a,c=lt(lt({isCustomElement:i,delimiters:o},l),r);a.render=xr(n,c)}}e.render=a.render||vs,gd&&gd(e)}{const n=sl(e);ln();try{C0(e)}finally{on(),n()}}}const o_={get(e,t){return ws(e,"get",""),e[t]}};function sh(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,o_),slots:e.slots,emit:e.emit,expose:t}}function vo(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(eu(Zm(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in Ml)return Ml[s](e)},has(t,s){return s in t||s in Ml}})):e.proxy}function bd(e,t=!0){return Ve(e)?e.displayName||e.name:e.name||t&&e.__name}function r_(e){return Ve(e)&&"__vccOpts"in e}const B=(e,t)=>mx(e,t,ai);function qi(e,t,s){try{Zl(-1);const a=arguments.length;return a===2?ht(t)&&!Me(t)?cn(t)?Bt(e,null,[t]):Bt(e,t):Bt(e,null,t):(a>3?s=Array.prototype.slice.call(arguments,2):a===3&&cn(s)&&(s=[s]),Bt(e,t,s))}finally{Zl(1)}}function c_(){}function d_(e,t,s,a){const n=s[a];if(n&&ah(n,e))return n;const i=t();return i.memo=e.slice(),i.cacheIndex=a,s[a]=i}function ah(e,t){const s=e.memo;if(s.length!=t.length)return!1;for(let a=0;a0&&ks&&ks.push(e),!0}const nh="3.5.38",u_=vs,p_=kx,f_=Ti,m_=iv,v_={createComponentInstance:Qv,setupComponent:Xv,renderComponentRoot:er,setCurrentRenderingInstance:Gl,isVNode:cn,normalizeVNode:zs,getComponentPublicInstance:vo,ensureValidVNode:cu,pushWarningContext:yx,popWarningContext:xx},h_=v_,g_=null,b_=null,y_=null;/** * @vue/runtime-dom v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/let ud;const Sp=typeof window<"u"&&window.trustedTypes;if(Sp)try{ud=Sp.createPolicy("vue",{createHTML:e=>e})}catch{}const Jh=ud?e=>ud.createHTML(e):e=>e,X0="http://www.w3.org/2000/svg",e_="http://www.w3.org/1998/Math/MathML",Ha=typeof document<"u"?document:null,Cp=Ha&&Ha.createElement("template"),Zh={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,a)=>{const n=t==="svg"?Ha.createElementNS(X0,e):t==="mathml"?Ha.createElementNS(e_,e):s?Ha.createElement(e,{is:s}):Ha.createElement(e);return e==="select"&&a&&a.multiple!=null&&n.setAttribute("multiple",a.multiple),n},createText:e=>Ha.createTextNode(e),createComment:e=>Ha.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ha.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,a,n,i){const l=s?s.previousSibling:t.lastChild;if(n&&(n===i||n.nextSibling))for(;t.insertBefore(n.cloneNode(!0),s),!(n===i||!(n=n.nextSibling)););else{Cp.innerHTML=Jh(a==="svg"?`${e}`:a==="mathml"?`${e}`:e);const o=Cp.content;if(a==="svg"||a==="mathml"){const r=o.firstChild;for(;r.firstChild;)o.appendChild(r.firstChild);o.removeChild(r)}t.insertBefore(o,s)}return[l?l.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},fn="transition",ul="animation",Hi=Symbol("_vtc"),Qh={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Yh=nt({},Zd,Qh),t_=e=>(e.displayName="Transition",e.props=Yh,e),s_=t_((e,{slots:t})=>zi(nh,Xh(e),t)),Fn=(e,t=[])=>{Le(e)?e.forEach(s=>s(...t)):e&&e(...t)},Tp=e=>e?Le(e)?e.some(t=>t.length>1):e.length>1:!1;function Xh(e){const t={};for(const I in e)I in Qh||(t[I]=e[I]);if(e.css===!1)return t;const{name:s="v",type:a,duration:n,enterFromClass:i=`${s}-enter-from`,enterActiveClass:l=`${s}-enter-active`,enterToClass:o=`${s}-enter-to`,appearFromClass:r=i,appearActiveClass:c=l,appearToClass:d=o,leaveFromClass:u=`${s}-leave-from`,leaveActiveClass:f=`${s}-leave-active`,leaveToClass:m=`${s}-leave-to`}=e,h=a_(n),g=h&&h[0],A=h&&h[1],{onBeforeEnter:w,onEnter:y,onEnterCancelled:b,onLeave:x,onLeaveCancelled:S,onBeforeAppear:_=w,onAppear:T=y,onAppearCancelled:L=b}=t,E=(I,H,X,Q)=>{I._enterCancelled=Q,gn(I,H?d:o),gn(I,H?c:l),X&&X()},P=(I,H)=>{I._isLeaving=!1,gn(I,u),gn(I,m),gn(I,f),H&&H()},R=I=>(H,X)=>{const Q=I?T:y,N=()=>E(H,I,X);Fn(Q,[H,N]),Ep(()=>{gn(H,I?r:i),ka(H,I?d:o),Tp(Q)||Ap(H,a,g,N)})};return nt(t,{onBeforeEnter(I){Fn(w,[I]),ka(I,i),ka(I,l)},onBeforeAppear(I){Fn(_,[I]),ka(I,r),ka(I,c)},onEnter:R(!1),onAppear:R(!0),onLeave(I,H){I._isLeaving=!0;const X=()=>P(I,H);ka(I,u),I._enterCancelled?(ka(I,f),pd(I)):(pd(I),ka(I,f)),Ep(()=>{I._isLeaving&&(gn(I,u),ka(I,m),Tp(x)||Ap(I,a,A,X))}),Fn(x,[I,X])},onEnterCancelled(I){E(I,!1,void 0,!0),Fn(b,[I])},onAppearCancelled(I){E(I,!0,void 0,!0),Fn(L,[I])},onLeaveCancelled(I){P(I),Fn(S,[I])}})}function a_(e){if(e==null)return null;if(mt(e))return[Ac(e.enter),Ac(e.leave)];{const t=Ac(e);return[t,t]}}function Ac(e){return tr(e)}function ka(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.add(s)),(e[Hi]||(e[Hi]=new Set)).add(t)}function gn(e,t){t.split(/\s+/).forEach(a=>a&&e.classList.remove(a));const s=e[Hi];s&&(s.delete(t),s.size||(e[Hi]=void 0))}function Ep(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let n_=0;function Ap(e,t,s,a){const n=e._endId=++n_,i=()=>{n===e._endId&&a()};if(s!=null)return setTimeout(i,s);const{type:l,timeout:o,propCount:r}=ev(e,t);if(!l)return a();const c=l+"end";let d=0;const u=()=>{e.removeEventListener(c,f),i()},f=m=>{m.target===e&&++d>=r&&u()};setTimeout(()=>{d(s[h]||"").split(", "),n=a(`${fn}Delay`),i=a(`${fn}Duration`),l=Rp(n,i),o=a(`${ul}Delay`),r=a(`${ul}Duration`),c=Rp(o,r);let d=null,u=0,f=0;t===fn?l>0&&(d=fn,u=l,f=i.length):t===ul?c>0&&(d=ul,u=c,f=r.length):(u=Math.max(l,c),d=u>0?l>c?fn:ul:null,f=d?d===fn?i.length:r.length:0);const m=d===fn&&/\b(?:transform|all)(?:,|$)/.test(a(`${fn}Property`).toString());return{type:d,timeout:u,propCount:f,hasTransform:m}}function Rp(e,t){for(;e.lengthIp(s)+Ip(e[a])))}function Ip(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function pd(e){return(e?e.ownerDocument:document).body.offsetHeight}function i_(e,t,s){const a=e[Hi];a&&(t=(t?[t,...a]:[...a]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const mr=Symbol("_vod"),cu=Symbol("_vsh"),tv={name:"show",beforeMount(e,{value:t},{transition:s}){e[mr]=e.style.display==="none"?"":e.style.display,s&&t?s.beforeEnter(e):pl(e,t)},mounted(e,{value:t},{transition:s}){s&&t&&s.enter(e)},updated(e,{value:t,oldValue:s},{transition:a}){!t!=!s&&(a?t?(a.beforeEnter(e),pl(e,!0),a.enter(e)):a.leave(e,()=>{pl(e,!1)}):pl(e,t))},beforeUnmount(e,{value:t}){pl(e,t)}};function pl(e,t){e.style.display=t?e[mr]:"none",e[cu]=!t}function l_(){tv.getSSRProps=({value:e})=>{if(!e)return{style:{display:"none"}}}}const sv=Symbol("");function o_(e){const t=Ls();if(!t)return;const s=t.ut=(n=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(i=>hr(i,n))},a=()=>{const n=e(t.proxy);t.ce?hr(t.ce,n):fd(t.subTree,n),s(n)};Yd(()=>{Fl(a)}),tt(()=>{jt(a,hs,{flush:"post"});const n=new MutationObserver(a);n.observe(t.subTree.el.parentNode,{childList:!0}),xt(()=>n.disconnect())})}function fd(e,t){if(e.shapeFlag&128){const s=e.suspense;e=s.activeBranch,s.pendingBranch&&!s.isHydrating&&s.effects.push(()=>{fd(s.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)hr(e.el,t);else if(e.type===ls)e.children.forEach(s=>fd(s,t));else if(e.type===Kn){let{el:s,anchor:a}=e;for(;s&&(hr(s,t),s!==a);)s=s.nextSibling}}function hr(e,t){if(e.nodeType===1){const s=e.style;let a="";for(const n in t){const i=fy(t[n]);s.setProperty(`--${n}`,i),a+=`--${n}: ${i};`}s[sv]=a}}const r_=/(?:^|;)\s*display\s*:/;function c_(e,t,s){const a=e.style,n=Qe(s);let i=!1;if(s&&!n){if(t)if(Qe(t))for(const l of t.split(";")){const o=l.slice(0,l.indexOf(":")).trim();s[o]==null&&kl(a,o,"")}else for(const l in t)s[l]==null&&kl(a,l,"");for(const l in s){l==="display"&&(i=!0);const o=s[l];o!=null?u_(e,l,!Qe(t)&&t?t[l]:void 0,o)||kl(a,l,o):kl(a,l,"")}}else if(n){if(t!==s){const l=a[sv];l&&(s+=";"+l),a.cssText=s,i=r_.test(s)}}else t&&e.removeAttribute("style");mr in e&&(e[mr]=i?a.display:"",e[cu]&&(a.display="none"))}const Op=/\s*!important$/;function kl(e,t,s){if(Le(s))s.forEach(a=>kl(e,t,a));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const a=d_(e,t);Op.test(s)?e.setProperty(Us(a),s.replace(Op,""),"important"):e[a]=s}}const Lp=["Webkit","Moz","ms"],Rc={};function d_(e,t){const s=Rc[t];if(s)return s;let a=wt(t);if(a!=="filter"&&a in e)return Rc[t]=a;a=ai(a);for(let n=0;nIc||(h_.then(()=>Ic=0),Ic=Date.now());function g_(e,t){const s=a=>{if(!a._vts)a._vts=Date.now();else if(a._vts<=s.attached)return;const n=s.value;if(Le(n)){const i=a.stopImmediatePropagation;a.stopImmediatePropagation=()=>{i.call(a),a._stopped=!0};const l=n.slice(),o=[a];for(let r=0;re.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,av=(e,t,s,a,n,i)=>{const l=n==="svg";t==="class"?i_(e,a,l):t==="style"?c_(e,s,a):ti(t)?Lr(t)||f_(e,t,s,a,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):b_(e,t,a,l))?(Dp(e,t,a),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Mp(e,t,a,l,i,t!=="value")):e._isVueCE&&(y_(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!Qe(a)))?Dp(e,wt(t),a,i,t):(t==="true-value"?e._trueValue=a:t==="false-value"&&(e._falseValue=a),Mp(e,t,a,l))};function b_(e,t,s,a){if(a)return!!(t==="innerHTML"||t==="textContent"||t in e&&Fp(t)&&je(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const n=e.tagName;if(n==="IMG"||n==="VIDEO"||n==="CANVAS"||n==="SOURCE")return!1}return Fp(t)&&Qe(s)?!1:t in e}function y_(e,t){const s=e._def.props;if(!s)return!1;const a=wt(t);return Array.isArray(s)?s.some(n=>wt(n)===a):Object.keys(s).some(n=>wt(n)===a)}const Bp={};function nv(e,t,s){let a=lo(e,t);Nr(a)&&(a=nt({},a,t));class n extends Zr{constructor(l){super(a,l,s)}}return n.def=a,n}const x_=((e,t)=>nv(e,t,gv)),__=typeof HTMLElement<"u"?HTMLElement:class{};class Zr extends __{constructor(t,s={},a=br){super(),this._def=t,this._props=s,this._createApp=a,this._isVueCE=!0,this._instance=null,this._app=null,this._nonce=this._def.nonce,this._connected=!1,this._resolved=!1,this._patching=!1,this._dirty=!1,this._numberProps=null,this._styleChildren=new WeakSet,this._styleAnchors=new WeakMap,this._ob=null,this.shadowRoot&&a!==br?this._root=this.shadowRoot:t.shadowRoot!==!1?(this.attachShadow(nt({},t.shadowRootOptions,{mode:"open"})),this._root=this.shadowRoot):this._root=this}connectedCallback(){if(!this.isConnected)return;!this.shadowRoot&&!this._resolved&&this._parseSlots(),this._connected=!0;let t=this;for(;t=t&&(t.assignedSlot||t.parentNode||t.host);)if(t instanceof Zr){this._parent=t;break}this._instance||(this._resolved?this._mount(this._def):t&&t._pendingResolve?this._pendingResolve=t._pendingResolve.then(()=>{this._pendingResolve=void 0,this._resolveDef()}):this._resolveDef())}_setParent(t=this._parent){t&&(this._instance.parent=t._instance,this._inheritParentContext(t))}_inheritParentContext(t=this._parent){t&&this._app&&Object.setPrototypeOf(this._app._context.provides,t._instance.provides)}disconnectedCallback(){this._connected=!1,Mt(()=>{this._connected||(this._ob&&(this._ob.disconnect(),this._ob=null),this._app&&this._app.unmount(),this._instance&&(this._instance.ce=void 0),this._app=this._instance=null,this._teleportTargets&&(this._teleportTargets.clear(),this._teleportTargets=void 0))})}_processMutations(t){for(const s of t)this._setAttr(s.attributeName)}_resolveDef(){if(this._pendingResolve)return;for(let a=0;a{this._resolved=!0,this._pendingResolve=void 0;const{props:i,styles:l}=a;let o;if(i&&!Le(i))for(const r in i){const c=i[r];(c===Number||c&&c.type===Number)&&(r in this._props&&(this._props[r]=tr(this._props[r])),(o||(o=Object.create(null)))[wt(r)]=!0)}this._numberProps=o,this._resolveProps(a),this.shadowRoot&&this._applyStyles(l),this._mount(a)},s=this._def.__asyncLoader;s?this._pendingResolve=s().then(a=>{a.configureApp=this._def.configureApp,t(this._def=a,!0)}):t(this._def)}_mount(t){this._app=this._createApp(t),this._inheritParentContext(),t.configureApp&&t.configureApp(this._app),this._app._ceVNode=this._createVNode(),this._app.mount(this._root);const s=this._instance&&this._instance.exposed;if(s)for(const a in s)ht(this,a)||Object.defineProperty(this,a,{get:()=>Ra(s[a])})}_resolveProps(t){const{props:s}=t,a=Le(s)?s:Object.keys(s||{});for(const n of Object.keys(this))n[0]!=="_"&&a.includes(n)&&this._setProp(n,this[n]);for(const n of a.map(wt))Object.defineProperty(this,n,{get(){return this._getProp(n)},set(i){this._setProp(n,i,!0,!this._patching)}})}_setAttr(t){if(t.startsWith("data-v-"))return;const s=this.hasAttribute(t);let a=s?this.getAttribute(t):Bp;const n=wt(t);s&&this._numberProps&&this._numberProps[n]&&(a=tr(a)),this._setProp(n,a,!1,!0)}_getProp(t){return this._props[t]}_setProp(t,s,a=!0,n=!1){if(s!==this._props[t]&&(this._dirty=!0,s===Bp?delete this._props[t]:(this._props[t]=s,t==="key"&&this._app&&(this._app._ceVNode.key=s)),n&&this._instance&&this._update(),a)){const i=this._ob;i&&(this._processMutations(i.takeRecords()),i.disconnect()),s===!0?this.setAttribute(Us(t),""):typeof s=="string"||typeof s=="number"?this.setAttribute(Us(t),s+""):s||this.removeAttribute(Us(t)),i&&i.observe(this,{attributes:!0})}}_update(){const t=this._createVNode();this._app&&(t.appContext=this._app._context),vv(t,this._root)}_createVNode(){const t={};this.shadowRoot||(t.onVnodeMounted=t.onVnodeUpdated=this._renderSlots.bind(this));const s=Lt(this._def,nt(t,this._props));return this._instance||(s.ce=a=>{this._instance=a,a.ce=this,a.isCE=!0;const n=(i,l)=>{this.dispatchEvent(new CustomEvent(i,Nr(l[0])?nt({detail:l},l[0]):{detail:l}))};a.emit=(i,...l)=>{n(i,l),Us(i)!==i&&n(Us(i),l)},this._setParent()}),s}_applyStyles(t,s,a){if(!t)return;if(s){if(s===this._def||this._styleChildren.has(s))return;this._styleChildren.add(s)}const n=this._nonce,i=this.shadowRoot,l=a?this._getStyleAnchor(a)||this._getStyleAnchor(this._def):this._getRootStyleInsertionAnchor(i);let o=null;for(let r=t.length-1;r>=0;r--){const c=document.createElement("style");n&&c.setAttribute("nonce",n),c.textContent=t[r],i.insertBefore(c,o||l),o=c,r===0&&(a||this._styleAnchors.set(this._def,c),s&&this._styleAnchors.set(s,c))}}_getStyleAnchor(t){if(!t)return null;const s=this._styleAnchors.get(t);return s&&s.parentNode===this.shadowRoot?s:(s&&this._styleAnchors.delete(t),null)}_getRootStyleInsertionAnchor(t){for(let s=0;s(delete e.props.mode,e),C_=S_({name:"TransitionGroup",props:nt({},Yh,{tag:String,moveClass:String}),setup(e,{slots:t}){const s=Ls(),a=Jd();let n,i;return Gr(()=>{if(!n.length)return;const l=e.moveClass||`${e.name||"v"}-move`;if(!I_(n[0].el,s.vnode.el,l)){n=[];return}n.forEach(E_),n.forEach(A_);const o=n.filter(R_);pd(s.vnode.el),o.forEach(r=>{const c=r.el,d=c.style;ka(c,l),d.transform=d.webkitTransform=d.transitionDuration="";const u=c[vr]=f=>{f&&f.target!==c||(!f||f.propertyName.endsWith("transform"))&&(c.removeEventListener("transitionend",u),c[vr]=null,gn(c,l))};c.addEventListener("transitionend",u)}),n=[]}),()=>{const l=dt(e),o=Xh(l);let r=l.tag||ls;if(n=[],i)for(let c=0;c{o.split(/\s+/).forEach(r=>r&&a.classList.remove(r))}),s.split(/\s+/).forEach(o=>o&&a.classList.add(o)),a.style.display="none";const i=t.nodeType===1?t:t.parentNode;i.appendChild(a);const{hasTransform:l}=ev(a);return i.removeChild(a),l}const Cn=e=>{const t=e.props["onUpdate:modelValue"]||!1;return Le(t)?s=>Li(t,s):t};function O_(e){e.target.composing=!0}function zp(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const ia=Symbol("_assign");function Hp(e,t,s){return t&&(e=e.trim()),s&&(e=Pr(e)),e}const gr={created(e,{modifiers:{lazy:t,trim:s,number:a}},n){e[ia]=Cn(n);const i=a||n.props&&n.props.type==="number";Wa(e,t?"change":"input",l=>{l.target.composing||e[ia](Hp(e.value,s,i))}),(s||i)&&Wa(e,"change",()=>{e.value=Hp(e.value,s,i)}),t||(Wa(e,"compositionstart",O_),Wa(e,"compositionend",zp),Wa(e,"change",zp))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:a,trim:n,number:i}},l){if(e[ia]=Cn(l),e.composing)return;const o=(i||e.type==="number")&&!/^0\d/.test(e.value)?Pr(e.value):e.value,r=t??"";if(o===r)return;const c=e.getRootNode();(c instanceof Document||c instanceof ShadowRoot)&&c.activeElement===e&&e.type!=="range"&&(a&&t===s||n&&e.value.trim()===r)||(e.value=r)}},du={deep:!0,created(e,t,s){e[ia]=Cn(s),Wa(e,"change",()=>{const a=e._modelValue,n=ji(e),i=e.checked,l=e[ia];if(Le(a)){const o=Fr(a,n),r=o!==-1;if(i&&!r)l(a.concat(n));else if(!i&&r){const c=[...a];c.splice(o,1),l(c)}}else if(si(a)){const o=new Set(a);i?o.add(n):o.delete(n),l(o)}else l(dv(e,i))})},mounted:jp,beforeUpdate(e,t,s){e[ia]=Cn(s),jp(e,t,s)}};function jp(e,{value:t,oldValue:s},a){e._modelValue=t;let n;if(Le(t))n=Fr(t,a.props.value)>-1;else if(si(t))n=t.has(a.props.value);else{if(t===s)return;n=en(t,dv(e,!0))}e.checked!==n&&(e.checked=n)}const uu={created(e,{value:t},s){e.checked=en(t,s.props.value),e[ia]=Cn(s),Wa(e,"change",()=>{e[ia](ji(e))})},beforeUpdate(e,{value:t,oldValue:s},a){e[ia]=Cn(a),t!==s&&(e.checked=en(t,a.props.value))}},cv={deep:!0,created(e,{value:t,modifiers:{number:s}},a){const n=si(t);Wa(e,"change",()=>{const i=Array.prototype.filter.call(e.options,l=>l.selected).map(l=>s?Pr(ji(l)):ji(l));e[ia](e.multiple?n?new Set(i):i:i[0]),e._assigning=!0,Mt(()=>{e._assigning=!1})}),e[ia]=Cn(a)},mounted(e,{value:t}){qp(e,t)},beforeUpdate(e,t,s){e[ia]=Cn(s)},updated(e,{value:t}){e._assigning||qp(e,t)}};function qp(e,t){const s=e.multiple,a=Le(t);if(!(s&&!a&&!si(t))){for(let n=0,i=e.options.length;nString(c)===String(o)):l.selected=Fr(t,o)>-1}else l.selected=t.has(o);else if(en(ji(l),t)){e.selectedIndex!==n&&(e.selectedIndex=n);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function ji(e){return"_value"in e?e._value:e.value}function dv(e,t){const s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}const uv={created(e,t,s){Ao(e,t,s,null,"created")},mounted(e,t,s){Ao(e,t,s,null,"mounted")},beforeUpdate(e,t,s,a){Ao(e,t,s,a,"beforeUpdate")},updated(e,t,s,a){Ao(e,t,s,a,"updated")}};function pv(e,t){switch(e){case"SELECT":return cv;case"TEXTAREA":return gr;default:switch(t){case"checkbox":return du;case"radio":return uu;default:return gr}}}function Ao(e,t,s,a,n){const l=pv(e.tagName,s.props&&s.props.type)[n];l&&l(e,t,s,a)}function L_(){gr.getSSRProps=({value:e})=>({value:e}),uu.getSSRProps=({value:e},t)=>{if(t.props&&en(t.props.value,e))return{checked:!0}},du.getSSRProps=({value:e},t)=>{if(Le(e)){if(t.props&&Fr(e,t.props.value)>-1)return{checked:!0}}else if(si(e)){if(t.props&&e.has(t.props.value))return{checked:!0}}else if(e)return{checked:!0}},uv.getSSRProps=(e,t)=>{if(typeof t.type!="string")return;const s=pv(t.type.toUpperCase(),t.props&&t.props.type);if(s.getSSRProps)return s.getSSRProps(e,t)}}const N_=["ctrl","shift","alt","meta"],M_={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>N_.some(s=>e[`${s}Key`]&&!t.includes(s))},D_=(e,t)=>{if(!e)return e;const s=e._withMods||(e._withMods={}),a=t.join(".");return s[a]||(s[a]=((n,...i)=>{for(let l=0;l{const s=e._withKeys||(e._withKeys={}),a=t.join(".");return s[a]||(s[a]=(n=>{if(!("key"in n))return;const i=Us(n.key);if(t.some(l=>l===i||P_[l]===i))return e(n)}))},fv=nt({patchProp:av},Zh);let Il,Vp=!1;function mv(){return Il||(Il=Eh(fv))}function hv(){return Il=Vp?Il:Ah(fv),Vp=!0,Il}const vv=((...e)=>{mv().render(...e)}),F_=((...e)=>{hv().hydrate(...e)}),br=((...e)=>{const t=mv().createApp(...e),{mount:s}=t;return t.mount=a=>{const n=yv(a);if(!n)return;const i=t._component;!je(i)&&!i.render&&!i.template&&(i.template=n.innerHTML),n.nodeType===1&&(n.textContent="");const l=s(n,!1,bv(n));return n instanceof Element&&(n.removeAttribute("v-cloak"),n.setAttribute("data-v-app","")),l},t}),gv=((...e)=>{const t=hv().createApp(...e),{mount:s}=t;return t.mount=a=>{const n=yv(a);if(n)return s(n,!0,bv(n))},t});function bv(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function yv(e){return Qe(e)?document.querySelector(e):e}let Gp=!1;const B_=()=>{Gp||(Gp=!0,L_(),l_())},U_=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:nh,BaseTransitionPropsValidators:Zd,Comment:Ht,DeprecationTypes:Y0,EffectScope:Ud,ErrorCodes:tx,ErrorTypeStrings:V0,Fragment:ls,KeepAlive:Mx,ReactiveEffect:Dl,Static:Kn,Suspense:T0,Teleport:vx,Text:kn,TrackOpTypes:Ky,Transition:s_,TransitionGroup:T_,TriggerOpTypes:Jy,VueElement:Zr,assertNumber:ex,callWithAsyncErrorHandling:Zs,callWithErrorHandling:Yi,camelize:wt,capitalize:ai,cloneVNode:Oa,compatUtils:Q0,computed:U,createApp:br,createBlock:ur,createCommentVNode:Bh,createElementBlock:L0,createElementVNode:lu,createHydrationRenderer:Ah,createPropsRestProxy:s0,createRenderer:Eh,createSSRApp:gv,createSlots:zx,createStaticVNode:D0,createTextVNode:ou,createVNode:Lt,customRef:Hm,defineAsyncComponent:Lx,defineComponent:lo,defineCustomElement:nv,defineEmits:Gx,defineExpose:Wx,defineModel:Zx,defineOptions:Kx,defineProps:Vx,defineSSRCustomElement:x_,defineSlots:Jx,devtools:G0,effect:gy,effectScope:my,getCurrentInstance:Ls,getCurrentScope:Sm,getCurrentWatcher:Zy,getTransitionRawChildren:qr,guardReactiveProps:Fh,h:zi,handleError:ni,hasInjectionContext:cx,hydrate:F_,hydrateOnIdle:Tx,hydrateOnInteraction:Ix,hydrateOnMediaQuery:Rx,hydrateOnVisible:Ax,initCustomFormatter:H0,initDirectivesForSSR:B_,inject:na,isMemoSame:Wh,isProxy:no,isReactive:Qa,isReadonly:Ia,isRef:Wt,isRuntimeOnly:B0,isShallow:Hs,isVNode:nn,markRaw:Um,mergeDefaults:e0,mergeModels:t0,mergeProps:Uh,nextTick:Mt,nodeOps:Zh,normalizeClass:ao,normalizeProps:ty,normalizeStyle:so,onActivated:rs,onBeforeMount:oh,onBeforeUnmount:Wr,onBeforeUpdate:Yd,onDeactivated:Kt,onErrorCaptured:uh,onMounted:tt,onRenderTracked:dh,onRenderTriggered:ch,onScopeDispose:hy,onServerPrefetch:rh,onUnmounted:xt,onUpdated:Gr,onWatcherCleanup:qm,openBlock:jl,patchProp:av,popScopeId:lx,provide:El,proxyRefs:Gd,pushScopeId:ix,queuePostFlushCb:Fl,reactive:Tn,readonly:ar,ref:p,registerRuntimeCompiler:qh,render:vv,renderList:Ux,renderSlot:Hx,resolveComponent:$x,resolveDirective:Bx,resolveDynamicComponent:Fx,resolveFilter:Z0,resolveTransitionHooks:Ui,setBlockTracking:ql,setDevtoolsHook:W0,setTransitionHooks:an,shallowReactive:qd,shallowReadonly:Py,shallowRef:Vd,ssrContextKey:Zm,ssrUtils:J0,stop:by,toDisplayString:wm,toHandlerKey:Oi,toHandlers:jx,toRaw:dt,toRef:Vy,toRefs:Hy,toValue:By,transformVNodeArgs:N0,triggerRef:Fy,unref:Ra,useAttrs:Xx,useCssModule:k_,useCssVars:o_,useHost:iv,useId:bx,useModel:u0,useSSRContext:Qm,useShadowRoot:w_,useSlots:Yx,useTemplateRef:yx,useTransitionState:Jd,vModelCheckbox:du,vModelDynamic:uv,vModelRadio:uu,vModelSelect:cv,vModelText:gr,vShow:tv,version:Kh,warn:q0,watch:jt,watchEffect:dx,watchPostEffect:ux,watchSyncEffect:Ym,withAsyncContext:a0,withCtx:Kd,withDefaults:Qx,withDirectives:rx,withKeys:$_,withMemo:j0,withModifiers:D_,withScopeId:ox},Symbol.toStringTag,{value:"Module"}));/** +**/let yd;const Mp=typeof window<"u"&&window.trustedTypes;if(Mp)try{yd=Mp.createPolicy("vue",{createHTML:e=>e})}catch{}const ih=yd?e=>yd.createHTML(e):e=>e,x_="http://www.w3.org/2000/svg",__="http://www.w3.org/1998/Math/MathML",Ga=typeof document<"u"?document:null,Dp=Ga&&Ga.createElement("template"),lh={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,a)=>{const n=t==="svg"?Ga.createElementNS(x_,e):t==="mathml"?Ga.createElementNS(__,e):s?Ga.createElement(e,{is:s}):Ga.createElement(e);return e==="select"&&a&&a.multiple!=null&&n.setAttribute("multiple",a.multiple),n},createText:e=>Ga.createTextNode(e),createComment:e=>Ga.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Ga.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,a,n,i){const l=s?s.previousSibling:t.lastChild;if(n&&(n===i||n.nextSibling))for(;t.insertBefore(n.cloneNode(!0),s),!(n===i||!(n=n.nextSibling)););else{Dp.innerHTML=ih(a==="svg"?`${e}`:a==="mathml"?`${e}`:e);const o=Dp.content;if(a==="svg"||a==="mathml"){const r=o.firstChild;for(;r.firstChild;)o.appendChild(r.firstChild);o.removeChild(r)}t.insertBefore(o,s)}return[l?l.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},_n="transition",fl="animation",Vi=Symbol("_vtc"),oh={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},rh=lt({},nu,oh),w_=e=>(e.displayName="Transition",e.props=rh,e),k_=w_((e,{slots:t})=>qi(mv,ch(e),t)),jn=(e,t=[])=>{Me(e)?e.forEach(s=>s(...t)):e&&e(...t)},Pp=e=>e?Me(e)?e.some(t=>t.length>1):e.length>1:!1;function ch(e){const t={};for(const I in e)I in oh||(t[I]=e[I]);if(e.css===!1)return t;const{name:s="v",type:a,duration:n,enterFromClass:i=`${s}-enter-from`,enterActiveClass:l=`${s}-enter-active`,enterToClass:o=`${s}-enter-to`,appearFromClass:r=i,appearActiveClass:c=l,appearToClass:d=o,leaveFromClass:u=`${s}-leave-from`,leaveActiveClass:p=`${s}-leave-active`,leaveToClass:m=`${s}-leave-to`}=e,h=S_(n),y=h&&h[0],k=h&&h[1],{onBeforeEnter:A,onEnter:g,onEnterCancelled:b,onLeave:x,onLeaveCancelled:w,onBeforeAppear:_=A,onAppear:T=g,onAppearCancelled:L=b}=t,E=(I,H,te,ee)=>{I._enterCancelled=ee,Cn(I,H?d:o),Cn(I,H?c:l),te&&te()},F=(I,H)=>{I._isLeaving=!1,Cn(I,u),Cn(I,m),Cn(I,p),H&&H()},O=I=>(H,te)=>{const ee=I?T:g,N=()=>E(H,I,te);jn(ee,[H,N]),$p(()=>{Cn(H,I?r:i),Ta(H,I?d:o),Pp(ee)||Fp(H,a,y,N)})};return lt(t,{onBeforeEnter(I){jn(A,[I]),Ta(I,i),Ta(I,l)},onBeforeAppear(I){jn(_,[I]),Ta(I,r),Ta(I,c)},onEnter:O(!1),onAppear:O(!0),onLeave(I,H){I._isLeaving=!0;const te=()=>F(I,H);Ta(I,u),I._enterCancelled?(Ta(I,p),xd(I)):(xd(I),Ta(I,p)),$p(()=>{I._isLeaving&&(Cn(I,u),Ta(I,m),Pp(x)||Fp(I,a,k,te))}),jn(x,[I,te])},onEnterCancelled(I){E(I,!1,void 0,!0),jn(b,[I])},onAppearCancelled(I){E(I,!0,void 0,!0),jn(L,[I])},onLeaveCancelled(I){F(I),jn(w,[I])}})}function S_(e){if(e==null)return null;if(ht(e))return[Fc(e.enter),Fc(e.leave)];{const t=Fc(e);return[t,t]}}function Fc(e){return rr(e)}function Ta(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.add(s)),(e[Vi]||(e[Vi]=new Set)).add(t)}function Cn(e,t){t.split(/\s+/).forEach(a=>a&&e.classList.remove(a));const s=e[Vi];s&&(s.delete(t),s.size||(e[Vi]=void 0))}function $p(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let C_=0;function Fp(e,t,s,a){const n=e._endId=++C_,i=()=>{n===e._endId&&a()};if(s!=null)return setTimeout(i,s);const{type:l,timeout:o,propCount:r}=dh(e,t);if(!l)return a();const c=l+"end";let d=0;const u=()=>{e.removeEventListener(c,p),i()},p=m=>{m.target===e&&++d>=r&&u()};setTimeout(()=>{d(s[h]||"").split(", "),n=a(`${_n}Delay`),i=a(`${_n}Duration`),l=Bp(n,i),o=a(`${fl}Delay`),r=a(`${fl}Duration`),c=Bp(o,r);let d=null,u=0,p=0;t===_n?l>0&&(d=_n,u=l,p=i.length):t===fl?c>0&&(d=fl,u=c,p=r.length):(u=Math.max(l,c),d=u>0?l>c?_n:fl:null,p=d?d===_n?i.length:r.length:0);const m=d===_n&&/\b(?:transform|all)(?:,|$)/.test(a(`${_n}Property`).toString());return{type:d,timeout:u,propCount:p,hasTransform:m}}function Bp(e,t){for(;e.lengthUp(s)+Up(e[a])))}function Up(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function xd(e){return(e?e.ownerDocument:document).body.offsetHeight}function T_(e,t,s){const a=e[Vi];a&&(t=(t?[t,...a]:[...a]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const _r=Symbol("_vod"),gu=Symbol("_vsh"),uh={name:"show",beforeMount(e,{value:t},{transition:s}){e[_r]=e.style.display==="none"?"":e.style.display,s&&t?s.beforeEnter(e):ml(e,t)},mounted(e,{value:t},{transition:s}){s&&t&&s.enter(e)},updated(e,{value:t,oldValue:s},{transition:a}){!t!=!s&&(a?t?(a.beforeEnter(e),ml(e,!0),a.enter(e)):a.leave(e,()=>{ml(e,!1)}):ml(e,t))},beforeUnmount(e,{value:t}){ml(e,t)}};function ml(e,t){e.style.display=t?e[_r]:"none",e[gu]=!t}function E_(){uh.getSSRProps=({value:e})=>{if(!e)return{style:{display:"none"}}}}const ph=Symbol("");function A_(e){const t=Ds();if(!t)return;const s=t.ut=(n=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(i=>wr(i,n))},a=()=>{const n=e(t.proxy);t.ce?wr(t.ce,n):_d(t.subTree,n),s(n)};lu(()=>{ql(a)}),st(()=>{Gt(a,vs,{flush:"post"});const n=new MutationObserver(a);n.observe(t.subTree.el.parentNode,{childList:!0}),wt(()=>n.disconnect())})}function _d(e,t){if(e.shapeFlag&128){const s=e.suspense;e=s.activeBranch,s.pendingBranch&&!s.isHydrating&&s.effects.push(()=>{_d(s.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)wr(e.el,t);else if(e.type===rs)e.children.forEach(s=>_d(s,t));else if(e.type===Xn){let{el:s,anchor:a}=e;for(;s&&(wr(s,t),s!==a);)s=s.nextSibling}}function wr(e,t){if(e.nodeType===1){const s=e.style;let a="";for(const n in t){const i=My(t[n]);s.setProperty(`--${n}`,i),a+=`--${n}: ${i};`}s[ph]=a}}const R_=/(?:^|;)\s*display\s*:/;function I_(e,t,s){const a=e.style,n=Ze(s);let i=!1;if(s&&!n){if(t)if(Ze(t))for(const l of t.split(";")){const o=l.slice(0,l.indexOf(":")).trim();s[o]==null&&Rl(a,o,"")}else for(const l in t)s[l]==null&&Rl(a,l,"");for(const l in s){l==="display"&&(i=!0);const o=s[l];o!=null?L_(e,l,!Ze(t)&&t?t[l]:void 0,o)||Rl(a,l,o):Rl(a,l,"")}}else if(n){if(t!==s){const l=a[ph];l&&(s+=";"+l),a.cssText=s,i=R_.test(s)}}else t&&e.removeAttribute("style");_r in e&&(e[_r]=i?a.display:"",e[gu]&&(a.display="none"))}const zp=/\s*!important$/;function Rl(e,t,s){if(Me(s))s.forEach(a=>Rl(e,t,a));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const a=O_(e,t);zp.test(s)?e.setProperty(Hs(a),s.replace(zp,""),"important"):e[a]=s}}const Hp=["Webkit","Moz","ms"],Bc={};function O_(e,t){const s=Bc[t];if(s)return s;let a=Tt(t);if(a!=="filter"&&a in e)return Bc[t]=a;a=ri(a);for(let n=0;nUc||(P_.then(()=>Uc=0),Uc=Date.now());function F_(e,t){const s=a=>{if(!a._vts)a._vts=Date.now();else if(a._vts<=s.attached)return;const n=s.value;if(Me(n)){const i=a.stopImmediatePropagation;a.stopImmediatePropagation=()=>{i.call(a),a._stopped=!0};const l=n.slice(),o=[a];for(let r=0;re.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,fh=(e,t,s,a,n,i)=>{const l=n==="svg";t==="class"?T_(e,a,l):t==="style"?I_(e,s,a):li(t)?Br(t)||M_(e,t,s,a,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):B_(e,t,a,l))?(Vp(e,t,a),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&qp(e,t,a,l,i,t!=="value")):e._isVueCE&&(U_(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!Ze(a)))?Vp(e,Tt(t),a,i,t):(t==="true-value"?e._trueValue=a:t==="false-value"&&(e._falseValue=a),qp(e,t,a,l))};function B_(e,t,s,a){if(a)return!!(t==="innerHTML"||t==="textContent"||t in e&&Kp(t)&&Ve(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const n=e.tagName;if(n==="IMG"||n==="VIDEO"||n==="CANVAS"||n==="SOURCE")return!1}return Kp(t)&&Ze(s)?!1:t in e}function U_(e,t){const s=e._def.props;if(!s)return!1;const a=Tt(t);return Array.isArray(s)?s.some(n=>Tt(n)===a):Object.keys(s).some(n=>Tt(n)===a)}const Jp={};function mh(e,t,s){let a=fo(e,t);Ur(a)&&(a=lt({},a,t));class n extends ac{constructor(l){super(a,l,s)}}return n.def=a,n}const z_=((e,t)=>mh(e,t,Eh)),H_=typeof HTMLElement<"u"?HTMLElement:class{};class ac extends H_{constructor(t,s={},a=Cr){super(),this._def=t,this._props=s,this._createApp=a,this._isVueCE=!0,this._instance=null,this._app=null,this._nonce=this._def.nonce,this._connected=!1,this._resolved=!1,this._patching=!1,this._dirty=!1,this._numberProps=null,this._styleChildren=new WeakSet,this._styleAnchors=new WeakMap,this._ob=null,this.shadowRoot&&a!==Cr?this._root=this.shadowRoot:t.shadowRoot!==!1?(this.attachShadow(lt({},t.shadowRootOptions,{mode:"open"})),this._root=this.shadowRoot):this._root=this}connectedCallback(){if(!this.isConnected)return;!this.shadowRoot&&!this._resolved&&this._parseSlots(),this._connected=!0;let t=this;for(;t=t&&(t.assignedSlot||t.parentNode||t.host);)if(t instanceof ac){this._parent=t;break}this._instance||(this._resolved?this._mount(this._def):t&&t._pendingResolve?this._pendingResolve=t._pendingResolve.then(()=>{this._pendingResolve=void 0,this._resolveDef()}):this._resolveDef())}_setParent(t=this._parent){t&&(this._instance.parent=t._instance,this._inheritParentContext(t))}_inheritParentContext(t=this._parent){t&&this._app&&Object.setPrototypeOf(this._app._context.provides,t._instance.provides)}disconnectedCallback(){this._connected=!1,Ut(()=>{this._connected||(this._ob&&(this._ob.disconnect(),this._ob=null),this._app&&this._app.unmount(),this._instance&&(this._instance.ce=void 0),this._app=this._instance=null,this._teleportTargets&&(this._teleportTargets.clear(),this._teleportTargets=void 0))})}_processMutations(t){for(const s of t)this._setAttr(s.attributeName)}_resolveDef(){if(this._pendingResolve)return;for(let a=0;a{this._resolved=!0,this._pendingResolve=void 0;const{props:i,styles:l}=a;let o;if(i&&!Me(i))for(const r in i){const c=i[r];(c===Number||c&&c.type===Number)&&(r in this._props&&(this._props[r]=rr(this._props[r])),(o||(o=Object.create(null)))[Tt(r)]=!0)}this._numberProps=o,this._resolveProps(a),this.shadowRoot&&this._applyStyles(l),this._mount(a)},s=this._def.__asyncLoader;s?this._pendingResolve=s().then(a=>{a.configureApp=this._def.configureApp,t(this._def=a,!0)}):t(this._def)}_mount(t){this._app=this._createApp(t),this._inheritParentContext(),t.configureApp&&t.configureApp(this._app),this._app._ceVNode=this._createVNode(),this._app.mount(this._root);const s=this._instance&&this._instance.exposed;if(s)for(const a in s)bt(this,a)||Object.defineProperty(this,a,{get:()=>La(s[a])})}_resolveProps(t){const{props:s}=t,a=Me(s)?s:Object.keys(s||{});for(const n of Object.keys(this))n[0]!=="_"&&a.includes(n)&&this._setProp(n,this[n]);for(const n of a.map(Tt))Object.defineProperty(this,n,{get(){return this._getProp(n)},set(i){this._setProp(n,i,!0,!this._patching)}})}_setAttr(t){if(t.startsWith("data-v-"))return;const s=this.hasAttribute(t);let a=s?this.getAttribute(t):Jp;const n=Tt(t);s&&this._numberProps&&this._numberProps[n]&&(a=rr(a)),this._setProp(n,a,!1,!0)}_getProp(t){return this._props[t]}_setProp(t,s,a=!0,n=!1){if(s!==this._props[t]&&(this._dirty=!0,s===Jp?delete this._props[t]:(this._props[t]=s,t==="key"&&this._app&&(this._app._ceVNode.key=s)),n&&this._instance&&this._update(),a)){const i=this._ob;i&&(this._processMutations(i.takeRecords()),i.disconnect()),s===!0?this.setAttribute(Hs(t),""):typeof s=="string"||typeof s=="number"?this.setAttribute(Hs(t),s+""):s||this.removeAttribute(Hs(t)),i&&i.observe(this,{attributes:!0})}}_update(){const t=this._createVNode();this._app&&(t.appContext=this._app._context),Th(t,this._root)}_createVNode(){const t={};this.shadowRoot||(t.onVnodeMounted=t.onVnodeUpdated=this._renderSlots.bind(this));const s=Bt(this._def,lt(t,this._props));return this._instance||(s.ce=a=>{this._instance=a,a.ce=this,a.isCE=!0;const n=(i,l)=>{this.dispatchEvent(new CustomEvent(i,Ur(l[0])?lt({detail:l},l[0]):{detail:l}))};a.emit=(i,...l)=>{n(i,l),Hs(i)!==i&&n(Hs(i),l)},this._setParent()}),s}_applyStyles(t,s,a){if(!t)return;if(s){if(s===this._def||this._styleChildren.has(s))return;this._styleChildren.add(s)}const n=this._nonce,i=this.shadowRoot,l=a?this._getStyleAnchor(a)||this._getStyleAnchor(this._def):this._getRootStyleInsertionAnchor(i);let o=null;for(let r=t.length-1;r>=0;r--){const c=document.createElement("style");n&&c.setAttribute("nonce",n),c.textContent=t[r],i.insertBefore(c,o||l),o=c,r===0&&(a||this._styleAnchors.set(this._def,c),s&&this._styleAnchors.set(s,c))}}_getStyleAnchor(t){if(!t)return null;const s=this._styleAnchors.get(t);return s&&s.parentNode===this.shadowRoot?s:(s&&this._styleAnchors.delete(t),null)}_getRootStyleInsertionAnchor(t){for(let s=0;s(delete e.props.mode,e),G_=V_({name:"TransitionGroup",props:lt({},rh,{tag:String,moveClass:String}),setup(e,{slots:t}){const s=Ds(),a=au();let n,i;return Xr(()=>{if(!n.length)return;const l=e.moveClass||`${e.name||"v"}-move`;if(!Q_(n[0].el,s.vnode.el,l)){n=[];return}n.forEach(K_),n.forEach(J_);const o=n.filter(Z_);xd(s.vnode.el),o.forEach(r=>{const c=r.el,d=c.style;Ta(c,l),d.transform=d.webkitTransform=d.transitionDuration="";const u=c[kr]=p=>{p&&p.target!==c||(!p||p.propertyName.endsWith("transform"))&&(c.removeEventListener("transitionend",u),c[kr]=null,Cn(c,l))};c.addEventListener("transitionend",u)}),n=[]}),()=>{const l=ft(e),o=ch(l);let r=l.tag||rs;if(n=[],i)for(let c=0;c{o.split(/\s+/).forEach(r=>r&&a.classList.remove(r))}),s.split(/\s+/).forEach(o=>o&&a.classList.add(o)),a.style.display="none";const i=t.nodeType===1?t:t.parentNode;i.appendChild(a);const{hasTransform:l}=dh(a);return i.removeChild(a),l}const Nn=e=>{const t=e.props["onUpdate:modelValue"]||!1;return Me(t)?s=>Di(t,s):t};function Y_(e){e.target.composing=!0}function Qp(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const ca=Symbol("_assign");function Yp(e,t,s){return t&&(e=e.trim()),s&&(e=jr(e)),e}const Sr={created(e,{modifiers:{lazy:t,trim:s,number:a}},n){e[ca]=Nn(n);const i=a||n.props&&n.props.type==="number";Qa(e,t?"change":"input",l=>{l.target.composing||e[ca](Yp(e.value,s,i))}),(s||i)&&Qa(e,"change",()=>{e.value=Yp(e.value,s,i)}),t||(Qa(e,"compositionstart",Y_),Qa(e,"compositionend",Qp),Qa(e,"change",Qp))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:a,trim:n,number:i}},l){if(e[ca]=Nn(l),e.composing)return;const o=(i||e.type==="number")&&!/^0\d/.test(e.value)?jr(e.value):e.value,r=t??"";if(o===r)return;const c=e.getRootNode();(c instanceof Document||c instanceof ShadowRoot)&&c.activeElement===e&&e.type!=="range"&&(a&&t===s||n&&e.value.trim()===r)||(e.value=r)}},bu={deep:!0,created(e,t,s){e[ca]=Nn(s),Qa(e,"change",()=>{const a=e._modelValue,n=Gi(e),i=e.checked,l=e[ca];if(Me(a)){const o=Vr(a,n),r=o!==-1;if(i&&!r)l(a.concat(n));else if(!i&&r){const c=[...a];c.splice(o,1),l(c)}}else if(oi(a)){const o=new Set(a);i?o.add(n):o.delete(n),l(o)}else l(xh(e,i))})},mounted:Xp,beforeUpdate(e,t,s){e[ca]=Nn(s),Xp(e,t,s)}};function Xp(e,{value:t,oldValue:s},a){e._modelValue=t;let n;if(Me(t))n=Vr(t,a.props.value)>-1;else if(oi(t))n=t.has(a.props.value);else{if(t===s)return;n=nn(t,xh(e,!0))}e.checked!==n&&(e.checked=n)}const yu={created(e,{value:t},s){e.checked=nn(t,s.props.value),e[ca]=Nn(s),Qa(e,"change",()=>{e[ca](Gi(e))})},beforeUpdate(e,{value:t,oldValue:s},a){e[ca]=Nn(a),t!==s&&(e.checked=nn(t,a.props.value))}},yh={deep:!0,created(e,{value:t,modifiers:{number:s}},a){const n=oi(t);Qa(e,"change",()=>{const i=Array.prototype.filter.call(e.options,l=>l.selected).map(l=>s?jr(Gi(l)):Gi(l));e[ca](e.multiple?n?new Set(i):i:i[0]),e._assigning=!0,Ut(()=>{e._assigning=!1})}),e[ca]=Nn(a)},mounted(e,{value:t}){ef(e,t)},beforeUpdate(e,t,s){e[ca]=Nn(s)},updated(e,{value:t}){e._assigning||ef(e,t)}};function ef(e,t){const s=e.multiple,a=Me(t);if(!(s&&!a&&!oi(t))){for(let n=0,i=e.options.length;nString(c)===String(o)):l.selected=Vr(t,o)>-1}else l.selected=t.has(o);else if(nn(Gi(l),t)){e.selectedIndex!==n&&(e.selectedIndex=n);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Gi(e){return"_value"in e?e._value:e.value}function xh(e,t){const s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}const _h={created(e,t,s){$o(e,t,s,null,"created")},mounted(e,t,s){$o(e,t,s,null,"mounted")},beforeUpdate(e,t,s,a){$o(e,t,s,a,"beforeUpdate")},updated(e,t,s,a){$o(e,t,s,a,"updated")}};function wh(e,t){switch(e){case"SELECT":return yh;case"TEXTAREA":return Sr;default:switch(t){case"checkbox":return bu;case"radio":return yu;default:return Sr}}}function $o(e,t,s,a,n){const l=wh(e.tagName,s.props&&s.props.type)[n];l&&l(e,t,s,a)}function X_(){Sr.getSSRProps=({value:e})=>({value:e}),yu.getSSRProps=({value:e},t)=>{if(t.props&&nn(t.props.value,e))return{checked:!0}},bu.getSSRProps=({value:e},t)=>{if(Me(e)){if(t.props&&Vr(e,t.props.value)>-1)return{checked:!0}}else if(oi(e)){if(t.props&&e.has(t.props.value))return{checked:!0}}else if(e)return{checked:!0}},_h.getSSRProps=(e,t)=>{if(typeof t.type!="string")return;const s=wh(t.type.toUpperCase(),t.props&&t.props.type);if(s.getSSRProps)return s.getSSRProps(e,t)}}const ew=["ctrl","shift","alt","meta"],tw={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>ew.some(s=>e[`${s}Key`]&&!t.includes(s))},sw=(e,t)=>{if(!e)return e;const s=e._withMods||(e._withMods={}),a=t.join(".");return s[a]||(s[a]=((n,...i)=>{for(let l=0;l{const s=e._withKeys||(e._withKeys={}),a=t.join(".");return s[a]||(s[a]=(n=>{if(!("key"in n))return;const i=Hs(n.key);if(t.some(l=>l===i||aw[l]===i))return e(n)}))},kh=lt({patchProp:fh},lh);let Pl,tf=!1;function Sh(){return Pl||(Pl=$v(kh))}function Ch(){return Pl=tf?Pl:Fv(kh),tf=!0,Pl}const Th=((...e)=>{Sh().render(...e)}),iw=((...e)=>{Ch().hydrate(...e)}),Cr=((...e)=>{const t=Sh().createApp(...e),{mount:s}=t;return t.mount=a=>{const n=Rh(a);if(!n)return;const i=t._component;!Ve(i)&&!i.render&&!i.template&&(i.template=n.innerHTML),n.nodeType===1&&(n.textContent="");const l=s(n,!1,Ah(n));return n instanceof Element&&(n.removeAttribute("v-cloak"),n.setAttribute("data-v-app","")),l},t}),Eh=((...e)=>{const t=Ch().createApp(...e),{mount:s}=t;return t.mount=a=>{const n=Rh(a);if(n)return s(n,!0,Ah(n))},t});function Ah(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Rh(e){return Ze(e)?document.querySelector(e):e}let sf=!1;const lw=()=>{sf||(sf=!0,X_(),E_())},ow=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:mv,BaseTransitionPropsValidators:nu,Comment:Zt,DeprecationTypes:y_,EffectScope:Kd,ErrorCodes:wx,ErrorTypeStrings:p_,Fragment:rs,KeepAlive:t0,ReactiveEffect:zl,Static:Xn,Suspense:W0,Teleport:$x,Text:On,TrackOpTypes:vx,Transition:k_,TransitionGroup:W_,TriggerOpTypes:hx,VueElement:ac,assertNumber:_x,callWithAsyncErrorHandling:Ys,callWithErrorHandling:tl,camelize:Tt,capitalize:ri,cloneVNode:Ma,compatUtils:b_,computed:B,createApp:Cr,createBlock:br,createCommentVNode:Jv,createElementBlock:X0,createElementVNode:mu,createHydrationRenderer:Fv,createPropsRestProxy:k0,createRenderer:$v,createSSRApp:Eh,createSlots:r0,createStaticVNode:s_,createTextVNode:vu,createVNode:Bt,customRef:Ym,defineAsyncComponent:Xx,defineComponent:fo,defineCustomElement:mh,defineEmits:f0,defineExpose:m0,defineModel:g0,defineOptions:v0,defineProps:p0,defineSSRCustomElement:z_,defineSlots:h0,devtools:f_,effect:Fy,effectScope:Dy,getCurrentInstance:Ds,getCurrentScope:Mm,getCurrentWatcher:gx,getTransitionRawChildren:Qr,guardReactiveProps:Kv,h:qi,handleError:ci,hasInjectionContext:Ix,hydrate:iw,hydrateOnIdle:Wx,hydrateOnInteraction:Qx,hydrateOnMediaQuery:Zx,hydrateOnVisible:Jx,initCustomFormatter:c_,initDirectivesForSSR:lw,inject:ra,isMemoSame:ah,isProxy:uo,isReactive:tn,isReadonly:Na,isRef:Xt,isRuntimeOnly:l_,isShallow:qs,isVNode:cn,markRaw:Zm,mergeDefaults:_0,mergeModels:w0,mergeProps:Zv,nextTick:Ut,nodeOps:lh,normalizeClass:co,normalizeProps:wy,normalizeStyle:ro,onActivated:cs,onBeforeMount:gv,onBeforeUnmount:ec,onBeforeUpdate:lu,onDeactivated:es,onErrorCaptured:_v,onMounted:st,onRenderTracked:xv,onRenderTriggered:yv,onScopeDispose:Py,onServerPrefetch:bv,onUnmounted:wt,onUpdated:Xr,onWatcherCleanup:ev,openBlock:Jl,patchProp:fh,popScopeId:Ex,provide:Nl,proxyRefs:eu,pushScopeId:Tx,queuePostFlushCb:ql,reactive:Mn,readonly:dr,ref:f,registerRuntimeCompiler:eh,render:Th,renderList:o0,renderSlot:c0,resolveComponent:n0,resolveDirective:l0,resolveDynamicComponent:i0,resolveFilter:g_,resolveTransitionHooks:ji,setBlockTracking:Zl,setDevtoolsHook:m_,setTransitionHooks:rn,shallowReactive:Yd,shallowReadonly:ax,shallowRef:Xd,ssrContextKey:lv,ssrUtils:h_,stop:By,toDisplayString:Lm,toHandlerKey:Mi,toHandlers:d0,toRaw:ft,toRef:px,toRefs:cx,toValue:lx,transformVNodeArgs:e_,triggerRef:ix,unref:La,useAttrs:x0,useCssModule:q_,useCssVars:A_,useHost:vh,useId:Bx,useModel:L0,useSSRContext:ov,useShadowRoot:j_,useSlots:y0,useTemplateRef:Ux,useTransitionState:au,vModelCheckbox:bu,vModelDynamic:_h,vModelRadio:yu,vModelSelect:yh,vModelText:Sr,vShow:uh,version:nh,warn:u_,watch:Gt,watchEffect:Ox,watchPostEffect:Lx,watchSyncEffect:rv,withAsyncContext:S0,withCtx:su,withDefaults:b0,withDirectives:Rx,withKeys:nw,withMemo:d_,withModifiers:sw,withScopeId:Ax},Symbol.toStringTag,{value:"Module"}));/** * @vue/compiler-core v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const Gl=Symbol(""),Ol=Symbol(""),pu=Symbol(""),yr=Symbol(""),xv=Symbol(""),Xn=Symbol(""),_v=Symbol(""),wv=Symbol(""),fu=Symbol(""),mu=Symbol(""),co=Symbol(""),hu=Symbol(""),kv=Symbol(""),vu=Symbol(""),gu=Symbol(""),bu=Symbol(""),yu=Symbol(""),xu=Symbol(""),_u=Symbol(""),Sv=Symbol(""),Cv=Symbol(""),Qr=Symbol(""),xr=Symbol(""),wu=Symbol(""),ku=Symbol(""),Wl=Symbol(""),uo=Symbol(""),Su=Symbol(""),md=Symbol(""),z_=Symbol(""),hd=Symbol(""),_r=Symbol(""),H_=Symbol(""),j_=Symbol(""),Cu=Symbol(""),q_=Symbol(""),V_=Symbol(""),Tu=Symbol(""),Tv=Symbol(""),qi={[Gl]:"Fragment",[Ol]:"Teleport",[pu]:"Suspense",[yr]:"KeepAlive",[xv]:"BaseTransition",[Xn]:"openBlock",[_v]:"createBlock",[wv]:"createElementBlock",[fu]:"createVNode",[mu]:"createElementVNode",[co]:"createCommentVNode",[hu]:"createTextVNode",[kv]:"createStaticVNode",[vu]:"resolveComponent",[gu]:"resolveDynamicComponent",[bu]:"resolveDirective",[yu]:"resolveFilter",[xu]:"withDirectives",[_u]:"renderList",[Sv]:"renderSlot",[Cv]:"createSlots",[Qr]:"toDisplayString",[xr]:"mergeProps",[wu]:"normalizeClass",[ku]:"normalizeStyle",[Wl]:"normalizeProps",[uo]:"guardReactiveProps",[Su]:"toHandlers",[md]:"camelize",[z_]:"capitalize",[hd]:"toHandlerKey",[_r]:"setBlockTracking",[H_]:"pushScopeId",[j_]:"popScopeId",[Cu]:"withCtx",[q_]:"unref",[V_]:"isRef",[Tu]:"withMemo",[Tv]:"isMemoSame"};function G_(e){Object.getOwnPropertySymbols(e).forEach(t=>{qi[t]=e[t]})}const Ys={start:{line:1,column:1,offset:0},end:{line:1,column:1,offset:0},source:""};function W_(e,t=""){return{type:0,source:t,children:e,helpers:new Set,components:[],directives:[],hoists:[],imports:[],cached:[],temps:0,codegenNode:void 0,loc:Ys}}function Kl(e,t,s,a,n,i,l,o=!1,r=!1,c=!1,d=Ys){return e&&(o?(e.helper(Xn),e.helper(Wi(e.inSSR,c))):e.helper(Gi(e.inSSR,c)),l&&e.helper(xu)),{type:13,tag:t,props:s,children:a,patchFlag:n,dynamicProps:i,directives:l,isBlock:o,disableTracking:r,isComponent:c,loc:d}}function Jn(e,t=Ys){return{type:17,loc:t,elements:e}}function aa(e,t=Ys){return{type:15,loc:t,properties:e}}function Gt(e,t){return{type:16,loc:Ys,key:Qe(e)?et(e,!0):e,value:t}}function et(e,t=!1,s=Ys,a=0){return{type:4,loc:s,content:e,isStatic:t,constType:t?3:a}}function ha(e,t=Ys){return{type:8,loc:t,children:e}}function ss(e,t=[],s=Ys){return{type:14,loc:s,callee:e,arguments:t}}function Vi(e,t=void 0,s=!1,a=!1,n=Ys){return{type:18,params:e,returns:t,newline:s,isSlot:a,loc:n}}function vd(e,t,s,a=!0){return{type:19,test:e,consequent:t,alternate:s,newline:a,loc:Ys}}function K_(e,t,s=!1,a=!1){return{type:20,index:e,value:t,needPauseTracking:s,inVOnce:a,needArraySpread:!1,loc:Ys}}function J_(e){return{type:21,body:e,loc:Ys}}function Gi(e,t){return e||t?fu:mu}function Wi(e,t){return e||t?_v:wv}function Eu(e,{helper:t,removeHelper:s,inSSR:a}){e.isBlock||(e.isBlock=!0,s(Gi(a,e.isComponent)),t(Xn),t(Wi(a,e.isComponent)))}const Wp=new Uint8Array([123,123]),Kp=new Uint8Array([125,125]);function Jp(e){return e>=97&&e<=122||e>=65&&e<=90}function Ks(e){return e===32||e===10||e===9||e===12||e===13}function mn(e){return e===47||e===62||Ks(e)}function wr(e){const t=new Uint8Array(e.length);for(let s=0;s100){let l=-1,o=n;for(;l+1>>1;this.newlines[r]=0;l--)if(t>this.newlines[l]){i=l;break}return i>=0&&(s=i+2,a=t-this.newlines[i]),{column:a,line:s,offset:t}}peek(){return this.buffer.charCodeAt(this.index+1)}stateText(t){t===60?(this.index>this.sectionStart&&this.cbs.ontext(this.sectionStart,this.index),this.state=5,this.sectionStart=this.index):!this.inVPre&&t===this.delimiterOpen[0]&&(this.state=2,this.delimiterIndex=0,this.stateInterpolationOpen(t))}stateInterpolationOpen(t){if(t===this.delimiterOpen[this.delimiterIndex])if(this.delimiterIndex===this.delimiterOpen.length-1){const s=this.index+1-this.delimiterOpen.length;s>this.sectionStart&&this.cbs.ontext(this.sectionStart,s),this.state=3,this.sectionStart=s}else this.delimiterIndex++;else this.inRCDATA?(this.state=32,this.stateInRCDATA(t)):(this.state=1,this.stateText(t))}stateInterpolation(t){t===this.delimiterClose[0]&&(this.state=4,this.delimiterIndex=0,this.stateInterpolationClose(t))}stateInterpolationClose(t){t===this.delimiterClose[this.delimiterIndex]?this.delimiterIndex===this.delimiterClose.length-1?(this.cbs.oninterpolation(this.sectionStart,this.index+1),this.inRCDATA?this.state=32:this.state=1,this.sectionStart=this.index+1):this.delimiterIndex++:(this.state=3,this.stateInterpolation(t))}stateSpecialStartSequence(t){const s=this.sequenceIndex===this.currentSequence.length;if(!(s?mn(t):(t|32)===this.currentSequence[this.sequenceIndex]))this.inRCDATA=!1;else if(!s){this.sequenceIndex++;return}this.sequenceIndex=0,this.state=6,this.stateInTagName(t)}stateInRCDATA(t){if(this.sequenceIndex===this.currentSequence.length){if(t===62||Ks(t)){const s=this.index-this.currentSequence.length;if(this.sectionStart=t||(this.state===28?this.currentSequence===ys.CdataEnd?this.cbs.oncdata(this.sectionStart,t):this.cbs.oncomment(this.sectionStart,t):this.state===6||this.state===11||this.state===18||this.state===17||this.state===12||this.state===13||this.state===14||this.state===15||this.state===16||this.state===20||this.state===19||this.state===21||this.state===9||this.cbs.ontext(this.sectionStart,t))}emitCodePoint(t,s){}}function Zp(e,{compatConfig:t}){const s=t&&t[e];return e==="MODE"?s||3:s}function Zn(e,t){const s=Zp("MODE",t),a=Zp(e,t);return s===3?a===!0:a!==!1}function Jl(e,t,s,...a){return Zn(e,t)}function Au(e){throw e}function Ev(e){}function Ot(e,t,s,a){const n=`https://vuejs.org/error-reference/#compiler-${e}`,i=new SyntaxError(String(n));return i.code=e,i.loc=t,i}const zs=e=>e.type===4&&e.isStatic;function Av(e){switch(e){case"Teleport":case"teleport":return Ol;case"Suspense":case"suspense":return pu;case"KeepAlive":case"keep-alive":return yr;case"BaseTransition":case"base-transition":return xv}}const Q_=/^$|^\d|[^\$\w\xA0-\uFFFF]/,Ru=e=>!Q_.test(e),Rv=/[A-Za-z_$\xA0-\uFFFF]/,Y_=/[\.\?\w$\xA0-\uFFFF]/,X_=/\s+[.[]\s*|\s*[.[]\s+/g,Iv=e=>e.type===4?e.content:e.loc.source,ew=e=>{const t=Iv(e).trim().replace(X_,o=>o.trim());let s=0,a=[],n=0,i=0,l=null;for(let o=0;o|^\s*(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/,sw=e=>tw.test(Iv(e)),aw=sw;function sa(e,t,s=!1){for(let a=0;at.type===7&&t.name==="bind"&&(!t.arg||t.arg.type!==4||!t.arg.isStatic))}function Oc(e){return e.type===5||e.type===2}function Qp(e){return e.type===7&&e.name==="pre"}function iw(e){return e.type===7&&e.name==="slot"}function kr(e){return e.type===1&&e.tagType===3}function Sr(e){return e.type===1&&e.tagType===2}const lw=new Set([Wl,uo]);function Lv(e,t=[]){if(e&&!Qe(e)&&e.type===14){const s=e.callee;if(!Qe(s)&&lw.has(s))return Lv(e.arguments[0],t.concat(e))}return[e,t]}function Cr(e,t,s){let a,n=e.type===13?e.props:e.arguments[2],i=[],l;if(n&&!Qe(n)&&n.type===14){const o=Lv(n);n=o[0],i=o[1],l=i[i.length-1]}if(n==null||Qe(n))a=aa([t]);else if(n.type===14){const o=n.arguments[0];!Qe(o)&&o.type===15?Yp(t,o)||o.properties.unshift(t):n.callee===Su?a=ss(s.helper(xr),[aa([t]),n]):n.arguments.unshift(aa([t])),!a&&(a=n)}else n.type===15?(Yp(t,n)||n.properties.unshift(t),a=n):(a=ss(s.helper(xr),[aa([t]),n]),l&&l.callee===uo&&(l=i[i.length-2]));e.type===13?l?l.arguments[0]=a:e.props=a:l?l.arguments[0]=a:e.arguments[2]=a}function Yp(e,t){let s=!1;if(e.key.type===4){const a=e.key.content;s=t.properties.some(n=>n.key.type===4&&n.key.content===a)}return s}function Zl(e,t){return`_${t}_${e.replace(/[^\w]/g,(s,a)=>s==="-"?"_":e.charCodeAt(a).toString())}`}function ow(e){return e.type===14&&e.callee===Tu?e.arguments[1].returns:e}const rw=/([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/;function Nv(e){for(let t=0;t0,isVoidTag:Ti,isPreTag:Ti,isIgnoreNewlineTag:Ti,isCustomElement:Ti,onError:Au,onWarn:Ev,comments:!1,prefixIdentifiers:!1};let ft=Dv,Ql=null,Xa="",_s=null,ot=null,Ps="",za=-1,Un=-1,Ou=0,xn=!1,gd=null;const It=[],Ft=new Z_(It,{onerr:Fa,ontext(e,t){Ro(ps(e,t),e,t)},ontextentity(e,t,s){Ro(e,t,s)},oninterpolation(e,t){if(xn)return Ro(ps(e,t),e,t);let s=e+Ft.delimiterOpen.length,a=t-Ft.delimiterClose.length;for(;Ks(Xa.charCodeAt(s));)s++;for(;Ks(Xa.charCodeAt(a-1));)a--;let n=ps(s,a);n.includes("&")&&(n=ft.decodeEntities(n,!1)),bd({type:5,content:Zo(n,!1,zt(s,a)),loc:zt(e,t)})},onopentagname(e,t){const s=ps(e,t);_s={type:1,tag:s,ns:ft.getNamespace(s,It[0],ft.ns),tagType:0,props:[],children:[],loc:zt(e-1,t),codegenNode:void 0}},onopentagend(e){ef(e)},onclosetag(e,t){const s=ps(e,t);if(!ft.isVoidTag(s)){let a=!1;for(let n=0;n0&&Fa(24,It[0].loc.start.offset);for(let l=0;l<=n;l++){const o=It.shift();Jo(o,t,l(a.type===7?a.rawName:a.name)===s)&&Fa(2,t)},onattribend(e,t){if(_s&&ot){if(qn(ot.loc,t),e!==0)if(Ps.includes("&")&&(Ps=ft.decodeEntities(Ps,!0)),ot.type===6)ot.name==="class"&&(Ps=Fv(Ps).trim()),e===1&&!Ps&&Fa(13,t),ot.value={type:2,content:Ps,loc:e===1?zt(za,Un):zt(za-1,Un+1)},Ft.inSFCRoot&&_s.tag==="template"&&ot.name==="lang"&&Ps&&Ps!=="html"&&Ft.enterRCDATA(wr("n.content==="sync"))>-1&&Jl("COMPILER_V_BIND_SYNC",ft,ot.loc,ot.arg.loc.source)&&(ot.name="model",ot.modifiers.splice(a,1))}(ot.type!==7||ot.name!=="pre")&&_s.props.push(ot)}Ps="",za=Un=-1},oncomment(e,t){ft.comments&&bd({type:3,content:ps(e,t),loc:zt(e-4,t+3)})},onend(){const e=Xa.length;for(let t=0;t{const h=t.start.offset+f,g=h+u.length;return Zo(u,!1,zt(h,g),0,m?1:0)},o={source:l(i.trim(),s.indexOf(i,n.length)),value:void 0,key:void 0,index:void 0,finalized:!1};let r=n.trim().replace(cw,"").trim();const c=n.indexOf(r),d=r.match(Xp);if(d){r=r.replace(Xp,"").trim();const u=d[1].trim();let f;if(u&&(f=s.indexOf(u,c+r.length),o.key=l(u,f,!0)),d[2]){const m=d[2].trim();m&&(o.index=l(m,s.indexOf(m,o.key?f+u.length:c+r.length),!0))}}return r&&(o.value=l(r,c,!0)),o}function ps(e,t){return Xa.slice(e,t)}function ef(e){Ft.inSFCRoot&&(_s.innerLoc=zt(e+1,e+1)),bd(_s);const{tag:t,ns:s}=_s;s===0&&ft.isPreTag(t)&&Ou++,ft.isVoidTag(t)?Jo(_s,e):(It.unshift(_s),(s===1||s===2)&&(Ft.inXML=!0)),_s=null}function Ro(e,t,s){{const i=It[0]&&It[0].tag;i!=="script"&&i!=="style"&&e.includes("&")&&(e=ft.decodeEntities(e,!1))}const a=It[0]||Ql,n=a.children[a.children.length-1];n&&n.type===2?(n.content+=e,qn(n.loc,s)):a.children.push({type:2,content:e,loc:zt(t,s)})}function Jo(e,t,s=!1){s?qn(e.loc,Pv(t,60)):qn(e.loc,uw(t,62)+1),Ft.inSFCRoot&&(e.children.length?e.innerLoc.end=nt({},e.children[e.children.length-1].loc.end):e.innerLoc.end=nt({},e.innerLoc.start),e.innerLoc.source=ps(e.innerLoc.start.offset,e.innerLoc.end.offset));const{tag:a,ns:n,children:i}=e;if(xn||(a==="slot"?e.tagType=2:tf(e)?e.tagType=3:fw(e)&&(e.tagType=1)),Ft.inRCDATA||(e.children=$v(i)),n===0&&ft.isIgnoreNewlineTag(a)){const l=i[0];l&&l.type===2&&(l.content=l.content.replace(/^\r?\n/,""))}n===0&&ft.isPreTag(a)&&Ou--,gd===e&&(xn=Ft.inVPre=!1,gd=null),Ft.inXML&&(It[0]?It[0].ns:ft.ns)===0&&(Ft.inXML=!1);{const l=e.props;if(!Ft.inSFCRoot&&Zn("COMPILER_NATIVE_TEMPLATE",ft)&&e.tag==="template"&&!tf(e)){const r=It[0]||Ql,c=r.children.indexOf(e);r.children.splice(c,1,...e.children)}const o=l.find(r=>r.type===6&&r.name==="inline-template");o&&Jl("COMPILER_INLINE_TEMPLATE",ft,o.loc)&&e.children.length&&(o.value={type:2,content:ps(e.children[0].loc.start.offset,e.children[e.children.length-1].loc.end.offset),loc:o.loc})}}function uw(e,t){let s=e;for(;Xa.charCodeAt(s)!==t&&s=0;)s--;return s}const pw=new Set(["if","else","else-if","for","slot"]);function tf({tag:e,props:t}){if(e==="template"){for(let s=0;s64&&e<91}const hw=/\r\n/g;function $v(e){const t=ft.whitespace!=="preserve";let s=!1;for(let a=0;as.type!==3);return t.length===1&&t[0].type===1&&!Sr(t[0])?t[0]:null}function Qo(e,t,s,a=!1,n=!1){const{children:i}=e,l=[];for(let d=0;d0){if(f>=2){u.codegenNode.patchFlag=-1,l.push(u);continue}}else{const m=u.codegenNode;if(m.type===13){const h=m.patchFlag;if((h===void 0||h===512||h===1)&&zv(u,s)>=2){const g=Hv(u);g&&(m.props=s.hoist(g))}m.dynamicProps&&(m.dynamicProps=s.hoist(m.dynamicProps))}}}else if(u.type===12&&(a?0:Js(u,s))>=2){u.codegenNode.type===14&&u.codegenNode.arguments.length>0&&u.codegenNode.arguments.push("-1"),l.push(u);continue}if(u.type===1){const f=u.tagType===1;f&&s.scopes.vSlot++,Qo(u,e,s,!1,n),f&&s.scopes.vSlot--}else if(u.type===11)Qo(u,e,s,u.children.length===1,!0);else if(u.type===9)for(let f=0;fm.key===u||m.key.content===u);return f&&f.value}}l.length&&s.transformHoist&&s.transformHoist(i,s,e)}function Js(e,t){const{constantCache:s}=t;switch(e.type){case 1:if(e.tagType!==0)return 0;const a=s.get(e);if(a!==void 0)return a;const n=e.codegenNode;if(n.type!==13||n.isBlock&&e.tag!=="svg"&&e.tag!=="foreignObject"&&e.tag!=="math")return 0;if(n.patchFlag===void 0){let l=3;const o=zv(e,t);if(o===0)return s.set(e,0),0;o1)for(let r=0;rR&&(L.childIndex--,L.onNodeRemoved()),L.parent.children.splice(R,1)},onNodeRemoved:hs,addIdentifiers(E){},removeIdentifiers(E){},hoist(E){Qe(E)&&(E=et(E)),L.hoists.push(E);const P=et(`_hoisted_${L.hoists.length}`,!1,E.loc,2);return P.hoisted=E,P},cache(E,P=!1,R=!1){const I=K_(L.cached.length,E,P,R);return L.cached.push(I),I}};return L.filters=new Set,L}function Sw(e,t){const s=kw(e,t);Xr(e,s),t.hoistStatic&&_w(e,s),t.ssr||Cw(e,s),e.helpers=new Set([...s.helpers.keys()]),e.components=[...s.components],e.directives=[...s.directives],e.imports=s.imports,e.hoists=s.hoists,e.temps=s.temps,e.cached=s.cached,e.transformed=!0,e.filters=[...s.filters]}function Cw(e,t){const{helper:s}=t,{children:a}=e;if(a.length===1){const n=Bv(e);if(n&&n.codegenNode){const i=n.codegenNode;i.type===13&&Eu(i,t),e.codegenNode=i}else e.codegenNode=a[0]}else if(a.length>1){let n=64;e.codegenNode=Kl(t,s(Gl),void 0,e.children,n,void 0,void 0,!0,void 0,!1)}}function Tw(e,t){let s=0;const a=()=>{s--};for(;sa===e:a=>e.test(a);return(a,n)=>{if(a.type===1){const{props:i}=a;if(a.tagType===3&&i.some(iw))return;const l=[];for(let o=0;o`${qi[e]}: _${qi[e]}`;function Ew(e,{mode:t="function",prefixIdentifiers:s=t==="module",sourceMap:a=!1,filename:n="template.vue.html",scopeId:i=null,optimizeImports:l=!1,runtimeGlobalName:o="Vue",runtimeModuleName:r="vue",ssrRuntimeModuleName:c="vue/server-renderer",ssr:d=!1,isTS:u=!1,inSSR:f=!1}){const m={mode:t,prefixIdentifiers:s,sourceMap:a,filename:n,scopeId:i,optimizeImports:l,runtimeGlobalName:o,runtimeModuleName:r,ssrRuntimeModuleName:c,ssr:d,isTS:u,inSSR:f,source:e.source,code:"",column:1,line:1,offset:0,indentLevel:0,pure:!1,map:void 0,helper(g){return`_${qi[g]}`},push(g,A=-2,w){m.code+=g},indent(){h(++m.indentLevel)},deindent(g=!1){g?--m.indentLevel:h(--m.indentLevel)},newline(){h(m.indentLevel)}};function h(g){m.push(` -`+" ".repeat(g),0)}return m}function Aw(e,t={}){const s=Ew(e,t);t.onContextCreated&&t.onContextCreated(s);const{mode:a,push:n,prefixIdentifiers:i,indent:l,deindent:o,newline:r,scopeId:c,ssr:d}=s,u=Array.from(e.helpers),f=u.length>0,m=!i&&a!=="module";Rw(e,s);const g=d?"ssrRender":"render",w=(d?["_ctx","_push","_parent","_attrs"]:["_ctx","_cache"]).join(", ");if(n(`function ${g}(${w}) {`),l(),m&&(n("with (_ctx) {"),l(),f&&(n(`const { ${u.map(qv).join(", ")} } = _Vue -`,-1),r())),e.components.length&&(Lc(e.components,"component",s),(e.directives.length||e.temps>0)&&r()),e.directives.length&&(Lc(e.directives,"directive",s),e.temps>0&&r()),e.filters&&e.filters.length&&(r(),Lc(e.filters,"filter",s),r()),e.temps>0){n("let ");for(let y=0;y0?", ":""}_temp${y}`)}return(e.components.length||e.directives.length||e.temps)&&(n(` -`,0),r()),d||n("return "),e.codegenNode?Ss(e.codegenNode,s):n("null"),m&&(o(),n("}")),o(),n("}"),{ast:e,code:s.code,preamble:"",map:s.map?s.map.toJSON():void 0}}function Rw(e,t){const{ssr:s,prefixIdentifiers:a,push:n,newline:i,runtimeModuleName:l,runtimeGlobalName:o,ssrRuntimeModuleName:r}=t,c=o,d=Array.from(e.helpers);if(d.length>0&&(n(`const _Vue = ${c} -`,-1),e.hoists.length)){const u=[fu,mu,co,hu,kv].filter(f=>d.includes(f)).map(qv).join(", ");n(`const { ${u} } = _Vue -`,-1)}Iw(e.hoists,t),i(),n("return ")}function Lc(e,t,{helper:s,push:a,newline:n,isTS:i}){const l=s(t==="filter"?yu:t==="component"?vu:bu);for(let o=0;o3||!1;t.push("["),s&&t.indent(),po(e,t,s),s&&t.deindent(),t.push("]")}function po(e,t,s=!1,a=!0){const{push:n,newline:i}=t;for(let l=0;ls||"null")}function $w(e,t){const{push:s,helper:a,pure:n}=t,i=Qe(e.callee)?e.callee:a(e.callee);n&&s(ec),s(i+"(",-2,e),po(e.arguments,t),s(")")}function Fw(e,t){const{push:s,indent:a,deindent:n,newline:i}=t,{properties:l}=e;if(!l.length){s("{}",-2,e);return}const o=l.length>1||!1;s(o?"{":"{ "),o&&a();for(let r=0;r "),(r||o)&&(s("{"),a()),l?(r&&s("return "),Le(l)?Lu(l,t):Ss(l,t)):o&&Ss(o,t),(r||o)&&(n(),s("}")),c&&(e.isNonScopedSlot&&s(", undefined, true"),s(")"))}function zw(e,t){const{test:s,consequent:a,alternate:n,newline:i}=e,{push:l,indent:o,deindent:r,newline:c}=t;if(s.type===4){const u=!Ru(s.content);u&&l("("),Vv(s,t),u&&l(")")}else l("("),Ss(s,t),l(")");i&&o(),t.indentLevel++,i||l(" "),l("? "),Ss(a,t),t.indentLevel--,i&&c(),i||l(" "),l(": ");const d=n.type===19;d||t.indentLevel++,Ss(n,t),d||t.indentLevel--,i&&r(!0)}function Hw(e,t){const{push:s,helper:a,indent:n,deindent:i,newline:l}=t,{needPauseTracking:o,needArraySpread:r}=e;r&&s("[...("),s(`_cache[${e.index}] || (`),o&&(n(),s(`${a(_r)}(-1`),e.inVOnce&&s(", true"),s("),"),l(),s("(")),s(`_cache[${e.index}] = `),Ss(e.value,t),o&&(s(`).cacheIndex = ${e.index},`),l(),s(`${a(_r)}(1),`),l(),s(`_cache[${e.index}]`),i()),s(")"),r&&s(")]")}new RegExp("\\b"+"arguments,await,break,case,catch,class,const,continue,debugger,default,delete,do,else,export,extends,finally,for,function,if,import,let,new,return,super,switch,throw,try,var,void,while,with,yield".split(",").join("\\b|\\b")+"\\b");const jw=jv(/^(?:if|else|else-if)$/,(e,t,s)=>qw(e,t,s,(a,n,i)=>{const l=s.parent.children;let o=l.indexOf(a),r=0;for(;o-->=0;){const c=l[o];c&&c.type===9&&(r+=c.branches.length)}return()=>{if(i)a.codegenNode=af(n,r,s);else{const c=Vw(a.codegenNode);c.alternate=af(n,r+a.branches.length-1,s)}}}));function qw(e,t,s,a){if(t.name!=="else"&&(!t.exp||!t.exp.content.trim())){const n=t.exp?t.exp.loc:e.loc;s.onError(Ot(28,t.loc)),t.exp=et("true",!1,n)}if(t.name==="if"){const n=sf(e,t),i={type:9,loc:gw(e.loc),branches:[n]};if(s.replaceNode(i),a)return a(i,n,!0)}else{const n=s.parent.children;let i=n.indexOf(e);for(;i-->=-1;){const l=n[i];if(l&&Mv(l)){s.removeNode(l);continue}if(l&&l.type===9){(t.name==="else-if"||t.name==="else")&&l.branches[l.branches.length-1].condition===void 0&&s.onError(Ot(30,e.loc)),s.removeNode();const o=sf(e,t);l.branches.push(o);const r=a&&a(l,o,!1);Xr(o,s),r&&r(),s.currentNode=null}else s.onError(Ot(30,e.loc));break}}}function sf(e,t){const s=e.tagType===3;return{type:10,loc:e.loc,condition:t.name==="else"?void 0:t.exp,children:s&&!sa(e,"for")?e.children:[e],userKey:Yr(e,"key"),isTemplateIf:s}}function af(e,t,s){return e.condition?vd(e.condition,nf(e,t,s),ss(s.helper(co),['""',"true"])):nf(e,t,s)}function nf(e,t,s){const{helper:a}=s,n=Gt("key",et(`${t}`,!1,Ys,2)),{children:i}=e,l=i[0];if(i.length!==1||l.type!==1)if(i.length===1&&l.type===11){const r=l.codegenNode;return Cr(r,n,s),r}else return Kl(s,a(Gl),aa([n]),i,64,void 0,void 0,!0,!1,!1,e.loc);else{const r=l.codegenNode,c=ow(r);return c.type===13&&Eu(c,s),Cr(c,n,s),r}}function Vw(e){for(;;)if(e.type===19)if(e.alternate.type===19)e=e.alternate;else return e;else e.type===20&&(e=e.value)}const Gw=jv("for",(e,t,s)=>{const{helper:a,removeHelper:n}=s;return Ww(e,t,s,i=>{const l=ss(a(_u),[i.source]),o=kr(e),r=sa(e,"memo"),c=Yr(e,"key",!1,!0);c&&c.type;let d=c&&(c.type===6?c.value?et(c.value.content,!0):void 0:c.exp);const u=d?Gt("key",d):null,f=i.source.type===4&&i.source.constType>0,m=f?64:c?128:256;return i.codegenNode=Kl(s,a(Gl),void 0,l,m,void 0,void 0,!0,!f,!1,e.loc),()=>{let h;const{children:g}=i,A=g.length!==1||g[0].type!==1,w=Sr(e)?e:o&&e.children.length===1&&Sr(e.children[0])?e.children[0]:null;if(w?(h=w.codegenNode,o&&u&&Cr(h,u,s)):A?h=Kl(s,a(Gl),u?aa([u]):void 0,e.children,64,void 0,void 0,!0,void 0,!1):(h=g[0].codegenNode,o&&u&&Cr(h,u,s),h.isBlock!==!f&&(h.isBlock?(n(Xn),n(Wi(s.inSSR,h.isComponent))):n(Gi(s.inSSR,h.isComponent))),h.isBlock=!f,h.isBlock?(a(Xn),a(Wi(s.inSSR,h.isComponent))):a(Gi(s.inSSR,h.isComponent))),r){const y=Vi(yd(i.parseResult,[et("_cached")]));y.body=J_([ha(["const _memo = (",r.exp,")"]),ha(["if (_cached && _cached.el",...d?[" && _cached.key === ",d]:[],` && ${s.helperString(Tv)}(_cached, _memo)) return _cached`]),ha(["const _item = ",h]),et("_item.memo = _memo"),et("return _item")]),l.arguments.push(y,et("_cache"),et(String(s.cached.length))),s.cached.push(null)}else l.arguments.push(Vi(yd(i.parseResult),h,!0))}})});function Ww(e,t,s,a){if(!t.exp){s.onError(Ot(31,t.loc));return}const n=t.forParseResult;if(!n){s.onError(Ot(32,t.loc));return}Wv(n);const{addIdentifiers:i,removeIdentifiers:l,scopes:o}=s,{source:r,value:c,key:d,index:u}=n,f={type:11,loc:t.loc,source:r,valueAlias:c,keyAlias:d,objectIndexAlias:u,parseResult:n,children:kr(e)?e.children:[e]};s.replaceNode(f),o.vFor++;const m=a&&a(f);return()=>{o.vFor--,m&&m()}}function Wv(e,t){e.finalized||(e.finalized=!0)}function yd({value:e,key:t,index:s},a=[]){return Kw([e,t,s,...a])}function Kw(e){let t=e.length;for(;t--&&!e[t];);return e.slice(0,t+1).map((s,a)=>s||et("_".repeat(a+1),!1))}const lf=et("undefined",!1),Jw=(e,t)=>{if(e.type===1&&(e.tagType===1||e.tagType===3)){const s=sa(e,"slot");if(s)return s.exp,t.scopes.vSlot++,()=>{t.scopes.vSlot--}}},Zw=(e,t,s,a)=>Vi(e,s,!1,!0,s.length?s[0].loc:a);function Qw(e,t,s=Zw){t.helper(Cu);const{children:a,loc:n}=e,i=[],l=[];let o=t.scopes.vSlot>0||t.scopes.vFor>0;const r=sa(e,"slot",!0);if(r){const{arg:A,exp:w}=r;A&&!zs(A)&&(o=!0),i.push(Gt(A||et("default",!0),s(w,void 0,a,n)))}let c=!1,d=!1;const u=[],f=new Set;let m=0;for(let A=0;A{const b=s(w,void 0,y,n);return t.compatConfig&&(b.isNonScopedSlot=!0),Gt("default",b)};c?u.length&&!u.every(Iu)&&(d?t.onError(Ot(39,u[0].loc)):i.push(A(void 0,u))):i.push(A(void 0,a))}const h=o?2:Yo(e.children)?3:1;let g=aa(i.concat(Gt("_",et(h+"",!1))),n);return l.length&&(g=ss(t.helper(Cv),[g,Jn(l)])),{slots:g,hasDynamicSlots:o}}function Io(e,t,s){const a=[Gt("name",e),Gt("fn",t)];return s!=null&&a.push(Gt("key",et(String(s),!0))),aa(a)}function Yo(e){for(let t=0;tfunction(){if(e=t.currentNode,!(e.type===1&&(e.tagType===0||e.tagType===1)))return;const{tag:a,props:n}=e,i=e.tagType===1;let l=i?Xw(e,t):`"${a}"`;const o=mt(l)&&l.callee===gu;let r,c,d=0,u,f,m,h=o||l===Ol||l===pu||!i&&(a==="svg"||a==="foreignObject"||a==="math");if(n.length>0){const g=Jv(e,t,void 0,i,o);r=g.props,d=g.patchFlag,f=g.dynamicPropNames;const A=g.directives;m=A&&A.length?Jn(A.map(w=>tk(w,t))):void 0,g.shouldUseBlock&&(h=!0)}if(e.children.length>0)if(l===yr&&(h=!0,d|=1024),i&&l!==Ol&&l!==yr){const{slots:A,hasDynamicSlots:w}=Qw(e,t);c=A,w&&(d|=1024)}else if(e.children.length===1&&l!==Ol){const A=e.children[0],w=A.type,y=w===5||w===8;y&&Js(A,t)===0&&(d|=1),y||w===2?c=A:c=e.children}else c=e.children;f&&f.length&&(u=sk(f)),e.codegenNode=Kl(t,l,r,c,d===0?void 0:d,u,m,!!h,!1,i,e.loc)};function Xw(e,t,s=!1){let{tag:a}=e;const n=xd(a),i=Yr(e,"is",!1,!0);if(i)if(n||Zn("COMPILER_IS_ON_ELEMENT",t)){let o;if(i.type===6?o=i.value&&et(i.value.content,!0):(o=i.exp,o||(o=et("is",!1,i.arg.loc))),o)return ss(t.helper(gu),[o])}else i.type===6&&i.value.content.startsWith("vue:")&&(a=i.value.content.slice(4));const l=Av(a)||t.isBuiltInComponent(a);return l?(s||t.helper(l),l):(t.helper(vu),t.components.add(a),Zl(a,"component"))}function Jv(e,t,s=e.props,a,n,i=!1){const{tag:l,loc:o,children:r}=e;let c=[];const d=[],u=[],f=r.length>0;let m=!1,h=0,g=!1,A=!1,w=!1,y=!1,b=!1,x=!1;const S=[],_=P=>{c.length&&(d.push(aa(of(c),o)),c=[]),P&&d.push(P)},T=()=>{t.scopes.vFor>0&&c.push(Gt(et("ref_for",!0),et("true")))},L=({key:P,value:R})=>{if(zs(P)){const I=P.content,H=ti(I);if(H&&(!a||n)&&I.toLowerCase()!=="onclick"&&I!=="onUpdate:modelValue"&&!Za(I)&&(y=!0),H&&Za(I)&&(x=!0),H&&R.type===14&&(R=R.arguments[0]),R.type===20||(R.type===4||R.type===8)&&Js(R,t)>0)return;I==="ref"?g=!0:I==="class"?A=!0:I==="style"?w=!0:I!=="key"&&!S.includes(I)&&S.push(I),a&&(I==="class"||I==="style")&&!S.includes(I)&&S.push(I)}else b=!0};for(let P=0;Pue.content==="prop")&&(h|=32);const ce=t.directiveTransforms[I];if(ce){const{props:ue,needRuntime:Z}=ce(R,e,t);!i&&ue.forEach(L),$&&H&&!zs(H)?_(aa(ue,o)):c.push(...ue),Z&&(u.push(R),Cs(Z)&&Kv.set(R,Z))}else Gb(I)||(u.push(R),f&&(m=!0))}}let E;if(d.length?(_(),d.length>1?E=ss(t.helper(xr),d,o):E=d[0]):c.length&&(E=aa(of(c),o)),b?h|=16:(A&&!a&&(h|=2),w&&!a&&(h|=4),S.length&&(h|=8),y&&(h|=32)),!m&&(h===0||h===32)&&(g||x||u.length>0)&&(h|=512),!t.inSSR&&E)switch(E.type){case 15:let P=-1,R=-1,I=!1;for(let Q=0;QGt(l,i)),n))}return Jn(s,e.loc)}function sk(e){let t="[";for(let s=0,a=e.length;s{if(Sr(e)){const{children:s,loc:a}=e,{slotName:n,slotProps:i}=nk(e,t),l=[t.prefixIdentifiers?"_ctx.$slots":"$slots",n,"{}","undefined","true"];let o=2;i&&(l[2]=i,o=3),s.length&&(l[3]=Vi([],s,!1,!1,a),o=4),t.scopeId&&!t.slotted&&(o=5),l.splice(o),e.codegenNode=ss(t.helper(Sv),l,a)}};function nk(e,t){let s='"default"',a;const n=[];for(let i=0;i0){const{props:i,directives:l}=Jv(e,t,n,!1,!1);a=i,l.length&&t.onError(Ot(36,l[0].loc))}return{slotName:s,slotProps:a}}const Zv=(e,t,s,a)=>{const{loc:n,modifiers:i,arg:l}=e;!e.exp&&!i.length&&s.onError(Ot(35,n));let o;if(l.type===4)if(l.isStatic){let u=l.content;u.startsWith("vue:")&&(u=`vnode-${u.slice(4)}`);const f=t.tagType!==0||u.startsWith("vnode")||!/[A-Z]/.test(u)?Oi(wt(u)):`on:${u}`;o=et(f,!0,l.loc)}else o=ha([`${s.helperString(hd)}(`,l,")"]);else o=l,o.children.unshift(`${s.helperString(hd)}(`),o.children.push(")");let r=e.exp;r&&!r.content.trim()&&(r=void 0);let c=s.cacheHandlers&&!r&&!s.inVOnce;if(r){const u=Ov(r),f=!(u||aw(r)),m=r.content.includes(";");(f||c&&u)&&(r=ha([`${f?"$event":"(...args)"} => ${m?"{":"("}`,r,m?"}":")"]))}let d={props:[Gt(o,r||et("() => {}",!1,n))]};return a&&(d=a(d)),c&&(d.props[0].value=s.cache(d.props[0].value)),d.props.forEach(u=>u.key.isHandlerKey=!0),d},ik=(e,t,s)=>{const{modifiers:a,loc:n}=e,i=e.arg;let{exp:l}=e;return l&&l.type===4&&!l.content.trim()&&(l=void 0),i.type!==4?(i.children.unshift("("),i.children.push(') || ""')):i.isStatic||(i.content=i.content?`${i.content} || ""`:'""'),a.some(o=>o.content==="camel")&&(i.type===4?i.isStatic?i.content=wt(i.content):i.content=`${s.helperString(md)}(${i.content})`:(i.children.unshift(`${s.helperString(md)}(`),i.children.push(")"))),s.inSSR||(a.some(o=>o.content==="prop")&&rf(i,"."),a.some(o=>o.content==="attr")&&rf(i,"^")),{props:[Gt(i,l)]}},rf=(e,t)=>{e.type===4?e.isStatic?e.content=t+e.content:e.content=`\`${t}\${${e.content}}\``:(e.children.unshift(`'${t}' + (`),e.children.push(")"))},lk=(e,t)=>{if(e.type===0||e.type===1||e.type===11||e.type===10)return()=>{const s=e.children;let a,n=!1;for(let i=0;ii.type===7&&!t.directiveTransforms[i.name])&&e.tag!=="template")))for(let i=0;i{if(e.type===1&&sa(e,"once",!0))return cf.has(e)||t.inVOnce||t.inSSR?void 0:(cf.add(e),t.inVOnce=!0,t.helper(_r),()=>{t.inVOnce=!1;const s=t.currentNode;s.codegenNode&&(s.codegenNode=t.cache(s.codegenNode,!0,!0))})},Qv=(e,t,s)=>{const{exp:a,arg:n}=e;if(!a)return s.onError(Ot(41,e.loc)),fl();const i=a.loc.source.trim(),l=a.type===4?a.content:i,o=s.bindingMetadata[i];if(o==="props"||o==="props-aliased")return s.onError(Ot(44,a.loc)),fl();if(o==="literal-const"||o==="setup-const")return s.onError(Ot(45,a.loc)),fl();if(!l.trim()||!Ov(a))return s.onError(Ot(42,a.loc)),fl();const r=n||et("modelValue",!0),c=n?zs(n)?`onUpdate:${wt(n.content)}`:ha(['"onUpdate:" + ',n]):"onUpdate:modelValue";let d;const u=s.isTS?"($event: any)":"$event";d=ha([`${u} => ((`,a,") = $event)"]);const f=[Gt(r,e.exp),Gt(c,d)];if(e.modifiers.length&&t.tagType===1){const m=e.modifiers.map(g=>g.content).map(g=>(Ru(g)?g:JSON.stringify(g))+": true").join(", "),h=n?zs(n)?`${n.content}Modifiers`:ha([n,' + "Modifiers"']):"modelModifiers";f.push(Gt(h,et(`{ ${m} }`,!1,e.loc,2)))}return fl(f)};function fl(e=[]){return{props:e}}const rk=/[\w).+\-_$\]]/,ck=(e,t)=>{Zn("COMPILER_FILTERS",t)&&(e.type===5?Tr(e.content,t):e.type===1&&e.props.forEach(s=>{s.type===7&&s.name!=="for"&&s.exp&&Tr(s.exp,t)}))};function Tr(e,t){if(e.type===4)df(e,t);else for(let s=0;s=0&&(y=s.charAt(w),y===" ");w--);(!y||!rk.test(y))&&(l=!0)}}h===void 0?h=s.slice(0,m).trim():d!==0&&A();function A(){g.push(s.slice(d,m).trim()),d=m+1}if(g.length){for(m=0;m{if(e.type===1){const s=sa(e,"memo");return!s||uf.has(e)||t.inSSR?void 0:(uf.add(e),()=>{const a=e.codegenNode||t.currentNode.codegenNode;a&&a.type===13&&(e.tagType!==1&&Eu(a,t),e.codegenNode=ss(t.helper(Tu),[s.exp,Vi(void 0,a),"_cache",String(t.cached.length)]),t.cached.push(null))})}},pk=(e,t)=>{if(e.type===1){for(const s of e.props)if(s.type===7&&s.name==="bind"&&(!s.exp||s.exp.type===4&&!s.exp.content.trim())&&s.arg){const a=s.arg;if(a.type!==4||!a.isStatic)t.onError(Ot(53,a.loc)),s.exp=et("",!0,a.loc);else{const n=wt(a.content);(Rv.test(n[0])||n[0]==="-")&&(s.exp=et(n,!1,a.loc))}}}};function fk(e){return[[pk,ok,jw,uk,Gw,ck,ak,Yw,Jw,lk],{on:Zv,bind:ik,model:Qv}]}function mk(e,t={}){const s=t.onError||Au,a=t.mode==="module";t.prefixIdentifiers===!0?s(Ot(48)):a&&s(Ot(49));const n=!1;t.cacheHandlers&&s(Ot(50)),t.scopeId&&!a&&s(Ot(51));const i=nt({},t,{prefixIdentifiers:n}),l=Qe(e)?xw(e,i):e,[o,r]=fk();return Sw(l,nt({},i,{nodeTransforms:[...o,...t.nodeTransforms||[]],directiveTransforms:nt({},r,t.directiveTransforms||{})})),Aw(l,i)}const hk=()=>({props:[]});/** +**/const Yl=Symbol(""),$l=Symbol(""),xu=Symbol(""),Tr=Symbol(""),Ih=Symbol(""),ni=Symbol(""),Oh=Symbol(""),Lh=Symbol(""),_u=Symbol(""),wu=Symbol(""),ho=Symbol(""),ku=Symbol(""),Nh=Symbol(""),Su=Symbol(""),Cu=Symbol(""),Tu=Symbol(""),Eu=Symbol(""),Au=Symbol(""),Ru=Symbol(""),Mh=Symbol(""),Dh=Symbol(""),nc=Symbol(""),Er=Symbol(""),Iu=Symbol(""),Ou=Symbol(""),Xl=Symbol(""),go=Symbol(""),Lu=Symbol(""),wd=Symbol(""),rw=Symbol(""),kd=Symbol(""),Ar=Symbol(""),cw=Symbol(""),dw=Symbol(""),Nu=Symbol(""),uw=Symbol(""),pw=Symbol(""),Mu=Symbol(""),Ph=Symbol(""),Wi={[Yl]:"Fragment",[$l]:"Teleport",[xu]:"Suspense",[Tr]:"KeepAlive",[Ih]:"BaseTransition",[ni]:"openBlock",[Oh]:"createBlock",[Lh]:"createElementBlock",[_u]:"createVNode",[wu]:"createElementVNode",[ho]:"createCommentVNode",[ku]:"createTextVNode",[Nh]:"createStaticVNode",[Su]:"resolveComponent",[Cu]:"resolveDynamicComponent",[Tu]:"resolveDirective",[Eu]:"resolveFilter",[Au]:"withDirectives",[Ru]:"renderList",[Mh]:"renderSlot",[Dh]:"createSlots",[nc]:"toDisplayString",[Er]:"mergeProps",[Iu]:"normalizeClass",[Ou]:"normalizeStyle",[Xl]:"normalizeProps",[go]:"guardReactiveProps",[Lu]:"toHandlers",[wd]:"camelize",[rw]:"capitalize",[kd]:"toHandlerKey",[Ar]:"setBlockTracking",[cw]:"pushScopeId",[dw]:"popScopeId",[Nu]:"withCtx",[uw]:"unref",[pw]:"isRef",[Mu]:"withMemo",[Ph]:"isMemoSame"};function fw(e){Object.getOwnPropertySymbols(e).forEach(t=>{Wi[t]=e[t]})}const ea={start:{line:1,column:1,offset:0},end:{line:1,column:1,offset:0},source:""};function mw(e,t=""){return{type:0,source:t,children:e,helpers:new Set,components:[],directives:[],hoists:[],imports:[],cached:[],temps:0,codegenNode:void 0,loc:ea}}function eo(e,t,s,a,n,i,l,o=!1,r=!1,c=!1,d=ea){return e&&(o?(e.helper(ni),e.helper(Zi(e.inSSR,c))):e.helper(Ji(e.inSSR,c)),l&&e.helper(Au)),{type:13,tag:t,props:s,children:a,patchFlag:n,dynamicProps:i,directives:l,isBlock:o,disableTracking:r,isComponent:c,loc:d}}function ei(e,t=ea){return{type:17,loc:t,elements:e}}function oa(e,t=ea){return{type:15,loc:t,properties:e}}function Yt(e,t){return{type:16,loc:ea,key:Ze(e)?tt(e,!0):e,value:t}}function tt(e,t=!1,s=ea,a=0){return{type:4,loc:s,content:e,isStatic:t,constType:t?3:a}}function va(e,t=ea){return{type:8,loc:t,children:e}}function ns(e,t=[],s=ea){return{type:14,loc:s,callee:e,arguments:t}}function Ki(e,t=void 0,s=!1,a=!1,n=ea){return{type:18,params:e,returns:t,newline:s,isSlot:a,loc:n}}function Sd(e,t,s,a=!0){return{type:19,test:e,consequent:t,alternate:s,newline:a,loc:ea}}function vw(e,t,s=!1,a=!1){return{type:20,index:e,value:t,needPauseTracking:s,inVOnce:a,needArraySpread:!1,loc:ea}}function hw(e){return{type:21,body:e,loc:ea}}function Ji(e,t){return e||t?_u:wu}function Zi(e,t){return e||t?Oh:Lh}function Du(e,{helper:t,removeHelper:s,inSSR:a}){e.isBlock||(e.isBlock=!0,s(Ji(a,e.isComponent)),t(ni),t(Zi(a,e.isComponent)))}const af=new Uint8Array([123,123]),nf=new Uint8Array([125,125]);function lf(e){return e>=97&&e<=122||e>=65&&e<=90}function Zs(e){return e===32||e===10||e===9||e===12||e===13}function wn(e){return e===47||e===62||Zs(e)}function Rr(e){const t=new Uint8Array(e.length);for(let s=0;s100){let l=-1,o=n;for(;l+1>>1;this.newlines[r]=0;l--)if(t>this.newlines[l]){i=l;break}return i>=0&&(s=i+2,a=t-this.newlines[i]),{column:a,line:s,offset:t}}peek(){return this.buffer.charCodeAt(this.index+1)}stateText(t){t===60?(this.index>this.sectionStart&&this.cbs.ontext(this.sectionStart,this.index),this.state=5,this.sectionStart=this.index):!this.inVPre&&t===this.delimiterOpen[0]&&(this.state=2,this.delimiterIndex=0,this.stateInterpolationOpen(t))}stateInterpolationOpen(t){if(t===this.delimiterOpen[this.delimiterIndex])if(this.delimiterIndex===this.delimiterOpen.length-1){const s=this.index+1-this.delimiterOpen.length;s>this.sectionStart&&this.cbs.ontext(this.sectionStart,s),this.state=3,this.sectionStart=s}else this.delimiterIndex++;else this.inRCDATA?(this.state=32,this.stateInRCDATA(t)):(this.state=1,this.stateText(t))}stateInterpolation(t){t===this.delimiterClose[0]&&(this.state=4,this.delimiterIndex=0,this.stateInterpolationClose(t))}stateInterpolationClose(t){t===this.delimiterClose[this.delimiterIndex]?this.delimiterIndex===this.delimiterClose.length-1?(this.cbs.oninterpolation(this.sectionStart,this.index+1),this.inRCDATA?this.state=32:this.state=1,this.sectionStart=this.index+1):this.delimiterIndex++:(this.state=3,this.stateInterpolation(t))}stateSpecialStartSequence(t){const s=this.sequenceIndex===this.currentSequence.length;if(!(s?wn(t):(t|32)===this.currentSequence[this.sequenceIndex]))this.inRCDATA=!1;else if(!s){this.sequenceIndex++;return}this.sequenceIndex=0,this.state=6,this.stateInTagName(t)}stateInRCDATA(t){if(this.sequenceIndex===this.currentSequence.length){if(t===62||Zs(t)){const s=this.index-this.currentSequence.length;if(this.sectionStart=t||(this.state===28?this.currentSequence===ys.CdataEnd?this.cbs.oncdata(this.sectionStart,t):this.cbs.oncomment(this.sectionStart,t):this.state===6||this.state===11||this.state===18||this.state===17||this.state===12||this.state===13||this.state===14||this.state===15||this.state===16||this.state===20||this.state===19||this.state===21||this.state===9||this.cbs.ontext(this.sectionStart,t))}emitCodePoint(t,s){}}function of(e,{compatConfig:t}){const s=t&&t[e];return e==="MODE"?s||3:s}function ti(e,t){const s=of("MODE",t),a=of(e,t);return s===3?a===!0:a!==!1}function to(e,t,s,...a){return ti(e,t)}function Pu(e){throw e}function $h(e){}function Mt(e,t,s,a){const n=`https://vuejs.org/error-reference/#compiler-${e}`,i=new SyntaxError(String(n));return i.code=e,i.loc=t,i}const js=e=>e.type===4&&e.isStatic;function Fh(e){switch(e){case"Teleport":case"teleport":return $l;case"Suspense":case"suspense":return xu;case"KeepAlive":case"keep-alive":return Tr;case"BaseTransition":case"base-transition":return Ih}}const bw=/^$|^\d|[^\$\w\xA0-\uFFFF]/,$u=e=>!bw.test(e),Bh=/[A-Za-z_$\xA0-\uFFFF]/,yw=/[\.\?\w$\xA0-\uFFFF]/,xw=/\s+[.[]\s*|\s*[.[]\s+/g,Uh=e=>e.type===4?e.content:e.loc.source,_w=e=>{const t=Uh(e).trim().replace(xw,o=>o.trim());let s=0,a=[],n=0,i=0,l=null;for(let o=0;o|^\s*(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/,kw=e=>ww.test(Uh(e)),Sw=kw;function la(e,t,s=!1){for(let a=0;at.type===7&&t.name==="bind"&&(!t.arg||t.arg.type!==4||!t.arg.isStatic))}function zc(e){return e.type===5||e.type===2}function rf(e){return e.type===7&&e.name==="pre"}function Tw(e){return e.type===7&&e.name==="slot"}function Ir(e){return e.type===1&&e.tagType===3}function Or(e){return e.type===1&&e.tagType===2}const Ew=new Set([Xl,go]);function Hh(e,t=[]){if(e&&!Ze(e)&&e.type===14){const s=e.callee;if(!Ze(s)&&Ew.has(s))return Hh(e.arguments[0],t.concat(e))}return[e,t]}function Lr(e,t,s){let a,n=e.type===13?e.props:e.arguments[2],i=[],l;if(n&&!Ze(n)&&n.type===14){const o=Hh(n);n=o[0],i=o[1],l=i[i.length-1]}if(n==null||Ze(n))a=oa([t]);else if(n.type===14){const o=n.arguments[0];!Ze(o)&&o.type===15?cf(t,o)||o.properties.unshift(t):n.callee===Lu?a=ns(s.helper(Er),[oa([t]),n]):n.arguments.unshift(oa([t])),!a&&(a=n)}else n.type===15?(cf(t,n)||n.properties.unshift(t),a=n):(a=ns(s.helper(Er),[oa([t]),n]),l&&l.callee===go&&(l=i[i.length-2]));e.type===13?l?l.arguments[0]=a:e.props=a:l?l.arguments[0]=a:e.arguments[2]=a}function cf(e,t){let s=!1;if(e.key.type===4){const a=e.key.content;s=t.properties.some(n=>n.key.type===4&&n.key.content===a)}return s}function so(e,t){return`_${t}_${e.replace(/[^\w]/g,(s,a)=>s==="-"?"_":e.charCodeAt(a).toString())}`}function Aw(e){return e.type===14&&e.callee===Mu?e.arguments[1].returns:e}const Rw=/([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/;function jh(e){for(let t=0;t0,isVoidTag:Ri,isPreTag:Ri,isIgnoreNewlineTag:Ri,isCustomElement:Ri,onError:Pu,onWarn:$h,comments:!1,prefixIdentifiers:!1};let vt=Vh,ao=null,an="",_s=null,dt=null,Fs="",Va=-1,Vn=-1,Bu=0,An=!1,Cd=null;const Nt=[],Vt=new gw(Nt,{onerr:Ha,ontext(e,t){Fo(ps(e,t),e,t)},ontextentity(e,t,s){Fo(e,t,s)},oninterpolation(e,t){if(An)return Fo(ps(e,t),e,t);let s=e+Vt.delimiterOpen.length,a=t-Vt.delimiterClose.length;for(;Zs(an.charCodeAt(s));)s++;for(;Zs(an.charCodeAt(a-1));)a--;let n=ps(s,a);n.includes("&")&&(n=vt.decodeEntities(n,!1)),Td({type:5,content:ar(n,!1,Jt(s,a)),loc:Jt(e,t)})},onopentagname(e,t){const s=ps(e,t);_s={type:1,tag:s,ns:vt.getNamespace(s,Nt[0],vt.ns),tagType:0,props:[],children:[],loc:Jt(e-1,t),codegenNode:void 0}},onopentagend(e){uf(e)},onclosetag(e,t){const s=ps(e,t);if(!vt.isVoidTag(s)){let a=!1;for(let n=0;n0&&Ha(24,Nt[0].loc.start.offset);for(let l=0;l<=n;l++){const o=Nt.shift();sr(o,t,l(a.type===7?a.rawName:a.name)===s)&&Ha(2,t)},onattribend(e,t){if(_s&&dt){if(Jn(dt.loc,t),e!==0)if(Fs.includes("&")&&(Fs=vt.decodeEntities(Fs,!0)),dt.type===6)dt.name==="class"&&(Fs=Kh(Fs).trim()),e===1&&!Fs&&Ha(13,t),dt.value={type:2,content:Fs,loc:e===1?Jt(Va,Vn):Jt(Va-1,Vn+1)},Vt.inSFCRoot&&_s.tag==="template"&&dt.name==="lang"&&Fs&&Fs!=="html"&&Vt.enterRCDATA(Rr("n.content==="sync"))>-1&&to("COMPILER_V_BIND_SYNC",vt,dt.loc,dt.arg.loc.source)&&(dt.name="model",dt.modifiers.splice(a,1))}(dt.type!==7||dt.name!=="pre")&&_s.props.push(dt)}Fs="",Va=Vn=-1},oncomment(e,t){vt.comments&&Td({type:3,content:ps(e,t),loc:Jt(e-4,t+3)})},onend(){const e=an.length;for(let t=0;t{const h=t.start.offset+p,y=h+u.length;return ar(u,!1,Jt(h,y),0,m?1:0)},o={source:l(i.trim(),s.indexOf(i,n.length)),value:void 0,key:void 0,index:void 0,finalized:!1};let r=n.trim().replace(Iw,"").trim();const c=n.indexOf(r),d=r.match(df);if(d){r=r.replace(df,"").trim();const u=d[1].trim();let p;if(u&&(p=s.indexOf(u,c+r.length),o.key=l(u,p,!0)),d[2]){const m=d[2].trim();m&&(o.index=l(m,s.indexOf(m,o.key?p+u.length:c+r.length),!0))}}return r&&(o.value=l(r,c,!0)),o}function ps(e,t){return an.slice(e,t)}function uf(e){Vt.inSFCRoot&&(_s.innerLoc=Jt(e+1,e+1)),Td(_s);const{tag:t,ns:s}=_s;s===0&&vt.isPreTag(t)&&Bu++,vt.isVoidTag(t)?sr(_s,e):(Nt.unshift(_s),(s===1||s===2)&&(Vt.inXML=!0)),_s=null}function Fo(e,t,s){{const i=Nt[0]&&Nt[0].tag;i!=="script"&&i!=="style"&&e.includes("&")&&(e=vt.decodeEntities(e,!1))}const a=Nt[0]||ao,n=a.children[a.children.length-1];n&&n.type===2?(n.content+=e,Jn(n.loc,s)):a.children.push({type:2,content:e,loc:Jt(t,s)})}function sr(e,t,s=!1){s?Jn(e.loc,Gh(t,60)):Jn(e.loc,Lw(t,62)+1),Vt.inSFCRoot&&(e.children.length?e.innerLoc.end=lt({},e.children[e.children.length-1].loc.end):e.innerLoc.end=lt({},e.innerLoc.start),e.innerLoc.source=ps(e.innerLoc.start.offset,e.innerLoc.end.offset));const{tag:a,ns:n,children:i}=e;if(An||(a==="slot"?e.tagType=2:pf(e)?e.tagType=3:Mw(e)&&(e.tagType=1)),Vt.inRCDATA||(e.children=Wh(i)),n===0&&vt.isIgnoreNewlineTag(a)){const l=i[0];l&&l.type===2&&(l.content=l.content.replace(/^\r?\n/,""))}n===0&&vt.isPreTag(a)&&Bu--,Cd===e&&(An=Vt.inVPre=!1,Cd=null),Vt.inXML&&(Nt[0]?Nt[0].ns:vt.ns)===0&&(Vt.inXML=!1);{const l=e.props;if(!Vt.inSFCRoot&&ti("COMPILER_NATIVE_TEMPLATE",vt)&&e.tag==="template"&&!pf(e)){const r=Nt[0]||ao,c=r.children.indexOf(e);r.children.splice(c,1,...e.children)}const o=l.find(r=>r.type===6&&r.name==="inline-template");o&&to("COMPILER_INLINE_TEMPLATE",vt,o.loc)&&e.children.length&&(o.value={type:2,content:ps(e.children[0].loc.start.offset,e.children[e.children.length-1].loc.end.offset),loc:o.loc})}}function Lw(e,t){let s=e;for(;an.charCodeAt(s)!==t&&s=0;)s--;return s}const Nw=new Set(["if","else","else-if","for","slot"]);function pf({tag:e,props:t}){if(e==="template"){for(let s=0;s64&&e<91}const Pw=/\r\n/g;function Wh(e){const t=vt.whitespace!=="preserve";let s=!1;for(let a=0;as.type!==3);return t.length===1&&t[0].type===1&&!Or(t[0])?t[0]:null}function nr(e,t,s,a=!1,n=!1){const{children:i}=e,l=[];for(let d=0;d0){if(p>=2){u.codegenNode.patchFlag=-1,l.push(u);continue}}else{const m=u.codegenNode;if(m.type===13){const h=m.patchFlag;if((h===void 0||h===512||h===1)&&Qh(u,s)>=2){const y=Yh(u);y&&(m.props=s.hoist(y))}m.dynamicProps&&(m.dynamicProps=s.hoist(m.dynamicProps))}}}else if(u.type===12&&(a?0:Qs(u,s))>=2){u.codegenNode.type===14&&u.codegenNode.arguments.length>0&&u.codegenNode.arguments.push("-1"),l.push(u);continue}if(u.type===1){const p=u.tagType===1;p&&s.scopes.vSlot++,nr(u,e,s,!1,n),p&&s.scopes.vSlot--}else if(u.type===11)nr(u,e,s,u.children.length===1,!0);else if(u.type===9)for(let p=0;pm.key===u||m.key.content===u);return p&&p.value}}l.length&&s.transformHoist&&s.transformHoist(i,s,e)}function Qs(e,t){const{constantCache:s}=t;switch(e.type){case 1:if(e.tagType!==0)return 0;const a=s.get(e);if(a!==void 0)return a;const n=e.codegenNode;if(n.type!==13||n.isBlock&&e.tag!=="svg"&&e.tag!=="foreignObject"&&e.tag!=="math")return 0;if(n.patchFlag===void 0){let l=3;const o=Qh(e,t);if(o===0)return s.set(e,0),0;o1)for(let r=0;rO&&(L.childIndex--,L.onNodeRemoved()),L.parent.children.splice(O,1)},onNodeRemoved:vs,addIdentifiers(E){},removeIdentifiers(E){},hoist(E){Ze(E)&&(E=tt(E)),L.hoists.push(E);const F=tt(`_hoisted_${L.hoists.length}`,!1,E.loc,2);return F.hoisted=E,F},cache(E,F=!1,O=!1){const I=vw(L.cached.length,E,F,O);return L.cached.push(I),I}};return L.filters=new Set,L}function Vw(e,t){const s=qw(e,t);lc(e,s),t.hoistStatic&&Hw(e,s),t.ssr||Gw(e,s),e.helpers=new Set([...s.helpers.keys()]),e.components=[...s.components],e.directives=[...s.directives],e.imports=s.imports,e.hoists=s.hoists,e.temps=s.temps,e.cached=s.cached,e.transformed=!0,e.filters=[...s.filters]}function Gw(e,t){const{helper:s}=t,{children:a}=e;if(a.length===1){const n=Jh(e);if(n&&n.codegenNode){const i=n.codegenNode;i.type===13&&Du(i,t),e.codegenNode=i}else e.codegenNode=a[0]}else if(a.length>1){let n=64;e.codegenNode=eo(t,s(Yl),void 0,e.children,n,void 0,void 0,!0,void 0,!1)}}function Ww(e,t){let s=0;const a=()=>{s--};for(;sa===e:a=>e.test(a);return(a,n)=>{if(a.type===1){const{props:i}=a;if(a.tagType===3&&i.some(Tw))return;const l=[];for(let o=0;o`${Wi[e]}: _${Wi[e]}`;function Kw(e,{mode:t="function",prefixIdentifiers:s=t==="module",sourceMap:a=!1,filename:n="template.vue.html",scopeId:i=null,optimizeImports:l=!1,runtimeGlobalName:o="Vue",runtimeModuleName:r="vue",ssrRuntimeModuleName:c="vue/server-renderer",ssr:d=!1,isTS:u=!1,inSSR:p=!1}){const m={mode:t,prefixIdentifiers:s,sourceMap:a,filename:n,scopeId:i,optimizeImports:l,runtimeGlobalName:o,runtimeModuleName:r,ssrRuntimeModuleName:c,ssr:d,isTS:u,inSSR:p,source:e.source,code:"",column:1,line:1,offset:0,indentLevel:0,pure:!1,map:void 0,helper(y){return`_${Wi[y]}`},push(y,k=-2,A){m.code+=y},indent(){h(++m.indentLevel)},deindent(y=!1){y?--m.indentLevel:h(--m.indentLevel)},newline(){h(m.indentLevel)}};function h(y){m.push(` +`+" ".repeat(y),0)}return m}function Jw(e,t={}){const s=Kw(e,t);t.onContextCreated&&t.onContextCreated(s);const{mode:a,push:n,prefixIdentifiers:i,indent:l,deindent:o,newline:r,scopeId:c,ssr:d}=s,u=Array.from(e.helpers),p=u.length>0,m=!i&&a!=="module";Zw(e,s);const y=d?"ssrRender":"render",A=(d?["_ctx","_push","_parent","_attrs"]:["_ctx","_cache"]).join(", ");if(n(`function ${y}(${A}) {`),l(),m&&(n("with (_ctx) {"),l(),p&&(n(`const { ${u.map(eg).join(", ")} } = _Vue +`,-1),r())),e.components.length&&(Hc(e.components,"component",s),(e.directives.length||e.temps>0)&&r()),e.directives.length&&(Hc(e.directives,"directive",s),e.temps>0&&r()),e.filters&&e.filters.length&&(r(),Hc(e.filters,"filter",s),r()),e.temps>0){n("let ");for(let g=0;g0?", ":""}_temp${g}`)}return(e.components.length||e.directives.length||e.temps)&&(n(` +`,0),r()),d||n("return "),e.codegenNode?Ss(e.codegenNode,s):n("null"),m&&(o(),n("}")),o(),n("}"),{ast:e,code:s.code,preamble:"",map:s.map?s.map.toJSON():void 0}}function Zw(e,t){const{ssr:s,prefixIdentifiers:a,push:n,newline:i,runtimeModuleName:l,runtimeGlobalName:o,ssrRuntimeModuleName:r}=t,c=o,d=Array.from(e.helpers);if(d.length>0&&(n(`const _Vue = ${c} +`,-1),e.hoists.length)){const u=[_u,wu,ho,ku,Nh].filter(p=>d.includes(p)).map(eg).join(", ");n(`const { ${u} } = _Vue +`,-1)}Qw(e.hoists,t),i(),n("return ")}function Hc(e,t,{helper:s,push:a,newline:n,isTS:i}){const l=s(t==="filter"?Eu:t==="component"?Su:Tu);for(let o=0;o3||!1;t.push("["),s&&t.indent(),bo(e,t,s),s&&t.deindent(),t.push("]")}function bo(e,t,s=!1,a=!0){const{push:n,newline:i}=t;for(let l=0;ls||"null")}function nk(e,t){const{push:s,helper:a,pure:n}=t,i=Ze(e.callee)?e.callee:a(e.callee);n&&s(oc),s(i+"(",-2,e),bo(e.arguments,t),s(")")}function ik(e,t){const{push:s,indent:a,deindent:n,newline:i}=t,{properties:l}=e;if(!l.length){s("{}",-2,e);return}const o=l.length>1||!1;s(o?"{":"{ "),o&&a();for(let r=0;r "),(r||o)&&(s("{"),a()),l?(r&&s("return "),Me(l)?Uu(l,t):Ss(l,t)):o&&Ss(o,t),(r||o)&&(n(),s("}")),c&&(e.isNonScopedSlot&&s(", undefined, true"),s(")"))}function rk(e,t){const{test:s,consequent:a,alternate:n,newline:i}=e,{push:l,indent:o,deindent:r,newline:c}=t;if(s.type===4){const u=!$u(s.content);u&&l("("),tg(s,t),u&&l(")")}else l("("),Ss(s,t),l(")");i&&o(),t.indentLevel++,i||l(" "),l("? "),Ss(a,t),t.indentLevel--,i&&c(),i||l(" "),l(": ");const d=n.type===19;d||t.indentLevel++,Ss(n,t),d||t.indentLevel--,i&&r(!0)}function ck(e,t){const{push:s,helper:a,indent:n,deindent:i,newline:l}=t,{needPauseTracking:o,needArraySpread:r}=e;r&&s("[...("),s(`_cache[${e.index}] || (`),o&&(n(),s(`${a(Ar)}(-1`),e.inVOnce&&s(", true"),s("),"),l(),s("(")),s(`_cache[${e.index}] = `),Ss(e.value,t),o&&(s(`).cacheIndex = ${e.index},`),l(),s(`${a(Ar)}(1),`),l(),s(`_cache[${e.index}]`),i()),s(")"),r&&s(")]")}new RegExp("\\b"+"arguments,await,break,case,catch,class,const,continue,debugger,default,delete,do,else,export,extends,finally,for,function,if,import,let,new,return,super,switch,throw,try,var,void,while,with,yield".split(",").join("\\b|\\b")+"\\b");const dk=Xh(/^(?:if|else|else-if)$/,(e,t,s)=>uk(e,t,s,(a,n,i)=>{const l=s.parent.children;let o=l.indexOf(a),r=0;for(;o-->=0;){const c=l[o];c&&c.type===9&&(r+=c.branches.length)}return()=>{if(i)a.codegenNode=mf(n,r,s);else{const c=pk(a.codegenNode);c.alternate=mf(n,r+a.branches.length-1,s)}}}));function uk(e,t,s,a){if(t.name!=="else"&&(!t.exp||!t.exp.content.trim())){const n=t.exp?t.exp.loc:e.loc;s.onError(Mt(28,t.loc)),t.exp=tt("true",!1,n)}if(t.name==="if"){const n=ff(e,t),i={type:9,loc:Fw(e.loc),branches:[n]};if(s.replaceNode(i),a)return a(i,n,!0)}else{const n=s.parent.children;let i=n.indexOf(e);for(;i-->=-1;){const l=n[i];if(l&&qh(l)){s.removeNode(l);continue}if(l&&l.type===9){(t.name==="else-if"||t.name==="else")&&l.branches[l.branches.length-1].condition===void 0&&s.onError(Mt(30,e.loc)),s.removeNode();const o=ff(e,t);l.branches.push(o);const r=a&&a(l,o,!1);lc(o,s),r&&r(),s.currentNode=null}else s.onError(Mt(30,e.loc));break}}}function ff(e,t){const s=e.tagType===3;return{type:10,loc:e.loc,condition:t.name==="else"?void 0:t.exp,children:s&&!la(e,"for")?e.children:[e],userKey:ic(e,"key"),isTemplateIf:s}}function mf(e,t,s){return e.condition?Sd(e.condition,vf(e,t,s),ns(s.helper(ho),['""',"true"])):vf(e,t,s)}function vf(e,t,s){const{helper:a}=s,n=Yt("key",tt(`${t}`,!1,ea,2)),{children:i}=e,l=i[0];if(i.length!==1||l.type!==1)if(i.length===1&&l.type===11){const r=l.codegenNode;return Lr(r,n,s),r}else return eo(s,a(Yl),oa([n]),i,64,void 0,void 0,!0,!1,!1,e.loc);else{const r=l.codegenNode,c=Aw(r);return c.type===13&&Du(c,s),Lr(c,n,s),r}}function pk(e){for(;;)if(e.type===19)if(e.alternate.type===19)e=e.alternate;else return e;else e.type===20&&(e=e.value)}const fk=Xh("for",(e,t,s)=>{const{helper:a,removeHelper:n}=s;return mk(e,t,s,i=>{const l=ns(a(Ru),[i.source]),o=Ir(e),r=la(e,"memo"),c=ic(e,"key",!1,!0);c&&c.type;let d=c&&(c.type===6?c.value?tt(c.value.content,!0):void 0:c.exp);const u=d?Yt("key",d):null,p=i.source.type===4&&i.source.constType>0,m=p?64:c?128:256;return i.codegenNode=eo(s,a(Yl),void 0,l,m,void 0,void 0,!0,!p,!1,e.loc),()=>{let h;const{children:y}=i,k=y.length!==1||y[0].type!==1,A=Or(e)?e:o&&e.children.length===1&&Or(e.children[0])?e.children[0]:null;if(A?(h=A.codegenNode,o&&u&&Lr(h,u,s)):k?h=eo(s,a(Yl),u?oa([u]):void 0,e.children,64,void 0,void 0,!0,void 0,!1):(h=y[0].codegenNode,o&&u&&Lr(h,u,s),h.isBlock!==!p&&(h.isBlock?(n(ni),n(Zi(s.inSSR,h.isComponent))):n(Ji(s.inSSR,h.isComponent))),h.isBlock=!p,h.isBlock?(a(ni),a(Zi(s.inSSR,h.isComponent))):a(Ji(s.inSSR,h.isComponent))),r){const g=Ki(Ed(i.parseResult,[tt("_cached")]));g.body=hw([va(["const _memo = (",r.exp,")"]),va(["if (_cached && _cached.el",...d?[" && _cached.key === ",d]:[],` && ${s.helperString(Ph)}(_cached, _memo)) return _cached`]),va(["const _item = ",h]),tt("_item.memo = _memo"),tt("return _item")]),l.arguments.push(g,tt("_cache"),tt(String(s.cached.length))),s.cached.push(null)}else l.arguments.push(Ki(Ed(i.parseResult),h,!0))}})});function mk(e,t,s,a){if(!t.exp){s.onError(Mt(31,t.loc));return}const n=t.forParseResult;if(!n){s.onError(Mt(32,t.loc));return}ag(n);const{addIdentifiers:i,removeIdentifiers:l,scopes:o}=s,{source:r,value:c,key:d,index:u}=n,p={type:11,loc:t.loc,source:r,valueAlias:c,keyAlias:d,objectIndexAlias:u,parseResult:n,children:Ir(e)?e.children:[e]};s.replaceNode(p),o.vFor++;const m=a&&a(p);return()=>{o.vFor--,m&&m()}}function ag(e,t){e.finalized||(e.finalized=!0)}function Ed({value:e,key:t,index:s},a=[]){return vk([e,t,s,...a])}function vk(e){let t=e.length;for(;t--&&!e[t];);return e.slice(0,t+1).map((s,a)=>s||tt("_".repeat(a+1),!1))}const hf=tt("undefined",!1),hk=(e,t)=>{if(e.type===1&&(e.tagType===1||e.tagType===3)){const s=la(e,"slot");if(s)return s.exp,t.scopes.vSlot++,()=>{t.scopes.vSlot--}}},gk=(e,t,s,a)=>Ki(e,s,!1,!0,s.length?s[0].loc:a);function bk(e,t,s=gk){t.helper(Nu);const{children:a,loc:n}=e,i=[],l=[];let o=t.scopes.vSlot>0||t.scopes.vFor>0;const r=la(e,"slot",!0);if(r){const{arg:k,exp:A}=r;k&&!js(k)&&(o=!0),i.push(Yt(k||tt("default",!0),s(A,void 0,a,n)))}let c=!1,d=!1;const u=[],p=new Set;let m=0;for(let k=0;k{const b=s(A,void 0,g,n);return t.compatConfig&&(b.isNonScopedSlot=!0),Yt("default",b)};c?u.length&&!u.every(Fu)&&(d?t.onError(Mt(39,u[0].loc)):i.push(k(void 0,u))):i.push(k(void 0,a))}const h=o?2:ir(e.children)?3:1;let y=oa(i.concat(Yt("_",tt(h+"",!1))),n);return l.length&&(y=ns(t.helper(Dh),[y,ei(l)])),{slots:y,hasDynamicSlots:o}}function Bo(e,t,s){const a=[Yt("name",e),Yt("fn",t)];return s!=null&&a.push(Yt("key",tt(String(s),!0))),oa(a)}function ir(e){for(let t=0;tfunction(){if(e=t.currentNode,!(e.type===1&&(e.tagType===0||e.tagType===1)))return;const{tag:a,props:n}=e,i=e.tagType===1;let l=i?xk(e,t):`"${a}"`;const o=ht(l)&&l.callee===Cu;let r,c,d=0,u,p,m,h=o||l===$l||l===xu||!i&&(a==="svg"||a==="foreignObject"||a==="math");if(n.length>0){const y=ig(e,t,void 0,i,o);r=y.props,d=y.patchFlag,p=y.dynamicPropNames;const k=y.directives;m=k&&k.length?ei(k.map(A=>wk(A,t))):void 0,y.shouldUseBlock&&(h=!0)}if(e.children.length>0)if(l===Tr&&(h=!0,d|=1024),i&&l!==$l&&l!==Tr){const{slots:k,hasDynamicSlots:A}=bk(e,t);c=k,A&&(d|=1024)}else if(e.children.length===1&&l!==$l){const k=e.children[0],A=k.type,g=A===5||A===8;g&&Qs(k,t)===0&&(d|=1),g||A===2?c=k:c=e.children}else c=e.children;p&&p.length&&(u=kk(p)),e.codegenNode=eo(t,l,r,c,d===0?void 0:d,u,m,!!h,!1,i,e.loc)};function xk(e,t,s=!1){let{tag:a}=e;const n=Ad(a),i=ic(e,"is",!1,!0);if(i)if(n||ti("COMPILER_IS_ON_ELEMENT",t)){let o;if(i.type===6?o=i.value&&tt(i.value.content,!0):(o=i.exp,o||(o=tt("is",!1,i.arg.loc))),o)return ns(t.helper(Cu),[o])}else i.type===6&&i.value.content.startsWith("vue:")&&(a=i.value.content.slice(4));const l=Fh(a)||t.isBuiltInComponent(a);return l?(s||t.helper(l),l):(t.helper(Su),t.components.add(a),so(a,"component"))}function ig(e,t,s=e.props,a,n,i=!1){const{tag:l,loc:o,children:r}=e;let c=[];const d=[],u=[],p=r.length>0;let m=!1,h=0,y=!1,k=!1,A=!1,g=!1,b=!1,x=!1;const w=[],_=F=>{c.length&&(d.push(oa(gf(c),o)),c=[]),F&&d.push(F)},T=()=>{t.scopes.vFor>0&&c.push(Yt(tt("ref_for",!0),tt("true")))},L=({key:F,value:O})=>{if(js(F)){const I=F.content,H=li(I);if(H&&(!a||n)&&I.toLowerCase()!=="onclick"&&I!=="onUpdate:modelValue"&&!en(I)&&(g=!0),H&&en(I)&&(x=!0),H&&O.type===14&&(O=O.arguments[0]),O.type===20||(O.type===4||O.type===8)&&Qs(O,t)>0)return;I==="ref"?y=!0:I==="class"?k=!0:I==="style"?A=!0:I!=="key"&&!w.includes(I)&&w.push(I),a&&(I==="class"||I==="style")&&!w.includes(I)&&w.push(I)}else b=!0};for(let F=0;Fce.content==="prop")&&(h|=32);const re=t.directiveTransforms[I];if(re){const{props:ce,needRuntime:Z}=re(O,e,t);!i&&ce.forEach(L),$&&H&&!js(H)?_(oa(ce,o)):c.push(...ce),Z&&(u.push(O),Cs(Z)&&ng.set(O,Z))}else fy(I)||(u.push(O),p&&(m=!0))}}let E;if(d.length?(_(),d.length>1?E=ns(t.helper(Er),d,o):E=d[0]):c.length&&(E=oa(gf(c),o)),b?h|=16:(k&&!a&&(h|=2),A&&!a&&(h|=4),w.length&&(h|=8),g&&(h|=32)),!m&&(h===0||h===32)&&(y||x||u.length>0)&&(h|=512),!t.inSSR&&E)switch(E.type){case 15:let F=-1,O=-1,I=!1;for(let ee=0;eeYt(l,i)),n))}return ei(s,e.loc)}function kk(e){let t="[";for(let s=0,a=e.length;s{if(Or(e)){const{children:s,loc:a}=e,{slotName:n,slotProps:i}=Ck(e,t),l=[t.prefixIdentifiers?"_ctx.$slots":"$slots",n,"{}","undefined","true"];let o=2;i&&(l[2]=i,o=3),s.length&&(l[3]=Ki([],s,!1,!1,a),o=4),t.scopeId&&!t.slotted&&(o=5),l.splice(o),e.codegenNode=ns(t.helper(Mh),l,a)}};function Ck(e,t){let s='"default"',a;const n=[];for(let i=0;i0){const{props:i,directives:l}=ig(e,t,n,!1,!1);a=i,l.length&&t.onError(Mt(36,l[0].loc))}return{slotName:s,slotProps:a}}const lg=(e,t,s,a)=>{const{loc:n,modifiers:i,arg:l}=e;!e.exp&&!i.length&&s.onError(Mt(35,n));let o;if(l.type===4)if(l.isStatic){let u=l.content;u.startsWith("vue:")&&(u=`vnode-${u.slice(4)}`);const p=t.tagType!==0||u.startsWith("vnode")||!/[A-Z]/.test(u)?Mi(Tt(u)):`on:${u}`;o=tt(p,!0,l.loc)}else o=va([`${s.helperString(kd)}(`,l,")"]);else o=l,o.children.unshift(`${s.helperString(kd)}(`),o.children.push(")");let r=e.exp;r&&!r.content.trim()&&(r=void 0);let c=s.cacheHandlers&&!r&&!s.inVOnce;if(r){const u=zh(r),p=!(u||Sw(r)),m=r.content.includes(";");(p||c&&u)&&(r=va([`${p?"$event":"(...args)"} => ${m?"{":"("}`,r,m?"}":")"]))}let d={props:[Yt(o,r||tt("() => {}",!1,n))]};return a&&(d=a(d)),c&&(d.props[0].value=s.cache(d.props[0].value)),d.props.forEach(u=>u.key.isHandlerKey=!0),d},Tk=(e,t,s)=>{const{modifiers:a,loc:n}=e,i=e.arg;let{exp:l}=e;return l&&l.type===4&&!l.content.trim()&&(l=void 0),i.type!==4?(i.children.unshift("("),i.children.push(') || ""')):i.isStatic||(i.content=i.content?`${i.content} || ""`:'""'),a.some(o=>o.content==="camel")&&(i.type===4?i.isStatic?i.content=Tt(i.content):i.content=`${s.helperString(wd)}(${i.content})`:(i.children.unshift(`${s.helperString(wd)}(`),i.children.push(")"))),s.inSSR||(a.some(o=>o.content==="prop")&&bf(i,"."),a.some(o=>o.content==="attr")&&bf(i,"^")),{props:[Yt(i,l)]}},bf=(e,t)=>{e.type===4?e.isStatic?e.content=t+e.content:e.content=`\`${t}\${${e.content}}\``:(e.children.unshift(`'${t}' + (`),e.children.push(")"))},Ek=(e,t)=>{if(e.type===0||e.type===1||e.type===11||e.type===10)return()=>{const s=e.children;let a,n=!1;for(let i=0;ii.type===7&&!t.directiveTransforms[i.name])&&e.tag!=="template")))for(let i=0;i{if(e.type===1&&la(e,"once",!0))return yf.has(e)||t.inVOnce||t.inSSR?void 0:(yf.add(e),t.inVOnce=!0,t.helper(Ar),()=>{t.inVOnce=!1;const s=t.currentNode;s.codegenNode&&(s.codegenNode=t.cache(s.codegenNode,!0,!0))})},og=(e,t,s)=>{const{exp:a,arg:n}=e;if(!a)return s.onError(Mt(41,e.loc)),vl();const i=a.loc.source.trim(),l=a.type===4?a.content:i,o=s.bindingMetadata[i];if(o==="props"||o==="props-aliased")return s.onError(Mt(44,a.loc)),vl();if(o==="literal-const"||o==="setup-const")return s.onError(Mt(45,a.loc)),vl();if(!l.trim()||!zh(a))return s.onError(Mt(42,a.loc)),vl();const r=n||tt("modelValue",!0),c=n?js(n)?`onUpdate:${Tt(n.content)}`:va(['"onUpdate:" + ',n]):"onUpdate:modelValue";let d;const u=s.isTS?"($event: any)":"$event";d=va([`${u} => ((`,a,") = $event)"]);const p=[Yt(r,e.exp),Yt(c,d)];if(e.modifiers.length&&t.tagType===1){const m=e.modifiers.map(y=>y.content).map(y=>($u(y)?y:JSON.stringify(y))+": true").join(", "),h=n?js(n)?`${n.content}Modifiers`:va([n,' + "Modifiers"']):"modelModifiers";p.push(Yt(h,tt(`{ ${m} }`,!1,e.loc,2)))}return vl(p)};function vl(e=[]){return{props:e}}const Rk=/[\w).+\-_$\]]/,Ik=(e,t)=>{ti("COMPILER_FILTERS",t)&&(e.type===5?Nr(e.content,t):e.type===1&&e.props.forEach(s=>{s.type===7&&s.name!=="for"&&s.exp&&Nr(s.exp,t)}))};function Nr(e,t){if(e.type===4)xf(e,t);else for(let s=0;s=0&&(g=s.charAt(A),g===" ");A--);(!g||!Rk.test(g))&&(l=!0)}}h===void 0?h=s.slice(0,m).trim():d!==0&&k();function k(){y.push(s.slice(d,m).trim()),d=m+1}if(y.length){for(m=0;m{if(e.type===1){const s=la(e,"memo");return!s||_f.has(e)||t.inSSR?void 0:(_f.add(e),()=>{const a=e.codegenNode||t.currentNode.codegenNode;a&&a.type===13&&(e.tagType!==1&&Du(a,t),e.codegenNode=ns(t.helper(Mu),[s.exp,Ki(void 0,a),"_cache",String(t.cached.length)]),t.cached.push(null))})}},Nk=(e,t)=>{if(e.type===1){for(const s of e.props)if(s.type===7&&s.name==="bind"&&(!s.exp||s.exp.type===4&&!s.exp.content.trim())&&s.arg){const a=s.arg;if(a.type!==4||!a.isStatic)t.onError(Mt(53,a.loc)),s.exp=tt("",!0,a.loc);else{const n=Tt(a.content);(Bh.test(n[0])||n[0]==="-")&&(s.exp=tt(n,!1,a.loc))}}}};function Mk(e){return[[Nk,Ak,dk,Lk,fk,Ik,Sk,yk,hk,Ek],{on:lg,bind:Tk,model:og}]}function Dk(e,t={}){const s=t.onError||Pu,a=t.mode==="module";t.prefixIdentifiers===!0?s(Mt(48)):a&&s(Mt(49));const n=!1;t.cacheHandlers&&s(Mt(50)),t.scopeId&&!a&&s(Mt(51));const i=lt({},t,{prefixIdentifiers:n}),l=Ze(e)?zw(e,i):e,[o,r]=Mk();return Vw(l,lt({},i,{nodeTransforms:[...o,...t.nodeTransforms||[]],directiveTransforms:lt({},r,t.directiveTransforms||{})})),Jw(l,i)}const Pk=()=>({props:[]});/** * @vue/compiler-dom v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const Yv=Symbol(""),Xv=Symbol(""),eg=Symbol(""),tg=Symbol(""),_d=Symbol(""),sg=Symbol(""),ag=Symbol(""),ng=Symbol(""),ig=Symbol(""),lg=Symbol("");G_({[Yv]:"vModelRadio",[Xv]:"vModelCheckbox",[eg]:"vModelText",[tg]:"vModelSelect",[_d]:"vModelDynamic",[sg]:"withModifiers",[ag]:"withKeys",[ng]:"vShow",[ig]:"Transition",[lg]:"TransitionGroup"});let bi;function vk(e,t=!1){return bi||(bi=document.createElement("div")),t?(bi.innerHTML=`
`,bi.children[0].getAttribute("foo")):(bi.innerHTML=e,bi.textContent)}const gk={parseMode:"html",isVoidTag:cy,isNativeTag:e=>ly(e)||oy(e)||ry(e),isPreTag:e=>e==="pre",isIgnoreNewlineTag:e=>e==="pre"||e==="textarea",decodeEntities:vk,isBuiltInComponent:e=>{if(e==="Transition"||e==="transition")return ig;if(e==="TransitionGroup"||e==="transition-group")return lg},getNamespace(e,t,s){let a=t?t.ns:s;if(t&&a===2)if(t.tag==="annotation-xml"){if(e==="svg")return 1;t.props.some(n=>n.type===6&&n.name==="encoding"&&n.value!=null&&(n.value.content==="text/html"||n.value.content==="application/xhtml+xml"))&&(a=0)}else/^m(?:[ions]|text)$/.test(t.tag)&&e!=="mglyph"&&e!=="malignmark"&&(a=0);else t&&a===1&&(t.tag==="foreignObject"||t.tag==="desc"||t.tag==="title")&&(a=0);if(a===0){if(e==="svg")return 1;if(e==="math")return 2}return a}},bk=e=>{e.type===1&&e.props.forEach((t,s)=>{t.type===6&&t.name==="style"&&t.value&&(e.props[s]={type:7,name:"bind",arg:et("style",!0,t.loc),exp:yk(t.value.content,t.loc),modifiers:[],loc:t.loc})})},yk=(e,t)=>{const s=ym(e);return et(JSON.stringify(s),!1,t,3)};function Sn(e,t){return Ot(e,t)}const xk=(e,t,s)=>{const{exp:a,loc:n}=e;return a||s.onError(Sn(54,n)),t.children.length&&(s.onError(Sn(55,n)),t.children.length=0),{props:[Gt(et("innerHTML",!0,n),a||et("",!0))]}},_k=(e,t,s)=>{const{exp:a,loc:n}=e;return a||s.onError(Sn(56,n)),t.children.length&&(s.onError(Sn(57,n)),t.children.length=0),{props:[Gt(et("textContent",!0),a?Js(a,s)>0?a:ss(s.helperString(Qr),[a],n):et("",!0))]}},wk=(e,t,s)=>{const a=Qv(e,t,s);if(!a.props.length||t.tagType===1)return a;e.arg&&s.onError(Sn(59,e.arg.loc));const{tag:n}=t,i=s.isCustomElement(n);if(n==="input"||n==="textarea"||n==="select"||i){let l=eg,o=!1;if(n==="input"||i){const r=Yr(t,"type");if(r){if(r.type===7)l=_d;else if(r.value)switch(r.value.content){case"radio":l=Yv;break;case"checkbox":l=Xv;break;case"file":o=!0,s.onError(Sn(60,e.loc));break}}else nw(t)&&(l=_d)}else n==="select"&&(l=tg);o||(a.needRuntime=s.helper(l))}else s.onError(Sn(58,e.loc));return a.props=a.props.filter(l=>!(l.key.type===4&&l.key.content==="modelValue")),a},kk=Qs("passive,once,capture"),Sk=Qs("stop,prevent,self,ctrl,shift,alt,meta,exact,middle"),Ck=Qs("left,right"),og=Qs("onkeyup,onkeydown,onkeypress"),Tk=(e,t,s,a)=>{const n=[],i=[],l=[];for(let o=0;ozs(e)&&e.content.toLowerCase()==="onclick"?et(t,!0):e.type!==4?ha(["(",e,`) === "onClick" ? "${t}" : (`,e,")"]):e,Ek=(e,t,s)=>Zv(e,t,s,a=>{const{modifiers:n}=e;if(!n.length)return a;let{key:i,value:l}=a.props[0];const{keyModifiers:o,nonKeyModifiers:r,eventOptionModifiers:c}=Tk(i,n,s,e.loc);if(r.includes("right")&&(i=pf(i,"onContextmenu")),r.includes("middle")&&(i=pf(i,"onMouseup")),r.length&&(l=ss(s.helper(sg),[l,JSON.stringify(r)])),o.length&&(!zs(i)||og(i.content.toLowerCase()))&&(l=ss(s.helper(ag),[l,JSON.stringify(o)])),c.length){const d=c.map(ai).join("");i=zs(i)?et(`${i.content}${d}`,!0):ha(["(",i,`) + "${d}"`])}return{props:[Gt(i,l)]}}),Ak=(e,t,s)=>{const{exp:a,loc:n}=e;return a||s.onError(Sn(62,n)),{props:[],needRuntime:s.helper(ng)}},Rk=(e,t)=>{e.type===1&&e.tagType===0&&(e.tag==="script"||e.tag==="style")&&t.removeNode()},Ik=[bk],Ok={cloak:hk,html:xk,text:_k,model:wk,on:Ek,show:Ak};function Lk(e,t={}){return mk(e,nt({},gk,t,{nodeTransforms:[Rk,...Ik,...t.nodeTransforms||[]],directiveTransforms:nt({},Ok,t.directiveTransforms||{}),transformHoist:null}))}/** +**/const rg=Symbol(""),cg=Symbol(""),dg=Symbol(""),ug=Symbol(""),Rd=Symbol(""),pg=Symbol(""),fg=Symbol(""),mg=Symbol(""),vg=Symbol(""),hg=Symbol("");fw({[rg]:"vModelRadio",[cg]:"vModelCheckbox",[dg]:"vModelText",[ug]:"vModelSelect",[Rd]:"vModelDynamic",[pg]:"withModifiers",[fg]:"withKeys",[mg]:"vShow",[vg]:"Transition",[hg]:"TransitionGroup"});let _i;function $k(e,t=!1){return _i||(_i=document.createElement("div")),t?(_i.innerHTML=`
`,_i.children[0].getAttribute("foo")):(_i.innerHTML=e,_i.textContent)}const Fk={parseMode:"html",isVoidTag:Iy,isNativeTag:e=>Ey(e)||Ay(e)||Ry(e),isPreTag:e=>e==="pre",isIgnoreNewlineTag:e=>e==="pre"||e==="textarea",decodeEntities:$k,isBuiltInComponent:e=>{if(e==="Transition"||e==="transition")return vg;if(e==="TransitionGroup"||e==="transition-group")return hg},getNamespace(e,t,s){let a=t?t.ns:s;if(t&&a===2)if(t.tag==="annotation-xml"){if(e==="svg")return 1;t.props.some(n=>n.type===6&&n.name==="encoding"&&n.value!=null&&(n.value.content==="text/html"||n.value.content==="application/xhtml+xml"))&&(a=0)}else/^m(?:[ions]|text)$/.test(t.tag)&&e!=="mglyph"&&e!=="malignmark"&&(a=0);else t&&a===1&&(t.tag==="foreignObject"||t.tag==="desc"||t.tag==="title")&&(a=0);if(a===0){if(e==="svg")return 1;if(e==="math")return 2}return a}},Bk=e=>{e.type===1&&e.props.forEach((t,s)=>{t.type===6&&t.name==="style"&&t.value&&(e.props[s]={type:7,name:"bind",arg:tt("style",!0,t.loc),exp:Uk(t.value.content,t.loc),modifiers:[],loc:t.loc})})},Uk=(e,t)=>{const s=Rm(e);return tt(JSON.stringify(s),!1,t,3)};function Ln(e,t){return Mt(e,t)}const zk=(e,t,s)=>{const{exp:a,loc:n}=e;return a||s.onError(Ln(54,n)),t.children.length&&(s.onError(Ln(55,n)),t.children.length=0),{props:[Yt(tt("innerHTML",!0,n),a||tt("",!0))]}},Hk=(e,t,s)=>{const{exp:a,loc:n}=e;return a||s.onError(Ln(56,n)),t.children.length&&(s.onError(Ln(57,n)),t.children.length=0),{props:[Yt(tt("textContent",!0),a?Qs(a,s)>0?a:ns(s.helperString(nc),[a],n):tt("",!0))]}},jk=(e,t,s)=>{const a=og(e,t,s);if(!a.props.length||t.tagType===1)return a;e.arg&&s.onError(Ln(59,e.arg.loc));const{tag:n}=t,i=s.isCustomElement(n);if(n==="input"||n==="textarea"||n==="select"||i){let l=dg,o=!1;if(n==="input"||i){const r=ic(t,"type");if(r){if(r.type===7)l=Rd;else if(r.value)switch(r.value.content){case"radio":l=rg;break;case"checkbox":l=cg;break;case"file":o=!0,s.onError(Ln(60,e.loc));break}}else Cw(t)&&(l=Rd)}else n==="select"&&(l=ug);o||(a.needRuntime=s.helper(l))}else s.onError(Ln(58,e.loc));return a.props=a.props.filter(l=>!(l.key.type===4&&l.key.content==="modelValue")),a},qk=Xs("passive,once,capture"),Vk=Xs("stop,prevent,self,ctrl,shift,alt,meta,exact,middle"),Gk=Xs("left,right"),gg=Xs("onkeyup,onkeydown,onkeypress"),Wk=(e,t,s,a)=>{const n=[],i=[],l=[];for(let o=0;ojs(e)&&e.content.toLowerCase()==="onclick"?tt(t,!0):e.type!==4?va(["(",e,`) === "onClick" ? "${t}" : (`,e,")"]):e,Kk=(e,t,s)=>lg(e,t,s,a=>{const{modifiers:n}=e;if(!n.length)return a;let{key:i,value:l}=a.props[0];const{keyModifiers:o,nonKeyModifiers:r,eventOptionModifiers:c}=Wk(i,n,s,e.loc);if(r.includes("right")&&(i=wf(i,"onContextmenu")),r.includes("middle")&&(i=wf(i,"onMouseup")),r.length&&(l=ns(s.helper(pg),[l,JSON.stringify(r)])),o.length&&(!js(i)||gg(i.content.toLowerCase()))&&(l=ns(s.helper(fg),[l,JSON.stringify(o)])),c.length){const d=c.map(ri).join("");i=js(i)?tt(`${i.content}${d}`,!0):va(["(",i,`) + "${d}"`])}return{props:[Yt(i,l)]}}),Jk=(e,t,s)=>{const{exp:a,loc:n}=e;return a||s.onError(Ln(62,n)),{props:[],needRuntime:s.helper(mg)}},Zk=(e,t)=>{e.type===1&&e.tagType===0&&(e.tag==="script"||e.tag==="style")&&t.removeNode()},Qk=[Bk],Yk={cloak:Pk,html:zk,text:Hk,model:jk,on:Kk,show:Jk};function Xk(e,t={}){return Dk(e,lt({},Fk,t,{nodeTransforms:[Zk,...Qk,...t.nodeTransforms||[]],directiveTransforms:lt({},Yk,t.directiveTransforms||{}),transformHoist:null}))}/** * vue v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const ff=Object.create(null);function Nk(e,t){if(!Qe(e))if(e.nodeType)e=e.innerHTML;else return hs;const s=Jb(e,t),a=ff[s];if(a)return a;if(e[0]==="#"){const o=document.querySelector(e);e=o?o.innerHTML:""}const n=nt({hoistStatic:!0,onError:void 0,onWarn:hs},t);!n.isCustomElement&&typeof customElements<"u"&&(n.isCustomElement=o=>!!customElements.get(o));const{code:i}=Lk(e,n),l=new Function("Vue",i)(U_);return l._rc=!0,ff[s]=l}qh(Nk);const Er=Tn({items:[]});let Mk=1;function tc(e,t="info",s=3e3){const a=Mk++;return Er.items.push({id:a,message:String(e),type:t}),s>0&&setTimeout(()=>Nu(a),s),a}function Nu(e){const t=Er.items.findIndex(s=>s.id===e);t>=0&&Er.items.splice(t,1)}function ke(e,t="info",s=3e3){return tc(e,t,s)}ke.success=(e,t=3e3)=>tc(e,"success",t);ke.error=(e,t=5e3)=>tc(e,"error",t);ke.info=(e,t=3e3)=>tc(e,"info",t);ke.dismiss=Nu;const Dk={setup(){return{state:Er,dismiss:Nu}},template:` +**/const kf=Object.create(null);function eS(e,t){if(!Ze(e))if(e.nodeType)e=e.innerHTML;else return vs;const s=hy(e,t),a=kf[s];if(a)return a;if(e[0]==="#"){const o=document.querySelector(e);e=o?o.innerHTML:""}const n=lt({hoistStatic:!0,onError:void 0,onWarn:vs},t);!n.isCustomElement&&typeof customElements<"u"&&(n.isCustomElement=o=>!!customElements.get(o));const{code:i}=Xk(e,n),l=new Function("Vue",i)(ow);return l._rc=!0,kf[s]=l}eh(eS);const Mr=Mn({items:[]});let tS=1;function rc(e,t="info",s=3e3){const a=tS++;return Mr.items.push({id:a,message:String(e),type:t}),s>0&&setTimeout(()=>zu(a),s),a}function zu(e){const t=Mr.items.findIndex(s=>s.id===e);t>=0&&Mr.items.splice(t,1)}function Se(e,t="info",s=3e3){return rc(e,t,s)}Se.success=(e,t=3e3)=>rc(e,"success",t);Se.error=(e,t=5e3)=>rc(e,"error",t);Se.info=(e,t=3e3)=>rc(e,"info",t);Se.dismiss=zu;const sS={setup(){return{state:Mr,dismiss:zu}},template:`
t in e?Bb(e,t,{enumerable:!0,config
- `},qa=Tn({open:!1,title:"Confirm",message:"",confirmLabel:"Confirm",cancelLabel:"Cancel",danger:!1});let $i=null;function os({title:e="Confirm",message:t="",confirmLabel:s="Confirm",cancelLabel:a="Cancel",danger:n=!1}={}){return $i&&$i(!1),qa.title=e,qa.message=t,qa.confirmLabel=s,qa.cancelLabel=a,qa.danger=n,qa.open=!0,new Promise(i=>{$i=i})}function mf(e){qa.open=!1,$i&&($i(e),$i=null)}const Pk={setup(){function e(t){qa.open&&t.key==="Escape"&&(t.stopPropagation(),mf(!1))}return tt(()=>document.addEventListener("keydown",e,!0)),xt(()=>document.removeEventListener("keydown",e,!0)),{state:qa,settle:mf}},template:` + `},Ka=Mn({open:!1,title:"Confirm",message:"",confirmLabel:"Confirm",cancelLabel:"Cancel",danger:!1});let Ui=null;function is({title:e="Confirm",message:t="",confirmLabel:s="Confirm",cancelLabel:a="Cancel",danger:n=!1}={}){return Ui&&Ui(!1),Ka.title=e,Ka.message=t,Ka.confirmLabel=s,Ka.cancelLabel=a,Ka.danger=n,Ka.open=!0,new Promise(i=>{Ui=i})}function Sf(e){Ka.open=!1,Ui&&(Ui(e),Ui=null)}const aS={setup(){function e(t){Ka.open&&t.key==="Escape"&&(t.stopPropagation(),Sf(!1))}return st(()=>document.addEventListener("keydown",e,!0)),wt(()=>document.removeEventListener("keydown",e,!0)),{state:Ka,settle:Sf}},template:` - `};function $u(e){if(e instanceof Date)return e;if(typeof e=="string"){const t=new Date(e);return isNaN(t.getTime())?null:t}return typeof e=="number"&&isFinite(e)?new Date(e<1e12?e*1e3:e):null}function ii(e){const t=$u(e);return t?t.toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):"—"}function t1(e){const t=$u(e);return t?t.toLocaleTimeString():"—"}function kg(e){const t=$u(e);if(!t)return"—";const s=Math.max(0,Math.floor((Date.now()-t.getTime())/1e3));return s<60?`${s}s ago`:s<3600?`${Math.floor(s/60)}m ago`:s<86400?`${Math.floor(s/3600)}h ago`:`${Math.floor(s/86400)}d ago`}function s1(e){if(e==null||!isFinite(e))return"—";const t=Math.max(0,Math.floor(Number(e)));return t<60?"less than 1 min ago":t<3600?`${Math.floor(t/60)} min ago`:t<86400?`${Math.floor(t/3600)} hr ago`:`${Math.floor(t/86400)} day ago`}function Zi(e){if(e==null||!isFinite(e))return"—";const t=Math.max(0,Math.round(e));if(t<60)return`${t}s`;if(t<3600){const n=Math.floor(t/60),i=t%60;return i?`${n}m ${i}s`:`${n}m`}const s=Math.floor(t/3600),a=Math.floor(t%3600/60);return a?`${s}h ${a}m`:`${s}h`}function Fu(e,t=200){const s=String(e??"");return s.length>t?s.slice(0,t)+"…":s}function Sg(e,t=5e3){const s=String(e??"");return s.length>t?s.slice(0,t)+` -... (truncated)`:s}function Lf(e){return String(e??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function Bu(e){return e==null||!isFinite(e)?"—":Number(e).toLocaleString()}function Cg(e){return e==null||!isFinite(e)?"—":e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}const Tg=Symbol("agent-detail-cancelled"),a1=15e3;function n1(e,{timeoutMs:t,timeoutLabel:s,scheduleTimeout:a,cancelTimeout:n}){const i=typeof AbortController=="function"?new AbortController:null;let l=null,o=!1,r,c;const d=new Promise((m,h)=>{r=m,c=h});function u(m,h){o||(o=!0,l!==null&&n(l),l=null,(m?r:c)(h))}let f;try{f=e(i==null?void 0:i.signal)}catch(m){u(!1,m)}return o||Promise.resolve(f).then(m=>u(!0,m),m=>u(!1,m)),!o&&Number.isFinite(t)&&t>0&&(l=a(()=>{const m=Math.max(1,Math.round(t/1e3));u(!1,new Error(`${s} request timed out after ${m}s`)),i==null||i.abort()},t)),{promise:d,cancel(){u(!0,Tg),i==null||i.abort()}}}function Eg({state:e,requestDetail:t,timeoutMs:s=a1,detailLabel:a="Agent detail",scheduleTimeout:n=globalThis.setTimeout.bind(globalThis),cancelTimeout:i=globalThis.clearTimeout.bind(globalThis)}){if(!e||typeof e!="object")throw new TypeError("agent detail state is required");if(typeof t!="function")throw new TypeError("requestDetail must be a function");let l=null;function o(){const f=l;l=null,f==null||f.cancel()}function r(f,{initial:m,coalesce:h}){if(!f)return Promise.resolve();if(h&&l&&l.agentId===f&&e.detailId===f)return l.promise;o();const g={agentId:f,cancel:null,promise:null};l=g,m?(e.detail=null,e.detailError=null,e.detailLoading=!0):e.detail===null&&e.detailError===null&&(e.detailLoading=!0);const A=n1(w=>t(f,{signal:w}),{timeoutMs:s,timeoutLabel:a,scheduleTimeout:n,cancelTimeout:i});return g.cancel=A.cancel,g.promise=(async()=>{let w=null,y=null;try{w=await A.promise}catch(b){y=b}w!==Tg&&(l!==g||e.detailId!==f||(l=null,!y&&(w===null||typeof w!="object")&&(y=new Error(`${a} response was empty or invalid`)),y?e.detail===null&&(e.detailError=(y==null?void 0:y.message)||`Failed to load ${a.toLowerCase()}`):(e.detail=w,e.detailError=null),e.detailLoading=!1))})(),g.promise}function c(f){return e.detailId=f,r(f,{initial:!0,coalesce:!1})}function d(){const f=e.detailId;return f?r(f,{initial:!1,coalesce:!0}):Promise.resolve()}function u(){o(),e.detailId=null,e.detail=null,e.detailError=null,e.detailLoading=!1}return{open:c,refresh:d,close:u,hasInFlight:()=>l!==null}}function i1({isEnabled:e,refreshList:t,hasOpenDetail:s,refreshDetail:a,intervalMs:n=5e3,scheduleInterval:i=globalThis.setInterval.bind(globalThis),cancelInterval:l=globalThis.clearInterval.bind(globalThis)}){let o=null;function r(){e()&&(t(),s()&&a())}function c(){o!==null&&(l(o),o=null)}function d(){c(),e()&&(o=i(r,n))}function u(){e()?d():c()}return{start:d,stop:c,sync:u,isRunning:()=>o!==null}}const l1={components:{ToolOutput:ic},template:` + `};function Vu(e){if(e instanceof Date)return e;if(typeof e=="string"){const t=new Date(e);return isNaN(t.getTime())?null:t}return typeof e=="number"&&isFinite(e)?new Date(e<1e12?e*1e3:e):null}function di(e){const t=Vu(e);return t?t.toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):"—"}function w1(e){const t=Vu(e);return t?t.toLocaleTimeString():"—"}function Ng(e){const t=Vu(e);if(!t)return"—";const s=Math.max(0,Math.floor((Date.now()-t.getTime())/1e3));return s<60?`${s}s ago`:s<3600?`${Math.floor(s/60)}m ago`:s<86400?`${Math.floor(s/3600)}h ago`:`${Math.floor(s/86400)}d ago`}function k1(e){if(e==null||!isFinite(e))return"—";const t=Math.max(0,Math.floor(Number(e)));return t<60?"less than 1 min ago":t<3600?`${Math.floor(t/60)} min ago`:t<86400?`${Math.floor(t/3600)} hr ago`:`${Math.floor(t/86400)} day ago`}function Xi(e){if(e==null||!isFinite(e))return"—";const t=Math.max(0,Math.round(e));if(t<60)return`${t}s`;if(t<3600){const n=Math.floor(t/60),i=t%60;return i?`${n}m ${i}s`:`${n}m`}const s=Math.floor(t/3600),a=Math.floor(t%3600/60);return a?`${s}h ${a}m`:`${s}h`}function Gu(e,t=200){const s=String(e??"");return s.length>t?s.slice(0,t)+"…":s}function Mg(e,t=5e3){const s=String(e??"");return s.length>t?s.slice(0,t)+` +... (truncated)`:s}function Hf(e){return String(e??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function Wu(e){return e==null||!isFinite(e)?"—":Number(e).toLocaleString()}function Dg(e){return e==null||!isFinite(e)?"—":e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}const Pg=Symbol("agent-detail-cancelled"),S1=15e3;function C1(e,{timeoutMs:t,timeoutLabel:s,scheduleTimeout:a,cancelTimeout:n}){const i=typeof AbortController=="function"?new AbortController:null;let l=null,o=!1,r,c;const d=new Promise((m,h)=>{r=m,c=h});function u(m,h){o||(o=!0,l!==null&&n(l),l=null,(m?r:c)(h))}let p;try{p=e(i==null?void 0:i.signal)}catch(m){u(!1,m)}return o||Promise.resolve(p).then(m=>u(!0,m),m=>u(!1,m)),!o&&Number.isFinite(t)&&t>0&&(l=a(()=>{const m=Math.max(1,Math.round(t/1e3));u(!1,new Error(`${s} request timed out after ${m}s`)),i==null||i.abort()},t)),{promise:d,cancel(){u(!0,Pg),i==null||i.abort()}}}function $g({state:e,requestDetail:t,timeoutMs:s=S1,detailLabel:a="Agent detail",scheduleTimeout:n=globalThis.setTimeout.bind(globalThis),cancelTimeout:i=globalThis.clearTimeout.bind(globalThis)}){if(!e||typeof e!="object")throw new TypeError("agent detail state is required");if(typeof t!="function")throw new TypeError("requestDetail must be a function");let l=null;function o(){const p=l;l=null,p==null||p.cancel()}function r(p,{initial:m,coalesce:h}){if(!p)return Promise.resolve();if(h&&l&&l.agentId===p&&e.detailId===p)return l.promise;o();const y={agentId:p,cancel:null,promise:null};l=y,m?(e.detail=null,e.detailError=null,e.detailLoading=!0):e.detail===null&&e.detailError===null&&(e.detailLoading=!0);const k=C1(A=>t(p,{signal:A}),{timeoutMs:s,timeoutLabel:a,scheduleTimeout:n,cancelTimeout:i});return y.cancel=k.cancel,y.promise=(async()=>{let A=null,g=null;try{A=await k.promise}catch(b){g=b}A!==Pg&&(l!==y||e.detailId!==p||(l=null,!g&&(A===null||typeof A!="object")&&(g=new Error(`${a} response was empty or invalid`)),g?e.detail===null&&(e.detailError=(g==null?void 0:g.message)||`Failed to load ${a.toLowerCase()}`):(e.detail=A,e.detailError=null),e.detailLoading=!1))})(),y.promise}function c(p){return e.detailId=p,r(p,{initial:!0,coalesce:!1})}function d(){const p=e.detailId;return p?r(p,{initial:!1,coalesce:!0}):Promise.resolve()}function u(){o(),e.detailId=null,e.detail=null,e.detailError=null,e.detailLoading=!1}return{open:c,refresh:d,close:u,hasInFlight:()=>l!==null}}function T1({isEnabled:e,refreshList:t,hasOpenDetail:s,refreshDetail:a,intervalMs:n=5e3,scheduleInterval:i=globalThis.setInterval.bind(globalThis),cancelInterval:l=globalThis.clearInterval.bind(globalThis)}){let o=null;function r(){e()&&(t(),s()&&a())}function c(){o!==null&&(l(o),o=null)}function d(){c(),e()&&(o=i(r,n))}function u(){e()?d():c()}return{start:d,stop:c,sync:u,isRunning:()=>o!==null}}const E1={components:{ToolOutput:pc},template:`

Agents

@@ -521,7 +521,7 @@ ${u.text}`:u.text).join(`
-
`,setup(){const e=p([]),t=p(!0),s=p(null),a=p(null),n=p(!0),i=p("all");let l=!1;const o=U(()=>e.value.filter($=>$.status==="running").length),r=U(()=>e.value.filter($=>$.status==="completed").length),c=U(()=>e.value.filter($=>["failed","timeout","killed"].includes($.status)).length),d=U(()=>[{value:"all",label:"All",count:e.value.length},{value:"running",label:"Running",count:o.value},{value:"completed",label:"Completed",count:r.value},{value:"failed",label:"Failed",count:c.value}]),u=U(()=>i.value==="all"?e.value:i.value==="failed"?e.value.filter($=>["failed","timeout","killed"].includes($.status)):e.value.filter($=>$.status===i.value));function f($){const ce=Number($.max_iterations)||0;return ce<=0?0:Math.min(100,Math.round($.iteration_count/ce*100))}function m($){return(Number($.max_iterations)||0)>0}function h($,ce){return $?$==="N/A"?"N/A":ce==="current_inheritance"?`inherit (currently ${$})`:$:"unknown"}function g($){return h($.display_model,$.display_model_source||$.display_source)}function A($){return h($.display_reasoning_effort,$.display_reasoning_effort_source||$.display_source)}function w($){return{last_execution:"last executed",current_inheritance:"inherited from current config — not yet executed",spawn_override_pending:"requested at spawn — not yet executed",unknown:"no execution data"}[$]||""}const y=p(null),b=p(null),x=p(!1),S=p(null),_=p(""),L=Eg({state:{get detail(){return y.value},set detail($){y.value=$},get detailId(){return b.value},set detailId($){b.value=$},get detailLoading(){return x.value},set detailLoading($){x.value=$},get detailError(){return S.value},set detailError($){S.value=$}},requestDetail:($,{signal:ce})=>z.get(`/api/agents/${encodeURIComponent($)}`,{signal:ce})});async function E($){_.value="",await L.open($.id)}function P(){L.close(),_.value=""}async function R(){await L.refresh()}async function I($,ce){try{await navigator.clipboard.writeText(ce||""),_.value=$,setTimeout(()=>{_.value===$&&(_.value="")},1500)}catch{ke.error("Copy failed")}}async function H($=!1){$=$===!0,$||(t.value=!0);try{const ce=await z.get("/api/agents");e.value=Array.isArray(ce)?ce:[],s.value=null}catch(ce){$||(s.value=ce.message)}$||(t.value=!1)}async function X($){const ce=e.value.find(Z=>Z.id===$);if(await os({title:"Kill agent",message:`Kill agent "${(ce==null?void 0:ce.label)||$}"? Its current work will be lost.`,confirmLabel:"Kill",danger:!0})){a.value=$;try{await z.del(`/api/agents/${encodeURIComponent($)}`),ke.success("Agent killed"),await H()}catch(Z){ke.error(Z.message||"Failed to kill agent")}a.value=null}}const Q=i1({isEnabled:()=>n.value&&l,refreshList:()=>H(!0),hasOpenDetail:()=>!!b.value,refreshDetail:R});function N(){Q.start()}function O(){Q.stop()}return jt(n,()=>Q.sync()),tt(()=>{l=!0,H(),N()}),rs(()=>{l=!0,H(!0),N()}),Kt(()=>{l=!1,O()}),xt(()=>{l=!1,O(),L.close()}),{agents:e,loading:t,error:s,killing:a,autoRefresh:n,statusFilter:i,runningCount:o,completedCount:r,failedCount:c,statusFilters:d,filteredAgents:u,formatTs:ii,formatDuration:Zi,progressPercent:f,hasProgress:m,displayModelText:g,displayEffortText:A,displaySourceLabel:w,detail:y,detailId:b,detailLoading:x,detailError:S,copied:_,openDetail:E,closeDetail:P,copyText:I,fetchAgents:H,killAgent:X,startAutoRefresh:N,stopAutoRefresh:O}}},o1={template:` +
`,setup(){const e=f([]),t=f(!0),s=f(null),a=f(null),n=f(!0),i=f("all");let l=!1;const o=B(()=>e.value.filter($=>$.status==="running").length),r=B(()=>e.value.filter($=>$.status==="completed").length),c=B(()=>e.value.filter($=>["failed","timeout","killed"].includes($.status)).length),d=B(()=>[{value:"all",label:"All",count:e.value.length},{value:"running",label:"Running",count:o.value},{value:"completed",label:"Completed",count:r.value},{value:"failed",label:"Failed",count:c.value}]),u=B(()=>i.value==="all"?e.value:i.value==="failed"?e.value.filter($=>["failed","timeout","killed"].includes($.status)):e.value.filter($=>$.status===i.value));function p($){const re=Number($.max_iterations)||0;return re<=0?0:Math.min(100,Math.round($.iteration_count/re*100))}function m($){return(Number($.max_iterations)||0)>0}function h($,re){return $?$==="N/A"?"N/A":re==="current_inheritance"?`inherit (currently ${$})`:$:"unknown"}function y($){return h($.display_model,$.display_model_source||$.display_source)}function k($){return h($.display_reasoning_effort,$.display_reasoning_effort_source||$.display_source)}function A($){return{last_execution:"last executed",current_inheritance:"inherited from current config — not yet executed",spawn_override_pending:"requested at spawn — not yet executed",unknown:"no execution data"}[$]||""}const g=f(null),b=f(null),x=f(!1),w=f(null),_=f(""),L=$g({state:{get detail(){return g.value},set detail($){g.value=$},get detailId(){return b.value},set detailId($){b.value=$},get detailLoading(){return x.value},set detailLoading($){x.value=$},get detailError(){return w.value},set detailError($){w.value=$}},requestDetail:($,{signal:re})=>z.get(`/api/agents/${encodeURIComponent($)}`,{signal:re})});async function E($){_.value="",await L.open($.id)}function F(){L.close(),_.value=""}async function O(){await L.refresh()}async function I($,re){try{await navigator.clipboard.writeText(re||""),_.value=$,setTimeout(()=>{_.value===$&&(_.value="")},1500)}catch{Se.error("Copy failed")}}async function H($=!1){$=$===!0,$||(t.value=!0);try{const re=await z.get("/api/agents");e.value=Array.isArray(re)?re:[],s.value=null}catch(re){$||(s.value=re.message)}$||(t.value=!1)}async function te($){const re=e.value.find(Z=>Z.id===$);if(await is({title:"Kill agent",message:`Kill agent "${(re==null?void 0:re.label)||$}"? Its current work will be lost.`,confirmLabel:"Kill",danger:!0})){a.value=$;try{await z.del(`/api/agents/${encodeURIComponent($)}`),Se.success("Agent killed"),await H()}catch(Z){Se.error(Z.message||"Failed to kill agent")}a.value=null}}const ee=T1({isEnabled:()=>n.value&&l,refreshList:()=>H(!0),hasOpenDetail:()=>!!b.value,refreshDetail:O});function N(){ee.start()}function M(){ee.stop()}return Gt(n,()=>ee.sync()),st(()=>{l=!0,H(),N()}),cs(()=>{l=!0,H(!0),N()}),es(()=>{l=!1,M()}),wt(()=>{l=!1,M(),L.close()}),{agents:e,loading:t,error:s,killing:a,autoRefresh:n,statusFilter:i,runningCount:o,completedCount:r,failedCount:c,statusFilters:d,filteredAgents:u,formatTs:di,formatDuration:Xi,progressPercent:p,hasProgress:m,displayModelText:y,displayEffortText:k,displaySourceLabel:A,detail:g,detailId:b,detailLoading:x,detailError:w,copied:_,openDetail:E,closeDetail:F,copyText:I,fetchAgents:H,killAgent:te,startAutoRefresh:N,stopAutoRefresh:M}}},A1={template:`

Autonomous Loops

@@ -787,7 +787,7 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p([]),t=p(!0),s=p(null),a=p(!1),n=p({goal:"",interval_seconds:60,mode:"notify",max_iterations:50,stop_condition:"",channel_id:""}),i=p(!1),l=p(null),o=p(null),r=p(null),c=p(null),d=p(null),u=p(!1),f=p(null),m=p("");let h=!1;const A=Eg({state:{get detail(){return c.value},set detail(O){c.value=O},get detailId(){return d.value},set detailId(O){d.value=O},get detailLoading(){return u.value},set detailLoading(O){u.value=O},get detailError(){return f.value},set detailError(O){f.value=O}},detailLabel:"Loop detail",requestDetail:(O,{signal:$})=>z.get(`/api/loops/${encodeURIComponent(O)}?limit=100`,{signal:$})});async function w(O){m.value="",await A.open(O.id)}function y(){A.close(),m.value=""}async function b(O,$){try{await navigator.clipboard.writeText($||""),m.value=O,setTimeout(()=>{m.value===O&&(m.value="")},1500)}catch{ke.error("Copy failed")}}const x=U(()=>e.value.reduce((O,$)=>O+($.iteration_count||0),0)),S=U(()=>e.value.filter(O=>O.status==="running").length);function _(O){return O==="running"?"loop-status-running":O==="error"?"loop-status-error":"loop-status-stopped"}function T(O){return O==="running"?"badge-success":O==="error"?"badge-danger":O==="completed"?"badge-info":"badge-warning"}function L(O){return O==="act"?"badge-warning":O==="silent"?"badge-info":"badge-success"}async function E(O=!1){O=O===!0,O||(t.value=!0);try{const $=await z.get("/api/loops");e.value=Array.isArray($)?$:[],s.value=null}catch($){O||(s.value=$.message)}O||(t.value=!1)}async function P(){l.value=null;const O=n.value;if(!O.goal.trim()){l.value="Goal is required";return}if(!O.channel_id.trim()){l.value="Channel ID is required";return}const $={goal:O.goal.trim(),channel_id:O.channel_id.trim(),interval_seconds:O.interval_seconds||60,mode:O.mode,max_iterations:O.max_iterations||50};O.stop_condition.trim()&&($.stop_condition=O.stop_condition.trim()),i.value=!0;try{const ce=await z.post("/api/loops",$);ke.success(`Loop started: ${ce.loop_id}`),n.value={goal:"",interval_seconds:60,mode:"notify",max_iterations:50,stop_condition:"",channel_id:""},a.value=!1,await E()}catch(ce){l.value=ce.message}i.value=!1}async function R(O){if(await os({title:"Stop loop",message:`Stop loop ${O}? The current iteration will finish before stopping.`,confirmLabel:"Stop Loop",danger:!0})){o.value=O;try{await z.del(`/api/loops/${encodeURIComponent(O)}`),ke.success("Loop stopped"),await E()}catch(ce){ke.error(ce.message||"Failed to stop loop")}o.value=null}}async function I(O){r.value=O;try{await z.post(`/api/loops/${encodeURIComponent(O)}/restart`),ke.success("Loop restarted"),await E()}catch($){ke.error($.message||"Failed to restart loop")}r.value=null}function H(O){h&&O.payload&&(O.payload.loop_id||O.payload.type==="loop")&&(E(!0),d.value&&A.refresh())}let X=null;function Q(){X!==null&&clearInterval(X),X=null}function N(){Q(),h&&(X=setInterval(()=>{E(!0),d.value&&A.refresh()},5e3))}return tt(()=>{h=!0,E(),ut.subscribe("events",H),N()}),rs(()=>{h=!0,E(!0),N()}),Kt(()=>{h=!1,Q()}),xt(()=>{h=!1,ut.unsubscribe("events",H),Q(),A.close()}),{loops:e,loading:t,error:s,showCreate:a,form:n,creating:i,createError:l,stoppingId:o,restartingId:r,detail:c,detailId:d,detailLoading:u,detailError:f,copied:m,totalIterations:x,runningCount:S,statusDotClass:_,statusBadge:T,modeBadge:L,formatAge:kg,formatDuration:Zi,formatTs:ii,formatTokens:Cg,openDetail:w,closeDetail:y,copyText:b,fetchLoops:E,doCreate:P,doStop:R,doRestart:I}}},r1={template:` + `,setup(){const e=f([]),t=f(!0),s=f(null),a=f(!1),n=f({goal:"",interval_seconds:60,mode:"notify",max_iterations:50,stop_condition:"",channel_id:""}),i=f(!1),l=f(null),o=f(null),r=f(null),c=f(null),d=f(null),u=f(!1),p=f(null),m=f("");let h=!1;const k=$g({state:{get detail(){return c.value},set detail(M){c.value=M},get detailId(){return d.value},set detailId(M){d.value=M},get detailLoading(){return u.value},set detailLoading(M){u.value=M},get detailError(){return p.value},set detailError(M){p.value=M}},detailLabel:"Loop detail",requestDetail:(M,{signal:$})=>z.get(`/api/loops/${encodeURIComponent(M)}?limit=100`,{signal:$})});async function A(M){m.value="",await k.open(M.id)}function g(){k.close(),m.value=""}async function b(M,$){try{await navigator.clipboard.writeText($||""),m.value=M,setTimeout(()=>{m.value===M&&(m.value="")},1500)}catch{Se.error("Copy failed")}}const x=B(()=>e.value.reduce((M,$)=>M+($.iteration_count||0),0)),w=B(()=>e.value.filter(M=>M.status==="running").length);function _(M){return M==="running"?"loop-status-running":M==="error"?"loop-status-error":"loop-status-stopped"}function T(M){return M==="running"?"badge-success":M==="error"?"badge-danger":M==="completed"?"badge-info":"badge-warning"}function L(M){return M==="act"?"badge-warning":M==="silent"?"badge-info":"badge-success"}async function E(M=!1){M=M===!0,M||(t.value=!0);try{const $=await z.get("/api/loops");e.value=Array.isArray($)?$:[],s.value=null}catch($){M||(s.value=$.message)}M||(t.value=!1)}async function F(){l.value=null;const M=n.value;if(!M.goal.trim()){l.value="Goal is required";return}if(!M.channel_id.trim()){l.value="Channel ID is required";return}const $={goal:M.goal.trim(),channel_id:M.channel_id.trim(),interval_seconds:M.interval_seconds||60,mode:M.mode,max_iterations:M.max_iterations||50};M.stop_condition.trim()&&($.stop_condition=M.stop_condition.trim()),i.value=!0;try{const re=await z.post("/api/loops",$);Se.success(`Loop started: ${re.loop_id}`),n.value={goal:"",interval_seconds:60,mode:"notify",max_iterations:50,stop_condition:"",channel_id:""},a.value=!1,await E()}catch(re){l.value=re.message}i.value=!1}async function O(M){if(await is({title:"Stop loop",message:`Stop loop ${M}? The current iteration will finish before stopping.`,confirmLabel:"Stop Loop",danger:!0})){o.value=M;try{await z.del(`/api/loops/${encodeURIComponent(M)}`),Se.success("Loop stopped"),await E()}catch(re){Se.error(re.message||"Failed to stop loop")}o.value=null}}async function I(M){r.value=M;try{await z.post(`/api/loops/${encodeURIComponent(M)}/restart`),Se.success("Loop restarted"),await E()}catch($){Se.error($.message||"Failed to restart loop")}r.value=null}function H(M){h&&M.payload&&(M.payload.loop_id||M.payload.type==="loop")&&(E(!0),d.value&&k.refresh())}let te=null;function ee(){te!==null&&clearInterval(te),te=null}function N(){ee(),h&&(te=setInterval(()=>{E(!0),d.value&&k.refresh()},5e3))}return st(()=>{h=!0,E(),mt.subscribe("events",H),N()}),cs(()=>{h=!0,E(!0),N()}),es(()=>{h=!1,ee()}),wt(()=>{h=!1,mt.unsubscribe("events",H),ee(),k.close()}),{loops:e,loading:t,error:s,showCreate:a,form:n,creating:i,createError:l,stoppingId:o,restartingId:r,detail:c,detailId:d,detailLoading:u,detailError:p,copied:m,totalIterations:x,runningCount:w,statusDotClass:_,statusBadge:T,modeBadge:L,formatAge:Ng,formatDuration:Xi,formatTs:di,formatTokens:Dg,openDetail:A,closeDetail:g,copyText:b,fetchLoops:E,doCreate:F,doStop:O,doRestart:I}}},R1={template:`
@@ -880,7 +880,7 @@ ${u.text}`:u.text).join(`
-
`,setup(){const e=p([]),t=p(!0),s=p(null),a=p(!0);let n=null;const i=p(null),l=U(()=>e.value.filter(y=>y.status==="running").length),o=U(()=>e.value.filter(y=>y.status!=="running").length);function r(y){return y==="running"?"loop-status-running":y==="failed"||y==="error"?"loop-status-error":"loop-status-stopped"}function c(y){return y==="running"?"badge-success":y==="completed"||y==="exited"?"badge-info":y==="killed"||y==="error"||y==="failed"?"badge-danger":"badge-warning"}async function d(y=!1){y=y===!0,y||(t.value=!0);try{e.value=await z.get("/api/processes"),s.value=null}catch(b){y||(s.value=b.message)}y||(t.value=!1)}function u(){f(),a.value&&(n=setInterval(()=>{t.value||d(!0)},5e3))}function f(){n&&(clearInterval(n),n=null)}jt(a,y=>{y?u():f()});async function m(y){if(await os({title:"Kill process",message:`Kill process ${y}?`,confirmLabel:"Kill",danger:!0})){i.value=y;try{await z.del(`/api/processes/${y}`),ke.success(`Process ${y} killed`),await d()}catch(x){ke.error(x.message||"Failed to kill process")}i.value=null}}function h(y){y.payload&&(y.payload.pid||y.payload.type==="process")&&d(!0)}let g=!1;function A(){g||(g=!0,d(),ut.subscribe("events",h),u())}function w(){g&&(g=!1,ut.unsubscribe("events",h),f())}return tt(A),rs(A),Kt(w),xt(w),{processes:e,loading:t,error:s,autoRefresh:a,killingPid:i,runningCount:l,completedCount:o,procStatusDot:r,statusBadge:c,formatDuration:Zi,fetchProcesses:d,doKill:m}}},c1=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;function Nf(e,t){return t==="cron"&&String(e.cron||"").trim()?e.run_at="":t==="run_at"&&String(e.run_at||"").trim()&&(e.cron=""),e}function d1(e,t=!1){const s=n=>String(n).padStart(2,"0"),a=`${e.getFullYear()}-${s(e.getMonth()+1)}-${s(e.getDate())}T${s(e.getHours())}:${s(e.getMinutes())}`;return t?`${a}:${s(e.getSeconds())}`:a}function u1(e){const t=-e.getTimezoneOffset(),s=t>=0?"+":"-",a=Math.abs(t),n=Math.floor(a/60),i=a%60;return`UTC${s}${n}${i?`:${String(i).padStart(2,"0")}`:""}`}function Mf(e){const t=String(e||"").trim();if(!t)return{state:"empty"};const s=c1.exec(t);if(!s)return{state:"invalid",typed:t};const[,a,n,i,l,o]=s.slice(0,6).map(Number),r=s[6]===void 0?0:Number(s[6]);if(r>59)return{state:"invalid",typed:t};const c=s[6]!==void 0,d=c?t.slice(0,19):t.slice(0,16),u=Date.UTC(a,n-1,i,l,o,r),f=new Date(u-864e5).getTimezoneOffset(),m=new Date(u+864e5).getTimezoneOffset(),h=[];for(const A of new Set([f,m])){const w=new Date(u+A*6e4);d1(w,c)===d&&(h.some(y=>y.getTime()===w.getTime())||h.push(w))}if(h.sort((A,w)=>A.getTime()-w.getTime()),h.length===0)return{state:"nonexistent",typed:t};if(h.length>1)return{state:"ambiguous",typed:t,options:h.map(A=>({instant:A,offset:u1(A),iso:A.toISOString()}))};const g=h[0];return{state:"ok",typed:t,instant:g,iso:g.toISOString()}}const p1=5e3;function Xo(e){const t=(e==null?void 0:e.available)===!0,s=typeof(e==null?void 0:e.reason)=="string"?e.reason:"provider_error";return{available:t,reason:s,epoch:Number.isInteger(e==null?void 0:e.epoch)?e.epoch:null}}function Lo(e){if(e.available)return"";switch(e.reason){case"unavailable":return"Scheduling is not configured.";case"connecting":return"Scheduling is connecting.";case"disconnected":return"Scheduling is disconnected.";case"provider_error":return"Scheduling status provider failed.";default:return"Scheduling is unavailable."}}function Df(e){var t;return(e==null?void 0:e.status)!==503||!((t=e==null?void 0:e.data)!=null&&t.connection)?null:Xo(e.data.connection)}function f1(e){return e!=="webhook"}function Pf(e,t){return!f1(t)||(e==null?void 0:e.available)===!0}function m1(e,t=null){if((e==null?void 0:e.state)==="ok")return e.instant.toISOString();if((e==null?void 0:e.state)==="ambiguous"){if(t==null||t==="")throw new Error("Choose which occurrence of this repeated local time to use.");const s=e.options[Number(t)];if(!(s!=null&&s.instant))throw new Error("Choose a valid occurrence of this local time.");return s.instant.toISOString()}throw(e==null?void 0:e.state)==="nonexistent"?new Error("That local time does not exist because clocks skip it. Choose another time."):new Error("Enter a valid new one-time run time.")}const h1={template:` + `,setup(){const e=f([]),t=f(!0),s=f(null),a=f(!0);let n=null;const i=f(null),l=B(()=>e.value.filter(g=>g.status==="running").length),o=B(()=>e.value.filter(g=>g.status!=="running").length);function r(g){return g==="running"?"loop-status-running":g==="failed"||g==="error"?"loop-status-error":"loop-status-stopped"}function c(g){return g==="running"?"badge-success":g==="completed"||g==="exited"?"badge-info":g==="killed"||g==="error"||g==="failed"?"badge-danger":"badge-warning"}async function d(g=!1){g=g===!0,g||(t.value=!0);try{e.value=await z.get("/api/processes"),s.value=null}catch(b){g||(s.value=b.message)}g||(t.value=!1)}function u(){p(),a.value&&(n=setInterval(()=>{t.value||d(!0)},5e3))}function p(){n&&(clearInterval(n),n=null)}Gt(a,g=>{g?u():p()});async function m(g){if(await is({title:"Kill process",message:`Kill process ${g}?`,confirmLabel:"Kill",danger:!0})){i.value=g;try{await z.del(`/api/processes/${g}`),Se.success(`Process ${g} killed`),await d()}catch(x){Se.error(x.message||"Failed to kill process")}i.value=null}}function h(g){g.payload&&(g.payload.pid||g.payload.type==="process")&&d(!0)}let y=!1;function k(){y||(y=!0,d(),mt.subscribe("events",h),u())}function A(){y&&(y=!1,mt.unsubscribe("events",h),p())}return st(k),cs(k),es(A),wt(A),{processes:e,loading:t,error:s,autoRefresh:a,killingPid:i,runningCount:l,completedCount:o,procStatusDot:r,statusBadge:c,formatDuration:Xi,fetchProcesses:d,doKill:m}}},I1=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;function jf(e,t){return t==="cron"&&String(e.cron||"").trim()?e.run_at="":t==="run_at"&&String(e.run_at||"").trim()&&(e.cron=""),e}function O1(e,t=!1){const s=n=>String(n).padStart(2,"0"),a=`${e.getFullYear()}-${s(e.getMonth()+1)}-${s(e.getDate())}T${s(e.getHours())}:${s(e.getMinutes())}`;return t?`${a}:${s(e.getSeconds())}`:a}function L1(e){const t=-e.getTimezoneOffset(),s=t>=0?"+":"-",a=Math.abs(t),n=Math.floor(a/60),i=a%60;return`UTC${s}${n}${i?`:${String(i).padStart(2,"0")}`:""}`}function qf(e){const t=String(e||"").trim();if(!t)return{state:"empty"};const s=I1.exec(t);if(!s)return{state:"invalid",typed:t};const[,a,n,i,l,o]=s.slice(0,6).map(Number),r=s[6]===void 0?0:Number(s[6]);if(r>59)return{state:"invalid",typed:t};const c=s[6]!==void 0,d=c?t.slice(0,19):t.slice(0,16),u=Date.UTC(a,n-1,i,l,o,r),p=new Date(u-864e5).getTimezoneOffset(),m=new Date(u+864e5).getTimezoneOffset(),h=[];for(const k of new Set([p,m])){const A=new Date(u+k*6e4);O1(A,c)===d&&(h.some(g=>g.getTime()===A.getTime())||h.push(A))}if(h.sort((k,A)=>k.getTime()-A.getTime()),h.length===0)return{state:"nonexistent",typed:t};if(h.length>1)return{state:"ambiguous",typed:t,options:h.map(k=>({instant:k,offset:L1(k),iso:k.toISOString()}))};const y=h[0];return{state:"ok",typed:t,instant:y,iso:y.toISOString()}}const N1=5e3;function lr(e){const t=(e==null?void 0:e.available)===!0,s=typeof(e==null?void 0:e.reason)=="string"?e.reason:"provider_error";return{available:t,reason:s,epoch:Number.isInteger(e==null?void 0:e.epoch)?e.epoch:null}}function zo(e){if(e.available)return"";switch(e.reason){case"unavailable":return"Scheduling is not configured.";case"connecting":return"Scheduling is connecting.";case"disconnected":return"Scheduling is disconnected.";case"provider_error":return"Scheduling status provider failed.";default:return"Scheduling is unavailable."}}function Vf(e){var t;return(e==null?void 0:e.status)!==503||!((t=e==null?void 0:e.data)!=null&&t.connection)?null:lr(e.data.connection)}function M1(e){return e!=="webhook"}function Gf(e,t){return!M1(t)||(e==null?void 0:e.available)===!0}function D1(e,t=null){if((e==null?void 0:e.state)==="ok")return e.instant.toISOString();if((e==null?void 0:e.state)==="ambiguous"){if(t==null||t==="")throw new Error("Choose which occurrence of this repeated local time to use.");const s=e.options[Number(t)];if(!(s!=null&&s.instant))throw new Error("Choose a valid occurrence of this local time.");return s.instant.toISOString()}throw(e==null?void 0:e.state)==="nonexistent"?new Error("That local time does not exist because clocks skip it. Choose another time."):new Error("Enter a valid new one-time run time.")}const P1={template:`
@@ -1298,7 +1298,7 @@ ${u.text}`:u.text).join(`
-
`,setup(){const e=p([]),t=p(!0),s=p(null),a=p(Xo(null)),n=U(()=>a.value.available),i=U(()=>Lo(a.value));let l=null;const o=p(!1),r=p({description:"",action:"reminder",channel_id:"",cron:"",run_at:"",message:"",tool_name:"",tool_input_str:"",report_format:"",webhook_url:"",webhook_method:"POST",webhook_headers_str:"",webhook_body:"",webhook_expected_status_str:""}),c=p(!1),d=p(null),u=U(()=>Pf(a.value,r.value.action));function f(B){return Pf(a.value,B)}const m=p(null),h=U(()=>Mf(r.value.run_at));jt(()=>r.value.run_at,()=>{m.value=null});const g=U(()=>{var fe;const B=h.value;return B.state==="ok"?B.instant:B.state==="ambiguous"&&m.value!==null&&((fe=B.options[m.value])==null?void 0:fe.instant)||null}),A=U(()=>{const B=g.value;return B?`${B.toLocaleString()} local — ${B.toISOString()} UTC`:""}),w=p(null),y=p(!1),b=[{label:"Every hour",expr:"0 * * * *"},{label:"Every 6h",expr:"0 */6 * * *"},{label:"Daily 9am",expr:"0 9 * * *"},{label:"Weekly Mon",expr:"0 9 * * 1"},{label:"Every 30m",expr:"*/30 * * * *"}],x=p(null),S=p(null),_=p(null),T=p(null),L=p(null),E=p(null),P=p({}),R=p({}),I=p(null),H=p([]),X=p(!1),Q=p("");let N=0;const O=U(()=>e.value.filter(B=>B.cron&&!B.one_time).length),$=U(()=>e.value.filter(B=>B.one_time).length),ce=U(()=>e.value.filter(B=>B.trigger).length),ue=U(()=>e.value.filter(B=>B.paused).length),Z=U(()=>e.value.filter(B=>B.consecutive_failures>0).length);function J(B){if(!B)return"-";const fe=Date.now(),be=(new Date(B).getTime()-fe)/1e3;if(be<0)return"overdue";if(be<60)return"in < 1 min";if(be<3600)return`in ${Math.floor(be/60)} min`;if(be<86400){const qe=Math.floor(be/3600),$e=Math.floor(be%3600/60);return $e>0?`in ${qe}h ${$e}m`:`in ${qe}h`}const Fe=Math.floor(be/86400);return`in ${Fe} day${Fe!==1?"s":""}`}function se(B){return B==null?"-":B<1e3?`${B}ms`:B<6e4?`${(B/1e3).toFixed(1)}s`:Zi(B/1e3)}function q(B=r.value.cron){r.value.cron=B,Nf(r.value,"cron"),w.value=null}function ye(B=r.value.run_at){r.value.run_at=B,Nf(r.value,"run_at"),w.value=null}function Oe(B){return Mf(P.value[B.id]||"")}function le(B,fe){P.value[B]=fe,delete R.value[B]}function oe(B,fe){R.value[B]=fe===""?null:Number(fe)}async function ve(B){let fe;try{fe=m1(Oe(B),R.value[B.id])}catch(de){ke.error(de.message);return}E.value=B.id;try{const de=await z.put(`/api/schedules/${encodeURIComponent(B.id)}`,{run_at:fe});de!=null&&de.inert_reason||de!=null&&de.paused?ke.error(`Schedule remains paused and inert: ${de.inert_reason||"the server kept it paused"}`):(ke.success("Schedule re-armed for the new run time"),delete P.value[B.id],delete R.value[B.id]),await V()}catch(de){k(de),ke.error(de.message||"Failed to re-arm schedule")}finally{E.value=null}}async function j(){const B=r.value.cron.trim();if(B){y.value=!0;try{w.value=await z.post("/api/schedules/validate-cron",{expression:B})}catch(fe){w.value={valid:!1,error:fe.message}}y.value=!1}}async function V(){t.value=!0,s.value=null;try{e.value=await z.get("/api/schedules")}catch(B){s.value=B.message}t.value=!1}async function xe(){try{a.value=Xo(await z.get("/api/schedules/status"))}catch(B){a.value=Df(B)||Xo(null)}}function k(B){const fe=Df(B);fe&&(a.value=fe)}async function D(B){if(I.value===B){I.value=null,H.value=[];return}I.value=B,X.value=!0,H.value=[];const fe=++N;try{const de=await z.get(`/api/schedules/${encodeURIComponent(B)}/history?limit=10`);if(fe!==N||I.value!==B)return;H.value=de,Q.value=""}catch(de){if(fe!==N||I.value!==B)return;H.value=[],Q.value=de.message||"Failed to load execution history"}fe===N&&(X.value=!1)}async function W(){if(d.value=null,!f(r.value.action)){d.value=Lo(a.value);return}const B=r.value;if(!B.description.trim()){d.value="Description is required";return}if(B.action!=="webhook"&&!B.channel_id.trim()){d.value="Channel ID is required";return}if(!B.cron.trim()&&!B.run_at.trim()){d.value="Cron expression or run_at time is required";return}if(B.cron.trim()&&B.run_at.trim()){d.value="Choose either Cron or One-Time, not both";return}const fe={description:B.description.trim(),action:B.action,channel_id:B.channel_id.trim()};if(B.cron.trim()&&(fe.cron=B.cron.trim()),B.run_at.trim()){const de=h.value;if(de.state==="nonexistent"){d.value="That local time does not exist (daylight saving gap)";return}if(de.state==="invalid"){d.value="One-time run time is not a valid date";return}const be=g.value;if(de.state==="ambiguous"&&m.value===null){d.value="That local time happens twice — choose which occurrence to use";return}if(!be){d.value="One-time run time could not be resolved";return}fe.run_at=be.toISOString()}if(B.action==="reminder"&&B.message.trim()&&(fe.message=B.message.trim()),B.action==="check"&&(B.tool_name.trim()&&(fe.tool_name=B.tool_name.trim()),B.report_format&&(fe.report_format=B.report_format),B.tool_input_str.trim()))try{fe.tool_input=JSON.parse(B.tool_input_str.trim())}catch{d.value="Tool input must be valid JSON";return}if(B.action==="webhook"){if(!B.webhook_url.trim()){d.value="Webhook URL is required";return}const de={url:B.webhook_url.trim(),method:B.webhook_method};if(B.webhook_headers_str.trim())try{const be=JSON.parse(B.webhook_headers_str.trim());if(!be||Array.isArray(be)||typeof be!="object")throw new Error("not an object");de.headers=be}catch{d.value="Webhook headers must be a valid JSON object";return}if(B.webhook_body&&(de.body=B.webhook_body),B.webhook_expected_status_str.trim()){const be=B.webhook_expected_status_str.split(",").map(Fe=>Number(Fe.trim()));if(be.some(Fe=>!Number.isInteger(Fe)||Fe<100||Fe>599)){d.value="Expected status codes must be comma-separated HTTP codes";return}de.expected_status_codes=be}fe.webhook_config=de}c.value=!0;try{await z.post("/api/schedules",fe),ke.success("Schedule created"),r.value={description:"",action:"reminder",channel_id:"",cron:"",run_at:"",message:"",tool_name:"",tool_input_str:"",report_format:"",webhook_url:"",webhook_method:"POST",webhook_headers_str:"",webhook_body:"",webhook_expected_status_str:""},w.value=null,o.value=!1,await V()}catch(de){k(de),d.value=de.message}c.value=!1}async function ge(B){if(!f(B.action)){ke.error(Lo(a.value));return}const fe=B.id;x.value=fe;try{const de=await z.post(`/api/schedules/${encodeURIComponent(fe)}/run`);if(de.status==="failure")ke.error(`Execution failed: ${de.error||"unknown error"}`);else{const be=de.warning?`Executed (${de.warning})`:"Executed successfully";ke.success(be)}await V()}catch(de){k(de),ke.error(de.message||"Failed to trigger")}x.value=null}async function pe(B){if(B.paused&&!f(B.action)){ke.error(Lo(a.value));return}_.value=B.id;const fe=!B.paused;try{const de=await z.put(`/api/schedules/${encodeURIComponent(B.id)}`,{paused:fe});!fe&&(de!=null&&de.inert_reason)?ke.error(`Schedule remains paused and inert: ${de.inert_reason}`):ke.success(fe?"Schedule paused":"Schedule resumed"),await V()}catch(de){k(de),ke.error(de.message||"Failed to update schedule")}_.value=null}const he=new Map;function we(B,fe){const de=he.get(B.id);de&&clearTimeout(de.timer);const be={run:()=>K(B,fe),timer:null};be.timer=setTimeout(()=>{he.delete(B.id),be.run()},500),he.set(B.id,be)}async function K(B,fe){L.value=B.id;try{await z.put(`/api/schedules/${encodeURIComponent(B.id)}`,{report_format:fe}),ke.success(fe?"Structured report enabled":"Plain-text report enabled")}catch(de){ke.error(`Update failed: ${de.message}`)}finally{await V(),L.value=null}}function ne(){for(const[B,fe]of[...he])clearTimeout(fe.timer),he.delete(B),fe.run()}async function te(B){T.value=B;try{await z.post(`/api/schedules/${encodeURIComponent(B)}/reset-failures`),ke.success("Failure counters reset"),await V()}catch(fe){ke.error(fe.message||"Failed to reset")}T.value=null}async function _e(B){const fe=e.value.find(be=>be.id===B);if(await os({title:"Delete schedule",message:`Delete "${(fe==null?void 0:fe.description)||B}"? This cannot be undone.`,confirmLabel:"Delete",danger:!0})){S.value=B;try{await z.del(`/api/schedules/${encodeURIComponent(B)}`),ke.success("Schedule deleted"),await V()}catch(be){ke.error(be.message||"Failed to delete schedule")}S.value=null}}return tt(()=>{V(),xe(),l=setInterval(xe,p1)}),xt(()=>{ne(),l&&clearInterval(l)}),{schedules:e,loading:t,error:s,schedulingAvailable:n,schedulingAvailabilityMessage:i,selectedActionAvailable:u,actionAvailable:f,showCreate:o,form:r,creating:c,createError:d,runAtUtcPreview:A,runAtAnalysis:h,runAtOccurrence:m,cronResult:w,validatingCron:y,cronPresets:b,runningId:x,deletingId:S,togglingId:_,resettingId:T,reportUpdatingId:L,flushReportFormatTimers:ne,rearmingId:E,rearmRunAt:P,rearmOccurrences:R,rearmAnalysis:Oe,expandedId:I,history:H,historyLoading:X,historyError:Q,cronCount:O,oneTimeCount:$,webhookCount:ce,pausedCount:ue,failingCount:Z,formatTs:ii,formatAge:kg,formatFuture:J,formatMs:se,formatDuration:Zi,onCronInput:q,onRunAtInput:ye,validateCron:j,toggleExpand:D,fetchSchedules:V,fetchSchedulingAvailability:xe,doCreate:W,doRunNow:ge,doTogglePause:pe,doUpdateReportFormat:we,doResetFailures:te,doDelete:_e,setRearmRunAt:le,setRearmOccurrence:oe,doRearmSchedule:ve}}},Ag=[{id:"live",label:"Live",component:e1},{id:"agents",label:"Agents",component:l1},{id:"loops",label:"Loops",component:o1},{id:"processes",label:"Processes",component:r1},{id:"schedules",label:"Schedules",component:h1}],v1={components:{TabbedPage:nc},setup(){return{tabs:Ag}},template:''},Rd=/^\d{15,25}$/,No=new Map,Mo=new Map;let Do=null,Po=null;async function g1(){return Do||(Po||(Po=z.get("/api/discord/members").then(e=>(Do=Array.isArray(e)?e:[],Do)).catch(()=>[]).finally(()=>{Po=null})),Po)}async function b1(e){if(!Rd.test(String(e)))return null;const t=(await g1()).find(s=>String(s.id)===String(e));return t?(No.set(String(e),t),t):No.has(String(e))?No.get(String(e)):(Mo.has(String(e))||Mo.set(String(e),z.get(`/api/discord/users/${encodeURIComponent(e)}`).then(s=>(s==null?void 0:s.user)||null).catch(()=>null).then(s=>(s&&No.set(String(e),s),Mo.delete(String(e)),s))),Mo.get(String(e)))}const En={props:{userId:{type:[String,Number],required:!0},members:{type:Array,default:()=>[]},showId:{type:Boolean,default:!0},fallbackLabel:{type:String,default:""}},template:` + `,setup(){const e=f([]),t=f(!0),s=f(null),a=f(lr(null)),n=B(()=>a.value.available),i=B(()=>zo(a.value));let l=null;const o=f(!1),r=f({description:"",action:"reminder",channel_id:"",cron:"",run_at:"",message:"",tool_name:"",tool_input_str:"",report_format:"",webhook_url:"",webhook_method:"POST",webhook_headers_str:"",webhook_body:"",webhook_expected_status_str:""}),c=f(!1),d=f(null),u=B(()=>Gf(a.value,r.value.action));function p(U){return Gf(a.value,U)}const m=f(null),h=B(()=>qf(r.value.run_at));Gt(()=>r.value.run_at,()=>{m.value=null});const y=B(()=>{var de;const U=h.value;return U.state==="ok"?U.instant:U.state==="ambiguous"&&m.value!==null&&((de=U.options[m.value])==null?void 0:de.instant)||null}),k=B(()=>{const U=y.value;return U?`${U.toLocaleString()} local — ${U.toISOString()} UTC`:""}),A=f(null),g=f(!1),b=[{label:"Every hour",expr:"0 * * * *"},{label:"Every 6h",expr:"0 */6 * * *"},{label:"Daily 9am",expr:"0 9 * * *"},{label:"Weekly Mon",expr:"0 9 * * 1"},{label:"Every 30m",expr:"*/30 * * * *"}],x=f(null),w=f(null),_=f(null),T=f(null),L=f(null),E=f(null),F=f({}),O=f({}),I=f(null),H=f([]),te=f(!1),ee=f("");let N=0;const M=B(()=>e.value.filter(U=>U.cron&&!U.one_time).length),$=B(()=>e.value.filter(U=>U.one_time).length),re=B(()=>e.value.filter(U=>U.trigger).length),ce=B(()=>e.value.filter(U=>U.paused).length),Z=B(()=>e.value.filter(U=>U.consecutive_failures>0).length);function Q(U){if(!U)return"-";const de=Date.now(),Ae=(new Date(U).getTime()-de)/1e3;if(Ae<0)return"overdue";if(Ae<60)return"in < 1 min";if(Ae<3600)return`in ${Math.floor(Ae/60)} min`;if(Ae<86400){const We=Math.floor(Ae/3600),je=Math.floor(Ae%3600/60);return je>0?`in ${We}h ${je}m`:`in ${We}h`}const $e=Math.floor(Ae/86400);return`in ${$e} day${$e!==1?"s":""}`}function ae(U){return U==null?"-":U<1e3?`${U}ms`:U<6e4?`${(U/1e3).toFixed(1)}s`:Xi(U/1e3)}function V(U=r.value.cron){r.value.cron=U,jf(r.value,"cron"),A.value=null}function be(U=r.value.run_at){r.value.run_at=U,jf(r.value,"run_at"),A.value=null}function Oe(U){return qf(F.value[U.id]||"")}function le(U,de){F.value[U]=de,delete O.value[U]}function oe(U,de){O.value[U]=de===""?null:Number(de)}async function ve(U){let de;try{de=D1(Oe(U),O.value[U.id])}catch(ge){Se.error(ge.message);return}E.value=U.id;try{const ge=await z.put(`/api/schedules/${encodeURIComponent(U.id)}`,{run_at:de});ge!=null&&ge.inert_reason||ge!=null&&ge.paused?Se.error(`Schedule remains paused and inert: ${ge.inert_reason||"the server kept it paused"}`):(Se.success("Schedule re-armed for the new run time"),delete F.value[U.id],delete O.value[U.id]),await Y()}catch(ge){S(ge),Se.error(ge.message||"Failed to re-arm schedule")}finally{E.value=null}}async function j(){const U=r.value.cron.trim();if(U){g.value=!0;try{A.value=await z.post("/api/schedules/validate-cron",{expression:U})}catch(de){A.value={valid:!1,error:de.message}}g.value=!1}}async function Y(){t.value=!0,s.value=null;try{e.value=await z.get("/api/schedules")}catch(U){s.value=U.message}t.value=!1}async function xe(){try{a.value=lr(await z.get("/api/schedules/status"))}catch(U){a.value=Vf(U)||lr(null)}}function S(U){const de=Vf(U);de&&(a.value=de)}async function P(U){if(I.value===U){I.value=null,H.value=[];return}I.value=U,te.value=!0,H.value=[];const de=++N;try{const ge=await z.get(`/api/schedules/${encodeURIComponent(U)}/history?limit=10`);if(de!==N||I.value!==U)return;H.value=ge,ee.value=""}catch(ge){if(de!==N||I.value!==U)return;H.value=[],ee.value=ge.message||"Failed to load execution history"}de===N&&(te.value=!1)}async function G(){if(d.value=null,!p(r.value.action)){d.value=zo(a.value);return}const U=r.value;if(!U.description.trim()){d.value="Description is required";return}if(U.action!=="webhook"&&!U.channel_id.trim()){d.value="Channel ID is required";return}if(!U.cron.trim()&&!U.run_at.trim()){d.value="Cron expression or run_at time is required";return}if(U.cron.trim()&&U.run_at.trim()){d.value="Choose either Cron or One-Time, not both";return}const de={description:U.description.trim(),action:U.action,channel_id:U.channel_id.trim()};if(U.cron.trim()&&(de.cron=U.cron.trim()),U.run_at.trim()){const ge=h.value;if(ge.state==="nonexistent"){d.value="That local time does not exist (daylight saving gap)";return}if(ge.state==="invalid"){d.value="One-time run time is not a valid date";return}const Ae=y.value;if(ge.state==="ambiguous"&&m.value===null){d.value="That local time happens twice — choose which occurrence to use";return}if(!Ae){d.value="One-time run time could not be resolved";return}de.run_at=Ae.toISOString()}if(U.action==="reminder"&&U.message.trim()&&(de.message=U.message.trim()),U.action==="check"&&(U.tool_name.trim()&&(de.tool_name=U.tool_name.trim()),U.report_format&&(de.report_format=U.report_format),U.tool_input_str.trim()))try{de.tool_input=JSON.parse(U.tool_input_str.trim())}catch{d.value="Tool input must be valid JSON";return}if(U.action==="webhook"){if(!U.webhook_url.trim()){d.value="Webhook URL is required";return}const ge={url:U.webhook_url.trim(),method:U.webhook_method};if(U.webhook_headers_str.trim())try{const Ae=JSON.parse(U.webhook_headers_str.trim());if(!Ae||Array.isArray(Ae)||typeof Ae!="object")throw new Error("not an object");ge.headers=Ae}catch{d.value="Webhook headers must be a valid JSON object";return}if(U.webhook_body&&(ge.body=U.webhook_body),U.webhook_expected_status_str.trim()){const Ae=U.webhook_expected_status_str.split(",").map($e=>Number($e.trim()));if(Ae.some($e=>!Number.isInteger($e)||$e<100||$e>599)){d.value="Expected status codes must be comma-separated HTTP codes";return}ge.expected_status_codes=Ae}de.webhook_config=ge}c.value=!0;try{await z.post("/api/schedules",de),Se.success("Schedule created"),r.value={description:"",action:"reminder",channel_id:"",cron:"",run_at:"",message:"",tool_name:"",tool_input_str:"",report_format:"",webhook_url:"",webhook_method:"POST",webhook_headers_str:"",webhook_body:"",webhook_expected_status_str:""},A.value=null,o.value=!1,await Y()}catch(ge){S(ge),d.value=ge.message}c.value=!1}async function he(U){if(!p(U.action)){Se.error(zo(a.value));return}const de=U.id;x.value=de;try{const ge=await z.post(`/api/schedules/${encodeURIComponent(de)}/run`);if(ge.status==="failure")Se.error(`Execution failed: ${ge.error||"unknown error"}`);else{const Ae=ge.warning?`Executed (${ge.warning})`:"Executed successfully";Se.success(Ae)}await Y()}catch(ge){S(ge),Se.error(ge.message||"Failed to trigger")}x.value=null}async function fe(U){if(U.paused&&!p(U.action)){Se.error(zo(a.value));return}_.value=U.id;const de=!U.paused;try{const ge=await z.put(`/api/schedules/${encodeURIComponent(U.id)}`,{paused:de});!de&&(ge!=null&&ge.inert_reason)?Se.error(`Schedule remains paused and inert: ${ge.inert_reason}`):Se.success(de?"Schedule paused":"Schedule resumed"),await Y()}catch(ge){S(ge),Se.error(ge.message||"Failed to update schedule")}_.value=null}const me=new Map;function we(U,de){const ge=me.get(U.id);ge&&clearTimeout(ge.timer);const Ae={run:()=>J(U,de),timer:null};Ae.timer=setTimeout(()=>{me.delete(U.id),Ae.run()},500),me.set(U.id,Ae)}async function J(U,de){L.value=U.id;try{await z.put(`/api/schedules/${encodeURIComponent(U.id)}`,{report_format:de}),Se.success(de?"Structured report enabled":"Plain-text report enabled")}catch(ge){Se.error(`Update failed: ${ge.message}`)}finally{await Y(),L.value=null}}function ie(){for(const[U,de]of[...me])clearTimeout(de.timer),me.delete(U),de.run()}async function X(U){T.value=U;try{await z.post(`/api/schedules/${encodeURIComponent(U)}/reset-failures`),Se.success("Failure counters reset"),await Y()}catch(de){Se.error(de.message||"Failed to reset")}T.value=null}async function _e(U){const de=e.value.find(Ae=>Ae.id===U);if(await is({title:"Delete schedule",message:`Delete "${(de==null?void 0:de.description)||U}"? This cannot be undone.`,confirmLabel:"Delete",danger:!0})){w.value=U;try{await z.del(`/api/schedules/${encodeURIComponent(U)}`),Se.success("Schedule deleted"),await Y()}catch(Ae){Se.error(Ae.message||"Failed to delete schedule")}w.value=null}}return st(()=>{Y(),xe(),l=setInterval(xe,N1)}),wt(()=>{ie(),l&&clearInterval(l)}),{schedules:e,loading:t,error:s,schedulingAvailable:n,schedulingAvailabilityMessage:i,selectedActionAvailable:u,actionAvailable:p,showCreate:o,form:r,creating:c,createError:d,runAtUtcPreview:k,runAtAnalysis:h,runAtOccurrence:m,cronResult:A,validatingCron:g,cronPresets:b,runningId:x,deletingId:w,togglingId:_,resettingId:T,reportUpdatingId:L,flushReportFormatTimers:ie,rearmingId:E,rearmRunAt:F,rearmOccurrences:O,rearmAnalysis:Oe,expandedId:I,history:H,historyLoading:te,historyError:ee,cronCount:M,oneTimeCount:$,webhookCount:re,pausedCount:ce,failingCount:Z,formatTs:di,formatAge:Ng,formatFuture:Q,formatMs:ae,formatDuration:Xi,onCronInput:V,onRunAtInput:be,validateCron:j,toggleExpand:P,fetchSchedules:Y,fetchSchedulingAvailability:xe,doCreate:G,doRunNow:he,doTogglePause:fe,doUpdateReportFormat:we,doResetFailures:X,doDelete:_e,setRearmRunAt:le,setRearmOccurrence:oe,doRearmSchedule:ve}}},Fg=[{id:"live",label:"Live",component:_1},{id:"agents",label:"Agents",component:E1},{id:"loops",label:"Loops",component:A1},{id:"processes",label:"Processes",component:R1},{id:"schedules",label:"Schedules",component:P1}],$1={components:{TabbedPage:uc},setup(){return{tabs:Fg}},template:''},$d=/^\d{15,25}$/,Ho=new Map,jo=new Map;let qo=null,Vo=null;async function F1(){return qo||(Vo||(Vo=z.get("/api/discord/members").then(e=>(qo=Array.isArray(e)?e:[],qo)).catch(()=>[]).finally(()=>{Vo=null})),Vo)}async function B1(e){if(!$d.test(String(e)))return null;const t=(await F1()).find(s=>String(s.id)===String(e));return t?(Ho.set(String(e),t),t):Ho.has(String(e))?Ho.get(String(e)):(jo.has(String(e))||jo.set(String(e),z.get(`/api/discord/users/${encodeURIComponent(e)}`).then(s=>(s==null?void 0:s.user)||null).catch(()=>null).then(s=>(s&&Ho.set(String(e),s),jo.delete(String(e)),s))),jo.get(String(e)))}const Dn={props:{userId:{type:[String,Number],required:!0},members:{type:Array,default:()=>[]},showId:{type:Boolean,default:!0},fallbackLabel:{type:String,default:""}},template:` - `},rC=["INFO","WARNING","ERROR"],Hf="WARNING+",cC=[{id:"all",name:"All Logs",icon:"list",filters:{}},{id:"errors",name:"Errors Only",icon:"error",filters:{level:"ERROR"}},{id:"warnings",name:"Warnings+",icon:"warning",filters:{levels:["WARNING","ERROR"]}},{id:"tools",name:"Tool Activity",icon:"wrench",filters:{hasToolName:!0}},{id:"recent-errors",name:"Recent Errors",icon:"flame",filters:{level:"ERROR",timeRange:"last_1h"}}],Uc=[{value:"",label:"All Time"},{value:"last_5m",label:"Last 5 min",seconds:300},{value:"last_15m",label:"Last 15 min",seconds:900},{value:"last_1h",label:"Last 1 hour",seconds:3600},{value:"last_4h",label:"Last 4 hours",seconds:14400},{value:"last_24h",label:"Last 24 hours",seconds:86400}],dC=[50,100,200,500],uC={components:{ToolOutput:ic,LogRecord:oC,DiscordIdentity:En},template:` + `},RC=["INFO","WARNING","ERROR"],Yf="WARNING+",IC=[{id:"all",name:"All Logs",icon:"list",filters:{}},{id:"errors",name:"Errors Only",icon:"error",filters:{level:"ERROR"}},{id:"warnings",name:"Warnings+",icon:"warning",filters:{levels:["WARNING","ERROR"]}},{id:"tools",name:"Tool Activity",icon:"wrench",filters:{hasToolName:!0}},{id:"recent-errors",name:"Recent Errors",icon:"flame",filters:{level:"ERROR",timeRange:"last_1h"}}],Zc=[{value:"",label:"All Time"},{value:"last_5m",label:"Last 5 min",seconds:300},{value:"last_15m",label:"Last 15 min",seconds:900},{value:"last_1h",label:"Last 1 hour",seconds:3600},{value:"last_4h",label:"Last 4 hours",seconds:14400},{value:"last_24h",label:"Last 24 hours",seconds:86400}],OC=[50,100,200,500],LC={components:{ToolOutput:pc,LogRecord:AC,DiscordIdentity:Dn},template:`
@@ -4332,10 +4340,10 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p("live"),t=p([]);let s=0;const a=p(!1),n=p(!1),i=p(!0),l=p(""),o=p(""),r=p(!1),c=p(!1),d=p(ut.state||"disconnected"),u=U(()=>{switch(d.value){case"connected":return"Live";case"connecting":return"Connecting…";case"reconnecting":return"Reconnecting…";default:return"Disconnected"}}),f=p(null),m=p(!1),h=p(null),g=2e3,A=rC,w=cC,y=Uc,b=p("all"),x=p(""),S=p(!1),_=p([]),T=p(!1),L=p(""),E=p([]);function P(){try{const re=localStorage.getItem("odin-log-presets");re&&(_.value=JSON.parse(re))}catch{}}function R(){try{localStorage.setItem("odin-log-presets",JSON.stringify(_.value))}catch{}}const I=U(()=>l.value!==""||o.value.trim()!==""||x.value!==""||S.value),H=U(()=>{const re=Uc.find(Ce=>Ce.value===x.value);return re?re.label:""}),X=U(()=>{if(!r.value||!o.value)return null;try{return new RegExp(o.value,"i"),null}catch(re){return re.message}}),Q=24,N=U(()=>{if(se.value.length===0)return[];const re=[],Ce=new Date,Ve=3600*1e3;for(let Ge=Q-1;Ge>=0;Ge--){const kt=new Date(Ce.getTime()-(Ge+1)*Ve),St=new Date(Ce.getTime()-Ge*Ve);re.push({start:kt,end:St,label:ue(kt,St),shortLabel:St.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}),total:0,info:0,warnings:0,errors:0})}for(const Ge of se.value){if(!Ge._time)continue;const kt=Ge._time.getTime();for(const St of re)if(kt>=St.start.getTime()&&kt{let re=1;for(const Ce of N.value)Ce.total>re&&(re=Ce.total);return re}),$=U(()=>{if(N.value.length===0)return"";const re=se.value.map(Ge=>Ge._time&&Ge._time.getTime()).filter(Boolean);if(re.length===0)return"";const Ce=new Date(Math.min(...re));return`${se.value.length} shown, oldest ${Ce.toLocaleTimeString()}`}),ce=U(()=>Math.ceil(Q/8));function ue(re,Ce){const Ve={hour:"2-digit",minute:"2-digit"};return re.toLocaleTimeString([],Ve)+" - "+Ce.toLocaleTimeString([],Ve)}function Z(re,Ce){return!Ce||!re?"0px":Math.max(2,re/Ce*100)+"%"}function J(re){const Ce=se.value.findIndex(Ve=>Ve._time&&Ve._time.getTime()>=re.start.getTime()&&Ve._time.getTime()=0&&f.value){const Ve=f.value.querySelector('[data-log-id="'+se.value[Ce].id+'"]');Ve&&(Ve.scrollIntoView({behavior:"smooth",block:"center"}),i.value=!1)}}const se=U(()=>{let re=t.value;if(S.value&&(re=re.filter(Ce=>!!Ce.tool)),l.value)if(l.value===Hf){const Ce={INFO:0,WARNING:1,ERROR:2,CRITICAL:3};re=re.filter(Ve=>Ce[Ve.level||"INFO"]>=Ce.WARNING)}else re=re.filter(Ce=>(Ce.level||"INFO")===l.value);if(x.value){const Ce=Uc.find(Ve=>Ve.value===x.value);if(Ce&&Ce.seconds){const Ve=new Date(Date.now()-Ce.seconds*1e3);re=re.filter(Ge=>Ge._time&&Ge._time>=Ve)}}if(o.value&&!X.value)if(r.value)try{const Ce=new RegExp(o.value,"i");re=re.filter(Ve=>{const Ge=Ve.searchText,kt=Ve.tool||"";return Ce.test(Ge)||Ce.test(kt)})}catch{}else{const Ce=o.value.toLowerCase();re=re.filter(Ve=>{const Ge=Ve.searchText.toLowerCase(),kt=(Ve.tool||"").toLowerCase();return Ge.includes(Ce)||kt.includes(Ce)})}return re}),q=U(()=>lC(se.value));function ye(re){const Ce=Od(re,++s);if(n.value){E.value.push(Ce);return}Oe(Ce)}function Oe(re){iC(t.value,re,g),i.value&&Mt(()=>le())}function le(re=!1){const Ce=f.value;Ce&&Ce.scrollTo({top:Ce.scrollHeight,behavior:re?"smooth":"instant"})}function oe(){i.value=!0,m.value=!1,Mt(()=>le(!0))}const ve=new Set(["PageUp","PageDown","ArrowUp","ArrowDown","Home","End"," "]);function j(){const re=f.value;if(!re)return;const Ce=re.scrollHeight-re.scrollTop-re.clientHeight<40;m.value=!i.value&&!Ce&&t.value.length>0,D.value&&V()}function V(){const re=f.value;!re||!i.value||re.scrollHeight-re.scrollTop-re.clientHeight>=40&&(i.value=!1,m.value=t.value.length>0)}function xe(){i.value&&requestAnimationFrame(V)}function k(re){ve.has(re.key)&&xe()}const D=p(!1);function W(){i.value&&(D.value=!0,requestAnimationFrame(V))}function ge(){D.value&&(D.value=!1,V())}function pe(){i.value&&(m.value=!1,Mt(()=>le()))}function he(){if(n.value=!n.value,!n.value&&E.value.length>0){for(const re of E.value)Oe(re);E.value=[]}}function we(){t.value=[],E.value=[],m.value=!1}function K(){let re;e.value==="search"?re=rt.value.map(kt=>{const St=kt.error?"ERROR":"INFO",cn=kt.tool_name?`[${kt.tool_name}] `:"";return`${kt.timestamp||""} ${St} ${cn}${kt.result_summary||kt.message||""}`}).join(` -`):re=se.value.map(Uf).join(` + `,setup(){const e=f("live"),t=f([]);let s=0;const a=f(!1),n=f(!1),i=f(!0),l=f(""),o=f(""),r=f(!1),c=f(!1),d=f(mt.state||"disconnected"),u=B(()=>{switch(d.value){case"connected":return"Live";case"connecting":return"Connecting…";case"reconnecting":return"Reconnecting…";default:return"Disconnected"}}),p=f(null),m=f(!1),h=f(null),y=2e3,k=RC,A=IC,g=Zc,b=f("all"),x=f(""),w=f(!1),_=f([]),T=f(!1),L=f(""),E=f([]);function F(){try{const ue=localStorage.getItem("odin-log-presets");ue&&(_.value=JSON.parse(ue))}catch{}}function O(){try{localStorage.setItem("odin-log-presets",JSON.stringify(_.value))}catch{}}const I=B(()=>l.value!==""||o.value.trim()!==""||x.value!==""||w.value),H=B(()=>{const ue=Zc.find(Ce=>Ce.value===x.value);return ue?ue.label:""}),te=B(()=>{if(!r.value||!o.value)return null;try{return new RegExp(o.value,"i"),null}catch(ue){return ue.message}}),ee=24,N=B(()=>{if(ae.value.length===0)return[];const ue=[],Ce=new Date,qe=3600*1e3;for(let Ge=ee-1;Ge>=0;Ge--){const It=new Date(Ce.getTime()-(Ge+1)*qe),ot=new Date(Ce.getTime()-Ge*qe);ue.push({start:It,end:ot,label:ce(It,ot),shortLabel:ot.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}),total:0,info:0,warnings:0,errors:0})}for(const Ge of ae.value){if(!Ge._time)continue;const It=Ge._time.getTime();for(const ot of ue)if(It>=ot.start.getTime()&&It{let ue=1;for(const Ce of N.value)Ce.total>ue&&(ue=Ce.total);return ue}),$=B(()=>{if(N.value.length===0)return"";const ue=ae.value.map(Ge=>Ge._time&&Ge._time.getTime()).filter(Boolean);if(ue.length===0)return"";const Ce=new Date(Math.min(...ue));return`${ae.value.length} shown, oldest ${Ce.toLocaleTimeString()}`}),re=B(()=>Math.ceil(ee/8));function ce(ue,Ce){const qe={hour:"2-digit",minute:"2-digit"};return ue.toLocaleTimeString([],qe)+" - "+Ce.toLocaleTimeString([],qe)}function Z(ue,Ce){return!Ce||!ue?"0px":Math.max(2,ue/Ce*100)+"%"}function Q(ue){const Ce=ae.value.findIndex(qe=>qe._time&&qe._time.getTime()>=ue.start.getTime()&&qe._time.getTime()=0&&p.value){const qe=p.value.querySelector('[data-log-id="'+ae.value[Ce].id+'"]');qe&&(qe.scrollIntoView({behavior:"smooth",block:"center"}),i.value=!1)}}const ae=B(()=>{let ue=t.value;if(w.value&&(ue=ue.filter(Ce=>!!Ce.tool)),l.value)if(l.value===Yf){const Ce={INFO:0,WARNING:1,ERROR:2,CRITICAL:3};ue=ue.filter(qe=>Ce[qe.level||"INFO"]>=Ce.WARNING)}else ue=ue.filter(Ce=>(Ce.level||"INFO")===l.value);if(x.value){const Ce=Zc.find(qe=>qe.value===x.value);if(Ce&&Ce.seconds){const qe=new Date(Date.now()-Ce.seconds*1e3);ue=ue.filter(Ge=>Ge._time&&Ge._time>=qe)}}if(o.value&&!te.value)if(r.value)try{const Ce=new RegExp(o.value,"i");ue=ue.filter(qe=>{const Ge=qe.searchText,It=qe.tool||"";return Ce.test(Ge)||Ce.test(It)})}catch{}else{const Ce=o.value.toLowerCase();ue=ue.filter(qe=>{const Ge=qe.searchText.toLowerCase(),It=(qe.tool||"").toLowerCase();return Ge.includes(Ce)||It.includes(Ce)})}return ue}),V=B(()=>EC(ae.value));function be(ue){const Ce=Bd(ue,++s);if(n.value){E.value.push(Ce);return}Oe(Ce)}function Oe(ue){TC(t.value,ue,y),i.value&&Ut(()=>le())}function le(ue=!1){const Ce=p.value;Ce&&Ce.scrollTo({top:Ce.scrollHeight,behavior:ue?"smooth":"instant"})}function oe(){i.value=!0,m.value=!1,Ut(()=>le(!0))}const ve=new Set(["PageUp","PageDown","ArrowUp","ArrowDown","Home","End"," "]);function j(){const ue=p.value;if(!ue)return;const Ce=ue.scrollHeight-ue.scrollTop-ue.clientHeight<40;m.value=!i.value&&!Ce&&t.value.length>0,P.value&&Y()}function Y(){const ue=p.value;!ue||!i.value||ue.scrollHeight-ue.scrollTop-ue.clientHeight>=40&&(i.value=!1,m.value=t.value.length>0)}function xe(){i.value&&requestAnimationFrame(Y)}function S(ue){ve.has(ue.key)&&xe()}const P=f(!1);function G(){i.value&&(P.value=!0,requestAnimationFrame(Y))}function he(){P.value&&(P.value=!1,Y())}function fe(){i.value&&(m.value=!1,Ut(()=>le()))}function me(){if(n.value=!n.value,!n.value&&E.value.length>0){for(const ue of E.value)Oe(ue);E.value=[]}}function we(){t.value=[],E.value=[],m.value=!1}function J(){let ue;e.value==="search"?ue=Ye.value.map(It=>{const ot=It.error?"ERROR":"INFO",pn=It.tool_name?`[${It.tool_name}] `:"";return`${It.timestamp||""} ${ot} ${pn}${It.result_summary||It.message||""}`}).join(` +`):ue=ae.value.map(Zf).join(` -`);const Ce=new Blob([re],{type:"text/plain"}),Ve=URL.createObjectURL(Ce),Ge=document.createElement("a");Ge.href=Ve,Ge.download=`odin-logs-${new Date().toISOString().slice(0,19).replace(/:/g,"-")}.txt`,Ge.click(),URL.revokeObjectURL(Ve)}function ne(re){const Ce=Uf(re);navigator.clipboard.writeText(Ce).then(()=>{h.value=re.id,setTimeout(()=>{h.value=null},1500)}).catch(()=>{})}function te(re){l.value=l.value===re?"":re,b.value="all",S.value=!1}function _e(re){return re.level==="ERROR"?"log-line-error":re.level==="WARNING"?"log-line-warning":"text-gray-300"}function B(re){return re==="ERROR"?"text-red-500 font-semibold":re==="WARNING"?"text-yellow-500":"text-blue-500"}function fe(re){return re==="ERROR"?"log-chip-error":re==="WARNING"?"log-chip-warning":"log-chip-info"}function de(re){b.value=re.id;const Ce=re.filters;l.value=Ce.level||"",x.value=Ce.timeRange||"",o.value=Ce.text||"",S.value=!!Ce.hasToolName,Ce.levels&&(l.value=Ce.levels.includes("WARNING")&&Ce.levels.includes("ERROR")?Hf:Ce.levels[0]||"")}function be(re){b.value=re.id,l.value=re.filters.level||"",x.value=re.filters.timeRange||"",o.value=re.filters.text||"",S.value=!!re.filters.hasToolName}function Fe(){if(!L.value.trim())return;const re={id:"custom-"+Date.now(),name:L.value.trim(),filters:{level:l.value,timeRange:x.value,text:o.value,hasToolName:S.value||void 0}};_.value=[..._.value,re],R(),T.value=!1,L.value=""}function qe(re){_.value=_.value.filter(Ce=>Ce.id!==re),R(),b.value===re&&(b.value="all",x.value="",S.value=!1,l.value="",o.value="")}const $e=p("all"),Ke=p(""),at=p(""),Xe=p(""),Y=p(""),Ae=p(""),Re=p(100),Me=dC,ae=p(!1),Ne=p(!1),He=p(""),rt=p([]),Zt=p(null),lt=p(null);function gt(){e.value="search",Zt.value||Qt()}async function Qt(){try{Zt.value=await z.get("/api/logs/stats")}catch{}}function cs(){const re=Ae.value;if(!re){Xe.value="",Y.value="";return}const Ve={last_5m:300,last_15m:900,last_1h:3600,last_4h:14400,last_24h:86400,last_7d:604800}[re];if(Ve){const Ge=new Date(Date.now()-Ve*1e3);Xe.value=oa(Ge),Y.value=""}}function oa(re){const Ce=Ve=>String(Ve).padStart(2,"0");return`${re.getFullYear()}-${Ce(re.getMonth()+1)}-${Ce(re.getDate())}T${Ce(re.getHours())}:${Ce(re.getMinutes())}`}function js(re){if(!re)return"";const Ce=new Date(re);return isNaN(Ce.getTime())?"":Ce.toISOString()}let Ze=0;async function ra(){if(ae.value)return;const re=++Ze;ae.value=!0,He.value="",Ne.value=!0,lt.value=null;try{const Ce=new URLSearchParams;$e.value&&$e.value!=="all"&&Ce.set("level",$e.value),Ke.value&&Ce.set("tool",Ke.value),at.value&&Ce.set("q",at.value);const Ve=js(Xe.value),Ge=js(Y.value);Ve&&Ce.set("start",Ve),Ge&&Ce.set("end",Ge),Ce.set("limit",String(Re.value));const kt=await z.get(`/api/logs/search?${Ce.toString()}`);if(re!==Ze)return;rt.value=kt.entries||[]}catch(Ce){if(re!==Ze)return;He.value=Ce.message||"Search failed",rt.value=[]}finally{re===Ze&&(ae.value=!1)}}function vs(){Ze++,$e.value="all",Ke.value="",at.value="",Xe.value="",Y.value="",Ae.value="",Re.value=100,rt.value=[],Ne.value=!1,He.value="",lt.value=null,ae.value=!1}function Ms(re){lt.value=lt.value===re?null:re}function gs(re){if(!re.timestamp)return"";try{return new Date(re.timestamp).toLocaleString()}catch{return re.timestamp}}function An(re){return re.type==="web_action"?`${re.status||""} (${re.execution_time_ms||0}ms)`:(re.result_summary||"").slice(0,200)}function Yt(re){return re.error?"log-line-error":"text-gray-300"}function _t(re){try{return JSON.stringify(re,null,2)}catch{return String(re)}}let ds=null,ba=!1;function rn(){ba||(ba=!0,ut.subscribe("logs",ye),c.value=ut.connected,d.value=ut.state||"disconnected",ds=ut.onState(re=>{d.value=re,c.value=re==="connected"}))}function ca(){ba&&(ba=!1,ut.unsubscribe("logs",ye),ds&&(ds(),ds=null))}return tt(()=>{P(),window.addEventListener("pointerup",ge),window.addEventListener("pointercancel",ge)}),rs(rn),Kt(ca),xt(()=>{ca(),window.removeEventListener("pointerup",ge),window.removeEventListener("pointercancel",ge)}),{mode:e,logs:t,paused:n,autoScroll:i,levelFilter:l,textFilter:o,useRegex:r,groupByTurn:a,groupedLogs:q,subscribed:c,wsState:d,wsStateLabel:u,logContainer:f,filteredLogs:se,pauseBuffer:E,showJumpBottom:m,copiedIndex:h,regexError:X,levels:A,logPresets:w,timeRanges:y,timeRange:x,toolOnly:S,activeLogPreset:b,customLogPresets:_,showSaveLogPreset:T,newLogPresetName:L,hasActiveLogFilters:I,timeRangeLabel:H,timelineBuckets:N,timelineMax:O,timelineSpanLabel:$,timelineLabelSkip:ce,togglePause:he,clearLogs:we,exportLogs:K,logLineClass:_e,levelClass:B,levelChipClass:fe,toggleLevel:te,copyLine:ne,jumpToBottom:oe,onScroll:j,onUserScrollIntent:xe,onUserScrollKey:k,onAutoScrollToggle:pe,onPointerDown:W,applyLogPreset:de,applyCustomLogPreset:be,saveLogCustomPreset:Fe,removeLogCustomPreset:qe,segmentHeight:Z,jumpToTimelineBucket:J,searchLevel:$e,searchTool:Ke,searchKeyword:at,searchStart:Xe,searchEnd:Y,searchTimePreset:Ae,searchLimit:Re,searchLimits:Me,searching:ae,searchRan:Ne,searchError:He,searchResults:rt,searchStats:Zt,expandedSearch:lt,switchToSearch:gt,runSearch:ra,clearSearchFilters:vs,toggleSearchExpand:Ms,formatSearchTs:gs,searchEntryText:An,searchLogLineClass:Yt,formatJson:_t,applySearchTimePreset:cs}}};function $o(e=[]){const t=[],s=new Set;function a(n){const i=[n.kind,n.label,n.apply_mode||"",n.code||"",n.text||""].join("\0");s.has(i)||(s.add(i),t.push({...n,key:i}))}for(const n of e)for(const i of(n==null?void 0:n.consumers)||[])a({kind:"consumer",label:i.name,apply_mode:i.apply_mode,text:i.detail});for(const n of e)n!=null&&n.apply_handler&&a({kind:"handler",label:"Apply handler",code:n.apply_handler});for(const n of e)n!=null&&n.restart_reason&&a({kind:"restart",label:"Why a restart is required",text:n.restart_reason});for(const n of e)n!=null&&n.activation_policy&&a({kind:"activation",label:"Activation policy",text:n.activation_policy});return t}const pC=Object.freeze([{key:"all",label:"All fields",short:"All",icon:"grid"},{key:"applied",label:"Applied",short:"Applied",icon:"success"},{key:"pending_restart",label:"Pending restart",short:"Restart",icon:"refresh"},{key:"dormant",label:"Saved, not active",short:"Saved only",icon:"pause"},{key:"invalid",label:"Invalid",short:"Invalid",icon:"error"},{key:"drift",label:"Drift",short:"Drift",icon:"warning"},{key:"unknown",label:"Effective state unknown",short:"Unknown",icon:"info"}]);function fC(e,t={}){var n,i;const s=t.getStyle||(l=>globalThis.getComputedStyle(l)),a=Object.hasOwn(t,"fallback")?t.fallback:(n=globalThis.document)==null?void 0:n.scrollingElement;for(let l=e;l;l=l.parentElement){const o=((i=s(l))==null?void 0:i.overflowY)||"";if(/^(auto|scroll|overlay)$/.test(o)&&l.scrollHeight>l.clientHeight)return l}return a&&a.scrollHeight>a.clientHeight?a:e||a||null}const Fi=[{key:"core",label:"Core",icon:"sliders",sections:["timezone","logging","permissions","graceful_degradation"]},{key:"models",label:"Models & AI",icon:"brain",sections:["image","llm_recovery"]},{key:"runtime",label:"Runtime",icon:"activity",sections:["context","sessions","agents","turn_state"]},{key:"data",label:"Data & Storage",icon:"database",sections:["learning","search","usage","audit","attachments"]},{key:"services",label:"Services",icon:"link",sections:["webhook","observability","email","browser","mcp"]},{key:"automation",label:"Automation",icon:"workflow",sections:["grafana_alerts","outbound_webhooks"]},{key:"infrastructure",label:"Infrastructure",icon:"server",sections:["tools","web"]}],mC={live_read:"Applies immediately",live_apply:"Dedicated live apply",live_for_new_work:"Applies to new work",restart:"Restart required",activation_required:"Saved only — see activation note",legacy_control:"Controlled elsewhere",dormant:"Saved for future support"},Fo=new Set(["llm_provider","openai_codex","ollama","openai_compatible","kimi","personality","discord","computer"]),hC=Object.freeze(["web.api_tokens","outbound_webhooks.targets"]);function jf(e){return hC.some(t=>e===t||e.startsWith(`${t}.`))}const Lg="odin_config_center_expanded_v1",Ng="odin_config_center_category_v1",vC=50,gC=650,hl=()=>z.get("/api/config/meta");function zn(e){return e===void 0?void 0:JSON.parse(JSON.stringify(e))}function Ai(e,t){return JSON.stringify(e)===JSON.stringify(t)}function yi(e){return String(e).replace(/[_-]+/g," ").replace(/\b\w/g,t=>t.toUpperCase())}function bC(e){return e===void 0?"unset":e===null?"null":typeof e=="boolean"?e?"Enabled":"Disabled":Array.isArray(e)?e.length?`${e.length} item${e.length===1?"":"s"}`:"Empty list":typeof e=="object"?Object.keys(e).length?`${Object.keys(e).length} field${Object.keys(e).length===1?"":"s"}`:"Empty object":e===""?"Empty":String(e)}function yC(e){if(e===void 0)return"unset";if(e===null)return"null";if(typeof e=="object")try{return JSON.stringify(e,null,2)}catch{return String(e)}return String(e)}function Mg(e,t){if(Ai(e,t))return;if(!(e&&t&&typeof e=="object"&&typeof t=="object"&&!Array.isArray(e)&&!Array.isArray(t)))return zn(t);const a={};for(const[n,i]of Object.entries(t)){const l=Mg(e[n],i);l!==void 0&&(a[n]=l)}return Object.keys(a).length?a:void 0}function xC(e,t){const s={};for(const[a,n]of Object.entries(t||{})){const i=Mg(e==null?void 0:e[a],n);i!==void 0&&(s[a]=i)}return s}function Dg(e,t,s,a){if(Ai(e,t))return;if(e&&t&&typeof e=="object"&&typeof t=="object"&&!Array.isArray(e)&&!Array.isArray(t)){const i=new Set([...Object.keys(e),...Object.keys(t)]);for(const l of i)Dg(e[l],t[l],s?`${s}.${l}`:l,a);return}a.push({path:s,oldVal:e,newVal:t})}function _C(){try{const e=JSON.parse(localStorage.getItem(Lg)||"{}");return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}function wC(){try{const e=localStorage.getItem(Ng);return Fi.some(t=>t.key===e)?e:Fi[0].key}catch{return Fi[0].key}}const kC={template:` +`);const Ce=new Blob([ue],{type:"text/plain"}),qe=URL.createObjectURL(Ce),Ge=document.createElement("a");Ge.href=qe,Ge.download=`odin-logs-${new Date().toISOString().slice(0,19).replace(/:/g,"-")}.txt`,Ge.click(),URL.revokeObjectURL(qe)}function ie(ue){const Ce=Zf(ue);navigator.clipboard.writeText(Ce).then(()=>{h.value=ue.id,setTimeout(()=>{h.value=null},1500)}).catch(()=>{})}function X(ue){l.value=l.value===ue?"":ue,b.value="all",w.value=!1}function _e(ue){return ue.level==="ERROR"?"log-line-error":ue.level==="WARNING"?"log-line-warning":"text-gray-300"}function U(ue){return ue==="ERROR"?"text-red-500 font-semibold":ue==="WARNING"?"text-yellow-500":"text-blue-500"}function de(ue){return ue==="ERROR"?"log-chip-error":ue==="WARNING"?"log-chip-warning":"log-chip-info"}function ge(ue){b.value=ue.id;const Ce=ue.filters;l.value=Ce.level||"",x.value=Ce.timeRange||"",o.value=Ce.text||"",w.value=!!Ce.hasToolName,Ce.levels&&(l.value=Ce.levels.includes("WARNING")&&Ce.levels.includes("ERROR")?Yf:Ce.levels[0]||"")}function Ae(ue){b.value=ue.id,l.value=ue.filters.level||"",x.value=ue.filters.timeRange||"",o.value=ue.filters.text||"",w.value=!!ue.filters.hasToolName}function $e(){if(!L.value.trim())return;const ue={id:"custom-"+Date.now(),name:L.value.trim(),filters:{level:l.value,timeRange:x.value,text:o.value,hasToolName:w.value||void 0}};_.value=[..._.value,ue],O(),T.value=!1,L.value=""}function We(ue){_.value=_.value.filter(Ce=>Ce.id!==ue),O(),b.value===ue&&(b.value="all",x.value="",w.value=!1,l.value="",o.value="")}const je=f("all"),Xe=f(""),it=f(""),Qe=f(""),se=f(""),Re=f(""),Le=f(100),Te=OC,W=f(!1),De=f(!1),ze=f(""),Ye=f([]),Dt=f(null),et=f(null);function Rt(){e.value="search",Dt.value||Pt()}async function Pt(){try{Dt.value=await z.get("/api/logs/stats")}catch{}}function Ts(){const ue=Re.value;if(!ue){Qe.value="",se.value="";return}const qe={last_5m:300,last_15m:900,last_1h:3600,last_4h:14400,last_24h:86400,last_7d:604800}[ue];if(qe){const Ge=new Date(Date.now()-qe*1e3);Qe.value=Vs(Ge),se.value=""}}function Vs(ue){const Ce=qe=>String(qe).padStart(2,"0");return`${ue.getFullYear()}-${Ce(ue.getMonth()+1)}-${Ce(ue.getDate())}T${Ce(ue.getHours())}:${Ce(ue.getMinutes())}`}function Gs(ue){if(!ue)return"";const Ce=new Date(ue);return isNaN(Ce.getTime())?"":Ce.toISOString()}let zt=0;async function ta(){if(W.value)return;const ue=++zt;W.value=!0,ze.value="",De.value=!0,et.value=null;try{const Ce=new URLSearchParams;je.value&&je.value!=="all"&&Ce.set("level",je.value),Xe.value&&Ce.set("tool",Xe.value),it.value&&Ce.set("q",it.value);const qe=Gs(Qe.value),Ge=Gs(se.value);qe&&Ce.set("start",qe),Ge&&Ce.set("end",Ge),Ce.set("limit",String(Le.value));const It=await z.get(`/api/logs/search?${Ce.toString()}`);if(ue!==zt)return;Ye.value=It.entries||[]}catch(Ce){if(ue!==zt)return;ze.value=Ce.message||"Search failed",Ye.value=[]}finally{ue===zt&&(W.value=!1)}}function Ws(){zt++,je.value="all",Xe.value="",it.value="",Qe.value="",se.value="",Re.value="",Le.value=100,Ye.value=[],De.value=!1,ze.value="",et.value=null,W.value=!1}function Pa(ue){et.value=et.value===ue?null:ue}function hs(ue){if(!ue.timestamp)return"";try{return new Date(ue.timestamp).toLocaleString()}catch{return ue.timestamp}}function $a(ue){return ue.type==="web_action"?`${ue.status||""} (${ue.execution_time_ms||0}ms)`:(ue.result_summary||"").slice(0,200)}function Es(ue){return ue.error?"log-line-error":"text-gray-300"}function Ct(ue){try{return JSON.stringify(ue,null,2)}catch{return String(ue)}}let ls=null,sa=!1;function ba(){sa||(sa=!0,mt.subscribe("logs",be),c.value=mt.connected,d.value=mt.state||"disconnected",ls=mt.onState(ue=>{d.value=ue,c.value=ue==="connected"}))}function Fe(){sa&&(sa=!1,mt.unsubscribe("logs",be),ls&&(ls(),ls=null))}return st(()=>{F(),window.addEventListener("pointerup",he),window.addEventListener("pointercancel",he)}),cs(ba),es(Fe),wt(()=>{Fe(),window.removeEventListener("pointerup",he),window.removeEventListener("pointercancel",he)}),{mode:e,logs:t,paused:n,autoScroll:i,levelFilter:l,textFilter:o,useRegex:r,groupByTurn:a,groupedLogs:V,subscribed:c,wsState:d,wsStateLabel:u,logContainer:p,filteredLogs:ae,pauseBuffer:E,showJumpBottom:m,copiedIndex:h,regexError:te,levels:k,logPresets:A,timeRanges:g,timeRange:x,toolOnly:w,activeLogPreset:b,customLogPresets:_,showSaveLogPreset:T,newLogPresetName:L,hasActiveLogFilters:I,timeRangeLabel:H,timelineBuckets:N,timelineMax:M,timelineSpanLabel:$,timelineLabelSkip:re,togglePause:me,clearLogs:we,exportLogs:J,logLineClass:_e,levelClass:U,levelChipClass:de,toggleLevel:X,copyLine:ie,jumpToBottom:oe,onScroll:j,onUserScrollIntent:xe,onUserScrollKey:S,onAutoScrollToggle:fe,onPointerDown:G,applyLogPreset:ge,applyCustomLogPreset:Ae,saveLogCustomPreset:$e,removeLogCustomPreset:We,segmentHeight:Z,jumpToTimelineBucket:Q,searchLevel:je,searchTool:Xe,searchKeyword:it,searchStart:Qe,searchEnd:se,searchTimePreset:Re,searchLimit:Le,searchLimits:Te,searching:W,searchRan:De,searchError:ze,searchResults:Ye,searchStats:Dt,expandedSearch:et,switchToSearch:Rt,runSearch:ta,clearSearchFilters:Ws,toggleSearchExpand:Pa,formatSearchTs:hs,searchEntryText:$a,searchLogLineClass:Es,formatJson:Ct,applySearchTimePreset:Ts}}};function Go(e=[]){const t=[],s=new Set;function a(n){const i=[n.kind,n.label,n.apply_mode||"",n.code||"",n.text||""].join("\0");s.has(i)||(s.add(i),t.push({...n,key:i}))}for(const n of e)for(const i of(n==null?void 0:n.consumers)||[])a({kind:"consumer",label:i.name,apply_mode:i.apply_mode,text:i.detail});for(const n of e)n!=null&&n.apply_handler&&a({kind:"handler",label:"Apply handler",code:n.apply_handler});for(const n of e)n!=null&&n.restart_reason&&a({kind:"restart",label:"Why a restart is required",text:n.restart_reason});for(const n of e)n!=null&&n.activation_policy&&a({kind:"activation",label:"Activation policy",text:n.activation_policy});return t}const NC=Object.freeze([{key:"all",label:"All fields",short:"All",icon:"grid"},{key:"applied",label:"Applied",short:"Applied",icon:"success"},{key:"pending_restart",label:"Pending restart",short:"Restart",icon:"refresh"},{key:"dormant",label:"Saved, not active",short:"Saved only",icon:"pause"},{key:"invalid",label:"Invalid",short:"Invalid",icon:"error"},{key:"drift",label:"Drift",short:"Drift",icon:"warning"},{key:"unknown",label:"Effective state unknown",short:"Unknown",icon:"info"}]);function MC(e,t={}){var n,i;const s=t.getStyle||(l=>globalThis.getComputedStyle(l)),a=Object.hasOwn(t,"fallback")?t.fallback:(n=globalThis.document)==null?void 0:n.scrollingElement;for(let l=e;l;l=l.parentElement){const o=((i=s(l))==null?void 0:i.overflowY)||"";if(/^(auto|scroll|overlay)$/.test(o)&&l.scrollHeight>l.clientHeight)return l}return a&&a.scrollHeight>a.clientHeight?a:e||a||null}const zi=[{key:"core",label:"Core",icon:"sliders",sections:["timezone","logging","permissions","graceful_degradation"]},{key:"models",label:"Models & AI",icon:"brain",sections:["image","llm_recovery"]},{key:"runtime",label:"Runtime",icon:"activity",sections:["context","sessions","agents","turn_state"]},{key:"data",label:"Data & Storage",icon:"database",sections:["learning","search","usage","audit","attachments"]},{key:"services",label:"Services",icon:"link",sections:["webhook","observability","email","browser","mcp"]},{key:"automation",label:"Automation",icon:"workflow",sections:["grafana_alerts","outbound_webhooks"]},{key:"infrastructure",label:"Infrastructure",icon:"server",sections:["tools","web"]}],DC={live_read:"Applies immediately",live_apply:"Dedicated live apply",live_for_new_work:"Applies to new work",restart:"Restart required",activation_required:"Saved only — see activation note",legacy_control:"Controlled elsewhere",dormant:"Saved for future support"},Wo=new Set(["llm_provider","openai_codex","ollama","openai_compatible","kimi","personality","discord","computer"]),PC=Object.freeze(["web.api_tokens","outbound_webhooks.targets"]);function Xf(e){return PC.some(t=>e===t||e.startsWith(`${t}.`))}const Hg="odin_config_center_expanded_v1",jg="odin_config_center_category_v1",$C=50,FC=650,gl=()=>z.get("/api/config/meta");function Gn(e){return e===void 0?void 0:JSON.parse(JSON.stringify(e))}function Oi(e,t){return JSON.stringify(e)===JSON.stringify(t)}function wi(e){return String(e).replace(/[_-]+/g," ").replace(/\b\w/g,t=>t.toUpperCase())}function BC(e){return e===void 0?"unset":e===null?"null":typeof e=="boolean"?e?"Enabled":"Disabled":Array.isArray(e)?e.length?`${e.length} item${e.length===1?"":"s"}`:"Empty list":typeof e=="object"?Object.keys(e).length?`${Object.keys(e).length} field${Object.keys(e).length===1?"":"s"}`:"Empty object":e===""?"Empty":String(e)}function UC(e){if(e===void 0)return"unset";if(e===null)return"null";if(typeof e=="object")try{return JSON.stringify(e,null,2)}catch{return String(e)}return String(e)}function qg(e,t){if(Oi(e,t))return;if(!(e&&t&&typeof e=="object"&&typeof t=="object"&&!Array.isArray(e)&&!Array.isArray(t)))return Gn(t);const a={};for(const[n,i]of Object.entries(t)){const l=qg(e[n],i);l!==void 0&&(a[n]=l)}return Object.keys(a).length?a:void 0}function zC(e,t){const s={};for(const[a,n]of Object.entries(t||{})){const i=qg(e==null?void 0:e[a],n);i!==void 0&&(s[a]=i)}return s}function Vg(e,t,s,a){if(Oi(e,t))return;if(e&&t&&typeof e=="object"&&typeof t=="object"&&!Array.isArray(e)&&!Array.isArray(t)){const i=new Set([...Object.keys(e),...Object.keys(t)]);for(const l of i)Vg(e[l],t[l],s?`${s}.${l}`:l,a);return}a.push({path:s,oldVal:e,newVal:t})}function HC(){try{const e=JSON.parse(localStorage.getItem(Hg)||"{}");return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}function jC(){try{const e=localStorage.getItem(jg);return zi.some(t=>t.key===e)?e:zi[0].key}catch{return zi[0].key}}const qC={template:`
@@ -4827,7 +4835,7 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p(null),t=p(null),s=p(!0),a=p(null),n=p(!1),i=p(null),l=p(!1),o=p(""),r=p(!1),c=p(""),d=p(""),u=p("");function f(C){i.value=C||null,C&&typeof C.authorized=="boolean"&&(l.value=C.authorized)}async function m(){try{const C=await z.get("/api/setup/status");f(C.listener),u.value=""}catch(C){f(null),u.value=`Listener status could not be loaded: ${C.message||"Unknown error"}`}}async function h(){if(!(!ge.value||!o.value.trim()||r.value||W.value)){r.value=!0,c.value="",d.value="";try{const C=z.setListenerExposure(o.value.trim(),l.value);o.value="";const G=await C;c.value=G.message,f(G.listener)}catch(C){d.value=C.message||"Listener consent could not be saved."}finally{o.value="",r.value=!1}}}const g=p(null),A=["image_model","outer_model"],w=p(null),y=p(null),b=p(null),x=p(!1),S=p(!1),_=p(null),T=p(""),L=p("all"),E=p(wC()),P=p(_C()),R=p({}),I=p({}),H=p(""),X=p({}),Q=p({}),N=p([]),O=p([]),$=p(!1),ce=p(!1),ue=p(!1);let Z=null,J=null,se={path:null,at:0},q=0;const ye=U(()=>{var C;return(((C=t.value)==null?void 0:C.fields)||[]).filter(G=>!Fo.has(G.path.split(".")[0])&&!jf(G.path))}),Oe=U(()=>new Map(ye.value.map(C=>[C.path,C]))),le=U(()=>xe.value.reduce((C,G)=>C+G.sections.length,0)),oe=U(()=>ye.value.length),ve=U(()=>pC),j=U(()=>N.value.length>0),V=U(()=>O.value.length>0),xe=U(()=>{if(!e.value)return[];const C=new Set(Fi.flatMap(Te=>Te.sections)),G=Fi.map(Te=>({...Te,sections:Te.sections.filter(Je=>Object.hasOwn(e.value,Je)&&!Fo.has(Je))})).filter(Te=>Te.sections.length),ie=Object.keys(e.value).filter(Te=>!C.has(Te)&&!Fo.has(Te));return ie.length&&G.push({key:"other",label:"Other",icon:"folder",sections:ie}),G}),k=U(()=>e.value?{...e.value,...R.value}:null),D=U(()=>{if(!e.value)return[];const C=[];for(const[G,ie]of Object.entries(R.value))Dg(e.value[G],ie,G,C);return C.filter(G=>!Ai(G.oldVal,G.newVal)).map(G=>{const ie=at(G.path);return{...G,label:(ie==null?void 0:ie.label)||yi(G.path.split(".").at(-1)),apply_mode:(ie==null?void 0:ie.apply_mode)||Ne(G.path.split(".")[0])}})}),W=U(()=>D.value.length>0),ge=U(()=>!!i.value&&l.value!==i.value.authorized),pe=U(()=>{var G;const C=(G=i.value)==null?void 0:G.state;return C==="active"||C==="authorized_loopback"?"active":["pending_widening","pending_narrowing","active_rebind_pending"].includes(C)?"pending":C==="restricted"?"restricted":"unknown"}),he=U(()=>{var C;return{active:"Exposure active",authorized_loopback:"Authorized · loopback host",pending_widening:"Authorized · restart pending",pending_narrowing:"Restriction saved · restart pending",active_rebind_pending:"Exposed · restart pending",restricted:"Loopback only",unknown:"Runtime state unavailable"}[(C=i.value)==null?void 0:C.state]||"Loading listener state"}),we=U(()=>i.value?i.value.authorized?i.value.authorization_source==="explicit"?"Beyond-loopback access is explicitly authorized":"Beyond-loopback access is retained from this installation":"Beyond-loopback access is not authorized":"Unavailable"),K=U(()=>{var C;return{explicit:"saved explicitly in config.yml",default:"schema default; no web.host key is saved",unknown:"source could not be verified"}[(C=i.value)==null?void 0:C.configured_host_source]||"source unavailable"}),ne=U(()=>{const C=i.value;return!C||C.running_scope==="unavailable"?"Actual bound address unavailable":`${(C.listening_hosts||[]).map((ie,Te)=>{var Ct;const Je=(Ct=C.listening_ports)==null?void 0:Ct[Te];return Je?`${ie}:${Je}`:ie}).join(", ")} · ${C.running_scope==="loopback"?"loopback only":"accepting beyond loopback"}`}),te=U(()=>D.value.length),_e=U(()=>new Set(D.value.map(C=>C.path.split(".")[0])).size),B=U(()=>!!T.value||L.value!=="all"),fe=U(()=>{const C={...Q.value};for(const G of D.value){const ie=at(G.path),Te=me(ie,G.newVal);Te&&(C[G.path]=Te)}return C}),de=U(()=>Object.keys(fe.value).length>0),be=U(()=>e.value?(B.value?xe.value:xe.value.filter(G=>G.key===E.value)).map(G=>({...G,sections:G.sections.filter(ie=>Yt(ie))})).filter(G=>G.sections.length):[]),Fe=U(()=>{const C=["live_read","live_apply","live_for_new_work","restart","activation_required","legacy_control","dormant"],G=new Map(C.map(ie=>[ie,[]]));for(const ie of D.value){const Te=G.has(ie.apply_mode)?ie.apply_mode:"restart";G.get(Te).push(ie)}return C.filter(ie=>G.get(ie).length).map(ie=>({key:ie,label:bs(ie),entries:G.get(ie)}))}),qe=U(()=>D.value.filter(C=>C.apply_mode==="restart").length),$e=U(()=>ye.value.filter(C=>C.pending_restart)),Ke=U(()=>$e.value.length);function at(C){const G=Oe.value.get(C);return G?{...G,apply_details:$o([G])}:null}function Xe(C){const G=`${C}.`;return ye.value.filter(ie=>ie.path===C||ie.path.startsWith(G))}function Y(){return ye.value.some(C=>C.path==="tools.hosts"||C.path.startsWith("tools.hosts."))}function Ae(){var ie,Te;const C=((Te=(ie=e.value)==null?void 0:ie.tools)==null?void 0:Te.hosts)||{},G=Object.keys(C).length;return`${G} host${G===1?"":"s"} configured.`}function Re(C){return Xe(C).length}function Me(C){return yi(C)}function ae(C){const G=Xe(C);if(!G.length)return`${yi(C)} configuration.`;const ie=G.find(Ct=>Ct.sensitivity==="public"&&Ct.description)||G.find(Ct=>Ct.description),Te=(ie==null?void 0:ie.description)||"";return Te.match(/setting for (.+)\.$/i)?`${yi(C)} settings and runtime behaviour.`:Te}function Ne(C){const G=[...new Set(Xe(C).map(ie=>ie.apply_mode))];return G.length===1?G[0]:G.includes("restart")?"restart":G.includes("activation_required")?"activation_required":G[0]||"restart"}function He(C){const G=[...new Set(Xe(C).map(ie=>bs(ie.apply_mode)))];return G.length?G.length===1?G[0]:`Mixed apply behaviour: ${G.join(" · ")}`:""}function rt(C){return $o(Xe(C))}function Zt(C){var G;return Object.hasOwn(R.value,C)?R.value[C]:(G=e.value)==null?void 0:G[C]}function lt(){const C=Zt("mcp")||{},G=Object.keys(C.servers||{}).length;return`${C.enabled?"Globally enabled":"Globally disabled"} · ${G} configured server${G===1?"":"s"}.`}function gt(C,G){return G.split(".").reduce((ie,Te)=>ie==null?void 0:ie[Te],C)}function Qt(C){const G=k.value;return Xe(C).filter(ie=>jf(ie.path)?!1:ie.path.split(".").length<=2?!0:!ie.path.includes(".*")).map(ie=>({...ie,key:ie.path.split(".").at(-1),value:gt(G,ie.path),apply_details:$o([ie]),editor:ie.path==="agents.final_warning_iterations"?"warning-chips":null}))}function cs(C){const G=C.path.split(".");return G.length>2?G.slice(0,2).join("."):null}function oa(C){const G=new Map;for(const ie of Qt(C)){const Te=cs(ie),Je=Te||`${C}.__root`;G.has(Je)||G.set(Je,{key:Je,path:Te,entries:[]}),G.get(Je).entries.push(ie)}return[...G.values()].map(ie=>{const Te=ie.entries.find(Je=>Je.group_description);return{...ie,label:ie.path?yi(ie.path.split(".").at(-1)):null,description:(Te==null?void 0:Te.group_description)||null,apply_details:$o(ie.entries),runtime_summaries:Ze(ie.entries)}})}function js(C){return{save:C.save_effect||(C.apply_mode==="dormant"?"Saving records this value in config.yml.":"Saving records this value and validates the section."),runtime:C.runtime_effect||{live_read:"Odin reads the saved value during current work.",live_apply:"Odin reloads this setting without a restart.",live_for_new_work:"New work uses the saved value; existing work keeps its snapshot.",restart:"Odin keeps using its startup value until a clean restart.",activation_required:"Odin keeps the current behavior until you enable this feature separately.",legacy_control:"Odin keeps the existing compatibility behavior until you apply this choice.",dormant:"This version of Odin does not use the saved value. Restarting will not activate it."}[C.apply_mode]||"Effective runtime state is not currently observable."}}function Ze(C){const G=new Map;for(const ie of C){const Te=js(ie),Je=`${ie.apply_mode}|${Te.save}|${Te.runtime}`;G.has(Je)||G.set(Je,{key:Je,label:bs(ie.apply_mode),save:Te.save,runtime:Te.runtime})}return[...G.values()]}function ra(C){if(vs(C))return C.runtime_effect||C.activation_policy||"";if(C.apply_mode==="activation_required"){const G=C.activation_policy||C.runtime_effect;return G?`Not active after saving. No activation control exists in this release. ${G}`:"Not active after saving; no activation control exists in this release."}return""}function vs(C){return C.action_available===!0&&!!(C.action_label&&C.action_endpoint)}async function Ms(C){if(vs(C))try{if(ca(C.path))throw new Error("Save this setting before applying its action.");const G=String(C.action_method||"POST").toLowerCase(),ie={post:z.post.bind(z),put:z.put.bind(z),delete:z.del.bind(z)}[G];if(!ie)throw new Error("Unsupported configuration action");await ie(C.action_endpoint,C.action_body||void 0),await el(),Ma("success",`${C.action_label} completed.`)}catch(G){Ma("error",G.message||`${C.action_label} failed`)}}function gs(C,G){return[C.label,C.path,C.description,...C.aliases||[]].filter(Boolean).join(" ").toLowerCase().includes(G)}function An(C){const G=T.value.trim().toLowerCase();return G?Xe(C).filter(ie=>gs(ie,G)):[]}function Yt(C){const G=Xe(C);if(L.value!=="all"&&!G.some(Te=>Te.apply_state===L.value))return!1;const ie=T.value.trim().toLowerCase();return!ie||`${Me(C)} ${C}`.toLowerCase().includes(ie)?!0:G.some(Te=>gs(Te,ie))}function _t(C,G){return Xe(C).filter(ie=>ie.apply_state===G).length}function ds(C){return C==="all"?oe.value:ye.value.filter(G=>G.apply_state===C).length}function ba(C){const G=C.sections.flatMap(ie=>Xe(ie));return{fields:G.length,modified:D.value.filter(ie=>C.sections.includes(ie.path.split(".")[0])).length,pending_restart:G.filter(ie=>ie.apply_state==="pending_restart").length,invalid:G.filter(ie=>ie.apply_state==="invalid").length,dormant:G.filter(ie=>ie.apply_state==="dormant").length}}function rn(C){var G;return Object.hasOwn(R.value,C)&&!Ai((G=e.value)==null?void 0:G[C],R.value[C])}function ca(C){return D.value.some(G=>G.path===C||G.path.startsWith(`${C}.`))}function re(C){E.value=C,T.value="",L.value="all";try{localStorage.setItem(Ng,C)}catch{}}function Ce(C){L.value=C}function Ve(){T.value="",L.value="all"}function Ge(C){var G;return((G=xe.value.find(ie=>ie.sections.includes(C)))==null?void 0:G.sections)||[]}function kt(C){const G=Ge(C),ie=G.find(Te=>P.value[Te]===!0);return ie||G.find(Te=>P.value[Te]!==!1)||null}function St(C){return T.value&&!ue.value&&Yt(C)?!0:ue.value?kt(C)===C:Object.hasOwn(P.value,C)?P.value[C]===!0:!0}function cn(C){const G=!St(C);if(ue.value){const ie={...P.value};for(const Te of Ge(C))ie[Te]===!0&&(ie[Te]=!1);ie[C]=G,P.value=ie;return}P.value={...P.value,[C]:G}}function Rn(){N.value.push(zn(R.value)),N.value.length>vC&&N.value.shift(),O.value=[]}function dn(){n.value||W.value&&(Rn(),R.value={},Q.value={},$.value=!1)}function In(C,G=!1){const ie=Date.now();if(G&&se.path===C&&ie-se.atJe-Te);H.value="",qt(C,ie)}function ci(C,G){qt(C,(C.value||[]).filter(ie=>ie!==G))}function ns(C){return C.apply_mode==="live_read"?"Odin reads the saved file value on next use.":C.apply_mode==="live_for_new_work"?"New work uses the saved file value.":C.apply_mode==="live_apply"?C.apply_handler?`Apply the saved value through ${C.apply_handler}.`:"Apply it through its dedicated owner page or endpoint.":C.apply_mode==="restart"?"Restart Odin for the saved collection to take effect.":C.apply_mode==="activation_required"?"Saving does not enable it. No activation control exists in this release.":C.apply_mode==="dormant"?"This release does not use the saved collection.":"Follow the runtime details shown for this setting."}function Ln(C){return C.type==="array"&&Array.isArray(C.value)&&!C.structured_container&&!C.structured_container_child&&C.sensitivity==="public"&&C.value.every(G=>["string","number","boolean"].includes(typeof G))}function Ie(C){const G=String(X.value[C.path]??"").trim();if(!G)return;const ie=[...new Set([...C.value||[],G])];X.value={...X.value,[C.path]:""},qt(C,ie)}function M(C,G){qt(C,(C.value||[]).filter(ie=>ie!==G))}function me(C,G){var Te;if(!C)return null;if((Te=C.enum)!=null&&Te.length&&!C.enum.includes(G))return`Choose one of: ${C.enum.join(", ")}`;if(C.path==="agents.final_warning_iterations"&&(!Array.isArray(G)||!G.length))return"Add at least one warning threshold.";const ie=C.constraints||{};if((C.type==="integer"||C.type==="number")&&typeof G=="number"){if(ie.minimum!==void 0&&Gie.maximum)return`Must be at most ${ie.maximum}${C.unit?` ${C.unit}`:""}`}return null}function Se(C){return fe.value[C.path]||null}function Ue(C){const G=`${C}.`;return Object.keys(fe.value).some(ie=>ie===C||ie.startsWith(G))}function We(){n.value||N.value.length&&(O.value.push(zn(R.value)),R.value=N.value.pop(),Q.value={},I.value={},se={path:null,at:0})}function Ye(){n.value||O.value.length&&(N.value.push(zn(R.value)),R.value=O.value.pop(),Q.value={},I.value={},se={path:null,at:0})}function Et(){!W.value||de.value||($.value=!0,ce.value=!1)}function pt(){$.value=!1}function At(){dn()}function bs(C){return mC[C]||yi(C||"unknown")}function Nt(C){return`apply-${String(C||"unknown").replaceAll("_","-")}`}function Na(C){return`cfgc-field-${C.replace(/[^a-zA-Z0-9_-]/g,"-")}`}function di(C){return`${Na(C)}-input`}function ho(C){const G=document.getElementById(Na(C))||document.getElementById(Na(C.split(".").slice(0,2).join(".")));G==null||G.scrollIntoView({behavior:"smooth",block:"center"})}function Ma(C,G){y.value={type:C,message:G},window.setTimeout(()=>{var ie;((ie=y.value)==null?void 0:ie.message)===G&&(y.value=null)},3500)}function rc(){x.value=!1,L.value="pending_restart",T.value="";const C=fC(a.value);C&&(C.scrollTop=0)}function vo(){x.value=!1}function ui(C=1800){J&&window.clearTimeout(J),J=window.setTimeout(cc,C)}async function cc(){if(S.value){if(q+=1,q>45){S.value=!1,_.value="Odin did not return with the new startup settings within 90 seconds.";return}try{if(t.value=await hl(),Ke.value===0){S.value=!1,_.value=null,Ma("success","Odin restarted and the saved startup settings are active.");return}}catch{}ui(2e3)}}async function Nn(){if(!S.value){_.value=null;try{await z.post("/api/restart",{}),S.value=!0,q=0,x.value=!1,ui()}catch(C){_.value=C.message||"Odin could not schedule a restart."}}}async function dc(){if(!(!W.value||de.value||n.value)){n.value=!0;try{const C=xC(e.value,R.value),G=await z.put("/api/config",C);e.value=G,R.value={},N.value=[],O.value=[],Q.value={},$.value=!1;try{t.value=await hl(),b.value=null,x.value=Ke.value>0,Ma("success",Ke.value?`Configuration saved. ${Ke.value} setting${Ke.value===1?"":"s"} still use startup values.`:"Configuration saved. Apply status has been refreshed.")}catch(ie){b.value=ie.message||"Unknown metadata error.",Ma("error",`Configuration saved, but apply status could not be refreshed: ${b.value}`)}}catch(C){Ma("error",C.message||"Configuration could not be saved")}finally{n.value=!1}}}async function Da(){if(!n.value){n.value=!0,g.value=null;try{t.value=await hl(),b.value=null}catch(C){g.value=`Image model status could not be refreshed: ${C.message||"Unknown error"}`}finally{n.value=!1}}}async function uc(C,G){if(n.value||!["follow","pin"].includes(G)||!C.length||C.some(Te=>{var Je,Ct;return!A.includes(Te)||!((Ct=(Je=t.value)==null?void 0:Je.image_model_defaults)!=null&&Ct[Te])}))return;n.value=!0,g.value=null;let ie=!1;try{const Te=await z.post("/api/config/image-models",{operations:Object.fromEntries(C.map(Je=>[Je,G])),expected_revision:t.value.image_model_revision});ie=!0;for(const Je of C){const Ct=`image.openai.${Je}`,qs=gt(e.value,Ct),Vs=gt(Te.config,Ct),pn=xa=>!Object.hasOwn(xa,"image")||!Ai(gt(xa,Ct),qs)?xa:da(xa,Ct.split("."),Vs);R.value=pn(R.value),N.value=N.value.map(pn),O.value=O.value.map(pn),e.value=da(e.value,Ct.split("."),Vs)}t.value={...t.value,image_model_defaults:Te.image_model_defaults,image_model_revision:Te.image_model_revision},Ma("success","Image model defaults saved. Unrelated drafts were not saved.")}catch(Te){g.value=`Image model operation failed: ${Te.message||"Unknown error"}. Refresh status before retrying if the outcome is uncertain.`}try{t.value=await hl(),b.value=null}catch(Te){const Je=`Image model status could not be refreshed: ${Te.message||"Unknown error"}`;b.value=Je,g.value=ie?`Image model defaults were saved, but ${Je}`:`${g.value} ${Je}`}finally{n.value=!1}}async function el(){var C,G;if(!(W.value||n.value)){s.value=!0,w.value=null;try{const ie=await z.get("/api/config"),Te=await hl();await m(),e.value=ie,t.value=Te,b.value=null;const Je=xe.value;if(Je.some(Ct=>Ct.key===E.value)||(E.value=((C=Je[0])==null?void 0:C.key)||Fi[0].key),ue.value){const qs=(((G=Je.find(Vs=>Vs.key===E.value))==null?void 0:G.sections)||[]).find(Vs=>P.value[Vs]===!0);P.value=qs?{...P.value,[qs]:!0}:{}}}catch(ie){w.value=ie.message||"Unknown configuration error"}finally{s.value=!1}}}function Pa(C){if($.value||!(C.ctrlKey||C.metaKey))return;const G=C.target;G instanceof HTMLElement&&(G.matches("input, textarea, select")||G.isContentEditable)||(!C.shiftKey&&C.key.toLowerCase()==="z"?(C.preventDefault(),We()):(C.key.toLowerCase()==="y"||C.shiftKey&&C.key.toLowerCase()==="z")&&(C.preventDefault(),Ye()))}function tl(C){ue.value=C.matches}jt(P,C=>{try{localStorage.setItem(Lg,JSON.stringify(C))}catch{}},{deep:!0});let pi=!1;function sl(){pi||(pi=!0,document.addEventListener("keydown",Pa))}function al(){pi&&(pi=!1,document.removeEventListener("keydown",Pa))}return tt(()=>{var C;el(),sl(),Z=window.matchMedia("(max-width: 760px)"),tl(Z),(C=Z.addEventListener)==null||C.call(Z,"change",tl)}),rs(sl),Kt(al),Kt(()=>{o.value=""}),xt(()=>{var C;o.value="",al(),(C=Z==null?void 0:Z.removeEventListener)==null||C.call(Z,"change",tl),J&&window.clearTimeout(J)}),{listenerState:i,listenerConsent:l,listenerCredential:o,listenerSaving:r,listenerMessage:c,listenerError:d,listenerStatusError:u,listenerChoiceChanged:ge,listenerStatusTone:pe,listenerStatusLabel:he,listenerAuthorizationCopy:we,listenerConfiguredSourceCopy:K,listenerRunningCopy:ne,saveListenerConsent:h,armKeydown:sl,disarmKeydown:al,handleKeydown:Pa,config:e,meta:t,loading:s,saving:n,error:w,toast:y,metaRefreshError:b,restartPromptOpen:x,restartScheduled:S,restartError:_,configMain:a,imageModelError:g,imageModelLeaves:A,setImageModelDefaults:uc,refreshImageModelMetadata:Da,searchQuery:T,healthFilter:L,activeCategory:E,reviewOpen:$,mobileOverflowOpen:ce,warningThresholdInput:H,arrayInputs:X,healthFilters:ve,visibleCategories:xe,displayGroups:be,reviewGroups:Fe,sectionCount:le,fieldCount:oe,hasChanges:W,changeCount:te,changedSectionCount:_e,hasDraftErrors:de,canUndo:j,canRedo:V,globalFilterActive:B,reviewRestartCount:qe,pendingRestartCount:Ke,pendingRestartFields:$e,healthCount:ds,categoryStats:ba,selectCategory:re,selectHealthFilter:Ce,clearFilters:Ve,sectionLabel:Me,sectionDescription:ae,sectionFieldCount:Re,sectionHealthCount:_t,sectionApplySummary:He,sectionApplyDetails:rt,sectionEntries:Qt,fieldGroups:oa,sectionSearchHits:An,mcpConfigSummary:lt,fieldRuntimeCopy:js,fieldSpecificRuntimeNote:ra,hasHonestAction:vs,runFieldAction:Ms,hasHostsCollection:Y,hostsConfigSummary:Ae,sectionChanged:rn,fieldChanged:ca,isSectionExpanded:St,toggleSection:cn,discardAllDrafts:dn,setFieldValue:qt,setNumberFieldValue:oi,numberInputValue:On,beginInputEdit:ua,endTextInputEdit:as,endInputEdit:un,addWarningThreshold:ri,removeWarningThreshold:ci,isScalarArray:Ln,addScalarArrayItem:Ie,removeScalarArrayItem:M,fieldError:Se,sectionHasErrors:Ue,undo:We,redo:Ye,openReview:Et,closeReview:pt,mobileCancel:At,applyModeLabel:bs,applyClass:Nt,compactValue:bC,formatValue:yC,structuredApplyCopy:ns,fieldId:Na,fieldInputId:di,focusField:ho,fetchConfig:el,saveConfig:dc,restartOdin:Nn,restartLater:vo,reviewPendingRestart:rc}}},SC=/^\d{15,25}$/;function Pg(e){return String((e==null?void 0:e.display_name)||(e==null?void 0:e.username)||(e==null?void 0:e.id)||"Unknown user")}const $g={props:{members:{type:Array,default:()=>[]},excludedIds:{type:Array,default:()=>[]},placeholder:{type:String,default:"Search Discord users…"},ariaLabel:{type:String,default:"Search Discord users"},optionsId:{type:String,required:!0},autofocus:{type:Boolean,default:!1},showAddButton:{type:Boolean,default:!1}},emits:["select"],template:` + `,setup(){const e=f(null),t=f(null),s=f(!0),a=f(null),n=f(!1),i=f(null),l=f(!1),o=f(""),r=f(!1),c=f(""),d=f(""),u=f("");function p(C){i.value=C||null,C&&typeof C.authorized=="boolean"&&(l.value=C.authorized)}async function m(){try{const C=await z.get("/api/setup/status");p(C.listener),u.value=""}catch(C){p(null),u.value=`Listener status could not be loaded: ${C.message||"Unknown error"}`}}async function h(){if(!(!he.value||!o.value.trim()||r.value||G.value)){r.value=!0,c.value="",d.value="";try{const C=z.setListenerExposure(o.value.trim(),l.value);o.value="";const K=await C;c.value=K.message,p(K.listener)}catch(C){d.value=C.message||"Listener consent could not be saved."}finally{o.value="",r.value=!1}}}const y=f(null),k=["image_model","outer_model"],A=f(null),g=f(null),b=f(null),x=f(!1),w=f(!1),_=f(null),T=f(""),L=f("all"),E=f(jC()),F=f(HC()),O=f({}),I=f({}),H=f(""),te=f({}),ee=f({}),N=f([]),M=f([]),$=f(!1),re=f(!1),ce=f(!1);let Z=null,Q=null,ae={path:null,at:0},V=0;const be=B(()=>{var C;return(((C=t.value)==null?void 0:C.fields)||[]).filter(K=>!Wo.has(K.path.split(".")[0])&&!Xf(K.path))}),Oe=B(()=>new Map(be.value.map(C=>[C.path,C]))),le=B(()=>xe.value.reduce((C,K)=>C+K.sections.length,0)),oe=B(()=>be.value.length),ve=B(()=>NC),j=B(()=>N.value.length>0),Y=B(()=>M.value.length>0),xe=B(()=>{if(!e.value)return[];const C=new Set(zi.flatMap(Ee=>Ee.sections)),K=zi.map(Ee=>({...Ee,sections:Ee.sections.filter(Je=>Object.hasOwn(e.value,Je)&&!Wo.has(Je))})).filter(Ee=>Ee.sections.length),ne=Object.keys(e.value).filter(Ee=>!C.has(Ee)&&!Wo.has(Ee));return ne.length&&K.push({key:"other",label:"Other",icon:"folder",sections:ne}),K}),S=B(()=>e.value?{...e.value,...O.value}:null),P=B(()=>{if(!e.value)return[];const C=[];for(const[K,ne]of Object.entries(O.value))Vg(e.value[K],ne,K,C);return C.filter(K=>!Oi(K.oldVal,K.newVal)).map(K=>{const ne=it(K.path);return{...K,label:(ne==null?void 0:ne.label)||wi(K.path.split(".").at(-1)),apply_mode:(ne==null?void 0:ne.apply_mode)||De(K.path.split(".")[0])}})}),G=B(()=>P.value.length>0),he=B(()=>!!i.value&&l.value!==i.value.authorized),fe=B(()=>{var K;const C=(K=i.value)==null?void 0:K.state;return C==="active"||C==="authorized_loopback"?"active":["pending_widening","pending_narrowing","active_rebind_pending"].includes(C)?"pending":C==="restricted"?"restricted":"unknown"}),me=B(()=>{var C;return{active:"Exposure active",authorized_loopback:"Authorized · loopback host",pending_widening:"Authorized · restart pending",pending_narrowing:"Restriction saved · restart pending",active_rebind_pending:"Exposed · restart pending",restricted:"Loopback only",unknown:"Runtime state unavailable"}[(C=i.value)==null?void 0:C.state]||"Loading listener state"}),we=B(()=>i.value?i.value.authorized?i.value.authorization_source==="explicit"?"Beyond-loopback access is explicitly authorized":"Beyond-loopback access is retained from this installation":"Beyond-loopback access is not authorized":"Unavailable"),J=B(()=>{var C;return{explicit:"saved explicitly in config.yml",default:"schema default; no web.host key is saved",unknown:"source could not be verified"}[(C=i.value)==null?void 0:C.configured_host_source]||"source unavailable"}),ie=B(()=>{const C=i.value;return!C||C.running_scope==="unavailable"?"Actual bound address unavailable":`${(C.listening_hosts||[]).map((ne,Ee)=>{var St;const Je=(St=C.listening_ports)==null?void 0:St[Ee];return Je?`${ne}:${Je}`:ne}).join(", ")} · ${C.running_scope==="loopback"?"loopback only":"accepting beyond loopback"}`}),X=B(()=>P.value.length),_e=B(()=>new Set(P.value.map(C=>C.path.split(".")[0])).size),U=B(()=>!!T.value||L.value!=="all"),de=B(()=>{const C={...ee.value};for(const K of P.value){const ne=it(K.path),Ee=pe(ne,K.newVal);Ee&&(C[K.path]=Ee)}return C}),ge=B(()=>Object.keys(de.value).length>0),Ae=B(()=>e.value?(U.value?xe.value:xe.value.filter(K=>K.key===E.value)).map(K=>({...K,sections:K.sections.filter(ne=>Es(ne))})).filter(K=>K.sections.length):[]),$e=B(()=>{const C=["live_read","live_apply","live_for_new_work","restart","activation_required","legacy_control","dormant"],K=new Map(C.map(ne=>[ne,[]]));for(const ne of P.value){const Ee=K.has(ne.apply_mode)?ne.apply_mode:"restart";K.get(Ee).push(ne)}return C.filter(ne=>K.get(ne).length).map(ne=>({key:ne,label:ds(ne),entries:K.get(ne)}))}),We=B(()=>P.value.filter(C=>C.apply_mode==="restart").length),je=B(()=>be.value.filter(C=>C.pending_restart)),Xe=B(()=>je.value.length);function it(C){const K=Oe.value.get(C);return K?{...K,apply_details:Go([K])}:null}function Qe(C){const K=`${C}.`;return be.value.filter(ne=>ne.path===C||ne.path.startsWith(K))}function se(){return be.value.some(C=>C.path==="tools.hosts"||C.path.startsWith("tools.hosts."))}function Re(){var ne,Ee;const C=((Ee=(ne=e.value)==null?void 0:ne.tools)==null?void 0:Ee.hosts)||{},K=Object.keys(C).length;return`${K} host${K===1?"":"s"} configured.`}function Le(C){return Qe(C).length}function Te(C){return wi(C)}function W(C){const K=Qe(C);if(!K.length)return`${wi(C)} configuration.`;const ne=K.find(St=>St.sensitivity==="public"&&St.description)||K.find(St=>St.description),Ee=(ne==null?void 0:ne.description)||"";return Ee.match(/setting for (.+)\.$/i)?`${wi(C)} settings and runtime behaviour.`:Ee}function De(C){const K=[...new Set(Qe(C).map(ne=>ne.apply_mode))];return K.length===1?K[0]:K.includes("restart")?"restart":K.includes("activation_required")?"activation_required":K[0]||"restart"}function ze(C){const K=[...new Set(Qe(C).map(ne=>ds(ne.apply_mode)))];return K.length?K.length===1?K[0]:`Mixed apply behaviour: ${K.join(" · ")}`:""}function Ye(C){return Go(Qe(C))}function Dt(C){var K;return Object.hasOwn(O.value,C)?O.value[C]:(K=e.value)==null?void 0:K[C]}function et(){const C=Dt("mcp")||{},K=Object.keys(C.servers||{}).length;return`${C.enabled?"Globally enabled":"Globally disabled"} · ${K} configured server${K===1?"":"s"}.`}function Rt(C,K){return K.split(".").reduce((ne,Ee)=>ne==null?void 0:ne[Ee],C)}function Pt(C){const K=S.value;return Qe(C).filter(ne=>Xf(ne.path)?!1:ne.path.split(".").length<=2?!0:!ne.path.includes(".*")).map(ne=>({...ne,key:ne.path.split(".").at(-1),value:Rt(K,ne.path),apply_details:Go([ne]),editor:ne.path==="agents.final_warning_iterations"?"warning-chips":null}))}function Ts(C){const K=C.path.split(".");return K.length>2?K.slice(0,2).join("."):null}function Vs(C){const K=new Map;for(const ne of Pt(C)){const Ee=Ts(ne),Je=Ee||`${C}.__root`;K.has(Je)||K.set(Je,{key:Je,path:Ee,entries:[]}),K.get(Je).entries.push(ne)}return[...K.values()].map(ne=>{const Ee=ne.entries.find(Je=>Je.group_description);return{...ne,label:ne.path?wi(ne.path.split(".").at(-1)):null,description:(Ee==null?void 0:Ee.group_description)||null,apply_details:Go(ne.entries),runtime_summaries:zt(ne.entries)}})}function Gs(C){return{save:C.save_effect||(C.apply_mode==="dormant"?"Saving records this value in config.yml.":"Saving records this value and validates the section."),runtime:C.runtime_effect||{live_read:"Odin reads the saved value during current work.",live_apply:"Odin reloads this setting without a restart.",live_for_new_work:"New work uses the saved value; existing work keeps its snapshot.",restart:"Odin keeps using its startup value until a clean restart.",activation_required:"Odin keeps the current behavior until you enable this feature separately.",legacy_control:"Odin keeps the existing compatibility behavior until you apply this choice.",dormant:"This version of Odin does not use the saved value. Restarting will not activate it."}[C.apply_mode]||"Effective runtime state is not currently observable."}}function zt(C){const K=new Map;for(const ne of C){const Ee=Gs(ne),Je=`${ne.apply_mode}|${Ee.save}|${Ee.runtime}`;K.has(Je)||K.set(Je,{key:Je,label:ds(ne.apply_mode),save:Ee.save,runtime:Ee.runtime})}return[...K.values()]}function ta(C){if(Ws(C))return C.runtime_effect||C.activation_policy||"";if(C.apply_mode==="activation_required"){const K=C.activation_policy||C.runtime_effect;return K?`Not active after saving. No activation control exists in this release. ${K}`:"Not active after saving; no activation control exists in this release."}return""}function Ws(C){return C.action_available===!0&&!!(C.action_label&&C.action_endpoint)}async function Pa(C){if(Ws(C))try{if(Fe(C.path))throw new Error("Save this setting before applying its action.");const K=String(C.action_method||"POST").toLowerCase(),ne={post:z.post.bind(z),put:z.put.bind(z),delete:z.del.bind(z)}[K];if(!ne)throw new Error("Unsupported configuration action");await ne(C.action_endpoint,C.action_body||void 0),await gn(),As("success",`${C.action_label} completed.`)}catch(K){As("error",K.message||`${C.action_label} failed`)}}function hs(C,K){return[C.label,C.path,C.description,...C.aliases||[]].filter(Boolean).join(" ").toLowerCase().includes(K)}function $a(C){const K=T.value.trim().toLowerCase();return K?Qe(C).filter(ne=>hs(ne,K)):[]}function Es(C){const K=Qe(C);if(L.value!=="all"&&!K.some(Ee=>Ee.apply_state===L.value))return!1;const ne=T.value.trim().toLowerCase();return!ne||`${Te(C)} ${C}`.toLowerCase().includes(ne)?!0:K.some(Ee=>hs(Ee,ne))}function Ct(C,K){return Qe(C).filter(ne=>ne.apply_state===K).length}function ls(C){return C==="all"?oe.value:be.value.filter(K=>K.apply_state===C).length}function sa(C){const K=C.sections.flatMap(ne=>Qe(ne));return{fields:K.length,modified:P.value.filter(ne=>C.sections.includes(ne.path.split(".")[0])).length,pending_restart:K.filter(ne=>ne.apply_state==="pending_restart").length,invalid:K.filter(ne=>ne.apply_state==="invalid").length,dormant:K.filter(ne=>ne.apply_state==="dormant").length}}function ba(C){var K;return Object.hasOwn(O.value,C)&&!Oi((K=e.value)==null?void 0:K[C],O.value[C])}function Fe(C){return P.value.some(K=>K.path===C||K.path.startsWith(`${C}.`))}function ue(C){E.value=C,T.value="",L.value="all";try{localStorage.setItem(jg,C)}catch{}}function Ce(C){L.value=C}function qe(){T.value="",L.value="all"}function Ge(C){var K;return((K=xe.value.find(ne=>ne.sections.includes(C)))==null?void 0:K.sections)||[]}function It(C){const K=Ge(C),ne=K.find(Ee=>F.value[Ee]===!0);return ne||K.find(Ee=>F.value[Ee]!==!1)||null}function ot(C){return T.value&&!ce.value&&Es(C)?!0:ce.value?It(C)===C:Object.hasOwn(F.value,C)?F.value[C]===!0:!0}function pn(C){const K=!ot(C);if(ce.value){const ne={...F.value};for(const Ee of Ge(C))ne[Ee]===!0&&(ne[Ee]=!1);ne[C]=K,F.value=ne;return}F.value={...F.value,[C]:K}}function pi(){N.value.push(Gn(O.value)),N.value.length>$C&&N.value.shift(),M.value=[]}function Fa(){n.value||G.value&&(pi(),O.value={},ee.value={},$.value=!1)}function fi(C,K=!1){const ne=Date.now();if(K&&ae.path===C&&ne-ae.atJe-Ee);H.value="",kt(C,ne)}function $n(C,K){kt(C,(C.value||[]).filter(ne=>ne!==K))}function Ua(C){return C.apply_mode==="live_read"?"Odin reads the saved file value on next use.":C.apply_mode==="live_for_new_work"?"New work uses the saved file value.":C.apply_mode==="live_apply"?C.apply_handler?`Apply the saved value through ${C.apply_handler}.`:"Apply it through its dedicated owner page or endpoint.":C.apply_mode==="restart"?"Restart Odin for the saved collection to take effect.":C.apply_mode==="activation_required"?"Saving does not enable it. No activation control exists in this release.":C.apply_mode==="dormant"?"This release does not use the saved collection.":"Follow the runtime details shown for this setting."}function vn(C){return C.type==="array"&&Array.isArray(C.value)&&!C.structured_container&&!C.structured_container_child&&C.sensitivity==="public"&&C.value.every(K=>["string","number","boolean"].includes(typeof K))}function Ie(C){const K=String(te.value[C.path]??"").trim();if(!K)return;const ne=[...new Set([...C.value||[],K])];te.value={...te.value,[C.path]:""},kt(C,ne)}function D(C,K){kt(C,(C.value||[]).filter(ne=>ne!==K))}function pe(C,K){var Ee;if(!C)return null;if((Ee=C.enum)!=null&&Ee.length&&!C.enum.includes(K))return`Choose one of: ${C.enum.join(", ")}`;if(C.path==="agents.final_warning_iterations"&&(!Array.isArray(K)||!K.length))return"Add at least one warning threshold.";const ne=C.constraints||{};if((C.type==="integer"||C.type==="number")&&typeof K=="number"){if(ne.minimum!==void 0&&Kne.maximum)return`Must be at most ${ne.maximum}${C.unit?` ${C.unit}`:""}`}return null}function ke(C){return de.value[C.path]||null}function Be(C){const K=`${C}.`;return Object.keys(de.value).some(ne=>ne===C||ne.startsWith(K))}function Ue(){n.value||N.value.length&&(M.value.push(Gn(O.value)),O.value=N.value.pop(),ee.value={},I.value={},ae={path:null,at:0})}function Ke(){n.value||M.value.length&&(N.value.push(Gn(O.value)),O.value=M.value.pop(),ee.value={},I.value={},ae={path:null,at:0})}function $t(){!G.value||ge.value||($.value=!0,re.value=!1)}function gt(){$.value=!1}function Ot(){Fa()}function ds(C){return DC[C]||wi(C||"unknown")}function ut(C){return`apply-${String(C||"unknown").replaceAll("_","-")}`}function ua(C){return`cfgc-field-${C.replace(/[^a-zA-Z0-9_-]/g,"-")}`}function hn(C){return`${ua(C)}-input`}function _o(C){const K=document.getElementById(ua(C))||document.getElementById(ua(C.split(".").slice(0,2).join(".")));K==null||K.scrollIntoView({behavior:"smooth",block:"center"})}function As(C,K){g.value={type:C,message:K},window.setTimeout(()=>{var ne;((ne=g.value)==null?void 0:ne.message)===K&&(g.value=null)},3500)}function vc(){x.value=!1,L.value="pending_restart",T.value="";const C=MC(a.value);C&&(C.scrollTop=0)}function hc(){x.value=!1}function wo(C=1800){Q&&window.clearTimeout(Q),Q=window.setTimeout(gc,C)}async function gc(){if(w.value){if(V+=1,V>45){w.value=!1,_.value="Odin did not return with the new startup settings within 90 seconds.";return}try{if(t.value=await gl(),Xe.value===0){w.value=!1,_.value=null,As("success","Odin restarted and the saved startup settings are active.");return}}catch{}wo(2e3)}}async function bc(){if(!w.value){_.value=null;try{await z.post("/api/restart",{}),w.value=!0,V=0,x.value=!1,wo()}catch(C){_.value=C.message||"Odin could not schedule a restart."}}}async function ko(){if(!(!G.value||ge.value||n.value)){n.value=!0;try{const C=zC(e.value,O.value),K=await z.put("/api/config",C);e.value=K,O.value={},N.value=[],M.value=[],ee.value={},$.value=!1;try{t.value=await gl(),b.value=null,x.value=Xe.value>0,As("success",Xe.value?`Configuration saved. ${Xe.value} setting${Xe.value===1?"":"s"} still use startup values.`:"Configuration saved. Apply status has been refreshed.")}catch(ne){b.value=ne.message||"Unknown metadata error.",As("error",`Configuration saved, but apply status could not be refreshed: ${b.value}`)}}catch(C){As("error",C.message||"Configuration could not be saved")}finally{n.value=!1}}}async function yc(){if(!n.value){n.value=!0,y.value=null;try{t.value=await gl(),b.value=null}catch(C){y.value=`Image model status could not be refreshed: ${C.message||"Unknown error"}`}finally{n.value=!1}}}async function xc(C,K){if(n.value||!["follow","pin"].includes(K)||!C.length||C.some(Ee=>{var Je,St;return!k.includes(Ee)||!((St=(Je=t.value)==null?void 0:Je.image_model_defaults)!=null&&St[Ee])}))return;n.value=!0,y.value=null;let ne=!1;try{const Ee=await z.post("/api/config/image-models",{operations:Object.fromEntries(C.map(Je=>[Je,K])),expected_revision:t.value.image_model_revision});ne=!0;for(const Je of C){const St=`image.openai.${Je}`,Ks=Rt(e.value,St),gs=Rt(Ee.config,St),yn=Rs=>!Object.hasOwn(Rs,"image")||!Oi(Rt(Rs,St),Ks)?Rs:Ba(Rs,St.split("."),gs);O.value=yn(O.value),N.value=N.value.map(yn),M.value=M.value.map(yn),e.value=Ba(e.value,St.split("."),gs)}t.value={...t.value,image_model_defaults:Ee.image_model_defaults,image_model_revision:Ee.image_model_revision},As("success","Image model defaults saved. Unrelated drafts were not saved.")}catch(Ee){y.value=`Image model operation failed: ${Ee.message||"Unknown error"}. Refresh status before retrying if the outcome is uncertain.`}try{t.value=await gl(),b.value=null}catch(Ee){const Je=`Image model status could not be refreshed: ${Ee.message||"Unknown error"}`;b.value=Je,y.value=ne?`Image model defaults were saved, but ${Je}`:`${y.value} ${Je}`}finally{n.value=!1}}async function gn(){var C,K;if(!(G.value||n.value)){s.value=!0,A.value=null;try{const ne=await z.get("/api/config"),Ee=await gl();await m(),e.value=ne,t.value=Ee,b.value=null;const Je=xe.value;if(Je.some(St=>St.key===E.value)||(E.value=((C=Je[0])==null?void 0:C.key)||zi[0].key),ce.value){const Ks=(((K=Je.find(gs=>gs.key===E.value))==null?void 0:K.sections)||[]).find(gs=>F.value[gs]===!0);F.value=Ks?{...F.value,[Ks]:!0}:{}}}catch(ne){A.value=ne.message||"Unknown configuration error"}finally{s.value=!1}}}function bn(C){if($.value||!(C.ctrlKey||C.metaKey))return;const K=C.target;K instanceof HTMLElement&&(K.matches("input, textarea, select")||K.isContentEditable)||(!C.shiftKey&&C.key.toLowerCase()==="z"?(C.preventDefault(),Ue()):(C.key.toLowerCase()==="y"||C.shiftKey&&C.key.toLowerCase()==="z")&&(C.preventDefault(),Ke()))}function mi(C){ce.value=C.matches}Gt(F,C=>{try{localStorage.setItem(Hg,JSON.stringify(C))}catch{}},{deep:!0});let Fn=!1;function vi(){Fn||(Fn=!0,document.addEventListener("keydown",bn))}function al(){Fn&&(Fn=!1,document.removeEventListener("keydown",bn))}return st(()=>{var C;gn(),vi(),Z=window.matchMedia("(max-width: 760px)"),mi(Z),(C=Z.addEventListener)==null||C.call(Z,"change",mi)}),cs(vi),es(al),es(()=>{o.value=""}),wt(()=>{var C;o.value="",al(),(C=Z==null?void 0:Z.removeEventListener)==null||C.call(Z,"change",mi),Q&&window.clearTimeout(Q)}),{listenerState:i,listenerConsent:l,listenerCredential:o,listenerSaving:r,listenerMessage:c,listenerError:d,listenerStatusError:u,listenerChoiceChanged:he,listenerStatusTone:fe,listenerStatusLabel:me,listenerAuthorizationCopy:we,listenerConfiguredSourceCopy:J,listenerRunningCopy:ie,saveListenerConsent:h,armKeydown:vi,disarmKeydown:al,handleKeydown:bn,config:e,meta:t,loading:s,saving:n,error:A,toast:g,metaRefreshError:b,restartPromptOpen:x,restartScheduled:w,restartError:_,configMain:a,imageModelError:y,imageModelLeaves:k,setImageModelDefaults:xc,refreshImageModelMetadata:yc,searchQuery:T,healthFilter:L,activeCategory:E,reviewOpen:$,mobileOverflowOpen:re,warningThresholdInput:H,arrayInputs:te,healthFilters:ve,visibleCategories:xe,displayGroups:Ae,reviewGroups:$e,sectionCount:le,fieldCount:oe,hasChanges:G,changeCount:X,changedSectionCount:_e,hasDraftErrors:ge,canUndo:j,canRedo:Y,globalFilterActive:U,reviewRestartCount:We,pendingRestartCount:Xe,pendingRestartFields:je,healthCount:ls,categoryStats:sa,selectCategory:ue,selectHealthFilter:Ce,clearFilters:qe,sectionLabel:Te,sectionDescription:W,sectionFieldCount:Le,sectionHealthCount:Ct,sectionApplySummary:ze,sectionApplyDetails:Ye,sectionEntries:Pt,fieldGroups:Vs,sectionSearchHits:$a,mcpConfigSummary:et,fieldRuntimeCopy:Gs,fieldSpecificRuntimeNote:ta,hasHonestAction:Ws,runFieldAction:Pa,hasHostsCollection:se,hostsConfigSummary:Re,sectionChanged:ba,fieldChanged:Fe,isSectionExpanded:ot,toggleSection:pn,discardAllDrafts:Fa,setFieldValue:kt,setNumberFieldValue:Pn,numberInputValue:xa,beginInputEdit:$s,endTextInputEdit:Wt,endInputEdit:ya,addWarningThreshold:mn,removeWarningThreshold:$n,isScalarArray:vn,addScalarArrayItem:Ie,removeScalarArrayItem:D,fieldError:ke,sectionHasErrors:Be,undo:Ue,redo:Ke,openReview:$t,closeReview:gt,mobileCancel:Ot,applyModeLabel:ds,applyClass:ut,compactValue:BC,formatValue:UC,structuredApplyCopy:Ua,fieldId:ua,fieldInputId:hn,focusField:_o,fetchConfig:gn,saveConfig:ko,restartOdin:bc,restartLater:hc,reviewPendingRestart:vc}}},VC=/^\d{15,25}$/;function Gg(e){return String((e==null?void 0:e.display_name)||(e==null?void 0:e.username)||(e==null?void 0:e.id)||"Unknown user")}const Wg={props:{members:{type:Array,default:()=>[]},excludedIds:{type:Array,default:()=>[]},placeholder:{type:String,default:"Search Discord users…"},ariaLabel:{type:String,default:"Search Discord users"},optionsId:{type:String,required:!0},autofocus:{type:Boolean,default:!1},showAddButton:{type:Boolean,default:!1}},emits:["select"],template:`
- `,setup(e,{emit:t}){const s=p(""),a=p(!1),n=p(0),i=p(null),l=U(()=>new Set((e.excludedIds||[]).map(String))),o=U(()=>{const _=s.value.toLowerCase().trim();return(e.members||[]).filter(T=>l.value.has(String(T.id))?!1:_?f(T).toLowerCase().includes(_)||String(T.username||"").toLowerCase().includes(_)||String(T.id).includes(_):!0)}),r=U(()=>{const _=s.value.trim();return o.value.length===0&&SC.test(_)&&!l.value.has(_)?_:""}),c=U(()=>o.value.length+(r.value?1:0)),d=U(()=>!!s.value.trim()&&!!(o.value[n.value]||r.value&&n.value===o.value.length)),u=U(()=>{if(a.value){if(o.value[n.value])return`${e.optionsId}-${n.value}`;if(r.value&&n.value===o.value.length)return`${e.optionsId}-raw`}});function f(_){return Pg(_)}function m(){a.value=!0,n.value=0}function h(){m()}function g(){const _=Math.max(c.value-1,0);n.value=Math.min(n.value+1,_)}function A(){n.value=Math.max(n.value-1,0)}function w(){const _=o.value[n.value];_?y(_):r.value&&n.value===o.value.length&&b(r.value)}function y(_){b(String(_.id))}function b(_){t("select",_),s.value="",a.value=!1,n.value=0}function x(){a.value=!1}function S(){setTimeout(x,150)}return tt(()=>{e.autofocus&&Mt(()=>{var _;return(_=i.value)==null?void 0:_.focus()})}),{query:s,open:a,highlightedIndex:n,input:i,filteredMembers:o,rawId:r,activeOptionId:u,selectableValue:d,memberName:f,openOptions:m,onInput:h,highlightNext:g,highlightPrevious:A,selectHighlighted:w,selectMember:y,selectId:b,closeOptions:x,onBlur:S}}};function qf(e,t,s){var a;return((a=e==null?void 0:e.config)==null?void 0:a[t])!=null?e.config[t]:s==null?void 0:s[t]}const CC={components:{DiscordUserCombobox:$g},template:` + `,setup(e,{emit:t}){const s=f(""),a=f(!1),n=f(0),i=f(null),l=B(()=>new Set((e.excludedIds||[]).map(String))),o=B(()=>{const _=s.value.toLowerCase().trim();return(e.members||[]).filter(T=>l.value.has(String(T.id))?!1:_?p(T).toLowerCase().includes(_)||String(T.username||"").toLowerCase().includes(_)||String(T.id).includes(_):!0)}),r=B(()=>{const _=s.value.trim();return o.value.length===0&&VC.test(_)&&!l.value.has(_)?_:""}),c=B(()=>o.value.length+(r.value?1:0)),d=B(()=>!!s.value.trim()&&!!(o.value[n.value]||r.value&&n.value===o.value.length)),u=B(()=>{if(a.value){if(o.value[n.value])return`${e.optionsId}-${n.value}`;if(r.value&&n.value===o.value.length)return`${e.optionsId}-raw`}});function p(_){return Gg(_)}function m(){a.value=!0,n.value=0}function h(){m()}function y(){const _=Math.max(c.value-1,0);n.value=Math.min(n.value+1,_)}function k(){n.value=Math.max(n.value-1,0)}function A(){const _=o.value[n.value];_?g(_):r.value&&n.value===o.value.length&&b(r.value)}function g(_){b(String(_.id))}function b(_){t("select",_),s.value="",a.value=!1,n.value=0}function x(){a.value=!1}function w(){setTimeout(x,150)}return st(()=>{e.autofocus&&Ut(()=>{var _;return(_=i.value)==null?void 0:_.focus()})}),{query:s,open:a,highlightedIndex:n,input:i,filteredMembers:o,rawId:r,activeOptionId:u,selectableValue:d,memberName:p,openOptions:m,onInput:h,highlightNext:y,highlightPrevious:k,selectHighlighted:A,selectMember:g,selectId:b,closeOptions:x,onBlur:w}}};function em(e,t,s){var a;return((a=e==null?void 0:e.config)==null?void 0:a[t])!=null?e.config[t]:s==null?void 0:s[t]}const GC={components:{DiscordUserCombobox:Wg},template:`

Discord Channels

@@ -5073,7 +5081,7 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p([]),t=p({persisted:!1,credential_usable:!1,credential_preferred_storage:!1,connection:{state:"unavailable",detail:"Connection status unavailable"}}),s=p(""),a=p(!1),n=p(!1),i=p(null);let l=null;const o=p(!0),r=p(null),c=p({}),d=p(null),u=p(null),f=p(!1),m=p(null),h=p({}),g=p([]);let A=0;const w=Object.freeze([{key:"allowed_users",label:"Allowed users",description:"Absolute gate for ordinary conversational intake. Guild/channel settings cannot readmit blocked users; explicitly allowed test webhooks bypass this gate.",placeholder:"Search users",userAutocomplete:!0},{key:"channels",label:"Allowed channels",description:"Absolute gate for ordinary conversational intake. Guild/channel settings cannot readmit blocked channels.",placeholder:"Search channels"},{key:"ignore_bot_ids",label:"Ignored bot IDs",description:"Ignored unless the bot explicitly mentions Odin; the effective respond-to-bots policy still applies.",placeholder:"Bot ID",userAutocomplete:!0}]),y=U(()=>{var V,xe,k,D;const oe=String(((xe=(V=t.value)==null?void 0:V.connection)==null?void 0:xe.state)||"").toLowerCase();return{...{connected:{key:"connected",label:"Connected",badgeClass:"badge-success"},connecting:{key:"connecting",label:"Connecting",badgeClass:"badge-warning"},disconnected:{key:"disconnected",label:"Disconnected",badgeClass:""},detached:{key:"disconnected",label:"Disconnected",badgeClass:""},detaching:{key:"disconnected",label:"Disconnected",badgeClass:""},stopped:{key:"unavailable",label:"Unavailable",badgeClass:"badge-warning"},failed:{key:"unavailable",label:"Unavailable",badgeClass:"badge-warning"}}[oe]||{key:"unavailable",label:"Unavailable",badgeClass:"badge-warning"},detail:((D=(k=t.value)==null?void 0:k.connection)==null?void 0:D.detail)||"Connection status unavailable"}}),b=U(()=>JSON.stringify(d.value)!==JSON.stringify(u.value)),x=U(()=>new Map(g.value.map(oe=>[String(oe.id),oe])));function S(oe){return oe.config&&oe.config.enabled!==void 0?oe.config.enabled:!0}function _(oe){return qf(oe,"require_mention",d.value)}function T(oe){return qf(oe,"respond_to_bots",d.value)}function L(oe){return oe.config&&Object.keys(oe.config).length>0}function E(oe){c.value[oe]=!c.value[oe]}function P(oe){const ve=oe.discord||{};return{allowed_users:[...ve.allowed_users||[]],channels:[...ve.channels||[]],respond_to_bots:!!ve.respond_to_bots,require_mention:!!ve.require_mention,ignore_bot_ids:[...ve.ignore_bot_ids||[]]}}async function R({showLoading:oe=!0}={}){const ve=++A;oe&&(o.value=!0),r.value=null;try{const j=await z.get("/api/discord/guilds");ve===A&&(e.value=j)}catch(j){ve===A&&(r.value=j.message)}finally{oe&&ve===A&&(o.value=!1)}}async function I(){try{t.value=await z.get("/api/discord/connection"),i.value=null}catch(oe){i.value=oe.message}}async function H(oe,ve=null){if(!n.value){n.value=!0,i.value=null;try{const j={operation:oe};ve!==null&&(j.token=ve),t.value=await z.post("/api/discord/connection",j),oe==="credentials"&&(s.value="",a.value=!1)}catch(j){i.value=j.message||"Connection update failed."}finally{n.value=!1}}}function X(){if(!(!a.value||!s.value))return H("credentials",s.value)}function Q(){return H("connect")}function N(){return H("detach")}async function O(){o.value=!0,r.value=null;try{const[oe,ve,j]=await Promise.all([z.get("/api/discord/guilds"),z.get("/api/discord/members").catch(()=>[]),z.get("/api/config")]),V=P(j),xe=b.value;d.value=V,xe||(u.value=JSON.parse(JSON.stringify(V))),g.value=ve,e.value=oe,m.value=null}catch(oe){r.value=oe.message}finally{o.value=!1}}let $=Promise.resolve();const ce=p(new Set);function ue(oe,ve){const j=new Set(ce.value);j.add(oe),ce.value=j;const V=$.then(ve);return $=V.catch(()=>{}),V.finally(()=>{const xe=new Set(ce.value);xe.delete(oe),ce.value=xe})}function Z(oe,ve,j,V){const xe=(V==null?void 0:V.target)??null;return ue(`guild:${oe}:${ve}`,async()=>{try{await z.put("/api/discord/guild/"+oe+"/config",{[ve]:j}),await R({showLoading:!1})}catch(k){r.value=k.message,xe&&typeof j=="boolean"&&(xe.checked=!j)}})}function J(oe,ve,j,V,xe){const k=(xe==null?void 0:xe.target)??null;return ue(`channel:${oe}:${j}`,async()=>{try{await z.put("/api/discord/channel/"+oe+"/config",{[j]:V}),await R({showLoading:!1})}catch(D){r.value=D.message,k&&typeof V=="boolean"&&(k.checked=!V)}})}function se(oe,ve){return ue(`channel:${oe}:clear`,async()=>{try{await z.put("/api/discord/channel/"+oe+"/config",{clear:!0}),await R({showLoading:!1})}catch(j){r.value=j.message}})}function q(oe,ve){const j=String(ve);if(!oe.userAutocomplete)return j;const V=x.value.get(j);return V?Pg(V):j}function ye(oe,ve=null){const j=String(ve??h.value[oe]??"").trim();!j||u.value[oe].includes(j)||(u.value[oe]=[...u.value[oe],j],h.value={...h.value,[oe]:""})}function Oe(oe,ve){u.value[oe]=u.value[oe].filter(j=>j!==ve)}async function le(){if(!(!b.value||f.value)){f.value=!0,m.value=null;try{const ve=(await z.put("/api/config",{discord:u.value})).discord||u.value;d.value={allowed_users:[...ve.allowed_users||[]],channels:[...ve.channels||[]],respond_to_bots:!!ve.respond_to_bots,require_mention:!!ve.require_mention,ignore_bot_ids:[...ve.ignore_bot_ids||[]]},u.value=JSON.parse(JSON.stringify(d.value))}catch(oe){m.value=oe.message||"Global defaults could not be saved."}finally{f.value=!1}}}return tt(()=>{O(),I(),l=window.setInterval(I,5e3)}),xt(()=>{l!==null&&window.clearInterval(l),l=null}),{guilds:e,loading:o,error:r,expanded:c,globalDraft:u,globalSaving:f,globalError:m,globalArrayInputs:h,globalMembers:g,globalListEditors:w,globalChanged:b,guildEnabled:S,guildMention:_,guildBots:T,hasOverride:L,toggleGuild:E,fetchAll:O,fetchGuilds:R,setGuildConfig:Z,setChannelConfig:J,clearOverride:se,mutationPending:ce,globalItemLabel:q,addGlobalItem:ye,removeGlobalItem:Oe,saveGlobalDefaults:le,connection:t,connectionState:y,connectionToken:s,connectionTokenDirty:a,connectionBusy:n,connectionError:i,saveDiscordCredentials:X,connectDiscord:Q,detachDiscord:N}}},Ws=e=>e==null?e:JSON.parse(JSON.stringify(e));function TC({applyDefault:e,applyUser:t,applyDelete:s,onDefaultConfirmed:a=()=>{},onDefaultRollback:n=()=>{},onUserConfirmed:i=()=>{},onUserRollback:l=()=>{},onUserDeleted:o=()=>{},onError:r=()=>{}}){let c=Promise.resolve(),d=0,u=0;const f=new Map;let m=null;const h=new Map;function g(_){d+=1;const T=c.then(_,_);return c=T.catch(()=>{}),T}function A(_,T){m=Ws(_),h.clear();for(const[L,E]of Object.entries(T||{}))h.set(L,Ws(E))}function w(_){const T=Ws(_),L=++u;return g(async()=>{try{await e(Ws(T)),m=Ws(T),L===u&&a(Ws(T))}catch(E){L===u&&(n(Ws(m)),r(E,{kind:"default"}))}})}function y(_,T){const L=Ws(T),E=(f.get(_)||0)+1;return f.set(_,E),g(async()=>{try{await t(_,Ws(L)),h.set(_,Ws(L)),E===f.get(_)&&i(_,Ws(L))}catch(P){E===f.get(_)&&(l(_,Ws(h.get(_)??null)),r(P,{kind:"user",uid:_}))}})}function b(_){const T=(f.get(_)||0)+1;return f.set(_,T),g(async()=>{try{await s(_),h.delete(_),T===f.get(_)&&o(_)}catch(L){T===f.get(_)&&(l(_,Ws(h.get(_)??null)),r(L,{kind:"delete",uid:_}))}})}async function x(){for(;;){const _=c;if(await _,_===c)return d}}async function S(_){for(;;){const T=await x(),L=await _();if(T===d)return L}}return{seed:A,saveDefault:w,saveUser:y,deleteUser:b,whenIdle:x,readSnapshot:S,get revision(){return d}}}const EC={components:{DiscordUserCombobox:$g,DiscordIdentity:En},template:` + `,setup(){const e=f([]),t=f({persisted:!1,credential_usable:!1,credential_preferred_storage:!1,connection:{state:"unavailable",detail:"Connection status unavailable"}}),s=f(""),a=f(!1),n=f(!1),i=f(null);let l=null;const o=f(!0),r=f(null),c=f({}),d=f(null),u=f(null),p=f(!1),m=f(null),h=f({}),y=f([]);let k=0;const A=Object.freeze([{key:"allowed_users",label:"Allowed users",description:"Absolute gate for ordinary conversational intake. Guild/channel settings cannot readmit blocked users; explicitly allowed test webhooks bypass this gate.",placeholder:"Search users",userAutocomplete:!0},{key:"channels",label:"Allowed channels",description:"Absolute gate for ordinary conversational intake. Guild/channel settings cannot readmit blocked channels.",placeholder:"Search channels"},{key:"ignore_bot_ids",label:"Ignored bot IDs",description:"Ignored unless the bot explicitly mentions Odin; the effective respond-to-bots policy still applies.",placeholder:"Bot ID",userAutocomplete:!0}]),g=B(()=>{var Y,xe,S,P;const oe=String(((xe=(Y=t.value)==null?void 0:Y.connection)==null?void 0:xe.state)||"").toLowerCase();return{...{connected:{key:"connected",label:"Connected",badgeClass:"badge-success"},connecting:{key:"connecting",label:"Connecting",badgeClass:"badge-warning"},disconnected:{key:"disconnected",label:"Disconnected",badgeClass:""},detached:{key:"disconnected",label:"Disconnected",badgeClass:""},detaching:{key:"disconnected",label:"Disconnected",badgeClass:""},stopped:{key:"unavailable",label:"Unavailable",badgeClass:"badge-warning"},failed:{key:"unavailable",label:"Unavailable",badgeClass:"badge-warning"}}[oe]||{key:"unavailable",label:"Unavailable",badgeClass:"badge-warning"},detail:((P=(S=t.value)==null?void 0:S.connection)==null?void 0:P.detail)||"Connection status unavailable"}}),b=B(()=>JSON.stringify(d.value)!==JSON.stringify(u.value)),x=B(()=>new Map(y.value.map(oe=>[String(oe.id),oe])));function w(oe){return oe.config&&oe.config.enabled!==void 0?oe.config.enabled:!0}function _(oe){return em(oe,"require_mention",d.value)}function T(oe){return em(oe,"respond_to_bots",d.value)}function L(oe){return oe.config&&Object.keys(oe.config).length>0}function E(oe){c.value[oe]=!c.value[oe]}function F(oe){const ve=oe.discord||{};return{allowed_users:[...ve.allowed_users||[]],channels:[...ve.channels||[]],respond_to_bots:!!ve.respond_to_bots,require_mention:!!ve.require_mention,ignore_bot_ids:[...ve.ignore_bot_ids||[]]}}async function O({showLoading:oe=!0}={}){const ve=++k;oe&&(o.value=!0),r.value=null;try{const j=await z.get("/api/discord/guilds");ve===k&&(e.value=j)}catch(j){ve===k&&(r.value=j.message)}finally{oe&&ve===k&&(o.value=!1)}}async function I(){try{t.value=await z.get("/api/discord/connection"),i.value=null}catch(oe){i.value=oe.message}}async function H(oe,ve=null){if(!n.value){n.value=!0,i.value=null;try{const j={operation:oe};ve!==null&&(j.token=ve),t.value=await z.post("/api/discord/connection",j),oe==="credentials"&&(s.value="",a.value=!1)}catch(j){i.value=j.message||"Connection update failed."}finally{n.value=!1}}}function te(){if(!(!a.value||!s.value))return H("credentials",s.value)}function ee(){return H("connect")}function N(){return H("detach")}async function M(){o.value=!0,r.value=null;try{const[oe,ve,j]=await Promise.all([z.get("/api/discord/guilds"),z.get("/api/discord/members").catch(()=>[]),z.get("/api/config")]),Y=F(j),xe=b.value;d.value=Y,xe||(u.value=JSON.parse(JSON.stringify(Y))),y.value=ve,e.value=oe,m.value=null}catch(oe){r.value=oe.message}finally{o.value=!1}}let $=Promise.resolve();const re=f(new Set);function ce(oe,ve){const j=new Set(re.value);j.add(oe),re.value=j;const Y=$.then(ve);return $=Y.catch(()=>{}),Y.finally(()=>{const xe=new Set(re.value);xe.delete(oe),re.value=xe})}function Z(oe,ve,j,Y){const xe=(Y==null?void 0:Y.target)??null;return ce(`guild:${oe}:${ve}`,async()=>{try{await z.put("/api/discord/guild/"+oe+"/config",{[ve]:j}),await O({showLoading:!1})}catch(S){r.value=S.message,xe&&typeof j=="boolean"&&(xe.checked=!j)}})}function Q(oe,ve,j,Y,xe){const S=(xe==null?void 0:xe.target)??null;return ce(`channel:${oe}:${j}`,async()=>{try{await z.put("/api/discord/channel/"+oe+"/config",{[j]:Y}),await O({showLoading:!1})}catch(P){r.value=P.message,S&&typeof Y=="boolean"&&(S.checked=!Y)}})}function ae(oe,ve){return ce(`channel:${oe}:clear`,async()=>{try{await z.put("/api/discord/channel/"+oe+"/config",{clear:!0}),await O({showLoading:!1})}catch(j){r.value=j.message}})}function V(oe,ve){const j=String(ve);if(!oe.userAutocomplete)return j;const Y=x.value.get(j);return Y?Gg(Y):j}function be(oe,ve=null){const j=String(ve??h.value[oe]??"").trim();!j||u.value[oe].includes(j)||(u.value[oe]=[...u.value[oe],j],h.value={...h.value,[oe]:""})}function Oe(oe,ve){u.value[oe]=u.value[oe].filter(j=>j!==ve)}async function le(){if(!(!b.value||p.value)){p.value=!0,m.value=null;try{const ve=(await z.put("/api/config",{discord:u.value})).discord||u.value;d.value={allowed_users:[...ve.allowed_users||[]],channels:[...ve.channels||[]],respond_to_bots:!!ve.respond_to_bots,require_mention:!!ve.require_mention,ignore_bot_ids:[...ve.ignore_bot_ids||[]]},u.value=JSON.parse(JSON.stringify(d.value))}catch(oe){m.value=oe.message||"Global defaults could not be saved."}finally{p.value=!1}}}return st(()=>{M(),I(),l=window.setInterval(I,5e3)}),wt(()=>{l!==null&&window.clearInterval(l),l=null}),{guilds:e,loading:o,error:r,expanded:c,globalDraft:u,globalSaving:p,globalError:m,globalArrayInputs:h,globalMembers:y,globalListEditors:A,globalChanged:b,guildEnabled:w,guildMention:_,guildBots:T,hasOverride:L,toggleGuild:E,fetchAll:M,fetchGuilds:O,setGuildConfig:Z,setChannelConfig:Q,clearOverride:ae,mutationPending:re,globalItemLabel:V,addGlobalItem:be,removeGlobalItem:Oe,saveGlobalDefaults:le,connection:t,connectionState:g,connectionToken:s,connectionTokenDirty:a,connectionBusy:n,connectionError:i,saveDiscordCredentials:te,connectDiscord:ee,detachDiscord:N}}},Js=e=>e==null?e:JSON.parse(JSON.stringify(e));function WC({applyDefault:e,applyUser:t,applyDelete:s,onDefaultConfirmed:a=()=>{},onDefaultRollback:n=()=>{},onUserConfirmed:i=()=>{},onUserRollback:l=()=>{},onUserDeleted:o=()=>{},onError:r=()=>{}}){let c=Promise.resolve(),d=0,u=0;const p=new Map;let m=null;const h=new Map;function y(_){d+=1;const T=c.then(_,_);return c=T.catch(()=>{}),T}function k(_,T){m=Js(_),h.clear();for(const[L,E]of Object.entries(T||{}))h.set(L,Js(E))}function A(_){const T=Js(_),L=++u;return y(async()=>{try{await e(Js(T)),m=Js(T),L===u&&a(Js(T))}catch(E){L===u&&(n(Js(m)),r(E,{kind:"default"}))}})}function g(_,T){const L=Js(T),E=(p.get(_)||0)+1;return p.set(_,E),y(async()=>{try{await t(_,Js(L)),h.set(_,Js(L)),E===p.get(_)&&i(_,Js(L))}catch(F){E===p.get(_)&&(l(_,Js(h.get(_)??null)),r(F,{kind:"user",uid:_}))}})}function b(_){const T=(p.get(_)||0)+1;return p.set(_,T),y(async()=>{try{await s(_),h.delete(_),T===p.get(_)&&o(_)}catch(L){T===p.get(_)&&(l(_,Js(h.get(_)??null)),r(L,{kind:"delete",uid:_}))}})}async function x(){for(;;){const _=c;if(await _,_===c)return d}}async function w(_){for(;;){const T=await x(),L=await _();if(T===d)return L}}return{seed:k,saveDefault:A,saveUser:g,deleteUser:b,whenIdle:x,readSnapshot:w,get revision(){return d}}}const KC={components:{DiscordUserCombobox:Wg,DiscordIdentity:Dn},template:`

Host Access Control

@@ -5181,7 +5189,7 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p(!0),t=p(""),s=p(null),a=p([]),n=p({}),i=p({allowed_hosts:[],default_host:""}),l=p({}),o=p([]),r=p(!1),c=p([]),d=p({overrides:{},config_tiers:{},invalid_overrides:{}}),u=["admin","user","guest"],f=p({}),m=p(""),h=p(null),g=p(null),A=p(!1),w=p(""),y=p(!1),b=p(""),x=p(""),S=U(()=>a.value.filter(j=>`${j} ${n.value[j]||""}`.toLowerCase().includes(b.value.toLowerCase()))),_=U(()=>a.value.filter(j=>{var V,xe;return((V=g.value)==null?void 0:V.allow_all)||((xe=g.value)==null?void 0:xe.allowed_hosts.includes(j))})),T=U(()=>g.value?g.value.default_host&&!_.value.includes(g.value.default_host)?_.value.length?"Choose a new default from the allowed hosts before saving.":"No hosts are allowed. Select None for the default before saving.":y.value&&_.value.length&&!g.value.default_host?"Choose a new default from the allowed hosts before saving.":"":""),L=U(()=>[...new Set([...Object.keys(l.value),...Object.keys(d.value.overrides||{}),...Object.keys(d.value.config_tiers||{}),...Object.keys(d.value.invalid_overrides||{})])]),E=U(()=>L.value.filter(j=>{var V,xe;return`${((V=R(j))==null?void 0:V.display_name)||""} ${((xe=R(j))==null?void 0:xe.username)||""} ${j}`.toLowerCase().includes(x.value.toLowerCase())})),P=U(()=>{const j={};for(const V of c.value)j[V.id]=V;return j});function R(j){return P.value[j]||null}function I(j){if(!j||j.allow_all)return"All hosts";const V=j.allowed_hosts||[];return V.length>3?`${V.slice(0,3).join(", ")} +${V.length-3}`:V.join(", ")||"No hosts"}function H(j){if(j.allow_all)return["All hosts"];const V=j.allowed_hosts||[];return[...V.slice(0,2),...V.length>2?[`+${V.length-2}`]:[],...V.length?[]:["None"]]}function X(j){return o.value.includes(j)?H(l.value[j]):["Inherit policy"]}async function Q(j){var xe,k;if(A.value)return;const V=j==="default"?i.value:l.value[j]||i.value;m.value=j,g.value={...V,allowed_hosts:[...V.allowed_hosts]},y.value=!!V.default_host,b.value="",w.value="",await Mt(),g.value&&!((xe=h.value)!=null&&xe.open)&&((k=h.value)==null||k.showModal())}function N(){var j;A.value||((j=h.value)==null||j.close(),g.value=null,m.value="")}function O(j){if(j.key!=="Tab")return;const V=[...h.value.querySelectorAll("button, input, select")].filter(D=>!D.matches(":disabled")),xe=V[0],k=V.at(-1);if(!xe){j.preventDefault();return}j.shiftKey&&document.activeElement===xe?(j.preventDefault(),k.focus()):!j.shiftKey&&document.activeElement===k&&(j.preventDefault(),xe.focus())}async function $(){var xe;if(!g.value||A.value||T.value)return;const j=m.value,V={...g.value,allowed_hosts:[...g.value.allowed_hosts]};A.value=!0,w.value="",await Mt(),(xe=h.value)!=null&&xe.open&&h.value.focus();try{j==="default"?(i.value=V,await se.saveDefault(V)):(l.value[j]=V,o.value.includes(j)||o.value.push(j),await se.saveUser(j,V))}finally{A.value=!1}w.value||N()}function ce(j){var V,xe;return((V=d.value.overrides)==null?void 0:V[j])||((xe=d.value.config_tiers)==null?void 0:xe[j])||"default"}async function ue(j,V){var xe;try{if(V==="default"){if(!((xe=d.value.overrides)!=null&&xe[j]))return;await z.del(`/api/permissions/user/${encodeURIComponent(j)}`)}else await z.put(`/api/permissions/user/${encodeURIComponent(j)}`,{tier:V});V==="default"?delete d.value.overrides[j]:d.value.overrides[j]=V,ke.success("Permission tier updated")}catch(k){ke.error(k.message||"Failed to update tier"),await ye()}}async function Z(j){const V=f.value[j];if(V)try{await z.post(`/api/permissions/user/${encodeURIComponent(j)}/repair`,{tier:V}),delete d.value.invalid_overrides[j],d.value.overrides[j]=V}catch(xe){ke.error(xe.message||"Failed to repair tier")}}function J(j,V){return j?j.allowed_hosts===null||j.allowed_hosts===void 0?{allowed_hosts:[...V],default_host:j.default_host||"",allow_all:!0}:{allowed_hosts:j.allowed_hosts,default_host:j.default_host||"",allow_all:!1}:{allowed_hosts:[...V],default_host:V[0]||"",allow_all:!0}}const se=TC({applyDefault:async j=>{const V=j.allow_all?null:j.allowed_hosts;await z.put("/api/host-access/default-policy",{allowed_hosts:V,default_host:j.default_host})},applyUser:async(j,V)=>{const xe=V.allow_all?null:V.allowed_hosts;await z.put(`/api/host-access/user/${j}`,{allowed_hosts:xe,default_host:V.default_host})},applyDelete:j=>z.del(`/api/host-access/user/${j}`),onDefaultConfirmed:()=>ke.success("Default policy updated"),onDefaultRollback:j=>{j&&(i.value=j)},onUserConfirmed:j=>{const V=R(j);ke.success(`Updated access for ${V?V.display_name:j}`)},onUserRollback:(j,V)=>{const xe={...l.value};V?xe[j]=V:delete xe[j],l.value=xe,V||(o.value=o.value.filter(k=>k!==j))},onUserDeleted:j=>{const V={...l.value};delete V[j],l.value=V,o.value=o.value.filter(xe=>xe!==j)},onError:(j,V)=>{var k;A.value&&(w.value=j.message||"Failed to save. Your draft is retained; try again.");const xe=V.uid?` ${((k=R(V.uid))==null?void 0:k.display_name)||V.uid}`:"";ke.error(`${j.message||"Failed to save"} — reverted${xe}`)}});let q=0;async function ye(){const j=++q;e.value=!0,t.value="";try{const V=await se.readSnapshot(()=>z.get("/api/host-access"));if(j!==q)return;s.value=V,a.value=V.available_hosts||[],n.value=V.host_descriptions||{},i.value=J(V.default_policy,a.value);const xe=V.users||{};o.value=Object.keys(xe);const k={};for(const[D,W]of Object.entries(xe))k[D]=J(W,a.value);l.value=k,se.seed(i.value,k),d.value=await z.get("/api/permissions/tiers")}catch(V){j===q&&(t.value=V.message||"Failed to fetch host access data")}finally{j===q&&(e.value=!1)}try{const V=await z.get("/api/discord/members")||[];j===q&&(c.value=V)}catch{j===q&&(c.value=[])}}function Oe(){r.value=!0}function le(j){!/^\d{15,25}$/.test(j)||l.value[j]||(l.value[j]={allowed_hosts:[...a.value],default_host:a.value[0]||"",allow_all:!1},o.value.includes(j)||o.value.push(j),se.saveUser(j,l.value[j]),r.value=!1)}async function oe(j){const V=R(j);await os({title:"Remove user override",message:`Remove the host access override for ${V?V.display_name:j}? They will fall back to the default policy.`,confirmLabel:"Remove",danger:!0})&&(await se.deleteUser(j),l.value[j]||ke.success(`Removed override for ${V?V.display_name:j}`))}tt(ye);function ve(){var j;(j=h.value)==null||j.close(),g.value=null,m.value=""}return Kt(ve),xt(ve),{loading:e,error:t,data:s,availableHosts:a,hostDescriptions:n,defaultPolicy:i,users:l,showAddUser:r,members:c,hostOverrideIds:o,fetchData:ye,getMember:R,openAddUser:Oe,addUserById:le,deleteUser:oe,permissions:d,validTiers:u,repairTiers:f,repairTier:Z,tierValue:ce,setTier:ue,editor:m,hostQuery:b,userQuery:x,filteredHosts:S,allUserIds:L,visibleUserIds:E,hostSummary:I,hostChips:H,visibleHostChips:X,hostDialog:h,draft:g,saving:A,saveError:w,requiresDefault:y,defaultHostOptions:_,defaultHostError:T,openEditor:Q,closeEditor:N,saveEditor:$,trapEditorFocus:O}}},AC={template:` + `,setup(){const e=f(!0),t=f(""),s=f(null),a=f([]),n=f({}),i=f({allowed_hosts:[],default_host:""}),l=f({}),o=f([]),r=f(!1),c=f([]),d=f({overrides:{},config_tiers:{},invalid_overrides:{}}),u=["admin","user","guest"],p=f({}),m=f(""),h=f(null),y=f(null),k=f(!1),A=f(""),g=f(!1),b=f(""),x=f(""),w=B(()=>a.value.filter(j=>`${j} ${n.value[j]||""}`.toLowerCase().includes(b.value.toLowerCase()))),_=B(()=>a.value.filter(j=>{var Y,xe;return((Y=y.value)==null?void 0:Y.allow_all)||((xe=y.value)==null?void 0:xe.allowed_hosts.includes(j))})),T=B(()=>y.value?y.value.default_host&&!_.value.includes(y.value.default_host)?_.value.length?"Choose a new default from the allowed hosts before saving.":"No hosts are allowed. Select None for the default before saving.":g.value&&_.value.length&&!y.value.default_host?"Choose a new default from the allowed hosts before saving.":"":""),L=B(()=>[...new Set([...Object.keys(l.value),...Object.keys(d.value.overrides||{}),...Object.keys(d.value.config_tiers||{}),...Object.keys(d.value.invalid_overrides||{})])]),E=B(()=>L.value.filter(j=>{var Y,xe;return`${((Y=O(j))==null?void 0:Y.display_name)||""} ${((xe=O(j))==null?void 0:xe.username)||""} ${j}`.toLowerCase().includes(x.value.toLowerCase())})),F=B(()=>{const j={};for(const Y of c.value)j[Y.id]=Y;return j});function O(j){return F.value[j]||null}function I(j){if(!j||j.allow_all)return"All hosts";const Y=j.allowed_hosts||[];return Y.length>3?`${Y.slice(0,3).join(", ")} +${Y.length-3}`:Y.join(", ")||"No hosts"}function H(j){if(j.allow_all)return["All hosts"];const Y=j.allowed_hosts||[];return[...Y.slice(0,2),...Y.length>2?[`+${Y.length-2}`]:[],...Y.length?[]:["None"]]}function te(j){return o.value.includes(j)?H(l.value[j]):["Inherit policy"]}async function ee(j){var xe,S;if(k.value)return;const Y=j==="default"?i.value:l.value[j]||i.value;m.value=j,y.value={...Y,allowed_hosts:[...Y.allowed_hosts]},g.value=!!Y.default_host,b.value="",A.value="",await Ut(),y.value&&!((xe=h.value)!=null&&xe.open)&&((S=h.value)==null||S.showModal())}function N(){var j;k.value||((j=h.value)==null||j.close(),y.value=null,m.value="")}function M(j){if(j.key!=="Tab")return;const Y=[...h.value.querySelectorAll("button, input, select")].filter(P=>!P.matches(":disabled")),xe=Y[0],S=Y.at(-1);if(!xe){j.preventDefault();return}j.shiftKey&&document.activeElement===xe?(j.preventDefault(),S.focus()):!j.shiftKey&&document.activeElement===S&&(j.preventDefault(),xe.focus())}async function $(){var xe;if(!y.value||k.value||T.value)return;const j=m.value,Y={...y.value,allowed_hosts:[...y.value.allowed_hosts]};k.value=!0,A.value="",await Ut(),(xe=h.value)!=null&&xe.open&&h.value.focus();try{j==="default"?(i.value=Y,await ae.saveDefault(Y)):(l.value[j]=Y,o.value.includes(j)||o.value.push(j),await ae.saveUser(j,Y))}finally{k.value=!1}A.value||N()}function re(j){var Y,xe;return((Y=d.value.overrides)==null?void 0:Y[j])||((xe=d.value.config_tiers)==null?void 0:xe[j])||"default"}async function ce(j,Y){var xe;try{if(Y==="default"){if(!((xe=d.value.overrides)!=null&&xe[j]))return;await z.del(`/api/permissions/user/${encodeURIComponent(j)}`)}else await z.put(`/api/permissions/user/${encodeURIComponent(j)}`,{tier:Y});Y==="default"?delete d.value.overrides[j]:d.value.overrides[j]=Y,Se.success("Permission tier updated")}catch(S){Se.error(S.message||"Failed to update tier"),await be()}}async function Z(j){const Y=p.value[j];if(Y)try{await z.post(`/api/permissions/user/${encodeURIComponent(j)}/repair`,{tier:Y}),delete d.value.invalid_overrides[j],d.value.overrides[j]=Y}catch(xe){Se.error(xe.message||"Failed to repair tier")}}function Q(j,Y){return j?j.allowed_hosts===null||j.allowed_hosts===void 0?{allowed_hosts:[...Y],default_host:j.default_host||"",allow_all:!0}:{allowed_hosts:j.allowed_hosts,default_host:j.default_host||"",allow_all:!1}:{allowed_hosts:[...Y],default_host:Y[0]||"",allow_all:!0}}const ae=WC({applyDefault:async j=>{const Y=j.allow_all?null:j.allowed_hosts;await z.put("/api/host-access/default-policy",{allowed_hosts:Y,default_host:j.default_host})},applyUser:async(j,Y)=>{const xe=Y.allow_all?null:Y.allowed_hosts;await z.put(`/api/host-access/user/${j}`,{allowed_hosts:xe,default_host:Y.default_host})},applyDelete:j=>z.del(`/api/host-access/user/${j}`),onDefaultConfirmed:()=>Se.success("Default policy updated"),onDefaultRollback:j=>{j&&(i.value=j)},onUserConfirmed:j=>{const Y=O(j);Se.success(`Updated access for ${Y?Y.display_name:j}`)},onUserRollback:(j,Y)=>{const xe={...l.value};Y?xe[j]=Y:delete xe[j],l.value=xe,Y||(o.value=o.value.filter(S=>S!==j))},onUserDeleted:j=>{const Y={...l.value};delete Y[j],l.value=Y,o.value=o.value.filter(xe=>xe!==j)},onError:(j,Y)=>{var S;k.value&&(A.value=j.message||"Failed to save. Your draft is retained; try again.");const xe=Y.uid?` ${((S=O(Y.uid))==null?void 0:S.display_name)||Y.uid}`:"";Se.error(`${j.message||"Failed to save"} — reverted${xe}`)}});let V=0;async function be(){const j=++V;e.value=!0,t.value="";try{const Y=await ae.readSnapshot(()=>z.get("/api/host-access"));if(j!==V)return;s.value=Y,a.value=Y.available_hosts||[],n.value=Y.host_descriptions||{},i.value=Q(Y.default_policy,a.value);const xe=Y.users||{};o.value=Object.keys(xe);const S={};for(const[P,G]of Object.entries(xe))S[P]=Q(G,a.value);l.value=S,ae.seed(i.value,S),d.value=await z.get("/api/permissions/tiers")}catch(Y){j===V&&(t.value=Y.message||"Failed to fetch host access data")}finally{j===V&&(e.value=!1)}try{const Y=await z.get("/api/discord/members")||[];j===V&&(c.value=Y)}catch{j===V&&(c.value=[])}}function Oe(){r.value=!0}function le(j){!/^\d{15,25}$/.test(j)||l.value[j]||(l.value[j]={allowed_hosts:[...a.value],default_host:a.value[0]||"",allow_all:!1},o.value.includes(j)||o.value.push(j),ae.saveUser(j,l.value[j]),r.value=!1)}async function oe(j){const Y=O(j);await is({title:"Remove user override",message:`Remove the host access override for ${Y?Y.display_name:j}? They will fall back to the default policy.`,confirmLabel:"Remove",danger:!0})&&(await ae.deleteUser(j),l.value[j]||Se.success(`Removed override for ${Y?Y.display_name:j}`))}st(be);function ve(){var j;(j=h.value)==null||j.close(),y.value=null,m.value=""}return es(ve),wt(ve),{loading:e,error:t,data:s,availableHosts:a,hostDescriptions:n,defaultPolicy:i,users:l,showAddUser:r,members:c,hostOverrideIds:o,fetchData:be,getMember:O,openAddUser:Oe,addUserById:le,deleteUser:oe,permissions:d,validTiers:u,repairTiers:p,repairTier:Z,tierValue:re,setTier:ce,editor:m,hostQuery:b,userQuery:x,filteredHosts:w,allUserIds:L,visibleUserIds:E,hostSummary:I,hostChips:H,visibleHostChips:te,hostDialog:h,draft:y,saving:k,saveError:A,requiresDefault:g,defaultHostOptions:_,defaultHostError:T,openEditor:ee,closeEditor:N,saveEditor:$,trapEditorFocus:M}}},JC={template:`

Managed Hosts

Live inventory, pinned SSH trust, and connection health.

@@ -5228,8 +5236,8 @@ ${u.text}`:u.text).join(`

Activation is live. Users with allowed_hosts: null gain this host automatically. Review grants on Host Access after saving.

Open Host Access
-
`,setup(){const e=p([]),t=p(!1),s=p(""),a=p([]),n=p(!1),i=p(!1),l=p(1),o=p(""),r=p(!1),c=p(null),d=p(""),u=p([]),f=p(!1),m=p(null),h=p(""),g=()=>({alias:"",address:"",port:22,ssh_user:"root",os:"linux",description:"",trust_mode:"pinned",enabled:!0,confirm_local:!1,confirm_tofu:!1}),A=p(g()),w=U(()=>["127.0.0.1","localhost","::1"].includes(A.value.address));async function y(){t.value=!0,s.value="";try{const Q=await z.get("/api/hosts");e.value=Q.hosts||[],o.value=Q.default_host||"",r.value=!!Q.tofu_enabled}catch(Q){s.value=Q.message}finally{t.value=!1}}async function b(){try{await z.post("/api/hosts/settings",{default_host:o.value,allow_host_tofu:r.value}),ke.success("Host settings saved and published live"),await y()}catch(Q){ke.error(Q.message)}}function x(){d.value="",u.value=[],f.value=!1,m.value=null,c.value=null,h.value="",l.value=1,n.value=!0}function S(){i.value=!1,A.value=g(),x()}function _(Q){i.value=!0,A.value={...g(),...Q},x()}async function T(){try{c.value=await z.get("/api/hosts/public-key")}catch(Q){ke.error(Q.message)}}async function L(Q){try{const N=await z.post("/api/hosts/"+encodeURIComponent(Q.alias)+"/import-legacy",{});i.value=!0,A.value={...g(),...Q,trust_mode:"pinned"},x(),d.value=N.candidate_token,u.value=N.fingerprints||[],h.value=u.value.join(` -`),l.value=4,ke.info("Imported existing known_hosts trust. Test before activation.")}catch(N){ke.error(N.message)}}async function E(){try{const Q=h.value.split(/\s+/).filter(Boolean),N={...A.value,expected_fingerprints:Q,candidate_fingerprints:u.value},O=await z.post("/api/hosts/candidates",N);if(d.value=O.candidate_token,u.value=O.fingerprints||[],A.value.trust_mode==="tofu"&&N.candidate_fingerprints.length===0){A.value.confirm_tofu=!1,ke.info("Fingerprint scanned. Review it, tick confirmation, then scan again.");return}l.value=4}catch(Q){ke.error(Q.message)}}async function P(){var Q,N;f.value=!1,m.value=null;try{const O=await z.post("/api/hosts/candidates/"+d.value+"/test",{});f.value=!!O.tested,m.value=O.last_test,f.value&&(l.value=5)}catch(O){const $=(Q=O.data)==null?void 0:Q.last_test;$&&typeof $=="object"&&!Array.isArray($)&&(m.value=$);const ce=(N=m.value)==null?void 0:N.detail;ke.error(typeof ce=="string"&&ce.trim()?ce:O.message)}}async function R(){try{await z.post("/api/hosts/candidates/"+d.value+"/commit",{}),ke.success("Host saved and published live"),n.value=!1,await y()}catch(Q){ke.error(Q.message)}}async function I(Q){try{await z.post("/api/hosts/"+encodeURIComponent(Q.alias)+"/enabled",{enabled:!Q.enabled}),await y()}catch(N){ke.error(N.message)}}async function H(Q){var N;if(await os("Delete host "+Q.alias+"? Dependencies will block deletion.")){a.value=[];try{await z.del("/api/hosts/"+encodeURIComponent(Q.alias)),await y()}catch(O){a.value=Array.isArray((N=O.data)==null?void 0:N.pending_references)?O.data.pending_references:[],ke.error(O.message)}}}async function X(Q){if(await os("Force revoke "+Q.alias+"? Remote outcomes may be unknown."))try{await z.post("/api/hosts/"+encodeURIComponent(Q.alias)+"/force-revoke",{}),await y()}catch(N){ke.error(N.message)}}return tt(y),{hosts:e,loading:t,error:s,pendingReferences:a,wizard:n,editing:i,step:l,defaultHost:o,tofuEnabled:r,form:A,isLocal:w,keyInfo:c,candidate:d,observed:u,tested:f,testResult:m,fingerprintsText:h,load:y,saveSettings:b,beginAdd:S,beginEdit:_,loadKey:T,importLegacy:L,prepare:E,testConnection:P,commit:R,toggle:I,remove:H,forceRevoke:X}}},RC={components:{DiscordIdentity:En},template:` +
`,setup(){const e=f([]),t=f(!1),s=f(""),a=f([]),n=f(!1),i=f(!1),l=f(1),o=f(""),r=f(!1),c=f(null),d=f(""),u=f([]),p=f(!1),m=f(null),h=f(""),y=()=>({alias:"",address:"",port:22,ssh_user:"root",os:"linux",description:"",trust_mode:"pinned",enabled:!0,confirm_local:!1,confirm_tofu:!1}),k=f(y()),A=B(()=>["127.0.0.1","localhost","::1"].includes(k.value.address));async function g(){t.value=!0,s.value="";try{const ee=await z.get("/api/hosts");e.value=ee.hosts||[],o.value=ee.default_host||"",r.value=!!ee.tofu_enabled}catch(ee){s.value=ee.message}finally{t.value=!1}}async function b(){try{await z.post("/api/hosts/settings",{default_host:o.value,allow_host_tofu:r.value}),Se.success("Host settings saved and published live"),await g()}catch(ee){Se.error(ee.message)}}function x(){d.value="",u.value=[],p.value=!1,m.value=null,c.value=null,h.value="",l.value=1,n.value=!0}function w(){i.value=!1,k.value=y(),x()}function _(ee){i.value=!0,k.value={...y(),...ee},x()}async function T(){try{c.value=await z.get("/api/hosts/public-key")}catch(ee){Se.error(ee.message)}}async function L(ee){try{const N=await z.post("/api/hosts/"+encodeURIComponent(ee.alias)+"/import-legacy",{});i.value=!0,k.value={...y(),...ee,trust_mode:"pinned"},x(),d.value=N.candidate_token,u.value=N.fingerprints||[],h.value=u.value.join(` +`),l.value=4,Se.info("Imported existing known_hosts trust. Test before activation.")}catch(N){Se.error(N.message)}}async function E(){try{const ee=h.value.split(/\s+/).filter(Boolean),N={...k.value,expected_fingerprints:ee,candidate_fingerprints:u.value},M=await z.post("/api/hosts/candidates",N);if(d.value=M.candidate_token,u.value=M.fingerprints||[],k.value.trust_mode==="tofu"&&N.candidate_fingerprints.length===0){k.value.confirm_tofu=!1,Se.info("Fingerprint scanned. Review it, tick confirmation, then scan again.");return}l.value=4}catch(ee){Se.error(ee.message)}}async function F(){var ee,N;p.value=!1,m.value=null;try{const M=await z.post("/api/hosts/candidates/"+d.value+"/test",{});p.value=!!M.tested,m.value=M.last_test,p.value&&(l.value=5)}catch(M){const $=(ee=M.data)==null?void 0:ee.last_test;$&&typeof $=="object"&&!Array.isArray($)&&(m.value=$);const re=(N=m.value)==null?void 0:N.detail;Se.error(typeof re=="string"&&re.trim()?re:M.message)}}async function O(){try{await z.post("/api/hosts/candidates/"+d.value+"/commit",{}),Se.success("Host saved and published live"),n.value=!1,await g()}catch(ee){Se.error(ee.message)}}async function I(ee){try{await z.post("/api/hosts/"+encodeURIComponent(ee.alias)+"/enabled",{enabled:!ee.enabled}),await g()}catch(N){Se.error(N.message)}}async function H(ee){var N;if(await is("Delete host "+ee.alias+"? Dependencies will block deletion.")){a.value=[];try{await z.del("/api/hosts/"+encodeURIComponent(ee.alias)),await g()}catch(M){a.value=Array.isArray((N=M.data)==null?void 0:N.pending_references)?M.data.pending_references:[],Se.error(M.message)}}}async function te(ee){if(await is("Force revoke "+ee.alias+"? Remote outcomes may be unknown."))try{await z.post("/api/hosts/"+encodeURIComponent(ee.alias)+"/force-revoke",{}),await g()}catch(N){Se.error(N.message)}}return st(g),{hosts:e,loading:t,error:s,pendingReferences:a,wizard:n,editing:i,step:l,defaultHost:o,tofuEnabled:r,form:k,isLocal:A,keyInfo:c,candidate:d,observed:u,tested:p,testResult:m,fingerprintsText:h,load:g,saveSettings:b,beginAdd:w,beginEdit:_,loadKey:T,importLegacy:L,prepare:E,testConnection:F,commit:O,toggle:I,remove:H,forceRevoke:te}}},ZC={components:{DiscordIdentity:Dn},template:`

API Tokens

@@ -5482,7 +5490,7 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p(!0),t=p(""),s=p(null),a=p([]),n=p([]),i=p("missing"),l=p(!1),o=p(!1),r=p(null),c=p(null),d=p(!1);async function u(R){var I;try{await z._request("DELETE","/api/tokens/unusable/"+R.index,{reason:R.reason,...R.user_id===void 0?{}:{user_id:R.user_id}}),ke.success("Unusable token entry removed"),await w()}catch(H){ke.error(((I=H.data)==null?void 0:I.error)||H.message||"Failed to remove unusable token")}}const f=p({user_id:"",username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""}),m=p({username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""}),h=U(()=>f.value.host_mode==="select"?f.value.allowed_hosts:f.value.host_mode==="none"?[]:a.value),g=U(()=>m.value.host_mode==="select"?m.value.allowed_hosts:m.value.host_mode==="none"?[]:a.value);function A(R){return R==="admin"?"text-xs px-1.5 py-0.5 rounded bg-red-900/50 text-red-400":R==="user"?"text-xs px-1.5 py-0.5 rounded bg-blue-900/50 text-blue-400":"text-xs px-1.5 py-0.5 rounded bg-gray-700 text-gray-400"}async function w(){e.value=!0,t.value="";try{const R=await z.get("/api/tokens");s.value=R.tokens||[],a.value=R.available_hosts||[],n.value=R.invalid_entries||[],i.value=R.store_status||"missing"}catch(R){t.value=R.message||"Failed to load tokens"}finally{e.value=!1}}function y(R){return!R||!R.trim()?[]:R.split(",").map(I=>I.trim()).filter(Boolean)}function b(R,I){const H=f.value.allowed_hosts;if(I&&!H.includes(R)&&H.push(R),!I){const X=H.indexOf(R);X>=0&&H.splice(X,1)}}function x(R,I){const H=m.value.allowed_hosts;if(I&&!H.includes(R)&&H.push(R),!I){const X=H.indexOf(R);X>=0&&H.splice(X,1)}}async function S(){var R;o.value=!0;try{const I=y(f.value.allowed_tools_str),H=f.value.host_mode,X=H==="none"?[]:H==="select"?f.value.allowed_hosts:null,Q={user_id:f.value.user_id.trim(),username:f.value.username.trim()||"API",tier:f.value.tier,label:f.value.label.trim(),allowed_tools:I.length?I:[]};X!==null&&(Q.allowed_hosts=X),Q.default_host=f.value.default_host||"";const N=await z.post("/api/tokens",Q);r.value=N.token,f.value={user_id:"",username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""},l.value=!1,ke.success("Token created"),await w()}catch(I){ke.error(((R=I.data)==null?void 0:R.error)||I.message||"Failed to create token")}finally{o.value=!1}}function _(R){c.value=R;const I=R.allowed_hosts;let H="default";I==null?H="default":Array.isArray(I)&&I.length===0?H="none":Array.isArray(I)&&(H="select"),m.value={username:R.username||"",tier:R.tier||"admin",label:R.label||"",host_mode:H,allowed_hosts:Array.isArray(I)?[...I]:[],default_host:R.default_host||"",allowed_tools_str:(R.allowed_tools||[]).join(", ")}}async function T(){var R;if(c.value){d.value=!0;try{const I=y(m.value.allowed_tools_str),H=m.value.host_mode,X={username:m.value.username,tier:m.value.tier,label:m.value.label,allowed_tools:I};H==="none"?X.allowed_hosts=[]:H==="select"?X.allowed_hosts=m.value.allowed_hosts:X.allowed_hosts=null,X.default_host=m.value.default_host||"",await z.put("/api/tokens/"+encodeURIComponent(c.value.user_id),X),c.value=null,ke.success("Token updated"),await w()}catch(I){ke.error(((R=I.data)==null?void 0:R.error)||I.message||"Failed to update")}finally{d.value=!1}}}async function L(R){var H;if(await os({title:"Regenerate token",message:`Regenerate token for ${R.username||R.user_id}? The old token will stop working immediately.`,confirmLabel:"Regenerate",danger:!0}))try{const X=await z.post("/api/tokens/"+encodeURIComponent(R.user_id)+"/regenerate");r.value=X.token,ke.success("Token regenerated")}catch(X){ke.error(((H=X.data)==null?void 0:H.error)||X.message||"Failed to regenerate")}}async function E(R){var H;if(await os({title:"Delete token",message:`Delete token for ${R.username||R.user_id}? This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await z.del("/api/tokens/"+encodeURIComponent(R.user_id)),ke.success("Token deleted"),await w()}catch(X){ke.error(((H=X.data)==null?void 0:H.error)||X.message||"Failed to delete")}}async function P(){if(r.value)try{await navigator.clipboard.writeText(r.value),ke.success("Copied to clipboard")}catch{ke.error("Copy failed — select and copy manually")}}return tt(w),{loading:e,error:t,tokens:s,availableHosts:a,invalidEntries:n,storeStatus:i,showCreate:l,creating:o,newToken:r,editing:c,saving:d,createForm:f,editForm:m,removeUnusable:u,createDefaultHostOptions:h,editDefaultHostOptions:g,fetchData:w,tierBadge:A,toggleCreateHost:b,toggleEditHost:x,createToken:S,startEdit:_,saveEdit:T,confirmRegenerate:L,confirmDelete:E,copyToken:P}}};function IC(e,t=()=>"unknown"){const s=e==null?void 0:e.quota;return!s||typeof s!="object"?[]:["primary","secondary"].flatMap(a=>{const n=s[a],i=Number(n==null?void 0:n.used_percent),l=Number(n==null?void 0:n.window_minutes);if(!n||!Number.isFinite(i)||i<0||!Number.isFinite(l)||l<=0)return[];let o;return l===300?o="5-hour usage limit":l===10080?o="Weekly usage limit":Number.isFinite(l)&&l>0?o=l<60?`${l}-minute usage limit`:l<1440?`${Math.round(l/60)}-hour usage limit`:`${Math.round(l/1440)}-day usage limit`:o=a==="primary"?"Primary usage limit":"Secondary usage limit",[{key:a,label:o,remaining:Math.round(Math.max(0,Math.min(100,100-i))),limitReached:i>=100,statusLabel:i>=100?"Limit reached":"",resetLabel:n.resets_at==null?"unknown":t(n.resets_at)}]})}function OC(e){return!!(e!=null&&e.quota_check_failed)}const LC=Object.freeze(["enabled","model","reasoning_effort","agent_reasoning_effort"]),NC=Object.freeze(["request_timeout_seconds","stream_stall_timeout_seconds","retry","connection_pool","context_compression","context_budget_overrides","context_utilization"]),MC=Object.freeze(["enabled","base_url","model","max_tokens","num_ctx"]),DC=Object.freeze(["enabled","base_url","model","reasoning_effort"]);function fo(e,t){return Object.fromEntries(t.map(s=>[s,e[s]]))}function Bo(e,t={}){const s=fo(e,DC);return t.includeApiKey&&(s.api_key=e.api_key),s}function zc(e){return fo(e,["request_timeout_seconds","stream_stall_timeout_seconds","preset","model_profiles","context_utilization","openrouter"])}function Uo(e){return fo(e,LC)}function zo(e){return fo(e,NC)}function Ho(e,{includeApiKey:t=!1}={}){const s=fo(e,MC);return t&&(s.api_key=e.api_key),s}function Hc(e){return{timeout:e.timeout}}function jo(e,t=500){let s=null;const a=(...n)=>{s&&clearTimeout(s),s=setTimeout(()=>{s=null,e(...n)},t)};return a.pending=()=>s!==null,a.cancel=()=>{s&&(clearTimeout(s),s=null)},a}const PC={template:` + `,setup(){const e=f(!0),t=f(""),s=f(null),a=f([]),n=f([]),i=f("missing"),l=f(!1),o=f(!1),r=f(null),c=f(null),d=f(!1);async function u(O){var I;try{await z._request("DELETE","/api/tokens/unusable/"+O.index,{reason:O.reason,...O.user_id===void 0?{}:{user_id:O.user_id}}),Se.success("Unusable token entry removed"),await A()}catch(H){Se.error(((I=H.data)==null?void 0:I.error)||H.message||"Failed to remove unusable token")}}const p=f({user_id:"",username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""}),m=f({username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""}),h=B(()=>p.value.host_mode==="select"?p.value.allowed_hosts:p.value.host_mode==="none"?[]:a.value),y=B(()=>m.value.host_mode==="select"?m.value.allowed_hosts:m.value.host_mode==="none"?[]:a.value);function k(O){return O==="admin"?"text-xs px-1.5 py-0.5 rounded bg-red-900/50 text-red-400":O==="user"?"text-xs px-1.5 py-0.5 rounded bg-blue-900/50 text-blue-400":"text-xs px-1.5 py-0.5 rounded bg-gray-700 text-gray-400"}async function A(){e.value=!0,t.value="";try{const O=await z.get("/api/tokens");s.value=O.tokens||[],a.value=O.available_hosts||[],n.value=O.invalid_entries||[],i.value=O.store_status||"missing"}catch(O){t.value=O.message||"Failed to load tokens"}finally{e.value=!1}}function g(O){return!O||!O.trim()?[]:O.split(",").map(I=>I.trim()).filter(Boolean)}function b(O,I){const H=p.value.allowed_hosts;if(I&&!H.includes(O)&&H.push(O),!I){const te=H.indexOf(O);te>=0&&H.splice(te,1)}}function x(O,I){const H=m.value.allowed_hosts;if(I&&!H.includes(O)&&H.push(O),!I){const te=H.indexOf(O);te>=0&&H.splice(te,1)}}async function w(){var O;o.value=!0;try{const I=g(p.value.allowed_tools_str),H=p.value.host_mode,te=H==="none"?[]:H==="select"?p.value.allowed_hosts:null,ee={user_id:p.value.user_id.trim(),username:p.value.username.trim()||"API",tier:p.value.tier,label:p.value.label.trim(),allowed_tools:I.length?I:[]};te!==null&&(ee.allowed_hosts=te),ee.default_host=p.value.default_host||"";const N=await z.post("/api/tokens",ee);r.value=N.token,p.value={user_id:"",username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""},l.value=!1,Se.success("Token created"),await A()}catch(I){Se.error(((O=I.data)==null?void 0:O.error)||I.message||"Failed to create token")}finally{o.value=!1}}function _(O){c.value=O;const I=O.allowed_hosts;let H="default";I==null?H="default":Array.isArray(I)&&I.length===0?H="none":Array.isArray(I)&&(H="select"),m.value={username:O.username||"",tier:O.tier||"admin",label:O.label||"",host_mode:H,allowed_hosts:Array.isArray(I)?[...I]:[],default_host:O.default_host||"",allowed_tools_str:(O.allowed_tools||[]).join(", ")}}async function T(){var O;if(c.value){d.value=!0;try{const I=g(m.value.allowed_tools_str),H=m.value.host_mode,te={username:m.value.username,tier:m.value.tier,label:m.value.label,allowed_tools:I};H==="none"?te.allowed_hosts=[]:H==="select"?te.allowed_hosts=m.value.allowed_hosts:te.allowed_hosts=null,te.default_host=m.value.default_host||"",await z.put("/api/tokens/"+encodeURIComponent(c.value.user_id),te),c.value=null,Se.success("Token updated"),await A()}catch(I){Se.error(((O=I.data)==null?void 0:O.error)||I.message||"Failed to update")}finally{d.value=!1}}}async function L(O){var H;if(await is({title:"Regenerate token",message:`Regenerate token for ${O.username||O.user_id}? The old token will stop working immediately.`,confirmLabel:"Regenerate",danger:!0}))try{const te=await z.post("/api/tokens/"+encodeURIComponent(O.user_id)+"/regenerate");r.value=te.token,Se.success("Token regenerated")}catch(te){Se.error(((H=te.data)==null?void 0:H.error)||te.message||"Failed to regenerate")}}async function E(O){var H;if(await is({title:"Delete token",message:`Delete token for ${O.username||O.user_id}? This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await z.del("/api/tokens/"+encodeURIComponent(O.user_id)),Se.success("Token deleted"),await A()}catch(te){Se.error(((H=te.data)==null?void 0:H.error)||te.message||"Failed to delete")}}async function F(){if(r.value)try{await navigator.clipboard.writeText(r.value),Se.success("Copied to clipboard")}catch{Se.error("Copy failed — select and copy manually")}}return st(A),{loading:e,error:t,tokens:s,availableHosts:a,invalidEntries:n,storeStatus:i,showCreate:l,creating:o,newToken:r,editing:c,saving:d,createForm:p,editForm:m,removeUnusable:u,createDefaultHostOptions:h,editDefaultHostOptions:y,fetchData:A,tierBadge:k,toggleCreateHost:b,toggleEditHost:x,createToken:w,startEdit:_,saveEdit:T,confirmRegenerate:L,confirmDelete:E,copyToken:F}}};function QC(e,t=()=>"unknown"){const s=e==null?void 0:e.quota;return!s||typeof s!="object"?[]:["primary","secondary"].flatMap(a=>{const n=s[a],i=Number(n==null?void 0:n.used_percent),l=Number(n==null?void 0:n.window_minutes);if(!n||!Number.isFinite(i)||i<0||!Number.isFinite(l)||l<=0)return[];let o;return l===300?o="5-hour usage limit":l===10080?o="Weekly usage limit":Number.isFinite(l)&&l>0?o=l<60?`${l}-minute usage limit`:l<1440?`${Math.round(l/60)}-hour usage limit`:`${Math.round(l/1440)}-day usage limit`:o=a==="primary"?"Primary usage limit":"Secondary usage limit",[{key:a,label:o,remaining:Math.round(Math.max(0,Math.min(100,100-i))),limitReached:i>=100,statusLabel:i>=100?"Limit reached":"",resetLabel:n.resets_at==null?"unknown":t(n.resets_at)}]})}function YC(e){return!!(e!=null&&e.quota_check_failed)}const XC=Object.freeze(["enabled","model","reasoning_effort","agent_reasoning_effort"]),eT=Object.freeze(["request_timeout_seconds","stream_stall_timeout_seconds","retry","connection_pool","context_compression","context_budget_overrides","context_utilization"]),tT=Object.freeze(["enabled","base_url","model","max_tokens","num_ctx"]),sT=Object.freeze(["enabled","base_url","model","reasoning_effort"]);function yo(e,t){return Object.fromEntries(t.map(s=>[s,e[s]]))}function bl(e,t={}){const s=yo(e,sT);return t.includeApiKey&&(s.api_key=e.api_key),s}function Ko(e){return yo(e,["request_timeout_seconds","stream_stall_timeout_seconds","preset","model_profiles","context_utilization","openrouter"])}function yl(e){return yo(e,XC)}function xl(e){return yo(e,eT)}function _l(e,{includeApiKey:t=!1}={}){const s=yo(e,tT);return t&&(s.api_key=e.api_key),s}function Jo(e){return{timeout:e.timeout}}function Zo(e,t=500){let s=null;const a=(...n)=>{s&&clearTimeout(s),s=setTimeout(()=>{s=null,e(...n)},t)};return a.pending=()=>s!==null,a.cancel=()=>{s&&(clearTimeout(s),s=null)},a}const aT={template:`
@@ -5507,9 +5515,9 @@ ${u.text}`:u.text).join(`

Model Selection

-

Choose models, not a provider. Disabled or unreachable catalogue entries remain visible.

+

Enabled providers supply model choices. Unavailable saved selections are retained until you choose another model.

-
+
@@ -5517,13 +5525,16 @@ ${u.text}`:u.text).join(`
+

Main model is configured on a disabled provider. Enable that provider below or choose another model.

+

Agent model is configured on a disabled provider. Enable that provider below or choose another model.

+

Auxiliary model is configured on a disabled provider. Enable that provider below or choose another model.

Used for compaction, reflection, consolidation, and background follow-up. Its output feeds sessions and memory, so choose deliberately.

@@ -6218,7 +6235,7 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p(!0),t=p(null),s=15e3;let a=null,n=!1,i=null,l=null;const o={codexBasic:null,codexAdvanced:null,ollamaBasic:null,ollamaAdvanced:null,compatibleBasic:null,compatibleAdvanced:null,auxiliary:null,mainModel:null,agents:null},r=v=>JSON.stringify(v),c=(v,F)=>{o[v]=r(F)},d=(v,F)=>o[v]!==null&&o[v]!==r(F),u=p(null),f=p(!1),m=p({main:"",main_capability:"medium",agent_capability:"adaptive"}),h=p(""),g=["none","low","medium","high","xhigh","max"],A=["none","low","medium","high","xhigh","max"],w=p({enabled:!1,model:"gpt-6-sol",reasoning_effort:"xhigh",agent_reasoning_effort:"auto",request_timeout_seconds:3600,stream_stall_timeout_seconds:180,retry:{max_retries:3,base_delay:1,max_delay:30},connection_pool:{max_connections:10,keepalive_timeout:30},context_compression:{enabled:!0,max_context_chars:null,keep_recent_iterations:30},context_budget_overrides:{},context_utilization:60}),y=["gpt-6-astra","gpt-6-sol","gpt-6-luna","gpt-5.6-sol","gpt-5.6-terra","gpt-5.6-luna"],b=U(()=>{const v=u.value||{},F=v.model_catalogue||v.model_catalog||{},ee=Be=>{var Xt,De,Bt;return((Xt=be.value.model_profiles)==null?void 0:Xt[Be])||((Bt=(De=be.value.openrouter)==null?void 0:De.catalogue_profiles)==null?void 0:Bt[Be])||null},Ee=(Be,Xt,De)=>Xt.map(Bt=>{var ap,np;const Ut=typeof Bt=="string"?Bt:Bt.name,Gs=Be==="compat"?ee(Ut):null;return{ref:Be==="codex"?Bt:`${Be}:${Ut}`,name:Ut,provider:Be,available:!!(De!=null&&De.enabled&&(Be==="codex"?De.configured:(ap=De.health)!=null&&ap.healthy)),unavailable_reason:De!=null&&De.enabled?De!=null&&De.configured?!((np=De==null?void 0:De.health)!=null&&np.healthy)&&Be!=="codex"?"unreachable":"":"not configured":"disabled",capability:Be==="codex"||Gs!=null&&Gs.supports_reasoning?"reasoning":Gs!=null&&Gs.supports_thinking_mode?"thinking":"none",efforts:Gs==null?void 0:Gs.supported_efforts,profile:Gs}}),Pe=[...F.codex||Ee("codex",y,v.codex),...F.compat||F.openai_compatible||[],...F.ollama||Ee("ollama",ae.value,v.ollama)].map(Be=>typeof Be=="string"?{ref:Be,name:Be,provider:"codex",available:!0,capability:"reasoning"}:Be),ct=Be=>{const Xt=Pe.findIndex(De=>De.ref===Be.ref);Xt===-1?Pe.push(Be):Pe[Xt]={...Pe[Xt],...Be}};for(const Be of Ee("compat",gt.value,v.openai_compatible))ct(Be);if(ns.value&&Ge.value.length)for(const Be of Ge.value){const Xt=`compat:${Be.id}`;ct({ref:Xt,name:Be.name||Be.id,provider:"compat",available:!0,unavailable_reason:"",capability:Be.supports_reasoning?"reasoning":"none",efforts:g.filter(De=>{var Bt;return(Bt=Be.supported_efforts)==null?void 0:Bt.includes(De)}),agent_available:Be.agent_eligible&&!!Be.profile,agent_unavailable_reason:Be.agent_unavailable_reason||""})}const Ts=new Set(Pe.map(Be=>Be.ref));for(const Be of[m.value.main,Ze.value.model,...Yt.value])Be&&Be!=="auto"&&!Ts.has(Be)&&Pe.unshift({ref:Be,name:Be.replace(/^(compat|ollama):/,""),provider:Be.split(":")[0]||"codex",available:!1,unavailable_reason:"unavailable",capability:Be.startsWith("ollama:")||Be.includes(":")?"none":"reasoning"});return Pe.filter(Be=>!ns.value||Be.provider!=="compat"||Be.ref.slice(7).includes("/")).map(Be=>Be.efforts?{...Be,efforts:g.filter(Xt=>Be.efforts.includes(Xt)&&!q(Be.ref,Xt))}:Be)}),x=U(()=>[["codex","Codex"],["compat","OpenAI-compatible"],["ollama","Ollama"]].map(([v,F])=>({id:v,label:F,models:b.value.filter(ee=>{if(ee.provider!==v)return!1;const Ee=h.value.trim().toLowerCase();return!Ee||`${ee.name} ${ee.ref}`.toLowerCase().includes(Ee)})})).filter(v=>v.models.length)),S=v=>v.available&&v.agent_available!==!1,_=v=>v.available?`${v.name}${v.agent_available===!1?` (${v.agent_unavailable_reason||"not agent-eligible"})`:""}`:ue(v),T=U(()=>[["codex","Codex"],["compat","OpenAI-compatible"],["ollama","Ollama"]].map(([v,F])=>({id:v,label:F,models:b.value.filter(ee=>ee.provider===v&&!(v==="compat"&&ns.value)&&`${ee.ref} ${ee.name}`.toLowerCase().includes(kt.value.trim().toLowerCase()))})).filter(v=>v.models.length)),L=U(()=>b.value.find(v=>v.ref===m.value.main)),E=U(()=>b.value.find(v=>v.ref===Ze.value.model)),P=v=>b.value.find(F=>F.ref===v),R=v=>{const F=P(v);return(F==null?void 0:F.provider)==="codex"?g.filter(ee=>!q(v,ee)):(F==null?void 0:F.efforts)||[]},I=v=>gs.value.find(F=>Ms(F)===v),H=(v,F)=>{const ee=I(v);return typeof ee=="string"?"":(ee==null?void 0:ee[F])||""},X=U(()=>Yt.value.map(v=>P(v)).filter(v=>v&&S(v))),Q=U(()=>[...new Set(X.value.map(v=>v.capability!=="reasoning"?v.capability||"none":v.provider==="codex"?"codex_reasoning":"compatible_reasoning"))]),N=U(()=>Q.value.length>1),O=U(()=>{var F;if(Ze.value.model!=="auto")return((F=E.value)==null?void 0:F.capability)||"none";if(N.value)return"mixed";const v=Q.value[0]||"none";return v.endsWith("_reasoning")?"reasoning":v}),$=U(()=>{var v;return Ze.value.model==="auto"?g:((v=E.value)==null?void 0:v.efforts)||g}),ce=U(()=>O.value==="thinking"?Ze.value.thinking_mode??w.value.agent_reasoning_effort??"":w.value.agent_reasoning_effort??""),ue=v=>`${v.name}${v.available?"":` (${v.unavailable_reason||"unavailable"})`}`,Z=U(()=>{const v=w.value.model;return v&&!y.includes(v)?[v,...y]:y}),J=U(()=>{const v=Ze.value.model;return v&&v!=="auto"&&!y.includes(v)?[v,...y]:y}),se={"gpt-5.4":["max"],"gpt-5.4-mini":["max"],"gpt-6-astra":["none"]},q=(v,F)=>!!v&&!!F&&(se[v]||[]).includes(F),ye=U(()=>{const v=Ze.value.model;return v&&!v.includes(":")?v:null}),Oe=v=>!q(w.value.model,v)&&!(w.value.agent_reasoning_effort===""&&q(ye.value,v)),le=v=>{const F=Ze.value.model;return F==="auto"?!0:!q(F||w.value.model,v)},oe=U(()=>{const v=w.value.agent_reasoning_effort;return v==="auto"?null:v||w.value.reasoning_effort}),ve=v=>q(v,w.value.reasoning_effort)||Ze.value.model===""&&q(v,oe.value),j=v=>q(v,oe.value),V=p({enabled:!1,model:"gpt-6-luna"}),xe=p({unavailable_reason:null}),k=U(()=>{const v=V.value.model;return v&&!y.includes(v)?[v,...y]:y});function D(v){const F=v.target.value;V.value.enabled=F!=="",F!==""&&(V.value.model=F),F.startsWith("compat:")&&ns.value?Pa(Te(F),Je(F)).then(()=>Dn()).catch(ee=>ze(ee.message||"Failed to prepare OpenRouter model","error")):Dn()}const W=p(!1),ge=p({codex:!1,ollama:!1,compatible:!1}),pe=p(null),he=p(!1),we=p(""),K=p(null),ne=p(!1);let te=0;const _e=U(()=>{var v;return Object.entries(((v=pe.value)==null?void 0:v.models)||{}).map(([F,ee])=>{var Ee,Pe,ct;return{model:F,floor:ee.floor,override:ee.override,effectiveBudget:(Ee=ee.effective)==null?void 0:Ee.effective_budget,configuredPrimaryChars:(Pe=ee.configured)==null?void 0:Pe.primary_chars,primaryChars:(ct=ee.effective)==null?void 0:ct.primary_chars,provenance:ee.provenance,clampExpiresAt:ee.clamp_expires_at,densityPriorMilli:ee.density_prior_milli,densityScope:ee.density_scope,workloadCalibration:ee.workload_calibration}})}),B=U(()=>{var v;return((v=pe.value)==null?void 0:v.clamps)||[]}),fe=U(()=>{var v,F;return((F=(v=pe.value)==null?void 0:v.models)==null?void 0:F[w.value.model])||null}),de=p({enabled:!1,base_url:"",model:"",api_key:"",max_tokens:4096,num_ctx:32768,timeout:300}),be=p({enabled:!1,base_url:"https://api.deepseek.com/v1",api_key:"",model:"deepseek-v4-flash",request_timeout_seconds:3600,stream_stall_timeout_seconds:180,preset:"deepseek",reasoning_effort:"medium",model_profiles:{},context_utilization:75,openrouter:{order:[],allow_fallbacks:!0,quantizations:[],sort:null,data_collection:null,reasoning_effort:"medium",model_pins:{},catalogue_profiles:{}}}),Fe=p(!1),qe=p(!1),$e=p(!1),Ke=p(!1),at=p(!1),Xe=p(!1);function Y(){var F,ee,Ee;return!!(((F=t.value)==null?void 0:F.contains(document.activeElement))&&((Ee=(ee=document.activeElement)==null?void 0:ee.matches)==null?void 0:Ee.call(ee,'input, textarea, select, [contenteditable="true"]'))||ra.value||go.value!==null||ne.value||Fe.value||qe.value||vs.value||$e.value||Ke.value||at.value||W.value||mi.pending()||hi.pending()||Pn.pending()||Dn.pending()||d("codexBasic",Uo(w.value))||d("codexAdvanced",zo(w.value))||d("ollamaBasic",Ho(de.value))||d("ollamaAdvanced",Hc(de.value))||d("compatibleBasic",Bo(be.value))||d("compatibleAdvanced",zc(be.value))||d("auxiliary",V.value)||d("mainModel",m.value.main)||d("agents",Ze.value))}function Ae(){o.codexBasic=r(Uo(w.value)),o.codexAdvanced=r(zo(w.value)),o.ollamaBasic=r(Ho(de.value)),o.ollamaAdvanced=r(Hc(de.value)),o.compatibleBasic=r(Bo(be.value)),o.compatibleAdvanced=r(zc(be.value)),o.auxiliary=r(V.value),o.mainModel=r(m.value.main)}const Re=p({configured:null}),Me=p(!1),ae=p([]),Ne=p(""),He=p(!1),rt=p(!1),Zt=p({configured:null}),lt=p(!1),gt=p([]),Qt=U(()=>ns.value?Ge.value.map(v=>v.id):gt.value),cs=p(""),oa=p(!1),js=p(!1),Ze=p({model:"auto",thinking_mode:null,auto_model_allowlist:[]}),ra=p(!1),vs=p(!1),Ms=v=>typeof v=="string"?v:v==null?void 0:v.model,gs=U(()=>(Ze.value.auto_model_allowlist||[]).map(v=>typeof v=="string"?v:{...v}).filter(v=>Ms(v))),An=U(()=>!w.value.enabled&&be.value.enabled?be.value.model?[`compat:${be.value.model}`]:[]:!w.value.enabled&&!be.value.enabled&&de.value.enabled?de.value.model?[`ollama:${de.value.model}`]:[]:y),Yt=U(()=>gs.value.length?gs.value.map(Ms):An.value),_t=U(()=>{var v;return(v=Ze.value.auto_model_allowlist)!=null&&v.length?`Allowlist: ${Yt.value.length} models`:`Default: ${Yt.value.join(", ")||"no available agent models"}`}),ds=new Map;function ba(){ra.value=!1,G()}const rn=v=>{if(ns.value&&v.startsWith("compat:")&&!v.slice(7).includes("/"))return"OpenRouter requires a namespaced vendor/model ID; this is a direct-endpoint profile.";const F=b.value.find(ee=>ee.ref===v);return F?S(F)?"":F.agent_unavailable_reason||F.unavailable_reason||"Not agent-eligible":"Model is absent from the current endpoint catalogue."},ca=v=>{if(Et.value.has(v))return bs(v);const F=b.value.find(Ee=>Ee.ref===v),ee=(F==null?void 0:F.hint_metadata)||{};return[ee.hint||ee.hint_derived,ee.as_of&&`as of ${ee.as_of}`,ee.scope_note,ee.evidence&&`Evidence: ${ee.evidence}`,F&&Vs(F)].filter(Boolean).join(" · ")||"No catalogue hint. Add an operator hint below."},re=p(null),Ce=p(!1),Ve=p(""),Ge=U(()=>{var v;return(((v=re.value)==null?void 0:v.models)||[]).map(F=>{var Ee,Pe,ct;const ee=((Ee=be.value.model_profiles)==null?void 0:Ee[F.id])||F.profile||((ct=(Pe=be.value.openrouter)==null?void 0:Pe.catalogue_profiles)==null?void 0:ct[F.id])||(F.context_length>0&&F.max_completion_tokens>0?{total_window_tokens:F.context_length,max_output_tokens:F.max_completion_tokens}:null);return{...F,profile:ee}})}),kt=p(""),St=p(""),cn=p(!0),Rn=p(!0),dn=p(!0),In=p(!1),da=p(null),ya=p(""),qt=p(null),ua=p(""),as=p([]),un=p(!1),On=p("throughput"),oi=(v,F,ee=null)=>{const Ee=v[F],Pe=ee&&Ee&&typeof Ee=="object"?Ee[ee]:Ee,ct=Number(Pe);return Number.isFinite(ct)?ct:null},ri=U(()=>{const v=[...as.value],F=On.value;return v.sort((ee,Ee)=>{var Ut,Gs;if(F==="quantization")return String(ee.quantization).localeCompare(String(Ee.quantization));const[Pe,ct,Ts]=F==="throughput"?["throughput_last_30m","p50",!0]:F==="latency_p99"?["latency_last_30m","p99",!1]:F==="cache_price"?["cache_read_per_token",null,!1]:["prompt_per_token",null,!1],Be=(Ut=ee.pricing)==null?void 0:Ut[Pe],Xt=(Gs=Ee.pricing)==null?void 0:Gs[Pe],De=ct?oi(ee,Pe,ct):Be==null?null:Number(Be),Bt=ct?oi(Ee,Pe,ct):Xt==null?null:Number(Xt);return Number.isFinite(De)?Number.isFinite(Bt)?Ts?Bt-De:De-Bt:-1:1})}),ci=U(()=>{try{return new URL(be.value.base_url).hostname}catch{return""}}),ns=U(()=>/(^|\.)openrouter\.ai$/i.test(ci.value)),Ln=U(()=>{var v,F;return ns.value?Ce.value?"OpenRouter recognized · fetching catalogue…":Ve.value?`OpenRouter recognized · catalogue failed: ${Ve.value}`:`OpenRouter recognized · ${((F=(v=re.value)==null?void 0:v.models)==null?void 0:F.length)||0} catalogue models loaded`:gt.value.length?`${gt.value.length} endpoint models loaded`:"Catalogue not loaded"}),Ie=U(()=>Ve.value?"text-red-400":"text-gray-500"),M=U(()=>{var v;return[...new Set((((v=re.value)==null?void 0:v.models)||[]).map(F=>F.vendor))].sort()}),me=U(()=>{var v;return[...new Set((((v=re.value)==null?void 0:v.models)||[]).flatMap(F=>(F.endpoints||[]).map(ee=>ee.quantization)).filter(Boolean))].sort()}),Se=v=>{var F;return(((F=re.value)==null?void 0:F.measured_cache)||[]).some(ee=>ee.model===v.id&&ee.samples>0&&ee.cached_percent>0)},Ue=U(()=>{const v=kt.value.trim().toLowerCase();return Ge.value.filter(F=>{var ee;return!(v&&!`${F.id} ${F.name} ${F.vendor}`.toLowerCase().includes(v)||St.value&&F.vendor!==St.value||cn.value&&!F.supports_tools||Rn.value&&!F.agent_eligible||dn.value&&F.variant!=="standard"||In.value&&!Se(F)||da.value!=null&&Number((ee=F.pricing)==null?void 0:ee.prompt_per_token)*1e6>Number(da.value)||ya.value&&!(F.endpoints||[]).some(Ee=>Ee.quantization===ya.value))})}),We=U(()=>Ue.value.slice(0,100)),Ye=U(()=>Ue.value.length),Et=U(()=>new Map(Ge.value.map(v=>[`compat:${v.id}`,v]))),pt=v=>v==null?"n/a":`$${(Number(v)*1e6).toFixed(3)}/M`,At=v=>{var F,ee,Ee,Pe;return[v.vendor,v.context_length?`${Number(v.context_length).toLocaleString()} ctx`:"context unknown",`${pt((F=v.pricing)==null?void 0:F.prompt_per_token)} in`,`${pt((ee=v.pricing)==null?void 0:ee.completion_per_token)} out`,`${pt((Ee=v.pricing)==null?void 0:Ee.cache_read_per_token)} cache read`,`${pt((Pe=v.pricing)==null?void 0:Pe.cache_write_per_token)} cache write`,v.supports_tools?"tools":"no tools",v.supports_reasoning?"reasoning":"no reasoning",v.variant!=="standard"?v.variant:null].filter(Boolean).join(" · ")},bs=v=>{var Pe;const F=Et.value.get(v);if(!F)return"Catalogue facts unavailable";const ee=(((Pe=re.value)==null?void 0:Pe.measured_cache)||[]).filter(ct=>ct.model===F.id),Ee=ee.length?ee.map(ct=>`${ct.upstream_provider}: ${ct.cached_percent}% cached`).join(" · "):"No measured cache evidence yet";return`${At(F)} · ${Ee}${F.profile_conflict?" · operator profile conflicts with catalogue":""}`},Nt=U(()=>gt.value.map(v=>typeof v=="string"?v:v.name).filter(Boolean)),Na=async()=>{var F,ee,Ee;const v=(Ee=(ee=(F=u.value)==null?void 0:F.openai_compatible)==null?void 0:ee.preset_catalogue)==null?void 0:Ee[be.value.preset];v&&(be.value.base_url=v.base_url),Pn.cancel(),await rl()},di=(v,F)=>{be.value.openrouter[v]=F.split(",").map(ee=>ee.trim()).filter(Boolean)},ho=U(()=>ae.value||[]),Ma=U(()=>{const v=[...y,...Nt.value.map(F=>`compat:${F}`),...ho.value.map(F=>`ollama:${F.name}`)];for(const F of[Ze.value.model,...Yt.value])F&&F!=="auto"&&!v.includes(F)&&v.unshift(F);return v}),rc=v=>v==="codex-auto-review"?"codex-auto-review (Codex alias → gpt-5.6-luna)":v;async function vo({poll:v=!1}={}){try{const F=await z.get("/api/agents/model");if(v&&Y())return;Ze.value={...Ze.value,...F},o.agents=r(Ze.value)}catch{}}async function ui(){if(!ns.value){re.value=null,Ve.value="";return}Ce.value=!0;try{re.value=await z.get("/api/openrouter/catalogue"),Ve.value=""}catch(v){Ve.value=v.message||"Failed to load OpenRouter catalogue"}finally{Ce.value=!1}}async function cc(){var v;try{(v=Ze.value.model)!=null&&v.startsWith("compat:")&&ns.value&&await Pa(Te(Ze.value.model),Je(Ze.value.model));const F=await z.put("/api/agents/model",{model:Ze.value.model||null});Ze.value={...Ze.value,...F},o.agents=r(Ze.value),ze("Agent model policy saved")}catch(F){ze(F.message||"Failed to save agent model policy","error")}}const Nn=()=>[...gs.value.length?gs.value:Yt.value];async function dc(v,F){const ee=Nn(),Ee=ee.findIndex(Ts=>Ms(Ts)===v);let Pe=null;if(F.target.checked&&Ee<0&&ee.push(ds.get(v)||v),!F.target.checked&&Ee>=0&&([Pe]=ee.splice(Ee,1)),!ee.length){F.target.checked=!0,ze("Keep one model selected. An empty list restores the provider default.","error");return}const ct=await Da(ee,"Agent Auto allowlist saved");ct&&F.target.checked&&ds.delete(v),ct&&!F.target.checked&&typeof Pe=="object"&&ds.set(v,Pe),ct||(F.target.checked=Yt.value.includes(v))}async function Da(v,F){if(vs.value)return!1;vs.value=!0;try{const ee=await z.put("/api/agents/model",{auto_model_allowlist:v});return Ze.value={...Ze.value,...ee},o.agents=r(Ze.value),ze(F),!0}catch(ee){return ze(ee.message||"Failed to save agent allowlist","error"),!1}finally{vs.value=!1}}const uc=()=>Da([],"Provider default restored");async function el(v,F,ee){const Ee=Yt.value.map(Pe=>{const ct=I(Pe);if(Pe!==v)return ct||Pe;const Ts=typeof ct=="string"?{model:Pe}:{...ct||{model:Pe}};return delete Ts.reasoning_effort,delete Ts.thinking_mode,ee&&(Ts[F]=ee),Object.keys(Ts).length===1?Ts.model:Ts});await Da(Ee,"Model default saved")}async function Pa(v,F=""){const[ee,...Ee]=v.split("/");if(!ee||!Ee.length)throw new Error("OpenRouter model id is not namespaced");return z.post(`/api/openrouter/models/${encodeURIComponent(ee)}/${encodeURIComponent(Ee.join("/"))}/select`,{provider_tag:F})}const tl=(v,F)=>{var Ee;const ee=(((Ee=re.value)==null?void 0:Ee.measured_cache)||[]).find(Pe=>Pe.model===v&&Pe.upstream_provider===F);return ee?`${ee.cached_percent}% cached over ${ee.samples} calls`:"no measured cache evidence"},pi=v=>v==null?"n/a":`$${(Number(v)*1e6).toFixed(4)}/M`,sl=(v,F)=>{if(v==null)return"n/a";if(typeof v=="number")return Number(v).toLocaleString();const ee=v[F];return ee==null?"n/a":Number(ee).toLocaleString()},al=v=>{var Pe;const F=[];v.quantization==="fp4"&&F.push("fp4 quantization may change quality");const ee=typeof v.latency_last_30m=="object"?Number((Pe=v.latency_last_30m)==null?void 0:Pe.p99):null,Ee=Number(Ze.value.iteration_timeout_seconds||0)*1e3;return ee&&Ee&&ee>Ee&&F.push("p99 exceeds the agent iteration budget"),F.join("; ")};async function C(v){var F;qt.value=v,ua.value=((F=be.value.openrouter.model_pins)==null?void 0:F[v.id])||"",un.value=!0;try{const[ee,...Ee]=v.id.split("/"),Pe=await z.get(`/api/openrouter/models/${encodeURIComponent(ee)}/${encodeURIComponent(Ee.join("/"))}/endpoints`);as.value=Pe.endpoints||[]}catch(ee){as.value=[],ze(ee.message||"Failed to load OpenRouter provider routes","error")}finally{un.value=!1}}function G(){qt.value=null,ua.value="",as.value=[]}async function ie(v,F=""){try{await Pa(v.id,F);const ee=Nn(),Ee=`compat:${v.id}`;if(ee.some(ct=>Ms(ct)===Ee)||ee.push(Ee),!await Da(ee,F?"OpenRouter model added and provider pinned.":"OpenRouter model added unpinned."))return;G(),await _a()}catch(ee){ze(ee.message||"Failed to add OpenRouter model","error")}}const Te=v=>v.startsWith("compat:")?v.slice(7):v,Je=v=>{var F;return((F=be.value.openrouter.model_pins)==null?void 0:F[Te(v)])||""},Ct=v=>Yt.value.length>1&&Da(Nn().filter(F=>Ms(F)!==v),"Model removed from allowlist");async function qs(){var v,F;try{const ee=Nn();for(const Ee of((v=re.value)==null?void 0:v.quick_add)||[])(F=Et.value.get(Ee))!=null&&F.agent_eligible&&(await Pa(Te(Ee),""),ee.some(Pe=>Ms(Pe)===Ee)||ee.push(Ee));await Da(ee,"Curated OpenRouter models added"),await _a()}catch(ee){ze(ee.message||"Failed to add curated OpenRouter models","error")}}function Vs(v){const F=v.hint_metadata||{},ee=[];return F.context_tokens&&ee.push(`context ${Number(F.context_tokens).toLocaleString()}`),F.max_output_tokens&&ee.push(`max output ${Number(F.max_output_tokens).toLocaleString()}`),F.structural_source&&ee.push(`source ${F.structural_source}`),ee.join("; ")}async function pn(v,F){const ee={...Ze.value.model_selection_hints||{}},Ee=F.trim();Ee?ee[v]=Ee:delete ee[v];try{const Pe=await z.put("/api/agents/model",{model_selection_hints:ee});Ze.value={...Ze.value,...Pe},o.agents=r(Ze.value),ze("Model hint saved")}catch(Pe){ze(Pe.message||"Failed to save model hint","error")}}function xa(v,F){const ee=Yt.value.indexOf(v);return!vs.value&&ee>=0&&ee+F>=0&&ee+FMs(Pe)===v);Ee<0||!xa(v,F)||([ee[Ee],ee[Ee+F]]=[ee[Ee+F],ee[Ee]],await Da(ee,"Agent Auto allowlist order saved"))}const pc=p(!0),fc=p(""),Ju=p({configured:null,accounts:[]}),mc=p(null),go=p(null),hc=p(""),nl=p(null),vc=p(!1),gc=p(null),Zu=p(null),Qu=p("");let fi=null;function ze(v,F="success"){ke(v,F==="error"?"error":"success")}function tb(v){if(!v)return"?";const F=v/(1024*1024*1024);return F>=1?F.toFixed(1)+" GB":(v/(1024*1024)).toFixed(0)+" MB"}function sb(v){return Number.isFinite(Number(v))?Number(v).toLocaleString():"—"}function ab(v){return v==null?"automatic (model-derived)":Number(v).toLocaleString()+" characters"}function nb(v){const F=new Date(v);return Number.isNaN(F.getTime())?"unknown":F.toLocaleString([],{dateStyle:"medium",timeStyle:"short"})}function ib(v){const F=new Date(Number(v)*1e3);return Number.isNaN(F.getTime())?"unknown":F.toLocaleString([],{dateStyle:"medium",timeStyle:"short"})}function lb(v){const F=Number(v);if(!Number.isFinite(F)||F<=0)return"unknown";const ee=Math.max(0,Math.floor((Date.now()/1e3-F)/60));return ee<1?"<1 min":`${ee} min`}function ob(v){return typeof v=="string"&&v.length>12?v.slice(0,8)+"…"+v.slice(-4):v}function rb(v){return typeof v!="number"||!Number.isFinite(v)?"—":(v/1e3).toFixed(2)}function cb(v){return v==="temporary learned clamp"?"is-clamp":v==="override"?"is-override":"is-built-in"}function db(v){const F=w.value.context_budget_overrides[v.model];return v.floor!=null&&Number.isFinite(Number(F))&&Number(F)>v.floor}function ub(v,F){const ee={...w.value.context_budget_overrides};F.target.value===""?delete ee[v]:ee[v]=Number(F.target.value),w.value.context_budget_overrides=ee,ne.value=!0}function pb(v){w.value.context_utilization=v.target.value===""?"":Number(v.target.value),ne.value=!0}function fb(v){const F={...w.value.context_budget_overrides};delete F[v],w.value.context_budget_overrides=F,ne.value=!0}async function _a({quiet:v=!1,poll:F=!1}={}){if(!(F&&Y())){if(l&&await l,i)return i;v||(e.value=!0),i=(async()=>{await Promise.all([Ds({poll:F}),il(),ll(),vo({poll:F}),pa(),Mn({poll:F})]),await ui(),Y()||Ae()})();try{await i}finally{i=null,v||(e.value=!1)}}}async function mb(){if(!(Y()||i)){if(l)return l;l=Promise.all([Ds({poll:!0}),il({refreshModels:!1}),ll({refreshModels:!1}),vo({poll:!0}),pa(),Mn({poll:!0})]).then(()=>{Y()||Ae()});try{await l}finally{l=null}}}async function Ds({preserveBasic:v=!1,preserveAdvanced:F=!1,poll:ee=!1}={}){var Ee,Pe,ct,Ts,Be,Xt;try{const De=await z.get("/api/llm/status"),Bt=ee&&Y();u.value=De,f.value=!1,Bt||(m.value.main=De.main_model||De.active_model||(De.active_provider==="compat"?`compat:${((Ee=De.openai_compatible)==null?void 0:Ee.model)||""}`:De.active_provider==="ollama"?`ollama:${((Pe=De.ollama)==null?void 0:Pe.model)||""}`:((ct=De.codex)==null?void 0:ct.model)||"gpt-6-sol")),!Bt&&De.codex&&!mi.pending()&&(v||(w.value.enabled=De.codex.enabled,w.value.model=De.codex.model||"gpt-6-sol",w.value.reasoning_effort=De.codex.reasoning_effort||"medium",w.value.agent_reasoning_effort=De.codex.agent_reasoning_effort||""),F||(w.value.request_timeout_seconds=De.codex.request_timeout_seconds??w.value.request_timeout_seconds,w.value.stream_stall_timeout_seconds=De.codex.stream_stall_timeout_seconds??w.value.stream_stall_timeout_seconds,w.value.retry={...w.value.retry,...De.codex.retry||{}},w.value.connection_pool={...w.value.connection_pool,...De.codex.connection_pool||{}},w.value.context_compression={...w.value.context_compression,...De.codex.context_compression||{}},!ne.value&&!$e.value&&(w.value.context_budget_overrides={...De.codex.context_budget_overrides||{}},w.value.context_utilization=De.codex.context_utilization??w.value.context_utilization))),!Bt&&De.ollama&&!hi.pending()&&(v||(de.value.enabled=De.ollama.enabled,de.value.base_url=De.ollama.base_url||"",de.value.model=De.ollama.model||"",de.value.max_tokens=De.ollama.max_tokens||4096,de.value.num_ctx=De.ollama.num_ctx||32768),F||(de.value.timeout=De.ollama.timeout??de.value.timeout));const Ut=De.openai_compatible;!Bt&&Ut&&!Pn.pending()&&(v||(be.value.enabled=Ut.enabled,be.value.base_url=Ut.base_url||be.value.base_url,be.value.model=Ut.model||be.value.model,be.value.preset=Ut.preset||be.value.preset,be.value.reasoning_effort=Ut.reasoning_effort||"medium"),F||(be.value.request_timeout_seconds=Ut.request_timeout_seconds??be.value.request_timeout_seconds,be.value.stream_stall_timeout_seconds=Ut.stream_stall_timeout_seconds??be.value.stream_stall_timeout_seconds,be.value.model_profiles=Ut.model_profiles||be.value.model_profiles,be.value.context_utilization=Ut.context_utilization??be.value.context_utilization,be.value.openrouter={...be.value.openrouter,...Ut.openrouter||{}})),Bt||((Ts=m.value.main)!=null&&Ts.startsWith("compat:")?m.value.main_capability=(Ut==null?void 0:Ut.reasoning_effort)||"medium":(Be=m.value.main)!=null&&Be.startsWith("ollama:")||(m.value.main_capability=((Xt=De.codex)==null?void 0:Xt.reasoning_effort)||"medium")),!Bt&&De.auxiliary&&(xe.value=De.auxiliary,Dn.pending()||(V.value.enabled=De.auxiliary.enabled,V.value.model=De.auxiliary.model||"gpt-6-luna"))}catch{u.value||(u.value={active_provider:"",codex:{configured:null},ollama:{configured:null},openai_compatible:{configured:null}}),f.value=!0}}async function Mn({poll:v=!1}={}){const F=++te;he.value=!0,we.value="";try{const ee=await z.get("/api/context/windows");if(F!==te)return;pe.value=ee,!(v&&Y())&&!$e.value&&!ne.value&&(w.value.context_budget_overrides=Object.fromEntries(Object.entries(ee.models||{}).filter(([,Ee])=>Ee.override!=null).map(([Ee,Pe])=>[Ee,Pe.override])),w.value.context_utilization=ee.utilization??w.value.context_utilization)}catch(ee){F===te&&(we.value=ee.message||"Failed to load context budgets")}finally{F===te&&(he.value=!1)}}async function il({refreshModels:v=!0}={}){try{if(Re.value=await z.get("/api/ollama/status"),Me.value=!1,Re.value.model&&(Ne.value=Re.value.model),v&&Re.value.configured)try{const F=await z.get("/api/ollama/models");ae.value=F.models||[]}catch{ae.value=[]}else if(v&&de.value.base_url)try{const F=await z.post("/api/ollama/probe-models",{base_url:de.value.base_url});ae.value=F.models||[]}catch{ae.value=[]}}catch{Me.value=!0}}async function pa(){pc.value=!0,fc.value="";try{Ju.value=await z.get("/api/codex/status")}catch(v){fc.value=v.message||"Failed to fetch Codex status"}finally{pc.value=!1}}async function hb(){try{m.value.main.startsWith("compat:")&&ns.value&&await Pa(Te(m.value.main),Je(m.value.main));try{await z.put("/api/llm/main-model",{model:m.value.main})}catch(v){if(!/404|not found/i.test(v.message||""))throw v;await z.post("/api/llm/switch",{model:m.value.main})}c("mainModel",m.value.main),ze("Main model saved"),await _a()}catch(v){ze(v.message||"Failed to save main model","error"),await Ds()}}async function vb(v){m.value.main_capability=v;const F=L.value;F&&(F.provider==="compat"&&F.capability!=="none"?(be.value.reasoning_effort=v,await rl()):F.capability==="reasoning"?(w.value.reasoning_effort=v,await ol()):F.capability==="thinking"&&(await z.put("/api/openai-compatible/config",{thinking_mode:v}),ze("Thinking mode saved")))}async function gb(v){m.value.agent_capability=v;const F=E.value,ee=(F==null?void 0:F.capability)||O.value;if(ee!=="none"){if(v===""||v==="auto"||ee==="reasoning"||ee==="mixed"){if(w.value.agent_reasoning_effort=v,ee==="thinking"){const Ee=await z.put("/api/agents/model",{thinking_mode:null});Ze.value={...Ze.value,...Ee}}await ol()}else if(ee==="thinking"){const Ee=await z.put("/api/agents/model",{thinking_mode:v});Ze.value={...Ze.value,...Ee},ze("Agent thinking mode saved")}}}async function bb(){He.value=!0;try{const v=await z.post("/api/ollama/reload");ze(v.configured?"Ollama reloaded":v.reason||"Ollama not configured",v.configured?"success":"error"),await _a()}catch(v){ze(v.message||"Reload failed","error")}finally{He.value=!1}}async function yb(){rt.value=!0;try{await z.post("/api/ollama/model",{model:Ne.value}),ze("Model set to "+Ne.value),await _a()}catch(v){ze(v.message||"Failed","error")}finally{rt.value=!1}}async function xb(){const v=de.value.base_url;if(!v){ze("Enter a base URL first","error");return}Xe.value=!0;try{const F=await z.post("/api/ollama/probe-models",{base_url:v});ae.value=F.models||[],ae.value.length?(ze(ae.value.length+" model(s) found"),!de.value.model&&ae.value.length&&(de.value.model=ae.value[0].name)):ze("No models found at "+v,"error")}catch(F){ze(F.message||"Could not reach Ollama","error")}finally{Xe.value=!1}}async function ll({refreshModels:v=!0}={}){try{if(Zt.value=await z.get("/api/openai-compatible/status"),lt.value=!1,Zt.value.model&&(cs.value=Zt.value.model),v&&Zt.value.configured)try{const F=await z.get("/api/openai-compatible/models");gt.value=F.models||[]}catch{gt.value=[]}}catch{lt.value=!0}}async function _b(){oa.value=!0;try{const v=await z.post("/api/openai-compatible/reload");ze(v.configured?"OpenAI-compatible reloaded":v.reason||"OpenAI-compatible not configured",v.configured?"success":"error"),await _a()}catch(v){ze(v.message||"Reload failed","error")}finally{oa.value=!1}}async function wb(){js.value=!0;try{await z.post("/api/openai-compatible/model",{model:cs.value}),ze("Model set to "+cs.value),await _a()}catch(v){ze(v.message||"Failed","error")}finally{js.value=!1}}async function ol(){if($e.value){mi();return}$e.value=!0;const v=Uo(w.value);try{await z.put("/api/llm/codex/config",v),c("codexBasic",v),ze("Codex config saved"),await Promise.all([Ds({preserveBasic:!0,preserveAdvanced:!0}),pa()])}catch(F){ze(F.message||"Failed","error");const ee=JSON.stringify(Uo(w.value))!==JSON.stringify(v);await Promise.all([Ds({preserveBasic:ee,preserveAdvanced:!0}),pa()])}finally{$e.value=!1}}async function Yu(){if($e.value)return;$e.value=!0;const v=zo(w.value);try{await z.put("/api/llm/codex/config",v),c("codexAdvanced",v),JSON.stringify({context_budget_overrides:w.value.context_budget_overrides,context_utilization:w.value.context_utilization})===JSON.stringify({context_budget_overrides:v.context_budget_overrides,context_utilization:v.context_utilization})&&(ne.value=!1),ze("Codex advanced settings saved"),await Promise.all([Ds({preserveBasic:!0,preserveAdvanced:!0}),pa(),Mn()])}catch(F){ze(F.message||"Failed","error");const ee=JSON.stringify(zo(w.value))!==JSON.stringify(v);await Promise.all([Ds({preserveBasic:!0,preserveAdvanced:ee}),pa(),Mn()])}finally{$e.value=!1}}async function bc(){if(Ke.value){hi();return}Ke.value=!0;try{const v=Fe.value?de.value.api_key:null,F=Ho(de.value,{includeApiKey:v!==null});await z.put("/api/llm/ollama/config",F),ze("Ollama config saved"),v!==null&&de.value.api_key===v&&(de.value.api_key="",Fe.value=!1),c("ollamaBasic",Ho(de.value)),await Promise.all([Ds({preserveBasic:!0,preserveAdvanced:!0}),il()])}catch(v){ze(v.message||"Failed","error")}finally{Ke.value=!1}}async function Xu(){if(!Ke.value){Ke.value=!0;try{const v=Hc(de.value);await z.put("/api/llm/ollama/config",v),c("ollamaAdvanced",v),ze("Ollama timeout saved"),await Promise.all([Ds({preserveBasic:!0,preserveAdvanced:!0}),il()])}catch(v){ze(v.message||"Failed","error")}finally{Ke.value=!1}}}async function rl(){if(at.value){Pn();return}at.value=!0;try{const v=qe.value?be.value.api_key:null,F=Bo(be.value,{includeApiKey:v!==null});await z.put("/api/openai-compatible/config",F),ze("OpenAI-compatible config saved"),v!==null&&be.value.api_key===v&&(be.value.api_key="",qe.value=!1),c("compatibleBasic",Bo(be.value)),await Promise.all([Ds({preserveBasic:!0,preserveAdvanced:!0}),ll()]),await ui()}catch(v){ze(v.message||"Failed","error")}finally{at.value=!1}}async function ep(){if(!at.value){at.value=!0;try{const v=zc(be.value);await z.put("/api/openai-compatible/config",v),c("compatibleAdvanced",v),ze("OpenAI-compatible endpoint settings saved"),await Promise.all([Ds({preserveBasic:!0,preserveAdvanced:!0}),ll()]),await ui()}catch(v){ze(v.message||"Failed","error")}finally{at.value=!1}}}async function kb(){if(W.value){Dn();return}W.value=!0;try{const v={...V.value};await z.put("/api/llm/auxiliary/config",v),c("auxiliary",v),ze("Auxiliary config saved"),await Ds()}catch(v){ze(v.message||"Failed","error"),await Ds()}finally{W.value=!1}}const mi=jo(ol),Dn=jo(kb),hi=jo(bc),Pn=jo(rl),Sb=()=>(mi.cancel(),ol()),Cb=()=>(hi.cancel(),bc()),Tb=()=>(Pn.cancel(),rl()),Eb=()=>Yu(),Ab=()=>Xu(),Rb=()=>ep();async function Ib(v){const F=v.account_key+":"+v.model;K.value=F;try{const ee=await z.post("/api/context/windows/clear",{account_key:v.account_key,model:v.model});ze(ee.cleared?"Temporary clamp cleared":"Clamp was already inactive"),await Mn()}catch(ee){ze(ee.message||"Failed to clear clamp","error"),await Mn()}finally{K.value=null}}async function Ob(v){try{await z.post("/api/codex/account/"+v+"/activate"),ze("Active account switched"),await pa()}catch(F){ze(F.message||"Failed","error")}}async function Lb(v){mc.value=v;try{await z.post("/api/codex/account/"+v+"/refresh"),ze("Token refreshed"),await pa()}catch(F){ze(F.message||"Refresh failed","error")}finally{mc.value=null}}function Nb(v,F){go.value=v,hc.value=F||""}async function Mb(v){try{await z.put("/api/codex/account/"+v+"/label",{label:hc.value}),ze("Label updated"),go.value=null,await pa()}catch(F){ze(F.message||"Failed","error")}}async function Db(v,F){if(await os({title:"Delete Codex account",message:`Delete ${F||"account #"+(v+1)}? The pool will reload without it.`,confirmLabel:"Delete",danger:!0}))try{await z.del("/api/codex/account/"+v),ze("Deleted. Pool reloaded."),await pa()}catch(Ee){ze(Ee.message||"Failed","error")}}async function Pb(){vc.value=!0;try{const v=await z.post("/api/codex/device-code");gc.value=v,nl.value="pending",$b(v)}catch(v){ze(v.message||"Failed","error")}finally{vc.value=!1}}async function $b(v){fi={cancelled:!1};const F=fi;try{const ee=await z.post("/api/codex/device-poll",{device_auth_id:v.device_auth_id,user_code:v.user_code,interval:v.interval});if(F.cancelled)return;Zu.value=ee,nl.value="success",await _a()}catch(ee){if(F.cancelled)return;Qu.value=ee.message||"Device login failed",nl.value="error"}}function Fb(){fi&&(fi.cancelled=!0),nl.value=null,gc.value=null}function tp(){n||(n=!0,_a(),a=window.setInterval(mb,s))}function sp(){n&&(n=!1,a!==null&&window.clearInterval(a),a=null)}return tt(tp),rs(tp),Kt(sp),xt(()=>{sp(),fi&&(fi.cancelled=!0),mi.cancel(),Dn.cancel(),hi.cancel(),Pn.cancel()}),{pageRoot:t,allowlistModalOpen:ra,closeAllowlistModal:ba,allowlistSaving:vs,effectiveAllowlist:Yt,allowlistSummary:_t,resetAgentAllowlist:uc,selectedUnavailableReason:rn,selectedModelFacts:ca,loading:e,llmStatus:u,llmStatusLoadFailed:f,modelSelection:m,modelSelectorSearch:h,reasoningEfforts:g,neutralReasoningLevels:A,modelCatalog:b,modelGroups:x,selectedMainModel:L,selectedAgentModel:E,selectedAgentCapabilityValue:ce,agentCapabilityKind:O,agentCapabilityEfforts:$,autoAllowlistModels:X,allowlistModel:P,allowlistModelEfforts:R,allowlistEntryCapabilityValue:H,modelOptionLabel:ue,agentModelAvailable:S,agentModelOptionLabel:_,advancedOpen:ge,codexForm:w,codexModelOptions:Z,codexAgentModelOptions:J,mainEffortAllowed:Oe,agentEffortAllowed:le,mainModelOptionDisabled:ve,agentModelOptionDisabled:j,auxForm:V,auxData:xe,auxModelOptions:k,onAuxModelChange:D,savingAux:W,saveAuxConfigDebounced:Dn,ollamaForm:de,compatibleForm:be,savingCodex:$e,savingOllama:Ke,savingCompatible:at,probingOllama:Xe,ollamaKeyDirty:Fe,compatibleKeyDirty:qe,fetchCodexStatus:pa,ollamaStatus:Re,ollamaStatusLoadFailed:Me,ollamaModels:ae,ollamaSelectedModel:Ne,reloading:He,settingModel:rt,compatibleStatus:Zt,compatibleStatusLoadFailed:lt,compatibleModels:gt,visibleCompatibleModels:Qt,compatibleSelectedModel:cs,reloadingCompatible:oa,settingCompatibleModel:js,applyCompatiblePreset:Na,setOpenRouterList:di,agentsConfig:Ze,compatibleAgentModels:Nt,ollamaAgentModels:ho,knownAgentModelRefs:Ma,agentModelLabel:rc,saveAgentsModel:cc,toggleAgentAutoAllowlist:dc,saveAllowlistEntryCapability:el,autoAllowlistGroups:T,structuralFacts:Vs,saveModelHint:pn,canMoveAllowlist:xa,moveAgentAutoAllowlist:eb,openRouterCatalogue:re,openRouterCatalogueLoading:Ce,openRouterCatalogueError:Ve,openRouterRecognized:ns,compatibleCatalogueStatus:Ln,compatibleCatalogueStatusClass:Ie,openRouterSearch:kt,openRouterVendor:St,openRouterVendors:M,openRouterToolsOnly:cn,openRouterEligibleOnly:Rn,openRouterStandardOnly:dn,openRouterMeasuredCacheOnly:In,openRouterMaxPromptPrice:da,openRouterQuantization:ya,openRouterQuantizations:me,openRouterResults:We,openRouterMatchCount:Ye,openRouterInlineFacts:At,openRouterSelectedFacts:bs,prepareOpenRouterModel:C,addOpenRouterModel:ie,removeOpenRouterModel:Ct,quickAddOpenRouter:qs,openRouterModelMap:Et,openRouterPin:Je,openRouterPendingModel:qt,openRouterPendingTag:ua,openRouterPendingEndpoints:as,openRouterPendingLoading:un,openRouterEndpointSort:On,openRouterSortedPendingEndpoints:ri,openRouterEndpointCacheFact:tl,openRouterRate:pi,openRouterMetric:sl,openRouterRouteWarning:al,cancelOpenRouterPending:G,codexLoading:pc,codexError:fc,codexData:Ju,refreshing:mc,editingLabel:go,labelValue:hc,contextWindows:pe,contextWindowsLoading:he,contextWindowsError:we,contextBudgetRows:_e,activeClampRows:B,activeContextBudget:fe,clearingClamp:K,contextPolicyDirty:ne,deviceState:nl,deviceLoading:vc,deviceInfo:gc,deviceResult:Zu,deviceError:Qu,fetchAll:_a,fetchLLMStatus:Ds,fetchOllamaStatus:il,fetchCompatibleStatus:ll,saveMainModel:hb,saveMainCapability:vb,saveAgentCapability:gb,reloadOllama:bb,setOllamaModel:yb,reloadCompatible:_b,setCompatibleModel:wb,probeOllamaModels:xb,saveCodexConfig:ol,saveOllamaConfig:bc,saveCompatibleConfig:rl,saveCodexAdvancedConfig:Yu,saveOllamaAdvancedConfig:Xu,saveCompatibleAdvancedConfig:ep,saveCodexConfigDebounced:mi,saveOllamaConfigDebounced:hi,saveCompatibleConfigDebounced:Pn,saveCodexConfigNow:Sb,saveOllamaConfigNow:Cb,saveCompatibleConfigNow:Tb,saveCodexAdvancedConfigNow:Eb,saveOllamaAdvancedConfigNow:Ab,saveCompatibleAdvancedConfigNow:Rb,activateAccount:Ob,refreshAccount:Lb,startEditLabel:Nb,saveLabel:Mb,deleteAccount:Db,startDeviceLogin:Pb,cancelDeviceLogin:Fb,formatSize:tb,fetchContextWindows:Mn,clearContextClamp:Ib,setContextOverride:ub,setContextUtilization:pb,resetContextOverride:fb,overrideAboveFloor:db,formatCount:sb,formatContextCeiling:ab,formatExpiry:nb,shortAccountKey:ob,provenanceClass:cb,formatDensity:rb,quotaBlocks:IC,quotaFailureVisible:OC,quotaAge:lb,formatQuotaDate:ib}}},Vf={ok:"text-green-400",pass:"text-green-400",degraded:"text-yellow-400",warn:"text-yellow-400",down:"text-red-400",fail:"text-red-400",unconfigured:"text-gray-500",skipped:"text-gray-500"};function $C(e){return Vf[e]||Vf[(e||"").toLowerCase()]||"text-gray-400"}const FC={template:` + `,setup(){const e=f(!0),t=f(null),s=15e3;let a=null,n=!1,i=null,l=null,o=!1;const r={codexBasic:null,codexAdvanced:null,ollamaBasic:null,ollamaAdvanced:null,compatibleBasic:null,compatibleAdvanced:null,auxiliary:null,mainModel:null,agents:null},c=v=>JSON.stringify(v),d=(v,R)=>{r[v]=c(R)},u=(v,R)=>r[v]!==null&&r[v]!==c(R),p=f(null),m=f(!1),h=f({main:"",main_capability:"medium",agent_capability:"adaptive"}),y=f(""),k=["none","low","medium","high","xhigh","max"],A=["none","low","medium","high","xhigh","max"],g=f({enabled:!1,model:"gpt-6-sol",reasoning_effort:"xhigh",agent_reasoning_effort:"auto",request_timeout_seconds:3600,stream_stall_timeout_seconds:180,retry:{max_retries:3,base_delay:1,max_delay:30},connection_pool:{max_connections:10,keepalive_timeout:30},context_compression:{enabled:!0,max_context_chars:null,keep_recent_iterations:30},context_budget_overrides:{},context_utilization:60}),b=["gpt-6-astra","gpt-6-sol","gpt-6-luna","gpt-5.6-sol","gpt-5.6-terra","gpt-5.6-luna"],x=v=>!v||v==="auto"?null:v.startsWith("compat:")?"compat":v.startsWith("ollama:")?"ollama":v.includes(":")?null:"codex",w=v=>{var R,q;return((q=(R=p.value)==null?void 0:R[v==="compat"?"openai_compatible":v])==null?void 0:q.enabled)===!0},_=B(()=>w("codex")&&!w("compat")&&!w("ollama")),T=v=>{const R=v==="main"?h.value.main:v==="agent"?Fe.value.model:X.value.enabled?X.value.model:"";return L(R)},L=v=>{var q,ye;const R=x(v);return!!(R&&((ye=(q=p.value)==null?void 0:q[R==="compat"?"openai_compatible":R])==null?void 0:ye.enabled)===!1)},E=v=>{var q;const R=v==="main"?h.value.main:v==="agent"?Fe.value.model:X.value.model;return`Configured: ${((q=I.value.find(ye=>ye.ref===R))==null?void 0:q.name)||R.replace(/^(compat|ollama):/,"")} (provider disabled)`},F=v=>{var Ne,rt;const R=[],q=x(((Ne=p.value)==null?void 0:Ne.main_model)||h.value.main);q===v&&R.push("Main"),(Fe.value.model===""?q:x(Fe.value.model))===v&&R.push("Agent");const ye=((rt=p.value)==null?void 0:rt.auxiliary)||X.value;return ye.enabled&&x(ye.model)===v&&R.push("Auxiliary"),Fe.value.model==="auto"&&ot.value.filter(Lt=>x(Lt)!==v).map(Lt=>I.value.find(wa=>wa.ref===Lt)).filter(Lt=>Lt&&N(Lt)&&!(Lt.provider==="compat"&&ut.value&&!Lt.ref.slice(7).includes("/"))).length===0&&R.push("Agent Auto (zero effective choices)"),R};async function O(v,R){if(R!==!1||!w(v))return!0;const q=F(v);return is({title:`Disable ${v==="compat"?"OpenAI-compatible":v==="codex"?"Codex":"Ollama"}?`,message:`${q.length?`This provider is used by ${q.join(", ")}. `:""}Saved selections and ranked allowlist entries remain configured, not replaced. Continue?`,confirmLabel:"Disable provider",danger:!0})}const I=B(()=>{var wa,xn,at,Kt;const v=p.value||{},R=v.model_catalogue||v.model_catalog||{},q=Pe=>{var Ft,pt,ka;return((Ft=W.value.model_profiles)==null?void 0:Ft[Pe])||((ka=(pt=W.value.openrouter)==null?void 0:pt.catalogue_profiles)==null?void 0:ka[Pe])||null},ye=(Pe,Ft,pt)=>Ft.map(ka=>{var fp,mp;const Ac=typeof ka=="string"?ka:ka.name,Sa=Pe==="compat"?q(Ac):null;return{ref:Pe==="codex"?ka:`${Pe}:${Ac}`,name:Ac,provider:Pe,available:!!(pt!=null&&pt.enabled&&(Pe==="codex"?pt.configured:(fp=pt.health)!=null&&fp.healthy)),unavailable_reason:pt!=null&&pt.enabled?pt!=null&&pt.configured?!((mp=pt==null?void 0:pt.health)!=null&&mp.healthy)&&Pe!=="codex"?"unreachable":"":"not configured":"disabled",capability:Pe==="codex"||Sa!=null&&Sa.supports_reasoning?"reasoning":Sa!=null&&Sa.supports_thinking_mode?"thinking":"none",efforts:Sa==null?void 0:Sa.supported_efforts,profile:Sa}}),Ne=[...R.codex||ye("codex",b,v.codex),...R.compat||R.openai_compatible||[],...R.ollama||ye("ollama",zt.value,v.ollama)].map(Pe=>typeof Pe=="string"?{ref:Pe,name:Pe,provider:"codex",available:w("codex"),capability:"reasoning"}:{...Pe,available:w(Pe.provider)&&Pe.available,unavailable_reason:w(Pe.provider)?Pe.unavailable_reason:"disabled"}),rt=Pe=>{const Ft=Ne.findIndex(pt=>pt.ref===Pe.ref);if(Ft===-1)Ne.push(Pe);else{const pt=Ne[Ft];Ne[Ft]={...pt,...Pe,available:pt.available,unavailable_reason:pt.unavailable_reason,agent_available:pt.agent_available}}};for(const Pe of ye("compat",Es.value,v.openai_compatible))rt(Pe);if(As.value&&xa.value.length)for(const Pe of xa.value){const Ft=`compat:${Pe.id}`;rt({ref:Ft,name:Pe.name||Pe.id,provider:"compat",available:((wa=Ne.find(pt=>pt.ref===Ft))==null?void 0:wa.available)??!1,unavailable_reason:(xn=Ne.find(pt=>pt.ref===Ft))!=null&&xn.available?"":((at=Ne.find(pt=>pt.ref===Ft))==null?void 0:at.unavailable_reason)||"unavailable",capability:Pe.supports_reasoning?"reasoning":"none",efforts:k.filter(pt=>{var ka;return(ka=Pe.supported_efforts)==null?void 0:ka.includes(pt)}),agent_available:!!(Pe.agent_eligible&&Pe.profile&&((Kt=Ne.find(pt=>pt.ref===Ft))==null?void 0:Kt.agent_available)===!0),agent_unavailable_reason:Pe.agent_unavailable_reason||""})}const Lt=new Set(Ne.map(Pe=>Pe.ref));for(const Pe of[h.value.main,Fe.value.model,...X.value.enabled?[X.value.model]:[],...ot.value])if(Pe&&Pe!=="auto"&&!Lt.has(Pe)&&x(Pe)){const Ft=x(Pe);Ne.unshift({ref:Pe,name:Pe.replace(/^(compat|ollama):/,""),provider:Ft,available:!1,unavailable_reason:w(Ft)?"unavailable":"disabled",capability:Ft==="codex"?"reasoning":"none"}),Lt.add(Pe)}return Ne.filter(Pe=>!ut.value||Pe.provider!=="compat"||Pe.ref.slice(7).includes("/")||[h.value.main,Fe.value.model,X.value.enabled&&X.value.model,...ot.value].includes(Pe.ref)).map(Pe=>Pe.efforts?{...Pe,efforts:k.filter(Ft=>Pe.efforts.includes(Ft)&&!G(Pe.ref,Ft))}:Pe)}),H=()=>[h.value.main,Fe.value.model,X.value.enabled&&X.value.model],te=B(()=>[["codex","Codex"],["compat","OpenAI-compatible"],["ollama","Ollama"]].filter(([v])=>w(v)).map(([v,R])=>({id:v,label:R,models:I.value.filter(q=>{if(q.provider!==v||L(q.ref)&&H().includes(q.ref))return!1;const ye=_.value?"":y.value.trim().toLowerCase();return!ye||`${q.name} ${q.ref}`.toLowerCase().includes(ye)})})).filter(v=>v.models.length)),ee=B(()=>{var v;return((v=te.value.find(R=>R.id==="codex"))==null?void 0:v.models)||[]}),N=v=>w(v.provider)&&v.available&&v.agent_available!==!1,M=v=>v.available?`${v.name}${v.agent_available===!1?` (${v.agent_unavailable_reason||"not agent-eligible"})`:""}`:Y(v),$=B(()=>[["codex","Codex"],["compat","OpenAI-compatible"],["ollama","Ollama"]].filter(([v])=>w(v)).map(([v,R])=>({id:v,label:R,models:I.value.filter(q=>q.provider===v&&!(v==="compat"&&ut.value)&&`${q.ref} ${q.name}`.toLowerCase().includes(Pn.value.trim().toLowerCase()))})).filter(v=>v.models.length)),re=B(()=>I.value.find(v=>v.ref===h.value.main)),ce=B(()=>I.value.find(v=>v.ref===Fe.value.model)),Z=v=>I.value.find(R=>R.ref===v),Q=v=>{const R=Z(v);return(R==null?void 0:R.provider)==="codex"?k.filter(q=>!G(v,q)):(R==null?void 0:R.efforts)||[]},ae=v=>Ge.value.find(R=>qe(R)===v),V=(v,R)=>{const q=ae(v);return typeof q=="string"?"":(q==null?void 0:q[R])||""},be=B(()=>ot.value.map(v=>Z(v)).filter(v=>v&&N(v)&&!(v.provider==="compat"&&ut.value&&!v.ref.slice(7).includes("/")))),Oe=B(()=>[...new Set(be.value.map(v=>v.capability!=="reasoning"?v.capability||"none":v.provider==="codex"?"codex_reasoning":"compatible_reasoning"))]),le=B(()=>Oe.value.length>1),oe=B(()=>{var R;if(Fe.value.model!=="auto")return((R=ce.value)==null?void 0:R.capability)||"none";if(le.value)return"mixed";const v=Oe.value[0]||"none";return v.endsWith("_reasoning")?"reasoning":v}),ve=B(()=>{var v;return Fe.value.model==="auto"?k:((v=ce.value)==null?void 0:v.efforts)||k}),j=B(()=>oe.value==="thinking"?Fe.value.thinking_mode??g.value.agent_reasoning_effort??"":g.value.agent_reasoning_effort??""),Y=v=>`${v.name}${v.available?"":` (${v.unavailable_reason||"unavailable"})`}`,xe=B(()=>{const v=g.value.model;return v&&!b.includes(v)?[v,...b]:b}),S=B(()=>{const v=Fe.value.model;return v&&v!=="auto"&&!b.includes(v)?[v,...b]:b}),P={"gpt-5.4":["max"],"gpt-5.4-mini":["max"],"gpt-6-astra":["none"]},G=(v,R)=>!!v&&!!R&&(P[v]||[]).includes(R),he=B(()=>{const v=Fe.value.model;return v&&!v.includes(":")?v:null}),fe=v=>!G(g.value.model,v)&&!(g.value.agent_reasoning_effort===""&&G(he.value,v)),me=v=>{const R=Fe.value.model;return R==="auto"?!0:!G(R||g.value.model,v)},we=B(()=>{const v=g.value.agent_reasoning_effort;return v==="auto"?null:v||g.value.reasoning_effort}),J=v=>G(v,g.value.reasoning_effort)||Fe.value.model===""&&G(v,we.value),ie=v=>G(v,we.value),X=f({enabled:!1,model:"gpt-6-luna"}),_e=f({unavailable_reason:null}),U=B(()=>{const v=X.value.model;return v&&!b.includes(v)?[v,...b]:b});function de(v){const R=v.target.value;X.value.enabled=R!=="",R!==""&&(X.value.model=R),R.startsWith("compat:")&&ut.value?nl(il(R),Co(R)).then(()=>Un()).catch(q=>He(q.message||"Failed to prepare OpenRouter model","error")):Un()}const ge=f(!1),Ae=f({codex:!1,ollama:!1,compatible:!1}),$e=f(null),We=f(!1),je=f(""),Xe=f(null),it=f(!1);let Qe=0;const se=B(()=>{var v;return Object.entries(((v=$e.value)==null?void 0:v.models)||{}).map(([R,q])=>{var ye,Ne,rt;return{model:R,floor:q.floor,override:q.override,effectiveBudget:(ye=q.effective)==null?void 0:ye.effective_budget,configuredPrimaryChars:(Ne=q.configured)==null?void 0:Ne.primary_chars,primaryChars:(rt=q.effective)==null?void 0:rt.primary_chars,provenance:q.provenance,clampExpiresAt:q.clamp_expires_at,densityPriorMilli:q.density_prior_milli,densityScope:q.density_scope,workloadCalibration:q.workload_calibration}})}),Re=B(()=>{var v;return((v=$e.value)==null?void 0:v.clamps)||[]}),Le=B(()=>{var v,R;return((R=(v=$e.value)==null?void 0:v.models)==null?void 0:R[g.value.model])||null}),Te=f({enabled:!1,base_url:"",model:"",api_key:"",max_tokens:4096,num_ctx:32768,timeout:300}),W=f({enabled:!1,base_url:"https://api.deepseek.com/v1",api_key:"",model:"deepseek-v4-flash",request_timeout_seconds:3600,stream_stall_timeout_seconds:180,preset:"deepseek",reasoning_effort:"medium",model_profiles:{},context_utilization:75,openrouter:{order:[],allow_fallbacks:!0,quantizations:[],sort:null,data_collection:null,reasoning_effort:"medium",model_pins:{},catalogue_profiles:{}}}),De=f(!1),ze=f(!1),Ye=f(!1),Dt=f(!1),et=f(!1),Rt=f(!1);function Pt(){var R,q,ye;return!!(((R=t.value)==null?void 0:R.contains(document.activeElement))&&((ye=(q=document.activeElement)==null?void 0:q.matches)==null?void 0:ye.call(q,'input, textarea, select, [contenteditable="true"]'))||ue.value||To.value!==null||it.value||De.value||ze.value||Ce.value||Ye.value||Dt.value||et.value||ge.value||gi.pending()||bi.pending()||zn.pending()||Un.pending()||u("codexBasic",yl(g.value))||u("codexAdvanced",xl(g.value))||u("ollamaBasic",_l(Te.value))||u("ollamaAdvanced",Jo(Te.value))||u("compatibleBasic",bl(W.value))||u("compatibleAdvanced",Ko(W.value))||u("auxiliary",X.value)||u("mainModel",h.value.main)||u("agents",Fe.value))}function Ts(){r.codexBasic=c(yl(g.value)),r.codexAdvanced=c(xl(g.value)),r.ollamaBasic=c(_l(Te.value)),r.ollamaAdvanced=c(Jo(Te.value)),r.compatibleBasic=c(bl(W.value)),r.compatibleAdvanced=c(Ko(W.value)),r.auxiliary=c(X.value),r.mainModel=c(h.value.main)}const Vs=f({configured:null}),Gs=f(!1),zt=f([]),ta=f(""),Ws=f(!1),Pa=f(!1),hs=f({configured:null}),$a=f(!1),Es=f([]),Ct=B(()=>ut.value?xa.value.map(v=>v.id):Es.value),ls=f(""),sa=f(!1),ba=f(!1),Fe=f({model:"auto",thinking_mode:null,auto_model_allowlist:[]}),ue=f(!1),Ce=f(!1),qe=v=>typeof v=="string"?v:v==null?void 0:v.model,Ge=B(()=>(Fe.value.auto_model_allowlist||[]).map(v=>typeof v=="string"?v:{...v}).filter(v=>qe(v))),It=B(()=>{var v,R,q,ye;if(!w("codex")&&w("compat")){const Ne=(R=(v=p.value)==null?void 0:v.openai_compatible)==null?void 0:R.model;return Ne?[`compat:${Ne}`]:[]}if(!w("codex")&&!w("compat")&&w("ollama")){const Ne=(ye=(q=p.value)==null?void 0:q.ollama)==null?void 0:ye.model;return Ne?[`ollama:${Ne}`]:[]}return w("codex")?b:[]}),ot=B(()=>Ge.value.length?Ge.value.map(qe):It.value),pn=B(()=>be.value.length),pi=B(()=>{var v;return`${(v=Fe.value.auto_model_allowlist)!=null&&v.length?"Allowlist":"Default"}: ${ot.value.length} configured, ${pn.value} effective models`}),Fa=new Map;function fi(){ue.value=!1,So()}const Ba=v=>{const R=x(v);if(R&&!w(R))return"provider disabled";if(ut.value&&v.startsWith("compat:")&&!v.slice(7).includes("/"))return"OpenRouter requires a namespaced vendor/model ID; this is a direct-endpoint profile.";const q=I.value.find(ye=>ye.ref===v);return q?N(q)&&!(q.provider==="compat"&&ut.value&&!v.slice(7).includes("/"))?"":q.agent_unavailable_reason||q.unavailable_reason||"Not agent-eligible":"Model is absent from the current endpoint catalogue."},fn=v=>{if(gn.value.has(v))return Fn(v);const R=I.value.find(ye=>ye.ref===v),q=(R==null?void 0:R.hint_metadata)||{};return[q.hint||q.hint_derived,q.as_of&&`as of ${q.as_of}`,q.scope_note,q.evidence&&`Evidence: ${q.evidence}`,R&&ap(R)].filter(Boolean).join(" · ")||"No catalogue hint. Add an operator hint below."},kt=f(null),$s=f(!1),Wt=f("");let ya=0;const xa=B(()=>{var v;return(((v=kt.value)==null?void 0:v.models)||[]).map(R=>{var ye,Ne,rt;const q=((ye=W.value.model_profiles)==null?void 0:ye[R.id])||R.profile||((rt=(Ne=W.value.openrouter)==null?void 0:Ne.catalogue_profiles)==null?void 0:rt[R.id])||(R.context_length>0&&R.max_completion_tokens>0?{total_window_tokens:R.context_length,max_output_tokens:R.max_completion_tokens}:null);return{...R,profile:q}})}),Pn=f(""),mn=f(""),$n=f(!0),Ua=f(!0),vn=f(!0),Ie=f(!1),D=f(null),pe=f(""),ke=f(null),Be=f(""),Ue=f([]),Ke=f(!1),$t=f("throughput"),gt=(v,R,q=null)=>{const ye=v[R],Ne=q&&ye&&typeof ye=="object"?ye[q]:ye,rt=Number(Ne);return Number.isFinite(rt)?rt:null},Ot=B(()=>{const v=[...Ue.value],R=$t.value;return v.sort((q,ye)=>{var Pe,Ft;if(R==="quantization")return String(q.quantization).localeCompare(String(ye.quantization));const[Ne,rt,Lt]=R==="throughput"?["throughput_last_30m","p50",!0]:R==="latency_p99"?["latency_last_30m","p99",!1]:R==="cache_price"?["cache_read_per_token",null,!1]:["prompt_per_token",null,!1],wa=(Pe=q.pricing)==null?void 0:Pe[Ne],xn=(Ft=ye.pricing)==null?void 0:Ft[Ne],at=rt?gt(q,Ne,rt):wa==null?null:Number(wa),Kt=rt?gt(ye,Ne,rt):xn==null?null:Number(xn);return Number.isFinite(at)?Number.isFinite(Kt)?Lt?Kt-at:at-Kt:-1:1})}),ds=B(()=>{try{return new URL(W.value.base_url).hostname}catch{return""}}),ut=B(()=>/(^|\.)openrouter\.ai$/i.test(ds.value)),ua=B(()=>{var v,R;return((R=(v=p.value)==null?void 0:v.openai_compatible)==null?void 0:R.base_url)||""}),hn=B(()=>{try{return new URL(ua.value).href}catch{return""}}),_o=B(()=>{try{return new URL(W.value.base_url).href}catch{return""}}),As=B(()=>w("compat")&&ut.value&&!!hn.value&&hn.value===_o.value);Gt(()=>[As.value,_o.value,hn.value],()=>{++ya,kt.value=null,Wt.value="",$s.value=!1,So()},{flush:"sync"});const vc=B(()=>{var v,R;return ut.value?$s.value?"OpenRouter recognized · fetching catalogue…":Wt.value?`OpenRouter recognized · catalogue failed: ${Wt.value}`:`OpenRouter recognized · ${((R=(v=kt.value)==null?void 0:v.models)==null?void 0:R.length)||0} catalogue models loaded`:Es.value.length?`${Es.value.length} endpoint models loaded`:"Catalogue not loaded"}),hc=B(()=>Wt.value?"text-red-400":"text-gray-500"),wo=B(()=>{var v;return[...new Set((((v=kt.value)==null?void 0:v.models)||[]).map(R=>R.vendor))].sort()}),gc=B(()=>{var v;return[...new Set((((v=kt.value)==null?void 0:v.models)||[]).flatMap(R=>(R.endpoints||[]).map(q=>q.quantization)).filter(Boolean))].sort()}),bc=v=>{var R;return(((R=kt.value)==null?void 0:R.measured_cache)||[]).some(q=>q.model===v.id&&q.samples>0&&q.cached_percent>0)},ko=B(()=>{const v=Pn.value.trim().toLowerCase();return xa.value.filter(R=>{var q;return!(v&&!`${R.id} ${R.name} ${R.vendor}`.toLowerCase().includes(v)||mn.value&&R.vendor!==mn.value||$n.value&&!R.supports_tools||Ua.value&&!R.agent_eligible||vn.value&&R.variant!=="standard"||Ie.value&&!bc(R)||D.value!=null&&Number((q=R.pricing)==null?void 0:q.prompt_per_token)*1e6>Number(D.value)||pe.value&&!(R.endpoints||[]).some(ye=>ye.quantization===pe.value))})}),yc=B(()=>ko.value.slice(0,100)),xc=B(()=>ko.value.length),gn=B(()=>new Map((As.value?xa.value:[]).map(v=>[`compat:${v.id}`,v]))),bn=v=>v==null?"n/a":`$${(Number(v)*1e6).toFixed(3)}/M`,mi=v=>{var R,q,ye,Ne;return[v.vendor,v.context_length?`${Number(v.context_length).toLocaleString()} ctx`:"context unknown",`${bn((R=v.pricing)==null?void 0:R.prompt_per_token)} in`,`${bn((q=v.pricing)==null?void 0:q.completion_per_token)} out`,`${bn((ye=v.pricing)==null?void 0:ye.cache_read_per_token)} cache read`,`${bn((Ne=v.pricing)==null?void 0:Ne.cache_write_per_token)} cache write`,v.supports_tools?"tools":"no tools",v.supports_reasoning?"reasoning":"no reasoning",v.variant!=="standard"?v.variant:null].filter(Boolean).join(" · ")},Fn=v=>{var Ne;const R=gn.value.get(v);if(!R)return"Catalogue facts unavailable";const q=(((Ne=kt.value)==null?void 0:Ne.measured_cache)||[]).filter(rt=>rt.model===R.id),ye=q.length?q.map(rt=>`${rt.upstream_provider}: ${rt.cached_percent}% cached`).join(" · "):"No measured cache evidence yet";return`${mi(R)} · ${ye}${R.profile_conflict?" · operator profile conflicts with catalogue":""}`},vi=B(()=>Es.value.map(v=>typeof v=="string"?v:v.name).filter(Boolean)),al=async()=>{var R,q,ye;const v=(ye=(q=(R=p.value)==null?void 0:R.openai_compatible)==null?void 0:q.preset_catalogue)==null?void 0:ye[W.value.preset];v&&(W.value.base_url=v.base_url),zn.cancel(),await dl()},C=(v,R)=>{W.value.openrouter[v]=R.split(",").map(q=>q.trim()).filter(Boolean)},K=B(()=>zt.value||[]),ne=B(()=>{const v=[...b,...vi.value.map(R=>`compat:${R}`),...K.value.map(R=>`ollama:${R.name}`)];for(const R of[Fe.value.model,...ot.value])R&&R!=="auto"&&!v.includes(R)&&v.unshift(R);return v}),Ee=v=>v==="codex-auto-review"?"codex-auto-review (Codex alias → gpt-5.6-luna)":v;async function Je({poll:v=!1}={}){try{const R=await z.get("/api/agents/model");if(v&&Pt()||ue.value||Ce.value||u("agents",Fe.value))return;Fe.value={...Fe.value,...R},r.agents=c(Fe.value)}catch{}}async function St(){const v=ds.value,R=ua.value,q=++ya;if(!As.value){kt.value=null,Wt.value="",$s.value=!1;return}$s.value=!0;try{const ye=await z.get("/api/openrouter/catalogue");if(q!==ya||!As.value||v!==ds.value||R!==ua.value)return;kt.value=ye,Wt.value=""}catch(ye){if(q!==ya||!As.value)return;Wt.value=ye.message||"Failed to load OpenRouter catalogue"}finally{q===ya&&($s.value=!1)}}async function Ks(){var v;try{(v=Fe.value.model)!=null&&v.startsWith("compat:")&&ut.value&&await nl(il(Fe.value.model),Co(Fe.value.model));const R=await z.put("/api/agents/model",{model:Fe.value.model||null});Fe.value={...Fe.value,...R},r.agents=c(Fe.value),He("Agent model policy saved")}catch(R){He(R.message||"Failed to save agent model policy","error")}}const gs=()=>[...Ge.value.length?Ge.value:ot.value];async function yn(v,R){const q=gs(),ye=q.findIndex(Lt=>qe(Lt)===v);let Ne=null;if(R.target.checked&&ye<0&&q.push(Fa.get(v)||v),!R.target.checked&&ye>=0&&([Ne]=q.splice(ye,1)),!q.length){R.target.checked=!0,He("Keep one model selected. An empty list restores the provider default.","error");return}const rt=await Rs(q,"Agent Auto allowlist saved");rt&&R.target.checked&&Fa.delete(v),rt&&!R.target.checked&&typeof Ne=="object"&&Fa.set(v,Ne),rt||(R.target.checked=ot.value.includes(v))}async function Rs(v,R){if(Ce.value)return!1;Ce.value=!0;try{const q=await z.put("/api/agents/model",{auto_model_allowlist:v});return Fe.value={...Fe.value,...q},r.agents=c(Fe.value),He(R),!0}catch(q){return He(q.message||"Failed to save agent allowlist","error"),!1}finally{Ce.value=!1}}const db=()=>Rs([],"Provider default restored");async function ub(v,R,q){const ye=ot.value.map(Ne=>{const rt=ae(Ne);if(Ne!==v)return rt||Ne;const Lt=typeof rt=="string"?{model:Ne}:{...rt||{model:Ne}};return delete Lt.reasoning_effort,delete Lt.thinking_mode,q&&(Lt[R]=q),Object.keys(Lt).length===1?Lt.model:Lt});await Rs(ye,"Model default saved")}async function nl(v,R=""){const[q,...ye]=v.split("/");if(!q||!ye.length)throw new Error("OpenRouter model id is not namespaced");return z.post(`/api/openrouter/models/${encodeURIComponent(q)}/${encodeURIComponent(ye.join("/"))}/select`,{provider_tag:R})}const pb=(v,R)=>{var ye;const q=(((ye=kt.value)==null?void 0:ye.measured_cache)||[]).find(Ne=>Ne.model===v&&Ne.upstream_provider===R);return q?`${q.cached_percent}% cached over ${q.samples} calls`:"no measured cache evidence"},fb=v=>v==null?"n/a":`$${(Number(v)*1e6).toFixed(4)}/M`,mb=(v,R)=>{if(v==null)return"n/a";if(typeof v=="number")return Number(v).toLocaleString();const q=v[R];return q==null?"n/a":Number(q).toLocaleString()},vb=v=>{var Ne;const R=[];v.quantization==="fp4"&&R.push("fp4 quantization may change quality");const q=typeof v.latency_last_30m=="object"?Number((Ne=v.latency_last_30m)==null?void 0:Ne.p99):null,ye=Number(Fe.value.iteration_timeout_seconds||0)*1e3;return q&&ye&&q>ye&&R.push("p99 exceeds the agent iteration budget"),R.join("; ")};async function hb(v){var R;ke.value=v,Be.value=((R=W.value.openrouter.model_pins)==null?void 0:R[v.id])||"",Ke.value=!0;try{const[q,...ye]=v.id.split("/"),Ne=await z.get(`/api/openrouter/models/${encodeURIComponent(q)}/${encodeURIComponent(ye.join("/"))}/endpoints`);Ue.value=Ne.endpoints||[]}catch(q){Ue.value=[],He(q.message||"Failed to load OpenRouter provider routes","error")}finally{Ke.value=!1}}function So(){ke.value=null,Be.value="",Ue.value=[]}async function gb(v,R=""){try{await nl(v.id,R);const q=gs(),ye=`compat:${v.id}`;if(q.some(rt=>qe(rt)===ye)||q.push(ye),!await Rs(q,R?"OpenRouter model added and provider pinned.":"OpenRouter model added unpinned."))return;So(),await _a()}catch(q){He(q.message||"Failed to add OpenRouter model","error")}}const il=v=>v.startsWith("compat:")?v.slice(7):v,Co=v=>{var R;return((R=W.value.openrouter.model_pins)==null?void 0:R[il(v)])||""},bb=v=>ot.value.length>1&&Rs(gs().filter(R=>qe(R)!==v),"Model removed from allowlist");async function yb(){var v,R;try{const q=gs();for(const ye of((v=kt.value)==null?void 0:v.quick_add)||[])(R=gn.value.get(ye))!=null&&R.agent_eligible&&(await nl(il(ye),""),q.some(Ne=>qe(Ne)===ye)||q.push(ye));await Rs(q,"Curated OpenRouter models added"),await _a()}catch(q){He(q.message||"Failed to add curated OpenRouter models","error")}}function ap(v){const R=v.hint_metadata||{},q=[];return R.context_tokens&&q.push(`context ${Number(R.context_tokens).toLocaleString()}`),R.max_output_tokens&&q.push(`max output ${Number(R.max_output_tokens).toLocaleString()}`),R.structural_source&&q.push(`source ${R.structural_source}`),q.join("; ")}async function xb(v,R){const q={...Fe.value.model_selection_hints||{}},ye=R.trim();ye?q[v]=ye:delete q[v];try{const Ne=await z.put("/api/agents/model",{model_selection_hints:q});Fe.value={...Fe.value,...Ne},r.agents=c(Fe.value),He("Model hint saved")}catch(Ne){He(Ne.message||"Failed to save model hint","error")}}function np(v,R){const q=ot.value.indexOf(v);return!Ce.value&&q>=0&&q+R>=0&&q+Rqe(Ne)===v);ye<0||!np(v,R)||([q[ye],q[ye+R]]=[q[ye+R],q[ye]],await Rs(q,"Agent Auto allowlist order saved"))}const _c=f(!0),wc=f(""),ip=f({configured:null,accounts:[]}),kc=f(null),To=f(null),Sc=f(""),ll=f(null),Cc=f(!1),Tc=f(null),lp=f(null),op=f("");let hi=null;function He(v,R="success"){Se(v,R==="error"?"error":"success")}function wb(v){if(!v)return"?";const R=v/(1024*1024*1024);return R>=1?R.toFixed(1)+" GB":(v/(1024*1024)).toFixed(0)+" MB"}function kb(v){return Number.isFinite(Number(v))?Number(v).toLocaleString():"—"}function Sb(v){return v==null?"automatic (model-derived)":Number(v).toLocaleString()+" characters"}function Cb(v){const R=new Date(v);return Number.isNaN(R.getTime())?"unknown":R.toLocaleString([],{dateStyle:"medium",timeStyle:"short"})}function Tb(v){const R=new Date(Number(v)*1e3);return Number.isNaN(R.getTime())?"unknown":R.toLocaleString([],{dateStyle:"medium",timeStyle:"short"})}function Eb(v){const R=Number(v);if(!Number.isFinite(R)||R<=0)return"unknown";const q=Math.max(0,Math.floor((Date.now()/1e3-R)/60));return q<1?"<1 min":`${q} min`}function Ab(v){return typeof v=="string"&&v.length>12?v.slice(0,8)+"…"+v.slice(-4):v}function Rb(v){return typeof v!="number"||!Number.isFinite(v)?"—":(v/1e3).toFixed(2)}function Ib(v){return v==="temporary learned clamp"?"is-clamp":v==="override"?"is-override":"is-built-in"}function Ob(v){const R=g.value.context_budget_overrides[v.model];return v.floor!=null&&Number.isFinite(Number(R))&&Number(R)>v.floor}function Lb(v,R){const q={...g.value.context_budget_overrides};R.target.value===""?delete q[v]:q[v]=Number(R.target.value),g.value.context_budget_overrides=q,it.value=!0}function Nb(v){g.value.context_utilization=v.target.value===""?"":Number(v.target.value),it.value=!0}function Mb(v){const R={...g.value.context_budget_overrides};delete R[v],g.value.context_budget_overrides=R,it.value=!0}async function _a({quiet:v=!1,poll:R=!1}={}){if(!(R&&Pt())){if(l&&await l,i)return i;v||(e.value=!0),i=(async()=>{await Promise.all([bs({poll:R}),ol(),rl(),Je({poll:R}),pa(),Bn({poll:R})]),await St(),Pt()||Ts()})();try{await i}finally{i=null,v||(e.value=!1)}}}async function Db(){if(!(Pt()||i)){if(l)return l;l=Promise.all([bs({poll:!0}),ol({refreshModels:!1}),rl({refreshModels:!1}),Je({poll:!0}),pa(),Bn({poll:!0})]).then(()=>{Pt()||Ts()});try{await l}finally{l=null}}}async function bs({preserveBasic:v=!1,preserveAdvanced:R=!1,poll:q=!1}={}){var ye,Ne,rt,Lt,wa,xn;try{const at=await z.get("/api/llm/status");p.value=at,m.value=!1,!o&&!u("mainModel",h.value.main)&&(h.value.main=at.main_model||at.active_model||(at.active_provider==="compat"?`compat:${((ye=at.openai_compatible)==null?void 0:ye.model)||""}`:at.active_provider==="ollama"?`ollama:${((Ne=at.ollama)==null?void 0:Ne.model)||""}`:((rt=at.codex)==null?void 0:rt.model)||"gpt-6-sol")),at.codex&&!gi.pending()&&(!v&&!Ye.value&&!u("codexBasic",yl(g.value))&&(g.value.enabled=at.codex.enabled,g.value.model=at.codex.model||"gpt-6-sol",g.value.reasoning_effort=at.codex.reasoning_effort||"medium",g.value.agent_reasoning_effort=at.codex.agent_reasoning_effort||""),!R&&!Ye.value&&!u("codexAdvanced",xl(g.value))&&(g.value.request_timeout_seconds=at.codex.request_timeout_seconds??g.value.request_timeout_seconds,g.value.stream_stall_timeout_seconds=at.codex.stream_stall_timeout_seconds??g.value.stream_stall_timeout_seconds,g.value.retry={...g.value.retry,...at.codex.retry||{}},g.value.connection_pool={...g.value.connection_pool,...at.codex.connection_pool||{}},g.value.context_compression={...g.value.context_compression,...at.codex.context_compression||{}},!it.value&&!Ye.value&&(g.value.context_budget_overrides={...at.codex.context_budget_overrides||{}},g.value.context_utilization=at.codex.context_utilization??g.value.context_utilization))),at.ollama&&!bi.pending()&&(!v&&!Dt.value&&!u("ollamaBasic",_l(Te.value))&&(Te.value.enabled=at.ollama.enabled,Te.value.base_url=at.ollama.base_url||"",Te.value.model=at.ollama.model||"",Te.value.max_tokens=at.ollama.max_tokens||4096,Te.value.num_ctx=at.ollama.num_ctx||32768),!R&&!Dt.value&&!u("ollamaAdvanced",Jo(Te.value))&&(Te.value.timeout=at.ollama.timeout??Te.value.timeout));const Kt=at.openai_compatible;Kt&&!zn.pending()&&(!v&&!et.value&&!u("compatibleBasic",bl(W.value))&&(W.value.enabled=Kt.enabled,W.value.base_url=Kt.base_url||W.value.base_url,W.value.model=Kt.model||W.value.model,W.value.preset=Kt.preset||W.value.preset,W.value.reasoning_effort=Kt.reasoning_effort||"medium"),!R&&!et.value&&!u("compatibleAdvanced",Ko(W.value))&&(W.value.request_timeout_seconds=Kt.request_timeout_seconds??W.value.request_timeout_seconds,W.value.stream_stall_timeout_seconds=Kt.stream_stall_timeout_seconds??W.value.stream_stall_timeout_seconds,W.value.model_profiles=Kt.model_profiles||W.value.model_profiles,W.value.context_utilization=Kt.context_utilization??W.value.context_utilization,W.value.openrouter={...W.value.openrouter,...Kt.openrouter||{}})),!o&&!u("mainModel",h.value.main)&&((Lt=h.value.main)!=null&&Lt.startsWith("compat:")?h.value.main_capability=(Kt==null?void 0:Kt.reasoning_effort)||"medium":(wa=h.value.main)!=null&&wa.startsWith("ollama:")||(h.value.main_capability=((xn=at.codex)==null?void 0:xn.reasoning_effort)||"medium")),at.auxiliary&&(_e.value=at.auxiliary,!Un.pending()&&!ge.value&&!u("auxiliary",X.value)&&(X.value.enabled=at.auxiliary.enabled,X.value.model=at.auxiliary.model||"gpt-6-luna"))}catch{p.value||(p.value={active_provider:"",codex:{configured:null},ollama:{configured:null},openai_compatible:{configured:null}}),m.value=!0}}async function Bn({poll:v=!1}={}){const R=++Qe;We.value=!0,je.value="";try{const q=await z.get("/api/context/windows");if(R!==Qe)return;$e.value=q,!(v&&Pt())&&!Ye.value&&!it.value&&(g.value.context_budget_overrides=Object.fromEntries(Object.entries(q.models||{}).filter(([,ye])=>ye.override!=null).map(([ye,Ne])=>[ye,Ne.override])),g.value.context_utilization=q.utilization??g.value.context_utilization)}catch(q){R===Qe&&(je.value=q.message||"Failed to load context budgets")}finally{R===Qe&&(We.value=!1)}}async function ol({refreshModels:v=!0}={}){try{if(Vs.value=await z.get("/api/ollama/status"),Gs.value=!1,Vs.value.model&&(ta.value=Vs.value.model),v&&Vs.value.configured)try{const R=await z.get("/api/ollama/models");zt.value=R.models||[]}catch{zt.value=[]}else if(v&&Te.value.base_url)try{const R=await z.post("/api/ollama/probe-models",{base_url:Te.value.base_url});zt.value=R.models||[]}catch{zt.value=[]}}catch{Gs.value=!0}}async function pa(){_c.value=!0,wc.value="";try{ip.value=await z.get("/api/codex/status")}catch(v){wc.value=v.message||"Failed to fetch Codex status"}finally{_c.value=!1}}async function Pb(){if(o)return;o=!0;const v=h.value.main;try{v.startsWith("compat:")&&ut.value&&await nl(il(v),Co(v));try{await z.put("/api/llm/main-model",{model:v})}catch(R){if(!/404|not found/i.test(R.message||""))throw R;await z.post("/api/llm/switch",{model:v})}d("mainModel",v),He("Main model saved"),await _a()}catch(R){He(R.message||"Failed to save main model","error"),await bs()}finally{o=!1}}async function $b(v){h.value.main_capability=v;const R=re.value;R&&(R.provider==="compat"&&R.capability!=="none"?(W.value.reasoning_effort=v,await dl()):R.capability==="reasoning"?(g.value.reasoning_effort=v,await cl()):R.capability==="thinking"&&(await z.put("/api/openai-compatible/config",{thinking_mode:v}),He("Thinking mode saved")))}async function Fb(v){h.value.agent_capability=v;const R=ce.value,q=(R==null?void 0:R.capability)||oe.value;if(q!=="none"){if(v===""||v==="auto"||q==="reasoning"||q==="mixed"){if(g.value.agent_reasoning_effort=v,q==="thinking"){const ye=await z.put("/api/agents/model",{thinking_mode:null});Fe.value={...Fe.value,...ye}}await cl()}else if(q==="thinking"){const ye=await z.put("/api/agents/model",{thinking_mode:v});Fe.value={...Fe.value,...ye},He("Agent thinking mode saved")}}}async function Bb(){Ws.value=!0;try{const v=await z.post("/api/ollama/reload");He(v.configured?"Ollama reloaded":v.reason||"Ollama not configured",v.configured?"success":"error"),await _a()}catch(v){He(v.message||"Reload failed","error")}finally{Ws.value=!1}}async function Ub(){Pa.value=!0;try{await z.post("/api/ollama/model",{model:ta.value}),He("Model set to "+ta.value),await _a()}catch(v){He(v.message||"Failed","error")}finally{Pa.value=!1}}async function zb(){const v=Te.value.base_url;if(!v){He("Enter a base URL first","error");return}Rt.value=!0;try{const R=await z.post("/api/ollama/probe-models",{base_url:v});zt.value=R.models||[],zt.value.length?(He(zt.value.length+" model(s) found"),!Te.value.model&&zt.value.length&&(Te.value.model=zt.value[0].name)):He("No models found at "+v,"error")}catch(R){He(R.message||"Could not reach Ollama","error")}finally{Rt.value=!1}}async function rl({refreshModels:v=!0}={}){try{if(hs.value=await z.get("/api/openai-compatible/status"),$a.value=!1,hs.value.model&&(ls.value=hs.value.model),v&&hs.value.configured)try{const R=await z.get("/api/openai-compatible/models");Es.value=R.models||[]}catch{Es.value=[]}}catch{$a.value=!0}}async function Hb(){sa.value=!0;try{const v=await z.post("/api/openai-compatible/reload");He(v.configured?"OpenAI-compatible reloaded":v.reason||"OpenAI-compatible not configured",v.configured?"success":"error"),await _a()}catch(v){He(v.message||"Reload failed","error")}finally{sa.value=!1}}async function jb(){ba.value=!0;try{await z.post("/api/openai-compatible/model",{model:ls.value}),He("Model set to "+ls.value),await _a()}catch(v){He(v.message||"Failed","error")}finally{ba.value=!1}}async function cl(){var R,q;if(Ye.value){gi();return}Ye.value=!0;const v=yl(g.value);try{if(!await O("codex",v.enabled)){g.value.enabled=((q=(R=p.value)==null?void 0:R.codex)==null?void 0:q.enabled)===!0;return}await z.put("/api/llm/codex/config",v),d("codexBasic",v),He("Codex config saved"),await Promise.all([bs({preserveBasic:!0,preserveAdvanced:!0}),pa()])}catch(ye){He(ye.message||"Failed","error");const Ne=JSON.stringify(yl(g.value))!==JSON.stringify(v);await Promise.all([bs({preserveBasic:Ne,preserveAdvanced:!0}),pa()])}finally{Ye.value=!1}}async function rp(){if(Ye.value)return;Ye.value=!0;const v=xl(g.value);try{await z.put("/api/llm/codex/config",v),d("codexAdvanced",v),JSON.stringify({context_budget_overrides:g.value.context_budget_overrides,context_utilization:g.value.context_utilization})===JSON.stringify({context_budget_overrides:v.context_budget_overrides,context_utilization:v.context_utilization})&&(it.value=!1),He("Codex advanced settings saved"),await Promise.all([bs({preserveBasic:!0,preserveAdvanced:!0}),pa(),Bn()])}catch(R){He(R.message||"Failed","error");const q=JSON.stringify(xl(g.value))!==JSON.stringify(v);await Promise.all([bs({preserveBasic:!0,preserveAdvanced:q}),pa(),Bn()])}finally{Ye.value=!1}}async function Ec(){var v,R;if(Dt.value){bi();return}Dt.value=!0;try{const q=De.value?Te.value.api_key:null,ye=_l(Te.value,{includeApiKey:q!==null});if(!await O("ollama",ye.enabled)){Te.value.enabled=((R=(v=p.value)==null?void 0:v.ollama)==null?void 0:R.enabled)===!0;return}await z.put("/api/llm/ollama/config",ye),He("Ollama config saved"),q!==null&&Te.value.api_key===q&&(Te.value.api_key="",De.value=!1),d("ollamaBasic",_l(Te.value)),await Promise.all([bs({preserveBasic:!0,preserveAdvanced:!0}),ol()])}catch(q){He(q.message||"Failed","error"),await bs({preserveBasic:!0,preserveAdvanced:!0})}finally{Dt.value=!1}}async function cp(){if(!Dt.value){Dt.value=!0;try{const v=Jo(Te.value);await z.put("/api/llm/ollama/config",v),d("ollamaAdvanced",v),He("Ollama timeout saved"),await Promise.all([bs({preserveBasic:!0,preserveAdvanced:!0}),ol()])}catch(v){He(v.message||"Failed","error")}finally{Dt.value=!1}}}async function dl(){var v,R;if(et.value){zn();return}et.value=!0;try{const q=ze.value?W.value.api_key:null,ye=bl(W.value,{includeApiKey:q!==null});if(!await O("compat",ye.enabled)){W.value.enabled=((R=(v=p.value)==null?void 0:v.openai_compatible)==null?void 0:R.enabled)===!0;return}await z.put("/api/openai-compatible/config",ye),He("OpenAI-compatible config saved"),q!==null&&W.value.api_key===q&&(W.value.api_key="",ze.value=!1),d("compatibleBasic",bl(W.value)),await Promise.all([bs({preserveBasic:!0,preserveAdvanced:!0}),rl()]),await St()}catch(q){He(q.message||"Failed","error"),await bs({preserveBasic:!0,preserveAdvanced:!0})}finally{et.value=!1}}async function dp(){if(!et.value){et.value=!0;try{const v=Ko(W.value);await z.put("/api/openai-compatible/config",v),d("compatibleAdvanced",v),He("OpenAI-compatible endpoint settings saved"),await Promise.all([bs({preserveBasic:!0,preserveAdvanced:!0}),rl()]),await St()}catch(v){He(v.message||"Failed","error")}finally{et.value=!1}}}async function qb(){if(ge.value){Un();return}ge.value=!0;try{const v={...X.value};await z.put("/api/llm/auxiliary/config",v),d("auxiliary",v),He("Auxiliary config saved"),await bs()}catch(v){He(v.message||"Failed","error"),await bs()}finally{ge.value=!1}}const gi=Zo(cl),Un=Zo(qb),bi=Zo(Ec),zn=Zo(dl),Vb=()=>(gi.cancel(),cl()),Gb=()=>(bi.cancel(),Ec()),Wb=()=>(zn.cancel(),dl()),Kb=()=>rp(),Jb=()=>cp(),Zb=()=>dp();async function Qb(v){const R=v.account_key+":"+v.model;Xe.value=R;try{const q=await z.post("/api/context/windows/clear",{account_key:v.account_key,model:v.model});He(q.cleared?"Temporary clamp cleared":"Clamp was already inactive"),await Bn()}catch(q){He(q.message||"Failed to clear clamp","error"),await Bn()}finally{Xe.value=null}}async function Yb(v){try{await z.post("/api/codex/account/"+v+"/activate"),He("Active account switched"),await pa()}catch(R){He(R.message||"Failed","error")}}async function Xb(v){kc.value=v;try{await z.post("/api/codex/account/"+v+"/refresh"),He("Token refreshed"),await pa()}catch(R){He(R.message||"Refresh failed","error")}finally{kc.value=null}}function ey(v,R){To.value=v,Sc.value=R||""}async function ty(v){try{await z.put("/api/codex/account/"+v+"/label",{label:Sc.value}),He("Label updated"),To.value=null,await pa()}catch(R){He(R.message||"Failed","error")}}async function sy(v,R){if(await is({title:"Delete Codex account",message:`Delete ${R||"account #"+(v+1)}? The pool will reload without it.`,confirmLabel:"Delete",danger:!0}))try{await z.del("/api/codex/account/"+v),He("Deleted. Pool reloaded."),await pa()}catch(ye){He(ye.message||"Failed","error")}}async function ay(){Cc.value=!0;try{const v=await z.post("/api/codex/device-code");Tc.value=v,ll.value="pending",ny(v)}catch(v){He(v.message||"Failed","error")}finally{Cc.value=!1}}async function ny(v){hi={cancelled:!1};const R=hi;try{const q=await z.post("/api/codex/device-poll",{device_auth_id:v.device_auth_id,user_code:v.user_code,interval:v.interval});if(R.cancelled)return;lp.value=q,ll.value="success",await _a()}catch(q){if(R.cancelled)return;op.value=q.message||"Device login failed",ll.value="error"}}function iy(){hi&&(hi.cancelled=!0),ll.value=null,Tc.value=null}function up(){n||(n=!0,_a(),a=window.setInterval(Db,s))}function pp(){n&&(n=!1,a!==null&&window.clearInterval(a),a=null)}return st(up),cs(up),es(pp),wt(()=>{pp(),hi&&(hi.cancelled=!0),gi.cancel(),Un.cancel(),bi.cancel(),zn.cancel()}),{pageRoot:t,savedProviderEnabled:w,codexOnly:_,codexChoices:ee,configuredDisabledSelection:T,configuredDisabledLabel:E,disableWarningRoles:F,effectiveAutoAllowlistCount:pn,allowlistModalOpen:ue,closeAllowlistModal:fi,allowlistSaving:Ce,effectiveAllowlist:ot,allowlistSummary:pi,resetAgentAllowlist:db,selectedUnavailableReason:Ba,selectedModelFacts:fn,loading:e,llmStatus:p,llmStatusLoadFailed:m,modelSelection:h,modelSelectorSearch:y,reasoningEfforts:k,neutralReasoningLevels:A,modelCatalog:I,modelGroups:te,selectedMainModel:re,selectedAgentModel:ce,selectedAgentCapabilityValue:j,agentCapabilityKind:oe,agentCapabilityEfforts:ve,autoAllowlistModels:be,allowlistModel:Z,allowlistModelEfforts:Q,allowlistEntryCapabilityValue:V,modelOptionLabel:Y,agentModelAvailable:N,agentModelOptionLabel:M,advancedOpen:Ae,codexForm:g,codexModelOptions:xe,codexAgentModelOptions:S,mainEffortAllowed:fe,agentEffortAllowed:me,mainModelOptionDisabled:J,agentModelOptionDisabled:ie,auxForm:X,auxData:_e,auxModelOptions:U,onAuxModelChange:de,savingAux:ge,saveAuxConfigDebounced:Un,ollamaForm:Te,compatibleForm:W,savingCodex:Ye,savingOllama:Dt,savingCompatible:et,probingOllama:Rt,ollamaKeyDirty:De,compatibleKeyDirty:ze,fetchCodexStatus:pa,ollamaStatus:Vs,ollamaStatusLoadFailed:Gs,ollamaModels:zt,ollamaSelectedModel:ta,reloading:Ws,settingModel:Pa,compatibleStatus:hs,compatibleStatusLoadFailed:$a,compatibleModels:Es,visibleCompatibleModels:Ct,compatibleSelectedModel:ls,reloadingCompatible:sa,settingCompatibleModel:ba,applyCompatiblePreset:al,setOpenRouterList:C,agentsConfig:Fe,compatibleAgentModels:vi,ollamaAgentModels:K,knownAgentModelRefs:ne,agentModelLabel:Ee,saveAgentsModel:Ks,toggleAgentAutoAllowlist:yn,saveAllowlistEntryCapability:ub,autoAllowlistGroups:$,structuralFacts:ap,saveModelHint:xb,canMoveAllowlist:np,moveAgentAutoAllowlist:_b,openRouterCatalogue:kt,openRouterCatalogueLoading:$s,openRouterCatalogueError:Wt,openRouterRecognized:ut,openRouterCatalogueActive:As,compatibleCatalogueStatus:vc,compatibleCatalogueStatusClass:hc,openRouterSearch:Pn,openRouterVendor:mn,openRouterVendors:wo,openRouterToolsOnly:$n,openRouterEligibleOnly:Ua,openRouterStandardOnly:vn,openRouterMeasuredCacheOnly:Ie,openRouterMaxPromptPrice:D,openRouterQuantization:pe,openRouterQuantizations:gc,openRouterResults:yc,openRouterMatchCount:xc,openRouterInlineFacts:mi,openRouterSelectedFacts:Fn,prepareOpenRouterModel:hb,addOpenRouterModel:gb,removeOpenRouterModel:bb,quickAddOpenRouter:yb,openRouterModelMap:gn,openRouterPin:Co,openRouterPendingModel:ke,openRouterPendingTag:Be,openRouterPendingEndpoints:Ue,openRouterPendingLoading:Ke,openRouterEndpointSort:$t,openRouterSortedPendingEndpoints:Ot,openRouterEndpointCacheFact:pb,openRouterRate:fb,openRouterMetric:mb,openRouterRouteWarning:vb,cancelOpenRouterPending:So,codexLoading:_c,codexError:wc,codexData:ip,refreshing:kc,editingLabel:To,labelValue:Sc,contextWindows:$e,contextWindowsLoading:We,contextWindowsError:je,contextBudgetRows:se,activeClampRows:Re,activeContextBudget:Le,clearingClamp:Xe,contextPolicyDirty:it,deviceState:ll,deviceLoading:Cc,deviceInfo:Tc,deviceResult:lp,deviceError:op,fetchAll:_a,fetchLLMStatus:bs,fetchOllamaStatus:ol,fetchCompatibleStatus:rl,saveMainModel:Pb,saveMainCapability:$b,saveAgentCapability:Fb,reloadOllama:Bb,setOllamaModel:Ub,reloadCompatible:Hb,setCompatibleModel:jb,probeOllamaModels:zb,saveCodexConfig:cl,saveOllamaConfig:Ec,saveCompatibleConfig:dl,saveCodexAdvancedConfig:rp,saveOllamaAdvancedConfig:cp,saveCompatibleAdvancedConfig:dp,saveCodexConfigDebounced:gi,saveOllamaConfigDebounced:bi,saveCompatibleConfigDebounced:zn,saveCodexConfigNow:Vb,saveOllamaConfigNow:Gb,saveCompatibleConfigNow:Wb,saveCodexAdvancedConfigNow:Kb,saveOllamaAdvancedConfigNow:Jb,saveCompatibleAdvancedConfigNow:Zb,activateAccount:Yb,refreshAccount:Xb,startEditLabel:ey,saveLabel:ty,deleteAccount:sy,startDeviceLogin:ay,cancelDeviceLogin:iy,formatSize:wb,fetchContextWindows:Bn,clearContextClamp:Qb,setContextOverride:Lb,setContextUtilization:Nb,resetContextOverride:Mb,overrideAboveFloor:Ob,formatCount:kb,formatContextCeiling:Sb,formatExpiry:Cb,shortAccountKey:Ab,provenanceClass:Ib,formatDensity:Rb,quotaBlocks:QC,quotaFailureVisible:YC,quotaAge:Eb,formatQuotaDate:Tb}}},tm={ok:"text-green-400",pass:"text-green-400",degraded:"text-yellow-400",warn:"text-yellow-400",down:"text-red-400",fail:"text-red-400",unconfigured:"text-gray-500",skipped:"text-gray-500"};function nT(e){return tm[e]||tm[(e||"").toLowerCase()]||"text-gray-400"}const iT={template:`
@@ -6386,7 +6403,7 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p(!0),t=p({}),s=p([]),a=p({}),n=p({}),i=p(null),l=p(null),o=p(null),r=p(null),c=p(null),d=U(()=>{var _;return Object.values(((_=i.value)==null?void 0:_.totals)||{}).reduce((T,L)=>T+Number(L||0),0)}),u=p(""),f=p(0),m=p([]),h=U(()=>m.value.map(_=>`${_.label} (${_.path}${_.reason?`: ${_.reason}`:""})`).join("; ")),g=Object.freeze([{key:"startup",label:"Startup diagnostics",path:"/api/startup/diagnostics"},{key:"subsystems",label:"Subsystem status",path:"/api/subsystems/status"},{key:"sshPool",label:"SSH pool",path:"/api/pools/ssh"},{key:"httpPool",label:"HTTP pool",path:"/api/pools/http"},{key:"riskStats",label:"Risk stats",path:"/api/risk/stats"},{key:"recoveryStats",label:"Recovery stats",path:"/api/recovery/stats"},{key:"compressionStats",label:"Compression stats",path:"/api/compression/stats"},{key:"freshnessStats",label:"Freshness stats",path:"/api/freshness/stats"},{key:"governorStats",label:"Governor stats",path:"/api/governor/stats"}]);let A=null;async function w(){var P;const _=await Promise.allSettled(g.map(R=>z.get(R.path))),T=R=>_[R].status==="fulfilled"?_[R].value:null;t.value=T(0)||{};const L=T(1);s.value=Array.isArray(L)?L:L&&L.subsystems||[],a.value=T(2)||{},n.value=T(3)||{},i.value=T(4),l.value=T(5),o.value=T(6),r.value=T(7),c.value=T(8);const E=_.filter(R=>R.status==="rejected");if(m.value=_.flatMap((R,I)=>{var H;return R.status==="rejected"?[{...g[I],reason:((H=R.reason)==null?void 0:H.message)||"request failed"}]:[]}),f.value=m.value.length,E.length===_.length){const R=(P=E[0])==null?void 0:P.reason;u.value=(R==null?void 0:R.message)||"Failed to load internals"}else u.value="";e.value=!1}function y(){e.value=!0,u.value="",w()}let b=!1;function x(){b||(b=!0,w(),A||(A=setInterval(w,3e4)))}function S(){b&&(b=!1,A&&(clearInterval(A),A=null))}return tt(x),rs(x),Kt(S),xt(S),{loading:e,error:u,failedCount:f,failedEndpoints:m,failedEndpointSummary:h,endpoints:g,retry:y,startup:t,subsystems:s,sshPool:a,httpPool:n,riskStats:i,riskTotal:d,recoveryStats:l,compressionStats:o,freshnessStats:r,governorStats:c,statusColor:$C,formatAgeSeconds:s1}}},BC=1e4,Gf=3e4;function vl(e,t){return Math.max(0,e-t)}function jc(e,t){return new Set((e.operations||[]).map(a=>a.state)).has("MANUAL_RESOLUTION_REQUIRED")?0:e.expired_lease||e.status==="ACTIVE"&&(!e.lease_expires_at||e.lease_expires_at{var _;return Object.values(((_=i.value)==null?void 0:_.totals)||{}).reduce((T,L)=>T+Number(L||0),0)}),u=f(""),p=f(0),m=f([]),h=B(()=>m.value.map(_=>`${_.label} (${_.path}${_.reason?`: ${_.reason}`:""})`).join("; ")),y=Object.freeze([{key:"startup",label:"Startup diagnostics",path:"/api/startup/diagnostics"},{key:"subsystems",label:"Subsystem status",path:"/api/subsystems/status"},{key:"sshPool",label:"SSH pool",path:"/api/pools/ssh"},{key:"httpPool",label:"HTTP pool",path:"/api/pools/http"},{key:"riskStats",label:"Risk stats",path:"/api/risk/stats"},{key:"recoveryStats",label:"Recovery stats",path:"/api/recovery/stats"},{key:"compressionStats",label:"Compression stats",path:"/api/compression/stats"},{key:"freshnessStats",label:"Freshness stats",path:"/api/freshness/stats"},{key:"governorStats",label:"Governor stats",path:"/api/governor/stats"}]);let k=null;async function A(){var F;const _=await Promise.allSettled(y.map(O=>z.get(O.path))),T=O=>_[O].status==="fulfilled"?_[O].value:null;t.value=T(0)||{};const L=T(1);s.value=Array.isArray(L)?L:L&&L.subsystems||[],a.value=T(2)||{},n.value=T(3)||{},i.value=T(4),l.value=T(5),o.value=T(6),r.value=T(7),c.value=T(8);const E=_.filter(O=>O.status==="rejected");if(m.value=_.flatMap((O,I)=>{var H;return O.status==="rejected"?[{...y[I],reason:((H=O.reason)==null?void 0:H.message)||"request failed"}]:[]}),p.value=m.value.length,E.length===_.length){const O=(F=E[0])==null?void 0:F.reason;u.value=(O==null?void 0:O.message)||"Failed to load internals"}else u.value="";e.value=!1}function g(){e.value=!0,u.value="",A()}let b=!1;function x(){b||(b=!0,A(),k||(k=setInterval(A,3e4)))}function w(){b&&(b=!1,k&&(clearInterval(k),k=null))}return st(x),cs(x),es(w),wt(w),{loading:e,error:u,failedCount:p,failedEndpoints:m,failedEndpointSummary:h,endpoints:y,retry:g,startup:t,subsystems:s,sshPool:a,httpPool:n,riskStats:i,riskTotal:d,recoveryStats:l,compressionStats:o,freshnessStats:r,governorStats:c,statusColor:nT,formatAgeSeconds:k1}}},lT=1e4,sm=3e4;function wl(e,t){return Math.max(0,e-t)}function Qc(e,t){return new Set((e.operations||[]).map(a=>a.state)).has("MANUAL_RESOLUTION_REQUIRED")?0:e.expired_lease||e.status==="ACTIVE"&&(!e.lease_expires_at||e.lease_expires_at

Turn State

@@ -6547,7 +6564,7 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p(null),t=p(""),s=p(null),a=p(!1),n=p(0),i=p(null),l=p(""),o=p(null),r=p(!1),c=p(0),d=p(Date.now());let u=null,f=0,m=0;async function h(){const O=++f;a.value=!0;try{const $=await z.get("/api/turn-state/turns?limit=100");if(O!==f)return;t.value=$.availability,e.value=$.availability==="available"?$.data:null,s.value=null,n.value=Date.now()}catch($){if(O!==f)return;s.value=$.message||"Turn-state read failed",$.status===503&&(t.value="unavailable")}O===f&&(a.value=!1)}async function g(){const O=++m;r.value=!0;try{const $=await z.get("/api/turn-state/capacity-breakers");if(O!==m)return;l.value=$.availability,i.value=$.availability==="available"?$.data:null,o.value=null,c.value=Date.now()}catch($){if(O!==m)return;o.value=$.message||"Breaker read failed",$.status===503&&(l.value="unavailable")}O===m&&(r.value=!1)}function A(){h(),g()}const w=U(()=>e.value!==null&&vl(d.value,n.value)>Gf),y=U(()=>i.value!==null&&vl(d.value,c.value)>Gf),b=U(()=>w.value||y.value),x=U(()=>Math.round(vl(d.value,n.value)/1e3)),S=U(()=>Math.round(vl(d.value,c.value)/1e3));function _(O){return jc(O,d.value/1e3)}function T(O){return UC[_(O)]}const L=U(()=>{var ce;const O=[...((ce=e.value)==null?void 0:ce.turns)||[]],$=d.value/1e3;return O.sort((ue,Z)=>jc(ue,$)-jc(Z,$)||(Z.last_progress_at||0)-(ue.last_progress_at||0))});function E(O){return O.state==="closed"?"badge-success":O.state==="probing"?"badge-warning":"badge-danger"}function P(O){if(O.state==="closed")return"—";const $=vl(d.value,c.value)/1e3,ce=Math.max(0,(O.cooldown_remaining_seconds||0)-$);return ce>0?`${Math.ceil(ce)}s`:O.state==="probing"?"probe in flight":"probe eligible"}function R(O){if(!O)return"";const $=Math.max(0,Math.round(d.value/1e3-O));if($<90)return`${$}s ago`;const ce=Math.round($/60);return ce<90?`${ce}m ago`:`${Math.round(ce/60)}h ago`}let I=null,H=null,X=!1;function Q(){X||(X=!0,A(),I=setInterval(A,BC),u=setInterval(()=>{d.value=Date.now()},1e3),H=ut.onReconnected(A))}function N(){X&&(X=!1,I&&(clearInterval(I),I=null),u&&(clearInterval(u),u=null),H&&(H(),H=null))}return tt(Q),rs(Q),Kt(N),xt(N),{turnsData:e,turnsAvailability:t,turnsError:s,turnsLoading:a,breakersData:i,breakersAvailability:l,breakersError:o,breakersLoading:r,turnsStale:w,breakersStale:y,anyStale:b,turnsAgeSeconds:x,breakersAgeSeconds:S,sortedTurns:L,priorityOf:_,priorityBadge:T,breakerBadge:E,cooldownLabel:P,ageLabel:R,fetchTurns:h,fetchBreakers:g,refreshAll:A,arm:Q,disarm:N}}},HC={setup(){const e=p(""),t=p(""),s=p(!1),a=p(""),n=p(!1),i=p(!1),l=p(!1),o=p(null),r=p(!1);async function c(){n.value=!0,o.value=null,r.value=!1;try{const u=await z.get("/api/update/check");e.value=u.current||"",t.value=u.latest||"",s.value=u.update_available||!1,a.value=u.changelog||"",u.error&&(o.value=u.error),r.value=!0}catch(u){o.value=u.message}finally{n.value=!1}}async function d(){if(await os({title:"Update & restart",message:"Update Odin and restart? Active tasks will be interrupted.",confirmLabel:"Update & Restart",danger:!0})){i.value=!0,o.value=null;try{await z.post("/api/update/apply",{version:"latest"}),l.value=!0,setTimeout(()=>location.reload(),8e3)}catch(f){o.value=f.message}finally{i.value=!1}}}return tt(c),{current:e,latest:t,updateAvailable:s,changelog:a,checking:n,applying:i,applied:l,error:o,checkDone:r,checkUpdate:c,applyUpdate:d}},template:` + `,setup(){const e=f(null),t=f(""),s=f(null),a=f(!1),n=f(0),i=f(null),l=f(""),o=f(null),r=f(!1),c=f(0),d=f(Date.now());let u=null,p=0,m=0;async function h(){const M=++p;a.value=!0;try{const $=await z.get("/api/turn-state/turns?limit=100");if(M!==p)return;t.value=$.availability,e.value=$.availability==="available"?$.data:null,s.value=null,n.value=Date.now()}catch($){if(M!==p)return;s.value=$.message||"Turn-state read failed",$.status===503&&(t.value="unavailable")}M===p&&(a.value=!1)}async function y(){const M=++m;r.value=!0;try{const $=await z.get("/api/turn-state/capacity-breakers");if(M!==m)return;l.value=$.availability,i.value=$.availability==="available"?$.data:null,o.value=null,c.value=Date.now()}catch($){if(M!==m)return;o.value=$.message||"Breaker read failed",$.status===503&&(l.value="unavailable")}M===m&&(r.value=!1)}function k(){h(),y()}const A=B(()=>e.value!==null&&wl(d.value,n.value)>sm),g=B(()=>i.value!==null&&wl(d.value,c.value)>sm),b=B(()=>A.value||g.value),x=B(()=>Math.round(wl(d.value,n.value)/1e3)),w=B(()=>Math.round(wl(d.value,c.value)/1e3));function _(M){return Qc(M,d.value/1e3)}function T(M){return oT[_(M)]}const L=B(()=>{var re;const M=[...((re=e.value)==null?void 0:re.turns)||[]],$=d.value/1e3;return M.sort((ce,Z)=>Qc(ce,$)-Qc(Z,$)||(Z.last_progress_at||0)-(ce.last_progress_at||0))});function E(M){return M.state==="closed"?"badge-success":M.state==="probing"?"badge-warning":"badge-danger"}function F(M){if(M.state==="closed")return"—";const $=wl(d.value,c.value)/1e3,re=Math.max(0,(M.cooldown_remaining_seconds||0)-$);return re>0?`${Math.ceil(re)}s`:M.state==="probing"?"probe in flight":"probe eligible"}function O(M){if(!M)return"";const $=Math.max(0,Math.round(d.value/1e3-M));if($<90)return`${$}s ago`;const re=Math.round($/60);return re<90?`${re}m ago`:`${Math.round(re/60)}h ago`}let I=null,H=null,te=!1;function ee(){te||(te=!0,k(),I=setInterval(k,lT),u=setInterval(()=>{d.value=Date.now()},1e3),H=mt.onReconnected(k))}function N(){te&&(te=!1,I&&(clearInterval(I),I=null),u&&(clearInterval(u),u=null),H&&(H(),H=null))}return st(ee),cs(ee),es(N),wt(N),{turnsData:e,turnsAvailability:t,turnsError:s,turnsLoading:a,breakersData:i,breakersAvailability:l,breakersError:o,breakersLoading:r,turnsStale:A,breakersStale:g,anyStale:b,turnsAgeSeconds:x,breakersAgeSeconds:w,sortedTurns:L,priorityOf:_,priorityBadge:T,breakerBadge:E,cooldownLabel:F,ageLabel:O,fetchTurns:h,fetchBreakers:y,refreshAll:k,arm:ee,disarm:N}}},cT={setup(){const e=f(""),t=f(""),s=f(!1),a=f(""),n=f(!1),i=f(!1),l=f(!1),o=f(null),r=f(!1);async function c(){n.value=!0,o.value=null,r.value=!1;try{const u=await z.get("/api/update/check");e.value=u.current||"",t.value=u.latest||"",s.value=u.update_available||!1,a.value=u.changelog||"",u.error&&(o.value=u.error),r.value=!0}catch(u){o.value=u.message}finally{n.value=!1}}async function d(){if(await is({title:"Update & restart",message:"Update Odin and restart? Active tasks will be interrupted.",confirmLabel:"Update & Restart",danger:!0})){i.value=!0,o.value=null;try{await z.post("/api/update/apply",{version:"latest"}),l.value=!0,setTimeout(()=>location.reload(),8e3)}catch(p){o.value=p.message}finally{i.value=!1}}}return st(c),{current:e,latest:t,updateAvailable:s,changelog:a,checking:n,applying:i,applied:l,error:o,checkDone:r,checkUpdate:c,applyUpdate:d}},template:`

Updates

@@ -6602,7 +6619,7 @@ ${u.text}`:u.text).join(`

{{ error }}

- `},Wf=e=>JSON.parse(JSON.stringify(e)),jC=(e,t)=>JSON.stringify(e)===JSON.stringify(t),qC={emits:["saved"],template:` + `},am=e=>JSON.parse(JSON.stringify(e)),dT=(e,t)=>JSON.stringify(e)===JSON.stringify(t),uT={emits:["saved"],template:`
@@ -6655,8 +6672,8 @@ ${u.text}`:u.text).join(`

Drafts are local to this view and discarded when leaving it. Backend validation remains authoritative.

- `,setup(e,{emit:t}){const s=p([]),a=p({}),n=p({}),i=p(!1),l=p(!1),o=p(!1),r=p(!1),c=p(!1),d=p(""),u=p("");let f=!1,m=0,h=null;const g=(N,O)=>f&&m===N&&z.token===O,A=N=>"computer-provisioning-"+N.key,w=N=>N===null?"Unset":N===""?"Empty":JSON.stringify(N),y=N=>{const O=n.value[N.key];return N.type==="array"?String(O||"").split(/\r?\n/).map($=>$.trim()).filter(Boolean):["integer","number"].includes(N.type)?O===""||O==null?null:Number(O):O},b=U(()=>s.value.map(N=>({...N,value:y(N)})).filter(N=>!jC(N.value,a.value[N.key]))),x=U(()=>s.value.filter(N=>N.pending_restart).map(N=>N.label)),S=U(()=>s.value.some(N=>N.apply_state==="unknown")),_=U(()=>{const N={};for(const O of s.value){const $=y(O),ce=O.constraints||{};["integer","number"].includes(O.type)&&($===null&&!O.nullable?N[O.key]="A number is required.":$!==null&&(!Number.isFinite($)||O.type==="integer"&&!Number.isInteger($)||ce.minimum!=null&&$ce.maximum)&&(N[O.key]="Enter a number within the allowed range.")),O.key==="monitor_names"&&($.length>16||new Set($).size!==$.length||$.some(ue=>!/^[A-Za-z0-9_.-]{1,64}$/.test(ue)))&&(N[O.key]="Use up to 16 unique monitor names, one per line (letters, digits, dot, underscore or hyphen).")}return N}),T=U(()=>Object.keys(_.value).length>0);function L(N,O){n.value[N.key]=O,u.value=""}function E(){n.value=Object.fromEntries(s.value.map(N=>[N.key,N.type==="array"?a.value[N.key].join(` -`):a.value[N.key]])),r.value=!1}async function P(N,O){const[$,ce]=await Promise.all([z.get("/api/config"),z.get("/api/config/meta")]);if(!g(N,O))return!1;const ue=(ce.fields||[]).filter(Z=>/^computer\.[^.]+$/.test(Z.path)&&Z.path!=="computer.enabled"&&Z.sensitivity==="public"&&Z.apply_mode==="restart");if(!$.computer||!ue.length)throw new Error("Provisioning metadata is unavailable. Reload before editing.");return s.value=ue.map(Z=>({...Z,key:Z.path.split(".")[1]})),a.value=Object.fromEntries(s.value.map(Z=>[Z.key,Wf($.computer[Z.key])])),E(),h=O,i.value=!0,c.value=!1,!0}async function R(){if(!f||l.value||o.value)return;const N=++m,O=z.token;l.value=!0,i.value=!1,d.value="",u.value="",r.value=!1;try{await P(N,O)}catch($){g(N,O)&&(c.value=!0,d.value=$.message||"Could not load provisioning. No changes were sent.")}finally{g(N,O)&&(l.value=!1)}}function I(){i.value&&!l.value&&!o.value&&!c.value&&b.value.length&&!T.value&&(r.value=!0)}async function H(){if(!f||!i.value||!r.value||o.value||l.value||c.value||T.value||!b.value.length)return;if(h!==z.token){Q(),X();return}const N={computer:Object.fromEntries(b.value.map(ue=>[ue.key,Wf(ue.value)]))},O=m,$=z.token;o.value=!0,d.value="",u.value="";let ce=!1;try{if(await z.put("/api/config",N),ce=!0,!g(O,$))return;await P(O,$)&&(u.value="Provisioning saved and configuration reloaded. Startup settings remain pinned until Odin restarts; no lifecycle action was requested.",t("saved"))}catch(ue){g(O,$)&&(c.value=!0,r.value=!1,d.value=ce?"Provisioning was saved, but configuration refresh failed. Reload saved values before editing again.":`Save failed or its outcome is unknown${ue.status===400?": "+ue.message:"."} Reload saved values before editing or retrying. No request was replayed.`)}finally{g(O,$)&&(o.value=!1)}}function X(){f||(f=!0,R())}function Q(){f=!1,m++,i.value=!1,l.value=!1,o.value=!1,r.value=!1,h=null,s.value=[],a.value={},n.value={},d.value="",u.value=""}return tt(X),rs(X),Kt(Q),xt(Q),{fields:s,original:a,draft:n,ready:i,loading:l,saving:o,reviewing:r,uncertain:c,error:d,message:u,pending:x,effectiveUnknown:S,changes:b,validation:_,invalid:T,fieldId:A,format:w,edit:L,discard:E,load:R,openReview:I,save:H}}},VC={components:{ComputerProvisioning:qC,DiscordIdentity:En},template:` + `,setup(e,{emit:t}){const s=f([]),a=f({}),n=f({}),i=f(!1),l=f(!1),o=f(!1),r=f(!1),c=f(!1),d=f(""),u=f("");let p=!1,m=0,h=null;const y=(N,M)=>p&&m===N&&z.token===M,k=N=>"computer-provisioning-"+N.key,A=N=>N===null?"Unset":N===""?"Empty":JSON.stringify(N),g=N=>{const M=n.value[N.key];return N.type==="array"?String(M||"").split(/\r?\n/).map($=>$.trim()).filter(Boolean):["integer","number"].includes(N.type)?M===""||M==null?null:Number(M):M},b=B(()=>s.value.map(N=>({...N,value:g(N)})).filter(N=>!dT(N.value,a.value[N.key]))),x=B(()=>s.value.filter(N=>N.pending_restart).map(N=>N.label)),w=B(()=>s.value.some(N=>N.apply_state==="unknown")),_=B(()=>{const N={};for(const M of s.value){const $=g(M),re=M.constraints||{};["integer","number"].includes(M.type)&&($===null&&!M.nullable?N[M.key]="A number is required.":$!==null&&(!Number.isFinite($)||M.type==="integer"&&!Number.isInteger($)||re.minimum!=null&&$re.maximum)&&(N[M.key]="Enter a number within the allowed range.")),M.key==="monitor_names"&&($.length>16||new Set($).size!==$.length||$.some(ce=>!/^[A-Za-z0-9_.-]{1,64}$/.test(ce)))&&(N[M.key]="Use up to 16 unique monitor names, one per line (letters, digits, dot, underscore or hyphen).")}return N}),T=B(()=>Object.keys(_.value).length>0);function L(N,M){n.value[N.key]=M,u.value=""}function E(){n.value=Object.fromEntries(s.value.map(N=>[N.key,N.type==="array"?a.value[N.key].join(` +`):a.value[N.key]])),r.value=!1}async function F(N,M){const[$,re]=await Promise.all([z.get("/api/config"),z.get("/api/config/meta")]);if(!y(N,M))return!1;const ce=(re.fields||[]).filter(Z=>/^computer\.[^.]+$/.test(Z.path)&&Z.path!=="computer.enabled"&&Z.sensitivity==="public"&&Z.apply_mode==="restart");if(!$.computer||!ce.length)throw new Error("Provisioning metadata is unavailable. Reload before editing.");return s.value=ce.map(Z=>({...Z,key:Z.path.split(".")[1]})),a.value=Object.fromEntries(s.value.map(Z=>[Z.key,am($.computer[Z.key])])),E(),h=M,i.value=!0,c.value=!1,!0}async function O(){if(!p||l.value||o.value)return;const N=++m,M=z.token;l.value=!0,i.value=!1,d.value="",u.value="",r.value=!1;try{await F(N,M)}catch($){y(N,M)&&(c.value=!0,d.value=$.message||"Could not load provisioning. No changes were sent.")}finally{y(N,M)&&(l.value=!1)}}function I(){i.value&&!l.value&&!o.value&&!c.value&&b.value.length&&!T.value&&(r.value=!0)}async function H(){if(!p||!i.value||!r.value||o.value||l.value||c.value||T.value||!b.value.length)return;if(h!==z.token){ee(),te();return}const N={computer:Object.fromEntries(b.value.map(ce=>[ce.key,am(ce.value)]))},M=m,$=z.token;o.value=!0,d.value="",u.value="";let re=!1;try{if(await z.put("/api/config",N),re=!0,!y(M,$))return;await F(M,$)&&(u.value="Provisioning saved and configuration reloaded. Startup settings remain pinned until Odin restarts; no lifecycle action was requested.",t("saved"))}catch(ce){y(M,$)&&(c.value=!0,r.value=!1,d.value=re?"Provisioning was saved, but configuration refresh failed. Reload saved values before editing again.":`Save failed or its outcome is unknown${ce.status===400?": "+ce.message:"."} Reload saved values before editing or retrying. No request was replayed.`)}finally{y(M,$)&&(o.value=!1)}}function te(){p||(p=!0,O())}function ee(){p=!1,m++,i.value=!1,l.value=!1,o.value=!1,r.value=!1,h=null,s.value=[],a.value={},n.value={},d.value="",u.value=""}return st(te),cs(te),es(ee),wt(ee),{fields:s,original:a,draft:n,ready:i,loading:l,saving:o,reviewing:r,uncertain:c,error:d,message:u,pending:x,effectiveUnknown:w,changes:b,validation:_,invalid:T,fieldId:k,format:A,edit:L,discard:E,load:O,openReview:I,save:H}}},pT={components:{ComputerProvisioning:uT,DiscordIdentity:Dn},template:`
-
`,setup(){const e=p({state:"unknown",available:!1}),t=p(!1),s=p(!1),a=p(!1),n=p(!1),i=p(!1),l=p(!1),o=p(""),r=p(!1),c=p(!1),d=p(!1),u=p(0),f=p(""),m=p(""),h=p(null),g=p(""),A=p(!1),w=p(Date.now()),y=p(""),b=p(null);let x=0,S=null,_=!1,T=z.token,L=0,E=null,P=null,R=!1;const I=K=>K===!0?"Enabled":K===!1?"Disabled":"Unknown",H=U(()=>{var K;return((K=e.value.backend)==null?void 0:K.environment)==="existing_session"}),X=U(()=>{var ne;const K=Date.parse(((ne=e.value.accessibility)==null?void 0:ne.checked_at)||"");return c.value&&Number.isFinite(K)&&w.value-K<15e3&&w.value>=K-5e3}),Q=U(()=>{var K;return X.value?I((K=e.value.accessibility)==null?void 0:K.enabled):"Unknown / not current"}),N=U(()=>{var K;return X.value?((K=e.value.accessibility)==null?void 0:K.reason)==="property_read"?"Last checked: "+e.value.accessibility.checked_at:"Property unavailable for this target. Isolated sessions, an unbound operator identity, an inactive accessibility service or an inaccessible session bus may prevent this read. Unknown does not mean disabled.":"No current property read; unknown does not mean disabled."}),O=U(()=>Object.entries(e.value.input_limits||{}).filter(([,K])=>typeof K=="number"&&Number.isFinite(K)).map(([K,ne])=>`${K}: ${ne}`).join(", ")),$=U(()=>{var ne;const K=(ne=e.value.application_provenance)==null?void 0:ne.script_identity;return typeof K=="string"?K:!K||typeof K!="object"?"Not observed":`${K.interpreter_basename||"Unknown interpreter"}; argv digest ${K.argv_digest||"not recorded"}; ${K.verified===!0?"verified":"not verified"}`}),ce=U(()=>Array.isArray(e.value.application_profiles)?e.value.application_profiles.filter(K=>K&&typeof K.id=="string"&&typeof K.label=="string"&&["supported","capture_only"].includes(K.input)).slice(0,16):[]),ue=U(()=>{const K=e.value.restart_required;return Array.isArray(K)?K.length?K.join(", "):"None reported":K===!0?"Pending; restart required":K===!1?"None reported":"Unknown"}),Z=U(()=>{var ne,te;const K=Date.parse(((ne=h.value)==null?void 0:ne.captured_at)||"");return Number.isFinite(K)&&w.value2097152||!Number.isFinite(Date.parse(_e.frame.expires_at))||Date.parse(_e.frame.expires_at)<=Date.now())throw new Error("Invalid evidence");h.value=_e.frame,g.value=URL.createObjectURL(B),o.value=""}catch(_e){q(K,ne)&&Oe(_e)}finally{K===x&&(s.value=!1)}}async function ge(){if(!ye()||i.value||!e.value.available)return;b.value=null;const K=x,ne=z.token;i.value=!0;try{const te=await z.post("/api/computer/export",{name:y.value});q(K,ne)&&(b.value=te,o.value="")}catch(te){q(K,ne)&&Oe(te)}finally{K===x&&(i.value=!1)}}async function pe(){if(!ye()||l.value||!b.value)return;const K=x,ne=z.token,te=b.value;l.value=!0;try{if(!/^[A-Za-z0-9_-]{8,128}$/.test((te==null?void 0:te.artifact_id)||""))throw new Error("Invalid export");const _e=await z.getBlob("/api/computer/download/"+te.artifact_id);if(!q(K,ne))return;const B=URL.createObjectURL(_e),fe=document.createElement("a");fe.href=B,fe.download=te.name,fe.click(),setTimeout(()=>URL.revokeObjectURL(B),1e3)}catch(_e){q(K,ne)&&Oe(_e)}finally{K===x&&(l.value=!1)}}function he(){_||(T!==z.token&&(T=z.token,se(),u.value=0,P=null,e.value={state:"unknown",available:!1},o.value="",f.value=""),_=!0,le(),S=setInterval(()=>{w.value=Date.now(),T!==z.token&&(T=z.token,se(),u.value=0,P=null,o.value="",f.value="",e.value={state:"unknown",available:!1}),c.value&&w.value-u.value>=15e3&&se(),h.value&&Date.parse(h.value.expires_at)<=w.value&&(J(),A.value=!0),b.value&&Date.parse(b.value.expires_at)<=w.value&&(b.value=null),!R&&w.value-L>=5e3&&le()},500))}function we(){_=!1,clearInterval(S),S=null,se(),c.value=!1}return tt(he),rs(he),Kt(we),xt(we),{status:e,checkedAt:u,remedy:f,loading:t,observing:s,stopping:a,pausing:n,exporting:i,downloading:l,error:o,frame:h,frameUrl:g,frameExpired:A,freshness:Z,name:y,artifact:b,refresh:le,control:j,observe:W,clearFrame:J,exportFile:ge,download:pe,toggling:r,adminReady:c,enabledLabel:I,restartSettings:ue,setEnabled:ve,recovering:d,recover:xe,reconcile:k,releaseOwnedInput:V,reconciliationAck:m,applicationProfiles:ce,attached:H,scriptIdentity:$,inputLimits:O,accessibilityLabel:Q,accessibilityDetail:N}}},Fg=[{id:"health",label:"Health",component:Y1},{id:"resources",label:"Resources",component:X1},{id:"logs",label:"Logs",component:uC},{id:"config",label:"Config",component:kC},{id:"discord",label:"Discord",component:CC},{id:"hosts",label:"Hosts",component:AC},{id:"host-access",label:"Host Access",component:EC},{id:"api-tokens",label:"API Tokens",component:RC},{id:"llm",label:"LLM Config",component:PC},{id:"internals",label:"Internals",component:FC},{id:"turn-state",label:"Turn State",component:zC},{id:"computer",label:"Computer",component:VC},{id:"update",label:"Update",component:HC}],GC={components:{TabbedPage:nc},setup(){return{tabs:Fg}},template:''},qo=(e,t,s,a)=>a.map(({id:n,label:i})=>({group:e,label:i,icon:t,to:{path:s,query:{tab:n}}})),WC=[{group:"Workspace",label:"Dashboard",icon:"dashboard",to:{path:"/dashboard"}},{group:"Workspace",label:"Chat",icon:"chat",to:{path:"/chat"}},...qo("Operations","operations","/operations",Ag),...qo("History","history","/history",Rg),...qo("Capabilities","capabilities","/capabilities",Ig),{group:"Manage",label:"Personality",icon:"personality",to:{path:"/personality"}},...qo("System","system","/system",Fg)],Fs=Tn({open:!1,query:"",selected:0});function Kf(){Fs.query="",Fs.selected=0,Fs.open=!0}function qc(){Fs.open=!1}function KC(e,t){const s=e.label.toLowerCase(),a=`${e.group} ${e.label}`.toLowerCase();return t?s.startsWith(t)?100:a.startsWith(t)?80:s.includes(t)?60:a.includes(t)?40:0:1}const JC={setup(){const e=xg(),t=p(null),s=U(()=>{const i=Fs.query.trim().toLowerCase();return WC.map(l=>({...l,_score:KC(l,i)})).filter(l=>l._score>0).sort((l,o)=>o._score-l._score)});jt(()=>Fs.open,async i=>{var l;i&&(await Mt(),(l=t.value)==null||l.focus())}),jt(()=>Fs.query,()=>{Fs.selected=0});function a(i){qc(),e.push(i.to)}function n(i){if(i.key==="Escape"){i.preventDefault(),qc();return}if(i.key==="ArrowDown")i.preventDefault(),Fs.selected=Math.min(Fs.selected+1,s.value.length-1);else if(i.key==="ArrowUp")i.preventDefault(),Fs.selected=Math.max(Fs.selected-1,0);else if(i.key==="Enter"){i.preventDefault();const l=s.value[Fs.selected];l&&a(l)}}return{state:Fs,results:s,inputEl:t,go:a,onKeydown:n,closePalette:qc}},template:` +
`,setup(){const e=f({state:"unknown",available:!1}),t=f(!1),s=f(!1),a=f(!1),n=f(!1),i=f(!1),l=f(!1),o=f(""),r=f(!1),c=f(!1),d=f(!1),u=f(0),p=f(""),m=f(""),h=f(null),y=f(""),k=f(!1),A=f(Date.now()),g=f(""),b=f(null);let x=0,w=null,_=!1,T=z.token,L=0,E=null,F=null,O=!1;const I=J=>J===!0?"Enabled":J===!1?"Disabled":"Unknown",H=B(()=>{var J;return((J=e.value.backend)==null?void 0:J.environment)==="existing_session"}),te=B(()=>{var ie;const J=Date.parse(((ie=e.value.accessibility)==null?void 0:ie.checked_at)||"");return c.value&&Number.isFinite(J)&&A.value-J<15e3&&A.value>=J-5e3}),ee=B(()=>{var J;return te.value?I((J=e.value.accessibility)==null?void 0:J.enabled):"Unknown / not current"}),N=B(()=>{var J;return te.value?((J=e.value.accessibility)==null?void 0:J.reason)==="property_read"?"Last checked: "+e.value.accessibility.checked_at:"Property unavailable for this target. Isolated sessions, an unbound operator identity, an inactive accessibility service or an inaccessible session bus may prevent this read. Unknown does not mean disabled.":"No current property read; unknown does not mean disabled."}),M=B(()=>Object.entries(e.value.input_limits||{}).filter(([,J])=>typeof J=="number"&&Number.isFinite(J)).map(([J,ie])=>`${J}: ${ie}`).join(", ")),$=B(()=>{var ie;const J=(ie=e.value.application_provenance)==null?void 0:ie.script_identity;return typeof J=="string"?J:!J||typeof J!="object"?"Not observed":`${J.interpreter_basename||"Unknown interpreter"}; argv digest ${J.argv_digest||"not recorded"}; ${J.verified===!0?"verified":"not verified"}`}),re=B(()=>Array.isArray(e.value.application_profiles)?e.value.application_profiles.filter(J=>J&&typeof J.id=="string"&&typeof J.label=="string"&&["supported","capture_only"].includes(J.input)).slice(0,16):[]),ce=B(()=>{const J=e.value.restart_required;return Array.isArray(J)?J.length?J.join(", "):"None reported":J===!0?"Pending; restart required":J===!1?"None reported":"Unknown"}),Z=B(()=>{var ie,X;const J=Date.parse(((ie=h.value)==null?void 0:ie.captured_at)||"");return Number.isFinite(J)&&A.value2097152||!Number.isFinite(Date.parse(_e.frame.expires_at))||Date.parse(_e.frame.expires_at)<=Date.now())throw new Error("Invalid evidence");h.value=_e.frame,y.value=URL.createObjectURL(U),o.value=""}catch(_e){V(J,ie)&&Oe(_e)}finally{J===x&&(s.value=!1)}}async function he(){if(!be()||i.value||!e.value.available)return;b.value=null;const J=x,ie=z.token;i.value=!0;try{const X=await z.post("/api/computer/export",{name:g.value});V(J,ie)&&(b.value=X,o.value="")}catch(X){V(J,ie)&&Oe(X)}finally{J===x&&(i.value=!1)}}async function fe(){if(!be()||l.value||!b.value)return;const J=x,ie=z.token,X=b.value;l.value=!0;try{if(!/^[A-Za-z0-9_-]{8,128}$/.test((X==null?void 0:X.artifact_id)||""))throw new Error("Invalid export");const _e=await z.getBlob("/api/computer/download/"+X.artifact_id);if(!V(J,ie))return;const U=URL.createObjectURL(_e),de=document.createElement("a");de.href=U,de.download=X.name,de.click(),setTimeout(()=>URL.revokeObjectURL(U),1e3)}catch(_e){V(J,ie)&&Oe(_e)}finally{J===x&&(l.value=!1)}}function me(){_||(T!==z.token&&(T=z.token,ae(),u.value=0,F=null,e.value={state:"unknown",available:!1},o.value="",p.value=""),_=!0,le(),w=setInterval(()=>{A.value=Date.now(),T!==z.token&&(T=z.token,ae(),u.value=0,F=null,o.value="",p.value="",e.value={state:"unknown",available:!1}),c.value&&A.value-u.value>=15e3&&ae(),h.value&&Date.parse(h.value.expires_at)<=A.value&&(Q(),k.value=!0),b.value&&Date.parse(b.value.expires_at)<=A.value&&(b.value=null),!O&&A.value-L>=5e3&&le()},500))}function we(){_=!1,clearInterval(w),w=null,ae(),c.value=!1}return st(me),cs(me),es(we),wt(we),{status:e,checkedAt:u,remedy:p,loading:t,observing:s,stopping:a,pausing:n,exporting:i,downloading:l,error:o,frame:h,frameUrl:y,frameExpired:k,freshness:Z,name:g,artifact:b,refresh:le,control:j,observe:G,clearFrame:Q,exportFile:he,download:fe,toggling:r,adminReady:c,enabledLabel:I,restartSettings:ce,setEnabled:ve,recovering:d,recover:xe,reconcile:S,releaseOwnedInput:Y,reconciliationAck:m,applicationProfiles:re,attached:H,scriptIdentity:$,inputLimits:M,accessibilityLabel:ee,accessibilityDetail:N}}},Kg=[{id:"health",label:"Health",component:yC},{id:"resources",label:"Resources",component:xC},{id:"logs",label:"Logs",component:LC},{id:"config",label:"Config",component:qC},{id:"discord",label:"Discord",component:GC},{id:"hosts",label:"Hosts",component:JC},{id:"host-access",label:"Host Access",component:KC},{id:"api-tokens",label:"API Tokens",component:ZC},{id:"llm",label:"LLM Config",component:aT},{id:"internals",label:"Internals",component:iT},{id:"turn-state",label:"Turn State",component:rT},{id:"computer",label:"Computer",component:pT},{id:"update",label:"Update",component:cT}],fT={components:{TabbedPage:uc},setup(){return{tabs:Kg}},template:''},Qo=(e,t,s,a)=>a.map(({id:n,label:i})=>({group:e,label:i,icon:t,to:{path:s,query:{tab:n}}})),mT=[{group:"Workspace",label:"Dashboard",icon:"dashboard",to:{path:"/dashboard"}},{group:"Workspace",label:"Chat",icon:"chat",to:{path:"/chat"}},...Qo("Operations","operations","/operations",Fg),...Qo("History","history","/history",Bg),...Qo("Capabilities","capabilities","/capabilities",Ug),{group:"Manage",label:"Personality",icon:"personality",to:{path:"/personality"}},...Qo("System","system","/system",Kg)],Us=Mn({open:!1,query:"",selected:0});function nm(){Us.query="",Us.selected=0,Us.open=!0}function Yc(){Us.open=!1}function vT(e,t){const s=e.label.toLowerCase(),a=`${e.group} ${e.label}`.toLowerCase();return t?s.startsWith(t)?100:a.startsWith(t)?80:s.includes(t)?60:a.includes(t)?40:0:1}const hT={setup(){const e=Ig(),t=f(null),s=B(()=>{const i=Us.query.trim().toLowerCase();return mT.map(l=>({...l,_score:vT(l,i)})).filter(l=>l._score>0).sort((l,o)=>o._score-l._score)});Gt(()=>Us.open,async i=>{var l;i&&(await Ut(),(l=t.value)==null||l.focus())}),Gt(()=>Us.query,()=>{Us.selected=0});function a(i){Yc(),e.push(i.to)}function n(i){if(i.key==="Escape"){i.preventDefault(),Yc();return}if(i.key==="ArrowDown")i.preventDefault(),Us.selected=Math.min(Us.selected+1,s.value.length-1);else if(i.key==="ArrowUp")i.preventDefault(),Us.selected=Math.max(Us.selected-1,0);else if(i.key==="Enter"){i.preventDefault();const l=s.value[Us.selected];l&&a(l)}}return{state:Us,results:s,inputEl:t,go:a,onKeydown:n,closePalette:Yc}},template:` - `},Ld={brand:"M12 3 4.5 8v8L12 21l7.5-5V8L12 3Zm0 4.2 4.6 3.1L12 16.8l-4.6-6.5L12 7.2Zm0 3.3v3.7",dashboard:"M4 13h6V4H4v9Zm0 7h6v-4H4v4Zm10 0h6v-9h-6v9Zm0-16v4h6V4h-6Z",chat:"M20 15a3 3 0 0 1-3 3H9l-5 3v-6a3 3 0 0 1-1-2.2V7a3 3 0 0 1 3-3h11a3 3 0 0 1 3 3v8Z",operations:"M5 12h3l2-6 4 12 2-6h3M4 4v16h16",history:"M4 12a8 8 0 1 0 2.3-5.7L4 8.5M4 4v4.5h4.5M12 7v5l3 2",home:"M3 11.5 12 4l9 7.5M5.5 10v10h13V10M9 20v-6h6v6",users:"M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2m7-10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm13 10v-2a4 4 0 0 0-3-3.9m-2-11.8a4 4 0 0 1 0 7.7",capabilities:"M14.7 6.3a4 4 0 0 0-5.6 5.6L4 17v3h3v-2h2v-2h2l1.1-1.1a4 4 0 0 0 5.6-5.6l-3 3-3-3 3-3Z",personality:"M12 3a8 8 0 0 0-8 8c0 4 3 7 7 7v3h3v-3c3 0 6-3 6-7a8 8 0 0 0-8-8ZM8.5 10h.01M15.5 10h.01M9 14c1.7 1.2 4.3 1.2 6 0",system:"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0-5v2m0 14v2M3 12h2m14 0h2M5.6 5.6 7 7m10 10 1.4 1.4M18.4 5.6 17 7M7 17l-1.4 1.4",menu:"M4 7h16M4 12h16M4 17h16",panelLeft:"M9 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4V4Zm0 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H9M6 8h.01M6 12h.01",chevronLeft:"m15 18-6-6 6-6",chevronRight:"m9 18 6-6-6-6",chevronDown:"m6 9 6 6 6-6",chevronUp:"m18 15-6-6-6 6",search:"m21 21-4.3-4.3M19 11a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z",logout:"M10 17l5-5-5-5m5 5H3m10-8h5a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-5",success:"m5 12 4 4L19 6",warning:"M12 3 2.8 20h18.4L12 3Zm0 6v4m0 3h.01",info:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-8v4m0-8h.01",error:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm-3-12 6 6m0-6-6 6",edit:"M4 20h4l11-11-4-4L4 16v4Zm9-13 4 4",trash:"M4 7h16m-10 4v5m4-5v5M9 4h6l1 3H8l1-3Zm-3 3 1 13h10l1-13",brain:"M9 5a3 3 0 0 0-5 2.2A3.5 3.5 0 0 0 4 14a3 3 0 0 0 5 2.2V5Zm6 0a3 3 0 0 1 5 2.2 3.5 3.5 0 0 1 0 6.8 3 3 0 0 1-5 2.2V5ZM9 9H7m2 4H6m9-4h2m-2 4h3M12 4v16",refresh:"M20 6v5h-5M4 18v-5h5M18.5 10A7 7 0 0 0 6 7.5L4 11m16 2-2 3.5A7 7 0 0 1 5.5 14",close:"M6 6l12 12M18 6 6 18",command:"M7 8a3 3 0 1 1-3-3h3v14a3 3 0 1 1-3-3h13a3 3 0 1 1-3 3V5a3 3 0 1 1 3 3H7Z",external:"M14 4h6v6m0-6-9 9M19 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h6",activity:"M4 12h4l2-5 4 10 2-5h4",shield:"M12 3 5 6v5c0 4.5 2.8 7.7 7 10 4.2-2.3 7-5.5 7-10V6l-7-3Z",database:"M20 6c0 1.7-3.6 3-8 3S4 7.7 4 6s3.6-3 8-3 8 1.3 8 3Zm0 0v6c0 1.7-3.6 3-8 3s-8-1.3-8-3V6m16 6v6c0 1.7-3.6 3-8 3s-8-1.3-8-3v-6",server:"M4 4h16v6H4V4Zm0 10h16v6H4v-6Zm3-7h.01M7 17h.01",terminal:"M5 7l4 4-4 4m6 1h8M3 4h18v16H3V4Z",wrench:"M14.7 6.3a4 4 0 0 0-5.6 5.6L4 17v3h3v-2h2v-2h2l1.1-1.1a4 4 0 0 0 5.6-5.6l-3 3-3-3 3-3Z",bot:"M8 4h8m-4-2v2M5 8h14a2 2 0 0 1 2 2v8H3v-8a2 2 0 0 1 2-2Zm3 4h.01M16 12h.01M8 16h8M3 13H1m22 0h-2",workflow:"M5 5h5v5H5V5Zm9 9h5v5h-5v-5ZM10 7.5h4a3 3 0 0 1 3 3V14M7.5 10v4a3 3 0 0 0 3 3H14",globe:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-18c2.2 2.5 3.3 5.5 3.3 9S14.2 18.5 12 21m0-18C9.8 5.5 8.7 8.5 8.7 12s1.1 6.5 3.3 9M3 12h18",book:"M4 5a3 3 0 0 1 3-2h5v17H7a3 3 0 0 0-3 1V5Zm16 0a3 3 0 0 0-3-2h-5v17h5a3 3 0 0 1 3 1V5Z",message:"M4 4h16v13H8l-4 4V4Zm4 5h8m-8 4h5",puzzle:"M9 4h3a2 2 0 1 1 4 0h4v5a2 2 0 1 0 0 4v7h-7a2 2 0 1 1-4 0H4v-7a2 2 0 1 0 0-4V4h5",sparkles:"m12 3 1.2 3.8L17 8l-3.8 1.2L12 13l-1.2-3.8L7 8l3.8-1.2L12 3Zm6 10 .8 2.2L21 16l-2.2.8L18 19l-.8-2.2L15 16l2.2-.8L18 13ZM5 14l1 2.8L9 18l-3 1.2L5 22l-1-2.8L1 18l3-1.2L5 14Z",link:"M9.5 14.5 14.5 9m-7 8H6a4 4 0 0 1 0-8h3m6 0h3a4 4 0 0 1 0 8h-3",file:"M6 3h8l4 4v14H6V3Zm8 0v5h5M9 13h6m-6 4h6",folder:"M3 6h7l2 2h9v11H3V6Z",image:"M4 4h16v16H4V4Zm3 12 4-4 3 3 2-2 4 4M9 9h.01",attachment:"m8 12 5-5a3 3 0 1 1 4 4l-7 7a5 5 0 0 1-7-7l7-7",clock:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-13v5l3 2",calendar:"M5 5h14v15H5V5Zm3-2v4m8-4v4M5 10h14",chart:"M4 20V10m5 10V4m5 16v-7m5 7V7M2 20h20",sliders:"M4 7h10m4 0h2M4 17h2m4 0h10M16 4v6M8 14v6",code:"m9 6-6 6 6 6m6-12 6 6-6 6",copy:"M8 8h11v12H8V8Zm-3 8H4V4h11v1",play:"m8 5 11 7-11 7V5Z",grid:"M4 4h6v6H4V4Zm10 0h6v6h-6V4ZM4 14h6v6H4v-6Zm10 0h6v6h-6v-6Z",list:"M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01",target:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-5a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm0-4h.01",rotate:"M20 6v5h-5M4 18v-5h5M18.5 10A7 7 0 0 0 6 7.5L4 11m16 2-2 3.5A7 7 0 0 1 5.5 14",archive:"M4 8h16v12H4V8Zm-1-4h18v4H3V4Zm6 8h6",flame:"M12 22c4 0 7-3 7-7 0-5-4-7-4-11-3 2-5 5-5 8-1-1-2-3-1-5-3 2-5 5-5 8 0 4 3 7 8 7Z",eye:"M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Zm10 3a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z",upload:"M12 16V4m-5 5 5-5 5 5M5 20h14",download:"M12 4v12m-5-5 5 5 5-5M5 20h14",undo:"M9 7 4 12l5 5m-5-5h10a6 6 0 0 1 6 6",redo:"m15 7 5 5-5 5m5-5H10a6 6 0 0 0-6 6",minus:"M5 12h14",plus:"M12 5v14M5 12h14",network:"M12 3v4m0 10v4M3 12h4m10 0h4M7.8 7.8l2.1 2.1m4.2 4.2 2.1 2.1m0-8.4-2.1 2.1m-4.2 4.2-2.1 2.1M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z",more:"M6 12h.01M12 12h.01M18 12h.01",pause:"M9 5v14m6-14v14",sort:"M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3"};Object.freeze(Object.keys(Ld));const ZC={name:"OdinIcon",props:{name:{type:String,required:!0},size:{type:[Number,String],default:18},strokeWidth:{type:[Number,String],default:1.8}},setup(e,{attrs:t}){return()=>zi("svg",{...t,class:["odin-icon",t.class],width:e.size,height:e.size,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":e.strokeWidth,"stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":t["aria-label"]?void 0:"true",focusable:"false"},[zi("path",{d:Ld[e.name]||Ld.info})])}},QC=["a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])'].join(",");function Jf(e){return[...e.querySelectorAll(QC)].filter(t=>!t.hasAttribute("hidden")&&t.getAttribute("aria-hidden")!=="true")}const YC={mounted(e){const t=document.activeElement,s=a=>{if(a.key!=="Tab")return;const n=Jf(e);if(!n.length){a.preventDefault(),e.focus();return}const i=n[0],l=n[n.length-1];a.shiftKey&&document.activeElement===i?(a.preventDefault(),l.focus()):!a.shiftKey&&document.activeElement===l&&(a.preventDefault(),i.focus())};e.__odinModalFocus={previous:t,onKeydown:s},e.addEventListener("keydown",s),requestAnimationFrame(()=>{(e.querySelector("[autofocus]")||Jf(e)[0]||e).focus()})},unmounted(e){var s;const t=e.__odinModalFocus;t&&(e.removeEventListener("keydown",t.onKeydown),(s=t.previous)!=null&&s.isConnected&&typeof t.previous.focus=="function"&&requestAnimationFrame(()=>t.previous.focus()),delete e.__odinModalFocus)}},XC={template:` + `},Ud={brand:"M12 3 4.5 8v8L12 21l7.5-5V8L12 3Zm0 4.2 4.6 3.1L12 16.8l-4.6-6.5L12 7.2Zm0 3.3v3.7",dashboard:"M4 13h6V4H4v9Zm0 7h6v-4H4v4Zm10 0h6v-9h-6v9Zm0-16v4h6V4h-6Z",chat:"M20 15a3 3 0 0 1-3 3H9l-5 3v-6a3 3 0 0 1-1-2.2V7a3 3 0 0 1 3-3h11a3 3 0 0 1 3 3v8Z",operations:"M5 12h3l2-6 4 12 2-6h3M4 4v16h16",history:"M4 12a8 8 0 1 0 2.3-5.7L4 8.5M4 4v4.5h4.5M12 7v5l3 2",home:"M3 11.5 12 4l9 7.5M5.5 10v10h13V10M9 20v-6h6v6",users:"M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2m7-10a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm13 10v-2a4 4 0 0 0-3-3.9m-2-11.8a4 4 0 0 1 0 7.7",capabilities:"M14.7 6.3a4 4 0 0 0-5.6 5.6L4 17v3h3v-2h2v-2h2l1.1-1.1a4 4 0 0 0 5.6-5.6l-3 3-3-3 3-3Z",personality:"M12 3a8 8 0 0 0-8 8c0 4 3 7 7 7v3h3v-3c3 0 6-3 6-7a8 8 0 0 0-8-8ZM8.5 10h.01M15.5 10h.01M9 14c1.7 1.2 4.3 1.2 6 0",system:"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0-5v2m0 14v2M3 12h2m14 0h2M5.6 5.6 7 7m10 10 1.4 1.4M18.4 5.6 17 7M7 17l-1.4 1.4",menu:"M4 7h16M4 12h16M4 17h16",panelLeft:"M9 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4V4Zm0 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H9M6 8h.01M6 12h.01",chevronLeft:"m15 18-6-6 6-6",chevronRight:"m9 18 6-6-6-6",chevronDown:"m6 9 6 6 6-6",chevronUp:"m18 15-6-6-6 6",search:"m21 21-4.3-4.3M19 11a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z",logout:"M10 17l5-5-5-5m5 5H3m10-8h5a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-5",success:"m5 12 4 4L19 6",warning:"M12 3 2.8 20h18.4L12 3Zm0 6v4m0 3h.01",info:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-8v4m0-8h.01",error:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm-3-12 6 6m0-6-6 6",edit:"M4 20h4l11-11-4-4L4 16v4Zm9-13 4 4",trash:"M4 7h16m-10 4v5m4-5v5M9 4h6l1 3H8l1-3Zm-3 3 1 13h10l1-13",brain:"M9 5a3 3 0 0 0-5 2.2A3.5 3.5 0 0 0 4 14a3 3 0 0 0 5 2.2V5Zm6 0a3 3 0 0 1 5 2.2 3.5 3.5 0 0 1 0 6.8 3 3 0 0 1-5 2.2V5ZM9 9H7m2 4H6m9-4h2m-2 4h3M12 4v16",refresh:"M20 6v5h-5M4 18v-5h5M18.5 10A7 7 0 0 0 6 7.5L4 11m16 2-2 3.5A7 7 0 0 1 5.5 14",close:"M6 6l12 12M18 6 6 18",command:"M7 8a3 3 0 1 1-3-3h3v14a3 3 0 1 1-3-3h13a3 3 0 1 1-3 3V5a3 3 0 1 1 3 3H7Z",external:"M14 4h6v6m0-6-9 9M19 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h6",activity:"M4 12h4l2-5 4 10 2-5h4",shield:"M12 3 5 6v5c0 4.5 2.8 7.7 7 10 4.2-2.3 7-5.5 7-10V6l-7-3Z",database:"M20 6c0 1.7-3.6 3-8 3S4 7.7 4 6s3.6-3 8-3 8 1.3 8 3Zm0 0v6c0 1.7-3.6 3-8 3s-8-1.3-8-3V6m16 6v6c0 1.7-3.6 3-8 3s-8-1.3-8-3v-6",server:"M4 4h16v6H4V4Zm0 10h16v6H4v-6Zm3-7h.01M7 17h.01",terminal:"M5 7l4 4-4 4m6 1h8M3 4h18v16H3V4Z",wrench:"M14.7 6.3a4 4 0 0 0-5.6 5.6L4 17v3h3v-2h2v-2h2l1.1-1.1a4 4 0 0 0 5.6-5.6l-3 3-3-3 3-3Z",bot:"M8 4h8m-4-2v2M5 8h14a2 2 0 0 1 2 2v8H3v-8a2 2 0 0 1 2-2Zm3 4h.01M16 12h.01M8 16h8M3 13H1m22 0h-2",workflow:"M5 5h5v5H5V5Zm9 9h5v5h-5v-5ZM10 7.5h4a3 3 0 0 1 3 3V14M7.5 10v4a3 3 0 0 0 3 3H14",globe:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-18c2.2 2.5 3.3 5.5 3.3 9S14.2 18.5 12 21m0-18C9.8 5.5 8.7 8.5 8.7 12s1.1 6.5 3.3 9M3 12h18",book:"M4 5a3 3 0 0 1 3-2h5v17H7a3 3 0 0 0-3 1V5Zm16 0a3 3 0 0 0-3-2h-5v17h5a3 3 0 0 1 3 1V5Z",message:"M4 4h16v13H8l-4 4V4Zm4 5h8m-8 4h5",puzzle:"M9 4h3a2 2 0 1 1 4 0h4v5a2 2 0 1 0 0 4v7h-7a2 2 0 1 1-4 0H4v-7a2 2 0 1 0 0-4V4h5",sparkles:"m12 3 1.2 3.8L17 8l-3.8 1.2L12 13l-1.2-3.8L7 8l3.8-1.2L12 3Zm6 10 .8 2.2L21 16l-2.2.8L18 19l-.8-2.2L15 16l2.2-.8L18 13ZM5 14l1 2.8L9 18l-3 1.2L5 22l-1-2.8L1 18l3-1.2L5 14Z",link:"M9.5 14.5 14.5 9m-7 8H6a4 4 0 0 1 0-8h3m6 0h3a4 4 0 0 1 0 8h-3",file:"M6 3h8l4 4v14H6V3Zm8 0v5h5M9 13h6m-6 4h6",folder:"M3 6h7l2 2h9v11H3V6Z",image:"M4 4h16v16H4V4Zm3 12 4-4 3 3 2-2 4 4M9 9h.01",attachment:"m8 12 5-5a3 3 0 1 1 4 4l-7 7a5 5 0 0 1-7-7l7-7",clock:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-13v5l3 2",calendar:"M5 5h14v15H5V5Zm3-2v4m8-4v4M5 10h14",chart:"M4 20V10m5 10V4m5 16v-7m5 7V7M2 20h20",sliders:"M4 7h10m4 0h2M4 17h2m4 0h10M16 4v6M8 14v6",code:"m9 6-6 6 6 6m6-12 6 6-6 6",copy:"M8 8h11v12H8V8Zm-3 8H4V4h11v1",play:"m8 5 11 7-11 7V5Z",grid:"M4 4h6v6H4V4Zm10 0h6v6h-6V4ZM4 14h6v6H4v-6Zm10 0h6v6h-6v-6Z",list:"M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01",target:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-5a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm0-4h.01",rotate:"M20 6v5h-5M4 18v-5h5M18.5 10A7 7 0 0 0 6 7.5L4 11m16 2-2 3.5A7 7 0 0 1 5.5 14",archive:"M4 8h16v12H4V8Zm-1-4h18v4H3V4Zm6 8h6",flame:"M12 22c4 0 7-3 7-7 0-5-4-7-4-11-3 2-5 5-5 8-1-1-2-3-1-5-3 2-5 5-5 8 0 4 3 7 8 7Z",eye:"M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Zm10 3a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z",upload:"M12 16V4m-5 5 5-5 5 5M5 20h14",download:"M12 4v12m-5-5 5 5 5-5M5 20h14",undo:"M9 7 4 12l5 5m-5-5h10a6 6 0 0 1 6 6",redo:"m15 7 5 5-5 5m5-5H10a6 6 0 0 0-6 6",minus:"M5 12h14",plus:"M12 5v14M5 12h14",network:"M12 3v4m0 10v4M3 12h4m10 0h4M7.8 7.8l2.1 2.1m4.2 4.2 2.1 2.1m0-8.4-2.1 2.1m-4.2 4.2-2.1 2.1M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z",more:"M6 12h.01M12 12h.01M18 12h.01",pause:"M9 5v14m6-14v14",sort:"M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3"};Object.freeze(Object.keys(Ud));const gT={name:"OdinIcon",props:{name:{type:String,required:!0},size:{type:[Number,String],default:18},strokeWidth:{type:[Number,String],default:1.8}},setup(e,{attrs:t}){return()=>qi("svg",{...t,class:["odin-icon",t.class],width:e.size,height:e.size,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":e.strokeWidth,"stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":t["aria-label"]?void 0:"true",focusable:"false"},[qi("path",{d:Ud[e.name]||Ud.info})])}},bT=["a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])'].join(",");function im(e){return[...e.querySelectorAll(bT)].filter(t=>!t.hasAttribute("hidden")&&t.getAttribute("aria-hidden")!=="true")}const yT={mounted(e){const t=document.activeElement,s=a=>{if(a.key!=="Tab")return;const n=im(e);if(!n.length){a.preventDefault(),e.focus();return}const i=n[0],l=n[n.length-1];a.shiftKey&&document.activeElement===i?(a.preventDefault(),l.focus()):!a.shiftKey&&document.activeElement===l&&(a.preventDefault(),i.focus())};e.__odinModalFocus={previous:t,onKeydown:s},e.addEventListener("keydown",s),requestAnimationFrame(()=>{(e.querySelector("[autofocus]")||im(e)[0]||e).focus()})},unmounted(e){var s;const t=e.__odinModalFocus;t&&(e.removeEventListener("keydown",t.onKeydown),(s=t.previous)!=null&&s.isConnected&&typeof t.previous.focus=="function"&&requestAnimationFrame(()=>t.previous.focus()),delete e.__odinModalFocus)}},xT={template:`
@@ -7069,53 +7086,53 @@ ${u.text}`:u.text).join(`
- `,setup(){const e=p({}),t=p(!0),s=p(null),a=p([]),n=p(!1),i=p([]),l=p(!1),o=p(!1),r=p([]),c=p(0),d=p(null),u=p({reload:!1,clearSessions:!1,stopLoops:!1});let f=0;const m=U(()=>{const ue=e.value.uptime_seconds||0,Z=Math.floor(ue/86400),J=Math.floor(ue%86400/3600),se=Math.floor(ue%3600/60),q=[];return Z>0&&q.push(`${Z}d`),J>0&&q.push(`${J}h`),(q.length===0||Z===0&&J===0)&&q.push(`${se}m`),q.join(" ")}),h=U(()=>{const ue=e.value.uptime_seconds||0;return 125.66*(1-Math.min(ue/86400,1))}),g=U(()=>{const ue=e.value;return[{label:"Guilds",value:ue.guild_count??0,icon:"home",iconColor:"text-blue-400"},{label:"Sessions",value:ue.session_count??0,icon:"message",iconColor:"text-yellow-400"},{label:"Tools",value:ue.tool_count??0,icon:"wrench",iconColor:"text-purple-400",sub:`${ue.skill_count??0} skills`,subColor:"text-gray-500"},{label:"Loops",value:ue.loop_count??0,icon:"rotate",iconColor:"text-green-400",color:ue.loop_count>0?"text-green-400":"",highlight:ue.loop_count>0},{label:"Agents",value:ue.agent_running??0,icon:"bot",iconColor:"text-cyan-400",sub:ue.agent_count>0?`${ue.agent_count} total`:"",subColor:"text-gray-500",highlight:(ue.agent_running??0)>0},{label:"Processes",value:ue.process_running??0,icon:"sliders",iconColor:"text-orange-400",sub:ue.process_count>0?`${ue.process_count} total`:"",subColor:"text-gray-500",highlight:(ue.process_running??0)>0},{label:"Schedules",value:ue.schedule_count??0,icon:"clock",iconColor:"text-amber-400",sub:(ue.schedule_failing>0?`${ue.schedule_failing} failing`:"")+(ue.schedule_failing>0&&ue.schedule_paused>0?", ":"")+(ue.schedule_paused>0?`${ue.schedule_paused} paused`:"")||void 0,subColor:ue.schedule_failing>0?"text-red-400":"text-yellow-400",color:ue.schedule_failing>0?"text-red-400":"",highlight:ue.schedule_failing>0},{label:"Users",value:ue.user_count??0,icon:"users",iconColor:"text-indigo-400"},...d.value!==null?[{label:"Knowledge",value:d.value,icon:"book",iconColor:"text-teal-400",sub:"chunks",subColor:"text-gray-500"}]:[]]}),A=U(()=>{const ue=e.value,Z=[];return Z.push({label:"Bot",status:ue.status==="online"?"ok":"warn",detail:ue.status==="online"?"Online":"Starting"}),(ue.schedule_failing||0)>0?Z.push({label:"Schedules",status:"error",detail:`${ue.schedule_failing} failing`}):(ue.schedule_count||0)>0&&Z.push({label:"Schedules",status:"ok",detail:`${ue.schedule_count} configured`}),(ue.loop_count||0)>0&&Z.push({label:"Loops",status:"ok",detail:`${ue.loop_count} active`}),(ue.agent_running||0)>0&&Z.push({label:"Agents",status:"ok",detail:`${ue.agent_running} running`}),(ue.process_running||0)>0&&Z.push({label:"Processes",status:"ok",detail:`${ue.process_running} running`}),Z});async function w(){try{e.value=await z.get("/api/status"),s.value=null}catch(ue){s.value=ue.message}finally{t.value=!1}}let y=0,b=0,x=0,S=0;function _(ue,Z){const J=new Set;return[...Z,...ue].filter(se=>{const q=se._hmac||JSON.stringify([se.timestamp,se.tool_name,se.user_id,se.result_summary,se.error]);return J.has(q)?!1:(J.add(q),!0)})}async function T(){const ue=++y,Z=x;n.value=!0;try{const J=await z.get("/api/audit?limit=10");if(ue!==y)return;const se=Z===x?[]:a.value.filter(q=>(q._liveEpoch||0)>Z);a.value=_(J,se).slice(0,10),c.value=se.length}catch{}ue===y&&(n.value=!1)}async function L(){const ue=++b,Z=S;l.value=!0;try{const J=await z.get("/api/audit?error_only=1&limit=5");if(ue!==b)return;const se=Z===S?[]:i.value.filter(q=>(q._liveErrorEpoch||0)>Z);i.value=_(J,se).slice(0,5),o.value=!1}catch{if(ue!==b)return;o.value=Z===S||i.value.length===0}ue===b&&(l.value=!1)}async function E(){try{const ue=await z.get("/api/knowledge");d.value=(Array.isArray(ue)?ue:[]).reduce((Z,J)=>Z+(J.chunks||0),0)}catch{d.value=null}}async function P(){try{const ue=await z.get("/api/agents");r.value=ue.filter(Z=>Z.status==="running")}catch{}}async function R(){u.value={...u.value,reload:!0};try{await z.post("/api/reload"),ke.success("Config reloaded")}catch(ue){ke.error(ue.message)}u.value={...u.value,reload:!1}}async function I(){if(!await os({title:"Clear all sessions",message:"Clear all conversation sessions? This cannot be undone.",confirmLabel:"Clear All",danger:!0}))return;u.value={...u.value,clearSessions:!0};const Z=e.value.session_count;e.value={...e.value,session_count:0};try{const J=await z.post("/api/sessions/clear-all");ke.success(`Cleared ${J.count} session${J.count!==1?"s":""}`),await w()}catch(J){e.value={...e.value,session_count:Z},ke.error(J.message)}u.value={...u.value,clearSessions:!1}}async function H(){if(!await os({title:"Stop all loops",message:"Stop all running loops?",confirmLabel:"Stop Loops",danger:!0}))return;u.value={...u.value,stopLoops:!0};const Z=e.value.loop_count;e.value={...e.value,loop_count:0};try{const J=await z.post("/api/loops/stop-all");ke.success(J.result),await w()}catch(J){e.value={...e.value,loop_count:Z},ke.error(J.message)}u.value={...u.value,stopLoops:!1}}function X(){t.value=!0,s.value=null,w(),T(),L(),P()}let Q=null,N=null,O=null;function $(ue){if(ue.payload&&ue.payload.tool_name){x+=1;const Z={...ue.payload,_isNew:!0,_key:++f,_liveEpoch:x};a.value.unshift(Z),a.value.length>10&&a.value.pop(),c.value++,Z.error&&(S+=1,Z._liveErrorEpoch=S,o.value=!1,i.value.unshift(Z),i.value.length>5&&i.value.pop()),setTimeout(()=>{Z._isNew=!1},1500),clearTimeout(O),O=setTimeout(()=>{c.value=0},1e4)}}let ce=null;return tt(async()=>{await Promise.all([w(),T(),L(),P(),E()]),Q=setInterval(w,15e3),N=setInterval(P,1e4),ut.subscribe("events",$),ce=ut.onReconnected(()=>{T(),L()})}),xt(()=>{Q&&clearInterval(Q),N&&clearInterval(N),clearTimeout(O),ut.unsubscribe("events",$),ce&&(ce(),ce=null)}),{status:e,loading:t,error:s,uptime:m,uptimeRingOffset:h,stats:g,healthIndicators:A,activity:a,activityLoading:n,newEventCount:c,errors:i,errorsLoading:l,errorsError:o,agents:r,actionLoading:u,fetchActivity:T,fetchErrors:L,fetchStatus:w,onEvent:$,formatTime:t1,formatDuration:Zi,retry:X,reloadConfig:R,clearSessions:I,stopAllLoops:H}}};/*! @license DOMPurify 3.4.9 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.9/LICENSE */function Zf(e,t){(t==null||t>e.length)&&(t=e.length);for(var s=0,a=Array(t);s2?a-2:0),i=2;i1?s-1:0),n=1;n"u"?null:Jt(BigInt.prototype.toString),sm=typeof Symbol>"u"?null:Jt(Symbol.prototype.toString),Pt=Jt(Object.prototype.hasOwnProperty),gl=Jt(Object.prototype.toString),us=Jt(RegExp.prototype.test),Bn=fT(TypeError);function Jt(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var s=arguments.length,a=new Array(s>1?s-1:0),n=1;n2&&arguments[2]!==void 0?arguments[2]:Sl;if(Qf&&Qf(e,null),!As(t))return e;let a=t.length;for(;a--;){let n=t[a];if(typeof n=="string"){const i=s(n);i!==n&&(iT(t)||(t[a]=i),n=i)}e[n]=!0}return e}function mT(e){for(let t=0;t/g),_T=la(/\${[\w\W]*/g),wT=la(/^data-[\-\w.\u00B7-\uFFFF]+$/),kT=la(/^aria-[\-\w]+$/),om=la(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ST=la(/^(?:\w+script|data):/i),CT=la(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),TT=la(/^html$/i),ET=la(/^[a-z][.\w]*(-[.\w]+)+$/i),wa={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,progressingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},AT=function(){return typeof window>"u"?null:window},RT=function(t,s){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let a=null;const n="data-tt-policy-suffix";s&&s.hasAttribute(n)&&(a=s.getAttribute(n));const i="dompurify"+(a?"#"+a:"");try{return t.createPolicy(i,{createHTML(l){return l},createScriptURL(l){return l}})}catch{return console.warn("TrustedTypes policy "+i+" could not be created."),null}},rm=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function zg(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:AT();const t=Ie=>zg(Ie);if(t.version="3.4.9",t.removed=[],!e||!e.document||e.document.nodeType!==wa.document||!e.Element)return t.isSupported=!1,t;let s=e.document;const a=s,n=a.currentScript;e.DocumentFragment;const i=e.HTMLTemplateElement,l=e.Node,o=e.Element,r=e.NodeFilter,c=e.NamedNodeMap;c===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;const d=e.DOMParser,u=e.trustedTypes,f=o.prototype,m=Sa(f,"cloneNode"),h=Sa(f,"remove"),g=Sa(f,"nextSibling"),A=Sa(f,"childNodes"),w=Sa(f,"parentNode"),y=Sa(f,"shadowRoot"),b=Sa(f,"attributes"),x=l&&l.prototype?Sa(l.prototype,"nodeType"):null,S=l&&l.prototype?Sa(l.prototype,"nodeName"):null;if(typeof i=="function"){const Ie=s.createElement("template");Ie.content&&Ie.content.ownerDocument&&(s=Ie.content.ownerDocument)}let _,T="",L,E=!1,P=0;const R=function(){if(P>0)throw Bn('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.')},I=function(M){R(),P++;try{return _.createHTML(M)}finally{P--}},H=function(M){R(),P++;try{return _.createScriptURL(M)}finally{P--}},X=function(){return E||(L=RT(u,n),E=!0),L},Q=s,N=Q.implementation,O=Q.createNodeIterator,$=Q.createDocumentFragment,ce=Q.getElementsByTagName,ue=a.importNode;let Z=rm();t.isSupported=typeof Bg=="function"&&typeof w=="function"&&N&&N.createHTMLDocument!==void 0;const J=yT,se=xT,q=_T,ye=wT,Oe=kT,le=ST,oe=CT,ve=ET;let j=om,V=null;const xe=st({},[...am,...Gc,...Wc,...Kc,...nm]);let k=null;const D=st({},[...im,...Jc,...lm,...Vo]);let W=Object.seal(Ci(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ge=null,pe=null;const he=Object.seal(Ci(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let we=!0,K=!0,ne=!1,te=!0,_e=!1,B=!0,fe=!1,de=!1,be=!1,Fe=!1,qe=!1,$e=!1,Ke=!0,at=!1;const Xe="user-content-";let Y=!0,Ae=!1,Re={},Me=null;const ae=st({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","selectedcontent","style","svg","template","thead","title","video","xmp"]);let Ne=null;const He=st({},["audio","video","img","source","image","track"]);let rt=null;const Zt=st({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),lt="http://www.w3.org/1998/Math/MathML",gt="http://www.w3.org/2000/svg",Qt="http://www.w3.org/1999/xhtml";let cs=Qt,oa=!1,js=null;const Ze=st({},[lt,gt,Qt],Vc);let ra=st({},["mi","mo","mn","ms","mtext"]),vs=st({},["annotation-xml"]);const Ms=st({},["title","style","font","a","script"]);let gs=null;const An=["application/xhtml+xml","text/html"],Yt="text/html";let _t=null,ds=null;const ba=s.createElement("form"),rn=function(M){return M instanceof RegExp||M instanceof Function},ca=function(){let M=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(ds&&ds===M)return;(!M||typeof M!="object")&&(M={}),M=xs(M),gs=An.indexOf(M.PARSER_MEDIA_TYPE)===-1?Yt:M.PARSER_MEDIA_TYPE,_t=gs==="application/xhtml+xml"?Vc:Sl,V=Pt(M,"ALLOWED_TAGS")&&As(M.ALLOWED_TAGS)?st({},M.ALLOWED_TAGS,_t):xe,k=Pt(M,"ALLOWED_ATTR")&&As(M.ALLOWED_ATTR)?st({},M.ALLOWED_ATTR,_t):D,js=Pt(M,"ALLOWED_NAMESPACES")&&As(M.ALLOWED_NAMESPACES)?st({},M.ALLOWED_NAMESPACES,Vc):Ze,rt=Pt(M,"ADD_URI_SAFE_ATTR")&&As(M.ADD_URI_SAFE_ATTR)?st(xs(Zt),M.ADD_URI_SAFE_ATTR,_t):Zt,Ne=Pt(M,"ADD_DATA_URI_TAGS")&&As(M.ADD_DATA_URI_TAGS)?st(xs(He),M.ADD_DATA_URI_TAGS,_t):He,Me=Pt(M,"FORBID_CONTENTS")&&As(M.FORBID_CONTENTS)?st({},M.FORBID_CONTENTS,_t):ae,ge=Pt(M,"FORBID_TAGS")&&As(M.FORBID_TAGS)?st({},M.FORBID_TAGS,_t):xs({}),pe=Pt(M,"FORBID_ATTR")&&As(M.FORBID_ATTR)?st({},M.FORBID_ATTR,_t):xs({}),Re=Pt(M,"USE_PROFILES")?M.USE_PROFILES&&typeof M.USE_PROFILES=="object"?xs(M.USE_PROFILES):M.USE_PROFILES:!1,we=M.ALLOW_ARIA_ATTR!==!1,K=M.ALLOW_DATA_ATTR!==!1,ne=M.ALLOW_UNKNOWN_PROTOCOLS||!1,te=M.ALLOW_SELF_CLOSE_IN_ATTR!==!1,_e=M.SAFE_FOR_TEMPLATES||!1,B=M.SAFE_FOR_XML!==!1,fe=M.WHOLE_DOCUMENT||!1,Fe=M.RETURN_DOM||!1,qe=M.RETURN_DOM_FRAGMENT||!1,$e=M.RETURN_TRUSTED_TYPE||!1,be=M.FORCE_BODY||!1,Ke=M.SANITIZE_DOM!==!1,at=M.SANITIZE_NAMED_PROPS||!1,Y=M.KEEP_CONTENT!==!1,Ae=M.IN_PLACE||!1,j=vT(M.ALLOWED_URI_REGEXP)?M.ALLOWED_URI_REGEXP:om,cs=typeof M.NAMESPACE=="string"?M.NAMESPACE:Qt,ra=Pt(M,"MATHML_TEXT_INTEGRATION_POINTS")&&M.MATHML_TEXT_INTEGRATION_POINTS&&typeof M.MATHML_TEXT_INTEGRATION_POINTS=="object"?xs(M.MATHML_TEXT_INTEGRATION_POINTS):st({},["mi","mo","mn","ms","mtext"]),vs=Pt(M,"HTML_INTEGRATION_POINTS")&&M.HTML_INTEGRATION_POINTS&&typeof M.HTML_INTEGRATION_POINTS=="object"?xs(M.HTML_INTEGRATION_POINTS):st({},["annotation-xml"]);const me=Pt(M,"CUSTOM_ELEMENT_HANDLING")&&M.CUSTOM_ELEMENT_HANDLING&&typeof M.CUSTOM_ELEMENT_HANDLING=="object"?xs(M.CUSTOM_ELEMENT_HANDLING):Ci(null);if(W=Ci(null),Pt(me,"tagNameCheck")&&rn(me.tagNameCheck)&&(W.tagNameCheck=me.tagNameCheck),Pt(me,"attributeNameCheck")&&rn(me.attributeNameCheck)&&(W.attributeNameCheck=me.attributeNameCheck),Pt(me,"allowCustomizedBuiltInElements")&&typeof me.allowCustomizedBuiltInElements=="boolean"&&(W.allowCustomizedBuiltInElements=me.allowCustomizedBuiltInElements),_e&&(K=!1),qe&&(Fe=!0),Re&&(V=st({},nm),k=Ci(null),Re.html===!0&&(st(V,am),st(k,im)),Re.svg===!0&&(st(V,Gc),st(k,Jc),st(k,Vo)),Re.svgFilters===!0&&(st(V,Wc),st(k,Jc),st(k,Vo)),Re.mathMl===!0&&(st(V,Kc),st(k,lm),st(k,Vo))),he.tagCheck=null,he.attributeCheck=null,Pt(M,"ADD_TAGS")&&(typeof M.ADD_TAGS=="function"?he.tagCheck=M.ADD_TAGS:As(M.ADD_TAGS)&&(V===xe&&(V=xs(V)),st(V,M.ADD_TAGS,_t))),Pt(M,"ADD_ATTR")&&(typeof M.ADD_ATTR=="function"?he.attributeCheck=M.ADD_ATTR:As(M.ADD_ATTR)&&(k===D&&(k=xs(k)),st(k,M.ADD_ATTR,_t))),Pt(M,"ADD_URI_SAFE_ATTR")&&As(M.ADD_URI_SAFE_ATTR)&&st(rt,M.ADD_URI_SAFE_ATTR,_t),Pt(M,"FORBID_CONTENTS")&&As(M.FORBID_CONTENTS)&&(Me===ae&&(Me=xs(Me)),st(Me,M.FORBID_CONTENTS,_t)),Pt(M,"ADD_FORBID_CONTENTS")&&As(M.ADD_FORBID_CONTENTS)&&(Me===ae&&(Me=xs(Me)),st(Me,M.ADD_FORBID_CONTENTS,_t)),Y&&(V["#text"]=!0),fe&&st(V,["html","head","body"]),V.table&&(st(V,["tbody"]),delete ge.tbody),M.TRUSTED_TYPES_POLICY){if(typeof M.TRUSTED_TYPES_POLICY.createHTML!="function")throw Bn('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof M.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Bn('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const Se=_;_=M.TRUSTED_TYPES_POLICY;try{T=I("")}catch(Ue){throw _=Se,Ue}}else M.TRUSTED_TYPES_POLICY===null?(_=void 0,T=""):(_===void 0&&(_=X()),_&&typeof T=="string"&&(T=I("")));(Z.uponSanitizeElement.length>0||Z.uponSanitizeAttribute.length>0)&&V===xe&&(V=xs(V)),Z.uponSanitizeAttribute.length>0&&k===D&&(k=xs(k)),Ns&&Ns(M),ds=M},re=st({},[...Gc,...Wc,...gT]),Ce=st({},[...Kc,...bT]),Ve=function(M){let me=w(M);(!me||!me.tagName)&&(me={namespaceURI:cs,tagName:"template"});const Se=Sl(M.tagName),Ue=Sl(me.tagName);return js[M.namespaceURI]?M.namespaceURI===gt?me.namespaceURI===Qt?Se==="svg":me.namespaceURI===lt?Se==="svg"&&(Ue==="annotation-xml"||ra[Ue]):!!re[Se]:M.namespaceURI===lt?me.namespaceURI===Qt?Se==="math":me.namespaceURI===gt?Se==="math"&&vs[Ue]:!!Ce[Se]:M.namespaceURI===Qt?me.namespaceURI===gt&&!vs[Ue]||me.namespaceURI===lt&&!ra[Ue]?!1:!Ce[Se]&&(Ms[Se]||!re[Se]):!!(gs==="application/xhtml+xml"&&js[M.namespaceURI]):!1},Ge=function(M){xi(t.removed,{element:M});try{w(M).removeChild(M)}catch{if(h(M),!w(M))throw Bn("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},kt=function(M){const me=A?A(M):M.childNodes;if(me){const Ue=[];Ua(me,We=>{xi(Ue,We)}),Ua(Ue,We=>{try{h(We)}catch{}})}const Se=b?b(M):null;if(Se)for(let Ue=Se.length-1;Ue>=0;--Ue){const We=Se[Ue],Ye=We&&We.name;if(typeof Ye=="string")try{M.removeAttribute(Ye)}catch{}}},St=function(M,me){try{xi(t.removed,{attribute:me.getAttributeNode(M),from:me})}catch{xi(t.removed,{attribute:null,from:me})}if(me.removeAttribute(M),M==="is")if(Fe||qe)try{Ge(me)}catch{}else try{me.setAttribute(M,"")}catch{}},cn=function(M){const me=b?b(M):M.attributes;if(me)for(let Se=me.length-1;Se>=0;--Se){const Ue=me[Se],We=Ue&&Ue.name;if(!(typeof We!="string"||k[_t(We)]))try{M.removeAttribute(We)}catch{}}},Rn=function(M){const me=[M];for(;me.length>0;){const Se=me.pop();(x?x(Se):Se.nodeType)===wa.element&&cn(Se);const We=A?A(Se):Se.childNodes;if(We)for(let Ye=We.length-1;Ye>=0;--Ye)me.push(We[Ye])}},dn=function(M){let me=null,Se=null;if(be)M=""+M;else{const Ye=Xf(M,/^[\r\n\t ]+/);Se=Ye&&Ye[0]}gs==="application/xhtml+xml"&&cs===Qt&&(M=''+M+"");const Ue=_?I(M):M;if(cs===Qt)try{me=new d().parseFromString(Ue,gs)}catch{}if(!me||!me.documentElement){me=N.createDocument(cs,"template",null);try{me.documentElement.innerHTML=oa?T:Ue}catch{}}const We=me.body||me.documentElement;return M&&Se&&We.insertBefore(s.createTextNode(Se),We.childNodes[0]||null),cs===Qt?ce.call(me,fe?"html":"body")[0]:fe?me.documentElement:We},In=function(M){return O.call(M.ownerDocument||M,M,r.SHOW_ELEMENT|r.SHOW_COMMENT|r.SHOW_TEXT|r.SHOW_PROCESSING_INSTRUCTION|r.SHOW_CDATA_SECTION,null)},da=function(M){var me,Se;M.normalize();const Ue=O.call(M.ownerDocument||M,M,r.SHOW_TEXT|r.SHOW_COMMENT|r.SHOW_CDATA_SECTION|r.SHOW_PROCESSING_INSTRUCTION,null);let We=Ue.nextNode();for(;We;){let Et=We.data;Ua([J,se,q],pt=>{Et=_i(Et,pt," ")}),We.data=Et,We=Ue.nextNode()}const Ye=(me=(Se=M.querySelectorAll)===null||Se===void 0?void 0:Se.call(M,"template"))!==null&&me!==void 0?me:[];Ua(Array.from(Ye),Et=>{qt(Et.content)&&da(Et.content)})},ya=function(M){const me=S?S(M):null;return typeof me!="string"||_t(me)!=="form"?!1:typeof M.nodeName!="string"||typeof M.textContent!="string"||typeof M.removeChild!="function"||M.attributes!==b(M)||typeof M.removeAttribute!="function"||typeof M.setAttribute!="function"||typeof M.namespaceURI!="string"||typeof M.insertBefore!="function"||typeof M.hasChildNodes!="function"||M.nodeType!==x(M)||M.childNodes!==A(M)},qt=function(M){if(!x||typeof M!="object"||M===null)return!1;try{return x(M)===wa.documentFragment}catch{return!1}},ua=function(M){if(!x||typeof M!="object"||M===null)return!1;try{return typeof x(M)=="number"}catch{return!1}};function as(Ie,M,me){Ua(Ie,Se=>{Se.call(t,M,me,ds)})}const un=function(M){let me=null;if(as(Z.beforeSanitizeElements,M,null),ya(M))return Ge(M),!0;const Se=_t(S?S(M):M.nodeName);if(as(Z.uponSanitizeElement,M,{tagName:Se,allowedTags:V}),B&&M.hasChildNodes()&&!ua(M.firstElementChild)&&us(/<[/\w!]/g,M.innerHTML)&&us(/<[/\w!]/g,M.textContent)||B&&M.namespaceURI===Qt&&Se==="style"&&ua(M.firstElementChild)||M.nodeType===wa.progressingInstruction||B&&M.nodeType===wa.comment&&us(/<[/\w]/g,M.data))return Ge(M),!0;if(ge[Se]||!(he.tagCheck instanceof Function&&he.tagCheck(Se))&&!V[Se]){if(!ge[Se]&&ri(Se)&&(W.tagNameCheck instanceof RegExp&&us(W.tagNameCheck,Se)||W.tagNameCheck instanceof Function&&W.tagNameCheck(Se)))return!1;if(Y&&!Me[Se]){const We=w(M),Ye=A(M);if(Ye&&We){const Et=Ye.length;for(let pt=Et-1;pt>=0;--pt){const At=Ae?Ye[pt]:m(Ye[pt],!0);We.insertBefore(At,g(M))}}}return Ge(M),!0}return(x?x(M):M.nodeType)===wa.element&&!Ve(M)||(Se==="noscript"||Se==="noembed"||Se==="noframes")&&us(/<\/no(script|embed|frames)/i,M.innerHTML)?(Ge(M),!0):(_e&&M.nodeType===wa.text&&(me=M.textContent,Ua([J,se,q],We=>{me=_i(me,We," ")}),M.textContent!==me&&(xi(t.removed,{element:M.cloneNode()}),M.textContent=me)),as(Z.afterSanitizeElements,M,null),!1)},On=function(M,me,Se){if(pe[me]||Ke&&(me==="id"||me==="name")&&(Se in s||Se in ba))return!1;const Ue=k[me]||he.attributeCheck instanceof Function&&he.attributeCheck(me,M);if(!(K&&!pe[me]&&us(ye,me))){if(!(we&&us(Oe,me))){if(!Ue||pe[me]){if(!(ri(M)&&(W.tagNameCheck instanceof RegExp&&us(W.tagNameCheck,M)||W.tagNameCheck instanceof Function&&W.tagNameCheck(M))&&(W.attributeNameCheck instanceof RegExp&&us(W.attributeNameCheck,me)||W.attributeNameCheck instanceof Function&&W.attributeNameCheck(me,M))||me==="is"&&W.allowCustomizedBuiltInElements&&(W.tagNameCheck instanceof RegExp&&us(W.tagNameCheck,Se)||W.tagNameCheck instanceof Function&&W.tagNameCheck(Se))))return!1}else if(!rt[me]){if(!us(j,_i(Se,oe,""))){if(!((me==="src"||me==="xlink:href"||me==="href")&&M!=="script"&&em(Se,"data:")===0&&Ne[M])){if(!(ne&&!us(le,_i(Se,oe,"")))){if(Se)return!1}}}}}}return!0},oi=st({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),ri=function(M){return!oi[Sl(M)]&&us(ve,M)},ci=function(M){as(Z.beforeSanitizeAttributes,M,null);const me=M.attributes;if(!me||ya(M))return;const Se={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:k,forceKeepAttr:void 0};let Ue=me.length;for(;Ue--;){const We=me[Ue],Ye=We.name,Et=We.namespaceURI,pt=We.value,At=_t(Ye),bs=pt;let Nt=Ye==="value"?bs:dT(bs);if(Se.attrName=At,Se.attrValue=Nt,Se.keepAttr=!0,Se.forceKeepAttr=void 0,as(Z.uponSanitizeAttribute,M,Se),Nt=Se.attrValue,at&&(At==="id"||At==="name")&&em(Nt,Xe)!==0&&(St(Ye,M),Nt=Xe+Nt),B&&us(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,Nt)){St(Ye,M);continue}if(At==="attributename"&&Xf(Nt,"href")){St(Ye,M);continue}if(Se.forceKeepAttr)continue;if(!Se.keepAttr){St(Ye,M);continue}if(!te&&us(/\/>/i,Nt)){St(Ye,M);continue}_e&&Ua([J,se,q],di=>{Nt=_i(Nt,di," ")});const Na=_t(M.nodeName);if(!On(Na,At,Nt)){St(Ye,M);continue}if(_&&typeof u=="object"&&typeof u.getAttributeType=="function"&&!Et)switch(u.getAttributeType(Na,At)){case"TrustedHTML":{Nt=I(Nt);break}case"TrustedScriptURL":{Nt=H(Nt);break}}if(Nt!==bs)try{Et?M.setAttributeNS(Et,Ye,Nt):M.setAttribute(Ye,Nt),ya(M)?Ge(M):Yf(t.removed)}catch{St(Ye,M)}}as(Z.afterSanitizeAttributes,M,null)},ns=function(M){let me=null;const Se=In(M);for(as(Z.beforeSanitizeShadowDOM,M,null);me=Se.nextNode();)if(as(Z.uponSanitizeShadowNode,me,null),un(me),ci(me),qt(me.content)&&ns(me.content),(x?x(me):me.nodeType)===wa.element){const We=y?y(me):me.shadowRoot;qt(We)&&(Ln(We),ns(We))}as(Z.afterSanitizeShadowDOM,M,null)},Ln=function(M){const me=[{node:M,shadow:null}];for(;me.length>0;){const Se=me.pop();if(Se.shadow){ns(Se.shadow);continue}const Ue=Se.node,Ye=(x?x(Ue):Ue.nodeType)===wa.element,Et=A?A(Ue):Ue.childNodes;if(Et)for(let pt=Et.length-1;pt>=0;--pt)me.push({node:Et[pt],shadow:null});if(Ye){const pt=S?S(Ue):null;if(typeof pt=="string"&&_t(pt)==="template"){const At=Ue.content;qt(At)&&me.push({node:At,shadow:null})}}if(Ye){const pt=y?y(Ue):Ue.shadowRoot;qt(pt)&&me.push({node:null,shadow:pt},{node:pt,shadow:null})}}};return t.sanitize=function(Ie){let M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},me=null,Se=null,Ue=null,We=null;if(oa=!Ie,oa&&(Ie=""),typeof Ie!="string"&&!ua(Ie)&&(Ie=hT(Ie),typeof Ie!="string"))throw Bn("dirty is not a string, aborting");if(!t.isSupported)return Ie;de||ca(M),t.removed=[];const Ye=Ae&&typeof Ie!="string"&&ua(Ie);if(Ye){const At=S?S(Ie):Ie.nodeName;if(typeof At=="string"){const bs=_t(At);if(!V[bs]||ge[bs])throw Bn("root node is forbidden and cannot be sanitized in-place")}if(ya(Ie))throw Bn("root node is clobbered and cannot be sanitized in-place");try{Ln(Ie)}catch(bs){throw kt(Ie),bs}}else if(ua(Ie))me=dn(""),Se=me.ownerDocument.importNode(Ie,!0),Se.nodeType===wa.element&&Se.nodeName==="BODY"||Se.nodeName==="HTML"?me=Se:me.appendChild(Se),Ln(Se);else{if(!Fe&&!_e&&!fe&&Ie.indexOf("<")===-1)return _&&$e?I(Ie):Ie;if(me=dn(Ie),!me)return Fe?null:$e?T:""}me&&be&&Ge(me.firstChild);const Et=In(Ye?Ie:me);try{for(;Ue=Et.nextNode();)un(Ue),ci(Ue),qt(Ue.content)&&ns(Ue.content)}catch(At){throw Ye&&kt(Ie),At}if(Ye)return Ua(t.removed,At=>{At.element&&Rn(At.element)}),_e&&da(Ie),Ie;if(Fe){if(_e&&da(me),qe)for(We=$.call(me.ownerDocument);me.firstChild;)We.appendChild(me.firstChild);else We=me;return(k.shadowroot||k.shadowrootmode)&&(We=ue.call(a,We,!0)),We}let pt=fe?me.outerHTML:me.innerHTML;return fe&&V["!doctype"]&&me.ownerDocument&&me.ownerDocument.doctype&&me.ownerDocument.doctype.name&&us(TT,me.ownerDocument.doctype.name)&&(pt=" -`+pt),_e&&Ua([J,se,q],At=>{pt=_i(pt,At," ")}),_&&$e?I(pt):pt},t.setConfig=function(){let Ie=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};ca(Ie),de=!0},t.clearConfig=function(){ds=null,de=!1,_=L,T=""},t.isValidAttribute=function(Ie,M,me){ds||ca({});const Se=_t(Ie),Ue=_t(M);return On(Se,Ue,me)},t.addHook=function(Ie,M){typeof M=="function"&&xi(Z[Ie],M)},t.removeHook=function(Ie,M){if(M!==void 0){const me=rT(Z[Ie],M);return me===-1?void 0:cT(Z[Ie],me,1)[0]}return Yf(Z[Ie])},t.removeHooks=function(Ie){Z[Ie]=[]},t.removeAllHooks=function(){Z=rm()},t}var cm=zg();function Uu(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var li=Uu();function Hg(e){li=e}var Nl={exec:()=>null};function yt(e,t=""){let s=typeof e=="string"?e:e.source;const a={replace:(n,i)=>{let l=typeof i=="string"?i:i.source;return l=l.replace(Os.caret,"$1"),s=s.replace(n,l),a},getRegex:()=>new RegExp(s,t)};return a}var Os={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},IT=/^(?:[ \t]*(?:\n|$))+/,OT=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,LT=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,mo=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,NT=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,zu=/(?:[*+-]|\d{1,9}[.)])/,jg=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,qg=yt(jg).replace(/bull/g,zu).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),MT=yt(jg).replace(/bull/g,zu).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Hu=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,DT=/^[^\n]+/,ju=/(?!\s*\])(?:\\.|[^\[\]\\])+/,PT=yt(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",ju).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),$T=yt(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,zu).getRegex(),lc="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",qu=/|$))/,FT=yt("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",qu).replace("tag",lc).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Vg=yt(Hu).replace("hr",mo).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",lc).getRegex(),BT=yt(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",Vg).getRegex(),Vu={blockquote:BT,code:OT,def:PT,fences:LT,heading:NT,hr:mo,html:FT,lheading:qg,list:$T,newline:IT,paragraph:Vg,table:Nl,text:DT},dm=yt("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",mo).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",lc).getRegex(),UT={...Vu,lheading:MT,table:dm,paragraph:yt(Hu).replace("hr",mo).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",dm).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",lc).getRegex()},zT={...Vu,html:yt(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",qu).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Nl,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:yt(Hu).replace("hr",mo).replace("heading",` *#{1,6} *[^ -]`).replace("lheading",qg).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},HT=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,jT=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,Gg=/^( {2,}|\\)\n(?!\s*$)/,qT=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,Jg=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,JT=yt(Jg,"u").replace(/punct/g,oc).getRegex(),ZT=yt(Jg,"u").replace(/punct/g,Kg).getRegex(),Zg="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",QT=yt(Zg,"gu").replace(/notPunctSpace/g,Wg).replace(/punctSpace/g,Gu).replace(/punct/g,oc).getRegex(),YT=yt(Zg,"gu").replace(/notPunctSpace/g,WT).replace(/punctSpace/g,GT).replace(/punct/g,Kg).getRegex(),XT=yt("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,Wg).replace(/punctSpace/g,Gu).replace(/punct/g,oc).getRegex(),eE=yt(/\\(punct)/,"gu").replace(/punct/g,oc).getRegex(),tE=yt(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),sE=yt(qu).replace("(?:-->|$)","-->").getRegex(),aE=yt("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",sE).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),Rr=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,nE=yt(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",Rr).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Qg=yt(/^!?\[(label)\]\[(ref)\]/).replace("label",Rr).replace("ref",ju).getRegex(),Yg=yt(/^!?\[(ref)\](?:\[\])?/).replace("ref",ju).getRegex(),iE=yt("reflink|nolink(?!\\()","g").replace("reflink",Qg).replace("nolink",Yg).getRegex(),Wu={_backpedal:Nl,anyPunctuation:eE,autolink:tE,blockSkip:KT,br:Gg,code:jT,del:Nl,emStrongLDelim:JT,emStrongRDelimAst:QT,emStrongRDelimUnd:XT,escape:HT,link:nE,nolink:Yg,punctuation:VT,reflink:Qg,reflinkSearch:iE,tag:aE,text:qT,url:Nl},lE={...Wu,link:yt(/^!?\[(label)\]\((.*?)\)/).replace("label",Rr).getRegex(),reflink:yt(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Rr).getRegex()},Dd={...Wu,emStrongRDelimAst:YT,emStrongLDelim:ZT,url:yt(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},um=e=>rE[e];function Ca(e,t){if(t){if(Os.escapeTest.test(e))return e.replace(Os.escapeReplace,um)}else if(Os.escapeTestNoEncode.test(e))return e.replace(Os.escapeReplaceNoEncode,um);return e}function pm(e){try{e=encodeURI(e).replace(Os.percentDecode,"%")}catch{return null}return e}function fm(e,t){var i;const s=e.replace(Os.findPipe,(l,o,r)=>{let c=!1,d=o;for(;--d>=0&&r[d]==="\\";)c=!c;return c?"|":" |"}),a=s.split(Os.splitPipe);let n=0;if(a[0].trim()||a.shift(),a.length>0&&!((i=a.at(-1))!=null&&i.trim())&&a.pop(),t)if(a.length>t)a.splice(t);else for(;a.length0?-2:-1}function mm(e,t,s,a,n){const i=t.href,l=t.title||null,o=e[1].replace(n.other.outputLinkReplace,"$1");a.state.inLink=!0;const r={type:e[0].charAt(0)==="!"?"image":"link",raw:s,href:i,title:l,text:o,tokens:a.inlineTokens(o)};return a.state.inLink=!1,r}function dE(e,t,s){const a=e.match(s.other.indentCodeCompensation);if(a===null)return t;const n=a[1];return t.split(` + `,setup(){const e=f({}),t=f(!0),s=f(null),a=f([]),n=f(!1),i=f([]),l=f(!1),o=f(!1),r=f([]),c=f(0),d=f(null),u=f({reload:!1,clearSessions:!1,stopLoops:!1});let p=0;const m=B(()=>{const ce=e.value.uptime_seconds||0,Z=Math.floor(ce/86400),Q=Math.floor(ce%86400/3600),ae=Math.floor(ce%3600/60),V=[];return Z>0&&V.push(`${Z}d`),Q>0&&V.push(`${Q}h`),(V.length===0||Z===0&&Q===0)&&V.push(`${ae}m`),V.join(" ")}),h=B(()=>{const ce=e.value.uptime_seconds||0;return 125.66*(1-Math.min(ce/86400,1))}),y=B(()=>{const ce=e.value;return[{label:"Guilds",value:ce.guild_count??0,icon:"home",iconColor:"text-blue-400"},{label:"Sessions",value:ce.session_count??0,icon:"message",iconColor:"text-yellow-400"},{label:"Tools",value:ce.tool_count??0,icon:"wrench",iconColor:"text-purple-400",sub:`${ce.skill_count??0} skills`,subColor:"text-gray-500"},{label:"Loops",value:ce.loop_count??0,icon:"rotate",iconColor:"text-green-400",color:ce.loop_count>0?"text-green-400":"",highlight:ce.loop_count>0},{label:"Agents",value:ce.agent_running??0,icon:"bot",iconColor:"text-cyan-400",sub:ce.agent_count>0?`${ce.agent_count} total`:"",subColor:"text-gray-500",highlight:(ce.agent_running??0)>0},{label:"Processes",value:ce.process_running??0,icon:"sliders",iconColor:"text-orange-400",sub:ce.process_count>0?`${ce.process_count} total`:"",subColor:"text-gray-500",highlight:(ce.process_running??0)>0},{label:"Schedules",value:ce.schedule_count??0,icon:"clock",iconColor:"text-amber-400",sub:(ce.schedule_failing>0?`${ce.schedule_failing} failing`:"")+(ce.schedule_failing>0&&ce.schedule_paused>0?", ":"")+(ce.schedule_paused>0?`${ce.schedule_paused} paused`:"")||void 0,subColor:ce.schedule_failing>0?"text-red-400":"text-yellow-400",color:ce.schedule_failing>0?"text-red-400":"",highlight:ce.schedule_failing>0},{label:"Users",value:ce.user_count??0,icon:"users",iconColor:"text-indigo-400"},...d.value!==null?[{label:"Knowledge",value:d.value,icon:"book",iconColor:"text-teal-400",sub:"chunks",subColor:"text-gray-500"}]:[]]}),k=B(()=>{const ce=e.value,Z=[];return Z.push({label:"Bot",status:ce.status==="online"?"ok":"warn",detail:ce.status==="online"?"Online":"Starting"}),(ce.schedule_failing||0)>0?Z.push({label:"Schedules",status:"error",detail:`${ce.schedule_failing} failing`}):(ce.schedule_count||0)>0&&Z.push({label:"Schedules",status:"ok",detail:`${ce.schedule_count} configured`}),(ce.loop_count||0)>0&&Z.push({label:"Loops",status:"ok",detail:`${ce.loop_count} active`}),(ce.agent_running||0)>0&&Z.push({label:"Agents",status:"ok",detail:`${ce.agent_running} running`}),(ce.process_running||0)>0&&Z.push({label:"Processes",status:"ok",detail:`${ce.process_running} running`}),Z});async function A(){try{e.value=await z.get("/api/status"),s.value=null}catch(ce){s.value=ce.message}finally{t.value=!1}}let g=0,b=0,x=0,w=0;function _(ce,Z){const Q=new Set;return[...Z,...ce].filter(ae=>{const V=ae._hmac||JSON.stringify([ae.timestamp,ae.tool_name,ae.user_id,ae.result_summary,ae.error]);return Q.has(V)?!1:(Q.add(V),!0)})}async function T(){const ce=++g,Z=x;n.value=!0;try{const Q=await z.get("/api/audit?limit=10");if(ce!==g)return;const ae=Z===x?[]:a.value.filter(V=>(V._liveEpoch||0)>Z);a.value=_(Q,ae).slice(0,10),c.value=ae.length}catch{}ce===g&&(n.value=!1)}async function L(){const ce=++b,Z=w;l.value=!0;try{const Q=await z.get("/api/audit?error_only=1&limit=5");if(ce!==b)return;const ae=Z===w?[]:i.value.filter(V=>(V._liveErrorEpoch||0)>Z);i.value=_(Q,ae).slice(0,5),o.value=!1}catch{if(ce!==b)return;o.value=Z===w||i.value.length===0}ce===b&&(l.value=!1)}async function E(){try{const ce=await z.get("/api/knowledge");d.value=(Array.isArray(ce)?ce:[]).reduce((Z,Q)=>Z+(Q.chunks||0),0)}catch{d.value=null}}async function F(){try{const ce=await z.get("/api/agents");r.value=ce.filter(Z=>Z.status==="running")}catch{}}async function O(){u.value={...u.value,reload:!0};try{await z.post("/api/reload"),Se.success("Config reloaded")}catch(ce){Se.error(ce.message)}u.value={...u.value,reload:!1}}async function I(){if(!await is({title:"Clear all sessions",message:"Clear all conversation sessions? This cannot be undone.",confirmLabel:"Clear All",danger:!0}))return;u.value={...u.value,clearSessions:!0};const Z=e.value.session_count;e.value={...e.value,session_count:0};try{const Q=await z.post("/api/sessions/clear-all");Se.success(`Cleared ${Q.count} session${Q.count!==1?"s":""}`),await A()}catch(Q){e.value={...e.value,session_count:Z},Se.error(Q.message)}u.value={...u.value,clearSessions:!1}}async function H(){if(!await is({title:"Stop all loops",message:"Stop all running loops?",confirmLabel:"Stop Loops",danger:!0}))return;u.value={...u.value,stopLoops:!0};const Z=e.value.loop_count;e.value={...e.value,loop_count:0};try{const Q=await z.post("/api/loops/stop-all");Se.success(Q.result),await A()}catch(Q){e.value={...e.value,loop_count:Z},Se.error(Q.message)}u.value={...u.value,stopLoops:!1}}function te(){t.value=!0,s.value=null,A(),T(),L(),F()}let ee=null,N=null,M=null;function $(ce){if(ce.payload&&ce.payload.tool_name){x+=1;const Z={...ce.payload,_isNew:!0,_key:++p,_liveEpoch:x};a.value.unshift(Z),a.value.length>10&&a.value.pop(),c.value++,Z.error&&(w+=1,Z._liveErrorEpoch=w,o.value=!1,i.value.unshift(Z),i.value.length>5&&i.value.pop()),setTimeout(()=>{Z._isNew=!1},1500),clearTimeout(M),M=setTimeout(()=>{c.value=0},1e4)}}let re=null;return st(async()=>{await Promise.all([A(),T(),L(),F(),E()]),ee=setInterval(A,15e3),N=setInterval(F,1e4),mt.subscribe("events",$),re=mt.onReconnected(()=>{T(),L()})}),wt(()=>{ee&&clearInterval(ee),N&&clearInterval(N),clearTimeout(M),mt.unsubscribe("events",$),re&&(re(),re=null)}),{status:e,loading:t,error:s,uptime:m,uptimeRingOffset:h,stats:y,healthIndicators:k,activity:a,activityLoading:n,newEventCount:c,errors:i,errorsLoading:l,errorsError:o,agents:r,actionLoading:u,fetchActivity:T,fetchErrors:L,fetchStatus:A,onEvent:$,formatTime:w1,formatDuration:Xi,retry:te,reloadConfig:O,clearSessions:I,stopAllLoops:H}}};/*! @license DOMPurify 3.4.9 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.9/LICENSE */function lm(e,t){(t==null||t>e.length)&&(t=e.length);for(var s=0,a=Array(t);s2?a-2:0),i=2;i1?s-1:0),n=1;n"u"?null:ts(BigInt.prototype.toString),pm=typeof Symbol>"u"?null:ts(Symbol.prototype.toString),jt=ts(Object.prototype.hasOwnProperty),kl=ts(Object.prototype.toString),us=ts(RegExp.prototype.test),qn=MT(TypeError);function ts(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var s=arguments.length,a=new Array(s>1?s-1:0),n=1;n2&&arguments[2]!==void 0?arguments[2]:Il;if(om&&om(e,null),!Os(t))return e;let a=t.length;for(;a--;){let n=t[a];if(typeof n=="string"){const i=s(n);i!==n&&(TT(t)||(t[a]=i),n=i)}e[n]=!0}return e}function DT(e){for(let t=0;t/g),HT=da(/\${[\w\W]*/g),jT=da(/^data-[\-\w.\u00B7-\uFFFF]+$/),qT=da(/^aria-[\-\w]+$/),gm=da(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),VT=da(/^(?:\w+script|data):/i),GT=da(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),WT=da(/^html$/i),KT=da(/^[a-z][.\w]*(-[.\w]+)+$/i),Ca={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,progressingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},JT=function(){return typeof window>"u"?null:window},ZT=function(t,s){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let a=null;const n="data-tt-policy-suffix";s&&s.hasAttribute(n)&&(a=s.getAttribute(n));const i="dompurify"+(a?"#"+a:"");try{return t.createPolicy(i,{createHTML(l){return l},createScriptURL(l){return l}})}catch{return console.warn("TrustedTypes policy "+i+" could not be created."),null}},bm=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function Qg(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:JT();const t=Ie=>Qg(Ie);if(t.version="3.4.9",t.removed=[],!e||!e.document||e.document.nodeType!==Ca.document||!e.Element)return t.isSupported=!1,t;let s=e.document;const a=s,n=a.currentScript;e.DocumentFragment;const i=e.HTMLTemplateElement,l=e.Node,o=e.Element,r=e.NodeFilter,c=e.NamedNodeMap;c===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;const d=e.DOMParser,u=e.trustedTypes,p=o.prototype,m=Ea(p,"cloneNode"),h=Ea(p,"remove"),y=Ea(p,"nextSibling"),k=Ea(p,"childNodes"),A=Ea(p,"parentNode"),g=Ea(p,"shadowRoot"),b=Ea(p,"attributes"),x=l&&l.prototype?Ea(l.prototype,"nodeType"):null,w=l&&l.prototype?Ea(l.prototype,"nodeName"):null;if(typeof i=="function"){const Ie=s.createElement("template");Ie.content&&Ie.content.ownerDocument&&(s=Ie.content.ownerDocument)}let _,T="",L,E=!1,F=0;const O=function(){if(F>0)throw qn('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.')},I=function(D){O(),F++;try{return _.createHTML(D)}finally{F--}},H=function(D){O(),F++;try{return _.createScriptURL(D)}finally{F--}},te=function(){return E||(L=ZT(u,n),E=!0),L},ee=s,N=ee.implementation,M=ee.createNodeIterator,$=ee.createDocumentFragment,re=ee.getElementsByTagName,ce=a.importNode;let Z=bm();t.isSupported=typeof Jg=="function"&&typeof A=="function"&&N&&N.createHTMLDocument!==void 0;const Q=UT,ae=zT,V=HT,be=jT,Oe=qT,le=VT,oe=GT,ve=KT;let j=gm,Y=null;const xe=nt({},[...fm,...ed,...td,...sd,...mm]);let S=null;const P=nt({},[...vm,...ad,...hm,...Yo]);let G=Object.seal(Ai(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),he=null,fe=null;const me=Object.seal(Ai(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let we=!0,J=!0,ie=!1,X=!0,_e=!1,U=!0,de=!1,ge=!1,Ae=!1,$e=!1,We=!1,je=!1,Xe=!0,it=!1;const Qe="user-content-";let se=!0,Re=!1,Le={},Te=null;const W=nt({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","selectedcontent","style","svg","template","thead","title","video","xmp"]);let De=null;const ze=nt({},["audio","video","img","source","image","track"]);let Ye=null;const Dt=nt({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",Rt="http://www.w3.org/2000/svg",Pt="http://www.w3.org/1999/xhtml";let Ts=Pt,Vs=!1,Gs=null;const zt=nt({},[et,Rt,Pt],Xc);let ta=nt({},["mi","mo","mn","ms","mtext"]),Ws=nt({},["annotation-xml"]);const Pa=nt({},["title","style","font","a","script"]);let hs=null;const $a=["application/xhtml+xml","text/html"],Es="text/html";let Ct=null,ls=null;const sa=s.createElement("form"),ba=function(D){return D instanceof RegExp||D instanceof Function},Fe=function(){let D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(ls&&ls===D)return;(!D||typeof D!="object")&&(D={}),D=xs(D),hs=$a.indexOf(D.PARSER_MEDIA_TYPE)===-1?Es:D.PARSER_MEDIA_TYPE,Ct=hs==="application/xhtml+xml"?Xc:Il,Y=jt(D,"ALLOWED_TAGS")&&Os(D.ALLOWED_TAGS)?nt({},D.ALLOWED_TAGS,Ct):xe,S=jt(D,"ALLOWED_ATTR")&&Os(D.ALLOWED_ATTR)?nt({},D.ALLOWED_ATTR,Ct):P,Gs=jt(D,"ALLOWED_NAMESPACES")&&Os(D.ALLOWED_NAMESPACES)?nt({},D.ALLOWED_NAMESPACES,Xc):zt,Ye=jt(D,"ADD_URI_SAFE_ATTR")&&Os(D.ADD_URI_SAFE_ATTR)?nt(xs(Dt),D.ADD_URI_SAFE_ATTR,Ct):Dt,De=jt(D,"ADD_DATA_URI_TAGS")&&Os(D.ADD_DATA_URI_TAGS)?nt(xs(ze),D.ADD_DATA_URI_TAGS,Ct):ze,Te=jt(D,"FORBID_CONTENTS")&&Os(D.FORBID_CONTENTS)?nt({},D.FORBID_CONTENTS,Ct):W,he=jt(D,"FORBID_TAGS")&&Os(D.FORBID_TAGS)?nt({},D.FORBID_TAGS,Ct):xs({}),fe=jt(D,"FORBID_ATTR")&&Os(D.FORBID_ATTR)?nt({},D.FORBID_ATTR,Ct):xs({}),Le=jt(D,"USE_PROFILES")?D.USE_PROFILES&&typeof D.USE_PROFILES=="object"?xs(D.USE_PROFILES):D.USE_PROFILES:!1,we=D.ALLOW_ARIA_ATTR!==!1,J=D.ALLOW_DATA_ATTR!==!1,ie=D.ALLOW_UNKNOWN_PROTOCOLS||!1,X=D.ALLOW_SELF_CLOSE_IN_ATTR!==!1,_e=D.SAFE_FOR_TEMPLATES||!1,U=D.SAFE_FOR_XML!==!1,de=D.WHOLE_DOCUMENT||!1,$e=D.RETURN_DOM||!1,We=D.RETURN_DOM_FRAGMENT||!1,je=D.RETURN_TRUSTED_TYPE||!1,Ae=D.FORCE_BODY||!1,Xe=D.SANITIZE_DOM!==!1,it=D.SANITIZE_NAMED_PROPS||!1,se=D.KEEP_CONTENT!==!1,Re=D.IN_PLACE||!1,j=$T(D.ALLOWED_URI_REGEXP)?D.ALLOWED_URI_REGEXP:gm,Ts=typeof D.NAMESPACE=="string"?D.NAMESPACE:Pt,ta=jt(D,"MATHML_TEXT_INTEGRATION_POINTS")&&D.MATHML_TEXT_INTEGRATION_POINTS&&typeof D.MATHML_TEXT_INTEGRATION_POINTS=="object"?xs(D.MATHML_TEXT_INTEGRATION_POINTS):nt({},["mi","mo","mn","ms","mtext"]),Ws=jt(D,"HTML_INTEGRATION_POINTS")&&D.HTML_INTEGRATION_POINTS&&typeof D.HTML_INTEGRATION_POINTS=="object"?xs(D.HTML_INTEGRATION_POINTS):nt({},["annotation-xml"]);const pe=jt(D,"CUSTOM_ELEMENT_HANDLING")&&D.CUSTOM_ELEMENT_HANDLING&&typeof D.CUSTOM_ELEMENT_HANDLING=="object"?xs(D.CUSTOM_ELEMENT_HANDLING):Ai(null);if(G=Ai(null),jt(pe,"tagNameCheck")&&ba(pe.tagNameCheck)&&(G.tagNameCheck=pe.tagNameCheck),jt(pe,"attributeNameCheck")&&ba(pe.attributeNameCheck)&&(G.attributeNameCheck=pe.attributeNameCheck),jt(pe,"allowCustomizedBuiltInElements")&&typeof pe.allowCustomizedBuiltInElements=="boolean"&&(G.allowCustomizedBuiltInElements=pe.allowCustomizedBuiltInElements),_e&&(J=!1),We&&($e=!0),Le&&(Y=nt({},mm),S=Ai(null),Le.html===!0&&(nt(Y,fm),nt(S,vm)),Le.svg===!0&&(nt(Y,ed),nt(S,ad),nt(S,Yo)),Le.svgFilters===!0&&(nt(Y,td),nt(S,ad),nt(S,Yo)),Le.mathMl===!0&&(nt(Y,sd),nt(S,hm),nt(S,Yo))),me.tagCheck=null,me.attributeCheck=null,jt(D,"ADD_TAGS")&&(typeof D.ADD_TAGS=="function"?me.tagCheck=D.ADD_TAGS:Os(D.ADD_TAGS)&&(Y===xe&&(Y=xs(Y)),nt(Y,D.ADD_TAGS,Ct))),jt(D,"ADD_ATTR")&&(typeof D.ADD_ATTR=="function"?me.attributeCheck=D.ADD_ATTR:Os(D.ADD_ATTR)&&(S===P&&(S=xs(S)),nt(S,D.ADD_ATTR,Ct))),jt(D,"ADD_URI_SAFE_ATTR")&&Os(D.ADD_URI_SAFE_ATTR)&&nt(Ye,D.ADD_URI_SAFE_ATTR,Ct),jt(D,"FORBID_CONTENTS")&&Os(D.FORBID_CONTENTS)&&(Te===W&&(Te=xs(Te)),nt(Te,D.FORBID_CONTENTS,Ct)),jt(D,"ADD_FORBID_CONTENTS")&&Os(D.ADD_FORBID_CONTENTS)&&(Te===W&&(Te=xs(Te)),nt(Te,D.ADD_FORBID_CONTENTS,Ct)),se&&(Y["#text"]=!0),de&&nt(Y,["html","head","body"]),Y.table&&(nt(Y,["tbody"]),delete he.tbody),D.TRUSTED_TYPES_POLICY){if(typeof D.TRUSTED_TYPES_POLICY.createHTML!="function")throw qn('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof D.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw qn('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const ke=_;_=D.TRUSTED_TYPES_POLICY;try{T=I("")}catch(Be){throw _=ke,Be}}else D.TRUSTED_TYPES_POLICY===null?(_=void 0,T=""):(_===void 0&&(_=te()),_&&typeof T=="string"&&(T=I("")));(Z.uponSanitizeElement.length>0||Z.uponSanitizeAttribute.length>0)&&Y===xe&&(Y=xs(Y)),Z.uponSanitizeAttribute.length>0&&S===P&&(S=xs(S)),Ps&&Ps(D),ls=D},ue=nt({},[...ed,...td,...FT]),Ce=nt({},[...sd,...BT]),qe=function(D){let pe=A(D);(!pe||!pe.tagName)&&(pe={namespaceURI:Ts,tagName:"template"});const ke=Il(D.tagName),Be=Il(pe.tagName);return Gs[D.namespaceURI]?D.namespaceURI===Rt?pe.namespaceURI===Pt?ke==="svg":pe.namespaceURI===et?ke==="svg"&&(Be==="annotation-xml"||ta[Be]):!!ue[ke]:D.namespaceURI===et?pe.namespaceURI===Pt?ke==="math":pe.namespaceURI===Rt?ke==="math"&&Ws[Be]:!!Ce[ke]:D.namespaceURI===Pt?pe.namespaceURI===Rt&&!Ws[Be]||pe.namespaceURI===et&&!ta[Be]?!1:!Ce[ke]&&(Pa[ke]||!ue[ke]):!!(hs==="application/xhtml+xml"&&Gs[D.namespaceURI]):!1},Ge=function(D){ki(t.removed,{element:D});try{A(D).removeChild(D)}catch{if(h(D),!A(D))throw qn("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},It=function(D){const pe=k?k(D):D.childNodes;if(pe){const Be=[];qa(pe,Ue=>{ki(Be,Ue)}),qa(Be,Ue=>{try{h(Ue)}catch{}})}const ke=b?b(D):null;if(ke)for(let Be=ke.length-1;Be>=0;--Be){const Ue=ke[Be],Ke=Ue&&Ue.name;if(typeof Ke=="string")try{D.removeAttribute(Ke)}catch{}}},ot=function(D,pe){try{ki(t.removed,{attribute:pe.getAttributeNode(D),from:pe})}catch{ki(t.removed,{attribute:null,from:pe})}if(pe.removeAttribute(D),D==="is")if($e||We)try{Ge(pe)}catch{}else try{pe.setAttribute(D,"")}catch{}},pn=function(D){const pe=b?b(D):D.attributes;if(pe)for(let ke=pe.length-1;ke>=0;--ke){const Be=pe[ke],Ue=Be&&Be.name;if(!(typeof Ue!="string"||S[Ct(Ue)]))try{D.removeAttribute(Ue)}catch{}}},pi=function(D){const pe=[D];for(;pe.length>0;){const ke=pe.pop();(x?x(ke):ke.nodeType)===Ca.element&&pn(ke);const Ue=k?k(ke):ke.childNodes;if(Ue)for(let Ke=Ue.length-1;Ke>=0;--Ke)pe.push(Ue[Ke])}},Fa=function(D){let pe=null,ke=null;if(Ae)D=""+D;else{const Ke=cm(D,/^[\r\n\t ]+/);ke=Ke&&Ke[0]}hs==="application/xhtml+xml"&&Ts===Pt&&(D=''+D+"");const Be=_?I(D):D;if(Ts===Pt)try{pe=new d().parseFromString(Be,hs)}catch{}if(!pe||!pe.documentElement){pe=N.createDocument(Ts,"template",null);try{pe.documentElement.innerHTML=Vs?T:Be}catch{}}const Ue=pe.body||pe.documentElement;return D&&ke&&Ue.insertBefore(s.createTextNode(ke),Ue.childNodes[0]||null),Ts===Pt?re.call(pe,de?"html":"body")[0]:de?pe.documentElement:Ue},fi=function(D){return M.call(D.ownerDocument||D,D,r.SHOW_ELEMENT|r.SHOW_COMMENT|r.SHOW_TEXT|r.SHOW_PROCESSING_INSTRUCTION|r.SHOW_CDATA_SECTION,null)},Ba=function(D){var pe,ke;D.normalize();const Be=M.call(D.ownerDocument||D,D,r.SHOW_TEXT|r.SHOW_COMMENT|r.SHOW_CDATA_SECTION|r.SHOW_PROCESSING_INSTRUCTION,null);let Ue=Be.nextNode();for(;Ue;){let $t=Ue.data;qa([Q,ae,V],gt=>{$t=Si($t,gt," ")}),Ue.data=$t,Ue=Be.nextNode()}const Ke=(pe=(ke=D.querySelectorAll)===null||ke===void 0?void 0:ke.call(D,"template"))!==null&&pe!==void 0?pe:[];qa(Array.from(Ke),$t=>{kt($t.content)&&Ba($t.content)})},fn=function(D){const pe=w?w(D):null;return typeof pe!="string"||Ct(pe)!=="form"?!1:typeof D.nodeName!="string"||typeof D.textContent!="string"||typeof D.removeChild!="function"||D.attributes!==b(D)||typeof D.removeAttribute!="function"||typeof D.setAttribute!="function"||typeof D.namespaceURI!="string"||typeof D.insertBefore!="function"||typeof D.hasChildNodes!="function"||D.nodeType!==x(D)||D.childNodes!==k(D)},kt=function(D){if(!x||typeof D!="object"||D===null)return!1;try{return x(D)===Ca.documentFragment}catch{return!1}},$s=function(D){if(!x||typeof D!="object"||D===null)return!1;try{return typeof x(D)=="number"}catch{return!1}};function Wt(Ie,D,pe){qa(Ie,ke=>{ke.call(t,D,pe,ls)})}const ya=function(D){let pe=null;if(Wt(Z.beforeSanitizeElements,D,null),fn(D))return Ge(D),!0;const ke=Ct(w?w(D):D.nodeName);if(Wt(Z.uponSanitizeElement,D,{tagName:ke,allowedTags:Y}),U&&D.hasChildNodes()&&!$s(D.firstElementChild)&&us(/<[/\w!]/g,D.innerHTML)&&us(/<[/\w!]/g,D.textContent)||U&&D.namespaceURI===Pt&&ke==="style"&&$s(D.firstElementChild)||D.nodeType===Ca.progressingInstruction||U&&D.nodeType===Ca.comment&&us(/<[/\w]/g,D.data))return Ge(D),!0;if(he[ke]||!(me.tagCheck instanceof Function&&me.tagCheck(ke))&&!Y[ke]){if(!he[ke]&&mn(ke)&&(G.tagNameCheck instanceof RegExp&&us(G.tagNameCheck,ke)||G.tagNameCheck instanceof Function&&G.tagNameCheck(ke)))return!1;if(se&&!Te[ke]){const Ue=A(D),Ke=k(D);if(Ke&&Ue){const $t=Ke.length;for(let gt=$t-1;gt>=0;--gt){const Ot=Re?Ke[gt]:m(Ke[gt],!0);Ue.insertBefore(Ot,y(D))}}}return Ge(D),!0}return(x?x(D):D.nodeType)===Ca.element&&!qe(D)||(ke==="noscript"||ke==="noembed"||ke==="noframes")&&us(/<\/no(script|embed|frames)/i,D.innerHTML)?(Ge(D),!0):(_e&&D.nodeType===Ca.text&&(pe=D.textContent,qa([Q,ae,V],Ue=>{pe=Si(pe,Ue," ")}),D.textContent!==pe&&(ki(t.removed,{element:D.cloneNode()}),D.textContent=pe)),Wt(Z.afterSanitizeElements,D,null),!1)},xa=function(D,pe,ke){if(fe[pe]||Xe&&(pe==="id"||pe==="name")&&(ke in s||ke in sa))return!1;const Be=S[pe]||me.attributeCheck instanceof Function&&me.attributeCheck(pe,D);if(!(J&&!fe[pe]&&us(be,pe))){if(!(we&&us(Oe,pe))){if(!Be||fe[pe]){if(!(mn(D)&&(G.tagNameCheck instanceof RegExp&&us(G.tagNameCheck,D)||G.tagNameCheck instanceof Function&&G.tagNameCheck(D))&&(G.attributeNameCheck instanceof RegExp&&us(G.attributeNameCheck,pe)||G.attributeNameCheck instanceof Function&&G.attributeNameCheck(pe,D))||pe==="is"&&G.allowCustomizedBuiltInElements&&(G.tagNameCheck instanceof RegExp&&us(G.tagNameCheck,ke)||G.tagNameCheck instanceof Function&&G.tagNameCheck(ke))))return!1}else if(!Ye[pe]){if(!us(j,Si(ke,oe,""))){if(!((pe==="src"||pe==="xlink:href"||pe==="href")&&D!=="script"&&dm(ke,"data:")===0&&De[D])){if(!(ie&&!us(le,Si(ke,oe,"")))){if(ke)return!1}}}}}}return!0},Pn=nt({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),mn=function(D){return!Pn[Il(D)]&&us(ve,D)},$n=function(D){Wt(Z.beforeSanitizeAttributes,D,null);const pe=D.attributes;if(!pe||fn(D))return;const ke={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:S,forceKeepAttr:void 0};let Be=pe.length;for(;Be--;){const Ue=pe[Be],Ke=Ue.name,$t=Ue.namespaceURI,gt=Ue.value,Ot=Ct(Ke),ds=gt;let ut=Ke==="value"?ds:OT(ds);if(ke.attrName=Ot,ke.attrValue=ut,ke.keepAttr=!0,ke.forceKeepAttr=void 0,Wt(Z.uponSanitizeAttribute,D,ke),ut=ke.attrValue,it&&(Ot==="id"||Ot==="name")&&dm(ut,Qe)!==0&&(ot(Ke,D),ut=Qe+ut),U&&us(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,ut)){ot(Ke,D);continue}if(Ot==="attributename"&&cm(ut,"href")){ot(Ke,D);continue}if(ke.forceKeepAttr)continue;if(!ke.keepAttr){ot(Ke,D);continue}if(!X&&us(/\/>/i,ut)){ot(Ke,D);continue}_e&&qa([Q,ae,V],hn=>{ut=Si(ut,hn," ")});const ua=Ct(D.nodeName);if(!xa(ua,Ot,ut)){ot(Ke,D);continue}if(_&&typeof u=="object"&&typeof u.getAttributeType=="function"&&!$t)switch(u.getAttributeType(ua,Ot)){case"TrustedHTML":{ut=I(ut);break}case"TrustedScriptURL":{ut=H(ut);break}}if(ut!==ds)try{$t?D.setAttributeNS($t,Ke,ut):D.setAttribute(Ke,ut),fn(D)?Ge(D):rm(t.removed)}catch{ot(Ke,D)}}Wt(Z.afterSanitizeAttributes,D,null)},Ua=function(D){let pe=null;const ke=fi(D);for(Wt(Z.beforeSanitizeShadowDOM,D,null);pe=ke.nextNode();)if(Wt(Z.uponSanitizeShadowNode,pe,null),ya(pe),$n(pe),kt(pe.content)&&Ua(pe.content),(x?x(pe):pe.nodeType)===Ca.element){const Ue=g?g(pe):pe.shadowRoot;kt(Ue)&&(vn(Ue),Ua(Ue))}Wt(Z.afterSanitizeShadowDOM,D,null)},vn=function(D){const pe=[{node:D,shadow:null}];for(;pe.length>0;){const ke=pe.pop();if(ke.shadow){Ua(ke.shadow);continue}const Be=ke.node,Ke=(x?x(Be):Be.nodeType)===Ca.element,$t=k?k(Be):Be.childNodes;if($t)for(let gt=$t.length-1;gt>=0;--gt)pe.push({node:$t[gt],shadow:null});if(Ke){const gt=w?w(Be):null;if(typeof gt=="string"&&Ct(gt)==="template"){const Ot=Be.content;kt(Ot)&&pe.push({node:Ot,shadow:null})}}if(Ke){const gt=g?g(Be):Be.shadowRoot;kt(gt)&&pe.push({node:null,shadow:gt},{node:gt,shadow:null})}}};return t.sanitize=function(Ie){let D=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},pe=null,ke=null,Be=null,Ue=null;if(Vs=!Ie,Vs&&(Ie=""),typeof Ie!="string"&&!$s(Ie)&&(Ie=PT(Ie),typeof Ie!="string"))throw qn("dirty is not a string, aborting");if(!t.isSupported)return Ie;ge||Fe(D),t.removed=[];const Ke=Re&&typeof Ie!="string"&&$s(Ie);if(Ke){const Ot=w?w(Ie):Ie.nodeName;if(typeof Ot=="string"){const ds=Ct(Ot);if(!Y[ds]||he[ds])throw qn("root node is forbidden and cannot be sanitized in-place")}if(fn(Ie))throw qn("root node is clobbered and cannot be sanitized in-place");try{vn(Ie)}catch(ds){throw It(Ie),ds}}else if($s(Ie))pe=Fa(""),ke=pe.ownerDocument.importNode(Ie,!0),ke.nodeType===Ca.element&&ke.nodeName==="BODY"||ke.nodeName==="HTML"?pe=ke:pe.appendChild(ke),vn(ke);else{if(!$e&&!_e&&!de&&Ie.indexOf("<")===-1)return _&&je?I(Ie):Ie;if(pe=Fa(Ie),!pe)return $e?null:je?T:""}pe&&Ae&&Ge(pe.firstChild);const $t=fi(Ke?Ie:pe);try{for(;Be=$t.nextNode();)ya(Be),$n(Be),kt(Be.content)&&Ua(Be.content)}catch(Ot){throw Ke&&It(Ie),Ot}if(Ke)return qa(t.removed,Ot=>{Ot.element&&pi(Ot.element)}),_e&&Ba(Ie),Ie;if($e){if(_e&&Ba(pe),We)for(Ue=$.call(pe.ownerDocument);pe.firstChild;)Ue.appendChild(pe.firstChild);else Ue=pe;return(S.shadowroot||S.shadowrootmode)&&(Ue=ce.call(a,Ue,!0)),Ue}let gt=de?pe.outerHTML:pe.innerHTML;return de&&Y["!doctype"]&&pe.ownerDocument&&pe.ownerDocument.doctype&&pe.ownerDocument.doctype.name&&us(WT,pe.ownerDocument.doctype.name)&&(gt=" +`+gt),_e&&qa([Q,ae,V],Ot=>{gt=Si(gt,Ot," ")}),_&&je?I(gt):gt},t.setConfig=function(){let Ie=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};Fe(Ie),ge=!0},t.clearConfig=function(){ls=null,ge=!1,_=L,T=""},t.isValidAttribute=function(Ie,D,pe){ls||Fe({});const ke=Ct(Ie),Be=Ct(D);return xa(ke,Be,pe)},t.addHook=function(Ie,D){typeof D=="function"&&ki(Z[Ie],D)},t.removeHook=function(Ie,D){if(D!==void 0){const pe=RT(Z[Ie],D);return pe===-1?void 0:IT(Z[Ie],pe,1)[0]}return rm(Z[Ie])},t.removeHooks=function(Ie){Z[Ie]=[]},t.removeAllHooks=function(){Z=bm()},t}var ym=Qg();function Ku(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var ui=Ku();function Yg(e){ui=e}var Bl={exec:()=>null};function _t(e,t=""){let s=typeof e=="string"?e:e.source;const a={replace:(n,i)=>{let l=typeof i=="string"?i:i.source;return l=l.replace(Ms.caret,"$1"),s=s.replace(n,l),a},getRegex:()=>new RegExp(s,t)};return a}var Ms={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},QT=/^(?:[ \t]*(?:\n|$))+/,YT=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,XT=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,xo=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,eE=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,Ju=/(?:[*+-]|\d{1,9}[.)])/,Xg=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,eb=_t(Xg).replace(/bull/g,Ju).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),tE=_t(Xg).replace(/bull/g,Ju).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Zu=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,sE=/^[^\n]+/,Qu=/(?!\s*\])(?:\\.|[^\[\]\\])+/,aE=_t(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",Qu).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),nE=_t(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,Ju).getRegex(),fc="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",Yu=/|$))/,iE=_t("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",Yu).replace("tag",fc).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),tb=_t(Zu).replace("hr",xo).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",fc).getRegex(),lE=_t(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",tb).getRegex(),Xu={blockquote:lE,code:YT,def:aE,fences:XT,heading:eE,hr:xo,html:iE,lheading:eb,list:nE,newline:QT,paragraph:tb,table:Bl,text:sE},xm=_t("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",xo).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",fc).getRegex(),oE={...Xu,lheading:tE,table:xm,paragraph:_t(Zu).replace("hr",xo).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",xm).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",fc).getRegex()},rE={...Xu,html:_t(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",Yu).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Bl,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:_t(Zu).replace("hr",xo).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",eb).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},cE=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,dE=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,sb=/^( {2,}|\\)\n(?!\s*$)/,uE=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,ib=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,hE=_t(ib,"u").replace(/punct/g,mc).getRegex(),gE=_t(ib,"u").replace(/punct/g,nb).getRegex(),lb="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",bE=_t(lb,"gu").replace(/notPunctSpace/g,ab).replace(/punctSpace/g,ep).replace(/punct/g,mc).getRegex(),yE=_t(lb,"gu").replace(/notPunctSpace/g,mE).replace(/punctSpace/g,fE).replace(/punct/g,nb).getRegex(),xE=_t("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ab).replace(/punctSpace/g,ep).replace(/punct/g,mc).getRegex(),_E=_t(/\\(punct)/,"gu").replace(/punct/g,mc).getRegex(),wE=_t(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),kE=_t(Yu).replace("(?:-->|$)","-->").getRegex(),SE=_t("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",kE).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),Pr=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,CE=_t(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",Pr).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ob=_t(/^!?\[(label)\]\[(ref)\]/).replace("label",Pr).replace("ref",Qu).getRegex(),rb=_t(/^!?\[(ref)\](?:\[\])?/).replace("ref",Qu).getRegex(),TE=_t("reflink|nolink(?!\\()","g").replace("reflink",ob).replace("nolink",rb).getRegex(),tp={_backpedal:Bl,anyPunctuation:_E,autolink:wE,blockSkip:vE,br:sb,code:dE,del:Bl,emStrongLDelim:hE,emStrongRDelimAst:bE,emStrongRDelimUnd:xE,escape:cE,link:CE,nolink:rb,punctuation:pE,reflink:ob,reflinkSearch:TE,tag:SE,text:uE,url:Bl},EE={...tp,link:_t(/^!?\[(label)\]\((.*?)\)/).replace("label",Pr).getRegex(),reflink:_t(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Pr).getRegex()},jd={...tp,emStrongRDelimAst:yE,emStrongLDelim:gE,url:_t(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},_m=e=>RE[e];function Aa(e,t){if(t){if(Ms.escapeTest.test(e))return e.replace(Ms.escapeReplace,_m)}else if(Ms.escapeTestNoEncode.test(e))return e.replace(Ms.escapeReplaceNoEncode,_m);return e}function wm(e){try{e=encodeURI(e).replace(Ms.percentDecode,"%")}catch{return null}return e}function km(e,t){var i;const s=e.replace(Ms.findPipe,(l,o,r)=>{let c=!1,d=o;for(;--d>=0&&r[d]==="\\";)c=!c;return c?"|":" |"}),a=s.split(Ms.splitPipe);let n=0;if(a[0].trim()||a.shift(),a.length>0&&!((i=a.at(-1))!=null&&i.trim())&&a.pop(),t)if(a.length>t)a.splice(t);else for(;a.length0?-2:-1}function Sm(e,t,s,a,n){const i=t.href,l=t.title||null,o=e[1].replace(n.other.outputLinkReplace,"$1");a.state.inLink=!0;const r={type:e[0].charAt(0)==="!"?"image":"link",raw:s,href:i,title:l,text:o,tokens:a.inlineTokens(o)};return a.state.inLink=!1,r}function OE(e,t,s){const a=e.match(s.other.indentCodeCompensation);if(a===null)return t;const n=a[1];return t.split(` `).map(i=>{const l=i.match(s.other.beginningSpace);if(l===null)return i;const[o]=l;return o.length>=n.length?i.slice(n.length):i}).join(` -`)}var Ir=class{constructor(e){Tt(this,"options");Tt(this,"rules");Tt(this,"lexer");this.options=e||li}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const s=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?s:yl(s,` -`)}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const s=t[0],a=dE(s,t[3]||"",this.rules);return{type:"code",raw:s,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:a}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let s=t[2].trim();if(this.rules.other.endingHash.test(s)){const a=yl(s,"#");(this.options.pedantic||!a||this.rules.other.endingSpaceChar.test(a))&&(s=a.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:s,tokens:this.lexer.inline(s)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:yl(t[0],` -`)}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let s=yl(t[0],` +`)}var $r=class{constructor(e){Et(this,"options");Et(this,"rules");Et(this,"lexer");this.options=e||ui}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const s=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?s:Cl(s,` +`)}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const s=t[0],a=OE(s,t[3]||"",this.rules);return{type:"code",raw:s,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:a}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let s=t[2].trim();if(this.rules.other.endingHash.test(s)){const a=Cl(s,"#");(this.options.pedantic||!a||this.rules.other.endingSpaceChar.test(a))&&(s=a.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:s,tokens:this.lexer.inline(s)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:Cl(t[0],` +`)}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let s=Cl(t[0],` `).split(` `),a="",n="";const i=[];for(;s.length>0;){let l=!1;const o=[];let r;for(r=0;r1,n={type:"list",raw:"",ordered:a,start:a?+s.slice(0,-1):"",loose:!1,items:[]};s=a?`\\d{1,9}\\${s.slice(-1)}`:`\\${s}`,this.options.pedantic&&(s=a?s:"[*+-]");const i=this.rules.other.listItemRegex(s);let l=!1;for(;e;){let r=!1,c="",d="";if(!(t=i.exec(e))||this.rules.block.hr.test(e))break;c=t[0],e=e.substring(c.length);let u=t[2].split(` -`,1)[0].replace(this.rules.other.listReplaceTabs,w=>" ".repeat(3*w.length)),f=e.split(` -`,1)[0],m=!u.trim(),h=0;if(this.options.pedantic?(h=2,d=u.trimStart()):m?h=t[1].length+1:(h=t[2].search(this.rules.other.nonSpaceChar),h=h>4?1:h,d=u.slice(h),h+=t[1].length),m&&this.rules.other.blankLine.test(f)&&(c+=f+` -`,e=e.substring(f.length+1),r=!0),!r){const w=this.rules.other.nextBulletRegex(h),y=this.rules.other.hrRegex(h),b=this.rules.other.fencesBeginRegex(h),x=this.rules.other.headingBeginRegex(h),S=this.rules.other.htmlBeginRegex(h);for(;e;){const _=e.split(` -`,1)[0];let T;if(f=_,this.options.pedantic?(f=f.replace(this.rules.other.listReplaceNesting," "),T=f):T=f.replace(this.rules.other.tabCharGlobal," "),b.test(f)||x.test(f)||S.test(f)||w.test(f)||y.test(f))break;if(T.search(this.rules.other.nonSpaceChar)>=h||!f.trim())d+=` -`+T.slice(h);else{if(m||u.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||b.test(u)||x.test(u)||y.test(u))break;d+=` -`+f}!m&&!f.trim()&&(m=!0),c+=_+` -`,e=e.substring(_.length+1),u=T.slice(h)}}n.loose||(l?n.loose=!0:this.rules.other.doubleBlankLine.test(c)&&(l=!0));let g=null,A;this.options.gfm&&(g=this.rules.other.listIsTask.exec(d),g&&(A=g[0]!=="[ ] ",d=d.replace(this.rules.other.listReplaceTask,""))),n.items.push({type:"list_item",raw:c,task:!!g,checked:A,loose:!1,text:d,tokens:[]}),n.raw+=c}const o=n.items.at(-1);if(o)o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd();else return;n.raw=n.raw.trimEnd();for(let r=0;ru.type==="space"),d=c.length>0&&c.some(u=>this.rules.other.anyLine.test(u.raw));n.loose=d}if(n.loose)for(let r=0;r({text:r,tokens:this.lexer.inline(r),header:!1,align:i.align[c]})));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const s=t[1].charAt(t[1].length-1)===` -`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:s,tokens:this.lexer.inline(s)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const s=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(s)){if(!this.rules.other.endAngleBracket.test(s))return;const i=yl(s.slice(0,-1),"\\");if((s.length-i.length)%2===0)return}else{const i=cE(t[2],"()");if(i===-2)return;if(i>-1){const o=(t[0].indexOf("!")===0?5:4)+t[1].length+i;t[2]=t[2].substring(0,i),t[0]=t[0].substring(0,o).trim(),t[3]=""}}let a=t[2],n="";if(this.options.pedantic){const i=this.rules.other.pedanticHrefTitle.exec(a);i&&(a=i[1],n=i[3])}else n=t[3]?t[3].slice(1,-1):"";return a=a.trim(),this.rules.other.startAngleBracket.test(a)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(s)?a=a.slice(1):a=a.slice(1,-1)),mm(t,{href:a&&a.replace(this.rules.inline.anyPunctuation,"$1"),title:n&&n.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let s;if((s=this.rules.inline.reflink.exec(e))||(s=this.rules.inline.nolink.exec(e))){const a=(s[2]||s[1]).replace(this.rules.other.multipleSpaceGlobal," "),n=t[a.toLowerCase()];if(!n){const i=s[0].charAt(0);return{type:"text",raw:i,text:i}}return mm(s,n,s[0],this.lexer,this.rules)}}emStrong(e,t,s=""){let a=this.rules.inline.emStrongLDelim.exec(e);if(!a||a[3]&&s.match(this.rules.other.unicodeAlphaNumeric))return;if(!(a[1]||a[2]||"")||!s||this.rules.inline.punctuation.exec(s)){const i=[...a[0]].length-1;let l,o,r=i,c=0;const d=a[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(d.lastIndex=0,t=t.slice(-1*e.length+i);(a=d.exec(t))!=null;){if(l=a[1]||a[2]||a[3]||a[4]||a[5]||a[6],!l)continue;if(o=[...l].length,a[3]||a[4]){r+=o;continue}else if((a[5]||a[6])&&i%3&&!((i+o)%3)){c+=o;continue}if(r-=o,r>0)continue;o=Math.min(o,o+r+c);const u=[...a[0]][0].length,f=e.slice(0,i+a.index+u+o);if(Math.min(i,o)%2){const h=f.slice(1,-1);return{type:"em",raw:f,text:h,tokens:this.lexer.inlineTokens(h)}}const m=f.slice(2,-2);return{type:"strong",raw:f,text:m,tokens:this.lexer.inlineTokens(m)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let s=t[2].replace(this.rules.other.newLineCharGlobal," ");const a=this.rules.other.nonSpaceChar.test(s),n=this.rules.other.startingSpaceChar.test(s)&&this.rules.other.endingSpaceChar.test(s);return a&&n&&(s=s.substring(1,s.length-1)),{type:"codespan",raw:t[0],text:s}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let s,a;return t[2]==="@"?(s=t[1],a="mailto:"+s):(s=t[1],a=s),{type:"link",raw:t[0],text:s,href:a,tokens:[{type:"text",raw:s,text:s}]}}}url(e){var s;let t;if(t=this.rules.inline.url.exec(e)){let a,n;if(t[2]==="@")a=t[0],n="mailto:"+a;else{let i;do i=t[0],t[0]=((s=this.rules.inline._backpedal.exec(t[0]))==null?void 0:s[0])??"";while(i!==t[0]);a=t[0],t[1]==="www."?n="http://"+t[0]:n=t[0]}return{type:"link",raw:t[0],text:a,href:n,tokens:[{type:"text",raw:a,text:a}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){const s=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:s}}}},Ka=class Pd{constructor(t){Tt(this,"tokens");Tt(this,"options");Tt(this,"state");Tt(this,"tokenizer");Tt(this,"inlineQueue");this.tokens=[],this.tokens.links=Object.create(null),this.options=t||li,this.options.tokenizer=this.options.tokenizer||new Ir,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const s={other:Os,block:Go.normal,inline:bl.normal};this.options.pedantic?(s.block=Go.pedantic,s.inline=bl.pedantic):this.options.gfm&&(s.block=Go.gfm,this.options.breaks?s.inline=bl.breaks:s.inline=bl.gfm),this.tokenizer.rules=s}static get rules(){return{block:Go,inline:bl}}static lex(t,s){return new Pd(s).lex(t)}static lexInline(t,s){return new Pd(s).inlineTokens(t)}lex(t){t=t.replace(Os.carriageReturn,` -`),this.blockTokens(t,this.tokens);for(let s=0;s(o=c.call({lexer:this},t,s))?(t=t.substring(o.raw.length),s.push(o),!0):!1))continue;if(o=this.tokenizer.space(t)){t=t.substring(o.raw.length);const c=s.at(-1);o.raw.length===1&&c!==void 0?c.raw+=` +`,1)[0].replace(this.rules.other.listReplaceTabs,A=>" ".repeat(3*A.length)),p=e.split(` +`,1)[0],m=!u.trim(),h=0;if(this.options.pedantic?(h=2,d=u.trimStart()):m?h=t[1].length+1:(h=t[2].search(this.rules.other.nonSpaceChar),h=h>4?1:h,d=u.slice(h),h+=t[1].length),m&&this.rules.other.blankLine.test(p)&&(c+=p+` +`,e=e.substring(p.length+1),r=!0),!r){const A=this.rules.other.nextBulletRegex(h),g=this.rules.other.hrRegex(h),b=this.rules.other.fencesBeginRegex(h),x=this.rules.other.headingBeginRegex(h),w=this.rules.other.htmlBeginRegex(h);for(;e;){const _=e.split(` +`,1)[0];let T;if(p=_,this.options.pedantic?(p=p.replace(this.rules.other.listReplaceNesting," "),T=p):T=p.replace(this.rules.other.tabCharGlobal," "),b.test(p)||x.test(p)||w.test(p)||A.test(p)||g.test(p))break;if(T.search(this.rules.other.nonSpaceChar)>=h||!p.trim())d+=` +`+T.slice(h);else{if(m||u.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||b.test(u)||x.test(u)||g.test(u))break;d+=` +`+p}!m&&!p.trim()&&(m=!0),c+=_+` +`,e=e.substring(_.length+1),u=T.slice(h)}}n.loose||(l?n.loose=!0:this.rules.other.doubleBlankLine.test(c)&&(l=!0));let y=null,k;this.options.gfm&&(y=this.rules.other.listIsTask.exec(d),y&&(k=y[0]!=="[ ] ",d=d.replace(this.rules.other.listReplaceTask,""))),n.items.push({type:"list_item",raw:c,task:!!y,checked:k,loose:!1,text:d,tokens:[]}),n.raw+=c}const o=n.items.at(-1);if(o)o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd();else return;n.raw=n.raw.trimEnd();for(let r=0;ru.type==="space"),d=c.length>0&&c.some(u=>this.rules.other.anyLine.test(u.raw));n.loose=d}if(n.loose)for(let r=0;r({text:r,tokens:this.lexer.inline(r),header:!1,align:i.align[c]})));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const s=t[1].charAt(t[1].length-1)===` +`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:s,tokens:this.lexer.inline(s)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const s=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(s)){if(!this.rules.other.endAngleBracket.test(s))return;const i=Cl(s.slice(0,-1),"\\");if((s.length-i.length)%2===0)return}else{const i=IE(t[2],"()");if(i===-2)return;if(i>-1){const o=(t[0].indexOf("!")===0?5:4)+t[1].length+i;t[2]=t[2].substring(0,i),t[0]=t[0].substring(0,o).trim(),t[3]=""}}let a=t[2],n="";if(this.options.pedantic){const i=this.rules.other.pedanticHrefTitle.exec(a);i&&(a=i[1],n=i[3])}else n=t[3]?t[3].slice(1,-1):"";return a=a.trim(),this.rules.other.startAngleBracket.test(a)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(s)?a=a.slice(1):a=a.slice(1,-1)),Sm(t,{href:a&&a.replace(this.rules.inline.anyPunctuation,"$1"),title:n&&n.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let s;if((s=this.rules.inline.reflink.exec(e))||(s=this.rules.inline.nolink.exec(e))){const a=(s[2]||s[1]).replace(this.rules.other.multipleSpaceGlobal," "),n=t[a.toLowerCase()];if(!n){const i=s[0].charAt(0);return{type:"text",raw:i,text:i}}return Sm(s,n,s[0],this.lexer,this.rules)}}emStrong(e,t,s=""){let a=this.rules.inline.emStrongLDelim.exec(e);if(!a||a[3]&&s.match(this.rules.other.unicodeAlphaNumeric))return;if(!(a[1]||a[2]||"")||!s||this.rules.inline.punctuation.exec(s)){const i=[...a[0]].length-1;let l,o,r=i,c=0;const d=a[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(d.lastIndex=0,t=t.slice(-1*e.length+i);(a=d.exec(t))!=null;){if(l=a[1]||a[2]||a[3]||a[4]||a[5]||a[6],!l)continue;if(o=[...l].length,a[3]||a[4]){r+=o;continue}else if((a[5]||a[6])&&i%3&&!((i+o)%3)){c+=o;continue}if(r-=o,r>0)continue;o=Math.min(o,o+r+c);const u=[...a[0]][0].length,p=e.slice(0,i+a.index+u+o);if(Math.min(i,o)%2){const h=p.slice(1,-1);return{type:"em",raw:p,text:h,tokens:this.lexer.inlineTokens(h)}}const m=p.slice(2,-2);return{type:"strong",raw:p,text:m,tokens:this.lexer.inlineTokens(m)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let s=t[2].replace(this.rules.other.newLineCharGlobal," ");const a=this.rules.other.nonSpaceChar.test(s),n=this.rules.other.startingSpaceChar.test(s)&&this.rules.other.endingSpaceChar.test(s);return a&&n&&(s=s.substring(1,s.length-1)),{type:"codespan",raw:t[0],text:s}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let s,a;return t[2]==="@"?(s=t[1],a="mailto:"+s):(s=t[1],a=s),{type:"link",raw:t[0],text:s,href:a,tokens:[{type:"text",raw:s,text:s}]}}}url(e){var s;let t;if(t=this.rules.inline.url.exec(e)){let a,n;if(t[2]==="@")a=t[0],n="mailto:"+a;else{let i;do i=t[0],t[0]=((s=this.rules.inline._backpedal.exec(t[0]))==null?void 0:s[0])??"";while(i!==t[0]);a=t[0],t[1]==="www."?n="http://"+t[0]:n=t[0]}return{type:"link",raw:t[0],text:a,href:n,tokens:[{type:"text",raw:a,text:a}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){const s=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:s}}}},Ya=class qd{constructor(t){Et(this,"tokens");Et(this,"options");Et(this,"state");Et(this,"tokenizer");Et(this,"inlineQueue");this.tokens=[],this.tokens.links=Object.create(null),this.options=t||ui,this.options.tokenizer=this.options.tokenizer||new $r,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const s={other:Ms,block:Xo.normal,inline:Sl.normal};this.options.pedantic?(s.block=Xo.pedantic,s.inline=Sl.pedantic):this.options.gfm&&(s.block=Xo.gfm,this.options.breaks?s.inline=Sl.breaks:s.inline=Sl.gfm),this.tokenizer.rules=s}static get rules(){return{block:Xo,inline:Sl}}static lex(t,s){return new qd(s).lex(t)}static lexInline(t,s){return new qd(s).inlineTokens(t)}lex(t){t=t.replace(Ms.carriageReturn,` +`),this.blockTokens(t,this.tokens);for(let s=0;s(o=c.call({lexer:this},t,s))?(t=t.substring(o.raw.length),s.push(o),!0):!1))continue;if(o=this.tokenizer.space(t)){t=t.substring(o.raw.length);const c=s.at(-1);o.raw.length===1&&c!==void 0?c.raw+=` `:s.push(o);continue}if(o=this.tokenizer.code(t)){t=t.substring(o.raw.length);const c=s.at(-1);(c==null?void 0:c.type)==="paragraph"||(c==null?void 0:c.type)==="text"?(c.raw+=` `+o.raw,c.text+=` `+o.text,this.inlineQueue.at(-1).src=c.text):s.push(o);continue}if(o=this.tokenizer.fences(t)){t=t.substring(o.raw.length),s.push(o);continue}if(o=this.tokenizer.heading(t)){t=t.substring(o.raw.length),s.push(o);continue}if(o=this.tokenizer.hr(t)){t=t.substring(o.raw.length),s.push(o);continue}if(o=this.tokenizer.blockquote(t)){t=t.substring(o.raw.length),s.push(o);continue}if(o=this.tokenizer.list(t)){t=t.substring(o.raw.length),s.push(o);continue}if(o=this.tokenizer.html(t)){t=t.substring(o.raw.length),s.push(o);continue}if(o=this.tokenizer.def(t)){t=t.substring(o.raw.length);const c=s.at(-1);(c==null?void 0:c.type)==="paragraph"||(c==null?void 0:c.type)==="text"?(c.raw+=` `+o.raw,c.text+=` -`+o.raw,this.inlineQueue.at(-1).src=c.text):this.tokens.links[o.tag]||(this.tokens.links[o.tag]={href:o.href,title:o.title});continue}if(o=this.tokenizer.table(t)){t=t.substring(o.raw.length),s.push(o);continue}if(o=this.tokenizer.lheading(t)){t=t.substring(o.raw.length),s.push(o);continue}let r=t;if((l=this.options.extensions)!=null&&l.startBlock){let c=1/0;const d=t.slice(1);let u;this.options.extensions.startBlock.forEach(f=>{u=f.call({lexer:this},d),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(r=t.substring(0,c+1))}if(this.state.top&&(o=this.tokenizer.paragraph(r))){const c=s.at(-1);a&&(c==null?void 0:c.type)==="paragraph"?(c.raw+=` +`+o.raw,this.inlineQueue.at(-1).src=c.text):this.tokens.links[o.tag]||(this.tokens.links[o.tag]={href:o.href,title:o.title});continue}if(o=this.tokenizer.table(t)){t=t.substring(o.raw.length),s.push(o);continue}if(o=this.tokenizer.lheading(t)){t=t.substring(o.raw.length),s.push(o);continue}let r=t;if((l=this.options.extensions)!=null&&l.startBlock){let c=1/0;const d=t.slice(1);let u;this.options.extensions.startBlock.forEach(p=>{u=p.call({lexer:this},d),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(r=t.substring(0,c+1))}if(this.state.top&&(o=this.tokenizer.paragraph(r))){const c=s.at(-1);a&&(c==null?void 0:c.type)==="paragraph"?(c.raw+=` `+o.raw,c.text+=` `+o.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=c.text):s.push(o),a=r.length!==t.length,t=t.substring(o.raw.length);continue}if(o=this.tokenizer.text(t)){t=t.substring(o.raw.length);const c=s.at(-1);(c==null?void 0:c.type)==="text"?(c.raw+=` `+o.raw,c.text+=` -`+o.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=c.text):s.push(o);continue}if(t){const c="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return this.state.top=!0,s}inline(t,s=[]){return this.inlineQueue.push({src:t,tokens:s}),s}inlineTokens(t,s=[]){var o,r,c;let a=t,n=null;if(this.tokens.links){const d=Object.keys(this.tokens.links);if(d.length>0)for(;(n=this.tokenizer.rules.inline.reflinkSearch.exec(a))!=null;)d.includes(n[0].slice(n[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,n.index)+"["+"a".repeat(n[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(n=this.tokenizer.rules.inline.anyPunctuation.exec(a))!=null;)a=a.slice(0,n.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(n=this.tokenizer.rules.inline.blockSkip.exec(a))!=null;)a=a.slice(0,n.index)+"["+"a".repeat(n[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,l="";for(;t;){i||(l=""),i=!1;let d;if((r=(o=this.options.extensions)==null?void 0:o.inline)!=null&&r.some(f=>(d=f.call({lexer:this},t,s))?(t=t.substring(d.raw.length),s.push(d),!0):!1))continue;if(d=this.tokenizer.escape(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.tag(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.link(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.reflink(t,this.tokens.links)){t=t.substring(d.raw.length);const f=s.at(-1);d.type==="text"&&(f==null?void 0:f.type)==="text"?(f.raw+=d.raw,f.text+=d.text):s.push(d);continue}if(d=this.tokenizer.emStrong(t,a,l)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.codespan(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.br(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.del(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.autolink(t)){t=t.substring(d.raw.length),s.push(d);continue}if(!this.state.inLink&&(d=this.tokenizer.url(t))){t=t.substring(d.raw.length),s.push(d);continue}let u=t;if((c=this.options.extensions)!=null&&c.startInline){let f=1/0;const m=t.slice(1);let h;this.options.extensions.startInline.forEach(g=>{h=g.call({lexer:this},m),typeof h=="number"&&h>=0&&(f=Math.min(f,h))}),f<1/0&&f>=0&&(u=t.substring(0,f+1))}if(d=this.tokenizer.inlineText(u)){t=t.substring(d.raw.length),d.raw.slice(-1)!=="_"&&(l=d.raw.slice(-1)),i=!0;const f=s.at(-1);(f==null?void 0:f.type)==="text"?(f.raw+=d.raw,f.text+=d.text):s.push(d);continue}if(t){const f="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(f);break}else throw new Error(f)}}return s}},Or=class{constructor(e){Tt(this,"options");Tt(this,"parser");this.options=e||li}space(e){return""}code({text:e,lang:t,escaped:s}){var i;const a=(i=(t||"").match(Os.notSpaceStart))==null?void 0:i[0],n=e.replace(Os.endingNewline,"")+` -`;return a?'
'+(s?n:Ca(n,!0))+`
-`:"
"+(s?n:Ca(n,!0))+`
+`+o.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=c.text):s.push(o);continue}if(t){const c="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return this.state.top=!0,s}inline(t,s=[]){return this.inlineQueue.push({src:t,tokens:s}),s}inlineTokens(t,s=[]){var o,r,c;let a=t,n=null;if(this.tokens.links){const d=Object.keys(this.tokens.links);if(d.length>0)for(;(n=this.tokenizer.rules.inline.reflinkSearch.exec(a))!=null;)d.includes(n[0].slice(n[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,n.index)+"["+"a".repeat(n[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(n=this.tokenizer.rules.inline.anyPunctuation.exec(a))!=null;)a=a.slice(0,n.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(n=this.tokenizer.rules.inline.blockSkip.exec(a))!=null;)a=a.slice(0,n.index)+"["+"a".repeat(n[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,l="";for(;t;){i||(l=""),i=!1;let d;if((r=(o=this.options.extensions)==null?void 0:o.inline)!=null&&r.some(p=>(d=p.call({lexer:this},t,s))?(t=t.substring(d.raw.length),s.push(d),!0):!1))continue;if(d=this.tokenizer.escape(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.tag(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.link(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.reflink(t,this.tokens.links)){t=t.substring(d.raw.length);const p=s.at(-1);d.type==="text"&&(p==null?void 0:p.type)==="text"?(p.raw+=d.raw,p.text+=d.text):s.push(d);continue}if(d=this.tokenizer.emStrong(t,a,l)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.codespan(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.br(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.del(t)){t=t.substring(d.raw.length),s.push(d);continue}if(d=this.tokenizer.autolink(t)){t=t.substring(d.raw.length),s.push(d);continue}if(!this.state.inLink&&(d=this.tokenizer.url(t))){t=t.substring(d.raw.length),s.push(d);continue}let u=t;if((c=this.options.extensions)!=null&&c.startInline){let p=1/0;const m=t.slice(1);let h;this.options.extensions.startInline.forEach(y=>{h=y.call({lexer:this},m),typeof h=="number"&&h>=0&&(p=Math.min(p,h))}),p<1/0&&p>=0&&(u=t.substring(0,p+1))}if(d=this.tokenizer.inlineText(u)){t=t.substring(d.raw.length),d.raw.slice(-1)!=="_"&&(l=d.raw.slice(-1)),i=!0;const p=s.at(-1);(p==null?void 0:p.type)==="text"?(p.raw+=d.raw,p.text+=d.text):s.push(d);continue}if(t){const p="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(p);break}else throw new Error(p)}}return s}},Fr=class{constructor(e){Et(this,"options");Et(this,"parser");this.options=e||ui}space(e){return""}code({text:e,lang:t,escaped:s}){var i;const a=(i=(t||"").match(Ms.notSpaceStart))==null?void 0:i[0],n=e.replace(Ms.endingNewline,"")+` +`;return a?'
'+(s?n:Aa(n,!0))+`
+`:"
"+(s?n:Aa(n,!0))+`
`}blockquote({tokens:e}){return`
${this.parser.parse(e)}
`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} `}hr(e){return`
`}list(e){const t=e.ordered,s=e.start;let a="";for(let l=0;l `+a+" -`}listitem(e){var s;let t="";if(e.task){const a=this.checkbox({checked:!!e.checked});e.loose?((s=e.tokens[0])==null?void 0:s.type)==="paragraph"?(e.tokens[0].text=a+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=a+" "+Ca(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:a+" ",text:a+" ",escaped:!0}):t+=a+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • +`}listitem(e){var s;let t="";if(e.task){const a=this.checkbox({checked:!!e.checked});e.loose?((s=e.tokens[0])==null?void 0:s.type)==="paragraph"?(e.tokens[0].text=a+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=a+" "+Aa(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:a+" ",text:a+" ",escaped:!0}):t+=a+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • `}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    `}table(e){let t="",s="";for(let n=0;n${a}`),` @@ -7124,9 +7141,9 @@ ${this.parser.parse(e)} `}tablerow({text:e}){return` ${e} `}tablecell(e){const t=this.parser.parseInline(e.tokens),s=e.header?"th":"td";return(e.align?`<${s} align="${e.align}">`:`<${s}>`)+t+` -`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${Ca(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:s}){const a=this.parser.parseInline(s),n=pm(e);if(n===null)return a;e=n;let i='",i}image({href:e,title:t,text:s,tokens:a}){a&&(s=this.parser.parseInline(a,this.parser.textRenderer));const n=pm(e);if(n===null)return Ca(s);e=n;let i=`${s}{const r=l[o].flat(1/0);s=s.concat(this.walkTokens(r,t))}):l.tokens&&(s=s.concat(this.walkTokens(l.tokens,t)))}}return s}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(s=>{const a={...s};if(a.async=this.defaults.async||a.async||!1,s.extensions&&(s.extensions.forEach(n=>{if(!n.name)throw new Error("extension name required");if("renderer"in n){const i=t.renderers[n.name];i?t.renderers[n.name]=function(...l){let o=n.renderer.apply(this,l);return o===!1&&(o=i.apply(this,l)),o}:t.renderers[n.name]=n.renderer}if("tokenizer"in n){if(!n.level||n.level!=="block"&&n.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");const i=t[n.level];i?i.unshift(n.tokenizer):t[n.level]=[n.tokenizer],n.start&&(n.level==="block"?t.startBlock?t.startBlock.push(n.start):t.startBlock=[n.start]:n.level==="inline"&&(t.startInline?t.startInline.push(n.start):t.startInline=[n.start]))}"childTokens"in n&&n.childTokens&&(t.childTokens[n.name]=n.childTokens)}),a.extensions=t),s.renderer){const n=this.defaults.renderer||new Or(this.defaults);for(const i in s.renderer){if(!(i in n))throw new Error(`renderer '${i}' does not exist`);if(["options","parser"].includes(i))continue;const l=i,o=s.renderer[l],r=n[l];n[l]=(...c)=>{let d=o.apply(n,c);return d===!1&&(d=r.apply(n,c)),d||""}}a.renderer=n}if(s.tokenizer){const n=this.defaults.tokenizer||new Ir(this.defaults);for(const i in s.tokenizer){if(!(i in n))throw new Error(`tokenizer '${i}' does not exist`);if(["options","rules","lexer"].includes(i))continue;const l=i,o=s.tokenizer[l],r=n[l];n[l]=(...c)=>{let d=o.apply(n,c);return d===!1&&(d=r.apply(n,c)),d}}a.tokenizer=n}if(s.hooks){const n=this.defaults.hooks||new er;for(const i in s.hooks){if(!(i in n))throw new Error(`hook '${i}' does not exist`);if(["options","block"].includes(i))continue;const l=i,o=s.hooks[l],r=n[l];er.passThroughHooks.has(i)?n[l]=c=>{if(this.defaults.async)return Promise.resolve(o.call(n,c)).then(u=>r.call(n,u));const d=o.call(n,c);return r.call(n,d)}:n[l]=(...c)=>{let d=o.apply(n,c);return d===!1&&(d=r.apply(n,c)),d}}a.hooks=n}if(s.walkTokens){const n=this.defaults.walkTokens,i=s.walkTokens;a.walkTokens=function(l){let o=[];return o.push(i.call(this,l)),n&&(o=o.concat(n.call(this,l))),o}}this.defaults={...this.defaults,...a}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return Ka.lex(e,t??this.defaults)}parser(e,t){return Ja.parse(e,t??this.defaults)}parseMarkdown(e){return(s,a)=>{const n={...a},i={...this.defaults,...n},l=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&n.async===!1)return l(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof s>"u"||s===null)return l(new Error("marked(): input parameter is undefined or null"));if(typeof s!="string")return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(s)+", string expected"));i.hooks&&(i.hooks.options=i,i.hooks.block=e);const o=i.hooks?i.hooks.provideLexer():e?Ka.lex:Ka.lexInline,r=i.hooks?i.hooks.provideParser():e?Ja.parse:Ja.parseInline;if(i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(s):s).then(c=>o(c,i)).then(c=>i.hooks?i.hooks.processAllTokens(c):c).then(c=>i.walkTokens?Promise.all(this.walkTokens(c,i.walkTokens)).then(()=>c):c).then(c=>r(c,i)).then(c=>i.hooks?i.hooks.postprocess(c):c).catch(l);try{i.hooks&&(s=i.hooks.preprocess(s));let c=o(s,i);i.hooks&&(c=i.hooks.processAllTokens(c)),i.walkTokens&&this.walkTokens(c,i.walkTokens);let d=r(c,i);return i.hooks&&(d=i.hooks.postprocess(d)),d}catch(c){return l(c)}}}onError(e,t){return s=>{if(s.message+=` -Please report this to https://github.com/markedjs/marked.`,e){const a="

    An error occurred:

    "+Ca(s.message+"",!0)+"
    ";return t?Promise.resolve(a):a}if(t)return Promise.reject(s);throw s}}},ei=new uE;function vt(e,t){return ei.parse(e,t)}vt.options=vt.setOptions=function(e){return ei.setOptions(e),vt.defaults=ei.defaults,Hg(vt.defaults),vt};vt.getDefaults=Uu;vt.defaults=li;vt.use=function(...e){return ei.use(...e),vt.defaults=ei.defaults,Hg(vt.defaults),vt};vt.walkTokens=function(e,t){return ei.walkTokens(e,t)};vt.parseInline=ei.parseInline;vt.Parser=Ja;vt.parser=Ja.parse;vt.Renderer=Or;vt.TextRenderer=Ku;vt.Lexer=Ka;vt.lexer=Ka.lex;vt.Tokenizer=Ir;vt.Hooks=er;vt.parse=vt;vt.options;vt.setOptions;vt.use;vt.walkTokens;vt.parseInline;Ja.parse;Ka.lex;const pE={breaks:!0,gfm:!0};function hm(e){if(!e)return"";try{if(typeof vt<"u"&&vt.parse){const t=vt.parse(e,pE);return typeof cm<"u"?cm.sanitize(t):t}}catch{}return e.replace(/&/g,"&").replace(//g,">").replace(/\n/g,"
    ")}function fE(e){const t=new Date(e),s=t.getHours().toString().padStart(2,"0"),a=t.getMinutes().toString().padStart(2,"0");return`${s}:${a}`}const mE={run_command:"terminal",ssh_command:"terminal",run_script:"terminal",read_file:"file",apply_patch:"edit",list_directory:"folder",search_knowledge:"search",ingest_document:"book",generate_image:"image",analyze_image:"eye",analyze_pdf:"file",browser_screenshot:"globe",manage_process:"sliders"};function hE(e){return mE[e]||"wrench"}const vE=/https?:\/\/\S+\.(?:png|jpg|jpeg|gif|webp|svg)(?:\?\S*)?/gi;function vm(e){if(!e)return[];const t=e.match(vE);return t?[...new Set(t)]:[]}const gE={template:` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${Aa(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:s}){const a=this.parser.parseInline(s),n=wm(e);if(n===null)return a;e=n;let i='
    ",i}image({href:e,title:t,text:s,tokens:a}){a&&(s=this.parser.parseInline(a,this.parser.textRenderer));const n=wm(e);if(n===null)return Aa(s);e=n;let i=`${s}{const r=l[o].flat(1/0);s=s.concat(this.walkTokens(r,t))}):l.tokens&&(s=s.concat(this.walkTokens(l.tokens,t)))}}return s}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(s=>{const a={...s};if(a.async=this.defaults.async||a.async||!1,s.extensions&&(s.extensions.forEach(n=>{if(!n.name)throw new Error("extension name required");if("renderer"in n){const i=t.renderers[n.name];i?t.renderers[n.name]=function(...l){let o=n.renderer.apply(this,l);return o===!1&&(o=i.apply(this,l)),o}:t.renderers[n.name]=n.renderer}if("tokenizer"in n){if(!n.level||n.level!=="block"&&n.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");const i=t[n.level];i?i.unshift(n.tokenizer):t[n.level]=[n.tokenizer],n.start&&(n.level==="block"?t.startBlock?t.startBlock.push(n.start):t.startBlock=[n.start]:n.level==="inline"&&(t.startInline?t.startInline.push(n.start):t.startInline=[n.start]))}"childTokens"in n&&n.childTokens&&(t.childTokens[n.name]=n.childTokens)}),a.extensions=t),s.renderer){const n=this.defaults.renderer||new Fr(this.defaults);for(const i in s.renderer){if(!(i in n))throw new Error(`renderer '${i}' does not exist`);if(["options","parser"].includes(i))continue;const l=i,o=s.renderer[l],r=n[l];n[l]=(...c)=>{let d=o.apply(n,c);return d===!1&&(d=r.apply(n,c)),d||""}}a.renderer=n}if(s.tokenizer){const n=this.defaults.tokenizer||new $r(this.defaults);for(const i in s.tokenizer){if(!(i in n))throw new Error(`tokenizer '${i}' does not exist`);if(["options","rules","lexer"].includes(i))continue;const l=i,o=s.tokenizer[l],r=n[l];n[l]=(...c)=>{let d=o.apply(n,c);return d===!1&&(d=r.apply(n,c)),d}}a.tokenizer=n}if(s.hooks){const n=this.defaults.hooks||new or;for(const i in s.hooks){if(!(i in n))throw new Error(`hook '${i}' does not exist`);if(["options","block"].includes(i))continue;const l=i,o=s.hooks[l],r=n[l];or.passThroughHooks.has(i)?n[l]=c=>{if(this.defaults.async)return Promise.resolve(o.call(n,c)).then(u=>r.call(n,u));const d=o.call(n,c);return r.call(n,d)}:n[l]=(...c)=>{let d=o.apply(n,c);return d===!1&&(d=r.apply(n,c)),d}}a.hooks=n}if(s.walkTokens){const n=this.defaults.walkTokens,i=s.walkTokens;a.walkTokens=function(l){let o=[];return o.push(i.call(this,l)),n&&(o=o.concat(n.call(this,l))),o}}this.defaults={...this.defaults,...a}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return Ya.lex(e,t??this.defaults)}parser(e,t){return Xa.parse(e,t??this.defaults)}parseMarkdown(e){return(s,a)=>{const n={...a},i={...this.defaults,...n},l=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&n.async===!1)return l(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof s>"u"||s===null)return l(new Error("marked(): input parameter is undefined or null"));if(typeof s!="string")return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(s)+", string expected"));i.hooks&&(i.hooks.options=i,i.hooks.block=e);const o=i.hooks?i.hooks.provideLexer():e?Ya.lex:Ya.lexInline,r=i.hooks?i.hooks.provideParser():e?Xa.parse:Xa.parseInline;if(i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(s):s).then(c=>o(c,i)).then(c=>i.hooks?i.hooks.processAllTokens(c):c).then(c=>i.walkTokens?Promise.all(this.walkTokens(c,i.walkTokens)).then(()=>c):c).then(c=>r(c,i)).then(c=>i.hooks?i.hooks.postprocess(c):c).catch(l);try{i.hooks&&(s=i.hooks.preprocess(s));let c=o(s,i);i.hooks&&(c=i.hooks.processAllTokens(c)),i.walkTokens&&this.walkTokens(c,i.walkTokens);let d=r(c,i);return i.hooks&&(d=i.hooks.postprocess(d)),d}catch(c){return l(c)}}}onError(e,t){return s=>{if(s.message+=` +Please report this to https://github.com/markedjs/marked.`,e){const a="

    An error occurred:

    "+Aa(s.message+"",!0)+"
    ";return t?Promise.resolve(a):a}if(t)return Promise.reject(s);throw s}}},ii=new LE;function yt(e,t){return ii.parse(e,t)}yt.options=yt.setOptions=function(e){return ii.setOptions(e),yt.defaults=ii.defaults,Yg(yt.defaults),yt};yt.getDefaults=Ku;yt.defaults=ui;yt.use=function(...e){return ii.use(...e),yt.defaults=ii.defaults,Yg(yt.defaults),yt};yt.walkTokens=function(e,t){return ii.walkTokens(e,t)};yt.parseInline=ii.parseInline;yt.Parser=Xa;yt.parser=Xa.parse;yt.Renderer=Fr;yt.TextRenderer=sp;yt.Lexer=Ya;yt.lexer=Ya.lex;yt.Tokenizer=$r;yt.Hooks=or;yt.parse=yt;yt.options;yt.setOptions;yt.use;yt.walkTokens;yt.parseInline;Xa.parse;Ya.lex;const NE={breaks:!0,gfm:!0};function Cm(e){if(!e)return"";try{if(typeof yt<"u"&&yt.parse){const t=yt.parse(e,NE);return typeof ym<"u"?ym.sanitize(t):t}}catch{}return e.replace(/&/g,"&").replace(//g,">").replace(/\n/g,"
    ")}function ME(e){const t=new Date(e),s=t.getHours().toString().padStart(2,"0"),a=t.getMinutes().toString().padStart(2,"0");return`${s}:${a}`}const DE={run_command:"terminal",ssh_command:"terminal",run_script:"terminal",read_file:"file",apply_patch:"edit",list_directory:"folder",search_knowledge:"search",ingest_document:"book",generate_image:"image",analyze_image:"eye",analyze_pdf:"file",browser_screenshot:"globe",manage_process:"sliders"};function PE(e){return DE[e]||"wrench"}const $E=/https?:\/\/\S+\.(?:png|jpg|jpeg|gif|webp|svg)(?:\?\S*)?/gi;function Tm(e){if(!e)return[];const t=e.match($E);return t?[...new Set(t)]:[]}const FE={template:`
    @@ -7290,7 +7307,7 @@ Please report this to https://github.com/markedjs/marked.`,e){const a="

    An err

    - `,setup(){const e=p([]),t=p(""),s=p(!1),a=p(""),n=p(null),i=p(null),l=p(0),o=p("");let r=null,c=0;const d=["Check system health","List running services","Show disk usage","What can you do?"],u=U(()=>t.value.trim().length>0&&!s.value),f=p(ut.state||"disconnected");let m=null;const h=U(()=>{const N=f.value;return N==="connected"?"Connected":N==="reconnecting"?"Reconnecting…":N==="connecting"?"Connecting…":"REST fallback"}),g=["Watching across all realms...","Processing...","Consulting the bifrost...","Observing..."],A=U(()=>{const N=Math.floor(l.value/4)%g.length,O=l.value;return O>3?`${g[N]} (${O}s)`:g[0]});function w(){Mt(()=>{n.value&&(n.value.scrollTop=n.value.scrollHeight)})}function y(){if(!i.value)return;const N=i.value;N.style.height="auto",N.style.height=Math.min(N.scrollHeight,120)+"px"}function b(N,O,$={}){const ce={id:++c,role:N,content:O,timestamp:Date.now(),html:N==="bot"?hm(O):"",tools_used:$.tools_used||[],is_error:$.is_error||!1,images:N==="bot"?vm(O):[],files:$.files||[],_showTools:!1};return e.value.push(ce),w(),N==="bot"&&Mt(()=>x()),ce}function x(){if(!n.value)return;n.value.querySelectorAll(".chat-markdown pre:not([data-copy])").forEach(O=>{O.setAttribute("data-copy","true"),O.style.position="relative";const $=document.createElement("button");$.className="chat-code-copy",$.textContent="Copy",$.addEventListener("click",()=>{const ce=O.querySelector("code"),ue=ce?ce.textContent:O.textContent;navigator.clipboard.writeText(ue).then(()=>{$.textContent="Copied!",setTimeout(()=>{$.textContent="Copy"},1500)}).catch(()=>{})}),O.appendChild($)})}function S(N){if(N===0)return!0;const O=e.value[N-1],$=e.value[N],ce=new Date(O.timestamp).toDateString(),ue=new Date($.timestamp).toDateString();return ce!==ue}function _(N){const O=new Date(N),$=new Date;if(O.toDateString()===$.toDateString())return"Today";const ce=new Date($);return ce.setDate(ce.getDate()-1),O.toDateString()===ce.toDateString()?"Yesterday":O.toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"})}function T(N){t.value=N,Mt(()=>X())}function L(N){window.open(N,"_blank","noopener")}function E(N){N.target.style.display="none"}function P(){l.value=0,r=setInterval(()=>{l.value++},1e3)}function R(){r&&(clearInterval(r),r=null),l.value=0}function I(N){s.value&&(s.value=!1,R(),N.type==="chat_response"?b("bot",N.content,{tools_used:N.tools_used||[],is_error:N.is_error||!1,files:N.files||[]}):N.type==="chat_error"&&b("bot",N.error||"Unknown error",{is_error:!0}),Mt(()=>{var O;return(O=i.value)==null?void 0:O.focus()}))}async function H(N){try{const O=await z.post("/api/chat",{content:N,channel_id:o.value});b("bot",O.response,{tools_used:O.tools_used||[],is_error:O.is_error||!1,files:O.files||[]})}catch(O){b("bot",O.message||"Failed to send message",{is_error:!0})}}async function X(){const N=t.value.trim();if(!N||s.value)return;b("user",N),t.value="",s.value=!0,P(),i.value&&(i.value.style.height="auto"),ut.connected&&ut.sendChat(N,{channelId:o.value})||(await H(N),s.value=!1,R()),Mt(()=>{var $;return($=i.value)==null?void 0:$.focus()})}async function Q(){a.value="";try{if(!o.value){const O=await z.get("/api/auth/session");o.value=O.channel_id||O.user_id||"web-user"}const N=await z.get("/api/sessions/"+encodeURIComponent(o.value));if(N&&N.messages&&N.messages.length>0){for(const O of N.messages){const $=O.role==="user"?"user":"bot";let ce=O.content||"";if($==="user"){const Z=ce.match(/^\[.*?\]:\s*/);Z&&(ce=ce.slice(Z[0].length))}if(!ce.trim())continue;const ue={id:++c,role:$,content:ce,timestamp:O.timestamp?O.timestamp*1e3:Date.now(),html:$==="bot"?hm(ce):"",tools_used:[],is_error:!1,images:$==="bot"?vm(ce):[],files:[],_showTools:!1};e.value.push(ue)}Mt(()=>{w(),x()})}}catch(N){N&&N.status!==404&&(a.value="Couldn't load chat history — earlier messages may be missing. Refresh to retry.",ke.error(a.value))}}return tt(()=>{ut.subscribe("chat",I),f.value=ut.state||"disconnected",m=ut.onState(N=>{f.value=N}),Q(),Mt(()=>{var N;return(N=i.value)==null?void 0:N.focus()})}),xt(()=>{ut.unsubscribe("chat",I),m&&(m(),m=null),R()}),{messages:e,input:t,sending:s,historyError:a,messagesEl:n,inputEl:i,canSend:u,wsStatus:h,typingText:A,suggestions:d,send:X,autoResize:y,formatTime:fE,formatDate:_,showDateSeparator:S,useSuggestion:T,openImage:L,onImageError:E,getToolIcon:hE,loadHistory:Q}}},bE={setup(){const e=p("odin"),t=p(""),s=p(""),a=p(""),n=p({}),i=p([]),l=p([]),o=p(!1),r=p(!1),c=p(null),d=p(!0),u=p(""),f=p(!1),m=p(!1),h=U(()=>e.value==="custom"),g=U(()=>[...i.value,...l.value]),A=U(()=>l.value.includes(e.value)),w=U(()=>{var L;return h.value?t.value||"Odin":((L=n.value[e.value])==null?void 0:L.name)||e.value}),y=U(()=>{var L;return h.value?s.value||"(empty — will use Odin default)":((L=n.value[e.value])==null?void 0:L.identity)||""}),b=U(()=>{var L;return h.value?a.value||"(empty — will use Odin default)":((L=n.value[e.value])==null?void 0:L.voice)||""});async function x(){d.value=!0;try{const L=await z.get("/api/personality");e.value=L.preset||"odin",t.value=L.custom_name||"",s.value=L.custom_identity||"",a.value=L.custom_voice||"",n.value=L.presets||{},i.value=L.builtin_presets||[],l.value=L.user_presets||[]}catch(L){c.value=L.message}finally{d.value=!1}}async function S(){o.value=!0,c.value=null,r.value=!1;try{await z.put("/api/personality",{preset:e.value,custom_name:t.value,custom_identity:s.value,custom_voice:a.value}),r.value=!0,setTimeout(()=>r.value=!1,3e3)}catch(L){c.value=L.message}finally{o.value=!1}}async function _(){const L=u.value.trim();if(L){m.value=!0,c.value=null;try{await z.post("/api/personality/presets",{name:L,display_name:w.value,identity:y.value,voice:b.value}),f.value=!1,u.value="",await x(),e.value=L.toLowerCase().replace(/ /g,"_")}catch(E){c.value=E.message}finally{m.value=!1}}}async function T(){if(await os({title:"Delete preset",message:`Delete preset "${e.value}"? This cannot be undone.`,confirmLabel:"Delete",danger:!0})){c.value=null;try{await z.del(`/api/personality/presets/${encodeURIComponent(e.value)}`),await x(),e.value="odin"}catch(E){c.value=E.message}}}return tt(x),{preset:e,customName:t,customIdentity:s,customVoice:a,presets:n,presetNames:g,isCustom:h,isUserPreset:A,previewName:w,previewIdentity:y,previewVoice:b,saving:o,saved:r,error:c,loading:d,save:S,showSavePreset:f,newPresetName:u,savingPreset:m,saveAsPreset:_,deletePreset:T,builtinPresets:i,userPresets:l}},template:` + `,setup(){const e=f([]),t=f(""),s=f(!1),a=f(""),n=f(null),i=f(null),l=f(0),o=f("");let r=null,c=0;const d=["Check system health","List running services","Show disk usage","What can you do?"],u=B(()=>t.value.trim().length>0&&!s.value),p=f(mt.state||"disconnected");let m=null;const h=B(()=>{const N=p.value;return N==="connected"?"Connected":N==="reconnecting"?"Reconnecting…":N==="connecting"?"Connecting…":"REST fallback"}),y=["Watching across all realms...","Processing...","Consulting the bifrost...","Observing..."],k=B(()=>{const N=Math.floor(l.value/4)%y.length,M=l.value;return M>3?`${y[N]} (${M}s)`:y[0]});function A(){Ut(()=>{n.value&&(n.value.scrollTop=n.value.scrollHeight)})}function g(){if(!i.value)return;const N=i.value;N.style.height="auto",N.style.height=Math.min(N.scrollHeight,120)+"px"}function b(N,M,$={}){const re={id:++c,role:N,content:M,timestamp:Date.now(),html:N==="bot"?Cm(M):"",tools_used:$.tools_used||[],is_error:$.is_error||!1,images:N==="bot"?Tm(M):[],files:$.files||[],_showTools:!1};return e.value.push(re),A(),N==="bot"&&Ut(()=>x()),re}function x(){if(!n.value)return;n.value.querySelectorAll(".chat-markdown pre:not([data-copy])").forEach(M=>{M.setAttribute("data-copy","true"),M.style.position="relative";const $=document.createElement("button");$.className="chat-code-copy",$.textContent="Copy",$.addEventListener("click",()=>{const re=M.querySelector("code"),ce=re?re.textContent:M.textContent;navigator.clipboard.writeText(ce).then(()=>{$.textContent="Copied!",setTimeout(()=>{$.textContent="Copy"},1500)}).catch(()=>{})}),M.appendChild($)})}function w(N){if(N===0)return!0;const M=e.value[N-1],$=e.value[N],re=new Date(M.timestamp).toDateString(),ce=new Date($.timestamp).toDateString();return re!==ce}function _(N){const M=new Date(N),$=new Date;if(M.toDateString()===$.toDateString())return"Today";const re=new Date($);return re.setDate(re.getDate()-1),M.toDateString()===re.toDateString()?"Yesterday":M.toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"})}function T(N){t.value=N,Ut(()=>te())}function L(N){window.open(N,"_blank","noopener")}function E(N){N.target.style.display="none"}function F(){l.value=0,r=setInterval(()=>{l.value++},1e3)}function O(){r&&(clearInterval(r),r=null),l.value=0}function I(N){s.value&&(s.value=!1,O(),N.type==="chat_response"?b("bot",N.content,{tools_used:N.tools_used||[],is_error:N.is_error||!1,files:N.files||[]}):N.type==="chat_error"&&b("bot",N.error||"Unknown error",{is_error:!0}),Ut(()=>{var M;return(M=i.value)==null?void 0:M.focus()}))}async function H(N){try{const M=await z.post("/api/chat",{content:N,channel_id:o.value});b("bot",M.response,{tools_used:M.tools_used||[],is_error:M.is_error||!1,files:M.files||[]})}catch(M){b("bot",M.message||"Failed to send message",{is_error:!0})}}async function te(){const N=t.value.trim();if(!N||s.value)return;b("user",N),t.value="",s.value=!0,F(),i.value&&(i.value.style.height="auto"),mt.connected&&mt.sendChat(N,{channelId:o.value})||(await H(N),s.value=!1,O()),Ut(()=>{var $;return($=i.value)==null?void 0:$.focus()})}async function ee(){a.value="";try{if(!o.value){const M=await z.get("/api/auth/session");o.value=M.channel_id||M.user_id||"web-user"}const N=await z.get("/api/sessions/"+encodeURIComponent(o.value));if(N&&N.messages&&N.messages.length>0){for(const M of N.messages){const $=M.role==="user"?"user":"bot";let re=M.content||"";if($==="user"){const Z=re.match(/^\[.*?\]:\s*/);Z&&(re=re.slice(Z[0].length))}if(!re.trim())continue;const ce={id:++c,role:$,content:re,timestamp:M.timestamp?M.timestamp*1e3:Date.now(),html:$==="bot"?Cm(re):"",tools_used:[],is_error:!1,images:$==="bot"?Tm(re):[],files:[],_showTools:!1};e.value.push(ce)}Ut(()=>{A(),x()})}}catch(N){N&&N.status!==404&&(a.value="Couldn't load chat history — earlier messages may be missing. Refresh to retry.",Se.error(a.value))}}return st(()=>{mt.subscribe("chat",I),p.value=mt.state||"disconnected",m=mt.onState(N=>{p.value=N}),ee(),Ut(()=>{var N;return(N=i.value)==null?void 0:N.focus()})}),wt(()=>{mt.unsubscribe("chat",I),m&&(m(),m=null),O()}),{messages:e,input:t,sending:s,historyError:a,messagesEl:n,inputEl:i,canSend:u,wsStatus:h,typingText:k,suggestions:d,send:te,autoResize:g,formatTime:ME,formatDate:_,showDateSeparator:w,useSuggestion:T,openImage:L,onImageError:E,getToolIcon:PE,loadHistory:ee}}},BE={setup(){const e=f("odin"),t=f(""),s=f(""),a=f(""),n=f({}),i=f([]),l=f([]),o=f(!1),r=f(!1),c=f(null),d=f(!0),u=f(""),p=f(!1),m=f(!1),h=B(()=>e.value==="custom"),y=B(()=>[...i.value,...l.value]),k=B(()=>l.value.includes(e.value)),A=B(()=>{var L;return h.value?t.value||"Odin":((L=n.value[e.value])==null?void 0:L.name)||e.value}),g=B(()=>{var L;return h.value?s.value||"(empty — will use Odin default)":((L=n.value[e.value])==null?void 0:L.identity)||""}),b=B(()=>{var L;return h.value?a.value||"(empty — will use Odin default)":((L=n.value[e.value])==null?void 0:L.voice)||""});async function x(){d.value=!0;try{const L=await z.get("/api/personality");e.value=L.preset||"odin",t.value=L.custom_name||"",s.value=L.custom_identity||"",a.value=L.custom_voice||"",n.value=L.presets||{},i.value=L.builtin_presets||[],l.value=L.user_presets||[]}catch(L){c.value=L.message}finally{d.value=!1}}async function w(){o.value=!0,c.value=null,r.value=!1;try{await z.put("/api/personality",{preset:e.value,custom_name:t.value,custom_identity:s.value,custom_voice:a.value}),r.value=!0,setTimeout(()=>r.value=!1,3e3)}catch(L){c.value=L.message}finally{o.value=!1}}async function _(){const L=u.value.trim();if(L){m.value=!0,c.value=null;try{await z.post("/api/personality/presets",{name:L,display_name:A.value,identity:g.value,voice:b.value}),p.value=!1,u.value="",await x(),e.value=L.toLowerCase().replace(/ /g,"_")}catch(E){c.value=E.message}finally{m.value=!1}}}async function T(){if(await is({title:"Delete preset",message:`Delete preset "${e.value}"? This cannot be undone.`,confirmLabel:"Delete",danger:!0})){c.value=null;try{await z.del(`/api/personality/presets/${encodeURIComponent(e.value)}`),await x(),e.value="odin"}catch(E){c.value=E.message}}}return st(x),{preset:e,customName:t,customIdentity:s,customVoice:a,presets:n,presetNames:y,isCustom:h,isUserPreset:k,previewName:A,previewIdentity:g,previewVoice:b,saving:o,saved:r,error:c,loading:d,save:w,showSavePreset:p,newPresetName:u,savingPreset:m,saveAsPreset:_,deletePreset:T,builtinPresets:i,userPresets:l}},template:`

    Personality

    @@ -7390,7 +7407,7 @@ Please report this to https://github.com/markedjs/marked.`,e){const a="

    An err

    - `},yE={props:["onComplete"],template:` + `},UE={props:["onComplete"],template:`
    -
    `,setup(e){const t=p(""),s=p(""),a=p(!1),n=p(!1),i=p(""),l=p("Initialization pending."),o=p(!1),r=p(""),c=p(null),d=p({}),u=p("");let f=0,m=null;function h(){return f+=1,m==null||m.abort(),m=null,f}function g(S,_,T){return typeof z.postWithOptions=="function"?z.postWithOptions(S,_,{signal:T}):z.post(S,_)}async function A(){var T,L,E;a.value=!0,i.value="",l.value="Saving setup…";const S={},_=!!s.value.trim();t.value.trim()&&(S.web_api_token=t.value.trim()),s.value.trim()&&(S.discord_token=s.value.trim());try{const P=z.post("/api/setup/complete",S);t.value="",s.value="";const R=await P,I=((T=R.discord)==null?void 0:T.state)||R.discord_status;if(I==="failed"?(i.value=((L=R.discord)==null?void 0:L.error)||"Discord attachment failed. Setup was saved.",l.value="Saved. Discord attachment failed."):I==="connecting"?l.value="Saved. Connecting Discord…":I==="ready"?l.value="Saved. Discord ready.":_?l.value="Saved. Discord token stored; attachment status is pending.":l.value=R.message||"Setup saved. Ready to sign in.",(E=R.restart_required)!=null&&E.length){const H=R.message||`Restart Odin to apply: ${R.restart_required.join(", ")}`;l.value.includes(H)||(l.value+=` ${H}`)}n.value=!0}catch(P){i.value=P.message||"Setup could not be saved.",l.value="Initialization pending."}finally{a.value=!1}}async function w(){const S=h(),_=typeof AbortController=="function"?new AbortController:null;m=_,o.value=!0,u.value="";try{const T=await g("/api/codex/device-code",void 0,_==null?void 0:_.signal);if(S!==f)return;c.value=T,r.value="pending";const L=await g("/api/codex/device-poll",{device_auth_id:T.device_auth_id,user_code:T.user_code,interval:T.interval},_==null?void 0:_.signal);if(S!==f)return;d.value=L||{},r.value="ready"}catch(T){S===f&&(T==null?void 0:T.name)!=="AbortError"&&(u.value=T.message||"Device sign-in failed.",r.value="failed")}finally{S===f&&(o.value=!1,m=null)}}function y(){h(),o.value=!1,x()}function b(){var S;(S=e.onComplete)==null||S.call(e)}function x(){r.value="",c.value=null,d.value={},u.value=""}return xt(()=>{h()}),{webApiToken:t,discordToken:s,saving:a,completed:n,error:i,statusMessage:l,save:A,onComplete:b,deviceLoading:o,deviceState:r,deviceInfo:c,deviceResult:d,deviceError:u,startDeviceLogin:w,cancelDeviceLogin:y,clearDeviceState:x}}},Dt=(e,t)=>s=>({path:e,query:{...s.query,tab:t}}),Xg=[{path:"/",redirect:"/dashboard"},{path:"/dashboard",component:XC,meta:{label:"Dashboard",icon:"dashboard",section:"Workspace",description:"System posture and recent activity"}},{path:"/chat",component:gE,meta:{label:"Chat",icon:"chat",section:"Workspace",description:"Direct operator conversation"}},{path:"/operations",component:v1,meta:{label:"Operations",icon:"operations",section:"Operate",description:"Execution, agents, loops, processes, and schedules"}},{path:"/history",component:E1,meta:{label:"History",icon:"history",section:"Observe",description:"Audit trail, sessions, traces, and usage"}},{path:"/capabilities",component:K1,meta:{label:"Capabilities",icon:"capabilities",section:"Manage",description:"Tools, skills, knowledge, and memory"}},{path:"/personality",component:bE,meta:{label:"Personality",icon:"personality",section:"Manage",description:"Behavior and response profile"}},{path:"/system",component:GC,meta:{label:"System",icon:"system",section:"Manage",description:"Health, configuration, access, and updates"}},{path:"/execution",redirect:Dt("/operations","live")},{path:"/agents",redirect:Dt("/operations","agents")},{path:"/loops",redirect:Dt("/operations","loops")},{path:"/processes",redirect:Dt("/operations","processes")},{path:"/schedules",redirect:Dt("/operations","schedules")},{path:"/audit",redirect:Dt("/history","audit")},{path:"/sessions",redirect:Dt("/history","sessions")},{path:"/traces",redirect:Dt("/history","traces")},{path:"/usage",redirect:Dt("/history","usage")},{path:"/tools",redirect:Dt("/capabilities","tools")},{path:"/skills",redirect:Dt("/capabilities","skills")},{path:"/mcp",redirect:Dt("/capabilities","mcp-servers")},{path:"/knowledge",redirect:Dt("/capabilities","knowledge")},{path:"/memory",redirect:Dt("/capabilities","memory")},{path:"/learned",redirect:Dt("/capabilities","learned")},{path:"/health",redirect:Dt("/system","health")},{path:"/resources",redirect:Dt("/system","resources")},{path:"/logs",redirect:Dt("/system","logs")},{path:"/config",redirect:Dt("/system","config")},{path:"/host-access",redirect:Dt("/system","host-access")},{path:"/hosts",redirect:Dt("/system","hosts")},{path:"/internals",redirect:Dt("/system","internals")}],Ml=jS({history:_S(),routes:Xg});Ml.afterEach(e=>{var s;const t=(s=e.meta)==null?void 0:s.label;document.title=t?`Odin — ${t}`:"Odin — Management"});const xE={template:` + `,setup(e){const t=f(""),s=f(""),a=f(!1),n=f(!1),i=f(""),l=f("Initialization pending."),o=f(!1),r=f(""),c=f(null),d=f({}),u=f("");let p=0,m=null;function h(){return p+=1,m==null||m.abort(),m=null,p}function y(w,_,T){return typeof z.postWithOptions=="function"?z.postWithOptions(w,_,{signal:T}):z.post(w,_)}async function k(){var T,L,E;a.value=!0,i.value="",l.value="Saving setup…";const w={},_=!!s.value.trim();t.value.trim()&&(w.web_api_token=t.value.trim()),s.value.trim()&&(w.discord_token=s.value.trim());try{const F=z.post("/api/setup/complete",w);t.value="",s.value="";const O=await F,I=((T=O.discord)==null?void 0:T.state)||O.discord_status;if(I==="failed"?(i.value=((L=O.discord)==null?void 0:L.error)||"Discord attachment failed. Setup was saved.",l.value="Saved. Discord attachment failed."):I==="connecting"?l.value="Saved. Connecting Discord…":I==="ready"?l.value="Saved. Discord ready.":_?l.value="Saved. Discord token stored; attachment status is pending.":l.value=O.message||"Setup saved. Ready to sign in.",(E=O.restart_required)!=null&&E.length){const H=O.message||`Restart Odin to apply: ${O.restart_required.join(", ")}`;l.value.includes(H)||(l.value+=` ${H}`)}n.value=!0}catch(F){i.value=F.message||"Setup could not be saved.",l.value="Initialization pending."}finally{a.value=!1}}async function A(){const w=h(),_=typeof AbortController=="function"?new AbortController:null;m=_,o.value=!0,u.value="";try{const T=await y("/api/codex/device-code",void 0,_==null?void 0:_.signal);if(w!==p)return;c.value=T,r.value="pending";const L=await y("/api/codex/device-poll",{device_auth_id:T.device_auth_id,user_code:T.user_code,interval:T.interval},_==null?void 0:_.signal);if(w!==p)return;d.value=L||{},r.value="ready"}catch(T){w===p&&(T==null?void 0:T.name)!=="AbortError"&&(u.value=T.message||"Device sign-in failed.",r.value="failed")}finally{w===p&&(o.value=!1,m=null)}}function g(){h(),o.value=!1,x()}function b(){var w;(w=e.onComplete)==null||w.call(e)}function x(){r.value="",c.value=null,d.value={},u.value=""}return wt(()=>{h()}),{webApiToken:t,discordToken:s,saving:a,completed:n,error:i,statusMessage:l,save:k,onComplete:b,deviceLoading:o,deviceState:r,deviceInfo:c,deviceResult:d,deviceError:u,startDeviceLogin:A,cancelDeviceLogin:g,clearDeviceState:x}}},Ht=(e,t)=>s=>({path:e,query:{...s.query,tab:t}}),cb=[{path:"/",redirect:"/dashboard"},{path:"/dashboard",component:xT,meta:{label:"Dashboard",icon:"dashboard",section:"Workspace",description:"System posture and recent activity"}},{path:"/chat",component:FE,meta:{label:"Chat",icon:"chat",section:"Workspace",description:"Direct operator conversation"}},{path:"/operations",component:$1,meta:{label:"Operations",icon:"operations",section:"Operate",description:"Execution, agents, loops, processes, and schedules"}},{path:"/history",component:K1,meta:{label:"History",icon:"history",section:"Observe",description:"Audit trail, sessions, traces, and usage"}},{path:"/capabilities",component:vC,meta:{label:"Capabilities",icon:"capabilities",section:"Manage",description:"Tools, skills, knowledge, and memory"}},{path:"/personality",component:BE,meta:{label:"Personality",icon:"personality",section:"Manage",description:"Behavior and response profile"}},{path:"/system",component:fT,meta:{label:"System",icon:"system",section:"Manage",description:"Health, configuration, access, and updates"}},{path:"/execution",redirect:Ht("/operations","live")},{path:"/agents",redirect:Ht("/operations","agents")},{path:"/loops",redirect:Ht("/operations","loops")},{path:"/processes",redirect:Ht("/operations","processes")},{path:"/schedules",redirect:Ht("/operations","schedules")},{path:"/audit",redirect:Ht("/history","audit")},{path:"/sessions",redirect:Ht("/history","sessions")},{path:"/traces",redirect:Ht("/history","traces")},{path:"/usage",redirect:Ht("/history","usage")},{path:"/tools",redirect:Ht("/capabilities","tools")},{path:"/skills",redirect:Ht("/capabilities","skills")},{path:"/mcp",redirect:Ht("/capabilities","mcp-servers")},{path:"/knowledge",redirect:Ht("/capabilities","knowledge")},{path:"/memory",redirect:Ht("/capabilities","memory")},{path:"/learned",redirect:Ht("/capabilities","learned")},{path:"/health",redirect:Ht("/system","health")},{path:"/resources",redirect:Ht("/system","resources")},{path:"/logs",redirect:Ht("/system","logs")},{path:"/config",redirect:Ht("/system","config")},{path:"/host-access",redirect:Ht("/system","host-access")},{path:"/hosts",redirect:Ht("/system","hosts")},{path:"/internals",redirect:Ht("/system","internals")}],Ul=d1({history:HS(),routes:cb});Ul.afterEach(e=>{var s;const t=(s=e.meta)==null?void 0:s.label;document.title=t?`Odin — ${t}`:"Odin — Management"});const zE={template:` `,props:["onLogin","sessionExpired"],setup(e){const t=p(""),s=p(null),a=p(!1),n=p(!1);async function i(){a.value=!0,s.value=null;try{z.setPersist(n.value),await z.login(t.value),e.onLogin()}catch(l){s.value=l.message||"Login failed"}finally{a.value=!1}}return{token:t,error:s,busy:a,persist:n,login:i}}},_E={template:` + `,props:["onLogin","sessionExpired"],setup(e){const t=f(""),s=f(null),a=f(!1),n=f(!1);async function i(){a.value=!0,s.value=null;try{z.setPersist(n.value),await z.login(t.value),e.onLogin()}catch(l){s.value=l.message||"Login failed"}finally{a.value=!1}}return{token:t,error:s,busy:a,persist:n,login:i}}},HE={template:`
    Loading application... @@ -7575,4 +7592,4 @@ Please report this to https://github.com/markedjs/marked.`,e){const a="

    An err

    - `,setup(){const e=p("checking"),t=p(!1),s=p(!1),a=p(!1),n=p(null),i=p(null),l=p(!1);let o=null,r=null;const c=p(!1),d=p("disconnected"),u=p(-1),f=p(null);let m=null;const h=p("starting"),g=p(""),A=Xg.filter(Z=>Z.meta),w=U(()=>["Workspace","Operate","Observe","Manage"].map(Z=>({name:Z,routes:A.filter(J=>J.meta.section===Z)})).filter(Z=>Z.routes.length)),y=U(()=>{var Z;return((Z=Ml.currentRoute.value.meta)==null?void 0:Z.label)||"Odin"}),b=U(()=>{var Z;return((Z=Ml.currentRoute.value.meta)==null?void 0:Z.section)||"Management"}),x=U(()=>{var Z;return((Z=Ml.currentRoute.value.meta)==null?void 0:Z.description)||"Management console"});function S(){ut.disconnect(),N&&(clearInterval(N),N=null)}z.onSessionExpired=()=>{t.value=!0,S(),z.setToken(""),e.value="login"};function _(Z){var J;if((Z.ctrlKey||Z.metaKey)&&Z.key.toLowerCase()==="k"){e.value==="ready"&&(Z.preventDefault(),Kf());return}if(a.value&&Z.key==="Tab"){const se=[...((J=n.value)==null?void 0:J.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'))||[]];if(se.length){const q=se[0],ye=se[se.length-1];if(Z.shiftKey&&(document.activeElement===q||!n.value.contains(document.activeElement))){Z.preventDefault(),ye.focus();return}if(!Z.shiftKey&&(document.activeElement===ye||!n.value.contains(document.activeElement))){Z.preventDefault(),q.focus();return}}}if(Z.key==="Escape"&&a.value){a.value=!1,Z.preventDefault();return}if(Z.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(Z.target.tagName)){Z.preventDefault();const se=document.querySelector('.hm-main input[type="text"], .hm-main .hm-input:not(textarea):not(select)');se&&se.focus()}}function T(){l.value=!!(o!=null&&o.matches),l.value||(a.value=!1)}async function L(){try{const Z=await z.get("/api/setup/status");if(Z.mode==="pending"||Z.needed===!0)return S(),e.value="setup",!0}catch(Z){Z==null||Z.name}return!1}tt(async()=>{if(document.addEventListener("keydown",_),o=window.matchMedia("(max-width: 900px)"),T(),o.addEventListener("change",T),await L())return;const Z=await z.check();Z.ok?(e.value="ready",ce()):Z.needsAuth?e.value="login":(e.value="ready",ce())});function E(){t.value=!1,e.value="ready",ce()}async function P(){if(await L())return;const Z=await z.check();Z.ok?(e.value="ready",ce()):Z.needsAuth?e.value="login":(e.value="ready",ce())}async function R(){S(),e.value="login",await z.logout()}function I(){s.value=!s.value}function H(){a.value=!a.value}jt(a,async Z=>{var J,se;if(Z)r=document.activeElement,await Mt(),(se=(J=n.value)==null?void 0:J.querySelector(".nav-item"))==null||se.focus();else if(r!=null&&r.isConnected){const q=r;r=null,requestAnimationFrame(()=>q.focus())}});const X=U(()=>{switch(d.value){case"connected":return"Live";case"connecting":return"Connecting…";case"reconnecting":return"Reconnecting…";default:return"Disconnected"}});function Q(Z,J="info",se=3e3){f.value={text:Z,level:J},clearTimeout(m),m=setTimeout(()=>{f.value=null},se)}let N=null,O=!1,$=[];function ce(){for(const Z of $)Z();$=[ut.onStatus(Z=>{c.value=Z}),ut.onLatencyChange(Z=>{u.value=Z}),ut.onState((Z,J)=>{d.value=Z,Z==="connected"?(O&&Q("Connection restored","success"),O=!0):Z==="reconnecting"&&J.attempt===1&&Q("Connection lost — reconnecting…","warn")})],ut.connect(),ue(),N&&clearInterval(N),N=setInterval(ue,15e3)}async function ue(){try{const Z=await z.get("/api/status");h.value=Z.status==="online"?"online":"starting";const J=Z.uptime_seconds||0,se=Math.floor(J/3600),q=Math.floor(J%3600/60);g.value=`${se}h ${q}m uptime`}catch{h.value="offline",g.value=""}}return xt(()=>{N&&clearInterval(N);for(const Z of $)Z();$=[],ut.disconnect(),document.removeEventListener("keydown",_),o==null||o.removeEventListener("change",T)}),{authState:e,sessionExpired:t,sidebarCollapsed:s,mobileOpen:a,wsConnected:c,wsState:d,wsLatency:u,wsLabel:X,wsToast:f,botStatus:h,botUptime:g,navRoutes:A,navGroups:w,currentPage:y,currentSection:b,currentDescription:x,sidebarEl:n,mobileMenuButton:i,isMobileViewport:l,onLogin:E,onSetupComplete:P,logout:R,toggleSidebar:I,toggleMobileNavigation:H,openPalette:Kf}}},on=br(_E);on.component("odin-icon",ZC);on.component("login-screen",xE);on.component("setup-page",yE);on.component("toast-container",Dk);on.component("confirm-host",Pk);on.component("command-palette",JC);on.directive("modal-focus",YC);on.use(Ml);on.mount("#app"); + `,setup(){const e=f("checking"),t=f(!1),s=f(!1),a=f(!1),n=f(null),i=f(null),l=f(!1);let o=null,r=null;const c=f(!1),d=f("disconnected"),u=f(-1),p=f(null);let m=null;const h=f("starting"),y=f(""),k=cb.filter(Z=>Z.meta),A=B(()=>["Workspace","Operate","Observe","Manage"].map(Z=>({name:Z,routes:k.filter(Q=>Q.meta.section===Z)})).filter(Z=>Z.routes.length)),g=B(()=>{var Z;return((Z=Ul.currentRoute.value.meta)==null?void 0:Z.label)||"Odin"}),b=B(()=>{var Z;return((Z=Ul.currentRoute.value.meta)==null?void 0:Z.section)||"Management"}),x=B(()=>{var Z;return((Z=Ul.currentRoute.value.meta)==null?void 0:Z.description)||"Management console"});function w(){mt.disconnect(),N&&(clearInterval(N),N=null)}z.onSessionExpired=()=>{t.value=!0,w(),z.setToken(""),e.value="login"};function _(Z){var Q;if((Z.ctrlKey||Z.metaKey)&&Z.key.toLowerCase()==="k"){e.value==="ready"&&(Z.preventDefault(),nm());return}if(a.value&&Z.key==="Tab"){const ae=[...((Q=n.value)==null?void 0:Q.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'))||[]];if(ae.length){const V=ae[0],be=ae[ae.length-1];if(Z.shiftKey&&(document.activeElement===V||!n.value.contains(document.activeElement))){Z.preventDefault(),be.focus();return}if(!Z.shiftKey&&(document.activeElement===be||!n.value.contains(document.activeElement))){Z.preventDefault(),V.focus();return}}}if(Z.key==="Escape"&&a.value){a.value=!1,Z.preventDefault();return}if(Z.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(Z.target.tagName)){Z.preventDefault();const ae=document.querySelector('.hm-main input[type="text"], .hm-main .hm-input:not(textarea):not(select)');ae&&ae.focus()}}function T(){l.value=!!(o!=null&&o.matches),l.value||(a.value=!1)}async function L(){try{const Z=await z.get("/api/setup/status");if(Z.mode==="pending"||Z.needed===!0)return w(),e.value="setup",!0}catch(Z){Z==null||Z.name}return!1}st(async()=>{if(document.addEventListener("keydown",_),o=window.matchMedia("(max-width: 900px)"),T(),o.addEventListener("change",T),await L())return;const Z=await z.check();Z.ok?(e.value="ready",re()):Z.needsAuth?e.value="login":(e.value="ready",re())});function E(){t.value=!1,e.value="ready",re()}async function F(){if(await L())return;const Z=await z.check();Z.ok?(e.value="ready",re()):Z.needsAuth?e.value="login":(e.value="ready",re())}async function O(){w(),e.value="login",await z.logout()}function I(){s.value=!s.value}function H(){a.value=!a.value}Gt(a,async Z=>{var Q,ae;if(Z)r=document.activeElement,await Ut(),(ae=(Q=n.value)==null?void 0:Q.querySelector(".nav-item"))==null||ae.focus();else if(r!=null&&r.isConnected){const V=r;r=null,requestAnimationFrame(()=>V.focus())}});const te=B(()=>{switch(d.value){case"connected":return"Live";case"connecting":return"Connecting…";case"reconnecting":return"Reconnecting…";default:return"Disconnected"}});function ee(Z,Q="info",ae=3e3){p.value={text:Z,level:Q},clearTimeout(m),m=setTimeout(()=>{p.value=null},ae)}let N=null,M=!1,$=[];function re(){for(const Z of $)Z();$=[mt.onStatus(Z=>{c.value=Z}),mt.onLatencyChange(Z=>{u.value=Z}),mt.onState((Z,Q)=>{d.value=Z,Z==="connected"?(M&&ee("Connection restored","success"),M=!0):Z==="reconnecting"&&Q.attempt===1&&ee("Connection lost — reconnecting…","warn")})],mt.connect(),ce(),N&&clearInterval(N),N=setInterval(ce,15e3)}async function ce(){try{const Z=await z.get("/api/status");h.value=Z.status==="online"?"online":"starting";const Q=Z.uptime_seconds||0,ae=Math.floor(Q/3600),V=Math.floor(Q%3600/60);y.value=`${ae}h ${V}m uptime`}catch{h.value="offline",y.value=""}}return wt(()=>{N&&clearInterval(N);for(const Z of $)Z();$=[],mt.disconnect(),document.removeEventListener("keydown",_),o==null||o.removeEventListener("change",T)}),{authState:e,sessionExpired:t,sidebarCollapsed:s,mobileOpen:a,wsConnected:c,wsState:d,wsLatency:u,wsLabel:te,wsToast:p,botStatus:h,botUptime:y,navRoutes:k,navGroups:A,currentPage:g,currentSection:b,currentDescription:x,sidebarEl:n,mobileMenuButton:i,isMobileViewport:l,onLogin:E,onSetupComplete:F,logout:O,toggleSidebar:I,toggleMobileNavigation:H,openPalette:nm}}},un=Cr(HE);un.component("odin-icon",gT);un.component("login-screen",zE);un.component("setup-page",UE);un.component("toast-container",sS);un.component("confirm-host",aS);un.component("command-palette",hT);un.directive("modal-focus",yT);un.use(Ul);un.mount("#app"); diff --git a/ui/dist/index.html b/ui/dist/index.html index d97903d6d..60d2dc324 100644 --- a/ui/dist/index.html +++ b/ui/dist/index.html @@ -9,7 +9,7 @@ Odin — Management - + diff --git a/ui/js/api.js b/ui/js/api.js index 075ec928e..27bd135f6 100644 --- a/ui/js/api.js +++ b/ui/js/api.js @@ -31,20 +31,33 @@ class OdinAPI { this._sessionTimeout = timeoutSeconds; this._lastActivity = Date.now(); if (token) { - const store = this._persist ? localStorage : sessionStorage; - store.setItem('odin_token', token); - if (this._persist) localStorage.setItem('odin_persist', '1'); - if (timeoutSeconds > 0) { - store.setItem('odin_session_timeout', String(timeoutSeconds)); + if (this._persist) { + localStorage.setItem('odin_token', token); + if (timeoutSeconds > 0) { + localStorage.setItem('odin_session_timeout', String(timeoutSeconds)); + } else { + localStorage.removeItem('odin_session_timeout'); + } + localStorage.setItem('odin_persist', '1'); + sessionStorage.removeItem('odin_token'); + sessionStorage.removeItem('odin_session_timeout'); } else { - store.removeItem('odin_session_timeout'); + localStorage.removeItem('odin_persist'); + localStorage.removeItem('odin_token'); + localStorage.removeItem('odin_session_timeout'); + sessionStorage.setItem('odin_token', token); + if (timeoutSeconds > 0) { + sessionStorage.setItem('odin_session_timeout', String(timeoutSeconds)); + } else { + sessionStorage.removeItem('odin_session_timeout'); + } } this._startActivityMonitor(); } else { + localStorage.removeItem('odin_persist'); sessionStorage.removeItem('odin_token'); sessionStorage.removeItem('odin_session_timeout'); localStorage.removeItem('odin_token'); - localStorage.removeItem('odin_persist'); localStorage.removeItem('odin_session_timeout'); this._stopActivityMonitor(); } diff --git a/ui/js/pages/audit.js b/ui/js/pages/audit.js index bfd887388..855b86c66 100644 --- a/ui/js/pages/audit.js +++ b/ui/js/pages/audit.js @@ -36,14 +36,22 @@ export default {

    - Chain valid — {{ verifyResult.verified }} signed entr{{ verifyResult.verified === 1 ? 'y' : 'ies' }} verified. + Chain valid — {{ verifyResult.verified }} signed entr{{ verifyResult.verified === 1 ? 'y' : 'ies' }} verified across {{ (verifyResult.segments || []).length }} retained file{{ (verifyResult.segments || []).length === 1 ? '' : 's' }}.

    - Chain INVALID — first break at entry {{ verifyResult.first_bad }}; {{ verifyResult.verified }} verified before it. + Chain INVALID — problem in {{ verifyResult.first_bad_file }}{{ verifyResult.first_bad ? ' at line ' + verifyResult.first_bad : '' }}.

    {{ verifyResult.unsigned_prefix.toLocaleString() }} older entries predate signing and are permanently unsigned — expected, not tampering.

    +
      +
    • + {{ seg.file }}{{ seg.position === 0 ? ' (current)' : '' }}: {{ segmentText(seg) }} +
    • +
    +

    + Each file is verified on its own from its first entry. Files already removed by rotation, or deleted by hand from the oldest end, cannot be detected. +

    @@ -242,6 +250,27 @@ export default { verifying.value = false; } + function segmentOk(seg) { + return seg.status === 'verified' || seg.status === 'unsigned'; + } + + function segmentText(seg) { + const n = (value) => Number(value || 0).toLocaleString(); + if (seg.status === 'verified') { + if (!seg.total) return 'no entries yet'; + const prefix = seg.unsigned_prefix > 0 ? `; ${n(seg.unsigned_prefix)} older entries predate signing` : ''; + return `verified — ${n(seg.verified)} signed entries${prefix}`; + } + if (seg.status === 'unsigned') return 'no signatures — written before signing was enabled, so it cannot be verified'; + if (seg.status === 'unreadable') return `could not be read (${seg.error || 'error'})`; + if (seg.status === 'missing') return 'missing — newer and older files exist, but this one does not'; + const before = `${n(seg.verified)} entries verified before it; later lines not checked`; + if (seg.reason === 'hmac_mismatch') return `break at line ${seg.first_bad} — entry altered, reordered, or signed with a different key; ${before}`; + if (seg.reason === 'signing_gap') return 'no signatures although older files are signed — signing was switched off for this period, or signatures were removed'; + if (seg.reason === 'unsigned_after_signed') return `break at line ${seg.first_bad} — unsigned entry after signed ones (signing was switched off, or a line was inserted); ${before}`; + return `break at line ${seg.first_bad} — not a valid audit entry; ${before}`; + } + async function fetchAudit() { const epoch = ++fetchEpoch; loading.value = true; @@ -270,7 +299,7 @@ export default { return { entries, loading, error, expandedIdx, filters, formatTs, formatDetail, truncateBlock, toggleExpand, clearFilters, fetchAudit, - verifying, verifyResult, verifyError, verifyIntegrity, + verifying, verifyResult, verifyError, verifyIntegrity, segmentOk, segmentText, }; }, }; diff --git a/ui/js/pages/llm-config.js b/ui/js/pages/llm-config.js index 1987386a3..c054f5b0e 100644 --- a/ui/js/pages/llm-config.js +++ b/ui/js/pages/llm-config.js @@ -2,7 +2,7 @@ import { api } from '../api.js'; import { toast } from '../toast.js'; import { confirmDialog } from '../confirm.js'; import { quotaBlocks, quotaFailureVisible } from '../codex-quota.js'; -import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue'; +import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, watch } from 'vue'; import { codexAdvancedPayload, codexBasicPayload, openaiCompatibleAdvancedPayload, openaiCompatibleBasicPayload, @@ -50,9 +50,9 @@ export default {

    Model Selection

    -

    Choose models, not a provider. Disabled or unreachable catalogue entries remain visible.

    +

    Enabled providers supply model choices. Unavailable saved selections are retained until you choose another model.

    -
    +
    @@ -60,13 +60,16 @@ export default {
    +

    Main model is configured on a disabled provider. Enable that provider below or choose another model.

    +

    Agent model is configured on a disabled provider. Enable that provider below or choose another model.

    +

    Auxiliary model is configured on a disabled provider. Enable that provider below or choose another model.

    Used for compaction, reflection, consolidation, and background follow-up. Its output feeds sessions and memory, so choose deliberately.

    @@ -771,6 +780,7 @@ export default { let pollArmed = false; let fetchAllInFlight = null; let pollInFlight = null; + let savingMainModel = false; const cleanSnapshots = { codexBasic: null, codexAdvanced: null, ollamaBasic: null, ollamaAdvanced: null, @@ -810,6 +820,46 @@ export default { // first, then the 5.6 family. The defunct // gpt-4.1/gpt-4o/gpt-4o-mini/gpt-5/gpt-5-mini entries were removed. const CODEX_MODELS = ['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna']; + const providerForRef = (ref) => !ref || ref === 'auto' ? null : ref.startsWith('compat:') ? 'compat' + : ref.startsWith('ollama:') ? 'ollama' : !ref.includes(':') ? 'codex' : null; + const savedProviderEnabled = (provider) => llmStatus.value?.[provider === 'compat' ? 'openai_compatible' : provider]?.enabled === true; + const codexOnly = computed(() => savedProviderEnabled('codex') && !savedProviderEnabled('compat') && !savedProviderEnabled('ollama')); + const configuredDisabledSelection = (role) => { + const ref = role === 'main' ? modelSelection.value.main : role === 'agent' ? agentsConfig.value.model : auxForm.value.enabled ? auxForm.value.model : ''; + return configuredDisabledRef(ref); + }; + const configuredDisabledRef = (ref) => { + const provider = providerForRef(ref); + return Boolean(provider && llmStatus.value?.[provider === 'compat' ? 'openai_compatible' : provider]?.enabled === false); + }; + const configuredDisabledLabel = (role) => { + const ref = role === 'main' ? modelSelection.value.main : role === 'agent' ? agentsConfig.value.model : auxForm.value.model; + return `Configured: ${modelCatalog.value.find(model => model.ref === ref)?.name || ref.replace(/^(compat|ollama):/, '')} (provider disabled)`; + }; + const disableWarningRoles = (provider) => { + const roles = []; + const mainProvider = providerForRef(llmStatus.value?.main_model || modelSelection.value.main); + if (mainProvider === provider) roles.push('Main'); + if ((agentsConfig.value.model === '' ? mainProvider : providerForRef(agentsConfig.value.model)) === provider) roles.push('Agent'); + const auxiliary = llmStatus.value?.auxiliary || auxForm.value; + if (auxiliary.enabled && providerForRef(auxiliary.model) === provider) roles.push('Auxiliary'); + if (agentsConfig.value.model === 'auto' && effectiveAllowlist.value + .filter(ref => providerForRef(ref) !== provider) + .map(ref => modelCatalog.value.find(model => model.ref === ref)) + .filter(model => model && agentModelAvailable(model) + && !(model.provider === 'compat' && openRouterRecognized.value && !model.ref.slice(7).includes('/'))).length === 0) { + roles.push('Agent Auto (zero effective choices)'); + } + return roles; + }; + async function confirmProviderDisable(provider, submittedEnabled) { + if (submittedEnabled !== false || !savedProviderEnabled(provider)) return true; + const roles = disableWarningRoles(provider); + const name = provider === 'compat' ? 'OpenAI-compatible' : provider === 'codex' ? 'Codex' : 'Ollama'; + return confirmDialog({ title: `Disable ${name}?`, + message: `${roles.length ? `This provider is used by ${roles.join(', ')}. ` : ''}Saved selections and ranked allowlist entries remain configured, not replaced. Continue?`, + confirmLabel: 'Disable provider', danger: true }); + } const modelCatalog = computed(() => { const status = llmStatus.value || {}; const providerModels = status.model_catalogue || status.model_catalog || {}; @@ -834,57 +884,78 @@ export default { ...(providerModels.codex || fallback('codex', CODEX_MODELS, status.codex)), ...(providerModels.compat || providerModels.openai_compatible || []), ...(providerModels.ollama || fallback('ollama', ollamaModels.value, status.ollama)), - ].map(entry => typeof entry === 'string' ? { ref: entry, name: entry, provider: 'codex', available: true, capability: 'reasoning' } : entry); + ].map(entry => typeof entry === 'string' + ? { ref: entry, name: entry, provider: 'codex', available: savedProviderEnabled('codex'), capability: 'reasoning' } + : { ...entry, available: savedProviderEnabled(entry.provider) && entry.available, + unavailable_reason: !savedProviderEnabled(entry.provider) ? 'disabled' : entry.unavailable_reason }); const mergeCompatible = (entry) => { const index = catalogue.findIndex(model => model.ref === entry.ref); - if (index === -1) catalogue.push(entry); else catalogue[index] = { ...catalogue[index], ...entry }; + if (index === -1) catalogue.push(entry); + else { + const serverVerdict = catalogue[index]; + // Keep the old presentation precedence, but discovery cannot upgrade admission. + catalogue[index] = { ...serverVerdict, ...entry, + available: serverVerdict.available, + unavailable_reason: serverVerdict.unavailable_reason, + agent_available: serverVerdict.agent_available }; + } }; for (const entry of fallback('compat', compatibleModels.value, status.openai_compatible)) mergeCompatible(entry); - if (openRouterRecognized.value && openRouterModels.value.length) { + if (openRouterCatalogueActive.value && openRouterModels.value.length) { for (const model of openRouterModels.value) { const ref = `compat:${model.id}`; mergeCompatible({ ref, name: model.name || model.id, provider: 'compat', - available: true, - unavailable_reason: '', + // Discovery metadata never upgrades the server's availability verdict. + available: catalogue.find(entry => entry.ref === ref)?.available ?? false, + unavailable_reason: catalogue.find(entry => entry.ref === ref)?.available + ? '' : catalogue.find(entry => entry.ref === ref)?.unavailable_reason || 'unavailable', capability: model.supports_reasoning ? 'reasoning' : 'none', efforts: reasoningEfforts.filter(effort => model.supported_efforts?.includes(effort)), - agent_available: model.agent_eligible && Boolean(model.profile), + agent_available: Boolean(model.agent_eligible && model.profile && catalogue.find(entry => entry.ref === ref)?.agent_available === true), agent_unavailable_reason: model.agent_unavailable_reason || '', }); } } const known = new Set(catalogue.map(model => model.ref)); - for (const ref of [modelSelection.value.main, agentsConfig.value.model, ...effectiveAllowlist.value]) { - if (ref && ref !== 'auto' && !known.has(ref)) catalogue.unshift({ ref, name: ref.replace(/^(compat|ollama):/, ''), provider: ref.split(':')[0] || 'codex', available: false, unavailable_reason: 'unavailable', capability: ref.startsWith('ollama:') ? 'none' : ref.includes(':') ? 'none' : 'reasoning' }); + for (const ref of [modelSelection.value.main, agentsConfig.value.model, ...(auxForm.value.enabled ? [auxForm.value.model] : []), ...effectiveAllowlist.value]) { + if (ref && ref !== 'auto' && !known.has(ref) && providerForRef(ref)) { + const provider = providerForRef(ref); + catalogue.unshift({ ref, name: ref.replace(/^(compat|ollama):/, ''), provider, available: false, + unavailable_reason: savedProviderEnabled(provider) ? 'unavailable' : 'disabled', capability: provider === 'codex' ? 'reasoning' : 'none' }); + known.add(ref); + } } - return catalogue.filter(model => !openRouterRecognized.value || model.provider !== 'compat' || model.ref.slice(7).includes('/')) + return catalogue.filter(model => !openRouterRecognized.value || model.provider !== 'compat' || model.ref.slice(7).includes('/') + || [modelSelection.value.main, agentsConfig.value.model, auxForm.value.enabled && auxForm.value.model, ...effectiveAllowlist.value].includes(model.ref)) .map(model => model.efforts ? { ...model, efforts: reasoningEfforts.filter(effort => model.efforts.includes(effort) && !modelRejects(model.ref, effort)), } : model); }); + const selectedRefs = () => [modelSelection.value.main, agentsConfig.value.model, auxForm.value.enabled && auxForm.value.model]; const modelGroups = computed(() => [ ['codex', 'Codex'], ['compat', 'OpenAI-compatible'], ['ollama', 'Ollama'], - ].map(([id, label]) => ({ + ].filter(([id]) => savedProviderEnabled(id)).map(([id, label]) => ({ id, label, models: modelCatalog.value.filter(model => { - if (model.provider !== id) return false; - const query = modelSelectorSearch.value.trim().toLowerCase(); + if (model.provider !== id || (configuredDisabledRef(model.ref) && selectedRefs().includes(model.ref))) return false; + const query = codexOnly.value ? '' : modelSelectorSearch.value.trim().toLowerCase(); return !query || `${model.name} ${model.ref}`.toLowerCase().includes(query); }), })).filter(group => group.models.length)); - const agentModelAvailable = (model) => model.available && model.agent_available !== false; + const codexChoices = computed(() => modelGroups.value.find(group => group.id === 'codex')?.models || []); + const agentModelAvailable = (model) => savedProviderEnabled(model.provider) && model.available && model.agent_available !== false; const agentModelOptionLabel = (model) => { if (!model.available) return modelOptionLabel(model); return `${model.name}${model.agent_available === false ? ` (${model.agent_unavailable_reason || 'not agent-eligible'})` : ''}`; }; const autoAllowlistGroups = computed(() => [ ['codex', 'Codex'], ['compat', 'OpenAI-compatible'], ['ollama', 'Ollama'], - ].map(([id, label]) => ({ + ].filter(([id]) => savedProviderEnabled(id)).map(([id, label]) => ({ id, label, models: modelCatalog.value.filter(model => model.provider === id && !(id === 'compat' && openRouterRecognized.value) @@ -907,7 +978,8 @@ export default { return typeof entry === 'string' ? '' : entry?.[field] || ''; }; const autoAllowlistModels = computed(() => effectiveAllowlist.value.map(ref => allowlistModel(ref)) - .filter(model => model && agentModelAvailable(model))); + .filter(model => model && agentModelAvailable(model) + && !(model.provider === 'compat' && openRouterRecognized.value && !model.ref.slice(7).includes('/')))); const autoCapabilityKinds = computed(() => [...new Set(autoAllowlistModels.value.map(model => { if (model.capability !== 'reasoning') return model.capability || 'none'; return model.provider === 'codex' ? 'codex_reasoning' : 'compatible_reasoning'; @@ -1100,31 +1172,36 @@ export default { .map(entry => typeof entry === 'string' ? entry : { ...entry }) .filter(entry => allowlistEntryRef(entry))); const defaultAgentAllowlist = computed(() => { - if (!codexForm.value.enabled && compatibleForm.value.enabled) { - return compatibleForm.value.model ? [`compat:${compatibleForm.value.model}`] : []; + if (!savedProviderEnabled('codex') && savedProviderEnabled('compat')) { + const model = llmStatus.value?.openai_compatible?.model; + return model ? [`compat:${model}`] : []; } - if (!codexForm.value.enabled && !compatibleForm.value.enabled && ollamaForm.value.enabled) { - return ollamaForm.value.model ? [`ollama:${ollamaForm.value.model}`] : []; + if (!savedProviderEnabled('codex') && !savedProviderEnabled('compat') && savedProviderEnabled('ollama')) { + const model = llmStatus.value?.ollama?.model; + return model ? [`ollama:${model}`] : []; } - return CODEX_MODELS; + return savedProviderEnabled('codex') ? CODEX_MODELS : []; }); const effectiveAllowlist = computed(() => configuredAllowlistEntries.value.length ? configuredAllowlistEntries.value.map(allowlistEntryRef) : defaultAgentAllowlist.value); - const allowlistSummary = computed(() => agentsConfig.value.auto_model_allowlist?.length - ? `Allowlist: ${effectiveAllowlist.value.length} models` - : `Default: ${effectiveAllowlist.value.join(', ') || 'no available agent models'}`); + const effectiveAutoAllowlistCount = computed(() => autoAllowlistModels.value.length); + const allowlistSummary = computed(() => `${agentsConfig.value.auto_model_allowlist?.length ? 'Allowlist' : 'Default'}: ` + + `${effectiveAllowlist.value.length} configured, ${effectiveAutoAllowlistCount.value} effective models`); const removedAllowlistEntries = new Map(); function closeAllowlistModal() { allowlistModalOpen.value = false; cancelOpenRouterPending(); } const selectedUnavailableReason = (ref) => { + const provider = providerForRef(ref); + if (provider && !savedProviderEnabled(provider)) return 'provider disabled'; if (openRouterRecognized.value && ref.startsWith('compat:') && !ref.slice(7).includes('/')) { return 'OpenRouter requires a namespaced vendor/model ID; this is a direct-endpoint profile.'; } const model = modelCatalog.value.find(item => item.ref === ref); if (!model) return 'Model is absent from the current endpoint catalogue.'; - return agentModelAvailable(model) ? '' : model.agent_unavailable_reason || model.unavailable_reason || 'Not agent-eligible'; + return agentModelAvailable(model) && !(model.provider === 'compat' && openRouterRecognized.value && !ref.slice(7).includes('/')) + ? '' : model.agent_unavailable_reason || model.unavailable_reason || 'Not agent-eligible'; }; const selectedModelFacts = (ref) => { if (openRouterModelMap.value.has(ref)) return openRouterSelectedFacts(ref); @@ -1137,6 +1214,7 @@ export default { const openRouterCatalogue = ref(null); const openRouterCatalogueLoading = ref(false); const openRouterCatalogueError = ref(''); + let openRouterRequestSeq = 0; // Populate presentation profiles on catalogue arrival, before selection is // possible. Selection still persists the conservative route-derived profile // through the existing API; catalogue preview never overwrites operator data. @@ -1195,6 +1273,23 @@ export default { try { return new URL(compatibleForm.value.base_url).hostname; } catch { return ''; } }); const openRouterRecognized = computed(() => /(^|\.)openrouter\.ai$/i.test(compatibleHostname.value)); + const savedCompatibleEndpoint = computed(() => llmStatus.value?.openai_compatible?.base_url || ''); + const savedCompatibleIdentity = computed(() => { + try { return new URL(savedCompatibleEndpoint.value).href; } catch { return ''; } + }); + const draftCompatibleIdentity = computed(() => { + try { return new URL(compatibleForm.value.base_url).href; } catch { return ''; } + }); + const openRouterCatalogueActive = computed(() => savedProviderEnabled('compat') + && openRouterRecognized.value && Boolean(savedCompatibleIdentity.value) + && savedCompatibleIdentity.value === draftCompatibleIdentity.value); + watch(() => [openRouterCatalogueActive.value, draftCompatibleIdentity.value, savedCompatibleIdentity.value], () => { + ++openRouterRequestSeq; + openRouterCatalogue.value = null; + openRouterCatalogueError.value = ''; + openRouterCatalogueLoading.value = false; + cancelOpenRouterPending(); + }, { flush: 'sync' }); const compatibleCatalogueStatus = computed(() => { if (!openRouterRecognized.value) return compatibleModels.value.length ? `${compatibleModels.value.length} endpoint models loaded` @@ -1225,7 +1320,7 @@ export default { }); const openRouterResults = computed(() => openRouterMatches.value.slice(0, 100)); const openRouterMatchCount = computed(() => openRouterMatches.value.length); - const openRouterModelMap = computed(() => new Map(openRouterModels.value.map(model => [`compat:${model.id}`, model]))); + const openRouterModelMap = computed(() => new Map((openRouterCatalogueActive.value ? openRouterModels.value : []).map(model => [`compat:${model.id}`, model]))); const dollarsPerMillion = (value) => value == null ? 'n/a' : `$${(Number(value) * 1000000).toFixed(3)}/M`; const openRouterInlineFacts = (model) => [ model.vendor, @@ -1277,25 +1372,36 @@ export default { async function fetchAgentsConfig({ poll = false } = {}) { try { const data = await api.get('/api/agents/model'); - if (poll && hasUnsavedDraft()) return; + // Explicit Refresh is not permission to discard a newer Agent draft. + // Saved provider visibility updates through /api/llm/status independently. + if ((poll && hasUnsavedDraft()) || allowlistModalOpen.value || allowlistSaving.value + || differsFromClean('agents', agentsConfig.value)) return; agentsConfig.value = { ...agentsConfig.value, ...data }; cleanSnapshots.agents = fingerprint(agentsConfig.value); } catch { /* config remains unavailable */ } } async function fetchOpenRouterCatalogue() { - if (!openRouterRecognized.value) { + const hostname = compatibleHostname.value; + const savedEndpoint = savedCompatibleEndpoint.value; + const request = ++openRouterRequestSeq; + if (!openRouterCatalogueActive.value) { openRouterCatalogue.value = null; openRouterCatalogueError.value = ''; + openRouterCatalogueLoading.value = false; return; } openRouterCatalogueLoading.value = true; try { - openRouterCatalogue.value = await api.get('/api/openrouter/catalogue'); + const result = await api.get('/api/openrouter/catalogue'); + if (request !== openRouterRequestSeq || !openRouterCatalogueActive.value + || hostname !== compatibleHostname.value || savedEndpoint !== savedCompatibleEndpoint.value) return; + openRouterCatalogue.value = result; openRouterCatalogueError.value = ''; } catch (error) { + if (request !== openRouterRequestSeq || !openRouterCatalogueActive.value) return; openRouterCatalogueError.value = error.message || 'Failed to load OpenRouter catalogue'; } finally { - openRouterCatalogueLoading.value = false; + if (request === openRouterRequestSeq) openRouterCatalogueLoading.value = false; } } async function saveAgentsModel() { @@ -1613,21 +1719,20 @@ export default { async function fetchLLMStatus({ preserveBasic = false, preserveAdvanced = false, poll = false } = {}) { try { const data = await api.get('/api/llm/status'); - const preserveDraft = poll && hasUnsavedDraft(); llmStatus.value = data; llmStatusLoadFailed.value = false; - if (!preserveDraft) modelSelection.value.main = data.main_model || data.active_model || (data.active_provider === 'compat' ? `compat:${data.openai_compatible?.model || ''}` : data.active_provider === 'ollama' ? `ollama:${data.ollama?.model || ''}` : data.codex?.model || 'gpt-6-sol'); + if (!savingMainModel && !differsFromClean('mainModel', modelSelection.value.main)) modelSelection.value.main = data.main_model || data.active_model || (data.active_provider === 'compat' ? `compat:${data.openai_compatible?.model || ''}` : data.active_provider === 'ollama' ? `ollama:${data.ollama?.model || ''}` : data.codex?.model || 'gpt-6-sol'); // Never clobber a form that has a NEWER edit waiting in its debounce // timer — the stale refresh would get re-saved (last-write-lost). - if (!preserveDraft && data.codex && !saveCodexConfigDebounced.pending()) { - if (!preserveBasic) { + if (data.codex && !saveCodexConfigDebounced.pending()) { + if (!preserveBasic && !savingCodex.value && !differsFromClean('codexBasic', codexBasicPayload(codexForm.value))) { codexForm.value.enabled = data.codex.enabled; codexForm.value.model = data.codex.model || 'gpt-6-sol'; codexForm.value.reasoning_effort = data.codex.reasoning_effort || 'medium'; // null (inherit) maps to the '' select option codexForm.value.agent_reasoning_effort = data.codex.agent_reasoning_effort || ''; } - if (!preserveAdvanced) { + if (!preserveAdvanced && !savingCodex.value && !differsFromClean('codexAdvanced', codexAdvancedPayload(codexForm.value))) { codexForm.value.request_timeout_seconds = data.codex.request_timeout_seconds ?? codexForm.value.request_timeout_seconds; codexForm.value.stream_stall_timeout_seconds = data.codex.stream_stall_timeout_seconds ?? codexForm.value.stream_stall_timeout_seconds; codexForm.value.retry = { ...codexForm.value.retry, ...(data.codex.retry || {}) }; @@ -1639,27 +1744,27 @@ export default { } } } - if (!preserveDraft && data.ollama && !saveOllamaConfigDebounced.pending()) { - if (!preserveBasic) { + if (data.ollama && !saveOllamaConfigDebounced.pending()) { + if (!preserveBasic && !savingOllama.value && !differsFromClean('ollamaBasic', ollamaBasicPayload(ollamaForm.value))) { ollamaForm.value.enabled = data.ollama.enabled; ollamaForm.value.base_url = data.ollama.base_url || ''; ollamaForm.value.model = data.ollama.model || ''; ollamaForm.value.max_tokens = data.ollama.max_tokens || 4096; ollamaForm.value.num_ctx = data.ollama.num_ctx || 32768; } - if (!preserveAdvanced) ollamaForm.value.timeout = data.ollama.timeout ?? ollamaForm.value.timeout; + if (!preserveAdvanced && !savingOllama.value && !differsFromClean('ollamaAdvanced', ollamaAdvancedPayload(ollamaForm.value))) ollamaForm.value.timeout = data.ollama.timeout ?? ollamaForm.value.timeout; // Don't overwrite api_key from server (it's masked) } const compatible = data.openai_compatible; - if (!preserveDraft && compatible && !saveCompatibleConfigDebounced.pending()) { - if (!preserveBasic) { + if (compatible && !saveCompatibleConfigDebounced.pending()) { + if (!preserveBasic && !savingCompatible.value && !differsFromClean('compatibleBasic', openaiCompatibleBasicPayload(compatibleForm.value))) { compatibleForm.value.enabled = compatible.enabled; compatibleForm.value.base_url = compatible.base_url || compatibleForm.value.base_url; compatibleForm.value.model = compatible.model || compatibleForm.value.model; compatibleForm.value.preset = compatible.preset || compatibleForm.value.preset; compatibleForm.value.reasoning_effort = compatible.reasoning_effort || 'medium'; } - if (!preserveAdvanced) { + if (!preserveAdvanced && !savingCompatible.value && !differsFromClean('compatibleAdvanced', openaiCompatibleAdvancedPayload(compatibleForm.value))) { compatibleForm.value.request_timeout_seconds = compatible.request_timeout_seconds ?? compatibleForm.value.request_timeout_seconds; compatibleForm.value.stream_stall_timeout_seconds = compatible.stream_stall_timeout_seconds ?? compatibleForm.value.stream_stall_timeout_seconds; compatibleForm.value.model_profiles = compatible.model_profiles || compatibleForm.value.model_profiles; @@ -1667,16 +1772,16 @@ export default { compatibleForm.value.openrouter = { ...compatibleForm.value.openrouter, ...(compatible.openrouter || {}) }; } } - if (!preserveDraft) { + if (!savingMainModel && !differsFromClean('mainModel', modelSelection.value.main)) { if (modelSelection.value.main?.startsWith('compat:')) { modelSelection.value.main_capability = compatible?.reasoning_effort || 'medium'; } else if (!modelSelection.value.main?.startsWith('ollama:')) { modelSelection.value.main_capability = data.codex?.reasoning_effort || 'medium'; } } - if (!preserveDraft && data.auxiliary) { + if (data.auxiliary) { auxData.value = data.auxiliary; - if (!saveAuxConfigDebounced.pending()) { + if (!saveAuxConfigDebounced.pending() && !savingAux.value && !differsFromClean('auxiliary', auxForm.value)) { auxForm.value.enabled = data.auxiliary.enabled; auxForm.value.model = data.auxiliary.model || 'gpt-6-luna'; } @@ -1758,23 +1863,27 @@ export default { } async function saveMainModel() { + if (savingMainModel) return; + savingMainModel = true; + const submitted = modelSelection.value.main; try { - if (modelSelection.value.main.startsWith('compat:') && openRouterRecognized.value) { + if (submitted.startsWith('compat:') && openRouterRecognized.value) { await selectOpenRouterModel( - compatibleModelId(modelSelection.value.main), - openRouterPin(modelSelection.value.main), + compatibleModelId(submitted), + openRouterPin(submitted), ); } // The model-first endpoint is preferred. Older servers retain the // compatibility switch route, whose model field has the same meaning. - try { await api.put('/api/llm/main-model', { model: modelSelection.value.main }); } + try { await api.put('/api/llm/main-model', { model: submitted }); } catch (error) { if (!/404|not found/i.test(error.message || '')) throw error; - await api.post('/api/llm/switch', { model: modelSelection.value.main }); + await api.post('/api/llm/switch', { model: submitted }); } - markClean('mainModel', modelSelection.value.main); + markClean('mainModel', submitted); showToast('Main model saved'); await fetchAll(); } catch (e) { showToast(e.message || 'Failed to save main model', 'error'); await fetchLLMStatus(); } + finally { savingMainModel = false; } } async function saveMainCapability(value) { modelSelection.value.main_capability = value; @@ -1894,6 +2003,10 @@ export default { savingCodex.value = true; const submitted = codexBasicPayload(codexForm.value); try { + if (!await confirmProviderDisable('codex', submitted.enabled)) { + codexForm.value.enabled = llmStatus.value?.codex?.enabled === true; + return; + } await api.put('/api/llm/codex/config', submitted); markClean('codexBasic', submitted); showToast('Codex config saved'); @@ -1937,6 +2050,10 @@ export default { try { const sentKey = ollamaKeyDirty.value ? ollamaForm.value.api_key : null; const payload = ollamaBasicPayload(ollamaForm.value, { includeApiKey: sentKey !== null }); + if (!await confirmProviderDisable('ollama', payload.enabled)) { + ollamaForm.value.enabled = llmStatus.value?.ollama?.enabled === true; + return; + } await api.put('/api/llm/ollama/config', payload); showToast('Ollama config saved'); if (sentKey !== null && ollamaForm.value.api_key === sentKey) { @@ -1945,7 +2062,7 @@ export default { } markClean('ollamaBasic', ollamaBasicPayload(ollamaForm.value)); await Promise.all([fetchLLMStatus({ preserveBasic: true, preserveAdvanced: true }), fetchOllamaStatus()]); - } catch (e) { showToast(e.message || 'Failed', 'error'); } + } catch (e) { showToast(e.message || 'Failed', 'error'); await fetchLLMStatus({ preserveBasic: true, preserveAdvanced: true }); } finally { savingOllama.value = false; } } @@ -1968,6 +2085,10 @@ export default { try { const sentKey = compatibleKeyDirty.value ? compatibleForm.value.api_key : null; const payload = openaiCompatibleBasicPayload(compatibleForm.value, { includeApiKey: sentKey !== null }); + if (!await confirmProviderDisable('compat', payload.enabled)) { + compatibleForm.value.enabled = llmStatus.value?.openai_compatible?.enabled === true; + return; + } await api.put('/api/openai-compatible/config', payload); showToast('OpenAI-compatible config saved'); if (sentKey !== null && compatibleForm.value.api_key === sentKey) { @@ -1977,7 +2098,7 @@ export default { markClean('compatibleBasic', openaiCompatibleBasicPayload(compatibleForm.value)); await Promise.all([fetchLLMStatus({ preserveBasic: true, preserveAdvanced: true }), fetchCompatibleStatus()]); await fetchOpenRouterCatalogue(); - } catch (e) { showToast(e.message || 'Failed', 'error'); } + } catch (e) { showToast(e.message || 'Failed', 'error'); await fetchLLMStatus({ preserveBasic: true, preserveAdvanced: true }); } finally { savingCompatible.value = false; } } @@ -2156,6 +2277,7 @@ export default { return { pageRoot, + savedProviderEnabled, codexOnly, codexChoices, configuredDisabledSelection, configuredDisabledLabel, disableWarningRoles, effectiveAutoAllowlistCount, allowlistModalOpen, closeAllowlistModal, allowlistSaving, effectiveAllowlist, allowlistSummary, resetAgentAllowlist, selectedUnavailableReason, selectedModelFacts, loading, llmStatus, llmStatusLoadFailed, modelSelection, modelSelectorSearch, reasoningEfforts, neutralReasoningLevels, modelCatalog, modelGroups, selectedMainModel, selectedAgentModel, selectedAgentCapabilityValue, agentCapabilityKind, agentCapabilityEfforts, autoAllowlistModels, allowlistModel, allowlistModelEfforts, allowlistEntryCapabilityValue, modelOptionLabel, agentModelAvailable, agentModelOptionLabel, advancedOpen, codexForm, codexModelOptions, codexAgentModelOptions, @@ -2166,7 +2288,7 @@ export default { ollamaStatus, ollamaStatusLoadFailed, ollamaModels, ollamaSelectedModel, reloading, settingModel, compatibleStatus, compatibleStatusLoadFailed, compatibleModels, visibleCompatibleModels, compatibleSelectedModel, reloadingCompatible, settingCompatibleModel, applyCompatiblePreset, setOpenRouterList, agentsConfig, compatibleAgentModels, ollamaAgentModels, knownAgentModelRefs, agentModelLabel, saveAgentsModel, toggleAgentAutoAllowlist, saveAllowlistEntryCapability, autoAllowlistGroups, structuralFacts, saveModelHint, canMoveAllowlist, moveAgentAutoAllowlist, - openRouterCatalogue, openRouterCatalogueLoading, openRouterCatalogueError, openRouterRecognized, compatibleCatalogueStatus, compatibleCatalogueStatusClass, + openRouterCatalogue, openRouterCatalogueLoading, openRouterCatalogueError, openRouterRecognized, openRouterCatalogueActive, compatibleCatalogueStatus, compatibleCatalogueStatusClass, openRouterSearch, openRouterVendor, openRouterVendors, openRouterToolsOnly, openRouterEligibleOnly, openRouterStandardOnly, openRouterMeasuredCacheOnly, openRouterMaxPromptPrice, openRouterQuantization, openRouterQuantizations, openRouterResults, openRouterMatchCount, openRouterInlineFacts, openRouterSelectedFacts, prepareOpenRouterModel, addOpenRouterModel, removeOpenRouterModel, quickAddOpenRouter, openRouterModelMap, openRouterPin, openRouterPendingModel, openRouterPendingTag, openRouterPendingEndpoints, openRouterPendingLoading, openRouterEndpointSort, openRouterSortedPendingEndpoints, openRouterEndpointCacheFact, openRouterRate, openRouterMetric, openRouterRouteWarning, cancelOpenRouterPending,