diff --git a/config.yml b/config.yml index f4122701..5fac64d9 100644 --- a/config.yml +++ b/config.yml @@ -14,21 +14,21 @@ openai_codex: enabled: true # ChatGPT-account Codex backend only supports the GPT-5 family # (gpt-4o et al return "model not supported" via this auth path). - model: gpt-5.5 + model: gpt-5.6-sol # Reasoning effort sent with every main-provider request: # none | low | medium | high | xhigh | max (backend default: medium). # "max" is gpt-5.6-family only — config load rejects a known-incompatible # model/effort pair (e.g. gpt-5.5 + max) rather than 400 on every request. - reasoning_effort: medium + reasoning_effort: xhigh # Effort for spawned-agent iterations only. Unset/null = inherit # reasoning_effort ("none" is a real effort level, not inherit). Set to # "auto" to let the spawner choose the effort per spawn from the catalogue. # Read at call time — live changes reach in-flight agents next iteration. - agent_reasoning_effort: null + agent_reasoning_effort: auto # Model for spawned-agent iterations only. Unset/null = inherit model; a # specific model = fixed; "auto" = let the spawner choose per spawn. # Read at call time — live changes reach in-flight agents next iteration. - agent_model: null + agent_model: auto credentials_path: ./data/codex_auth.json # Streaming transport timeouts. request_timeout_seconds is a generous # whole-request backstop (long high-effort reasoning turns stream past @@ -46,15 +46,17 @@ openai_codex: keepalive_timeout: 30 context_compression: enabled: true - max_context_chars: 750000 + # null = auto: the ceiling derives from the active model's input budget + # (a number only lowers the derived target, never raises it) + max_context_chars: null keep_recent_iterations: 30 # Auxiliary model: when enabled, a cheaper Codex model runs the background # jobs (compaction, reflection, consolidation, background follow-up) with # automatic fallback to the primary model on error. It shares the main Codex # OAuth credentials; only the model differs. Editable live from the WebUI. auxiliary: - enabled: false - model: gpt-5.6-luna + enabled: true + model: gpt-5.6-terra ollama: enabled: false diff --git a/coverage-baseline.json b/coverage-baseline.json index 7d33c74d..7897b50c 100644 --- a/coverage-baseline.json +++ b/coverage-baseline.json @@ -65,11 +65,17 @@ "percent": 100.0, "statements": 30 }, + "src/config/migrations.py": { + "covered": 294, + "missing": 0, + "percent": 100.0, + "statements": 294 + }, "src/config/schema.py": { - "covered": 628, + "covered": 706, "missing": 1, - "percent": 99.84, - "statements": 629 + "percent": 99.86, + "statements": 707 }, "src/constants.py": { "covered": 17, @@ -413,6 +419,12 @@ "percent": 100.0, "statements": 12 }, + "src/llm/account_key.py": { + "covered": 115, + "missing": 0, + "percent": 100.0, + "statements": 115 + }, "src/llm/auxiliary.py": { "covered": 65, "missing": 0, @@ -437,6 +449,12 @@ "percent": 93.28, "statements": 402 }, + "src/llm/context_budget.py": { + "covered": 45, + "missing": 0, + "percent": 100.0, + "statements": 45 + }, "src/llm/context_compressor.py": { "covered": 165, "missing": 18, @@ -852,10 +870,10 @@ "statements": 45 }, "src/tools/autonomous_loop.py": { - "covered": 155, - "missing": 83, - "percent": 65.13, - "statements": 238 + "covered": 197, + "missing": 72, + "percent": 73.23, + "statements": 269 }, "src/tools/branch_freshness.py": { "covered": 99, @@ -1254,10 +1272,10 @@ "statements": 281 }, "src/web/api/llm_admin.py": { - "covered": 510, + "covered": 625, "missing": 3, - "percent": 99.42, - "statements": 513 + "percent": 99.52, + "statements": 628 }, "src/web/api/observability.py": { "covered": 216, @@ -1312,5 +1330,11 @@ "missing": 27, "percent": 88.98, "statements": 245 + }, + "src/llm/window_observer.py": { + "covered": 308, + "missing": 39, + "percent": 88.76, + "statements": 347 } -} \ No newline at end of file +} diff --git a/docs/configuration.md b/docs/configuration.md index a5294171..9d8ab306 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -69,10 +69,10 @@ tools: ```yaml openai_codex: enabled: true - model: gpt-5.5 # ChatGPT subscription path - reasoning_effort: medium # none | low | medium | high | xhigh | max - agent_reasoning_effort: null # spawned agents; null = inherit, "auto" = per-spawn choice - agent_model: null # spawned agents; null = inherit, "auto" = per-spawn choice + model: gpt-5.6-sol # ChatGPT subscription path + reasoning_effort: xhigh # none | low | medium | high | xhigh | max + agent_reasoning_effort: auto # spawned agents; "auto" = per-spawn choice, null = inherit + agent_model: auto # spawned agents; "auto" = per-spawn choice, null = inherit credentials_path: ./data/codex_auth.json request_timeout_seconds: 3600 # whole-request backstop; long reasoning turns stream past 10 min stream_stall_timeout_seconds: 180 # fail fast when no stream bytes arrive for this long @@ -82,13 +82,24 @@ openai_codex: max_delay: 30.0 context_compression: enabled: true - max_context_chars: 48000 + max_context_chars: null # null = auto (model-derived ceiling); a number only lowers it keep_recent_iterations: 3 + # Per-model usable-input-budget overrides (tokens, 50192-2000000). Empty = + # built-in known-safe floors. Consumed by the context-budget resolver. + context_budget_overrides: {} + # Working-set policy: percent of the effective budget compaction targets + # (30-100). Never reduces budgets at or below 272K tokens. + context_utilization: 60 auxiliary: # cheaper model for background jobs - enabled: false - model: gpt-5.6-luna + enabled: true + model: gpt-5.6-terra ``` +A persisted `max_context_chars: 750000` from the pre-campaign default is +migrated to auto once (a provenance marker under `data/` records it, and one +warning names the marker); saving the compression settings afterwards makes +any explicit value — including 750000 — stick permanently. + Reasoning effort `max` is served only by the gpt-5.6 family (sol/terra/luna); gpt-5.5 rejects it per-request. Odin refuses a known-incompatible model/effort pair everywhere it can be introduced — config load, the admin API, per-spawn diff --git a/scripts/check-config-center-ui2.mjs b/scripts/check-config-center-ui2.mjs index 776309b0..22b6c9c5 100644 --- a/scripts/check-config-center-ui2.mjs +++ b/scripts/check-config-center-ui2.mjs @@ -332,11 +332,13 @@ const providerForm = { retry: { max_retries: 9, base_delay: 4, max_delay: 40 }, connection_pool: { max_connections: 19, keepalive_timeout: 41 }, context_compression: { enabled: false, max_context_chars: 123456, keep_recent_iterations: 11 }, + context_budget_overrides: { 'gpt-5.6-sol': 800000 }, + context_utilization: 72, timeout: 777, }; const expectedPayloadKeys = new Map([ [codexBasicPayload, ['agent_model', 'agent_reasoning_effort', 'enabled', 'model', 'reasoning_effort']], - [codexAdvancedPayload, ['connection_pool', 'context_compression', 'request_timeout_seconds', 'retry', 'stream_stall_timeout_seconds']], + [codexAdvancedPayload, ['connection_pool', 'context_budget_overrides', 'context_compression', 'context_utilization', 'request_timeout_seconds', 'retry', 'stream_stall_timeout_seconds']], [ollamaBasicPayload, ['base_url', 'enabled', 'max_tokens', 'model']], [ollamaAdvancedPayload, ['timeout']], [kimiBasicPayload, ['enabled', 'max_tokens', 'model']], @@ -359,6 +361,15 @@ assert.match(llm, /saveCodexConfig\(\)[\s\S]*codexBasicPayload\(codexForm\.value assert.match(llm, /saveOllamaConfig\(\)[\s\S]*ollamaBasicPayload\(ollamaForm\.value/, 'Ollama basic auto-save does not use its field-only payload'); assert.match(llm, /saveKimiConfig\(\)[\s\S]*kimiBasicPayload\(kimiForm\.value/, 'Kimi basic auto-save does not use its field-only payload'); assert.match(llm, /saveCodexAdvancedConfig\(\)[\s\S]*codexAdvancedPayload\(codexForm\.value\)/, 'Codex explicit Advanced save does not use its field-only payload'); +assert.match(llm, /Context budgets<\/strong>/, 'Codex Advanced panel lost the Context budgets table'); +assert.match(llm, /api\.get\('\/api\/context\/windows'\)/, 'Context budgets do not load backend derivation truth'); +assert.match(llm, /api\.post\('\/api\/context\/windows\/clear'/, 'Context budgets lost account-scoped clamp clearing'); +assert.match(llm, /formatContextCeiling\(llmStatus\.codex\.effective_context_compression\?\.max_context_chars\)/, 'Context-compression status lost truthful automatic-ceiling formatting'); +assert.doesNotMatch(llm, /effective_context_compression\?\.max_context_chars\s*\|\|\s*0/, 'Automatic context ceiling regressed to 0 characters'); +assert.match(llm, /details\.effective\?\.effective_budget/, 'effective budget is recomputed or not data-bound'); +assert.match(llm, /details\.effective\?\.primary_chars/, 'resulting target is recomputed or not data-bound'); +assert.doesNotMatch(llm, /921601|917506|270001|262146|124001/, 'browser duplicated the backend context-budget catalog'); +assert.match(llm, /enabled: false, model: 'gpt-5\.6-sol', reasoning_effort: 'xhigh', agent_reasoning_effort: 'auto', agent_model: 'auto'/, 'LLM owner-page fallback defaults drifted from the schema'); assert.match(llm, /saveOllamaAdvancedConfig\(\)[\s\S]*ollamaAdvancedPayload\(ollamaForm\.value\)/, 'Ollama explicit Advanced save does not use its field-only payload'); assert.match(llm, /saveKimiAdvancedConfig\(\)[\s\S]*kimiAdvancedPayload\(kimiForm\.value\)/, 'Kimi explicit Advanced save does not use its field-only payload'); for (const provider of ['Codex', 'Ollama', 'Kimi']) { diff --git a/scripts/check-config-save-boundaries.mjs b/scripts/check-config-save-boundaries.mjs index 30933fb4..7a247ba0 100644 --- a/scripts/check-config-save-boundaries.mjs +++ b/scripts/check-config-save-boundaries.mjs @@ -34,6 +34,8 @@ const llmState = { retry: { max_retries: 3, base_delay: 1, max_delay: 30 }, connection_pool: { max_connections: 10, keepalive_timeout: 30 }, context_compression: { enabled: true, max_context_chars: 750000, keep_recent_iterations: 30 }, + effective_context_compression: { enabled: false, max_context_chars: null, keep_recent_iterations: 30 }, + context_compression_pending_restart: true, }, ollama: { configured: true, @@ -54,6 +56,18 @@ const llmState = { }, auxiliary: { enabled: false, model: 'gpt-5.6-luna' }, }; +const contextWindowsState = { + utilization: 60, + max_context_chars: null, + models: { + 'gpt-5.6-sol': { + floor: 921601, override: null, active_clamp: null, provenance: 'built-in', clamp_expires_at: null, + configured: { effective_budget: 921601, primary_chars: 1277400 }, + effective: { effective_budget: 921601, primary_chars: 1277400 }, + }, + }, + clamps: [{ account_key: 'a'.repeat(32), model: 'gpt-5.6-sol', value: 300000, expires_at: '2026-08-19T12:00:00Z' }], evidence: { version: 1, accounts: {} }, +}; const globalConfig = { discord: { allowed_users: ['441'], @@ -107,7 +121,9 @@ globalThis.fetch = async (path, options = {}) => { return response({ error: 'injected save failure' }, failureStatus); } + if (method === 'POST' && path === '/api/context/windows/clear') return response({ cleared: 1 }); if (path === '/api/llm/status') return response(llmState); + if (path === '/api/context/windows') return response(contextWindowsState); if (path === '/api/codex/status') return response({ configured: true, accounts: [] }); if (path === '/api/ollama/status') return response({ configured: true, model: llmState.ollama.model, health: { healthy: true } }); if (path === '/api/ollama/models') return response({ active_model: llmState.ollama.model, models: [{ name: 'llama3', size: 10 }, { name: 'qwen', size: 20 }] }); @@ -115,6 +131,12 @@ globalThis.fetch = async (path, options = {}) => { if (path === '/api/kimi/models') return response({ models: ['kimi-k2', 'kimi-next'] }); if (method === 'PUT' && /^\/api\/llm\/(codex|ollama|kimi)\/config$/.test(path)) { Object.assign(providerConfig(path), body); + if (path.includes('/codex/')) { + if ('context_budget_overrides' in body) { + contextWindowsState.models['gpt-5.6-sol'].override = body.context_budget_overrides['gpt-5.6-sol'] ?? null; + } + if ('context_utilization' in body) contextWindowsState.utilization = body.context_utilization; + } return response({ status: 'updated' }); } if (path === '/api/discord/guilds') { @@ -152,8 +174,49 @@ console.warn = message => { }; const { default: LLMConfigPage } = await import('../ui/js/pages/llm-config.js'); +assert.match( + LLMConfigPage.template, + /formatContextCeiling\(llmStatus\.codex\.effective_context_compression\?\.max_context_chars\)/, + 'pending-restart template does not consume the truthful ceiling formatter', +); +assert.doesNotMatch( + LLMConfigPage.template, + /effective_context_compression\?\.max_context_chars\s*\|\|\s*0/, + 'pending-restart template renders automatic context as zero characters', +); const llm = LLMConfigPage.setup(); await llm.fetchAll(); +assert.equal(llm.contextBudgetRows.value[0].primaryChars, 1277400, 'Context target did not come from GET /api/context/windows'); +assert.equal(llm.formatContextCeiling(null), 'automatic (model-derived)', 'automatic runtime ceiling rendered as a numeric zero'); +assert.equal(llm.formatContextCeiling(500000), '500,000 characters', 'explicit runtime ceiling lost its unit/value'); +const lateContextRefresh = defer('GET /api/context/windows'); +const contextRefresh = llm.fetchContextWindows(); +await Promise.resolve(); +llm.setContextOverride('gpt-5.6-sol', { target: { value: '800000' } }); +llm.setContextUtilization({ target: { value: '72' } }); +lateContextRefresh.resolve(); +await contextRefresh; +assert.equal(llm.codexForm.value.context_budget_overrides['gpt-5.6-sol'], 800000, 'late context-window GET erased an unsaved override'); +assert.equal(llm.codexForm.value.context_utilization, 72, 'late context-window GET erased unsaved utilization'); +contextWindowsState.models['gpt-5.6-sol'].effective.primary_chars = 111111; +const olderWindows = defer('GET /api/context/windows'); +const olderWindowsRequest = llm.fetchContextWindows(); +await Promise.resolve(); +const newerWindowsRequest = llm.fetchContextWindows(); +await newerWindowsRequest; +assert.equal(llm.contextBudgetRows.value[0].primaryChars, 111111, 'newest context-window response did not render'); +contextWindowsState.models['gpt-5.6-sol'].effective.primary_chars = 222222; +olderWindows.resolve(); +await olderWindowsRequest; +assert.equal(llm.contextBudgetRows.value[0].primaryChars, 111111, 'older context-window response overwrote newer derivation truth'); +contextWindowsState.models['gpt-5.6-sol'].effective.primary_chars = 1277400; +const clearBefore = requests.length; +await llm.clearContextClamp(contextWindowsState.clamps[0]); +const clearRequest = requests.slice(clearBefore).find(request => request.method === 'POST' && request.path === '/api/context/windows/clear'); +assert.deepEqual(clearRequest?.body, { account_key: 'a'.repeat(32), model: 'gpt-5.6-sol' }, 'clamp clear lost account/model scope'); +assert.equal(requests.slice(clearBefore).some(request => request.method === 'GET' && request.path === '/api/context/windows'), true, 'clamp clear did not refresh derivation truth'); +assert.equal(llm.codexForm.value.context_budget_overrides['gpt-5.6-sol'], 800000, 'clamp clear erased an unsaved override'); +assert.equal(llm.codexForm.value.context_utilization, 72, 'clamp clear erased unsaved utilization'); const providerCases = [ { @@ -162,7 +225,7 @@ const providerCases = [ changeBasic: () => { llm.codexForm.value.model = 'gpt-5.6-sol'; }, save: llm.saveCodexConfig, saveAdvanced: llm.saveCodexAdvancedConfig, - advancedKeys: ['request_timeout_seconds', 'stream_stall_timeout_seconds', 'retry', 'connection_pool', 'context_compression'], + advancedKeys: ['request_timeout_seconds', 'stream_stall_timeout_seconds', 'retry', 'connection_pool', 'context_compression', 'context_budget_overrides', 'context_utilization'], serverAdvanced: () => llmState.codex.request_timeout_seconds, draftAdvanced: () => llm.codexForm.value.request_timeout_seconds, oldValue: 3600, @@ -222,6 +285,18 @@ for (const testCase of providerCases) { ); assert.equal(testCase.serverAdvanced(), testCase.draftValue, `${testCase.name} explicit Advanced save did not update server state`); } +assert.equal(llmState.codex.context_budget_overrides['gpt-5.6-sol'], 800000, 'Context override was not persisted by the Advanced save'); +assert.equal(llmState.codex.context_utilization, 72, 'Context utilization was not persisted by the Advanced save'); +assert.equal(llm.contextPolicyDirty.value, false, 'successful unchanged Advanced save did not clear context-policy dirty state'); +const postSaveRefresh = defer('GET /api/context/windows'); +const postSaveRequest = llm.fetchContextWindows(); +await Promise.resolve(); +llm.setContextOverride('gpt-5.6-sol', { target: { value: '810000' } }); +llm.setContextUtilization({ target: { value: '73' } }); +postSaveRefresh.resolve(); +await postSaveRequest; +assert.equal(llm.codexForm.value.context_budget_overrides['gpt-5.6-sol'], 810000, 'late post-save GET erased a newer override'); +assert.equal(llm.codexForm.value.context_utilization, 73, 'late post-save GET erased newer utilization'); // A response that finishes after a newer edit must not repopulate either axis diff --git a/src/agents/loop_bridge.py b/src/agents/loop_bridge.py index 89a20436..67045e95 100644 --- a/src/agents/loop_bridge.py +++ b/src/agents/loop_bridge.py @@ -10,6 +10,7 @@ - Tracks which agents belong to which loop - Collects agent results back into loop iteration context """ + from __future__ import annotations import time @@ -43,6 +44,7 @@ @dataclass class LoopAgentRecord: """Tracks an agent spawned from a loop iteration.""" + agent_id: str loop_id: str iteration: int @@ -99,6 +101,9 @@ def spawn_agents_for_loop( context_compression_enabled: bool = False, max_context_chars: int = 750000, keep_recent_iterations: int = 30, + budget_snapshot_provider_factory=None, + generation_plan_provider_factory=None, + evidence_recorder=None, ) -> list[str]: """Spawn agents for a loop iteration. @@ -165,10 +170,7 @@ async def _bound_tool_cb( _self_id: dict = _self_id, _shared_cb=tool_executor_callback, ) -> str: - if ( - tool_name == "spawn_agent" - and _self_id["id"] - ): + if tool_name == "spawn_agent" and _self_id["id"]: # Invocation ancestry is authoritative. A nested model may # not choose a sibling/root as parent and escape this # agent's depth, child, or lifetime-tree limits. @@ -202,6 +204,20 @@ async def _bound_tool_cb( context_compression_enabled=context_compression_enabled, max_context_chars=max_context_chars, keep_recent_iterations=keep_recent_iterations, + # Per-task like the iteration callback: each agent's budget + # follows ITS OWN effective model, so a mixed-model fleet + # compacts each member against the right window. + budget_snapshot_provider=( + budget_snapshot_provider_factory(model_override) + if budget_snapshot_provider_factory is not None + else None + ), + generation_plan_provider=( + generation_plan_provider_factory(model_override, effort_override) + if generation_plan_provider_factory is not None + else None + ), + evidence_recorder=evidence_recorder, ) if not agent_id.startswith("Error"): @@ -218,7 +234,10 @@ async def _bound_tool_cb( ) log.info( "Loop %s iter %d spawned agent %s (%s)", - loop_id, iteration, agent_id, label, + loop_id, + iteration, + agent_id, + label, ) results.append(agent_id) @@ -243,7 +262,8 @@ async def wait_and_collect( return {} results = await self._agent_manager.wait_for_agents( - agent_ids, timeout=timeout, + agent_ids, + timeout=timeout, ) # Mark collected @@ -284,12 +304,14 @@ def get_active_loop_agents(self, loop_id: str) -> list[dict]: continue agent_results = self._agent_manager.get_results(r.agent_id) if agent_results: - active.append({ - "agent_id": r.agent_id, - "label": r.label, - "iteration": r.iteration, - "status": agent_results.get("status", "unknown"), - }) + active.append( + { + "agent_id": r.agent_id, + "label": r.label, + "iteration": r.iteration, + "status": agent_results.get("status", "unknown"), + } + ) return active @property diff --git a/src/agents/manager.py b/src/agents/manager.py index 96417e32..1f70855c 100644 --- a/src/agents/manager.py +++ b/src/agents/manager.py @@ -4,6 +4,7 @@ isolated message history, and full tool access. Agents may spawn sub-agents up to a configurable nesting depth (default 2). """ + from __future__ import annotations import asyncio @@ -14,6 +15,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from enum import Enum +from typing import Protocol from ..error_presentation import format_user_facing_error from ..llm.secret_scrubber import scrub_output_secrets @@ -23,43 +25,43 @@ log = get_logger("agents") # --- Constants --- -MAX_CONCURRENT_AGENTS = 5 # per channel -MAX_AGENT_LIFETIME = 3600 # 1 hour -MAX_AGENT_ITERATIONS = 120 # LLM turns per agent (default, overridable via config/spawn) -STALE_WARN_SECONDS = 120 # 2 min no activity → log warning -CLEANUP_DELAY = 300 # 5 min after terminal state → remove -WAIT_DEFAULT_TIMEOUT = 300 # default timeout for wait_for_agents -WAIT_POLL_INTERVAL = 2 # poll interval for wait_for_agents -ITERATION_CB_TIMEOUT = 120 # 2 min timeout per LLM call -TOOL_EXEC_TIMEOUT = 300 # 5 min timeout per tool execution +MAX_CONCURRENT_AGENTS = 5 # per channel +MAX_AGENT_LIFETIME = 3600 # 1 hour +MAX_AGENT_ITERATIONS = 120 # LLM turns per agent (default, overridable via config/spawn) +STALE_WARN_SECONDS = 120 # 2 min no activity → log warning +CLEANUP_DELAY = 300 # 5 min after terminal state → remove +WAIT_DEFAULT_TIMEOUT = 300 # default timeout for wait_for_agents +WAIT_POLL_INTERVAL = 2 # poll interval for wait_for_agents +ITERATION_CB_TIMEOUT = 120 # 2 min timeout per LLM call +TOOL_EXEC_TIMEOUT = 300 # 5 min timeout per tool execution # (The manager-level MAX_RECOVERY_ATTEMPTS retry ladder was removed # 2026-07-30: transient-failure recovery now lives inside the iteration # callback via src/llm/recovery.py. AgentInfo.recovery_attempts remains for # API/trajectory shape compatibility and stays 0.) -MAX_NESTING_DEPTH = 2 # default max sub-agent depth (root=0) -MAX_CHILDREN_PER_AGENT = 3 # fallback direct-child limit (config overrides at spawn) -TREE_MAX_AGENTS = 25 # hard ceiling on agents in one tree's lifetime — - # breadth x depth must never compound into a - # geometric invoice, whatever config says - -# --- Agent context-overflow recovery (design settled with Odin, 2026-08-09) --- -# Served context window on the Codex/ChatGPT path, probed 2026-08-09 from -# /backend-api/codex/models: uniform 272K across sol/terra/luna/5.5 (luna -# served 372K in July and was re-tiered since — treat as observation, not -# spec). System prompt, response, and reasoning ride OUTSIDE agent.messages, -# hence the reserve; the remainder converts at a deliberately DENSE -# chars-per-token so a char measure can never overshoot real tokens on -# scraped content. Private constants, never config: a raisable knob would -# resurrect the overflow class this recovery exists to close. -_SERVED_CONTEXT_WINDOW_TOKENS = 272_000 -_EMERGENCY_RESERVE_TOKENS = 42_000 -_EMERGENCY_CHARS_PER_TOKEN = 2.5 -_EMERGENCY_TARGET_CHARS = int( - (_SERVED_CONTEXT_WINDOW_TOKENS - _EMERGENCY_RESERVE_TOKENS) - * _EMERGENCY_CHARS_PER_TOKEN -) -_EMERGENCY_TARGET_CHARS_AGGRESSIVE = 400_000 -_EMERGENCY_TARGETS = (_EMERGENCY_TARGET_CHARS, _EMERGENCY_TARGET_CHARS_AGGRESSIVE) +MAX_NESTING_DEPTH = 2 # default max sub-agent depth (root=0) +MAX_CHILDREN_PER_AGENT = 3 # fallback direct-child limit (config overrides at spawn) +TREE_MAX_AGENTS = 25 # hard ceiling on agents in one tree's lifetime — +# breadth x depth must never compound into a +# geometric invoice, whatever config says + +# --- Agent context-overflow recovery (design settled with Odin, 2026-08-09; +# per-model budgets since the context-budget campaign, 2026-08-17) --- +# Targets and rescue ladders come from the shared per-model resolver +# (src/llm/context_budget.py): each logical generation resolves the +# EFFECTIVE agent model's snapshot via the spawn-provided callback, so a +# sol-class agent works a sol-class budget while gpt-5.5 keeps the proven +# 272K-class math. When no provider is wired (legacy/direct construction, +# non-codex paths) the unknown-model snapshot reproduces the pre-campaign +# conservative budget behavior. The old private constants are gone: their +# "never config" rationale was retired by this recovery machinery itself — +# wrong-high is one rejected request plus an in-flight rescue, not a +# terminal failure. + + +def _fallback_budget_snapshot(): + from ..llm.context_budget import resolve_context_budget + + return resolve_context_budget(None) def _is_context_overflow(exc: BaseException) -> bool: @@ -67,20 +69,21 @@ def _is_context_overflow(exc: BaseException) -> bool: from ..llm.errors import LLMRequestError return ( - isinstance(exc, LLMRequestError) - and getattr(exc, "code", None) == "context_length_exceeded" + isinstance(exc, LLMRequestError) and getattr(exc, "code", None) == "context_length_exceeded" ) # Agent-management tools — allowed or blocked based on nesting depth -AGENT_MANAGEMENT_TOOLS = frozenset({ - "spawn_agent", - "send_to_agent", - "list_agents", - "kill_agent", - "get_agent_results", - "wait_for_agents", -}) +AGENT_MANAGEMENT_TOOLS = frozenset( + { + "spawn_agent", + "send_to_agent", + "list_agents", + "kill_agent", + "get_agent_results", + "wait_for_agents", + } +) # Legacy alias for backward compatibility AGENT_BLOCKED_TOOLS = AGENT_MANAGEMENT_TOOLS @@ -106,6 +109,7 @@ def filter_agent_tools( class AgentState(str, Enum): # noqa: UP042 — str(member) output differs under StrEnum; deferred to a typed-verification pass """Typed lifecycle states for agent workers.""" + SPAWNING = "spawning" READY = "ready" EXECUTING = "executing" @@ -116,34 +120,59 @@ class AgentState(str, Enum): # noqa: UP042 — str(member) output differs under KILLED = "killed" -TERMINAL_STATES = frozenset({ - AgentState.COMPLETED, AgentState.FAILED, - AgentState.TIMEOUT, AgentState.KILLED, -}) +TERMINAL_STATES = frozenset( + { + AgentState.COMPLETED, + AgentState.FAILED, + AgentState.TIMEOUT, + AgentState.KILLED, + } +) -ACTIVE_STATES = frozenset({ - AgentState.SPAWNING, AgentState.READY, - AgentState.EXECUTING, AgentState.RECOVERING, -}) +ACTIVE_STATES = frozenset( + { + AgentState.SPAWNING, + AgentState.READY, + AgentState.EXECUTING, + AgentState.RECOVERING, + } +) VALID_TRANSITIONS: dict[AgentState, frozenset[AgentState]] = { - AgentState.SPAWNING: frozenset({ - AgentState.READY, AgentState.KILLED, - AgentState.FAILED, AgentState.TIMEOUT, - }), - AgentState.READY: frozenset({ - AgentState.EXECUTING, AgentState.COMPLETED, - AgentState.KILLED, AgentState.TIMEOUT, - }), - AgentState.EXECUTING: frozenset({ - AgentState.READY, AgentState.RECOVERING, - AgentState.COMPLETED, AgentState.FAILED, - AgentState.KILLED, AgentState.TIMEOUT, - }), - AgentState.RECOVERING: frozenset({ - AgentState.EXECUTING, AgentState.FAILED, - AgentState.KILLED, AgentState.TIMEOUT, - }), + AgentState.SPAWNING: frozenset( + { + AgentState.READY, + AgentState.KILLED, + AgentState.FAILED, + AgentState.TIMEOUT, + } + ), + AgentState.READY: frozenset( + { + AgentState.EXECUTING, + AgentState.COMPLETED, + AgentState.KILLED, + AgentState.TIMEOUT, + } + ), + AgentState.EXECUTING: frozenset( + { + AgentState.READY, + AgentState.RECOVERING, + AgentState.COMPLETED, + AgentState.FAILED, + AgentState.KILLED, + AgentState.TIMEOUT, + } + ), + AgentState.RECOVERING: frozenset( + { + AgentState.EXECUTING, + AgentState.FAILED, + AgentState.KILLED, + AgentState.TIMEOUT, + } + ), AgentState.COMPLETED: frozenset(), AgentState.FAILED: frozenset(), AgentState.TIMEOUT: frozenset(), @@ -167,17 +196,17 @@ class AgentState(str, Enum): # noqa: UP042 — str(member) output differs under class InvalidStateTransition(Exception): # noqa: N818 — established public exception name; rename is an API break """Raised when an invalid state transition is attempted.""" + def __init__(self, from_state: AgentState, to_state: AgentState) -> None: self.from_state = from_state self.to_state = to_state - super().__init__( - f"Invalid state transition: {from_state.value} → {to_state.value}" - ) + super().__init__(f"Invalid state transition: {from_state.value} → {to_state.value}") @dataclass class StateTransition: """Record of a single state transition.""" + from_state: AgentState to_state: AgentState timestamp: float @@ -255,12 +284,24 @@ def history_as_dicts(self) -> list[dict]: # Callback types -# iteration_callback: (messages, system_prompt, tools) → LLMResponse-like dict -# dict with keys: "text" (str), "tool_calls" (list[dict]), "stop_reason" (str) -IterationCallback = Callable[ - [list[dict], str, list[dict]], - Awaitable[dict], -] +class IterationCallback(Protocol): + """Required callback contract for one logical agent generation. + + ``generation_state`` is a manager-owned, per-generation channel reused by + every physical attempt and emergency rescue retry. Three-argument + callbacks are no longer supported: silently omitting this channel would + make request identity and context-budget snapshots impossible to freeze. + """ + + def __call__( + self, + messages: list[dict], + sys_prompt: str, + tool_defs: list[dict], + *, + generation_state: dict, + ) -> Awaitable[dict]: ... + # tool_executor_callback: (tool_name, tool_input) → result string ToolExecutorCallback = Callable[ @@ -279,6 +320,7 @@ def history_as_dicts(self) -> list[dict]: @dataclass class AgentInfo: """Metadata and state for a running agent.""" + id: str label: str goal: str @@ -369,8 +411,10 @@ def transition(self, to: AgentState, reason: str = "") -> StateTransition: record = self._sm.transition(to, reason) log.debug( "Agent %s (%s): %s → %s%s", - self.id, self.label, - record.from_state.value, record.to_state.value, + self.id, + self.label, + record.from_state.value, + record.to_state.value, f" ({reason})" if reason else "", ) return record @@ -423,8 +467,20 @@ def spawn( context_compression_enabled: bool = False, max_context_chars: int = 750000, keep_recent_iterations: int = 30, + budget_snapshot_provider: Callable | None = None, + generation_plan_provider: Callable | None = None, + evidence_recorder: Callable | None = None, ) -> str: - """Spawn a new agent. Returns agent_id on success, or 'Error: ...' string.""" + """Spawn a new agent. Returns agent_id on success, or 'Error: ...' string. + + ``generation_plan_provider`` captures the authoritative serving + identity and ContextBudgetSnapshot once before pre-send compaction; + that exact plan is threaded through every physical attempt and rescue. + ``budget_snapshot_provider`` remains a compatibility fallback for + standalone callers that do not own a full serving identity. The + iteration callback must implement the required ``generation_state=`` + channel. + """ # Check the live per-channel admission limit. Existing agents are # never evicted when the setting falls; only subsequent spawns see it. configured_limit = ( @@ -433,18 +489,14 @@ def spawn( else None ) concurrent_limit = ( - configured_limit - if configured_limit is not None - else MAX_CONCURRENT_AGENTS + configured_limit if configured_limit is not None else MAX_CONCURRENT_AGENTS ) channel_count = sum( - 1 for a in self._agents.values() - if a.channel_id == channel_id and a._sm.is_active + 1 for a in self._agents.values() if a.channel_id == channel_id and a._sm.is_active ) if channel_count >= concurrent_limit: return ( - f"Error: Maximum concurrent agents ({concurrent_limit}) " - "reached for this channel." + f"Error: Maximum concurrent agents ({concurrent_limit}) reached for this channel." ) if not label or not goal: @@ -549,9 +601,7 @@ def spawn( ) # Filter tools based on depth - filtered_tools = filter_agent_tools( - tools or [], depth=depth, max_depth=agent.max_depth - ) + filtered_tools = filter_agent_tools(tools or [], depth=depth, max_depth=agent.max_depth) # Seed messages with the goal agent.messages = [{"role": "user", "content": goal}] @@ -575,19 +625,25 @@ def spawn( context_compression_enabled=context_compression_enabled, max_context_chars=max_context_chars, keep_recent_iterations=keep_recent_iterations, + budget_snapshot_provider=budget_snapshot_provider, + generation_plan_provider=generation_plan_provider, + evidence_recorder=evidence_recorder, ) ) agent._task = task # Schedule cleanup when the agent task finishes (any exit path) task.add_done_callback(lambda _t: self._schedule_cleanup(agent_id)) self._agents[agent_id] = agent - self._tree_spawn_counts[agent.root_id] = ( - self._tree_spawn_counts.get(agent.root_id, 0) + 1 - ) + self._tree_spawn_counts[agent.root_id] = self._tree_spawn_counts.get(agent.root_id, 0) + 1 log.info( "Spawned agent %s (%s) depth=%d for channel %s by %s: %s", - agent_id, label, depth, channel_id, requester_name, goal[:100], + agent_id, + label, + depth, + channel_id, + requester_name, + goal[:100], ) return agent_id @@ -612,19 +668,21 @@ def list(self, channel_id: str | None = None) -> list[dict]: if channel_id and agent.channel_id != channel_id: continue runtime = (agent.ended_at or time.time()) - agent.created_at - result.append({ - "id": agent.id, - "label": agent.label, - "status": agent.status, - "state": agent.state.value, - "iteration_count": agent.iteration_count, - "runtime_seconds": round(runtime, 1), - "tools_used": len(agent.tools_used), - "goal": agent.goal[:100], - "depth": agent.depth, - "parent_id": agent.parent_id, - "children_count": len(agent.children_ids), - }) + result.append( + { + "id": agent.id, + "label": agent.label, + "status": agent.status, + "state": agent.state.value, + "iteration_count": agent.iteration_count, + "runtime_seconds": round(runtime, 1), + "tools_used": len(agent.tools_used), + "goal": agent.goal[:100], + "depth": agent.depth, + "parent_id": agent.parent_id, + "children_count": len(agent.children_ids), + } + ) return result @staticmethod @@ -663,14 +721,13 @@ def kill(self, agent_id: str, cascade: bool = True) -> str: log.info( "Kill signal sent to agent %s (%s) and %d descendants", - agent_id, agent.label, len(killed_ids) - 1, + agent_id, + agent.label, + len(killed_ids) - 1, ) if len(killed_ids) == 1: return f"Kill signal sent to agent '{agent.label}'." - return ( - f"Kill signal sent to agent '{agent.label}' " - f"and {len(killed_ids) - 1} descendant(s)." - ) + return f"Kill signal sent to agent '{agent.label}' and {len(killed_ids) - 1} descendant(s)." def get_results(self, agent_id: str) -> dict | None: """Get structured results of an agent.""" @@ -795,13 +852,12 @@ async def wait_for_agents( "error": f"Agent '{aid}' not found.", } - still_running = [ - aid for aid, r in results.items() if r.get("status") == "running" - ] + still_running = [aid for aid, r in results.items() if r.get("status") == "running"] if still_running: log.warning( "wait_for_agents timed out with %d still running: %s", - len(still_running), still_running, + len(still_running), + still_running, ) return results @@ -885,9 +941,7 @@ def _remove_agent(self, agent_id: str, source: str = "") -> bool: ct.cancel() if agent: root = agent.root_id or agent_id - if not any( - (a.root_id or a.id) == root for a in self._agents.values() - ): + if not any((a.root_id or a.id) == root for a in self._agents.values()): # Last member gone: nothing can ever spawn into this tree # again (a parent must exist), so the lifetime counter can go. self._tree_spawn_counts.pop(root, None) @@ -897,6 +951,7 @@ def _remove_agent(self, agent_id: str, source: str = "") -> bool: def _schedule_cleanup(self, agent_id: str) -> None: """Schedule cleanup of an agent after CLEANUP_DELAY.""" + async def _delayed_cleanup(): await asyncio.sleep(CLEANUP_DELAY) self._remove_agent(agent_id, source="delayed_cleanup") @@ -926,13 +981,17 @@ def check_health(self) -> dict: killed += 1 log.warning( "Force-killed stuck agent %s (%s): lifetime exceeded (%ds)", - agent.id, agent.label, int(elapsed), + agent.id, + agent.label, + int(elapsed), ) elif idle > STALE_WARN_SECONDS: stale += 1 log.warning( "Agent %s (%s) appears stale: %ds idle", - agent.id, agent.label, int(idle), + agent.id, + agent.label, + int(idle), ) return {"killed": killed, "stale": stale} @@ -959,6 +1018,9 @@ async def _run_agent( context_compression_enabled: bool = False, max_context_chars: int = 750000, keep_recent_iterations: int = 30, + budget_snapshot_provider: Callable | None = None, + generation_plan_provider: Callable | None = None, + evidence_recorder: Callable | None = None, ) -> None: """Execute an agent's tool loop until completion, error, or timeout. @@ -1027,14 +1089,42 @@ def _check_lifetime() -> bool: while not agent._inbox.empty(): # type: ignore[union-attr] # __post_init__ always sets it try: msg = agent._inbox.get_nowait() # type: ignore[union-attr] # __post_init__ always sets it - agent.messages.append({ - "role": "user", - "content": f"[Message from parent] {msg}", - }) + agent.messages.append( + { + "role": "user", + "content": f"[Message from parent] {msg}", + } + ) log.debug("Agent %s received inbox message", agent.id) except asyncio.QueueEmpty: break + # ONE authoritative plan is captured before any compaction for + # this logical generation. Its serving identity and budget snapshot + # then govern the soft pass, latch pass, physical request, and every + # rescue rung. Live config reaches only the next generation. + generation_state: dict = {} + budget_snapshot = None + if generation_plan_provider is not None: + try: + plan = generation_plan_provider() + generation_state["plan"] = plan + budget_snapshot = plan.get("snapshot") if isinstance(plan, dict) else None + except Exception: + log.exception( + "agent generation plan provider failed (non-fatal); using fallback targets" + ) + elif budget_snapshot_provider is not None: + try: + budget_snapshot = budget_snapshot_provider() + except Exception: + log.exception( + "agent budget snapshot provider failed (non-fatal); using fallback targets" + ) + soft_target_chars = ( + budget_snapshot.primary_chars if budget_snapshot is not None else max_context_chars + ) + # Context compression: summarize older tool iterations when context grows too large if context_compression_enabled and iteration > 0: try: @@ -1042,10 +1132,11 @@ def _check_lifetime() -> bool: compress_tool_context, estimate_message_chars, ) - if estimate_message_chars(agent.messages) > max_context_chars: + + if estimate_message_chars(agent.messages) > soft_target_chars: agent.messages, saved = compress_tool_context( agent.messages, - max_context_chars=max_context_chars, + max_context_chars=soft_target_chars, keep_recent=keep_recent_iterations, ) log.info( @@ -1055,8 +1146,9 @@ def _check_lifetime() -> bool: saved, ) except Exception: - log.exception("agent context_compressor failed (non-fatal); continuing with " - "full context") + log.exception( + "agent context_compressor failed (non-fatal); continuing with full context" + ) # Budget warning: inject remaining-iterations notice before LLM call remaining = max_iterations - iteration @@ -1064,8 +1156,10 @@ def _check_lifetime() -> bool: if remaining == 1: warn_text = "[Agent budget: FINAL iteration. Produce your final summary NOW.]" elif remaining <= 5: - warn_text = (f"[Agent budget: {remaining} iterations remaining. Commit any " - f"changes, run validation, and produce your final summary.]") + warn_text = ( + f"[Agent budget: {remaining} iterations remaining. Commit any " + f"changes, run validation, and produce your final summary.]" + ) else: warn_text = ( f"[Agent budget: {remaining} iterations remaining. " @@ -1092,13 +1186,15 @@ def _check_lifetime() -> bool: emergency_compress_for_window, estimate_message_chars, ) - if ( - estimate_message_chars(agent.messages) - > agent.context_char_ceiling - ): + + # The latch is capability evidence; the snapshot's primary + # is policy. Compact to whichever is LOWER — a live budget + # drop must not be out-waited by a stale larger latch. + latch_target = min(agent.context_char_ceiling, soft_target_chars) + if estimate_message_chars(agent.messages) > latch_target: agent.messages, latch_report = emergency_compress_for_window( agent.messages, - target_chars=agent.context_char_ceiling, + target_chars=latch_target, ) latch_report["attempt"] = 0 latch_report["trigger"] = "latch" @@ -1109,13 +1205,18 @@ def _check_lifetime() -> bool: latch_report["compressed_chars"], ) except Exception: - log.exception( - "agent overflow-latch compaction failed (non-fatal)" - ) + log.exception("agent overflow-latch compaction failed (non-fatal)") - # Call LLM with recovery support + # Call LLM with recovery support using the plan captured before + # compaction (when one is available). response = await _call_llm_with_recovery( - agent, iteration_callback, system_prompt, tools, + agent, + iteration_callback, + system_prompt, + tools, + rescue_ladder=(budget_snapshot.ladder if budget_snapshot is not None else None), + generation_state=generation_state, + evidence_recorder=evidence_recorder, ) if response is None: # Terminal state already set by recovery logic @@ -1210,10 +1311,12 @@ def _check_lifetime() -> bool: iter_tool_results.append({"name": tool_name, "result": result}) # Append tool result to messages - agent.messages.append({ - "role": "user", - "content": f"[Tool result: {tool_name}]\n{result}", - }) + agent.messages.append( + { + "role": "user", + "content": f"[Tool result: {tool_name}]\n{result}", + } + ) trajectory.add_iteration( iteration=iteration + 1, @@ -1239,7 +1342,9 @@ def _check_lifetime() -> bool: if time.time() - agent.last_activity > STALE_WARN_SECONDS: log.warning( "Agent %s (%s) has been idle for >%ds", - agent.id, agent.label, STALE_WARN_SECONDS, + agent.id, + agent.label, + STALE_WARN_SECONDS, ) # Exhausted iterations — transition from READY → COMPLETED @@ -1321,6 +1426,9 @@ async def _call_llm_with_recovery( iteration_callback: IterationCallback, system_prompt: str, tools: list[dict], + rescue_ladder: tuple[int, ...] | None = None, + generation_state: dict | None = None, + evidence_recorder: Callable | None = None, ) -> dict | None: """Call the LLM for one agent iteration. @@ -1332,9 +1440,27 @@ async def _call_llm_with_recovery( the agent's snapshotted iteration_timeout capped at remaining lifetime hard-bounds the callback INCLUDING any recovery waits. + Production callbacks always receive the manager-created state channel. + ``generation_state=None`` remains only a helper-level convenience for + direct recovery tests; it creates the channel, it does not revive the + retired three-argument callback contract. + Returns the LLM response dict, or None if agent reached terminal state. """ + # The advisory rescue ladder is optional; an absent advisory source uses + # unknown-model math. Once the callback publishes an authoritative plan, + # its snapshot wins even when its ladder is empty (for example a zero + # observed clamp): empty means honest terminal failure, never fallback. + if rescue_ladder is None: + rescue_ladder = _fallback_budget_snapshot().ladder + if generation_state is None: + generation_state = {} emergency_passes = 0 + # Published only after a provider ACCEPTS the compacted payload: a local + # "fits" proves a character target was met, not that the server took it + # (R2: the latch comes from the size that actually received a successful + # response). + pending_ceiling: int | None = None # ONE monotonic deadline bounds the whole logical iteration — the initial # attempt, any emergency compaction, and every retry share it (Odin's # adversarial repro: per-attempt timeouts let one iteration consume ~3x @@ -1346,6 +1472,7 @@ async def _call_llm_with_recovery( call_timeout = min(agent.iteration_timeout, remaining) iteration_deadline = time.monotonic() + call_timeout first_attempt = True + last_overflow: BaseException | None = None while True: if _remaining_lifetime(agent) <= 0: _lifetime_timeout(agent) @@ -1367,10 +1494,27 @@ async def _call_llm_with_recovery( agent.ended_at = time.time() return None try: - return await asyncio.wait_for( - iteration_callback(agent.messages, system_prompt, tools), + response = await asyncio.wait_for( + iteration_callback( + agent.messages, + system_prompt, + tools, + generation_state=generation_state, + ), timeout=attempt_budget, ) + if pending_ceiling is not None: + # The rescue rung is now server-accepted evidence. + agent.context_char_ceiling = pending_ceiling + if evidence_recorder is not None and last_overflow is not None: + try: + # Phase 5: the overflow→acceptance pair feeds the + # window observer. Total — evidence never fails the + # iteration that just succeeded. + await evidence_recorder(last_overflow, response) + except Exception: + log.exception("agent window-evidence recording failed (non-fatal)") + return response except TimeoutError: if _remaining_lifetime(agent) <= 0: # The wait was lifetime-capped and the deadline has passed: @@ -1391,10 +1535,7 @@ async def _call_llm_with_recovery( # v3.59.0 rule: exhaustion is TIMEOUT, never FAILED). _lifetime_timeout(agent) return None - if ( - _is_context_overflow(exc) - and emergency_passes < len(_EMERGENCY_TARGETS) - ): + if _is_context_overflow(exc): # Window overflow: deterministic for THIS payload, so a plain # retry is doomed — but a smaller payload is not. Bound the # entire message list (recent iterations by SIZE, single huge @@ -1404,35 +1545,58 @@ async def _call_llm_with_recovery( # breaker/rotation machinery engaged. Bounded passes, no loop. from ..llm.context_compressor import emergency_compress_for_window - target = _EMERGENCY_TARGETS[emergency_passes] - emergency_passes += 1 - compressed, report = emergency_compress_for_window( - agent.messages, target_chars=target + plan = generation_state.get("plan") + plan_snapshot = plan.get("snapshot") if isinstance(plan, dict) else None + # The ladder of the request that ACTUALLY overflowed: the + # generation plan captured by the callback at send time. + # Spawn-provider advisory only when no authoritative plan + # snapshot exists. An authoritative EMPTY (or malformed- + # missing) ladder is a real terminal outcome and must not + # silently widen through the unknown-model fallback. + active_ladder = ( + tuple(getattr(plan_snapshot, "ladder", ()) or ()) + if plan_snapshot is not None + else rescue_ladder ) - report["attempt"] = emergency_passes - report["trigger"] = "overflow" - agent.context_recoveries.append(report) - if report["fits"]: - agent.messages = compressed - # Latch: later iterations compact BEFORE sending once they - # cross the size that just proved survivable. - agent.context_char_ceiling = report["compressed_chars"] - log.warning( - "Agent %s context overflow: emergency pass %d " - "compressed %d -> %d chars; retrying iteration", + if emergency_passes < len(active_ladder): + target = active_ladder[emergency_passes] + emergency_passes += 1 + compressed, report = emergency_compress_for_window( + agent.messages, target_chars=target + ) + report["attempt"] = emergency_passes + report["trigger"] = "overflow" + agent.context_recoveries.append(report) + if report["fits"]: + agent.messages = compressed + last_overflow = exc + # Latch candidate: held until the retry actually + # succeeds (the provider is the authority on + # survivable size). + pending_ceiling = report["compressed_chars"] + log.warning( + "Agent %s context overflow: emergency pass %d " + "compressed %d -> %d chars; retrying iteration", + agent.id, + emergency_passes, + report["original_chars"], + report["compressed_chars"], + ) + continue + log.error( + "Agent %s context overflow: payload cannot be bounded " + "under %d chars (prefix %d); failing", + agent.id, + target, + report["prefix_chars"], + ) + else: + log.error( + "Agent %s context overflow: rescue ladder exhausted " + "after %d passes; failing", agent.id, emergency_passes, - report["original_chars"], - report["compressed_chars"], ) - continue - log.error( - "Agent %s context overflow: payload cannot be bounded " - "under %d chars (prefix %d); failing", - agent.id, - target, - report["prefix_chars"], - ) # Typed fast-fail (auth / malformed request / quota-exhausted # after rotation) or a programming defect: neither earns a # manager-level retry — transient classes were already retried @@ -1441,9 +1605,7 @@ async def _call_llm_with_recovery( # fallback, and keeps upstream text out of agent.error / # state_history (both API- and trajectory-visible). err_desc = f"LLM error: {format_user_facing_error(exc)}" - log.error( - "Agent %s LLM call failed (no retry): %s", agent.id, err_desc, exc_info=exc - ) + log.error("Agent %s LLM call failed (no retry): %s", agent.id, err_desc, exc_info=exc) agent.transition(AgentState.FAILED, err_desc) agent.error = err_desc agent.ended_at = time.time() diff --git a/src/config/apply_registry.py b/src/config/apply_registry.py index 1cc3c5e2..b823b690 100644 --- a/src/config/apply_registry.py +++ b/src/config/apply_registry.py @@ -1007,10 +1007,30 @@ class SectionSpec: "openai_codex.context_compression.max_context_chars": FieldSpec( apply_mode="restart", unit="characters", - description="Context size at which compression begins.", + description="Explicit ceiling on the compaction target, in " + "characters. Null means auto: the per-model budget resolver derives " + "the target from the serving model's usable input budget. An " + "explicit value only lowers the derived target, never raises it.", restart_reason="The compressor holds the configuration object it was " "built with, which a save replaces rather than updates.", ), + "openai_codex.context_budget_overrides": FieldSpec( + apply_mode="live_read", + unit="tokens", + description="Per-model usable-input-budget overrides, keyed by " + "canonical model. Read at each logical generation's budget " + "resolution: chat turns, agent iterations, and rescue ladders pick " + "up a save on their next generation; an in-flight generation keeps " + "its snapshot.", + ), + "openai_codex.context_utilization": FieldSpec( + apply_mode="live_read", + unit="percent", + description="Working-set share of the effective budget that " + "compaction targets. Read at each logical generation's budget " + "resolution; never reduces budgets at or below 272K tokens, so a " + "change may have no effect on smaller models by design.", + ), "openai_codex.context_compression.keep_recent_iterations": FieldSpec( apply_mode="restart", unit="iterations", diff --git a/src/config/migrations.py b/src/config/migrations.py new file mode 100644 index 00000000..49110452 --- /dev/null +++ b/src/config/migrations.py @@ -0,0 +1,628 @@ +"""One-time configuration migrations. + +The context-budget campaign changes the historical soft-compaction default +from a materialized ``max_context_chars: 750000`` to ``null`` (automatic, +model-derived). This module performs that rewrite exactly once without +mistaking a later operator-authored 750000 for the shipped default. + +Only the exact scalar shipped in config.yml qualifies. YAML construction is +not evidence: it normalizes spellings such as ``750000.0``, ``0xB71B0``, and +``750_000`` to values equal to 750000. The gate therefore checks the original +scalar's token, tag, and style. + +Completion is a versioned, validated record under the unresolved config path's +sibling ``data`` directory. Marker path existence alone proves nothing. The +record is committed by temp-file write, file fsync, and atomic replacement. +""" + +from __future__ import annotations + +import contextlib +import hashlib +import json +import logging +import os +import tempfile +from dataclasses import dataclass +from datetime import UTC, datetime, timedelta +from enum import Enum +from pathlib import Path +from typing import Any + +import yaml +from yaml.nodes import MappingNode, Node, ScalarNode + +from .schema import LEGACY_MAX_CONTEXT_CHARS + +log = logging.getLogger("odin.config") + +LEGACY_CEILING_MARKER_NAME = "context_ceiling_migration.json" + +_CEILING_PATH = ("openai_codex", "context_compression", "max_context_chars") +_MIGRATION_ID = "legacy_max_context_chars_to_auto" +_MARKER_VERSION = 3 +_LEGACY_CLAIM_SUFFIX = ".claim" +_COMPLETION_REASONS = frozenset( + { + "migrated", + "not_applicable", + "prior_operator_saved", + "upgraded_preversioned_completion", + } +) + + +class MigrationCompletionError(RuntimeError): + """The migration could not establish durable, unambiguous provenance.""" + + +class _MarkerKind(Enum): + MISSING = "missing" + COMPLETE = "complete" + ROUND1_LEGACY = "round1_legacy" + ROUND1_OPERATOR = "round1_operator" + PREVERSIONED_COMPLETE = "preversioned_complete" + EMPTY = "empty" + CORRUPT = "corrupt" + DIRECTORY = "directory" + UNKNOWN = "unknown" + UNREADABLE = "unreadable" + + +@dataclass(frozen=True) +class _ScalarLexeme: + value: str + tag: str + style: str | None + token: str + + +def _config_identity(config_path: str | Path) -> str: + """Stable identity for the config rewrite target. + + The canonical path survives atomic config replacement (unlike inode + identity), makes symlink aliases rendezvous on one identity, and keeps two + config files in one directory distinct. + """ + target = Path(config_path).resolve() + material = b"odin-config-identity-v1\0" + os.fsencode(str(target)) + return hashlib.sha256(material).hexdigest() + + +def ceiling_marker_path(config_path: str | Path) -> Path: + """Return this config identity's marker in the unresolved data anchor.""" + launch = Path(config_path).absolute() + return launch.parent / "data" / "config_migrations" / ( + f"{_MIGRATION_ID}.{_config_identity(config_path)}.json" + ) + + +def _legacy_ceiling_marker_path(config_path: str | Path) -> Path: + """The pre-identity directory-wide marker, retained only for upgrade.""" + return Path(config_path).absolute().parent / "data" / LEGACY_CEILING_MARKER_NAME + + +def _shared_ceiling_marker_path(config_path: str | Path) -> Path: + """Alias rendezvous marker beside the canonical rewrite target. + + Launch-local provenance remains in the durable data directory. This second + identity-bound record is what lets aliases in different launch directories + observe one completed migration. + """ + target = Path(config_path).resolve() + return target.parent / ".odin-data" / "config_migrations" / ( + f"{_MIGRATION_ID}.{_config_identity(config_path)}.json" + ) + + +def _mapping_value(node: Node, key: str) -> Node | None: + """Return one unambiguous mapping value; duplicate keys prove nothing.""" + if not isinstance(node, MappingNode): + return None + matches = [ + value_node + for key_node, value_node in node.value + if isinstance(key_node, ScalarNode) + and key_node.tag == "tag:yaml.org,2002:str" + and key_node.value == key + ] + return matches[0] if len(matches) == 1 else None + + +def _literal_ceiling_lexeme(original_raw: str) -> _ScalarLexeme | None: + """Return original token/tag/style evidence for the configured scalar.""" + try: + node = yaml.compose(original_raw, Loader=yaml.SafeLoader) + except yaml.YAMLError: + return None + if node is None: + return None + current: Node | None = node + for segment in _CEILING_PATH: + if current is None: + return None + current = _mapping_value(current, segment) + if not isinstance(current, ScalarNode): + return None + return _ScalarLexeme( + value=current.value, + tag=current.tag, + style=current.style, + token=original_raw[current.start_mark.index : current.end_mark.index], + ) + + +def _is_shipped_legacy_literal(original_raw: str) -> bool: + """Only the shipped implicit, unstyled, plain-decimal token qualifies.""" + scalar = _literal_ceiling_lexeme(original_raw) + return bool( + scalar is not None + and scalar.value == str(LEGACY_MAX_CONTEXT_CHARS) + and scalar.tag == "tag:yaml.org,2002:int" + and scalar.style is None + and scalar.token == str(LEGACY_MAX_CONTEXT_CHARS) + ) + + +def _is_utc_timestamp(value: object) -> bool: + if not isinstance(value, str): + return False + try: + parsed = datetime.fromisoformat(value) + except ValueError: + return False + return parsed.tzinfo is not None and parsed.utcoffset() == timedelta(0) + + +def _classify_record(record: object) -> _MarkerKind: + if not isinstance(record, dict): + return _MarkerKind.UNKNOWN + + if set(record) == { + "version", + "migration", + "config_id", + "state", + "reason", + "completed_at", + }: + valid = ( + type(record["version"]) is int + and record["version"] == _MARKER_VERSION + and record["migration"] == _MIGRATION_ID + and isinstance(record["config_id"], str) + and len(record["config_id"]) == 64 + and record["state"] == "completed" + and isinstance(record["reason"], str) + and record["reason"] in _COMPLETION_REASONS + and _is_utc_timestamp(record["completed_at"]) + ) + return _MarkerKind.COMPLETE if valid else _MarkerKind.UNKNOWN + + # Version 2 was validated but directory-wide. It can only be adopted when + # found at the legacy launch-local path, never mistaken for an identity- + # bound record at the new path. + if set(record) == { + "version", + "migration", + "state", + "reason", + "completed_at", + }: + valid = ( + type(record["version"]) is int + and record["version"] == 2 + and record["migration"] == _MIGRATION_ID + and record["state"] == "completed" + and isinstance(record["reason"], str) + and record["reason"] in _COMPLETION_REASONS + and _is_utc_timestamp(record["completed_at"]) + ) + return _MarkerKind.PREVERSIONED_COMPLETE if valid else _MarkerKind.UNKNOWN + + # Round 1 recorded in-memory reinterpretation without rewriting config.yml. + if set(record) == {"migration", "legacy_value", "migrated_at"}: + valid = ( + record["migration"] == _MIGRATION_ID + and type(record["legacy_value"]) is int + and record["legacy_value"] == LEGACY_MAX_CONTEXT_CHARS + and _is_utc_timestamp(record["migrated_at"]) + ) + return _MarkerKind.ROUND1_LEGACY if valid else _MarkerKind.UNKNOWN + + # Round 1 could also affirm that a compression save was operator-authored. + if set(record) == {"migration", "operator_saved", "saved_at"}: + valid = ( + record["migration"] == _MIGRATION_ID + and record["operator_saved"] is True + and _is_utc_timestamp(record["saved_at"]) + ) + return _MarkerKind.ROUND1_OPERATOR if valid else _MarkerKind.UNKNOWN + + # The first R2 implementation emitted this unversioned completion shape. + if set(record) == {"migration", "reason", "completed_at"}: + valid = ( + record["migration"] == _MIGRATION_ID + and isinstance(record["reason"], str) + and record["reason"] in {"migrated", "not_applicable"} + and _is_utc_timestamp(record["completed_at"]) + ) + return _MarkerKind.PREVERSIONED_COMPLETE if valid else _MarkerKind.UNKNOWN + + return _MarkerKind.UNKNOWN + + +def _read_marker(marker: Path) -> _MarkerKind: + try: + raw = marker.read_text(encoding="utf-8") + except FileNotFoundError: + return _MarkerKind.MISSING + except IsADirectoryError: + return _MarkerKind.DIRECTORY + except (OSError, UnicodeError): + return _MarkerKind.UNREADABLE + if not raw.strip(): + return _MarkerKind.EMPTY + try: + record: Any = json.loads(raw) + except json.JSONDecodeError: + return _MarkerKind.CORRUPT + return _classify_record(record) + + +def _atomic_write_marker(marker: Path, record: dict[str, object]) -> None: + """Commit one marker revision via temp-file, file fsync, and replace.""" + marker.parent.mkdir(parents=True, exist_ok=True) + encoded = json.dumps(record, indent=2, sort_keys=True) + "\n" + fd, temporary_name = tempfile.mkstemp( + dir=marker.parent, + prefix=f".{marker.name}.", + suffix=".tmp", + ) + temporary = Path(temporary_name) + stream = None + try: + os.fchmod(fd, 0o600) + stream = os.fdopen(fd, "w", encoding="utf-8") + fd = -1 + with stream: + stream.write(encoded) + stream.flush() + os.fsync(stream.fileno()) + stream = None + os.replace(temporary, marker) + except BaseException: + if stream is not None: + with contextlib.suppress(OSError): + stream.close() + if fd >= 0: + with contextlib.suppress(OSError): + os.close(fd) + with contextlib.suppress(OSError): + temporary.unlink() + raise + + # The replace is already committed. Directory fsync is best effort on + # filesystems that support it and cannot truthfully roll that commit back. + with contextlib.suppress(OSError): + directory_fd = os.open(marker.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + + +def _legacy_claim_path(legacy_marker: Path) -> Path: + """Exclusive ownership record for one directory-wide legacy marker.""" + return legacy_marker.with_name(legacy_marker.name + _LEGACY_CLAIM_SUFFIX) + + +def _read_claim_owner(claim: Path) -> str | None: + """Read one strict claim owner; malformed claims fail closed.""" + try: + raw = claim.read_text(encoding="ascii") + except FileNotFoundError: + return None + except (OSError, UnicodeError) as exc: + raise MigrationCompletionError( + "legacy ceiling-migration claim is invalid or unreadable; inspect it before retrying" + ) from exc + owner = raw.rstrip("\n") + if len(owner) != 64 or any(ch not in "0123456789abcdef" for ch in owner): + raise MigrationCompletionError( + "legacy ceiling-migration claim is invalid or unreadable; inspect it before retrying" + ) + return owner + + +def _claim_legacy_marker(legacy_marker: Path, config_id: str) -> bool: + """Atomically claim ambiguous legacy provenance for one config identity. + + A fully written, fsynced temporary file is hard-linked into place. The + link is fail-if-exists, so losers can only observe the winner's complete + owner value — never a partially written O_EXCL destination. A crash may + conservatively strand the claim with its owner, but can never grant one + legacy completion to a second config identity. + """ + claim = _legacy_claim_path(legacy_marker) + temporary: Path | None = None + fd = -1 + stream = None + try: + claim.parent.mkdir(parents=True, exist_ok=True) + fd, temporary_name = tempfile.mkstemp( + dir=claim.parent, + prefix=f".{claim.name}.", + suffix=".tmp", + ) + temporary = Path(temporary_name) + os.fchmod(fd, 0o600) + stream = os.fdopen(fd, "w", encoding="ascii") + fd = -1 + with stream: + stream.write(config_id + "\n") + stream.flush() + os.fsync(stream.fileno()) + stream = None + try: + os.link(temporary, claim) + except FileExistsError: + return _read_claim_owner(claim) == config_id + with contextlib.suppress(OSError): + directory_fd = os.open(claim.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + return True + except OSError as exc: + raise MigrationCompletionError( + "could not claim legacy ceiling-migration provenance" + ) from exc + finally: + if stream is not None: + with contextlib.suppress(OSError): + stream.close() + if fd >= 0: + with contextlib.suppress(OSError): + os.close(fd) + if temporary is not None: + with contextlib.suppress(OSError): + temporary.unlink() + + +def _completion_record(reason: str, config_id: str) -> dict[str, object]: + return { + "version": _MARKER_VERSION, + "migration": _MIGRATION_ID, + "config_id": config_id, + "state": "completed", + "reason": reason, + "completed_at": datetime.now(UTC).isoformat(), + } + + +def _write_required(marker: Path, reason: str, purpose: str, config_id: str) -> None: + try: + _atomic_write_marker(marker, _completion_record(reason, config_id)) + except OSError as exc: + log.error("Could not %s at %s: %s", purpose, marker, exc) + raise MigrationCompletionError( + f"could not {purpose}; configuration was left unchanged" + ) from exc + + +def _record_after_rewrite(marker: Path, config_id: str) -> None: + try: + _atomic_write_marker(marker, _completion_record("migrated", config_id)) + except OSError as exc: + # The ambiguous value is already gone. A later load takes the + # non-legacy branch and safely retries this completion write. + log.warning( + "Could not record ceiling-migration completion at %s: %s; " + "the rewritten config is safe and completion retries next boot.", + marker, + exc, + ) + + +def _read_identity_marker(marker: Path, config_id: str) -> _MarkerKind: + kind = _read_marker(marker) + if kind is not _MarkerKind.COMPLETE: + return kind + try: + record = json.loads(marker.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError): + return _MarkerKind.UNREADABLE + return kind if record.get("config_id") == config_id else _MarkerKind.UNKNOWN + + +def _write_completion_pair( + marker: Path, + shared_marker: Path, + reason: str, + purpose: str, + config_id: str, +) -> None: + # Publish shared provenance first: once a launch-local marker says complete, + # every alias must already have a canonical rendezvous record to consult. + _write_required(shared_marker, reason, purpose, config_id) + _write_required(marker, reason, purpose, config_id) + + +def _set_runtime_auto(data: dict) -> None: + codex = data.get("openai_codex") + if not isinstance(codex, dict): + return + compression = codex.get("context_compression") + if isinstance(compression, dict): + compression["max_context_chars"] = None + + +def apply_legacy_ceiling_migration(data: dict, config_path: str | Path, original_raw: str) -> None: + """Apply the identity-bound one-time legacy-ceiling migration.""" + config_id = _config_identity(config_path) + marker = ceiling_marker_path(config_path) + shared_marker = _shared_ceiling_marker_path(config_path) + marker_kind = _read_identity_marker(marker, config_id) + shared_kind = _read_identity_marker(shared_marker, config_id) + + invalid = { + _MarkerKind.EMPTY, + _MarkerKind.CORRUPT, + _MarkerKind.DIRECTORY, + _MarkerKind.UNKNOWN, + _MarkerKind.UNREADABLE, + } + for path, kind in ((marker, marker_kind), (shared_marker, shared_kind)): + if kind in invalid: + log.error( + "Ceiling-migration record at %s is %s; refusing to guess at migration provenance.", + path, + kind.value, + ) + raise MigrationCompletionError( + "ceiling-migration record is invalid or unreadable; inspect it before retrying" + ) + + if marker_kind is _MarkerKind.COMPLETE and shared_kind is _MarkerKind.COMPLETE: + return + if shared_kind is _MarkerKind.COMPLETE: + # A different symlink alias already completed this config identity. + _write_required( + marker, + "upgraded_preversioned_completion", + "record alias-local ceiling-migration completion", + config_id, + ) + return + if marker_kind is _MarkerKind.COMPLETE: + _write_required( + shared_marker, + "upgraded_preversioned_completion", + "repair shared ceiling-migration completion", + config_id, + ) + return + if marker_kind in { + _MarkerKind.ROUND1_OPERATOR, + _MarkerKind.PREVERSIONED_COMPLETE, + }: + reason = ( + "prior_operator_saved" + if marker_kind is _MarkerKind.ROUND1_OPERATOR + else "upgraded_preversioned_completion" + ) + _write_completion_pair( + marker, + shared_marker, + reason, + "upgrade the ceiling-migration completion record", + config_id, + ) + return + if marker_kind is _MarkerKind.ROUND1_LEGACY: + log.info("Upgrading round-1 legacy migration provenance at %s.", marker) + + # Upgrade the old launch-directory marker through an exclusive claim. + # Directory scans are not arbitration: debris is irrelevant, and two sibling + # processes must not both inherit one ambiguous v1/v2 completion record. + legacy_marker = _legacy_ceiling_marker_path(config_path) + claim_owner = _read_claim_owner(_legacy_claim_path(legacy_marker)) + legacy_kind = _read_marker(legacy_marker) + if legacy_kind is _MarkerKind.COMPLETE: + # A v3 legacy-path record is already bound. A different config in the + # same launch directory must ignore it rather than inherit completion. + try: + legacy_record = json.loads(legacy_marker.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError): + legacy_kind = _MarkerKind.UNREADABLE + else: + if legacy_record.get("config_id") != config_id: + legacy_kind = _MarkerKind.MISSING + elif ( + claim_owner is not None + and claim_owner != config_id + and legacy_kind + in { + _MarkerKind.ROUND1_LEGACY, + _MarkerKind.ROUND1_OPERATOR, + _MarkerKind.PREVERSIONED_COMPLETE, + } + ): + # Another config won valid pre-versioned provenance. This identity must + # evaluate and migrate its own literal rather than inherit completion. + # Invalid legacy material remains fail-closed; a foreign claim cannot + # launder a corrupt or unknown marker into "missing". + legacy_kind = _MarkerKind.MISSING + if legacy_kind in invalid: + raise MigrationCompletionError( + "legacy ceiling-migration record is invalid or unreadable; inspect it before retrying" + ) + if legacy_kind in { + _MarkerKind.COMPLETE, + _MarkerKind.ROUND1_OPERATOR, + _MarkerKind.PREVERSIONED_COMPLETE, + }: + if legacy_kind is not _MarkerKind.COMPLETE and not _claim_legacy_marker( + legacy_marker, config_id + ): + # A sibling won between our read and claim. Its value is irrelevant + # to this config; proceed through the ordinary lexical migration. + legacy_kind = _MarkerKind.MISSING + else: + reason = ( + "prior_operator_saved" + if legacy_kind is _MarkerKind.ROUND1_OPERATOR + else "upgraded_preversioned_completion" + ) + _write_completion_pair( + marker, + shared_marker, + reason, + "upgrade the ceiling-migration completion record", + config_id, + ) + # Bind the old marker after the exclusive claim. The claim remains + # durable as the arbitration record; replacing the marker cannot + # grant another sibling the already-consumed provenance. + _write_required( + legacy_marker, + reason, + "bind legacy ceiling-migration completion to its config", + config_id, + ) + return + if legacy_kind is _MarkerKind.ROUND1_LEGACY: + log.info("Upgrading round-1 legacy migration provenance at %s.", legacy_marker) + + if not _is_shipped_legacy_literal(original_raw): + _write_completion_pair(marker, shared_marker, "not_applicable", + "record vacuous ceiling-migration completion", config_id) + return + + try: + from .persistence import patch_config_paths + + patch_config_paths([(_CEILING_PATH, None)], path=Path(config_path).resolve()) + except Exception as exc: # noqa: BLE001 — boot retains safe runtime behavior + _set_runtime_auto(data) + log.warning( + "Could not rewrite legacy max_context_chars to auto (%s); " + "interpreting as auto for this boot only — the migration retries next boot.", + exc, + ) + return + + _set_runtime_auto(data) + log.warning( + "Migrated legacy max_context_chars %d to auto (model-derived): the " + "config file now records null. Any later explicit value — including " + "%d — is honored verbatim.", + LEGACY_MAX_CONTEXT_CHARS, + LEGACY_MAX_CONTEXT_CHARS, + ) + # Shared first, then launch-local. Failure after rewrite is self-healing: + # the unambiguous null takes the vacuous branch on the next boot. + _record_after_rewrite(shared_marker, config_id) + _record_after_rewrite(marker, config_id) diff --git a/src/config/persistence.py b/src/config/persistence.py index 54d7eb19..51ed3774 100644 --- a/src/config/persistence.py +++ b/src/config/persistence.py @@ -136,18 +136,33 @@ def submitted_leaves( def walk(new: Mapping[str, Any], known: Any, model: Any, prefix: tuple[str, ...]) -> None: lookup = _field_lookup(model) if model is not None else {} for key, value in new.items(): - canonical, nested, aliases = lookup.get(str(key), (str(key), None, ())) + field = lookup.get(str(key)) + canonical, nested, aliases = field or (str(key), None, ()) path = (*prefix, canonical) if not isinstance(known, Mapping) or canonical not in known: # Validation dropped this path (unknown/removed field) — the # runtime ignores it, so disk must not carry it either. continue known_value = known[canonical] - if isinstance(value, Mapping) and isinstance(known_value, Mapping): + if ( + isinstance(value, Mapping) + and isinstance(known_value, Mapping) + and nested is not None + ): + # A nested BaseModel has schema-owned child fields, so preserve + # the ordinary leaf-only walk through those children. walk(value, known_value, nested, path) + elif isinstance(value, Mapping) and isinstance(known_value, Mapping) and field is None: + # Schema-less callers retain the historical recursive helper + # behavior. Config persistence always supplies a model class. + walk(value, known_value, None, path) elif aliases: out.append((path, known_value, aliases)) else: + # A schema-owned Mapping (for example context-budget overrides) + # is ONE normalized leaf. Its validator may canonicalize keys, + # so walking raw submitted keys against the validated mapping + # would drop aliases that changed spelling during validation. out.append((path, known_value)) walk(updates, validated, model_cls, ()) diff --git a/src/config/schema.py b/src/config/schema.py index 4bb2bde9..e3b3a9e8 100644 --- a/src/config/schema.py +++ b/src/config/schema.py @@ -187,11 +187,43 @@ def _keepalive_positive(cls, v): return v +# The pre-campaign soft-compaction ceiling. For years this was the shipped +# default of ``max_context_chars`` and is materialized verbatim in most +# persisted configs — the legacy-ceiling migration (src/config/migrations.py) +# keys off this exact value. +LEGACY_MAX_CONTEXT_CHARS = 750_000 + + class ContextCompressionConfig(BaseModel): enabled: bool = True - max_context_chars: int = 750_000 + # None = "auto": the ceiling derives from the active model's input budget. + # Until the per-model resolver is wired to the runtime surfaces (context- + # budget campaign phase 3), auto resolves to the legacy constant so + # behavior is bit-identical to pre-campaign installs. An explicit value + # can only LOWER the derived target, never raise it (the resolver takes + # min(explicit, derived)). + max_context_chars: int | None = None keep_recent_iterations: int = 30 + @field_validator("max_context_chars") + @classmethod + def _validate_max_context_chars(cls, v: int | None) -> int | None: + if v is not None and v < 1: + raise ValueError("max_context_chars must be positive, or null for auto") + return v + + @property + def resolved_max_context_chars(self) -> int: + """The ceiling consumers compare against — legacy value when auto. + + Campaign phase 3 replaces consumer reads with the per-model budget + resolver; until then this property keeps every consumer total (no + None comparisons) and byte-identical to pre-campaign behavior. + """ + if self.max_context_chars is not None: + return self.max_context_chars + return LEGACY_MAX_CONTEXT_CHARS + class GovernorConfig(BaseModel): block_critical: bool = True @@ -334,12 +366,13 @@ class AuxiliaryLLMConfig(BaseModel): differs. When ``enabled`` and a Codex provider is active, those four jobs route here; otherwise they use the primary model. - Default Luna: the Codex catalog positions it for the cheap extraction / - transformation tier that suits this workload. + Default Terra, enabled: the out-of-the-box configuration mirrors the + reference deployment — background jobs on the mid-tier model while the + primary handles conversation. """ - enabled: bool = False - model: str = "gpt-5.6-luna" + enabled: bool = True + model: str = "gpt-5.6-terra" # "minimal" is deliberately absent: it sits in the Codex API's generic @@ -402,6 +435,73 @@ def effort_incompatibility_error(model: str | None, effort: str | None) -> str | f"{str(model).strip()!r} (allowed for this model: {allowed})" ) +# --- Per-model usable input budgets (context-budget campaign, 2026-08-17) --- +# Values are KNOWN-SAFE USABLE INPUT BUDGETS (floors): each model's own +# highest server-accepted input observation (usage-echo bracketing, Pro and +# Team accounts served identically) — NOT vendor context-window claims. They +# already sit below the server's output reservation; never subtract another +# output reserve from them. A floor never exceeds its evidence: only sol +# received the fine-refinement acceptances, which is why sol reads 921_601 +# while its window-mates read 917_506. The served models catalog reports +# 272000 for every slug (stale through three regime changes) — never consume +# it. Serving moves silently in both directions; these floors are refreshed +# by manual probes, bounded downward at runtime only by observed clamps. +CODEX_MODEL_INPUT_BUDGETS: dict[str, int] = { + "gpt-5.6-sol": 921_601, + "gpt-5.6-terra": 917_506, + "gpt-5.6-luna": 917_506, + "gpt-5.4": 917_506, + "gpt-5.5": 270_001, + "gpt-5.4-mini": 262_146, + "gpt-5.3-codex-spark": 124_001, +} + +# Unknown exact slugs assume the pre-campaign uniform window, so a new or +# renamed model degrades to the long-proven conservative math — not a guess. +CODEX_UNKNOWN_MODEL_INPUT_BUDGET = 272_000 + +# Slugs the backend serves under another model's identity (probed via the +# served_model echo). Canonicalization maps them BEFORE any registry, +# override, or observer lookup — they are never registry rows themselves. +_CODEX_MODEL_ALIASES: dict[str, str] = { + "codex-auto-review": "gpt-5.6-luna", +} + + +def canonical_codex_model(model: str | None) -> str: + """THE Codex model canonicalizer: trim, map aliases, preserve spelling. + + Single authority for budget-registry lookups, override keys, observer + keys, and UI capability data — the UI consumes canonical keys served by + the backend and never reimplements this. Unknown models pass through + with their spelling preserved (no case folding: the server is the + authority on model names). + """ + trimmed = str(model or "").strip() + return _CODEX_MODEL_ALIASES.get(trimmed, trimmed) + + +def input_budget_floor_for_model(model: str | None) -> int: + """Known-safe usable input budget for ``model``. + + Callers pass RAW model names; canonicalization happens here so no lookup + site can forget it. Unknown slugs get the conservative default. + """ + return CODEX_MODEL_INPUT_BUDGETS.get( + canonical_codex_model(model), CODEX_UNKNOWN_MODEL_INPUT_BUDGET + ) + + +# Operator override bounds for per-model input budgets. The floor guarantees +# a positive compactable allowance above the fixed 42K-token request envelope +# (50_192 = 42_000 + 8_192); the ceiling bounds serialization/memory cost of +# derived character targets. Observed clamps (runtime evidence) deliberately +# BYPASS these bounds — evidence stays exact and the budget resolver is a +# total function under any clamp value. +CONTEXT_BUDGET_OVERRIDE_MIN = 50_192 +CONTEXT_BUDGET_OVERRIDE_MAX = 2_000_000 + + # Sentinel for the agent model/effort config axes meaning "let the spawner pick # per-spawn from the exposed catalogue". Deliberately NOT a member of # CODEX_REASONING_EFFORTS — that set is the values legal to SEND to Codex; "auto" @@ -432,19 +532,19 @@ class OpenAICodexConfig(BaseModel): model_config = ConfigDict(protected_namespaces=()) enabled: bool = False - model: str = "gpt-4o" - reasoning_effort: ReasoningEffort = "medium" + model: str = "gpt-5.6-sol" + reasoning_effort: ReasoningEffort = "xhigh" # Effort for SPAWNED-AGENT iterations only. None = inherit # reasoning_effort (the string "none" is a real effort level, not # inherit); "auto" = expose per-spawn effort selection to the spawner # ("auto" is policy, never sent to a provider). Read at call time, so # live changes reach in-flight agents on their next iteration. - agent_reasoning_effort: ReasoningEffort | Literal["auto"] | None = None + agent_reasoning_effort: ReasoningEffort | Literal["auto"] | None = "auto" # Model for SPAWNED-AGENT iterations only. None = inherit ``model``; # "auto" = expose per-spawn model selection to the spawner. Free string # like ``model`` otherwise (the WebUI dropdown is the constraint; an # unsupported value fails per-request). Read at call time. - agent_model: str | None = None + agent_model: str | None = "auto" credentials_path: str = "./data/codex_auth.json" # Streaming transport timeouts: a generous whole-request backstop (long # high-effort reasoning turns stream well past 10 minutes) plus a stall @@ -498,6 +598,49 @@ def _stream_stall_timeout_bounds(cls, v): connection_pool: ConnectionPoolConfig = ConnectionPoolConfig() auxiliary: AuxiliaryLLMConfig = AuxiliaryLLMConfig() context_compression: ContextCompressionConfig = ContextCompressionConfig() + # Per-model usable-input-budget overrides (tokens), keyed by canonical + # model name. Empty = built-in floors (CODEX_MODEL_INPUT_BUDGETS). An + # override may exceed the known-safe floor — overflow recovery is what + # makes that experimentation tolerable — but stays inside process-safety + # bounds. Consumed from campaign phase 3 (budget resolver). + context_budget_overrides: dict[str, int] = Field(default_factory=dict) + # Working-set policy: percent of the effective budget compaction actually + # targets (quality/latency/cost posture — NOT a capability claim). The + # resolver never lets utilization reduce budgets at or below 272K, so + # changing this may have no effect on smaller models by design. + context_utilization: int = 60 + + @field_validator("context_utilization") + @classmethod + def _validate_context_utilization(cls, v: int) -> int: + if isinstance(v, bool) or not 30 <= v <= 100: + raise ValueError("context_utilization must be an integer percent between 30 and 100") + return v + + @field_validator("context_budget_overrides") + @classmethod + def _validate_context_budget_overrides(cls, v: dict[str, int]) -> dict[str, int]: + canonical: dict[str, int] = {} + for raw_key, value in v.items(): + key = canonical_codex_model(raw_key) + if not key: + raise ValueError( + "context_budget_overrides keys must be non-empty model names" + ) + if key in canonical: + raise ValueError( + f"context_budget_overrides: {raw_key!r} duplicates " + f"{key!r} after canonicalization" + ) + if isinstance(value, bool) or not ( + CONTEXT_BUDGET_OVERRIDE_MIN <= value <= CONTEXT_BUDGET_OVERRIDE_MAX + ): + raise ValueError( + f"context_budget_overrides[{key!r}] must be an integer between " + f"{CONTEXT_BUDGET_OVERRIDE_MIN} and {CONTEXT_BUDGET_OVERRIDE_MAX} tokens" + ) + canonical[key] = value + return canonical @model_validator(mode="after") def _validate_effort_model_pairs(self): @@ -1055,9 +1198,9 @@ def set_active_config_path(path: str | Path | None) -> None: def load_config(path: str | Path = "config.yml") -> Config: path = Path(path) - raw = path.read_text() + original_raw = path.read_text() try: - raw = _substitute_env_vars(raw) + raw = _substitute_env_vars(original_raw) except ValueError as exc: raise SystemExit( f"Configuration error: {exc}\n" @@ -1082,6 +1225,20 @@ def load_config(path: str | Path = "config.yml") -> Config: # and the intended setting never applies. We warn rather than error # (extra="forbid") so a slightly-ahead config can't hard-fail boot. _warn_unknown_config_keys(data) + # One-time legacy-ceiling migration gate (see src/config/migrations.py). + # Runs on the raw dict so pydantic validates what will actually apply; + # the unsubstituted text distinguishes a literal legacy default from a + # deliberate ${VAR} placeholder. + from .migrations import MigrationCompletionError, apply_legacy_ceiling_migration + + try: + apply_legacy_ceiling_migration(data, path, original_raw) + except MigrationCompletionError as exc: + raise SystemExit( + f"Configuration migration failed for {path}: {exc}\n" + "Inspect the ceiling-migration record and retry; Odin will not " + "guess at operator provenance." + ) from exc try: cfg = Config(**data) except Exception as exc: diff --git a/src/discord/llm_gateway.py b/src/discord/llm_gateway.py index 9e88ad1e..4fd5757c 100644 --- a/src/discord/llm_gateway.py +++ b/src/discord/llm_gateway.py @@ -25,6 +25,7 @@ import asyncio from collections.abc import Callable from dataclasses import dataclass +from typing import Any, NamedTuple from ..config.persistence import config_transaction from ..llm import CodexChatClient, KimiClient, OllamaClient @@ -38,6 +39,25 @@ log = get_logger("discord") +class LLMServingIdentity(NamedTuple): + """Immutable identity for one uninterrupted logical generation. + + Chat captures this once before soft compaction. Preflight, breaker + admission, and every physical transport attempt then describe the same + provider/client/model/effort even if live configuration reloads in place + while recovery is waiting. + """ + + provider: str + client: Any + model: str | None + reasoning_effort: str | None + + @property + def is_codex(self) -> bool: + return self.provider == "codex" and self.client is not None + + @dataclass(frozen=True) class _AuxBuildInputs: """Immutable config consumed while constructing an auxiliary candidate.""" @@ -118,34 +138,64 @@ def __init__( # ---------- provider resolution ---------------------------------------- + def capture_serving_identity(self, config=None) -> LLMServingIdentity: + """Snapshot the client and request identity for one generation. + + This is deliberately synchronous: one root-config read and no await + means a live provider switch cannot split provider selection from the + client/model/effort facts captured beside it. If a configured local + provider is absent, the established Codex fallback is named Codex too + so breaker and subsystem identities describe the client actually sent. + """ + if config is None: + config = self.get_config() + provider_cfg = getattr(config, "llm_provider", None) + requested = provider_cfg.active_provider if provider_cfg else "codex" + provider: str + client: Any + if requested == "ollama" and self.ollama_client is not None: + provider, client = "ollama", self.ollama_client + elif requested == "kimi" and self.kimi_client is not None: + provider, client = "kimi", self.kimi_client + else: + provider, client = "codex", self.codex_client + return LLMServingIdentity( + provider=provider, + client=client, + model=getattr(client, "model", None) if client is not None else None, + reasoning_effort=( + getattr(client, "reasoning_effort", None) + if client is not None and hasattr(client, "reasoning_effort") + else None + ), + ) + @property def active_client(self): """Return whichever LLM provider is currently active.""" - provider_cfg = getattr(self.get_config(), "llm_provider", None) - active = provider_cfg.active_provider if provider_cfg else "codex" - if active == "ollama" and self.ollama_client is not None: - return self.ollama_client - if active == "kimi" and self.kimi_client is not None: - return self.kimi_client - return self.codex_client + return self.capture_serving_identity().client def recovery_policy(self) -> RecoveryPolicy: """The live recovery policy (config-backed via wiring).""" return self._recovery_policy_source() - def capacity_breaker_for(self, model: str | None = None) -> ModelCapacityBreaker: - """Model-scoped capacity breaker for the active provider. + def capacity_breaker_for( + self, + model: str | None = None, + *, + provider: str | None = None, + ) -> ModelCapacityBreaker: + """Return the breaker for an effective request identity. - ``model`` must be the EFFECTIVE model of the request when the caller - overrides it (agents); defaults to the active client's model. + Chat supplies both values from its frozen serving identity. Callers + that omit either retain the legacy live-resolution behavior. """ - provider_cfg = getattr(self.get_config(), "llm_provider", None) - active = provider_cfg.active_provider if provider_cfg else "codex" - effective = model - if not effective: - client = self.active_client - effective = getattr(client, "model", None) if client is not None else None - return self.model_breakers.for_model(active, str(effective or "unknown")) + if provider is None: + serving = self.capture_serving_identity() + provider = serving.provider + if not model: + model = serving.model + return self.model_breakers.for_model(str(provider or "codex"), str(model or "unknown")) def notify_generation_success(self, provider: str | None) -> None: """Success signal from a path that bypasses ``call_with_tools`` @@ -729,6 +779,7 @@ async def call_with_tools( user_id: str = "", channel_id: str = "", tools_used: list[str] | None = None, + serving_identity: LLMServingIdentity | None = None, **kwargs, ): """Wrap chat_with_tools with cost / subsystem wiring. @@ -740,14 +791,20 @@ async def call_with_tools( async with self.provider_lock: if self.switching: raise RuntimeError("LLM provider switch in progress — retry shortly") - client = self.active_client + # A supplied serving identity is authoritative even when its + # client is None; truthiness-based fallback would silently switch + # providers after capture. + serving = ( + serving_identity + if serving_identity is not None + else self.capture_serving_identity() + ) + client = serving.client if client is None: raise RuntimeError("No LLM provider configured") self.inflight_requests += 1 - provider_cfg = getattr(self.get_config(), "llm_provider", None) - active = provider_cfg.active_provider if provider_cfg else "codex" - guard_key = f"llm_{active}" + guard_key = f"llm_{serving.provider}" if self.subsystem_guard is not None: err = self.subsystem_guard.check(guard_key) if err: diff --git a/src/discord/native_tools/agents_tasks.py b/src/discord/native_tools/agents_tasks.py index e6c3d47f..ffcce5a7 100644 --- a/src/discord/native_tools/agents_tasks.py +++ b/src/discord/native_tools/agents_tasks.py @@ -115,14 +115,22 @@ def _parse_spawn_overrides( from ...config.schema import CODEX_REASONING_EFFORTS if "model" in inp and model_mode != "auto": - return None, None, ( - "model is not accepted because Agent Model is not set to Auto — select " - "'Auto — choose per spawn' in the WebUI to allow per-spawn model selection" + return ( + None, + None, + ( + "model is not accepted because Agent Model is not set to Auto — select " + "'Auto — choose per spawn' in the WebUI to allow per-spawn model selection" + ), ) if "reasoning_effort" in inp and effort_mode != "auto": - return None, None, ( - "reasoning_effort is not accepted because Agent Reasoning is not set to Auto — " - "select 'Auto — choose per spawn' in the WebUI to allow per-spawn effort selection" + return ( + None, + None, + ( + "reasoning_effort is not accepted because Agent Reasoning is not set to Auto — " + "select 'Auto — choose per spawn' in the WebUI to allow per-spawn effort selection" + ), ) raw_model = inp.get("model") @@ -165,6 +173,172 @@ def _spawn_pair_error( return effort_incompatibility_error(model_now, effort_now) +def _observer_clamp(observer, model) -> int | None: + """Total clamp lookup — a broken observer never breaks a spawn.""" + if observer is None: + return None + try: + return observer.active_clamp(model) + except Exception: + log.exception("active_clamp failed (non-fatal); treating as unclamped") + return None + + +def _generation_budget_snapshot(cfg, client, resolved_model, compressor, observer=None): + """The frozen generation's budget snapshot, from the SAME identity capture + as the request (collision-gated: non-Codex clients get unknown-model + math regardless of what their model is named).""" + from ...llm.context_budget import snapshot_for_codex_config + + if hasattr(client, "reasoning_effort"): + model_for_budget = resolved_model or getattr(client, "model", None) + else: + model_for_budget = None + return snapshot_for_codex_config( + model_for_budget, + getattr(cfg, "openai_codex", None), + max_context_chars=(getattr(compressor, "max_context_chars", None) if compressor else None), + observed_clamp=_observer_clamp(observer, model_for_budget), + ) + + +def _gateway_serving_for_config(gateway, config): + """Resolve one serving identity against an already-read root config. + + Production gateways return the immutable provider/client/model/effort + tuple. Narrow test doubles retain their historical ``active_client`` shape; + the generation-plan builder normalizes that legacy value conservatively. + """ + capture = getattr(gateway, "capture_serving_identity", None) + if capture is not None: + return capture(config) + return gateway.active_client + + +def _capture_agent_generation_plan( + get_config, + get_serving, + get_compressor, + *, + model_override: str | None, + effort_override: str | None, + observer=None, +) -> dict: + """Capture one immutable agent-generation identity and budget plan. + + The root configuration is read exactly once. Request policy and the + context-budget snapshot are therefore derived from the same config object, + rather than straddling a live config replacement. ``effort`` is the + resolved effective value for Codex-shaped clients; the ``None`` inherit + sentinel never enters a frozen Codex plan, where a later in-place client + mutation could otherwise change a rescue retry. + """ + cfg = get_config() + serving = get_serving(cfg) + if hasattr(serving, "client") and hasattr(serving, "provider"): + provider = serving.provider + client = serving.client + else: + client = serving + provider = ( + "codex" + if client is None or hasattr(client, "reasoning_effort") + else str(getattr(client, "provider_name", "unknown")) + ) + requested_effort, resolved_model = _agent_llm_policy( + cfg, + client, + model_override=model_override, + effort_override=effort_override, + ) + effective_effort = requested_effort + if effective_effort is None and hasattr(client, "reasoning_effort"): + effective_effort = getattr(client, "reasoning_effort", None) + if effective_effort is None and hasattr(client, "reasoning_effort"): + raise ValueError("Codex client has no resolved reasoning effort") + return { + "provider": provider, + "client": client, + "effort": effective_effort, + "model": resolved_model, + "snapshot": _generation_budget_snapshot( + cfg, + client, + resolved_model, + get_compressor(), + observer=observer, + ), + } + + +def _make_budget_snapshot_provider( + get_config, get_client, get_compressor, model_override, observer=None +): + """Per-generation context-budget snapshot for a spawned agent. + + Resolves the EFFECTIVE agent model exactly like the iteration callback + (override fixed for life; None tracks live config at call time) and + derives the budget snapshot the manager compacts against. Overrides and + utilization are live reads; the explicit character ceiling comes from the + boot-frozen compression object so its restart-bound classification stays + truthful. Total: any resolution failure surfaces in the manager as the + documented fallback, never as an agent failure. + """ + + def provider(): + from ...llm.context_budget import snapshot_for_codex_config + + cfg = get_config() + client = get_client() + _, resolved_model = _agent_llm_policy( + cfg, client, model_override=model_override, effort_override=None + ) + compressor = get_compressor() + # Provider identity gates the registry: a non-Codex client whose + # model happens to be NAMED like a Codex slug (an Ollama model tagged + # "gpt-5.6-sol") must get conservative unknown-model math, never a + # Codex capability floor. + if hasattr(client, "reasoning_effort"): + model_for_budget = resolved_model or getattr(client, "model", None) + else: + model_for_budget = None + return snapshot_for_codex_config( + model_for_budget, + getattr(cfg, "openai_codex", None), + max_context_chars=( + getattr(compressor, "max_context_chars", None) if compressor else None + ), + observed_clamp=_observer_clamp(observer, model_for_budget), + ) + + return provider + + +def _make_evidence_recorder(observer): + """Adapter feeding an agent rescue's (overflow, response-dict) pair to + the observer. The agent callback returns a plain dict, so the acceptance + facts are lifted into the attribute shape ``record_rescue`` reads. + Total — evidence never fails the iteration that just succeeded.""" + if observer is None: + return None + + async def recorder(overflow, response): + try: + if isinstance(response, dict): + from types import SimpleNamespace + + response = SimpleNamespace( + account_key=response.get("account_key"), + server_input_tokens=response.get("server_input_tokens"), + provenance_model=response.get("model"), + ) + await observer.record_rescue(overflow=overflow, response=response) + except Exception: + log.exception("agent window-evidence recording failed (non-fatal)") + + return recorder + + def _provenance_stamp(resp: object, client: object) -> dict: """Execution-provenance fields for an iteration record, from the response. @@ -208,11 +382,15 @@ class AgentTaskDeps: turn_recorder: TurnRecorder # lifecycle webhook emission prompt_builder: PromptBuilder tool_catalog: ToolCatalog + # Passive window observer (phase 5): clamp source for agent budget + # snapshots + evidence sink for agent rescues. None = feature-inert. + window_observer: object | None = None class AgentTaskTools: def __init__(self, deps: AgentTaskDeps) -> None: self._get_config = deps.get_config + self._window_observer = deps.window_observer self._llm_gateway = deps.llm_gateway self._channel_state = deps.channel_state self._tool_executor = deps.tool_executor @@ -422,12 +600,14 @@ async def _iteration_cb( prompt: str, channel: object, prev_context: str | None, + cancel_event: asyncio.Event, ) -> str: return await self._tool_loop.run_autonomous( prompt, channel, prev_context, str(message.author.id), + cancel_event=cancel_event, ) result = self._loop_manager.start_loop( @@ -466,12 +646,12 @@ async def _iteration_cb( f"(every {max(10, interval)}s, mode={mode}, max {max_iterations} iterations)" ) - def _handle_stop_loop(self, inp: dict) -> str: + async def _handle_stop_loop(self, inp: dict) -> str: """Stop an autonomous loop.""" loop_id = inp.get("loop_id", "") if not loop_id: return "A 'loop_id' is required." - result = self._loop_manager.stop_loop(loop_id) + result = await self._loop_manager.stop_loop(loop_id) # Lifecycle webhook: loop.stopped fire_and_forget( self._turn_recorder._emit_lifecycle_event( @@ -498,8 +678,9 @@ async def _agent_generate( messages: list[dict], sys_prompt: str, tool_defs: list[dict], - agent_effort, + agent_effort: str, resolved_model, + provider: str = "codex", ): """One agent LLM generation through the shared recovery policy. @@ -518,17 +699,20 @@ async def _agent_generate( # xhigh→max — could turn an approved gpt-5.5@xhigh into a rejected # gpt-5.5@max at request build). Live config still reaches agents on # their NEXT iteration, the contract these callbacks document. - effective_effort = ( - agent_effort - if agent_effort is not None - else getattr(client, "reasoning_effort", None) - ) + effective_effort = agent_effort + # The production callback contract always supplies a concrete resolved + # effort for Codex-shaped clients; accepting the inherit sentinel here + # would reintroduce live client reads on later physical retries. + if effective_effort is None and hasattr(client, "reasoning_effort"): + raise ValueError("agent generation plan has unresolved reasoning effort") # Pre-admission: validate the exact pair this generation will request # before touching the breaker — never wait out an open breaker's # deadline (or count a capacity failure) for a request that could not # legally be sent. preflight_incompatible_effort(client, model=resolved_model, effort=effective_effort) - breaker = self._llm_gateway.capacity_breaker_for(resolved_model) + breaker = self._llm_gateway.capacity_breaker_for( + resolved_model, provider=provider + ) policy = self._llm_gateway.recovery_policy() async def _attempt(): @@ -543,9 +727,7 @@ async def _attempt(): resp = await generate_with_recovery(_attempt, policy=policy, breaker=breaker) # Bypass-path success clears a latched llm_* guard key — provenance # only, never the post-await active provider. - self._llm_gateway.notify_generation_success( - getattr(resp, "provenance_provider", None) - ) + self._llm_gateway.notify_generation_success(getattr(resp, "provenance_provider", None)) return resp async def _handle_spawn_agent(self, message: object, inp: dict) -> str: @@ -613,37 +795,51 @@ async def _handle_spawn_agent(self, message: object, inp: dict) -> str: if parent is not None else configured_max_depth ) - tools = filter_agent_tools( - all_tools, depth=parent_depth, max_depth=effective_max_depth - ) + tools = filter_agent_tools(all_tools, depth=parent_depth, max_depth=effective_max_depth) # Iteration callback — wraps Codex chat_with_tools, returns dict async def _iteration_cb( messages: list[dict], sys_prompt: str, tool_defs: list[dict], + *, + generation_state: dict, ) -> dict: - # Resolve the client ONCE per call so the provider/model/effort - # stamp describes the client this request actually went to, and - # read the agent policy from live config at call time (a WebUI - # change reaches in-flight agents on their next iteration). - client = self._llm_gateway.active_client - agent_effort, resolved_model = _agent_llm_policy( - self._get_config(), client, - model_override=model_override, effort_override=effort_override, - ) + # ONE capture per logical generation: client, model, effort, and + # budget snapshot are resolved together on the FIRST attempt and + # reused verbatim by every rescue retry (R2 frozen-generation + # identity — a live reload between attempts must never split the + # budget from the request it governs). Live config reaches the + # NEXT generation, which starts with a fresh state dict. + plan = generation_state.get("plan") + if plan is None: + plan = _capture_agent_generation_plan( + self._get_config, + lambda config: _gateway_serving_for_config(self._llm_gateway, config), + self._get_context_compressor, + model_override=model_override, + effort_override=effort_override, + observer=self._window_observer, + ) + generation_state["plan"] = plan + client = plan["client"] resp = await self._agent_generate( client, messages=messages, sys_prompt=sys_prompt, tool_defs=tool_defs, - agent_effort=agent_effort, - resolved_model=resolved_model, + agent_effort=plan["effort"], + resolved_model=plan["model"], + provider=plan["provider"], ) return { "text": resp.text, "tool_calls": [{"name": tc.name, "input": tc.input} for tc in resp.tool_calls], "stop_reason": resp.stop_reason, + # Phase 5: server acceptance evidence rides the callback dict + # so a rescued iteration can qualify a window clamp. + "server_input_tokens": getattr(resp, "server_input_tokens", None), + "account_key": getattr(resp, "account_key", None), **_provenance_stamp(resp, client), } @@ -699,9 +895,7 @@ async def _tool_exec_cb(tool_name: str, tool_input: dict) -> str: iteration_timeout = ( getattr(agents_cfg, "iteration_timeout_seconds", 900) if agents_cfg else 900 ) - max_lifetime = ( - getattr(agents_cfg, "max_lifetime_seconds", 14400) if agents_cfg else 14400 - ) + max_lifetime = getattr(agents_cfg, "max_lifetime_seconds", 14400) if agents_cfg else 14400 agent_id = self._agent_manager.spawn( label=label, @@ -730,12 +924,21 @@ async def _tool_exec_cb(tool_name: str, tool_input: dict) -> str: model_override=model_override, reasoning_effort_override=effort_override, context_compression_enabled=bool(self._get_context_compressor()), - max_context_chars=self._get_context_compressor().max_context_chars + max_context_chars=self._get_context_compressor().resolved_max_context_chars if self._get_context_compressor() else 750000, keep_recent_iterations=self._get_context_compressor().keep_recent_iterations if self._get_context_compressor() else 30, + generation_plan_provider=lambda: _capture_agent_generation_plan( + self._get_config, + lambda config: _gateway_serving_for_config(self._llm_gateway, config), + self._get_context_compressor, + model_override=model_override, + effort_override=effort_override, + observer=self._window_observer, + ), + evidence_recorder=_make_evidence_recorder(self._window_observer), ) if agent_id.startswith("Error"): @@ -888,9 +1091,7 @@ async def _handle_wait_for_agents(self, inp: dict) -> str: # stays excluded — hashing elapsed time would make a hung # agent immortal. iters = r.get("iteration_count", 0) - lines.append( - f"**{label}** (`{aid}`): {status} [iterations={iters}]\n{content}" - ) + lines.append(f"**{label}** (`{aid}`): {status} [iterations={iters}]\n{content}") return "\n\n".join(lines) if lines else "No results." @@ -948,12 +1149,14 @@ async def _handle_spawn_loop_agents(self, message: object, inp: dict) -> str: ) if pair_err: return f"Error: task '{t.get('label', '?')}': {pair_err}" - validated_tasks.append({ - "label": t.get("label", ""), - "goal": t.get("goal", ""), - "model_override": mo, - "reasoning_effort_override": eo, - }) + validated_tasks.append( + { + "label": t.get("label", ""), + "goal": t.get("goal", ""), + "model_override": mo, + "reasoning_effort_override": eo, + } + ) tasks = validated_tasks # Per-task iteration callback FACTORY (same pattern as @@ -961,19 +1164,28 @@ async def _handle_spawn_loop_agents(self, message: object, inp: dict) -> str: # model/effort override, so a fleet can mix models. Overrides are fixed # for the agent's life; None fields track live config at call time. def _make_iteration_cb(model_override, effort_override): - async def _iteration_cb(messages, sys, tool_defs): - client = self._llm_gateway.active_client - agent_effort, resolved_model = _agent_llm_policy( - self._get_config(), client, - model_override=model_override, effort_override=effort_override, - ) + async def _iteration_cb(messages, sys, tool_defs, *, generation_state: dict): + # Same frozen-generation capture as the direct spawn path. + plan = generation_state.get("plan") + if plan is None: + plan = _capture_agent_generation_plan( + self._get_config, + lambda config: _gateway_serving_for_config(self._llm_gateway, config), + self._get_context_compressor, + model_override=model_override, + effort_override=effort_override, + observer=self._window_observer, + ) + generation_state["plan"] = plan + client = plan["client"] resp = await self._agent_generate( client, messages=messages, sys_prompt=sys, tool_defs=tool_defs, - agent_effort=agent_effort, - resolved_model=resolved_model, + agent_effort=plan["effort"], + resolved_model=plan["model"], + provider=plan["provider"], ) return { "text": resp.text or "", @@ -981,8 +1193,11 @@ async def _iteration_cb(messages, sys, tool_defs): {"name": tc.name, "input": tc.input} for tc in (resp.tool_calls or []) ], "stop_reason": resp.stop_reason or "end_turn", + "server_input_tokens": getattr(resp, "server_input_tokens", None), + "account_key": getattr(resp, "account_key", None), **_provenance_stamp(resp, client), } + return _iteration_cb async def _tool_cb(tool_name, tool_input): @@ -1021,15 +1236,24 @@ async def _tool_cb(tool_name, tool_input): max_lifetime=self._get_config().agents.max_lifetime_seconds, # Close the loop-path gap: depth and child limits now reach # loop-spawned agents too, instead of silently using built-ins. - max_depth=getattr( - self._get_config().agents, "max_nesting_depth", None - ), - max_children=getattr( - self._get_config().agents, "max_children_per_agent", None - ), + max_depth=getattr(self._get_config().agents, "max_nesting_depth", None), + max_children=getattr(self._get_config().agents, "max_children_per_agent", None), context_compression_enabled=bool(cc), - max_context_chars=cc.max_context_chars if cc else 750000, + max_context_chars=cc.resolved_max_context_chars if cc else 750000, keep_recent_iterations=cc.keep_recent_iterations if cc else 30, + generation_plan_provider_factory=lambda mo, eo: ( + lambda: _capture_agent_generation_plan( + self._get_config, + lambda config: _gateway_serving_for_config( + self._llm_gateway, config + ), + self._get_context_compressor, + model_override=mo, + effort_override=eo, + observer=self._window_observer, + ) + ), + evidence_recorder=_make_evidence_recorder(self._window_observer), ) # Format response diff --git a/src/discord/tool_loop.py b/src/discord/tool_loop.py index f4a46f10..82102212 100644 --- a/src/discord/tool_loop.py +++ b/src/discord/tool_loop.py @@ -38,7 +38,9 @@ from ..error_presentation import format_user_facing_error from ..llm import CircuitOpenError -from ..llm.errors import LLMCapacityError +from ..llm.context_budget import ContextBudgetSnapshot, snapshot_for_codex_config +from ..llm.context_compressor import SurfaceBoundary +from ..llm.errors import LLMCapacityError, LLMRequestError from ..llm.recovery import generate_with_recovery, preflight_incompatible_effort from ..llm.secret_scrubber import scrub_output_secrets from ..observability.correlation import get_turn, set_turn @@ -67,6 +69,7 @@ from .tool_catalog import ToolCatalog from .turn_recorder import TurnRecorder from .delivery import DISCORD_MAX_LEN, TOOL_STATUS_LABELS +from .llm_gateway import LLMServingIdentity from .response_guards import ( _CODE_HEDGING_RETRY_MSG, _CONTINUATION_MSG, @@ -97,6 +100,34 @@ log = get_logger("discord") + +def _serving_identity_for(gateway, config=None, fallback_client=None) -> LLMServingIdentity: + """Capture the serving identity, tolerating narrow test gateways. + + Production gateways expose ``capture_serving_identity`` (one root read); + fixtures that fake only ``active_client`` get an equivalent identity + built from that client so the freeze semantics still hold in tests. + """ + capture = getattr(gateway, "capture_serving_identity", None) + if capture is not None: + return capture(config) if config is not None else capture() + client = fallback_client or getattr(gateway, "active_client", None) + return LLMServingIdentity( + provider=( + "codex" + if client is None or hasattr(client, "reasoning_effort") + else getattr(client, "provider_name", "unknown") + ), + client=client, + model=getattr(client, "model", None) if client is not None else None, + reasoning_effort=( + getattr(client, "reasoning_effort", None) + if client is not None and hasattr(client, "reasoning_effort") + else None + ), + ) + + _LONG_TIMEOUT_TOOL_SET = frozenset({"claude_code"}) @@ -193,9 +224,7 @@ async def _best_effort_typing(channel): _TYPING_ATTEMPT_TIMEOUT, ) except Exception as exc: - log.warning( - "Typing indicator cleanup failed (non-fatal): %s", _error_summary(exc) - ) + log.warning("Typing indicator cleanup failed (non-fatal): %s", _error_summary(exc)) class _LoopMessageProxy: @@ -253,6 +282,18 @@ class LoopPolicy: llm_via_gateway: bool # chat: call_with_tools; autonomous: raw active client response_guards: bool completion_classifier: bool + # Context-budget campaign (phase 4) asymmetries — pinned so a later + # cleanup cannot "simplify" them into smoke: + overflow_recovery: bool # both surfaces rescue in-iteration since phase 4 + durable_recovery_checkpointing: bool # chat only (v3.67.0 turn store) + soft_compaction: bool # chat always had it; loops gained it in phase 4 + latch_scope: str # "turn" (durable chat turn) | "invocation" (one run_autonomous) + + +# Loop request shape is fixed: one protected autonomous prompt. Chat's +# protected envelope is dynamic — pre-tool control directives can be appended +# before the first tool cycle — so _ChatTurn carries its current envelope end. +_LOOP_ENVELOPE_LEN = 1 CHAT_POLICY = LoopPolicy( @@ -263,6 +304,10 @@ class LoopPolicy: llm_via_gateway=True, response_guards=True, completion_classifier=True, + overflow_recovery=True, + durable_recovery_checkpointing=True, + soft_compaction=True, + latch_scope="turn", ) AUTONOMOUS_POLICY = LoopPolicy( @@ -273,6 +318,10 @@ class LoopPolicy: llm_via_gateway=False, response_guards=False, completion_classifier=False, + overflow_recovery=True, + durable_recovery_checkpointing=False, + soft_compaction=True, + latch_scope="invocation", ) @@ -340,6 +389,23 @@ class _ChatTurn: _validation_required: bool = False _validation_retries: int = 0 _max_validation_retries: int = 2 + # Context-budget campaign (phase 4) — all PERSISTED (codec v3): + # the surface boundary as plain state (session history before + # request_start is elidable; the request envelope after it is + # protected), the durable-turn accepted-size latch, the rescue-ladder + # phase (a resumed generation continues at the NEXT rung, never + # re-arms rung 1), and the frozen generation identity FACTS + # (provider/model/effort/ladder — never process-local objects) so a + # suspend mid-recovery resumes the same logical generation. + _boundary_request_start: int = 0 + _boundary_elided_replay: int = 0 + _boundary_envelope_len: int | None = 0 + _char_latch: int | None = None + _rescue_passes: int = 0 + _gen_identity: dict | None = None + # Process-local, per-generation cache captured beside serving identity. + # Rebuilt from durable _gen_identity on resume; never serialized directly. + _generation_budget_snapshot: ContextBudgetSnapshot | None = None # Process-local durability handle (write-invariant driver). Classified # RECONSTRUCTED in the checkpoint codec: a resumed turn gets a fresh # handle bound to the resume lease, never a deserialized one. @@ -370,6 +436,16 @@ class _LoopTurn: final_text: str = "" completed_naturally: bool = False # True only when a tool-free turn ended the loop tool_calls_made: int = 0 + # Context-budget campaign (phase 4): the surface boundary for emergency + # recovery (prev_context replay elidable, current prompt protected), the + # per-run_autonomous-invocation accepted-size latch (a later scheduled + # iteration starts fresh — cross-iteration protection is the global + # clamp's job, not a stale local latch), recovery evidence for the + # trajectory, and the loop-local iteration index the soft pass guards on. + _boundary: SurfaceBoundary | None = None + _char_latch: int | None = None + context_recoveries: list = field(default_factory=list) + _iteration_index: int = 0 @dataclass(frozen=True) @@ -401,6 +477,9 @@ class ToolLoopDeps: # Called with (TurnKey, generation) when a turn suspends — wiring points # it at the resume manager's auto-resume registration. on_turn_suspended: Callable | None = None + # Passive window observer (phase 5): downward clamp source + rescue + # evidence sink. None = feature-inert (tests, minimal constructions). + window_observer: object | None = None class ToolLoopRunner: @@ -425,6 +504,7 @@ def __init__(self, deps: ToolLoopDeps) -> None: self._loop_manager = deps.loop_manager self._stuck_loop_tracker_cls = deps.stuck_loop_tracker_cls self._turn_store = deps.turn_store + self._window_observer = deps.window_observer self._on_turn_suspended = deps.on_turn_suspended # ------------------------------------------------------------------ @@ -470,9 +550,7 @@ async def run( "run it again. Send it as a new message if you want a " "fresh run." ), - "in_progress": ( - "This exact request is already being processed elsewhere." - ), + "in_progress": ("This exact request is already being processed elsewhere."), "resumable": ( "This request has preserved, resumable work — say " "`resume` to continue it instead of starting over." @@ -483,12 +561,12 @@ async def run( "execute it blind — try again shortly." ), } - text = notices.get( - st.durability.blocked, "This request cannot be re-run." - ) + text = notices.get(st.durability.blocked, "This request cannot be re-run.") log.warning( "Turn admission refused (%s) for message %s in channel %s", - st.durability.blocked, st._trajectory.message_id, st._ch_id, + st.durability.blocked, + st._trajectory.message_id, + st._ch_id, ) return (text, False, False, [], False) return await self._run_with_guards(st) @@ -507,9 +585,7 @@ async def run_resumed(self, st: _ChatTurn) -> tuple[str, bool, bool, list[str], await self._delivery.set_status("Resuming preserved work...", task_start=True) return await self._run_with_guards(st) - async def _run_with_guards( - self, st: _ChatTurn - ) -> tuple[str, bool, bool, list[str], bool]: + async def _run_with_guards(self, st: _ChatTurn) -> tuple[str, bool, bool, list[str], bool]: try: result = await self._run_chat_iterations(st) # Terminal bookkeeping (best-effort; a suspension already settled @@ -559,9 +635,7 @@ async def _run_with_guards( log.warning("Durable failure mark failed (non-fatal)") raise - async def _run_chat_iterations( - self, st: _ChatTurn - ) -> tuple[str, bool, bool, list[str], bool]: + async def _run_chat_iterations(self, st: _ChatTurn) -> tuple[str, bool, bool, list[str], bool]: """The chat iteration loop — every phase-method exit returns through here; unexpected escapes are handled by run()'s guard above. @@ -581,9 +655,36 @@ async def _run_chat_iterations( if st._cancel.is_set(): return self._stopped(st, "iteration_start") - self._maybe_compress(st) + # ONE capture of this uninterrupted generation's serving + # identity: soft compaction, preflight, breaker admission, and + # every physical retry all describe this exact client/model/effort. + # Durable suspend/resume persistence remains phase 4. Capture the + # serving identity and ONE budget snapshot together; soft policy + # and rescue must never observe different clamp generations. + config = self._get_config() + serving = self._llm_gateway.capture_serving_identity(config) + if st._gen_identity: + budget_snapshot = self._snapshot_from_generation_facts(st._gen_identity) + else: + budget_snapshot = self._capture_budget_snapshot(serving, config) + st._generation_budget_snapshot = budget_snapshot + if not self._maybe_compress(st, serving.client, config): + return await self._llm_error_done( + st, + LLMRequestError( + "protected request envelope exceeds the accepted context latch", + provider=serving.provider, + model=serving.model, + code="context_length_exceeded", + ), + ) - kind, val = await self._call_llm(st) + kind, val = await self._call_llm( + st, + serving_identity=serving, + request_config=config, + budget_snapshot=budget_snapshot, + ) if kind == "done": return val llm_resp = val @@ -617,9 +718,7 @@ async def _run_chat_iterations( # Build internal-format assistant content from LLMResponse # (the R5-extracted fragment; the pre-carve chat body inlined # the byte-identical block). - st.messages.append( - {"role": "assistant", "content": build_assistant_content(llm_resp)} - ) + st.messages.append({"role": "assistant", "content": build_assistant_content(llm_resp)}) tool_calls = llm_resp.tool_calls st.tools_used_in_loop.extend(t.name for t in tool_calls) @@ -690,8 +789,7 @@ async def _prepare_chat_turn( message.webhook_id and str(message.webhook_id) in _ALLOWED_WEBHOOK_IDS ) is_bot_message = bool( - getattr(message.author, "bot", False) - and (_respond_to_bots or is_allowed_webhook) + getattr(message.author, "bot", False) and (_respond_to_bots or is_allowed_webhook) ) else: # Intake already made the admission decision. Do not re-resolve a @@ -780,9 +878,7 @@ async def _prepare_chat_turn( trace.provider( name=getattr(provider_cfg, "active_provider", "codex") if provider_cfg else "codex", model=getattr(self._llm_gateway.active_client, "model", "") or "", - reasoning_effort=getattr( - self._llm_gateway.active_client, "reasoning_effort", None - ), + reasoning_effort=getattr(self._llm_gateway.active_client, "reasoning_effort", None), ) _turn_ctx = get_turn() or {} _trajectory = TrajectoryTurn( @@ -833,6 +929,11 @@ async def _prepare_chat_turn( return _ChatTurn( message=message, + # The request envelope is always the final two messages here + # (preamble + current user request); everything before them is + # replayed session history — the elidable side of the boundary. + _boundary_request_start=max(0, len(messages) - 2), + _boundary_envelope_len=2, policy=policy, trace=trace, system_prompt=system_prompt, @@ -849,6 +950,30 @@ async def _prepare_chat_turn( durability=durability, ) + def _observed_clamp(self, model: object) -> int | None: + """The window observer's active clamp for ``model`` (phase 5).""" + observer = getattr(self, "_window_observer", None) + if observer is None: + return None + try: + return observer.active_clamp(model) + except Exception: + log.exception("active_clamp failed (non-fatal); treating as unclamped") + return None + + async def _record_window_evidence(self, overflow: object, response: object) -> None: + """Feed one rescue's overflow→acceptance pair to the observer. + + Total: evidence is never worth failing the request that just + succeeded (plan §11 invariant).""" + observer = getattr(self, "_window_observer", None) + if observer is None or overflow is None: + return + try: + await observer.record_rescue(overflow=overflow, response=response) + except Exception: + log.exception("window-evidence recording failed (non-fatal)") + def _clear_active(self, st: _ChatTurn) -> None: self._channel_state.clear_active_request(st._ch_id, st._req_id) @@ -873,33 +998,148 @@ def _stopped(self, st: _ChatTurn, where: str) -> tuple[str, bool, bool, list[str False, ) - def _maybe_compress(self, st: _ChatTurn) -> None: - """Context auto-compression — when accumulated tool iterations push - the message list over the configured budget, summarise older - iterations into a single text message and keep the most recent N - iterations intact.""" - if self._get_context_compressor() is not None and st.iteration > 0: - try: - from ..llm.context_compressor import ( - compress_tool_context, - estimate_message_chars, + @staticmethod + def _snapshot_from_generation_facts(facts: dict) -> ContextBudgetSnapshot: + payload = facts.get("budget") or {} + primary = payload.get("primary_chars", 0) + return ContextBudgetSnapshot( + canonical_model=facts.get("model", ""), + base_budget=0, + base_source="persisted", + effective_budget=0, + clamp_applied=False, + working_budget=0, + compactable_tokens=0, + derived_chars=primary, + primary_chars=primary, + ceiling_applied=False, + ladder=tuple(facts.get("ladder") or ()), + ) + + def _capture_budget_snapshot(self, serving, config) -> ContextBudgetSnapshot: + """Capture one budget snapshot beside one serving identity.""" + compressor = self._get_context_compressor() + model_for_budget = serving.model if serving.is_codex else None + return snapshot_for_codex_config( + model_for_budget, + getattr(config, "openai_codex", None), + max_context_chars=(compressor.max_context_chars if compressor is not None else None), + observed_clamp=self._observed_clamp(model_for_budget), + ) + + def _maybe_compress( + self, + st: _ChatTurn, + request_client: object = None, + request_config: object = None, + *, + budget_snapshot=None, + ) -> bool: + """Apply optional soft compaction and the mandatory accepted-size latch. + + Latch enforcement is recovery state: it runs even when soft + compression is disabled and on restored iteration zero. Both passes + use the surface-declared boundary, never content heuristics. Ordinary + soft-compaction failures remain non-fatal; latch failures fail closed + because resending a size already refused by the server is forbidden. + """ + latch = getattr(st, "_char_latch", None) + if budget_snapshot is None: + budget_snapshot = getattr(st, "_generation_budget_snapshot", None) + try: + from ..llm.context_compressor import ( + SurfaceBoundary, + compress_tool_context, + emergency_compress_for_window, + estimate_message_chars, + ) + + compressor = self._get_context_compressor() + if budget_snapshot is None: + if request_client is None and request_config is None: + request_client = self._llm_gateway.active_client + model_for_budget = ( + getattr(request_client, "model", None) + if hasattr(request_client, "reasoning_effort") + else None + ) + from ..llm import context_budget + + budget_snapshot = context_budget.snapshot_for_codex_config( + model_for_budget, + getattr( + request_config if request_config is not None else self._get_config(), + "openai_codex", + None, + ), + max_context_chars=( + compressor.max_context_chars if compressor is not None else None + ), + observed_clamp=self._observed_clamp(model_for_budget), ) + snapshot = budget_snapshot + boundary = SurfaceBoundary( + request_start=getattr(st, "_boundary_request_start", 0), + elided_replay=getattr(st, "_boundary_elided_replay", 0), + envelope_len=getattr(st, "_boundary_envelope_len", None), + ) + except Exception: + log.exception("context policy resolution failed") + return latch is None - _cc = self._get_context_compressor() - if estimate_message_chars(st.messages) > _cc.max_context_chars: - st.messages, _saved = compress_tool_context( - st.messages, - max_context_chars=_cc.max_context_chars, - keep_recent=_cc.keep_recent_iterations, - stats=self._get_compression_stats(), - ) - log.info("context_compressor: trimmed %d chars", _saved) - except Exception: - log.exception( - "context_compressor failed (non-fatal); continuing with full context" + if ( + compressor is not None + and st.iteration > 0 + and estimate_message_chars(st.messages) > snapshot.primary_chars + ): + try: + st.messages, saved = compress_tool_context( + st.messages, + max_context_chars=snapshot.primary_chars, + keep_recent=compressor.keep_recent_iterations, + stats=self._get_compression_stats(), + boundary=boundary, ) + log.info("context_compressor: trimmed %d chars", saved) + except Exception: + log.exception("context_compressor failed (non-fatal); continuing with full context") + + if latch is None: + return True + try: + latch_target = min(latch, snapshot.primary_chars) + if estimate_message_chars(st.messages) <= latch_target: + return True + st.messages, latch_report = emergency_compress_for_window( + st.messages, + target_chars=latch_target, + boundary=boundary, + ) + if latch_report.get("boundary_request_start") is not None: + st._boundary_request_start = latch_report["boundary_request_start"] + st._boundary_elided_replay = latch_report["boundary_elided_replay"] + latch_report["attempt"] = 0 + latch_report["trigger"] = "latch" + if st._trajectory is not None: + st._trajectory.context_recoveries.append(latch_report) + if not latch_report.get("fits"): + # The protected request itself exceeds a size already known + # to be survivable. Never resend the known-doomed payload. + return False + return True + except Exception: + log.exception("mandatory context latch enforcement failed; refusing request") + return False - async def _call_llm(self, st: _ChatTurn): + async def _call_llm( + self, + st: _ChatTurn, + request_client: object = None, + *, + serving_identity=None, + request_config: object = None, + budget_snapshot=None, + ): """Guarded LLM call with typing indicator and deadline-based recovery. Returns ("ok", llm_resp) or ("done", ). @@ -913,27 +1153,83 @@ async def _call_llm(self, st: _ChatTurn): interrupts any recovery wait immediately. """ _channel_id = str(st.message.channel.id) - # Pre-admission: a known-incompatible live pair fails fast — never - # deadline-wait on (or count against) a breaker for a request that - # could not legally be sent. - preflight_incompatible_effort(self._llm_gateway.active_client) - breaker = self._llm_gateway.capacity_breaker_for() + if st._gen_identity: + # A turn resumed MID-RECOVERY continues the SAME logical + # generation: the persisted identity FACTS select the provider, + # client, breaker key, and both request axes. The live serving + # identity is never spliced in — a live client wearing frozen + # axes is neither the frozen generation nor a coherent new one. + # If the frozen provider's client no longer exists, the + # generation ends honestly instead of switching providers. + _facts = st._gen_identity + _fact_provider = str(_facts.get("provider") or "") + _fact_client = { + "codex": getattr(self._llm_gateway, "codex_client", None), + "ollama": getattr(self._llm_gateway, "ollama_client", None), + "kimi": getattr(self._llm_gateway, "kimi_client", None), + }.get(_fact_provider) + if _fact_client is None: + return ( + "done", + await self._llm_error_done( + st, + LLMRequestError( + "resumed generation's provider " + f"'{_fact_provider or 'unknown'}' is no longer configured" + ), + ), + ) + serving_identity = LLMServingIdentity( + provider=_fact_provider, + client=_fact_client, + model=_facts.get("model"), + reasoning_effort=_facts.get("effort"), + ) + elif serving_identity is None: + serving_identity = _serving_identity_for( + self._llm_gateway, fallback_client=request_client + ) + request_client = serving_identity.client + # Pre-admission and breaker identity are frozen beside the client that + # every physical attempt will invoke. + preflight_incompatible_effort( + request_client, + model=serving_identity.model, + effort=serving_identity.reasoning_effort, + ) + breaker = self._llm_gateway.capacity_breaker_for( + serving_identity.model, provider=serving_identity.provider + ) policy = self._llm_gateway.recovery_policy() def _on_wait(wait: float, remaining: float, error: BaseException) -> None: log.info( "LLM recovery (%s): waiting %.1fs, %.0fs of generation budget left", - type(error).__name__, wait, remaining, + type(error).__name__, + wait, + remaining, ) + # Pin both Codex request axes. The client object's attributes are + # live-reloadable in place, so merely retaining the object is not an + # identity freeze. + pin_kwargs = {} + if serving_identity.is_codex: + if serving_identity.model: + pin_kwargs["model"] = serving_identity.model + if serving_identity.reasoning_effort is not None: + pin_kwargs["reasoning_effort"] = serving_identity.reasoning_effort + async def _attempt(): return await self._llm_gateway.call_with_tools( messages=st.messages, system=st.system_prompt, tools=st.tools or [], + **pin_kwargs, user_id=st.user_id, channel_id=_channel_id, tools_used=st.tools_used_in_loop, + serving_identity=serving_identity, ) # A resumed generation carries only its REMAINING budget (persisted @@ -941,27 +1237,155 @@ async def _attempt(): # gets one attempt, not a fresh window. Later generations budget # normally. resume_budget = st.durability.pop_resume_budget() - deadline_seconds = ( - policy.deadline_seconds if resume_budget is None else resume_budget - ) + deadline_seconds = policy.deadline_seconds if resume_budget is None else resume_budget # Persist the absolute recovery deadline BEFORE the call: a restart # mid-recovery reconstructs only the remaining budget, never a fresh # five minutes. await st.durability.on_generation_start(st, deadline_seconds) + # Rescue ladder for this logical generation. A turn resumed + # MID-RECOVERY reuses its persisted identity FACTS (provider/model/ + # effort/ladder) so the continued generation stays the same + # generation — and continues at the NEXT rung via the persisted + # st._rescue_passes, never re-arming rung one. + from ..llm.context_compressor import estimate_message_chars + + if st._gen_identity: + # The durable generation owns its exact budget snapshot; current + # observer/config state is irrelevant until the next generation. + _snapshot = self._snapshot_from_generation_facts(st._gen_identity) + elif budget_snapshot is not None: + _snapshot = budget_snapshot + else: + _root_config = request_config if request_config is not None else self._get_config() + _snapshot = self._capture_budget_snapshot(serving_identity, _root_config) + _ladder: tuple[int, ...] = _snapshot.ladder + _generation_deadline = time.monotonic() + deadline_seconds + _pending_latch: int | None = None + _rescued_this_call = False + _last_overflow: BaseException | None = None + if st._gen_identity and st._rescue_passes: + # Resume reconstructs the pending rejection from durable attempt + # facts. The already-compressed payload is the acceptance + # candidate; on success it publishes both the latch and evidence. + _pending_latch = estimate_message_chars(st.messages) + prior_attempts = st._gen_identity.get("attempts") or [] + prior = prior_attempts[-1] if prior_attempts else {} + _last_overflow = LLMRequestError( + "resumed structural context overflow", + provider=serving_identity.provider, + model=serving_identity.model, + code="context_length_exceeded", + account_key=prior.get("account_key"), + server_input_tokens=prior.get("server_input_tokens"), + ) + # Typing is best-effort (shared helper): a typing failure — setup or # cleanup — must never fail the call or misclassify provider errors. async with _best_effort_typing(st.message.channel): try: - llm_resp = await generate_with_recovery( - _attempt, - policy=policy, - breaker=breaker, - deadline_seconds=deadline_seconds, - cancel_event=st._cancel, - on_wait=_on_wait, - ) + while True: + try: + llm_resp = await generate_with_recovery( + _attempt, + policy=policy, + breaker=breaker, + deadline_seconds=( + deadline_seconds + if not _rescued_this_call + else _generation_deadline - time.monotonic() + ), + cancel_event=st._cancel, + on_wait=_on_wait, + ) + if _pending_latch is not None: + # Server-accepted evidence (the settled latch + # rule); the generation is settled, so its frozen + # facts and rung phase reset for the next one. + st._char_latch = _pending_latch + await self._record_window_evidence(_last_overflow, llm_resp) + if st._gen_identity is not None or st._rescue_passes: + st._gen_identity = None + st._rescue_passes = 0 + break + except LLMRequestError as overflow_exc: + if ( + getattr(overflow_exc, "code", None) != "context_length_exceeded" + or st._rescue_passes >= len(_ladder) + or _generation_deadline - time.monotonic() <= 0 + ): + raise + from ..llm.context_compressor import ( + emergency_compress_for_window, + ) + + target = _ladder[st._rescue_passes] + compressed, report = emergency_compress_for_window( + st.messages, + target_chars=target, + boundary=SurfaceBoundary( + request_start=st._boundary_request_start, + elided_replay=st._boundary_elided_replay, + envelope_len=getattr(st, "_boundary_envelope_len", None), + ), + ) + report["attempt"] = st._rescue_passes + 1 + report["trigger"] = "overflow" + if st._trajectory is not None: + st._trajectory.context_recoveries.append(report) + if not report.get("fits"): + raise + st.messages = compressed + st._rescue_passes += 1 + _rescued_this_call = True + _last_overflow = overflow_exc + if report.get("boundary_request_start") is not None: + st._boundary_request_start = report["boundary_request_start"] + st._boundary_elided_replay = report["boundary_elided_replay"] + if st._gen_identity is None: + st._gen_identity = { + "provider": serving_identity.provider, + "model": serving_identity.model, + "effort": serving_identity.reasoning_effort, + "ladder": list(_ladder), + "budget": ( + {"primary_chars": _snapshot.primary_chars} + if _snapshot is not None + else None + ), + "attempts": [], + } + st._gen_identity.setdefault("attempts", []).append( + { + "attempt": st._rescue_passes, + "account_key": getattr(overflow_exc, "account_key", None), + "server_input_tokens": getattr( + overflow_exc, "server_input_tokens", None + ), + } + ) + _pending_latch = report["compressed_chars"] + # Durable BEFORE the resend (contract §7): mutated + # transcript + boundary + rung phase checkpoint with + # progressed=False and the stored deadline untouched. + # A write failure PROPAGATES — the retry never runs + # ahead of what resume can reconstruct. + await st.durability.on_context_recovery(st) + if _generation_deadline - time.monotonic() <= 0: + # The deadline expired during compression or the + # durability write. generate_with_recovery admits + # one attempt regardless of budget (it bounds + # WAITING), so refusal must happen HERE — never + # start a physical request after expiry. + raise + log.warning( + "Chat context overflow: rescue pass %d compressed " + "%d -> %d chars; retrying generation", + st._rescue_passes, + report["original_chars"], + report["compressed_chars"], + ) except asyncio.CancelledError: if st._cancel.is_set(): # /stop fired during a recovery wait — the graceful stop @@ -1008,9 +1432,7 @@ async def _suspend_turn(self, st: _ChatTurn, cap_err: LLMCapacityError): if not preserved: return await self._llm_error_done(st, cap_err) - minutes = max( - 1, round(self._llm_gateway.recovery_policy().deadline_seconds / 60.0) - ) + minutes = max(1, round(self._llm_gateway.recovery_policy().deadline_seconds / 60.0)) model = cap_err.model or "The model" n_tools = len(st.tools_used_in_loop) text = ( @@ -1029,13 +1451,24 @@ async def _suspend_turn(self, st: _ChatTurn, cap_err: LLMCapacityError): self._clear_active(st) if self._on_turn_suspended is not None and st.durability.lease is not None: try: - self._on_turn_suspended( - st.durability.lease.key, st.durability.lease.generation - ) + self._on_turn_suspended(st.durability.lease.key, st.durability.lease.generation) except Exception: log.exception("Auto-resume registration failed (non-fatal)") return (text, False, True, st.tools_used_in_loop, False) + @staticmethod + def _append_pre_tool_control(st: _ChatTurn, message: dict) -> None: + """Append a pre-tool directive and extend the protected envelope.""" + if st._boundary_envelope_len is None: + from ..llm.context_compressor import _structural_envelope_end + + rest = st.messages[st._boundary_request_start :] + st._boundary_envelope_len = _structural_envelope_end(rest) + st.messages.append(message) + envelope_end = st._boundary_request_start + st._boundary_envelope_len + if not st.tools_used_in_loop and len(st.messages) == envelope_end + 1: + st._boundary_envelope_len += 1 + async def _check_stuck_and_record(self, st: _ChatTurn, llm_resp): """Record this iteration's tool calls + LLM text into the trajectory and stuck tracker; terminate or nudge on a confirmed repeat cycle. @@ -1046,8 +1479,7 @@ async def _check_stuck_and_record(self, st: _ChatTurn, llm_resp): from ..trajectories.saver import ToolIteration iter_tool_calls = [ - {"id": tc.id, "name": tc.name, "input": tc.input} - for tc in (llm_resp.tool_calls or []) + {"id": tc.id, "name": tc.name, "input": tc.input} for tc in (llm_resp.tool_calls or []) ] st._trajectory.iterations.append( ToolIteration( @@ -1103,14 +1535,15 @@ async def _check_stuck_and_record(self, st: _ChatTurn, llm_resp): else: st.stuck_tracker.warned = True log.info("Stuck pattern detected — injecting nudge") - st.messages.append( + self._append_pre_tool_control( + st, { "role": "developer", "content": ( "You appear to be repeating the same tool-call sequence. " "Try a different approach or summarise progress and stop." ), - } + }, ) return ("retry", None) return None @@ -1136,9 +1569,7 @@ async def _judge_entry_stuck(self, st: _ChatTurn): return None last_fp = st.stuck_tracker.last_fingerprint if st.stuck_tracker.warned: - log.warning( - "Restored tracker already confirmed-stuck — terminating before generation" - ) + log.warning("Restored tracker already confirmed-stuck — terminating before generation") await self._turn_recorder._save_turn_trajectory(st._trajectory, trace=st.trace) await self._turn_recorder._emit_lifecycle_event( "loop.stuck", @@ -1168,20 +1599,24 @@ async def _judge_entry_stuck(self, st: _ChatTurn): # Alive-ness rides IN the fingerprint (wait:mp:::…). parts = last_fp.split(":") alive = len(parts) > 3 and parts[3] == "running" - nudge = dict(_WAIT_PROCESS_NUDGE) if alive else { - "role": "developer", - "content": ( - "You are repeating the same call against a finished or " - "missing target and getting the same result. Act on the " - "result you already have, or report and stop." - ), - } + nudge = ( + dict(_WAIT_PROCESS_NUDGE) + if alive + else { + "role": "developer", + "content": ( + "You are repeating the same call against a finished or " + "missing target and getting the same result. Act on the " + "result you already have, or report and stop." + ), + } + ) else: # Pending is set only by wait iterations, so the last # fingerprint is always wait:* — mp handled above, agents here. nudge = dict(_WAIT_AGENTS_NUDGE) log.info("Pending wait judgment tripped at entry — injecting nudge before generation") - st.messages.append(nudge) + self._append_pre_tool_control(st, nudge) return ("retry", None) @staticmethod @@ -1202,9 +1637,7 @@ def _record_wait_fingerprint(self, st: _ChatTurn, tool_calls, tool_results) -> b Returns True iff this was a wait-class iteration. """ - iter_tool_calls = [ - {"id": tc.id, "name": tc.name, "input": tc.input} for tc in tool_calls - ] + iter_tool_calls = [{"id": tc.id, "name": tc.name, "input": tc.input} for tc in tool_calls] if not is_wait_iteration(iter_tool_calls): return False tc = tool_calls[0] @@ -1239,9 +1672,7 @@ async def _judge_wait_stuck(self, st: _ChatTurn, tool_calls, tool_results): if not st.stuck_tracker.check(): return None if st.stuck_tracker.warned: - log.warning( - "Frozen wait repetition confirmed after warning — terminating tool loop" - ) + log.warning("Frozen wait repetition confirmed after warning — terminating tool loop") await self._turn_recorder._save_turn_trajectory(st._trajectory, trace=st.trace) await self._turn_recorder._emit_lifecycle_event( "loop.stuck", @@ -1269,9 +1700,7 @@ async def _judge_wait_stuck(self, st: _ChatTurn, tool_calls, tool_results): ) st.stuck_tracker.warned = True if wait_target_alive(tc.name, result_text): - nudge = ( - _WAIT_PROCESS_NUDGE if tc.name == "manage_process" else _WAIT_AGENTS_NUDGE - ) + nudge = _WAIT_PROCESS_NUDGE if tc.name == "manage_process" else _WAIT_AGENTS_NUDGE log.info("Frozen wait pattern detected (target alive) — injecting wait nudge") else: # Terminal/error results repeating: the target is NOT alive, so @@ -1285,7 +1714,7 @@ async def _judge_wait_stuck(self, st: _ChatTurn, tool_calls, tool_results): ), } log.info("Frozen wait pattern detected (target not alive) — injecting nudge") - st.messages.append(dict(nudge)) + self._append_pre_tool_control(st, dict(nudge)) return ("retry", None) async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): @@ -1302,18 +1731,18 @@ async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): if st._validation_required and st._validation_retries < st._max_validation_retries: st._validation_retries += 1 log.warning( - "Validation required but model returned text — " - "forcing continuation (attempt %d)", + "Validation required but model returned text — forcing continuation (attempt %d)", st._validation_retries, ) - st.messages.append( + self._append_pre_tool_control( + st, { "role": "developer", "content": ( "[VALIDATION REQUIRED] You have pending post-action validation. " "Call validate_action before responding to the user." ), - } + }, ) return ("retry", None) @@ -1326,7 +1755,7 @@ async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): ): log.warning("Fabrication detected — retrying with correction") st.fabrication_retried = True - st.messages.append(_FABRICATION_RETRY_MSG) + self._append_pre_tool_control(st, _FABRICATION_RETRY_MSG) return ("retry", None) if ( @@ -1336,7 +1765,7 @@ async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): ): log.warning("Promise without action detected — retrying") st.promise_retried = True - st.messages.append(_PROMISE_RETRY_MSG) + self._append_pre_tool_control(st, _PROMISE_RETRY_MSG) return ("retry", None) if ( @@ -1346,7 +1775,7 @@ async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): ): log.warning("Tool-unavailability fabrication detected — retrying") st.unavail_retried = True - st.messages.append(_TOOL_UNAVAIL_RETRY_MSG) + self._append_pre_tool_control(st, _TOOL_UNAVAIL_RETRY_MSG) return ("retry", None) # Hedging detection: fires for ALL messages — Odin is an @@ -1358,7 +1787,7 @@ async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): ): log.warning("Hedging detected — retrying") st.hedging_retried = True - st.messages.append(_HEDGING_RETRY_MSG) + self._append_pre_tool_control(st, _HEDGING_RETRY_MSG) return ("retry", None) if ( @@ -1368,7 +1797,7 @@ async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): ): log.warning("Code-block hedging detected — retrying") st.code_hedging_retried = True - st.messages.append(_CODE_HEDGING_RETRY_MSG) + self._append_pre_tool_control(st, _CODE_HEDGING_RETRY_MSG) return ("retry", None) # Premature failure: tools were called but gave up after one error @@ -1379,7 +1808,7 @@ async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): ): log.warning("Premature failure detected — retrying") st.premature_failure_retried = True - st.messages.append(_FAILURE_RETRY_MSG) + self._append_pre_tool_control(st, _FAILURE_RETRY_MSG) return ("retry", None) # Tier 3: Completion classifier — uses LLM to judge whether @@ -1402,16 +1831,17 @@ async def _finalize_or_retry(self, st: _ChatTurn, llm_resp): # message — inject the continuation nudge alone so # the model responds fresh with tool calls. if reason: - st.messages.append( + self._append_pre_tool_control( + st, { "role": "developer", "content": ( f"You are not done. {reason}. Continue with tool calls now." ), - } + }, ) else: - st.messages.append(_CONTINUATION_MSG) + self._append_pre_tool_control(st, _CONTINUATION_MSG) st.continuation_count += 1 return ("retry", None) @@ -1472,9 +1902,7 @@ async def _run_one_tool(self, st: _ChatTurn, block) -> dict: block, ok=False, uncertain=False, result_text=_rbac_denial ) return {"type": "tool_result", "tool_use_id": block.id, "content": _rbac_denial} - await self._delivery.set_status( - TOOL_STATUS_LABELS.get(tool_name, f"Running: {tool_name}") - ) + await self._delivery.set_status(TOOL_STATUS_LABELS.get(tool_name, f"Running: {tool_name}")) try: await self._audit.log_event( @@ -1562,9 +1990,7 @@ async def _run_one_tool(self, st: _ChatTurn, block) -> dict: # Handle special image block return from analyze_image if isinstance(result, dict) and "__image_block__" in result: st.pending_image_blocks.append(result["__image_block__"]) - result = ( - f"[Image loaded. Analyze it with this instruction: {result['__prompt__']}]" - ) + result = f"[Image loaded. Analyze it with this instruction: {result['__prompt__']}]" # Scrub secrets from tool output result = scrub_output_secrets(result) @@ -1579,7 +2005,13 @@ async def _run_one_tool(self, st: _ChatTurn, block) -> dict: result = ensure_failure_visible(result, tool_result.ok) await self._audit_tool_outcome( - st, tool_name, tool_input, result, elapsed_ms, error, tool_result, + st, + tool_name, + tool_input, + result, + elapsed_ms, + error, + tool_result, call_id=block.id, ) @@ -1619,8 +2051,16 @@ async def _run_one_tool(self, st: _ChatTurn, block) -> dict: } async def _audit_tool_outcome( - self, st: _ChatTurn, tool_name, tool_input, result, elapsed_ms, error, tool_result, - *, call_id: str | None = None, + self, + st: _ChatTurn, + tool_name, + tool_input, + result, + elapsed_ms, + error, + tool_result, + *, + call_id: str | None = None, ) -> None: """Write execution + tool_end audit records — never crash tool execution on audit failure. (Inline block of the old `_run_tool`.)""" @@ -1873,6 +2313,8 @@ async def run_autonomous( prev_context: str | None, user_id: str, policy: LoopPolicy = AUTONOMOUS_POLICY, + *, + cancel_event: asyncio.Event | None = None, ) -> str: """Run a single loop iteration through Codex with full tool access. @@ -1885,7 +2327,23 @@ async def run_autonomous( st = self._prepare_loop_turn(prompt, channel, prev_context, user_id, policy) for _iteration in range(st.loop_cap): - kind, val = await self._call_loop_llm(st) + if cancel_event is not None and cancel_event.is_set(): + raise asyncio.CancelledError + st._iteration_index = _iteration + # ONE capture per uninterrupted loop generation (same contract as + # chat): compaction thresholds, preflight, breaker admission, and + # every physical retry describe this exact client/model/effort. + _config = self._get_config() + _serving = _serving_identity_for(self._llm_gateway, _config) + _budget_snapshot = self._capture_budget_snapshot(_serving, _config) + self._maybe_compress_loop(st, _serving, _config, budget_snapshot=_budget_snapshot) + kind, val = await self._call_loop_llm( + st, + serving_identity=_serving, + request_config=_config, + budget_snapshot=_budget_snapshot, + cancel_event=cancel_event, + ) if kind == "done": return val response = val @@ -1897,10 +2355,10 @@ async def run_autonomous( # Build assistant content with tool_use blocks (matches the chat # pipeline's format) - st.messages.append( - {"role": "assistant", "content": build_assistant_content(response)} - ) + st.messages.append({"role": "assistant", "content": build_assistant_content(response)}) + if cancel_event is not None and cancel_event.is_set(): + raise asyncio.CancelledError await self._execute_loop_tools(st, response) return await self._finalize_loop(st) @@ -1965,6 +2423,13 @@ def _prepare_loop_turn( } ) messages.append({"role": "user", "content": prompt}) + # Surface boundary: the prev-context exchange (two messages when + # present) is replayable context; the current autonomous prompt and + # everything after it is protected/iteration territory. + loop_boundary = SurfaceBoundary( + request_start=2 if prev_context else 0, + envelope_len=_LOOP_ENVELOPE_LEN, + ) # Build system prompt and tool definitions if _trace is not None: @@ -1988,6 +2453,7 @@ def _prepare_loop_turn( loop_cap = self._get_config().tools.max_tool_iterations_loop return _LoopTurn( + _boundary=loop_boundary, prompt=prompt, channel=channel, user_id=user_id, @@ -2018,6 +2484,10 @@ async def _finish_loop( """Persist the loop turn and run gated reflection at every exit. (The old `_finish` closure.)""" if st._trajectory is not None: + if st.context_recoveries: + # Evidence rides the SAVED artifact, not a working list that + # dies with the turn object (review round-1 blocker #3). + st._trajectory.context_recoveries = list(st.context_recoveries) await self._turn_recorder._save_turn_trajectory( st._trajectory, error=error_text if is_error else "", @@ -2037,39 +2507,204 @@ async def _finish_loop( ) return outcome_text - async def _call_loop_llm(self, st: _LoopTurn): + def _maybe_compress_loop(self, st: _LoopTurn, serving, config, *, budget_snapshot=None) -> None: + """Loop pre-send compaction (campaign phase 4 — loops previously had + NO soft path at all): the shared soft pass at the serving model's + derived target once tool iterations exist, plus the invocation-local + accepted-size latch compaction with the loop's surface boundary. + Non-fatal like every compaction guard.""" + compressor = self._get_context_compressor() + try: + from ..llm.context_compressor import ( + compress_tool_context, + emergency_compress_for_window, + estimate_message_chars, + ) + + snapshot = ( + budget_snapshot + if budget_snapshot is not None + else self._capture_budget_snapshot(serving, config) + ) + if ( + compressor is not None + and st._iteration_index > 0 + and estimate_message_chars(st.messages) > snapshot.primary_chars + ): + st.messages, _saved = compress_tool_context( + st.messages, + max_context_chars=snapshot.primary_chars, + keep_recent=compressor.keep_recent_iterations, + stats=self._get_compression_stats(), + boundary=st._boundary, + ) + log.info( + "loop context_compressor: compressed %d older tool iterations", + _saved, + ) + if st._char_latch is not None: + latch_target = min(st._char_latch, snapshot.primary_chars) + if estimate_message_chars(st.messages) > latch_target: + st.messages, latch_report = emergency_compress_for_window( + st.messages, + target_chars=latch_target, + boundary=st._boundary, + ) + if latch_report.get("boundary_request_start") is not None: + st._boundary = SurfaceBoundary( + request_start=latch_report["boundary_request_start"], + elided_replay=latch_report["boundary_elided_replay"], + envelope_len=_LOOP_ENVELOPE_LEN, + ) + latch_report["attempt"] = 0 + latch_report["trigger"] = "latch" + st.context_recoveries.append(latch_report) + except Exception: + log.exception("loop compaction failed (non-fatal); continuing with full context") + + async def _call_loop_llm( + self, + st: _LoopTurn, + *, + serving_identity=None, + request_config=None, + budget_snapshot=None, + cancel_event: asyncio.Event | None = None, + ): """LLM call for one loop iteration with deadline-based recovery. Typed capacity/transport failures are retried in-iteration by the shared recovery policy; CircuitOpenError still re-raises to the loop manager (policy asymmetry — the manager owns backoff between iterations). The gateway bypass itself is unchanged (RFC-001 §4.3). + Since phase 4, a structural context overflow rescues in-iteration: + boundary-aware emergency compression, then a retry of the SAME frozen + serving identity under the SAME monotonic deadline — rescue rungs + never mint fresh budget, and an exhausted ladder finalizes exactly + once through the existing failure path. Returns ("ok", response) or ("done", ). """ - breaker = self._llm_gateway.capacity_breaker_for() + if serving_identity is None: + serving_identity = _serving_identity_for(self._llm_gateway, request_config) + if request_config is None: + request_config = self._get_config() + breaker = self._llm_gateway.capacity_breaker_for( + serving_identity.model, provider=serving_identity.provider + ) policy = self._llm_gateway.recovery_policy() + pin_kwargs = {} + if serving_identity.is_codex: + if serving_identity.model: + pin_kwargs["model"] = serving_identity.model + if serving_identity.reasoning_effort is not None: + pin_kwargs["reasoning_effort"] = serving_identity.reasoning_effort + async def _attempt(): - return await self._llm_gateway.active_client.chat_with_tools( + if cancel_event is not None and cancel_event.is_set(): + raise asyncio.CancelledError + return await serving_identity.client.chat_with_tools( messages=st.messages, system=st.system_prompt, tools=st.tools or [], + **pin_kwargs, ) + _snapshot = ( + budget_snapshot + if budget_snapshot is not None + else self._capture_budget_snapshot(serving_identity, request_config) + ) + # ONE monotonic deadline for the whole logical generation: the first + # attempt runs on the policy's own budget; rescue retries pay for the + # time already burned instead of minting a fresh window. + generation_deadline = time.monotonic() + policy.deadline_seconds + rescue_passes = 0 + pending_latch: int | None = None + last_overflow: BaseException | None = None + try: # Pre-admission fast-fail, same contract as the chat path — and # INSIDE the try, so LLMRequestError completes the loop through # _finish_loop (trajectory + reflection finalization) exactly # like any other failed generation instead of escaping - # run_autonomous(). - preflight_incompatible_effort(self._llm_gateway.active_client) - response = await generate_with_recovery( - _attempt, - policy=policy, - breaker=breaker, - retry_circuit_open=False, - ) + # run_autonomous(). Frozen: the captured client, not live state. + while True: + try: + preflight_incompatible_effort( + serving_identity.client, + model=serving_identity.model, + effort=serving_identity.reasoning_effort, + ) + response = await generate_with_recovery( + _attempt, + policy=policy, + breaker=breaker, + retry_circuit_open=False, + deadline_seconds=( + None if rescue_passes == 0 else generation_deadline - time.monotonic() + ), + ) + if cancel_event is not None and cancel_event.is_set(): + raise asyncio.CancelledError + if pending_latch is not None: + # Server-accepted evidence, per the settled latch rule. + st._char_latch = pending_latch + await self._record_window_evidence(last_overflow, response) + break + except Exception as overflow_exc: + from ..llm.errors import LLMRequestError + + is_overflow = ( + isinstance(overflow_exc, LLMRequestError) + and getattr(overflow_exc, "code", None) == "context_length_exceeded" + ) + ladder = _snapshot.ladder + if ( + not is_overflow + or rescue_passes >= len(ladder) + or generation_deadline - time.monotonic() <= 0 + ): + raise + from ..llm.context_compressor import ( + SurfaceBoundary, + emergency_compress_for_window, + ) + + target = ladder[rescue_passes] + rescue_passes += 1 + compressed, report = emergency_compress_for_window( + st.messages, + target_chars=target, + boundary=st._boundary, + ) + report["attempt"] = rescue_passes + report["trigger"] = "overflow" + st.context_recoveries.append(report) + if not report.get("fits"): + raise + st.messages = compressed + if report.get("boundary_request_start") is not None: + st._boundary = SurfaceBoundary( + request_start=report["boundary_request_start"], + elided_replay=report["boundary_elided_replay"], + envelope_len=_LOOP_ENVELOPE_LEN, + ) + pending_latch = report["compressed_chars"] + last_overflow = overflow_exc + if generation_deadline - time.monotonic() <= 0: + # Same rule as chat: recovery deadlines bound waiting, + # not an admitted stream — never start a request + # after expiry. + raise + log.warning( + "Loop context overflow: rescue pass %d compressed " + "%d -> %d chars; retrying iteration", + rescue_passes, + report["original_chars"], + report["compressed_chars"], + ) except CircuitOpenError: raise except Exception as e: @@ -2092,9 +2727,7 @@ async def _attempt(): # Bypass-path success: clear a latched llm_* guard key using the # response's immutable provenance (never the post-await active # provider) — the production mark_available wiring. - self._llm_gateway.notify_generation_success( - getattr(response, "provenance_provider", None) - ) + self._llm_gateway.notify_generation_success(getattr(response, "provenance_provider", None)) return ("ok", response) def _record_loop_iteration(self, st: _LoopTurn, response, _iteration: int) -> bool: diff --git a/src/discord/wiring.py b/src/discord/wiring.py index e518275c..1b3a156a 100644 --- a/src/discord/wiring.py +++ b/src/discord/wiring.py @@ -39,6 +39,7 @@ from ..llm.cost_tracker import CostTracker from ..llm.model_breaker import ModelBreakerRegistry from ..llm.recovery import RecoveryPolicy +from ..llm.window_observer import WindowObserver from ..odin_log import get_logger from ..permissions import PermissionManager from ..permissions.host_access import HostAccessManager @@ -130,6 +131,7 @@ class BotServices: turn_store: TurnStateStore | None = None model_breakers: ModelBreakerRegistry | None = None recovery_policy_source: Callable[[], RecoveryPolicy] | None = None + window_observer: WindowObserver | None = None def build_services( @@ -150,9 +152,7 @@ def build_services( # root rather than the boot object; config updates replace bot.config. agent_manager = AgentManager( max_concurrent_agents_provider=( - (lambda: get_config().agents.max_concurrent_agents) - if get_config is not None - else None + (lambda: get_config().agents.max_concurrent_agents) if get_config is not None else None ) ) # Autonomous loop manager (agent-aware) @@ -443,6 +443,10 @@ def recovery_policy_source() -> RecoveryPolicy: if not turn_store.available: turn_store = None # init failed — feature off, logged loudly + # Passive context-window observer (phase 5): evidence + downward clamps. + # Construction is total — a broken store loads empty, never blocks boot. + window_observer = WindowObserver() + # Action diff tracker — records before→after diffs. Always on. diff_tracker = DiffTracker() @@ -574,6 +578,7 @@ def recovery_policy_source() -> RecoveryPolicy: turn_store=turn_store, model_breakers=model_breakers, recovery_policy_source=recovery_policy_source, + window_observer=window_observer, ) @@ -646,6 +651,17 @@ def build_components(bot, services: BotServices) -> BotComponents: recovery_policy_source=_live_recovery_policy_source(bot), ) + # Dependency-inverted clamp scope: the observer sees only an opaque-key + # snapshot supplied by the LIVE Codex client, never the auth pool itself. + if services.window_observer is not None: + services.window_observer.set_eligible_account_keys_provider( + lambda: ( + llm_gateway.codex_client.eligible_account_keys_snapshot() + if llm_gateway.codex_client is not None + else frozenset() + ) + ) + # Wire LLM callbacks to whichever provider is active if llm_gateway.active_client is not None: llm_gateway.wire_callbacks() @@ -773,6 +789,7 @@ def build_components(bot, services: BotServices) -> BotComponents: loop_manager=services.loop_manager, stuck_loop_tracker_cls=services.stuck_loop_tracker_cls, turn_store=services.turn_store, + window_observer=services.window_observer, ) ) agent_task_tools = AgentTaskTools( @@ -783,7 +800,7 @@ def build_components(bot, services: BotServices) -> BotComponents: tool_executor=services.tool_executor, skill_manager=services.skill_manager, # live: swappable at runtime via the bot's `knowledge` property - get_knowledge_store=lambda: bot.knowledge, + get_knowledge_store=lambda: bot.knowledge, embedder=services.embedder, audit=services.audit, agent_manager=services.agent_manager, @@ -797,6 +814,7 @@ def build_components(bot, services: BotServices) -> BotComponents: turn_recorder=turn_recorder, prompt_builder=prompt_builder, tool_catalog=tool_catalog, + window_observer=services.window_observer, ) ) # Second phase of the P5 owner wiring: the agents domain exists now. diff --git a/src/health/server.py b/src/health/server.py index 999d4d1c..bf428f54 100644 --- a/src/health/server.py +++ b/src/health/server.py @@ -66,6 +66,7 @@ "/api/mcp", "/api/tokens", "/api/personality", "/api/tools/timeouts", "/api/pools", "/api/outbound-webhooks", "/api/grafana-alerts", "/api/slack", + "/api/context", "/api/restart", ) diff --git a/src/llm/account_key.py b/src/llm/account_key.py new file mode 100644 index 00000000..cf679f37 --- /dev/null +++ b/src/llm/account_key.py @@ -0,0 +1,250 @@ +"""Opaque, installation-local account keys for provider evidence. + +The context-window observer correlates a rejection with its rescue +acceptance only when both landed on the SAME account — but raw account +identifiers must never be exposed or persisted in evidence, exceptions, or +logs. This module derives a deterministic, non-reversible, installation- +local key: HMAC-SHA256 of the stable non-secret account identifier (the +ChatGPT account id — an identity label, never token material), keyed by a +random per-installation secret established on first use. + +Contract (plan of record R2 §10/§11, hardened in PR #272 review round 1): + +- Same account + same installation ⇒ same key across restarts AND across + concurrent processes: first use runs an exclusive-winner protocol + (temp-file write + fsync, then an atomic hard-link publication that fails + if a winner already exists; losers read and use the winner's material, so + every process converges on the one durable secret). +- Persisted material is accepted only on the exact generated shape: a + regular file (final-component symlinks refused), owned by this process's + uid, mode 0600, exactly 32 bytes. Anything else fails CLOSED — a warning, + ``None``, and the questionable material left untouched (replacing it + would silently decorrelate every previously recorded observation). +- No stable account identity — including an identifier that cannot be + UTF-8 encoded, e.g. an unpaired surrogate smuggled through credentials + JSON — ⇒ ``None``, and the attempt is disqualified from account-scoped + evidence. Key trouble degrades evidence, never requests: the public + function is total and non-raising by construction. +- The key file lives under the runtime ``data`` directory; it never leaves + the installation, so keys from different installs never correlate. +""" + +from __future__ import annotations + +import contextlib +import hmac +import logging +import os +import secrets +import stat as stat_module +import tempfile +from dataclasses import dataclass +from hashlib import sha256 +from pathlib import Path + +log = logging.getLogger("odin.llm") + +DEFAULT_KEY_PATH = Path("data") / "account_key.secret" + +_KEY_BYTES = 32 +#: Hex prefix length: 128 bits of a keyed MAC — far beyond collision concern +#: for a handful of pool accounts, short enough to read in evidence files. +_KEY_HEX_LENGTH = 32 + +_key_cache: dict[Path, bytes] = {} + + +@dataclass(frozen=True) +class _KeyReadResult: + """Result of a strict key read, preserving missing vs refused. + + A missing path is the only state that permits an exclusive publication + attempt. Refused material must never be replaced. + """ + + material: bytes | None + missing: bool = False + + +def _read_established_key(key_path: Path) -> _KeyReadResult: + """Read existing material under the strict generated-shape contract. + + Returns a result that distinguishes "missing" from "present but + refused". That distinction is security-sensitive: only a witnessed + missing path may enter the exclusive publication protocol. Never + modifies questionable material. + """ + try: + # O_NONBLOCK is required before inspecting the descriptor. Opening + # a hostile FIFO read-only would otherwise block forever before the + # regular-file check had a chance to reject it. + fd = os.open( + key_path, + os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, + ) + except FileNotFoundError: + return _KeyReadResult(material=None, missing=True) + except OSError as exc: + # ELOOP here means the final component is a symlink — refused. + log.warning("Refusing account key %s: %s", key_path, exc) + return _KeyReadResult(material=None) + try: + info = os.fstat(fd) + if not stat_module.S_ISREG(info.st_mode): + log.warning("Refusing account key %s: not a regular file", key_path) + return _KeyReadResult(material=None) + if info.st_uid != os.getuid(): + log.warning("Refusing account key %s: not owned by this user", key_path) + return _KeyReadResult(material=None) + if stat_module.S_IMODE(info.st_mode) != 0o600: + log.warning( + "Refusing account key %s: mode %o is not 0600 — fix the " + "permissions to re-enable account-scoped evidence.", + key_path, + stat_module.S_IMODE(info.st_mode), + ) + return _KeyReadResult(material=None) + # Validate the opened object size before reading. Asking read() for + # one sentinel byte is not sufficient: a short read from an + # oversized file could otherwise masquerade as the exact shape. + if info.st_size != _KEY_BYTES: + log.warning( + "Refusing account key %s: %d bytes is not the generated " + "%d-byte shape.", + key_path, + info.st_size, + _KEY_BYTES, + ) + return _KeyReadResult(material=None) + material = os.read(fd, _KEY_BYTES) + if len(material) != _KEY_BYTES: + log.warning( + "Refusing account key %s: %d bytes is not the generated " + "%d-byte shape.", + key_path, + len(material), + _KEY_BYTES, + ) + return _KeyReadResult(material=None) + return _KeyReadResult(material=material) + except OSError as exc: + log.warning("Could not read account key %s: %s", key_path, exc) + return _KeyReadResult(material=None) + finally: + with contextlib.suppress(OSError): + os.close(fd) + + +def _fsync_parent(key_path: Path) -> None: + """Best-effort directory fsync so the publication survives a crash.""" + with contextlib.suppress(OSError): + directory_fd = os.open(key_path.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + + +def _create_key(key_path: Path) -> bytes | None: + """Establish the installation key with an exclusive-winner protocol. + + The complete material is written and fsynced to a private temp file, + then published with ``os.link`` — atomic and fail-if-exists, so exactly + one process wins. Losers converge by reading the winner's file. A crash + can only ever leave a stray temp file, never a partial key. + """ + material = secrets.token_bytes(_KEY_BYTES) + try: + key_path.parent.mkdir(parents=True, exist_ok=True) + fd, temporary_name = tempfile.mkstemp( + dir=key_path.parent, prefix=f".{key_path.name}.", suffix=".tmp" + ) + temporary = Path(temporary_name) + # mkstemp gives this function ownership. Ownership transfers only + # after fdopen returns successfully; every earlier failure must close + # the raw descriptor explicitly. + owned_fd = fd + owns_fd = True + try: + os.fchmod(owned_fd, 0o600) + stream = os.fdopen(owned_fd, "wb") + owns_fd = False + with stream: + stream.write(material) + stream.flush() + os.fsync(stream.fileno()) + try: + os.link(temporary, key_path) + except FileExistsError: + # Another process won the race: use ITS material so every + # process MACs with the one durable secret. + return _read_established_key(key_path).material + _fsync_parent(key_path) + return material + finally: + if owns_fd: + with contextlib.suppress(OSError): + os.close(owned_fd) + with contextlib.suppress(OSError): + temporary.unlink() + except OSError as exc: + log.warning( + "Could not create account key %s: %s — account-scoped evidence " + "is skipped this boot.", + key_path, + exc, + ) + return None + + +def _load_or_create_key(key_path: Path) -> bytes | None: + cached = _key_cache.get(key_path) + if cached is not None: + return cached + read_result = _read_established_key(key_path) + material = read_result.material + if read_result.missing: + # Do not re-check with exists(): existence is only a snapshot and + # creates an ENOENT-to-publication race. Always enter the atomic + # fail-if-exists protocol after a witnessed miss; EEXIST adopts the + # winner, while refused existing material never reaches this branch. + material = _create_key(key_path) + if material is not None: + _key_cache[key_path] = material + return material + + +def opaque_account_key( + account_id: str | None, *, key_path: str | Path | None = None +) -> str | None: + """Derive the opaque key for ``account_id``; ``None`` disqualifies. + + Total and non-raising: any failure to establish key material or to + normalize the identity logs and returns ``None`` — evidence is + forfeited, the request is never affected. ``key_path`` defaults to + ``DEFAULT_KEY_PATH`` resolved at CALL time so tests can repoint the + module default (a def-time bound default would ignore the monkeypatch + and write into the working tree). + """ + try: + if not account_id or not str(account_id).strip(): + return None + try: + identity = str(account_id).strip().encode("utf-8") + except UnicodeError: + # E.g. an unpaired surrogate accepted by json.loads: there is no + # stable canonical encoding, so there is no stable identity. + log.warning( + "Account identifier is not UTF-8 encodable; disqualifying " + "it from account-scoped evidence." + ) + return None + material = _load_or_create_key( + Path(key_path) if key_path is not None else DEFAULT_KEY_PATH + ) + if material is None: + return None + return hmac.new(material, identity, sha256).hexdigest()[:_KEY_HEX_LENGTH] + except Exception: # noqa: BLE001 — the totality contract outranks specificity + log.exception("Account key derivation failed; evidence forfeited") + return None diff --git a/src/llm/codex_auth.py b/src/llm/codex_auth.py index 96fdf94d..91d9fd47 100644 --- a/src/llm/codex_auth.py +++ b/src/llm/codex_auth.py @@ -507,6 +507,17 @@ def is_configured(self) -> bool: def account_count(self) -> int: return len(self._accounts) + def eligible_account_ids_snapshot(self) -> frozenset[str]: + """Stable non-secret IDs for accounts eligible to serve right now.""" + result: set[str] = set() + for auth in self._accounts: + if auth.is_rate_limited() or not auth.is_configured(): + continue + account_id = auth.get_account_id() + if isinstance(account_id, str) and account_id: + result.add(account_id) + return frozenset(result) + @property def current(self) -> CodexAuth: if not self._accounts: diff --git a/src/llm/context_budget.py b/src/llm/context_budget.py new file mode 100644 index 00000000..622f17fb --- /dev/null +++ b/src/llm/context_budget.py @@ -0,0 +1,198 @@ +"""Per-model context-budget resolution (pure functions; campaign phase 1). + +The single derivation authority from capability (budget floors and operator +overrides), evidence (observed clamps), and policy (utilization, explicit +character ceiling) down to the character targets compaction consumes: + + base_budget = override[model] ?? floor[model] ?? 272_000 + effective_budget = min(base_budget, observed_clamp) # when present + working_budget = min(effective_budget, + max(272_000, effective_budget × utilization%)) + compactable_tokens = max(0, working_budget − 42_000) # total: never negative + derived_chars = compactable_tokens × 2.5 + primary_chars = min(derived_chars, explicit ceiling) # when non-null + rung_1 = primary_chars × 0.7 + rung_2 = min(rung_1, 400_000) + ladder = positive rungs only, deduplicated, non-increasing + +All arithmetic is exact integer math (×2.5 as ×5//2, percentages and the 0.7 +ratio as integer products before floor division) — identical to the settled +floor()-form for non-negative operands, with no float drift. + +Semantics settled with Odin (plan of record R2, 2026-08-17): + +- Budgets are known-safe usable INPUT floors, already below the server's + output reservation — nothing here subtracts an output reserve. +- The 42K envelope reserve covers the fixed request material that rides + outside compactable history (system prompt, tool schemas). +- Observed clamps are runtime evidence and deliberately bypass the operator + override bounds; the resolver is TOTAL under any clamp value — when no + positive rescue rung exists, recovery must fail honestly rather than + fabricate a target. +- The 272K legacy floor means utilization never reduces a budget at or below + 272K: small models keep their full derived targets; the policy knob only + bites above ~453K budgets. +- 400_000 chars is the rescue ceiling for models with a ≥272K usable budget + (it survives every historically served ≥272K window class); the ``min`` + keeps smaller models and low overrides/clamps on their own smaller rung — + recovery never enlarges context. +- Resolution is snapshotted per logical generation: the model and its budget + travel together through retries and rescue rungs; live configuration + changes reach the NEXT generation only. + +Nothing consumes this module at runtime yet — phase 3 wires it into the +agent, chat, and loop surfaces. Until then it is contract plus tests. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ..config.schema import ( + CODEX_MODEL_INPUT_BUDGETS, + CODEX_UNKNOWN_MODEL_INPUT_BUDGET, + canonical_codex_model, +) + +#: Tokens reserved for the fixed request envelope (system prompt, tool +#: schemas, non-history material) — NOT an output reserve; the server already +#: holds its own output reservation outside the usable input budget. +FIXED_ENVELOPE_RESERVE_TOKENS = 42_000 + +#: Deliberately DENSE chars-per-token so a character measure can never +#: overshoot real tokens on scraped content. Expressed as a ratio of two +#: integers (5/2) so derivations stay exact. +EMERGENCY_CHARS_PER_TOKEN = 2.5 + +#: Utilization never reduces budgets at or below the pre-campaign uniform +#: window — models of that class keep their full derived working set. +LEGACY_UTILIZATION_FLOOR_TOKENS = 272_000 + +#: Final-rung ceiling for models with a ≥272K usable budget: ~202K estimated +#: input tokens, which fits every historically served ≥272K window class, +#: including a silent 922K→372K regression. +RESCUE_CEILING_CHARS = 400_000 + +#: First rescue rung as a fraction of the final primary target (7/10, exact). +RESCUE_RATIO = 0.7 + +_BASE_SOURCE_OVERRIDE = "override" +_BASE_SOURCE_FLOOR = "floor" +_BASE_SOURCE_UNKNOWN = "unknown_default" + + +@dataclass(frozen=True) +class ContextBudgetSnapshot: + """One resolved (model, budget, targets) unit, frozen per logical generation. + + Retries and rescue rungs of the same generation reuse this snapshot; + a fresh generation resolves a fresh one (that is where live config and + clamp changes take effect). + """ + + canonical_model: str + base_budget: int + base_source: str # "override" | "floor" | "unknown_default" + effective_budget: int + clamp_applied: bool + working_budget: int + compactable_tokens: int + derived_chars: int + primary_chars: int + ceiling_applied: bool + ladder: tuple[int, ...] + + +def snapshot_for_codex_config( + model: str | None, + codex_config: object, + *, + max_context_chars: int | None, + observed_clamp: int | None = None, +) -> ContextBudgetSnapshot: + """Resolve a snapshot from the live codex config section, getattr-safe. + + ``overrides`` and ``utilization`` are read from ``codex_config`` at CALL + time — a live save reaches the next logical generation. The explicit + character ceiling is passed by the CALLER from wherever its truthful + lifetime lives (the boot-frozen compression object for chat, the + spawn-frozen value for agents) so the apply-registry classification of + ``max_context_chars`` stays honest: this helper never re-reads it live. + ``observed_clamp`` is the window observer's runtime evidence (phase 5) — + callers with an observer pass ``active_clamp(model)``; None = unclamped. + """ + return resolve_context_budget( + model, + overrides=getattr(codex_config, "context_budget_overrides", None), + utilization=getattr(codex_config, "context_utilization", 60), + max_context_chars=max_context_chars, + observed_clamp=observed_clamp, + ) + + +def resolve_context_budget( + model: str | None, + *, + overrides: dict[str, int] | None = None, + utilization: int = 60, + max_context_chars: int | None = None, + observed_clamp: int | None = None, +) -> ContextBudgetSnapshot: + """Resolve the full derivation chain for ``model``. Total by construction. + + ``overrides`` carries canonical keys (the config validator normalizes + them); the raw ``model`` is canonicalized here so no caller can forget. + ``observed_clamp`` is exact runtime evidence and may be arbitrarily low — + the chain absorbs it without ever going negative or fabricating a rung. + """ + canonical = canonical_codex_model(model) + overrides = overrides or {} + + if canonical in overrides: + base_budget, base_source = overrides[canonical], _BASE_SOURCE_OVERRIDE + elif canonical in CODEX_MODEL_INPUT_BUDGETS: + base_budget, base_source = CODEX_MODEL_INPUT_BUDGETS[canonical], _BASE_SOURCE_FLOOR + else: + base_budget, base_source = CODEX_UNKNOWN_MODEL_INPUT_BUDGET, _BASE_SOURCE_UNKNOWN + + if observed_clamp is not None and observed_clamp < base_budget: + effective_budget, clamp_applied = observed_clamp, True + else: + effective_budget, clamp_applied = base_budget, False + + # Integer form of floor(effective × utilization/100). + working_budget = min( + effective_budget, + max(LEGACY_UTILIZATION_FLOOR_TOKENS, effective_budget * utilization // 100), + ) + # Totality: an evidence clamp below the envelope reserve must yield an + # empty compactable allowance, never a negative one. + compactable_tokens = max(0, working_budget - FIXED_ENVELOPE_RESERVE_TOKENS) + # Integer form of floor(compactable × 2.5). + derived_chars = compactable_tokens * 5 // 2 + + if max_context_chars is not None and max_context_chars < derived_chars: + primary_chars, ceiling_applied = max_context_chars, True + else: + primary_chars, ceiling_applied = derived_chars, False + + # Integer form of floor(primary × 0.7). + rung_1 = primary_chars * 7 // 10 + rung_2 = min(rung_1, RESCUE_CEILING_CHARS) + ladder = tuple( + rung for i, rung in enumerate((rung_1, rung_2)) if rung > 0 and (i == 0 or rung != rung_1) + ) + + return ContextBudgetSnapshot( + canonical_model=canonical, + base_budget=base_budget, + base_source=base_source, + effective_budget=effective_budget, + clamp_applied=clamp_applied, + working_budget=working_budget, + compactable_tokens=compactable_tokens, + derived_chars=derived_chars, + primary_chars=primary_chars, + ceiling_applied=ceiling_applied, + ladder=ladder, + ) diff --git a/src/llm/context_compressor.py b/src/llm/context_compressor.py index 7ce9ff7a..9eaadc12 100644 --- a/src/llm/context_compressor.py +++ b/src/llm/context_compressor.py @@ -7,6 +7,7 @@ 2. Compressing older tool iterations when context exceeds a character budget. 3. Providing observability into compression events and cache efficiency. """ + from __future__ import annotations import hashlib @@ -42,9 +43,7 @@ def as_dict(self) -> dict: "prefix_misses": self.prefix_misses, "total_checks": self.total_checks, "prefix_hit_rate": ( - round(self.prefix_hits / self.total_checks, 3) - if self.total_checks > 0 - else 0.0 + round(self.prefix_hits / self.total_checks, 3) if self.total_checks > 0 else 0.0 ), } @@ -101,9 +100,7 @@ def _hash_prefix(system: str, messages: list[dict]) -> str: if isinstance(content, str): h.update(content.encode("utf-8", errors="replace")) else: - h.update( - json.dumps(content, sort_keys=True, default=str).encode() - ) + h.update(json.dumps(content, sort_keys=True, default=str).encode()) return h.hexdigest()[:16] @@ -111,14 +108,14 @@ def _hash_prefix(system: str, messages: list[dict]) -> str: # Message classification helpers # ------------------------------------------------------------------ + def _is_tool_message(msg: dict) -> bool: """True if a message contains tool_use or tool_result content blocks, or agent-style string tool result messages.""" content = msg.get("content") if isinstance(content, list): return any( - isinstance(b, dict) and b.get("type") in ("tool_use", "tool_result") - for b in content + isinstance(b, dict) and b.get("type") in ("tool_use", "tool_result") for b in content ) if isinstance(content, str) and content.startswith("[Tool result:"): return True @@ -128,20 +125,14 @@ def _is_tool_message(msg: dict) -> bool: def _is_tool_use_message(msg: dict) -> bool: content = msg.get("content") if isinstance(content, list): - return any( - isinstance(b, dict) and b.get("type") == "tool_use" - for b in content - ) + return any(isinstance(b, dict) and b.get("type") == "tool_use" for b in content) return False def _is_tool_result_message(msg: dict) -> bool: content = msg.get("content") if isinstance(content, list): - return any( - isinstance(b, dict) and b.get("type") == "tool_result" - for b in content - ) + return any(isinstance(b, dict) and b.get("type") == "tool_result" for b in content) if isinstance(content, str) and content.startswith("[Tool result:"): return True return False @@ -151,6 +142,7 @@ def _is_tool_result_message(msg: dict) -> bool: # Prefix / iteration splitting # ------------------------------------------------------------------ + def split_prefix_and_iterations( messages: list[dict], ) -> tuple[list[dict], list[list[dict]]]: @@ -178,9 +170,13 @@ def split_prefix_and_iterations( prefix = messages[:prefix_end] remaining = messages[prefix_end:] - if not remaining: - return prefix, [] + return prefix, _group_iterations(remaining) + +def _group_iterations(remaining: list[dict]) -> list[list[dict]]: + """Group iteration-territory messages into tool-call cycles.""" + if not remaining: + return [] iterations: list[list[dict]] = [] current: list[dict] = [] @@ -201,13 +197,32 @@ def split_prefix_and_iterations( if current: iterations.append(current) - return prefix, iterations + return iterations + + +def _structural_envelope_end(messages: list[dict]) -> int: + """Index of the first message carrying STRUCTURED tool blocks. + + Used only in surface-boundary mode: the request envelope is everything + before real tool traffic, judged by list-content ``tool_use`` / + ``tool_result`` blocks EXCLUSIVELY. String content is never consulted — + a user request that merely LOOKS like ``[Tool result: ...]`` or like a + compressor summary is envelope, not machinery (review round-1 blocker). + """ + for i, msg in enumerate(messages): + content = msg.get("content") + if isinstance(content, list) and any( + isinstance(b, dict) and b.get("type") in ("tool_use", "tool_result") for b in content + ): + return i + return len(messages) # ------------------------------------------------------------------ # Character estimation # ------------------------------------------------------------------ + def estimate_message_chars(messages: list[dict]) -> int: """Estimate total character payload across a message list.""" total = 0 @@ -236,8 +251,13 @@ def estimate_message_chars(messages: list[dict]) -> int: # ------------------------------------------------------------------ _ERROR_PREFIXES = ( - "Error", "error", "ERROR", "Command failed", "Timeout", - "Permission denied", "Unknown tool", + "Error", + "error", + "ERROR", + "Command failed", + "Timeout", + "Permission denied", + "Unknown tool", ) @@ -277,7 +297,7 @@ def summarize_iteration(iteration: list[dict]) -> str: if end > 14: name = content[14:end].strip() tool_names.append(name) - result_body = content[end + 1:].strip() if end > 0 else content + result_body = content[end + 1 :].strip() if end > 0 else content if result_body.startswith(_ERROR_PREFIXES): outcomes.append("ERR") else: @@ -298,12 +318,14 @@ def summarize_iteration(iteration: list[dict]) -> str: # Main compression entry point # ------------------------------------------------------------------ + def compress_tool_context( messages: list[dict], *, max_context_chars: int = DEFAULT_MAX_CONTEXT_CHARS, keep_recent: int = DEFAULT_KEEP_RECENT, stats: CompressionStats | None = None, + boundary: SurfaceBoundary | None = None, ) -> tuple[list[dict], int]: """Compress older tool iterations when context exceeds *max_context_chars*. @@ -325,6 +347,9 @@ def compress_tool_context( max_context_chars: Trigger compression above this threshold. keep_recent: Number of recent iterations to preserve verbatim. stats: Optional :class:`CompressionStats` to update. + boundary: Optional surface-declared replay/request partition. When + supplied, the declared request envelope is pinned structurally; + legacy string/tool-result heuristics never inspect it. Returns: ``(compressed_messages, iterations_compressed)`` @@ -333,7 +358,21 @@ def compress_tool_context( if total_chars <= max_context_chars: return messages, 0 - prefix, iterations = split_prefix_and_iterations(messages) + if boundary is None: + prefix, iterations = split_prefix_and_iterations(messages) + else: + # Surface soft compaction must use the SAME structural partition as + # emergency recovery. A current request is allowed to look exactly + # like legacy agent tool history; content never moves the boundary. + request_start = max(0, min(boundary.request_start, len(messages))) + rest = messages[request_start:] + if boundary.envelope_len is not None: + envelope_len = max(0, min(boundary.envelope_len, len(rest))) + else: + envelope_len = _structural_envelope_end(rest) + prefix_end = request_start + envelope_len + prefix = messages[:prefix_end] + iterations = _group_iterations(messages[prefix_end:]) if len(iterations) <= keep_recent: return messages, 0 @@ -489,15 +528,170 @@ def _emergency_summary_body(msg: dict) -> str: if content.startswith(_EMERGENCY_SUMMARY_PREFIX) and content.endswith( _EMERGENCY_SUMMARY_SUFFIX ): - return content[len(_EMERGENCY_SUMMARY_PREFIX):-len(_EMERGENCY_SUMMARY_SUFFIX)] + return content[len(_EMERGENCY_SUMMARY_PREFIX) : -len(_EMERGENCY_SUMMARY_SUFFIX)] return content +_REPLAY_MARKER_PREFIX = "[Context recovery: " +_REPLAY_MARKER_SUFFIX = " older conversation messages elided]" + + +@dataclass(frozen=True) +class SurfaceBoundary: + """Explicit compressible/protected partition for a surface's message list. + + ``request_start`` indexes the first message of the CURRENT request + envelope (chat: developer preamble + user message + pre-tool directives; + loops: the current autonomous prompt). Everything BEFORE it is replayed + context (chat session history / loop prev_context) — compressible by + oldest-first whole-message elision with an explicit count marker. + The envelope itself is protected verbatim; messages after it are tool + iterations under the existing newest-first emergency rules. + + Supplied by the SURFACE at turn construction and carried as state: + compression returns the updated boundary in its report + (``boundary_request_start`` / ``boundary_elided_replay``) because + indices shift as replay elides. ``elided_replay`` regenerates the + position-0 marker each pass — recognition is by THIS state, never by + matching marker text (user content can imitate any string). + + ``None`` boundary = agent semantics: the structural prefix (task and + parent messages) is protected, byte-identical to pre-campaign behavior. + """ + + request_start: int + elided_replay: int = 0 + # Number of messages in the protected request envelope, declared by the + # surface from its own STRUCTURE (chat: preamble + current user message; + # loops: the autonomous prompt). When set, the envelope/territory split + # is exactly this declaration — a prior pass's summary at territory head + # stays peelable, and no content heuristic runs at all. None falls back + # to the structural tool-block scan (first message carrying real tool + # blocks ends the envelope). + envelope_len: int | None = None + + +def _replay_marker_message(elided: int) -> dict: + return { + "role": "user", + "content": f"{_REPLAY_MARKER_PREFIX}{elided}{_REPLAY_MARKER_SUFFIX}", + } + + +def _compress_with_boundary( + messages: list[dict], + *, + target_chars: int, + boundary: SurfaceBoundary, + stats: CompressionStats | None, +) -> tuple[list[dict], dict]: + """Replay-elision wrapper around the agent-semantics emergency core. + + ``messages[boundary.request_start:]`` is exactly the shape the core + already handles — the request envelope becomes its protected prefix and + everything after it its tool iterations. This wrapper spends replayed + context (oldest first, whole messages, count marker regenerated from + boundary state — never a fabricated summary) only when iteration + compression alone cannot reach the target. A first-generation overflow + with zero iterations therefore recovers by replay elision alone. + """ + request_start = max(0, min(boundary.request_start, len(messages))) + marker_present = boundary.elided_replay > 0 and request_start > 0 + replay = list(messages[1 if marker_present else 0 : request_start]) + rest = list(messages[request_start:]) + elided_total = boundary.elided_replay + original_chars = estimate_message_chars(messages) + + def _assemble( + kept_replay: list[dict], inner: list[dict], elided: int + ) -> tuple[list[dict], int]: + head: list[dict] = [] + if elided > 0: + head.append(_replay_marker_message(elided)) + return head + kept_replay + inner, len(head) + len(kept_replay) + + # The envelope end is pinned STRUCTURALLY: the surface's own declared + # envelope length when it supplies one, else the first message carrying + # real tool blocks. Never content — no request text can be reclassified + # by the core's legacy string heuristics (review round-1 blocker #1). + # The declared form additionally keeps a PRIOR pass's summary (a string + # message sitting between envelope and iterations) in territory, where + # the core re-opens it instead of letting it ossify as pinned prefix. + if boundary.envelope_len is not None: + envelope_pin = max(0, min(boundary.envelope_len, len(rest))) + else: + envelope_pin = _structural_envelope_end(rest) + + best_inner, inner_report = emergency_compress_for_window( + rest, + target_chars=max( + 0, + target_chars + - estimate_message_chars( + ([_replay_marker_message(elided_total)] if elided_total else []) + replay + ), + ), + stats=stats, + _pinned_prefix=envelope_pin, + ) + kept_replay = replay + while True: + assembled, new_request_start = _assemble(kept_replay, best_inner, elided_total) + assembled_chars = estimate_message_chars(assembled) + if assembled_chars <= target_chars or not kept_replay: + break + # Iterations alone were not enough: spend the OLDEST replay message + # and retry the core with the space it freed. + kept_replay = kept_replay[1:] + elided_total += 1 + best_inner, inner_report = emergency_compress_for_window( + rest, + target_chars=max( + 0, + target_chars + - estimate_message_chars( + ([_replay_marker_message(elided_total)] if elided_total else []) + kept_replay + ), + ), + stats=stats, + _pinned_prefix=envelope_pin, + ) + + fits = assembled_chars <= target_chars and inner_report.get("fits", False) + if not fits and not inner_report.get("fits", False) and not kept_replay: + # The protected envelope (+ newest iteration) alone exceeds the rung: + # honest failure, original list preserved (the core already refused). + report = dict(inner_report) + report["original_chars"] = original_chars + report["compressed_chars"] = original_chars + report["fits"] = False + report["replay_original"] = len(replay) + report["replay_elided"] = elided_total - boundary.elided_replay + report["boundary_request_start"] = boundary.request_start + report["boundary_elided_replay"] = boundary.elided_replay + return messages, report + + report = dict(inner_report) + report["original_chars"] = original_chars + report["compressed_chars"] = assembled_chars + # Evidence truth: the report names the RUNG the caller requested, not + # the replay-reduced inner target the core happened to run with. + report["target_chars"] = target_chars + report["fits"] = fits + report["replay_original"] = len(replay) + report["replay_elided"] = elided_total - boundary.elided_replay + report["boundary_request_start"] = new_request_start + report["boundary_elided_replay"] = elided_total + return assembled, report + + def emergency_compress_for_window( messages: list[dict], *, target_chars: int, stats: CompressionStats | None = None, + boundary: SurfaceBoundary | None = None, + _pinned_prefix: int | None = None, ) -> tuple[list[dict], dict]: """Bound the ENTIRE payload under *target_chars* for overflow recovery. @@ -521,22 +715,39 @@ def emergency_compress_for_window( returned with ``report["fits"] = False``. That fallback still preserves the newest iteration; it is never summarized away merely to report fit. """ + if boundary is not None: + return _compress_with_boundary( + messages, target_chars=target_chars, boundary=boundary, stats=stats + ) original_chars = estimate_message_chars(messages) - raw_prefix, iterations = split_prefix_and_iterations(messages) - - # Emergency summaries are compressor state, not immutable task context. - # Peel them out before measuring the prefix so an aggressive second pass - # can replace/recompact the first pass's summary. - prefix = list(raw_prefix) carried_summaries: list[str] = [] - # Only summaries at the compressor's own boundary are replaceable. Do - # not search/remove matching text from the user's real task or parent - # context. A real prefix must remain before the generated marker. - while len(prefix) > 1 and _is_emergency_summary(prefix[-1]): - body = _emergency_summary_body(prefix.pop()) - if body: - carried_summaries.append(body) - carried_summaries.reverse() + if _pinned_prefix is not None: + # Surface-boundary mode: the prefix is pinned STRUCTURALLY and taken + # verbatim — no content heuristic may reclassify request text as an + # iteration or a replaceable summary. Prior-pass summaries live at + # the head of iteration territory and are peeled from THERE. + pin = max(0, min(_pinned_prefix, len(messages))) + prefix = list(messages[:pin]) + territory = list(messages[pin:]) + while territory and _is_emergency_summary(territory[0]): + body = _emergency_summary_body(territory.pop(0)) + if body: + carried_summaries.append(body) + iterations = _group_iterations(territory) + else: + raw_prefix, iterations = split_prefix_and_iterations(messages) + + # Emergency summaries are compressor state, not immutable task + # context. Peel them out before measuring the prefix so an + # aggressive second pass can replace/recompact the first pass's + # summary. Only summaries at the compressor's own boundary are + # replaceable; a real prefix must remain before the marker. + prefix = list(raw_prefix) + while len(prefix) > 1 and _is_emergency_summary(prefix[-1]): + body = _emergency_summary_body(prefix.pop()) + if body: + carried_summaries.append(body) + carried_summaries.reverse() prefix_chars = estimate_message_chars(prefix) report: dict = { @@ -676,7 +887,8 @@ def _assemble( if summary is not None and overflow > 0: summary_limit = max(0, summary_chars - overflow) new_messages, compressed_chars, summary = _assemble( - kept, summary_limit=summary_limit, + kept, + summary_limit=summary_limit, ) if compressed_chars > target_chars: diff --git a/src/llm/errors.py b/src/llm/errors.py index 6fc92927..60d2e56d 100644 --- a/src/llm/errors.py +++ b/src/llm/errors.py @@ -49,12 +49,22 @@ def __init__( model: str | None = None, retry_after: float | None = None, code: str | None = None, + server_input_tokens: int | None = None, + account_key: str | None = None, ) -> None: super().__init__(message) self.provider = provider self.model = model self.retry_after = retry_after self.code = code + # Provider-truth evidence (context-budget campaign phase 2): the + # server-authoritative input count from an authoritative failure + # event when present (never a client estimate), and the opaque + # installation-local key of the account that served the failing + # attempt (never a raw identifier). A rejection without + # authoritative usage is an occurrence, not a numeric bound. + self.server_input_tokens = server_input_tokens + self.account_key = account_key class LLMCapacityError(LLMError): diff --git a/src/llm/openai_codex.py b/src/llm/openai_codex.py index 3e1a3892..2d5b398d 100644 --- a/src/llm/openai_codex.py +++ b/src/llm/openai_codex.py @@ -231,11 +231,15 @@ def __init__( error_type: str | None = None, error_code: str | None = None, retry_after: float | None = None, + server_input_tokens: int | None = None, ) -> None: super().__init__(message) self.error_type = error_type self.error_code = error_code self.retry_after = retry_after + # Server-authoritative usage from the failure event, when the event + # carried one (strictly parsed; None otherwise — never an estimate). + self.server_input_tokens = server_input_tokens @property def is_capacity(self) -> bool: @@ -245,6 +249,20 @@ def is_capacity(self) -> bool: ) +def _server_input_tokens_from_usage(usage: object) -> int | None: + """Strictly parse the server's accepted-input count from a usage object. + + Absent, malformed, boolean, negative, or non-integer ⇒ ``None``. The + observer never substitutes the client estimate for this value. + """ + if not isinstance(usage, dict): + return None + value = usage.get("input_tokens") + if type(value) is not int or value < 0: + return None + return value + + def _stream_error_from_event(event_type: str, event: dict) -> CodexStreamError: """Build a classified CodexStreamError from a terminal SSE event. @@ -265,6 +283,10 @@ def _stream_error_from_event(event_type: str, event: dict) -> CodexStreamError: error_type = err.get("type") error_code = err.get("code") retry_after = err.get("retry_after") + # Authoritative usage rides the failure event's response object when the + # server provides one; strictly parsed, never estimated. + resp_obj = event.get("response") + usage = resp_obj.get("usage") if isinstance(resp_obj, dict) else None fields = _sanitized_error_fields({"error": err}) detail = "; ".join(fields)[:400] if fields else "unstructured stream error event" return CodexStreamError( @@ -272,6 +294,7 @@ def _stream_error_from_event(event_type: str, event: dict) -> CodexStreamError: error_type=error_type if isinstance(error_type, str) else None, error_code=error_code if isinstance(error_code, str) else None, retry_after=float(retry_after) if isinstance(retry_after, (int, float)) else None, + server_input_tokens=_server_input_tokens_from_usage(usage), ) @@ -345,6 +368,25 @@ async def close(self) -> None: def pool_stats(self) -> dict: return self.get_pool_metrics() + def eligible_account_keys_snapshot(self) -> frozenset[str]: + """Opaque identities for accounts the auth layer may serve now.""" + try: + from .account_key import opaque_account_key + + if isinstance(self.auth, CodexAuthPool): + raw_ids = self.auth.eligible_account_ids_snapshot() + else: + raw = self.auth.get_account_id() + raw_ids = frozenset({raw}) if isinstance(raw, str) and raw else frozenset() + return frozenset( + key + for account_id in raw_ids + if (key := opaque_account_key(account_id)) is not None + ) + except Exception: + log.exception("Could not resolve eligible Codex account keys") + return frozenset() + def get_pool_metrics(self) -> dict: """Return HTTP connection pool metrics for observability.""" active = 0 @@ -805,11 +847,19 @@ async def _send_with_retries(self, body: dict, reader, result_is_empty): # agent overflow recovery keys on ``code``. # The client breaker counts infrastructure # health, not payload validity: untouched. + from .account_key import opaque_account_key + raise LLMRequestError( f"Codex stream failed: {e}", provider="codex", model=str(body.get("model") or self.model), code=e.error_code or e.error_type, + # Provider truth for the observer: the + # failure event's own usage (None unless + # the server sent one) and the opaque key + # of the account that served THIS attempt. + server_input_tokens=e.server_input_tokens, + account_key=opaque_account_key(account_id), ) from e # response.failed / error event: the "200" turned # out to be a failure mid-stream — retryable. @@ -834,6 +884,13 @@ async def _send_with_retries(self, body: dict, reader, result_is_empty): ) from e if not result_is_empty(result): self.breaker.record_success() + if isinstance(result, LLMResponse): + # Per-attempt account provenance: the pool may + # rotate between attempts, so the stamp is the + # account that served THIS successful attempt. + from .account_key import opaque_account_key + + result.account_key = opaque_account_key(account_id) return result log.warning( "Codex returned 200 with empty response (attempt %d/%d)", @@ -998,6 +1055,7 @@ async def _read_tool_stream(self, resp: aiohttp.ClientResponse) -> LLMResponse: """ text_parts: list[str] = [] tool_calls: list[ToolCall] = [] + server_input_tokens: int | None = None incomplete = False # Track in-progress function calls by output_index @@ -1121,6 +1179,12 @@ async def _read_tool_stream(self, resp: aiohttp.ClientResponse) -> LLMResponse: # Final response object — fallback elif event_type == "response.completed": response_obj = event.get("response", {}) + # Server-authoritative accepted input from the usage echo — + # strictly parsed; the client estimate is a separate field + # and is never substituted for this one. + server_input_tokens = _server_input_tokens_from_usage( + response_obj.get("usage") + ) output = response_obj.get("output", []) for item in output: item_type = item.get("type", "") @@ -1159,7 +1223,12 @@ async def _read_tool_stream(self, resp: aiohttp.ClientResponse) -> LLMResponse: stop_reason = "incomplete" else: stop_reason = "end_turn" - return LLMResponse(text=text, tool_calls=tool_calls, stop_reason=stop_reason) + return LLMResponse( + text=text, + tool_calls=tool_calls, + stop_reason=stop_reason, + server_input_tokens=server_input_tokens, + ) async def _read_stream(self, resp: aiohttp.ClientResponse) -> str: """Read SSE stream and extract text content.""" diff --git a/src/llm/types.py b/src/llm/types.py index 90e82beb..27cf3953 100644 --- a/src/llm/types.py +++ b/src/llm/types.py @@ -44,6 +44,16 @@ class LLMResponse: provenance_provider: str = "" provenance_model: str = "" provenance_reasoning_effort: str | None = None + # Server-authoritative accepted input, parsed strictly from the provider's + # usage echo (absent/malformed ⇒ None). NEVER derived from the client + # estimate above — the observer refuses estimates; ``input_tokens`` keeps + # its historical estimate meaning untouched. + server_input_tokens: int | None = None + # Opaque installation-local key of the account that served THIS attempt + # (HMAC over the stable non-secret account id — never a raw identifier). + # None when no stable account identity or key material exists; such + # attempts are disqualified from account-scoped evidence. + account_key: str | None = None @property def is_tool_use(self) -> bool: diff --git a/src/llm/window_observer.py b/src/llm/window_observer.py new file mode 100644 index 00000000..008fe178 --- /dev/null +++ b/src/llm/window_observer.py @@ -0,0 +1,564 @@ +"""Passive context-window observer + downward-only clamps (campaign phase 5). + +Odin's normal work is the probe: every emergency rescue already carries the +server's own numbers — the overflow error's rejected input size and the +compressed retry's accepted usage echo (phase 2 stamping). This module turns +those pairs into per-account, per-model evidence and a temporary DOWNWARD +clamp on the budget resolver, so a silent serving-window regression stops +costing repeated overflow round-trips within minutes of first being seen. + +Settled semantics (plan of record R2 §11): + +- ``data/context_windows.json`` is runtime EVIDENCE, never configuration: + versioned schema, strict validation, atomic replacement, UTC timestamps, + opaque account keys only (the phase-2 HMAC identities — no raw account + material ever lands here). +- One lock guards the complete read–merge–atomic-write transaction; the + in-memory state is replaced wholesale under that lock and read lock-free + by the hot path (``active_clamp`` is synchronous and touches no disk). +- A clamp qualifies only when ALL hold: structural overflow, the SAME + logical request's successful compressed retry, a server-authoritative + accepted-input echo, canonical model match, and rejection/acceptance on + the same opaque account. A cross-account retry records both observations + but derives no clamp for the rejecting account. +- The clamp value IS the successful post-rejection acceptance, exact. + Clamps are downward-only: fresh evidence at or below a live clamp + replaces it (value + fresh TTL); higher evidence never raises or clears + a live clamp early. TTL is 24 hours; expiry is judged lazily at read. + Manual clearing is account-scoped. +- The active clamp for a model is the minimum non-expired clamp across the + currently ELIGIBLE pool accounts. Pool knowledge is dependency-inverted: + the observer consumes opaque-key snapshots and never imports the pool. +- An evidence-write failure logs and forfeits durability — it NEVER turns + a successful user request into an error. The merged evidence still + serves this process from memory; the next successful write persists it. +- No probe traffic, no autonomous upward adjustment: growth discovery + stays a manual procedure. +""" + +from __future__ import annotations + +import asyncio +import copy +import json +import os +import stat +import tempfile +from collections.abc import Callable, Collection +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import TypeGuard + +from ..config.schema import canonical_codex_model +from ..odin_log import get_logger + +log = get_logger("window_observer") + +STORE_VERSION = 1 +CLAMP_TTL = timedelta(hours=24) +DEFAULT_STORE_PATH = Path("data/context_windows.json") + +#: Evidence for three pool accounts across a handful of models is a few KB; +#: anything near this cap is not our file. +_MAX_STORE_BYTES = 4 * 1024 * 1024 + +_ACCOUNT_KEY_HEX = "0123456789abcdef" + + +def _utc_now() -> datetime: + return datetime.now(UTC) + + +def _iso(ts: datetime) -> str: + return ts.isoformat(timespec="seconds").replace("+00:00", "Z") + + +def _parse_iso(value: object) -> datetime | None: + if not isinstance(value, str): + return None + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + if parsed.tzinfo is None: + return None + return parsed + + +def _is_account_key(value: object) -> bool: + return ( + isinstance(value, str) and len(value) == 32 and all(ch in _ACCOUNT_KEY_HEX for ch in value) + ) + + +def _positive_int(value: object) -> TypeGuard[int]: + return isinstance(value, int) and not isinstance(value, bool) and value > 0 + + +def _empty_model_record() -> dict: + return { + "highest_accepted_input": None, + "highest_accepted_at": None, + "lowest_rejection_bound": None, + "lowest_rejection_at": None, + "overflow_occurrences": 0, + "last_overflow_at": None, + "clamp": None, + } + + +def _validate_store(data: object) -> bool: + """Strict schema check — anything off-shape is not our evidence.""" + if not isinstance(data, dict) or data.get("version") != STORE_VERSION: + return False + accounts = data.get("accounts") + if not isinstance(accounts, dict) or set(data) != {"version", "accounts"}: + return False + for account_key, account in accounts.items(): + if not _is_account_key(account_key): + return False + if not isinstance(account, dict) or set(account) != {"models"}: + return False + models = account["models"] + if not isinstance(models, dict): + return False + for model, record in models.items(): + if not isinstance(model, str) or not model.strip(): + return False + if not isinstance(record, dict) or set(record) != set(_empty_model_record()): + return False + for bound in ("highest_accepted_input", "lowest_rejection_bound"): + if record[bound] is not None and not _positive_int(record[bound]): + return False + occurrences = record["overflow_occurrences"] + if not isinstance(occurrences, int) or isinstance(occurrences, bool) or occurrences < 0: + return False + for ts_field in ("highest_accepted_at", "lowest_rejection_at", "last_overflow_at"): + if record[ts_field] is not None and _parse_iso(record[ts_field]) is None: + return False + clamp = record["clamp"] + if clamp is None: + continue + if not isinstance(clamp, dict) or set(clamp) != { + "value", + "set_at", + "expires_at", + "source", + }: + return False + if not _positive_int(clamp["value"]) or clamp["source"] != "rescue": + return False + if _parse_iso(clamp["set_at"]) is None or _parse_iso(clamp["expires_at"]) is None: + return False + return True + + +def _read_store_bytes(path: Path) -> bytes | None: + """Hostile-input-safe read: never block, never follow, never assume. + + Returns the raw bytes of a regular, sanely-sized file; ``None`` for + absent. Raises ``ValueError`` for anything present-but-wrong (FIFO, + directory, symlink, oversized) so the caller can quarantine it. + """ + flags = os.O_RDONLY | os.O_NONBLOCK | getattr(os, "O_NOFOLLOW", 0) + try: + fd = os.open(path, flags) + except FileNotFoundError: + return None + except OSError as exc: + # ELOOP = symlink refused by O_NOFOLLOW; other opens that fail on a + # present path are equally disqualifying. + raise ValueError(f"unreadable store file: {exc}") from exc + try: + info = os.fstat(fd) + if not stat.S_ISREG(info.st_mode): + raise ValueError("store path is not a regular file") + if info.st_size > _MAX_STORE_BYTES: + raise ValueError(f"store file too large ({info.st_size} bytes)") + chunks: list[bytes] = [] + remaining = info.st_size + while remaining > 0: + chunk = os.read(fd, min(remaining, 1 << 20)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks) + finally: + os.close(fd) + + +class WindowObserverMutationError(RuntimeError): + """An explicit operator mutation could not be durably committed.""" + + +class WindowObserver: + """Evidence store + clamp authority. Every passive entry point is total.""" + + def __init__( + self, + path: str | Path = DEFAULT_STORE_PATH, + *, + eligible_account_keys: Callable[[], Collection[str] | None] | None = None, + ): + self._path = Path(path) + self._lock = asyncio.Lock() + # None keeps standalone/test construction backward-compatible. The + # composition root installs the production pool-backed provider. + self._eligible_account_keys = eligible_account_keys + self._state: dict = {"version": STORE_VERSION, "accounts": {}} + try: + self._load_initial() + except Exception: + log.exception("Window-evidence store load failed; starting empty") + + # ── load / persist ──────────────────────────────────────────────── + + def _load_initial(self) -> None: + try: + raw = _read_store_bytes(self._path) + except ValueError as exc: + log.warning("Window-evidence store rejected (%s); quarantining", exc) + self._quarantine() + return + if raw is None: + return + try: + data = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + data = None + if data is None or not _validate_store(data): + log.warning("Window-evidence store failed validation; quarantining") + self._quarantine() + return + self._state = data + + def _quarantine(self) -> None: + """Preserve questionable prior material beside the store — evidence + is provenance-bearing and is never repaired or overwritten in place.""" + try: + stamp = _utc_now().strftime("%Y%m%dT%H%M%SZ") + target = self._path.with_name(f"{self._path.name}.corrupt-{stamp}") + os.replace(self._path, target) + log.warning("Quarantined window-evidence store to %s", target) + except OSError: + log.exception("Window-evidence quarantine failed; leaving file in place") + + def set_eligible_account_keys_provider( + self, provider: Callable[[], Collection[str] | None] | None + ) -> None: + """Install the pool-facing opaque-key snapshot provider. + + This narrow callback is the dependency-inversion boundary: observer + code knows nothing about credential pools or raw account identities. + """ + self._eligible_account_keys = provider + + def _persist_locked(self, state: dict | None = None) -> None: + """Atomic replacement: unique temp, fsync, rename, parent fsync.""" + payload = json.dumps(self._state if state is None else state, indent=2, sort_keys=True) + directory = self._path.parent + directory.mkdir(parents=True, exist_ok=True) + fd, tmp_name = tempfile.mkstemp(prefix=f".{self._path.name}.tmp-", dir=directory) + tmp_path = Path(tmp_name) + try: + try: + handle = os.fdopen(fd, "w", encoding="utf-8") + except BaseException: + # fdopen never took ownership — close the raw fd ourselves. + os.close(fd) + raise + with handle: + # handle owns fd from here; any failure below closes it. + handle.write(payload) + handle.flush() + os.fsync(handle.fileno()) + os.replace(tmp_path, self._path) + except BaseException: + try: + os.unlink(tmp_path) + except OSError: + pass + raise + dir_fd = os.open(directory, os.O_RDONLY) + try: + os.fsync(dir_fd) + finally: + os.close(dir_fd) + + async def _persist_owned( + self, state: dict + ) -> tuple[BaseException | None, asyncio.CancelledError | None]: + """Drain the persistence worker before the transaction lock releases. + + Cancelling an await of ``to_thread`` does not stop the worker. Shield + it and remember cancellation so no second writer can enter while the + first still owns an unpublished candidate. + """ + worker = asyncio.create_task(asyncio.to_thread(self._persist_locked, state)) + cancelled: asyncio.CancelledError | None = None + while True: + try: + await asyncio.shield(worker) + break + except asyncio.CancelledError as exc: + # If the worker itself raised CancelledError, it is done and its + # outcome belongs to the persistence operation. Otherwise this + # task was cancelled; keep owning the transaction until the + # non-cooperative thread has actually stopped. + if worker.done(): + break + if cancelled is None: + cancelled = exc + except BaseException: + # The worker outcome is collected below; the important thing + # here is that it is DONE before lock ownership can end. + break + try: + worker.result() + except BaseException as exc: + return exc, cancelled + return None, cancelled + + # ── hot-path read ───────────────────────────────────────────────── + + def _eligible_keys(self) -> tuple[bool, frozenset[str]]: + """Return ``(scoped, keys)`` for the current pool snapshot. + + Standalone construction has no pool provider and preserves legacy + all-account behavior (``scoped=False``). Once a provider is installed, + an unavailable snapshot fails open as an empty eligible set rather than + presenting stale evidence as active. + """ + if self._eligible_account_keys is None: + return False, frozenset() + supplied = self._eligible_account_keys() + if supplied is None: + return True, frozenset() + return True, frozenset(key for key in supplied if _is_account_key(key)) + + @staticmethod + def _active_clamp_for_record(record: object, now: datetime) -> int | None: + if not isinstance(record, dict): + return None + clamp = record.get("clamp") + if not isinstance(clamp, dict): + return None + expires = _parse_iso(clamp.get("expires_at")) + value = clamp.get("value") + if expires is None or expires <= now or not _positive_int(value): + return None + return value + + def active_clamp(self, model: str | None) -> int | None: + """Minimum non-expired clamp across currently eligible accounts. + + Synchronous and disk-free — safe inside budget resolution. Total: + any internal surprise returns ``None`` (no clamp) rather than ever + failing a request. + """ + try: + canonical = canonical_codex_model(model) + if not canonical: + return None + scoped, eligible = self._eligible_keys() + now = _utc_now() + best: int | None = None + for account_key, account in self._state.get("accounts", {}).items(): + if scoped and account_key not in eligible: + continue + value = self._active_clamp_for_record(account.get("models", {}).get(canonical), now) + if value is not None and (best is None or value < best): + best = value + return best + except Exception: + log.exception("active_clamp failed; treating as unclamped") + return None + + def account_clamps(self) -> list[dict]: + """Management-safe active clamp rows for the WebUI. + + The observer owns TTL and pool-eligibility semantics. Exposing a + normalized view keeps the browser from reimplementing either, while + retaining the opaque account key needed for an account-scoped clear. + """ + try: + scoped, eligible = self._eligible_keys() + now = _utc_now() + rows: list[dict] = [] + for account_key, account in self._state.get("accounts", {}).items(): + if scoped and account_key not in eligible: + continue + for model, record in account.get("models", {}).items(): + value = self._active_clamp_for_record(record, now) + if value is None: + continue + clamp = record["clamp"] + rows.append( + { + "account_key": account_key, + "model": model, + "value": value, + "set_at": clamp["set_at"], + "expires_at": clamp["expires_at"], + "source": clamp["source"], + } + ) + return sorted( + rows, key=lambda row: (row["model"], row["expires_at"], row["account_key"]) + ) + except Exception: + log.exception("account_clamps failed; serving no management rows") + return [] + + # ── evidence intake ─────────────────────────────────────────────── + + async def record_rescue(self, *, overflow: object, response: object) -> None: + """Record one overflow→compressed-retry-acceptance pair. + + ``overflow`` is the structural overflow error (phase-2 stamped); + ``response`` is the SAME logical request's successful retry. Total: + every failure logs and forfeits the observation. + """ + try: + if getattr(overflow, "code", None) != "context_length_exceeded": + return + reject_key = getattr(overflow, "account_key", None) + reject_tokens = getattr(overflow, "server_input_tokens", None) + reject_model = canonical_codex_model(getattr(overflow, "model", None)) + accept_key = getattr(response, "account_key", None) + accept_tokens = getattr(response, "server_input_tokens", None) + accept_model = canonical_codex_model(getattr(response, "provenance_model", None)) + if not _positive_int(reject_tokens): + reject_tokens = None + if not _positive_int(accept_tokens): + accept_tokens = None + if not _is_account_key(reject_key): + reject_key = None + if not _is_account_key(accept_key): + accept_key = None + now = _utc_now() + + cancellation: asyncio.CancelledError | None = None + async with self._lock: + state = copy.deepcopy(self._state) + if reject_key and reject_model: + record = self._record_for(state, reject_key, reject_model) + record["overflow_occurrences"] += 1 + record["last_overflow_at"] = _iso(now) + if reject_tokens is not None: + prior = record["lowest_rejection_bound"] + if prior is None or reject_tokens < prior: + record["lowest_rejection_bound"] = reject_tokens + record["lowest_rejection_at"] = _iso(now) + if accept_key and accept_model and accept_tokens is not None: + record = self._record_for(state, accept_key, accept_model) + prior = record["highest_accepted_input"] + if prior is None or accept_tokens > prior: + record["highest_accepted_input"] = accept_tokens + record["highest_accepted_at"] = _iso(now) + if ( + reject_key is not None + and reject_key == accept_key + and reject_model + and reject_model == accept_model + and accept_tokens is not None + ): + record = self._record_for(state, reject_key, reject_model) + record["clamp"] = self._merged_clamp(record.get("clamp"), accept_tokens, now) + self._state = state + persist_error, cancellation = await self._persist_owned(state) + if persist_error is not None: + log.error( + "Window-evidence write failed; observation forfeited " + "(in-memory clamp still serves this process)", + exc_info=(type(persist_error), persist_error, persist_error.__traceback__), + ) + if cancellation is not None: + raise cancellation + except asyncio.CancelledError: + raise + except Exception: + log.exception("record_rescue failed; observation forfeited") + + @staticmethod + def _record_for(state: dict, account_key: str, model: str) -> dict: + account = state["accounts"].setdefault(account_key, {"models": {}}) + return account["models"].setdefault(model, _empty_model_record()) + + @staticmethod + def _merged_clamp(existing: dict | None, accepted: int, now: datetime) -> dict: + """Downward-only merge: evidence at or below a live clamp replaces it + (exact value, fresh TTL); higher evidence never raises or refreshes a + live clamp; an expired clamp is replaced outright.""" + fresh = { + "value": accepted, + "set_at": _iso(now), + "expires_at": _iso(now + CLAMP_TTL), + "source": "rescue", + } + if not isinstance(existing, dict): + return fresh + expires = _parse_iso(existing.get("expires_at")) + value = existing.get("value") + if expires is None or expires <= now or not _positive_int(value): + return fresh + if accepted <= value: + return fresh + return existing + + # ── management surface ──────────────────────────────────────────── + + async def clear_account(self, account_key: str, model: str | None = None) -> int: + """Durably clear clamp(s) for one account (all models, or one). + + Bounds history stays. Persistence failure raises + ``WindowObserverMutationError`` and leaves the published in-memory + state unchanged, so API truth matches restart truth. + """ + if not _is_account_key(account_key): + return 0 + target_model = canonical_codex_model(model) if model else None + cancellation: asyncio.CancelledError | None = None + async with self._lock: + state = copy.deepcopy(self._state) + account = state.get("accounts", {}).get(account_key) + if not account: + return 0 + cleared = 0 + for name, record in account.get("models", {}).items(): + if target_model and name != target_model: + continue + if record.get("clamp") is not None: + record["clamp"] = None + cleared += 1 + if not cleared: + return 0 + persist_error, cancellation = await self._persist_owned(state) + if persist_error is not None: + raise WindowObserverMutationError( + "window-evidence clear could not be persisted" + ) from persist_error + # Publish only after durable commit. A cancellation delivered while + # the worker ran still observes matching memory and disk state. + self._state = state + if cancellation is not None: + raise cancellation + return cleared + + def view(self) -> dict: + """Deep-copied snapshot for the API: raw records plus, per clamp, a + computed ``expired`` flag so consumers never re-implement TTL math.""" + try: + snapshot = copy.deepcopy(self._state) + now = _utc_now() + for account in snapshot.get("accounts", {}).values(): + for record in account.get("models", {}).values(): + clamp = record.get("clamp") + if isinstance(clamp, dict): + expires = _parse_iso(clamp.get("expires_at")) + clamp["expired"] = expires is None or expires <= now + return snapshot + except Exception: + log.exception("view failed") + return {"version": STORE_VERSION, "accounts": {}} diff --git a/src/setup_wizard.py b/src/setup_wizard.py index 026e1f18..8ced6f62 100644 --- a/src/setup_wizard.py +++ b/src/setup_wizard.py @@ -27,7 +27,11 @@ }, "openai_codex": { "enabled": True, - "model": "gpt-5.5", + # Matches the schema default (the reference-deployment primary): an + # explicit legacy value here would silently override it on the one + # supported first-boot path and pin fresh installs to a 272K-class + # budget instead of sol's floor. + "model": "gpt-5.6-sol", "credentials_path": "./data/codex_auth.json", }, "context": { diff --git a/src/tools/autonomous_loop.py b/src/tools/autonomous_loop.py index 6f536ba2..d610f7ce 100644 --- a/src/tools/autonomous_loop.py +++ b/src/tools/autonomous_loop.py @@ -11,6 +11,7 @@ import uuid from collections import deque from collections.abc import Awaitable, Callable +from contextvars import ContextVar from dataclasses import dataclass, field from datetime import datetime from typing import Any @@ -22,10 +23,10 @@ log = get_logger("autonomous_loop") # Type for the LLM iteration callback: -# Takes (goal_prompt, channel, iteration_context) -> response text +# Takes (goal_prompt, channel, iteration_context, cancel_event) -> response text # The callback should run the full Codex + tool loop internally. LoopIterationCallback = Callable[ - [str, Any, str | None], # (prompt, channel, previous_context) + [str, Any, str | None, asyncio.Event], Awaitable[str], ] @@ -41,6 +42,14 @@ LOOP_STOP_SENTINEL = "LOOP_STOP" +# Logical loop ownership follows child tasks created by the iteration pipeline. +# asyncio.current_task() cannot identify self-stop from a gathered tool child: the +# child is not LoopInfo._task, but cancelling/awaiting the parent from that child +# creates a parent/child cancellation cycle. Context propagation is the authority. +_current_loop: ContextVar[tuple[int, str] | None] = ContextVar( + "odin_current_autonomous_loop", default=None +) + @dataclass class LoopInfo: @@ -61,6 +70,7 @@ class LoopInfo: status: str = "running" # running, stopped, completed, error _task: asyncio.Task | None = field(default=None, repr=False) _cancel_event: asyncio.Event = field(default_factory=asyncio.Event) + _stop_requested: bool = field(default=False, repr=False) _iteration_history: deque[str] = field( default_factory=lambda: deque(maxlen=MAX_CONTEXT_HISTORY * 2), ) @@ -131,30 +141,75 @@ def start_loop( ) return loop_id - def stop_loop(self, loop_id: str) -> str: - """Stop a loop by ID. Use 'all' to stop all loops.""" + async def stop_loop(self, loop_id: str) -> str: + """Cancel loop work and report stopped only after it has settled. + + The cooperative event reaches the tool loop and recovery layer; task + cancellation is the final authority that interrupts an admitted LLM + stream or an in-flight tool await. Awaiting the task closes the crucial + contract: once this method says ``stopped``, no retry or tool side + effect from that loop can begin afterward. + """ + logical_current = _current_loop.get() + current_loop_id = ( + logical_current[1] + if logical_current is not None and logical_current[0] == id(self) + else None + ) if loop_id == "all": - stopped = [] - for lid, info in list(self._loops.items()): - if info.status == "running": - info._cancel_event.set() - info.status = "stopped" - stopped.append(lid) - if not stopped: + running = [info for info in self._loops.values() if info.status == "running"] + if not running: return "No active loops to stop." - return f"Stopped {len(stopped)} loop(s): {', '.join(stopped)}" + for running_info in running: + if running_info.id == current_loop_id: + # The real tool path runs in an asyncio.gather() child. It + # must stop its logical parent cooperatively, never cancel + # and await that parent from below it. + running_info._stop_requested = True + running_info._cancel_event.set() + else: + self._request_stop(running_info) + await asyncio.gather( + *( + running_info._task + for running_info in running + if running_info.id != current_loop_id + and running_info._task is not None + ), + return_exceptions=True, + ) + ids = ", ".join(running_info.id for running_info in running) + if current_loop_id is not None: + return f"Stop requested for {len(running)} loop(s): {ids}" + return f"Stopped {len(running)} loop(s): {ids}" - # mypy binds `info` to the non-Optional loop var of the 'all' - # branch above (which always returns) — false conflict. - info = self._loops.get(loop_id) # type: ignore[assignment] + info = self._loops.get(loop_id) if not info: return f"No loop found with ID `{loop_id}`." if info.status != "running": return f"Loop `{loop_id}` is not running (status: {info.status})." - info._cancel_event.set() - info.status = "stopped" + if info.id == current_loop_id: + info._stop_requested = True + info._cancel_event.set() + # ContextVar ownership reaches gathered tool children. The tool is + # allowed to return, then run_autonomous observes the event and the + # manager task settles without any parent/child cancellation cycle. + return f"Loop `{loop_id}` stop requested." + self._request_stop(info) + if info._task is not None: + await asyncio.gather(info._task, return_exceptions=True) + if info.status == "running": + info.status = "stopped" return f"Loop `{loop_id}` stopped." + @staticmethod + def _request_stop(info: LoopInfo) -> None: + info._stop_requested = True + info._cancel_event.set() + task = info._task + if task is not None and task is not asyncio.current_task() and not task.done(): + task.cancel() + def list_loops(self) -> str: """Return a formatted list of all loops.""" if not self._loops: @@ -275,11 +330,17 @@ async def _run_loop( turn_id=f"loop:{info.id}:{info.iteration_count}", channel_id=info.channel_id, ) + _loop_token = _current_loop.set((id(self), info.id)) try: - response = await iteration_callback(prompt, channel, prev_context) + response = await iteration_callback( + prompt, channel, prev_context, info._cancel_event + ) finally: + _current_loop.reset(_loop_token) reset_turn(_turn_token) response = scrub_output_secrets(response.strip()) if response else "" + if info._cancel_event.is_set(): + break consecutive_errors = 0 # Reset on success except Exception as e: consecutive_errors += 1 @@ -385,15 +446,21 @@ async def _run_loop( if await self._interruptible_wait(info, info.interval_seconds): break # Cancel was set during the wait - # Loop ended normally (max iterations reached) + # Distinguish cooperative cancellation from natural exhaustion. + # A self-stop tool sets the event from a gathered child and cannot + # mark its logical parent settled; only this owner task publishes + # the terminal status after the callback/tool pipeline has unwound. if info.status == "running": - info.status = "completed" - try: - await channel.send( - f"Loop `{info.id}` completed after {info.iteration_count} iterations." - ) - except Exception: - pass + if info._stop_requested: + info.status = "stopped" + else: + info.status = "completed" + try: + await channel.send( + f"Loop `{info.id}` completed after {info.iteration_count} iterations." + ) + except Exception: + pass except asyncio.CancelledError: info.status = "stopped" diff --git a/src/trajectories/saver.py b/src/trajectories/saver.py index f857d89e..b203dfe8 100644 --- a/src/trajectories/saver.py +++ b/src/trajectories/saver.py @@ -116,6 +116,12 @@ class TrajectoryTurn: total_output_tokens: int = 0 total_duration_ms: int = 0 + # Context-overflow recovery evidence (campaign phase 4): one report per + # rescue/latch pass, same shape agents already persist. Optional and + # serialized only when non-empty — chat and pre-campaign records keep + # their exact on-disk schema. + context_recoveries: list[dict] = field(default_factory=list) + def add_iteration( self, iteration: int, @@ -181,6 +187,8 @@ def to_dict(self) -> dict: if self.user_content_truncated: d["user_content_truncated"] = True d["user_content_original_chars"] = self.user_content_original_chars + if self.context_recoveries: + d["context_recoveries"] = self.context_recoveries return d diff --git a/src/turn_state/codec.py b/src/turn_state/codec.py index 653cdfbb..034d38e6 100644 --- a/src/turn_state/codec.py +++ b/src/turn_state/codec.py @@ -24,13 +24,14 @@ from dataclasses import asdict from typing import Any +from ..config.schema import CODEX_REASONING_EFFORTS, model_rejects_effort from ..config.sensitivity import is_storage_sensitive_key as _is_sensitive_key from ..llm.secret_scrubber import scrub_output_secrets from ..odin_log import get_logger log = get_logger("turn_state") -CODEC_VERSION = 2 +CODEC_VERSION = 4 # ── The classification (census-pinned) ─────────────────────────────── @@ -65,8 +66,37 @@ "_req_id", "stuck_tracker", # exported as plain state, re-seeded on restore "_trajectory", # full dict incl. iterations; rebuilt on restore + # Context-budget campaign (codec v3): recovery state that must survive + # suspend/resume so a rescued generation stays the SAME generation. + "_boundary_request_start", + "_boundary_elided_replay", + "_boundary_envelope_len", + "_char_latch", + "_rescue_passes", + "_gen_identity", # identity FACTS (provider/model/effort/ladder) }) +#: Added in codec v3. Version-scoped normalization (the wait_judgment +#: precedent): payloads written before the campaign default these to the +#: pre-campaign semantics — request_start=0 protects the whole structural +#: prefix exactly as recovery-less chat always did. +_V3_FIELD_DEFAULTS: dict[str, object] = { + "_boundary_request_start": 0, + "_boundary_elided_replay": 0, + "_char_latch": None, + "_rescue_passes": 0, + "_gen_identity": None, +} + +_V4_FIELD_DEFAULTS: dict[str, object] = {"_boundary_envelope_len": None} +_GEN_IDENTITY_KEYS = { + "provider", "model", "effort", "ladder", "budget", "attempts", +} +_GEN_IDENTITY_V3_KEYS = {"provider", "model", "effort", "ladder"} +_GEN_ATTEMPT_KEYS = {"attempt", "account_key", "server_input_tokens"} +_GEN_PROVIDERS = {"codex", "ollama", "kimi"} +_GEN_EFFORTS = CODEX_REASONING_EFFORTS + #: Rebuilt by the resume flow from live state. Each entry documents why it #: is NOT persisted: #: - message: a live discord.Message — re-fetched from channel+message id; @@ -89,6 +119,7 @@ "tools", "policy", "trace", + "_generation_budget_snapshot", "durability", }) @@ -306,6 +337,7 @@ def _scrubbed_iteration(it) -> dict: "total_input_tokens": trajectory.total_input_tokens, "total_output_tokens": trajectory.total_output_tokens, "total_duration_ms": trajectory.total_duration_ms, + "context_recoveries": list(trajectory.context_recoveries or []), "iteration_revision": len(trajectory.iterations), } @@ -341,6 +373,7 @@ def trajectory_from_payload(data: dict): turn.user_content_truncated = bool(data.get("user_content_truncated", False)) turn.user_content_original_chars = int(data.get("user_content_original_chars", 0) or 0) turn.total_input_tokens = int(data.get("total_input_tokens", 0) or 0) + turn.context_recoveries = list(data.get("context_recoveries") or []) turn.total_output_tokens = int(data.get("total_output_tokens", 0) or 0) turn.total_duration_ms = int(data.get("total_duration_ms", 0) or 0) return turn @@ -390,6 +423,12 @@ def snapshot_chat_turn(st, *, store_blob, generation_seq: int, extra: dict | Non "_req_id": st._req_id, "stuck_tracker": export_stuck_tracker(st.stuck_tracker), "_trajectory": _trajectory_to_payload(st._trajectory), + "_boundary_request_start": st._boundary_request_start, + "_boundary_elided_replay": st._boundary_elided_replay, + "_boundary_envelope_len": st._boundary_envelope_len, + "_char_latch": st._char_latch, + "_rescue_passes": st._rescue_passes, + "_gen_identity": dict(st._gen_identity) if st._gen_identity else None, }, } if extra: @@ -476,6 +515,14 @@ def _exact_int(value: Any) -> bool: # normalization can never launder an edit. if version == 1 and "wait_judgment_pending" not in fields: fields["wait_judgment_pending"] = False + if version <= 2: + for name, default in _V3_FIELD_DEFAULTS.items(): + if name not in fields: + fields[name] = default + if version <= 3: + for name, default in _V4_FIELD_DEFAULTS.items(): + if name not in fields: + fields[name] = default missing = PERSISTED_FIELDS - fields.keys() if missing: raise CheckpointInvalidError(f"missing persisted fields: {sorted(missing)}") @@ -551,6 +598,18 @@ def _fail(name: str, why: str) -> None: # explode later in transcript repair, outside the rejection boundary). if not isinstance(fields["messages"], list): _fail("messages", "must be a list") + for name in ("_boundary_request_start", "_boundary_elided_replay", "_rescue_passes"): + value = fields[name] + if not _exact_int(value) or value < 0: + _fail(name, "must be a non-negative integer") + envelope_len = fields["_boundary_envelope_len"] + if envelope_len is not None and (not _exact_int(envelope_len) or envelope_len < 0): + _fail("_boundary_envelope_len", "must be null or a non-negative integer") + if version >= 4 and envelope_len is None: + _fail("_boundary_envelope_len", "must be an integer in codec v4") + latch = fields["_char_latch"] + if latch is not None and (not _exact_int(latch) or latch < 0): + _fail("_char_latch", "must be null or a non-negative integer") for i, msg in enumerate(fields["messages"]): if not isinstance(msg, dict) or not isinstance(msg.get("role"), str): raise CheckpointInvalidError(f"messages[{i}] is not a message object") @@ -565,6 +624,98 @@ def _fail(name: str, why: str) -> None: f"messages[{i}] content has invalid type {type(content).__name__}" ) + messages = fields["messages"] + request_start = fields["_boundary_request_start"] + elided_replay = fields["_boundary_elided_replay"] + if request_start > len(messages): + _fail("_boundary_request_start", "exceeds message count") + if envelope_len is not None and request_start + envelope_len > len(messages): + _fail("_boundary_envelope_len", "extends beyond messages") + if elided_replay: + expected = f"[Context recovery: {elided_replay} older conversation messages elided]" + if request_start < 1 or not messages or messages[0].get("content") != expected: + _fail("_boundary_elided_replay", "does not match the leading replay marker") + + gen_identity = fields["_gen_identity"] + rescue_passes = fields["_rescue_passes"] + if gen_identity is None: + if rescue_passes != 0: + _fail("_rescue_passes", "requires _gen_identity") + else: + if not isinstance(gen_identity, dict): + _fail("_gen_identity", "must be an object") + identity_keys = set(gen_identity) + legacy_v3_identity = version == 3 and identity_keys == _GEN_IDENTITY_V3_KEYS + if identity_keys != _GEN_IDENTITY_KEYS and not legacy_v3_identity: + _fail("_gen_identity", "has an invalid key set") + if gen_identity.get("provider") not in _GEN_PROVIDERS: + _fail("_gen_identity", "provider is invalid") + model = gen_identity.get("model") + if not isinstance(model, str) or not model.strip(): + _fail("_gen_identity", "model must be a non-empty string") + effort = gen_identity.get("effort") + provider = gen_identity["provider"] + if provider == "codex": + if effort not in _GEN_EFFORTS: + _fail("_gen_identity", "codex effort must be resolved") + if model_rejects_effort(model, effort): + _fail("_gen_identity", "codex model/effort pair is incompatible") + elif effort is not None: + _fail("_gen_identity", "non-codex provider cannot carry codex effort") + ladder = gen_identity.get("ladder") + if ( + not isinstance(ladder, list) + or not ladder + or not all(_exact_int(rung) and rung > 0 for rung in ladder) + or ladder != sorted(set(ladder), reverse=True) + ): + _fail("_gen_identity", "ladder must be positive, unique, and descending") + if rescue_passes < 1 or rescue_passes > len(ladder): + _fail("_rescue_passes", "is outside the frozen ladder") + if legacy_v3_identity: + # Early phase-4 checkpoints carried the frozen routing/ladder + # facts but predated provider-evidence fields. Normalize only + # that exact versioned shape; v4 is always the six-key schema. + gen_identity["budget"] = {"primary_chars": max(ladder)} + gen_identity["attempts"] = [ + { + "attempt": attempt, + "account_key": None, + "server_input_tokens": None, + } + for attempt in range(1, rescue_passes + 1) + ] + budget = gen_identity.get("budget") + if ( + not isinstance(budget, dict) + or set(budget) != {"primary_chars"} + or not _exact_int(budget.get("primary_chars")) + or budget["primary_chars"] < 0 + ): + _fail("_gen_identity", "budget must contain one non-negative primary_chars") + if any(rung > budget["primary_chars"] for rung in ladder): + _fail("_gen_identity", "ladder exceeds the frozen primary budget") + attempts = gen_identity.get("attempts") + if not isinstance(attempts, list) or len(attempts) != rescue_passes: + _fail("_gen_identity", "attempts must match the consumed rescue passes") + for index, attempt in enumerate(attempts, start=1): + if not isinstance(attempt, dict) or set(attempt) != _GEN_ATTEMPT_KEYS: + _fail("_gen_identity", f"attempt {index} has an invalid key set") + if attempt.get("attempt") != index: + _fail("_gen_identity", f"attempt {index} has an invalid ordinal") + account_key = attempt.get("account_key") + if account_key is not None and ( + not isinstance(account_key, str) + or len(account_key) != 32 + or any(ch not in "0123456789abcdef" for ch in account_key) + ): + _fail("_gen_identity", f"attempt {index} account_key is invalid") + server_tokens = attempt.get("server_input_tokens") + if server_tokens is not None and ( + not _exact_int(server_tokens) or server_tokens < 0 + ): + _fail("_gen_identity", f"attempt {index} server_input_tokens is invalid") + def restore_field_values(payload: dict, *, load_blob, stuck_tracker_cls) -> dict: """Persisted-field constructor kwargs for `_ChatTurn`. diff --git a/src/turn_state/durability.py b/src/turn_state/durability.py index f0255e2f..369a19db 100644 --- a/src/turn_state/durability.py +++ b/src/turn_state/durability.py @@ -289,6 +289,22 @@ async def on_generation_start(self, st, deadline_seconds: float) -> None: recovery_deadline_utc=time.time() + max(0.0, deadline_seconds), ) + async def on_context_recovery(self, st) -> None: + """Persist the rescued transcript + ladder phase BEFORE the retry. + + The settled six-step sequence (campaign phase 4, contract §7): + compression already succeeded locally and the recovery record is on + the trajectory; this checkpoint makes the mutated transcript, the + boundary state, and the rung phase durable with ``progressed=False`` + — no ``generation_seq`` bump, no intents, no progress claim, and the + stored ``recovery_deadline_utc`` untouched. A durability write + failure PROPAGATES: the retry must never run ahead of what resume + can reconstruct. + """ + if not self.enabled: + return + await asyncio.to_thread(self._checkpoint_sync, st, progressed=False) + async def on_llm_response(self, st, tool_calls: list) -> None: """WI-1: response transcript + PREPARED intents, before any effect. diff --git a/src/web/api/__init__.py b/src/web/api/__init__.py index 9581aa00..c1f11a52 100644 --- a/src/web/api/__init__.py +++ b/src/web/api/__init__.py @@ -66,6 +66,7 @@ ) from .llm_admin import ( # noqa: E501 register_connection_pools, + register_context_windows, register_kimi_admin, register_llm_provider, register_ollama_admin, @@ -178,6 +179,8 @@ def create_api_routes(bot: OdinBot) -> web.RouteTableDef: register_provider_config(routes, bot) + register_context_windows(routes, bot) + register_ollama_admin(routes, bot) register_kimi_admin(routes, bot) diff --git a/src/web/api/agents_loops.py b/src/web/api/agents_loops.py index da3fa561..4f459720 100644 --- a/src/web/api/agents_loops.py +++ b/src/web/api/agents_loops.py @@ -8,6 +8,7 @@ from __future__ import annotations +import asyncio import time from aiohttp import web @@ -161,9 +162,11 @@ async def start_loop(request: web.Request) -> web.Response: # Build iteration callback (same pattern as _handle_start_loop) async def _iteration_cb( prompt: str, ch: object, prev_context: str | None, + cancel_event: asyncio.Event, ) -> str: return await bot.tool_loop.run_autonomous( prompt, ch, prev_context, requester_id, + cancel_event=cancel_event, ) result = bot.loop_manager.start_loop( @@ -184,7 +187,7 @@ async def _iteration_cb( @routes.delete("/api/loops/{loop_id}") async def stop_loop(request: web.Request) -> web.Response: lid = request.match_info["loop_id"] - result = bot.loop_manager.stop_loop(lid) + result = await bot.loop_manager.stop_loop(lid) is_error = "not found" in result.lower() or "not running" in result.lower() return web.json_response( {"result": result}, status=404 if is_error else 200 @@ -209,7 +212,7 @@ async def restart_loop(request: web.Request) -> web.Response: # Stop if running if info.status == "running": - bot.loop_manager.stop_loop(lid) + await bot.loop_manager.stop_loop(lid) # Find the channel try: diff --git a/src/web/api/llm_admin.py b/src/web/api/llm_admin.py index 937f78e6..a5c587c7 100644 --- a/src/web/api/llm_admin.py +++ b/src/web/api/llm_admin.py @@ -27,13 +27,20 @@ allowed_efforts_for_model, effort_incompatibility_error, ) +from ...llm.window_observer import WindowObserverMutationError from ...odin_log import get_logger log = get_logger("web.api") -_ALLOWED_OLLAMA_HOSTS = frozenset({ - "localhost", "127.0.0.1", "::1", "0.0.0.0", -}) +_ALLOWED_OLLAMA_HOSTS = frozenset( + { + "localhost", + "127.0.0.1", + "::1", + "0.0.0.0", + } +) + def _validate_ollama_url(url: str) -> str: """Validate Ollama base_url — restrict to local/private networks to prevent SSRF.""" @@ -57,6 +64,7 @@ def _validate_ollama_url(url: str) -> str: pass try: import socket + resolved = socket.getaddrinfo(host, None, socket.AF_UNSPEC, socket.SOCK_STREAM) if not resolved: raise ValueError(f"Could not resolve hostname: {host}") @@ -85,6 +93,7 @@ def _parse_int(val, name: str, lo: int = 1, hi: int = 262000) -> int: raise ValueError(f"{name} must be between {lo} and {hi}") return v + def register_connection_pools(routes: web.RouteTableDef, bot) -> None: """Connection pool status (verbatim from the monolith).""" # ------------------------------------------------------------------ @@ -264,6 +273,8 @@ async def llm_status(_request: web.Request) -> web.Response: "context_compression": desired_compression, "effective_context_compression": effective_compression, "context_compression_pending_restart": compression_pending_restart, + "context_budget_overrides": dict(bot.config.openai_codex.context_budget_overrides), + "context_utilization": bot.config.openai_codex.context_utilization, }, "ollama": { "configured": ollama_configured, @@ -316,7 +327,7 @@ async def llm_switch(request: web.Request) -> web.Response: result = await bot.llm_gateway.switch_provider( provider, persist=lambda: patch_config_paths( - [(('llm_provider', 'active_provider'), provider)] + [(("llm_provider", "active_provider"), provider)] ), ) if "error" in result: @@ -337,9 +348,7 @@ def _provider_changes( return [((section, key), desired[key]) for key in desired if key in body] -async def _persist_or_response( - changes: list, label: str -) -> tuple[web.Response | None, bool]: +async def _persist_or_response(changes: list, label: str) -> tuple[web.Response | None, bool]: """Persist desired leaves and return explicit error/cancel outcomes.""" persist_exc, was_cancelled = await persist_config_paths_locked(changes) if persist_exc is not None: @@ -347,9 +356,7 @@ async def _persist_or_response( if was_cancelled: raise asyncio.CancelledError return ( - web.json_response( - {"error": f"{label} configuration not saved"}, status=500 - ), + web.json_response({"error": f"{label} configuration not saved"}, status=500), False, ) return None, was_cancelled @@ -414,7 +421,9 @@ def _schema_int(value: Any, name: str, lo: int, hi: int) -> int: if "stream_stall_timeout_seconds" in body: value = _schema_int( body["stream_stall_timeout_seconds"], - "stream_stall_timeout_seconds", 10, 3600, + "stream_stall_timeout_seconds", + 10, + 3600, ) persist.append((("openai_codex", "stream_stall_timeout_seconds"), value)) ops.append(("stream_stall_timeout_seconds", value)) @@ -453,10 +462,31 @@ def _schema_int(value: Any, name: str, lo: int, hi: int) -> int: status=400, ) ops.append((group, merged)) - persist.extend( - (("openai_codex", group, key), getattr(merged, key)) for key in submitted - ) + persist.extend((("openai_codex", group, key), getattr(merged, key)) for key in submitted) wants_reload = wants_reload or live + try: + candidate_payload = cfg.model_dump() + if "context_budget_overrides" in body: + candidate_payload["context_budget_overrides"] = body["context_budget_overrides"] + if "context_utilization" in body: + candidate_payload["context_utilization"] = body["context_utilization"] + if "context_budget_overrides" in body or "context_utilization" in body: + from ...config.schema import OpenAICodexConfig + + candidate = OpenAICodexConfig(**candidate_payload) + for field in ("context_budget_overrides", "context_utilization"): + if field not in body: + continue + value = getattr(candidate, field) + persist.append((("openai_codex", field), value)) + ops.append((field, value)) + except ValidationError as exc: + first = exc.errors()[0] + loc = ".".join(str(part) for part in first.get("loc", ())) + return web.json_response( + {"error": f"{loc}: {first.get('msg', 'invalid value')}"}, + status=400, + ) return persist, ops, wants_reload @@ -553,9 +583,7 @@ async def llm_codex_config(request: web.Request) -> web.Response: }, status=400, ) - desired_agent_model = ( - agent_model if agent_model_present else cfg.agent_model - ) + desired_agent_model = agent_model if agent_model_present else cfg.agent_model desired_agent_effort = ( (None if agent_effort is None else str(agent_effort)) if agent_effort_present @@ -565,12 +593,8 @@ async def llm_codex_config(request: web.Request) -> web.Response: # request-construction boundaries; concrete axes resolve here # (None inherits the main setting being saved). if AGENT_SETTING_AUTO not in (desired_agent_model, desired_agent_effort): - eff_model = ( - desired_agent_model if desired_agent_model else desired_model - ) - eff_effort = ( - desired_agent_effort if desired_agent_effort else desired_effort - ) + eff_model = desired_agent_model if desired_agent_model else desired_model + eff_effort = desired_agent_effort if desired_agent_effort else desired_effort pair_err = effort_incompatibility_error(eff_model, eff_effort) if pair_err: return web.json_response( @@ -599,9 +623,7 @@ async def llm_codex_config(request: web.Request) -> web.Response: adv_persist, adv_ops, adv_reload = advanced changes = _provider_changes("openai_codex", desired, body) changes = changes + adv_persist - persist_response, was_cancelled = await _persist_or_response( - changes, "Codex" - ) + persist_response, was_cancelled = await _persist_or_response(changes, "Codex") if persist_response is not None: return persist_response if changes: @@ -613,8 +635,7 @@ async def llm_codex_config(request: web.Request) -> web.Response: # handler dropped all of these silently and returned 200. adv_inverse = _apply_ops(cfg, adv_ops) needs_reload = adv_reload or any( - key in body - for key in ("enabled", "model", "reasoning_effort") + key in body for key in ("enabled", "model", "reasoning_effort") ) try: if needs_reload: @@ -622,9 +643,7 @@ async def llm_codex_config(request: web.Request) -> web.Response: except BaseException: _set_fields(cfg, prior) _apply_ops(cfg, adv_inverse) # restore prior objects - adv_prior_persist: list[ - tuple[tuple[str, ...], Any] - ] = [] + adv_prior_persist: list[tuple[tuple[str, ...], Any]] = [] for attr, prior_value in adv_inverse: if hasattr(prior_value, "model_dump"): adv_prior_persist.extend( @@ -632,9 +651,7 @@ async def llm_codex_config(request: web.Request) -> web.Response: for key, val in prior_value.model_dump().items() ) else: - adv_prior_persist.append( - (("openai_codex", attr), prior_value) - ) + adv_prior_persist.append((("openai_codex", attr), prior_value)) prior_persist: list[tuple[tuple[str, ...], Any]] = [ (("openai_codex", key), value) for key, value in prior.items() @@ -679,19 +696,19 @@ async def llm_codex_config(request: web.Request) -> web.Response: return web.json_response({"error": str(e)}, status=400) except Exception as e: log.warning("Codex configuration apply failed: %s", e) - return web.json_response( - {"error": "Codex configuration not applied"}, status=500 - ) - - return web.json_response({ - "status": "updated", - "enabled": cfg.enabled, - "model": cfg.model, - "reasoning_effort": cfg.reasoning_effort, - "agent_reasoning_effort": cfg.agent_reasoning_effort, - "agent_model": cfg.agent_model, - "configured": bot.llm_gateway.codex_client is not None, - }) + return web.json_response({"error": "Codex configuration not applied"}, status=500) + + return web.json_response( + { + "status": "updated", + "enabled": cfg.enabled, + "model": cfg.model, + "reasoning_effort": cfg.reasoning_effort, + "agent_reasoning_effort": cfg.agent_reasoning_effort, + "agent_model": cfg.agent_model, + "configured": bot.llm_gateway.codex_client is not None, + } + ) @routes.put("/api/llm/auxiliary/config") async def llm_auxiliary_config(request: web.Request) -> web.Response: @@ -708,13 +725,9 @@ async def llm_auxiliary_config(request: web.Request) -> web.Response: async with config_transaction(): aux_cfg = getattr(bot.config.openai_codex, "auxiliary", None) if aux_cfg is None: - return web.json_response( - {"error": "auxiliary config unavailable"}, status=503 - ) + return web.json_response({"error": "auxiliary config unavailable"}, status=503) - want_enabled = ( - bool(body["enabled"]) if "enabled" in body else aux_cfg.enabled - ) + want_enabled = bool(body["enabled"]) if "enabled" in body else aux_cfg.enabled want_model = aux_cfg.model if "model" in body and str(body["model"]).strip(): want_model = str(body["model"]).strip() @@ -782,9 +795,7 @@ async def llm_ollama_config(request: web.Request) -> web.Response: ), } changes = _provider_changes("ollama", desired, body) - persist_response, was_cancelled = await _persist_or_response( - changes, "Ollama" - ) + persist_response, was_cancelled = await _persist_or_response(changes, "Ollama") if persist_response is not None: return persist_response if changes: @@ -820,17 +831,17 @@ async def llm_ollama_config(request: web.Request) -> web.Response: return web.json_response({"error": str(e)}, status=400) except Exception as e: log.warning("Ollama configuration apply failed: %s", e) - return web.json_response( - {"error": "Ollama configuration not applied"}, status=500 - ) - - return web.json_response({ - "status": "updated", - "enabled": cfg.enabled, - "model": cfg.model, - "base_url": cfg.base_url, - "configured": bot.llm_gateway.ollama_client is not None, - }) + return web.json_response({"error": "Ollama configuration not applied"}, status=500) + + return web.json_response( + { + "status": "updated", + "enabled": cfg.enabled, + "model": cfg.model, + "base_url": cfg.base_url, + "configured": bot.llm_gateway.ollama_client is not None, + } + ) @routes.put("/api/llm/kimi/config") async def llm_kimi_config(request: web.Request) -> web.Response: @@ -865,9 +876,7 @@ async def llm_kimi_config(request: web.Request) -> web.Response: ), } changes = _provider_changes("kimi", desired, body) - persist_response, was_cancelled = await _persist_or_response( - changes, "Kimi" - ) + persist_response, was_cancelled = await _persist_or_response(changes, "Kimi") if persist_response is not None: return persist_response if changes: @@ -880,11 +889,7 @@ async def llm_kimi_config(request: web.Request) -> web.Response: _set_fields(cfg, prior) bot.llm_gateway.kimi_client = prior_client rollback_exc, rollback_cancelled = await persist_config_paths_locked( - [ - (("kimi", key), value) - for key, value in prior.items() - if key in body - ] + [(("kimi", key), value) for key, value in prior.items() if key in body] ) if rollback_exc is not None: log.critical( @@ -903,16 +908,190 @@ async def llm_kimi_config(request: web.Request) -> web.Response: return web.json_response({"error": str(e)}, status=400) except Exception as e: log.warning("Kimi configuration apply failed: %s", e) - return web.json_response( - {"error": "Kimi configuration not applied"}, status=500 + return web.json_response({"error": "Kimi configuration not applied"}, status=500) + + return web.json_response( + { + "status": "updated", + "enabled": cfg.enabled, + "model": cfg.model, + "configured": bot.llm_gateway.kimi_client is not None, + } + ) + + +def register_context_windows(routes: web.RouteTableDef, bot) -> None: + """Per-model context-budget view + clamp management (campaign phase 5). + + The GET serves canonical model keys with the built-in floor, configured + override, CONFIGURED resolution (no clamp) and EFFECTIVE resolution + (observer clamp applied) side by side, provenance for both, and the raw + per-account evidence (opaque account keys only). The POST clears clamps + for one account (optionally one model) — the manual, account-scoped + escape hatch; TTL expiry is otherwise the only way a clamp dies. + """ + + def _observer(): + return getattr(getattr(bot, "services", None), "window_observer", None) + + def _resolution(snapshot) -> dict: + return { + "base_budget": snapshot.base_budget, + "base_source": snapshot.base_source, + "effective_budget": snapshot.effective_budget, + "clamp_applied": snapshot.clamp_applied, + "working_budget": snapshot.working_budget, + "primary_chars": snapshot.primary_chars, + "ceiling_applied": snapshot.ceiling_applied, + "ladder": list(snapshot.ladder), + } + + @routes.get("/api/context/windows") + async def get_context_windows(_request: web.Request) -> web.Response: + from ...config.schema import ( + CODEX_MODEL_INPUT_BUDGETS, + canonical_codex_model, + ) + from ...llm.context_budget import resolve_context_budget + + observer = _observer() + codex_cfg = getattr(bot.config, "openai_codex", None) + overrides = { + canonical_codex_model(k): v + for k, v in (getattr(codex_cfg, "context_budget_overrides", None) or {}).items() + } + utilization = getattr(codex_cfg, "context_utilization", 60) + cc = getattr(codex_cfg, "context_compression", None) + configured_ceiling = ( + getattr(cc, "max_context_chars", None) if cc is not None else None + ) + desired_compression = cc.model_dump() if cc is not None else {} + effective_compression, ceiling_pending_restart = _boot_codex_group_status( + bot, "context_compression", desired_compression + ) + # The runtime compressor is boot-frozen. Prefer the shared boot snapshot + # used by /api/llm/status; a narrow embedding without it reports runtime + # truth directly from the compressor rather than pretending the saved + # restart-bound value is effective. + if effective_compression is not None: + # Disabled-at-boot means the runtime has no compressor and generation + # applies no explicit ceiling. The saved scalar remains configuration, + # not runtime policy, until compression is enabled by a restart. + runtime_ceiling = ( + effective_compression.get("max_context_chars") + if effective_compression.get("enabled") + else None + ) + else: + compressor = getattr(bot, "context_compressor", None) + # Production stores the boot-frozen ContextCompressionConfig object + # directly; a few embedders wrap it as ``.config``. + runtime_cfg = getattr(compressor, "config", compressor) + runtime_available = runtime_cfg is not None and hasattr( + runtime_cfg, "max_context_chars" + ) + # With no boot snapshot and no runtime compressor, the only safe + # runtime ceiling is none: generation applies the model-derived + # target. Restart-pending provenance remains unknown rather than + # guessing from a mutable saved config object. + runtime_ceiling = ( + getattr(runtime_cfg, "max_context_chars", None) + if runtime_available + else None ) + ceiling_pending_restart = ( + runtime_ceiling != configured_ceiling if runtime_available else None + ) + evidence: dict = observer.view() if observer is not None else {"version": 1, "accounts": {}} + # Resolve eligibility once for one internally-consistent management + # snapshot. Re-reading a changing pool provider per model could pair a + # clamp from one account set with expiry rows from another. + clamp_rows = observer.account_clamps() if observer is not None else [] + active_clamp_rows: dict[str, dict] = {} + for row in clamp_rows: + prior = active_clamp_rows.get(row["model"]) + if ( + prior is None + or row["value"] < prior["value"] + or (row["value"] == prior["value"] and row["expires_at"] > prior["expires_at"]) + ): + active_clamp_rows[row["model"]] = row + models = set(CODEX_MODEL_INPUT_BUDGETS) | set(overrides) + for account in evidence.get("accounts", {}).values(): + models |= set(account.get("models", {})) + out = {} + for model in sorted(models): + active_row = active_clamp_rows.get(model) + clamp = active_row["value"] if active_row is not None else None + configured = resolve_context_budget( + model, + overrides=overrides, + utilization=utilization, + max_context_chars=configured_ceiling, + ) + effective = resolve_context_budget( + model, + overrides=overrides, + utilization=utilization, + max_context_chars=runtime_ceiling, + observed_clamp=clamp, + ) + out[model] = { + "floor": CODEX_MODEL_INPUT_BUDGETS.get(model), + "override": overrides.get(model), + "active_clamp": clamp, + "provenance": ( + "temporary learned clamp" + if effective.clamp_applied + else "override" + if configured.base_source == "override" + else "built-in" + ), + "configured": _resolution(configured), + "effective": _resolution(effective), + "clamp_expires_at": ( + active_row["expires_at"] + if effective.clamp_applied and active_row is not None + else None + ), + } + return web.json_response( + { + "utilization": utilization, + "max_context_chars": configured_ceiling, + "runtime_max_context_chars": runtime_ceiling, + "max_context_chars_pending_restart": ceiling_pending_restart, + "models": out, + "clamps": clamp_rows, + "evidence": evidence, + } + ) - return web.json_response({ - "status": "updated", - "enabled": cfg.enabled, - "model": cfg.model, - "configured": bot.llm_gateway.kimi_client is not None, - }) + @routes.post("/api/context/windows/clear") + async def clear_context_window_clamp(request: web.Request) -> web.Response: + observer = _observer() + if observer is None: + return web.json_response({"error": "window observer not available"}, status=503) + try: + data = await request.json() + except Exception: + data = {} + if not isinstance(data, dict): + return web.json_response({"error": "JSON body must be an object"}, status=400) + account_key = data.get("account_key") + if not isinstance(account_key, str) or not account_key.strip(): + return web.json_response({"error": "account_key is required"}, status=400) + model = data.get("model") + try: + cleared = await observer.clear_account( + account_key.strip(), + model=str(model).strip() if isinstance(model, str) and model.strip() else None, + ) + except WindowObserverMutationError: + return web.json_response( + {"error": "context-window clear could not be persisted"}, status=503 + ) + return web.json_response({"cleared": cleared}) def register_ollama_admin(routes: web.RouteTableDef, bot) -> None: @@ -928,14 +1107,16 @@ async def ollama_status(_request: web.Request) -> web.Response: return web.json_response({"configured": False, "enabled": False}) health = await client.health_check() - return web.json_response({ - "configured": True, - "enabled": True, - "model": client.model, - "base_url": client.base_url, - "health": health, - "stats": client.pool_stats(), - }) + return web.json_response( + { + "configured": True, + "enabled": True, + "model": client.model, + "base_url": client.base_url, + "health": health, + "stats": client.pool_stats(), + } + ) @routes.post("/api/ollama/reload") async def ollama_reload(_request: web.Request) -> web.Response: @@ -962,6 +1143,7 @@ async def ollama_probe_models(request: web.Request) -> web.Response: ) try: import aiohttp as _aio + async with _aio.ClientSession(timeout=_aio.ClientTimeout(total=10)) as sess: async with sess.get(f"{base_url}/api/tags") as resp: if resp.status != 200: @@ -979,6 +1161,7 @@ async def ollama_models(_request: web.Request) -> web.Response: try: import aiohttp as _aiohttp + session = await client._get_session() async with session.get( f"{client.base_url}/api/tags", @@ -988,10 +1171,12 @@ async def ollama_models(_request: web.Request) -> web.Response: if resp.status != 200: return web.json_response({"error": f"HTTP {resp.status}"}, status=502) data = await resp.json() - return web.json_response({ - "models": data.get("models", []), - "active_model": client.model, - }) + return web.json_response( + { + "models": data.get("models", []), + "active_model": client.model, + } + ) except Exception as e: return web.json_response({"error": str(e)}, status=502) @@ -1022,22 +1207,23 @@ async def ollama_set_model(request: web.Request) -> web.Response: if available and model not in available: base = model.split(":")[0] if not any(m.startswith(base + ":") for m in available): - return web.json_response({ - "error": ( - f"Model '{model}' not available. " - f"Pulled models: {', '.join(available[:10])}" - ), - }, status=400) + return web.json_response( + { + "error": ( + f"Model '{model}' not available. " + f"Pulled models: {', '.join(available[:10])}" + ), + }, + status=400, + ) persist_exc, was_cancelled = await persist_config_paths_locked( - [(('ollama', 'model'), model)] + [(("ollama", "model"), model)] ) if persist_exc is not None: if was_cancelled: raise asyncio.CancelledError - return web.json_response( - {"error": "Ollama model not saved"}, status=500 - ) + return web.json_response({"error": "Ollama model not saved"}, status=500) client.model = model bot.config.ollama.model = model if was_cancelled: @@ -1058,14 +1244,16 @@ async def kimi_status(_request: web.Request) -> web.Response: return web.json_response({"configured": False, "enabled": False}) health = await client.health_check() - return web.json_response({ - "configured": True, - "enabled": True, - "model": client.model, - "base_url": client.base_url, - "health": health, - "stats": client.pool_stats(), - }) + return web.json_response( + { + "configured": True, + "enabled": True, + "model": client.model, + "base_url": client.base_url, + "health": health, + "stats": client.pool_stats(), + } + ) @routes.post("/api/kimi/reload") async def kimi_reload(_request: web.Request) -> web.Response: @@ -1082,10 +1270,12 @@ async def kimi_models(_request: web.Request) -> web.Response: health = await client.health_check() if not health.get("healthy"): return web.json_response({"error": health.get("error", "unhealthy")}, status=502) - return web.json_response({ - "models": health.get("models", []), - "active_model": client.model, - }) + return web.json_response( + { + "models": health.get("models", []), + "active_model": client.model, + } + ) @routes.post("/api/kimi/model") async def kimi_set_model(request: web.Request) -> web.Response: @@ -1112,23 +1302,24 @@ async def kimi_set_model(request: web.Request) -> web.Response: health = await client.health_check() available = health.get("models", []) if available and model not in available: - return web.json_response({ - "error": f"Model '{model}' not available. Models: {', '.join(available[:10])}", - }, status=400) + return web.json_response( + { + "error": ( + f"Model '{model}' not available. Models: {', '.join(available[:10])}" + ), + }, + status=400, + ) persist_exc, was_cancelled = await persist_config_paths_locked( - [(('kimi', 'model'), model)] + [(("kimi", "model"), model)] ) if persist_exc is not None: if was_cancelled: raise asyncio.CancelledError - return web.json_response( - {"error": "Kimi model not saved"}, status=500 - ) + return web.json_response({"error": "Kimi model not saved"}, status=500) client.model = model bot.config.kimi.model = model if was_cancelled: raise asyncio.CancelledError return web.json_response({"status": "updated", "model": model}) - - diff --git a/src/web/api/self_update.py b/src/web/api/self_update.py index 795b78a7..6791bc5e 100644 --- a/src/web/api/self_update.py +++ b/src/web/api/self_update.py @@ -252,7 +252,7 @@ def _rollback(reason: str) -> web.Response: @routes.post("/api/loops/stop-all") async def stop_all_loops(_request: web.Request) -> web.Response: - result = bot.loop_manager.stop_loop("all") + result = await bot.loop_manager.stop_loop("all") return web.json_response({"result": result}) diff --git a/tests/characterization/test_api_route_parity.py b/tests/characterization/test_api_route_parity.py index 36b9b3c0..5e292d7c 100644 --- a/tests/characterization/test_api_route_parity.py +++ b/tests/characterization/test_api_route_parity.py @@ -175,6 +175,8 @@ ("PUT", "/api/llm/auxiliary/config", "llm_auxiliary_config"), ("PUT", "/api/llm/ollama/config", "llm_ollama_config"), ("PUT", "/api/llm/kimi/config", "llm_kimi_config"), + ("GET", "/api/context/windows", "get_context_windows"), + ("POST", "/api/context/windows/clear", "clear_context_window_clamp"), ("GET", "/api/ollama/status", "ollama_status"), ("POST", "/api/ollama/reload", "ollama_reload"), ("POST", "/api/ollama/probe-models", "ollama_probe_models"), @@ -235,7 +237,7 @@ class TestRouteTableParity: def test_exact_route_list_and_order(self): actual = _routes() expected = [tuple(e) for e in EXPECTED_ROUTES] - assert len(actual) == len(expected) == 188 + assert len(actual) == len(expected) == 190 # set equality first for a readable diff on failure missing = set(expected) - set(actual) added = set(actual) - set(expected) diff --git a/tests/characterization/test_autonomous_loop.py b/tests/characterization/test_autonomous_loop.py index 3b357e6d..d5102f51 100644 --- a/tests/characterization/test_autonomous_loop.py +++ b/tests/characterization/test_autonomous_loop.py @@ -5,6 +5,7 @@ from __future__ import annotations +import asyncio from unittest.mock import AsyncMock import pytest @@ -65,6 +66,71 @@ async def test_natural_finish_returns_final_text(self): assert result == "iteration complete" assert len(fake.calls) == 2 + async def test_stop_loop_tool_self_stop_settles_without_cancellation_cycle(self): + """The REAL loop tool path runs stop_loop in a gather child task. + + Logical loop ownership, not asyncio.current_task identity, must make + this cooperative self-stop. It must return from the native handler, + settle the manager task, and never start another model attempt. + """ + bot = None + + def stop_current_loop(): + assert bot is not None + loop_id = next(iter(bot.loop_manager._loops)) + return tool_call_response(("stop_loop", {"loop_id": loop_id})) + + bot, fake = build([stop_current_loop]) + stop_entered = asyncio.Event() + release_stop = asyncio.Event() + real_stop = bot.loop_manager.stop_loop + + async def barrier_stop(loop_id): + stop_entered.set() + await release_stop.wait() + return await real_stop(loop_id) + + # Keep the production native-dispatch path intact; this barrier only + # holds the real manager call long enough to expose parent/child task + # topology deterministically. + bot.loop_manager.stop_loop = barrier_stop + channel = FakeChannel(id=777) + message = type( + "LoopStartMessage", + (), + { + "channel": channel, + "author": type( + "LoopStartAuthor", + (), + {"id": 4242, "__str__": lambda self: "tester"}, + )(), + }, + )() + started = bot.agent_task_tools._handle_start_loop( + message, + { + "goal": "stop yourself through the tool path", + "interval_seconds": 10, + "mode": "silent", + "max_iterations": 2, + }, + ) + assert started.startswith("Loop started") + loop_id = next(iter(bot.loop_manager._loops)) + task = bot.loop_manager._loops[loop_id]._task + assert task is not None + await asyncio.wait_for(stop_entered.wait(), timeout=2) + assert not task.done() + release_stop.set() + + await asyncio.wait_for(task, timeout=2) + + assert task.done() + assert bot.loop_manager._loops[loop_id].status == "stopped" + assert bot.loop_manager._loops[loop_id]._cancel_event.is_set() + assert len(fake.calls) == 1 + async def test_prev_context_synthetic_exchange_shape(self): bot, fake = build([text_response("ok")]) await run_iteration(bot, prompt="continue the work", prev="found 3 issues") diff --git a/tests/conftest.py b/tests/conftest.py index a94daca3..1d1215e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -257,3 +257,16 @@ def _process_containment(): os.environ.setdefault(JOB_TOKEN_ENV, DEFAULT_JOB_TOKEN) yield set_child_subreaper(previous) + + +@pytest.fixture(autouse=True) +def _isolated_account_key_path(tmp_path, monkeypatch): + """Keep the opaque-account-key material out of the working tree. + + Provider stamping derives keys via ``DEFAULT_KEY_PATH`` (resolved at + call time); without this, any test whose auth fake returns a real + account id would materialize ``data/account_key.secret`` in the repo. + """ + monkeypatch.setattr( + "src.llm.account_key.DEFAULT_KEY_PATH", tmp_path / "account_key.secret" + ) diff --git a/tests/test_agent_context_overflow.py b/tests/test_agent_context_overflow.py index a4b60571..8e6551d2 100644 --- a/tests/test_agent_context_overflow.py +++ b/tests/test_agent_context_overflow.py @@ -16,9 +16,8 @@ import pytest from src.agents.manager import ( - _EMERGENCY_TARGET_CHARS, - _EMERGENCY_TARGET_CHARS_AGGRESSIVE, AgentManager, + _fallback_budget_snapshot, _is_context_overflow, ) from src.llm.context_compressor import ( @@ -27,6 +26,13 @@ ) from src.llm.errors import LLMRequestError, LLMTransportError +# The no-provider fallback ladder (unknown-model snapshot): what legacy and +# non-codex spawn paths recover against. Rung names keep the old constants' +# roles — first rescue target, then the rescue-ceiling rung. +_FALLBACK_LADDER = _fallback_budget_snapshot().ladder +_RUNG_PRIMARY = _FALLBACK_LADDER[0] +_RUNG_AGGRESSIVE = _FALLBACK_LADDER[-1] + def _overflow_error() -> LLMRequestError: return LLMRequestError( @@ -92,7 +98,7 @@ def __init__(self, script): self.calls: list[list[dict]] = [] self.script = script - async def iteration_callback(self, messages, system_prompt, tools): + async def iteration_callback(self, messages, system_prompt, tools, generation_state=None): self.calls.append(copy.deepcopy(messages)) return await self.script(len(self.calls), messages) @@ -193,14 +199,14 @@ async def script(n, messages): # and the SECOND saw a compressed payload under the primary target. assert len(h.calls) == 2 retry_payload = h.calls[1] - assert estimate_message_chars(retry_payload) <= _EMERGENCY_TARGET_CHARS + assert estimate_message_chars(retry_payload) <= _RUNG_PRIMARY # Task preserved verbatim, newest iteration retained, pairing valid. assert retry_payload[0] == {"role": "user", "content": "TASK: research the thing"} assert "result-39" in json.dumps(retry_payload) assert _pairing_valid(retry_payload) # Latch set to the size that succeeded; recovery recorded. assert agent.context_char_ceiling is not None - assert agent.context_char_ceiling <= _EMERGENCY_TARGET_CHARS + assert agent.context_char_ceiling <= _RUNG_PRIMARY assert len(agent.context_recoveries) == 1 r = agent.context_recoveries[0] assert r["trigger"] == "overflow" and r["attempt"] == 1 and r["fits"] @@ -222,7 +228,7 @@ async def script(n, messages): # Exactly two emergency passes, then the existing failure handling — # no loops. assert len(h.calls) == 3 - assert estimate_message_chars(h.calls[2]) <= _EMERGENCY_TARGET_CHARS_AGGRESSIVE + assert estimate_message_chars(h.calls[2]) <= _RUNG_AGGRESSIVE assert agent.state.name == "FAILED" assert "context_length_exceeded" in (agent.error or "") assert [r["attempt"] for r in agent.context_recoveries] == [1, 2] @@ -398,13 +404,11 @@ def test_many_summarized_iterations_still_converge_at_both_targets(self): """Odin's repro: the fixed summary reserve underestimates a large summary and the candidate misses target by a hair — the compressor must CONVERGE, never return the original oversized payload.""" - from src.agents.manager import _EMERGENCY_TARGETS - # Runtime-shaped: hundreds of small-but-real iterations whose # summaries alone exceed the old 2,000-char reserve. msgs = _messages(520, 1_400) - assert estimate_message_chars(msgs) > max(_EMERGENCY_TARGETS) - for target in _EMERGENCY_TARGETS: + assert estimate_message_chars(msgs) > max(_FALLBACK_LADDER) + for target in _FALLBACK_LADDER: out, report = emergency_compress_for_window(msgs, target_chars=target) assert report["fits"], (target, report) assert estimate_message_chars(out) <= target @@ -453,7 +457,7 @@ async def fake_wait_for(awaitable, timeout): raise _overflow_error() return {"text": "DONE", "tool_calls": [], "stop_reason": "end_turn"} - async def cb(messages, system_prompt, tools): + async def cb(messages, system_prompt, tools, generation_state=None): return {"text": "unused", "tool_calls": []} with patch("src.agents.manager.asyncio.wait_for", side_effect=fake_wait_for): @@ -644,11 +648,11 @@ def test_aggressive_pass_reopens_first_pass_summary(self): for i in range(300): msgs.extend(_iteration(i, 1_400)) primary, first = emergency_compress_for_window( - msgs, target_chars=_EMERGENCY_TARGET_CHARS + msgs, target_chars=_RUNG_PRIMARY ) assert first["fits"] first_size = estimate_message_chars(primary) - assert first_size > _EMERGENCY_TARGET_CHARS_AGGRESSIVE + assert first_size > _RUNG_AGGRESSIVE summaries = [ m for m in primary if isinstance(m.get("content"), str) @@ -661,10 +665,10 @@ def test_aggressive_pass_reopens_first_pass_summary(self): assert estimate_message_chars(prefix + summaries) > 400_000 aggressive, second = emergency_compress_for_window( - primary, target_chars=_EMERGENCY_TARGET_CHARS_AGGRESSIVE + primary, target_chars=_RUNG_AGGRESSIVE ) assert second["fits"], second - assert estimate_message_chars(aggressive) <= _EMERGENCY_TARGET_CHARS_AGGRESSIVE + assert estimate_message_chars(aggressive) <= _RUNG_AGGRESSIVE assert estimate_message_chars(aggressive) < first_size assert aggressive[0] == msgs[0] assert "tu_299" in json.dumps(aggressive) @@ -688,7 +692,7 @@ def test_user_text_resembling_summary_is_not_removed_from_real_prefix(self): msgs.extend(_iteration(i, 1_400)) out, report = emergency_compress_for_window( - msgs, target_chars=_EMERGENCY_TARGET_CHARS + msgs, target_chars=_RUNG_PRIMARY ) assert report["fits"] assert out[:2] == [quoted, parent] @@ -720,11 +724,11 @@ def test_newest_multi_tool_iteration_survives_more_than_32_results(self): assert estimate_message_chars(msgs) > 1_200_000 out, report = emergency_compress_for_window( - msgs, target_chars=_EMERGENCY_TARGET_CHARS_AGGRESSIVE + msgs, target_chars=_RUNG_AGGRESSIVE ) blob = json.dumps(out) assert report["fits"], report - assert estimate_message_chars(out) <= _EMERGENCY_TARGET_CHARS_AGGRESSIVE + assert estimate_message_chars(out) <= _RUNG_AGGRESSIVE assert report["iterations_kept"] >= 1 assert all(f'"id": "bulk_{i}"' in blob for i in range(count)) assert all(f'"tool_use_id": "bulk_{i}"' in blob for i in range(count)) @@ -735,7 +739,7 @@ def test_no_summary_reserve_for_398500_prefix_and_one_iteration(self): # characters at the 400K target. The sole newest iteration is # compressible into that space, and no summary will exist. Charging # the old hypothetical 2K reserve rejected this recoverable payload. - target = _EMERGENCY_TARGET_CHARS_AGGRESSIVE + target = _RUNG_AGGRESSIVE prefix_content = "P" * (398_500 - len("user")) prefix = [{"role": "user", "content": prefix_content}] assert estimate_message_chars(prefix) == 398_500 @@ -756,3 +760,174 @@ def test_no_summary_reserve_for_398500_prefix_and_one_iteration(self): and m["content"].startswith("[Emergency context compression") for m in out ) + +class TestRound1BlockerPins: + """PR #273 review round-1 reproductions, pinned.""" + + async def test_latch_published_only_after_server_acceptance(self): + """Blocker #3: overflow → local fit → retry fails on a NON-overflow + error ⇒ the ceiling must NOT be published (a local fit is not + provider acceptance).""" + big = _messages(40, 30_000) + + async def script(n, messages): + if n == 1: + messages.clear() + messages.extend(copy.deepcopy(big)) + raise _overflow_error() + raise LLMTransportError("mid-retry transport death") + + h = _Harness(script) + mgr = AgentManager() + agent_id = h.spawn(mgr) + await _run_to_terminal(mgr, agent_id) + agent = mgr._agents[agent_id] + assert agent.state.name == "FAILED" + assert agent.context_char_ceiling is None + assert [r["trigger"] for r in agent.context_recoveries] == ["overflow"] + + async def test_latch_published_after_successful_retry(self): + big = _messages(40, 30_000) + + async def script(n, messages): + if n == 1: + messages.clear() + messages.extend(copy.deepcopy(big)) + raise _overflow_error() + return {"text": "DONE", "tool_calls": [], "stop_reason": "end_turn"} + + h = _Harness(script) + mgr = AgentManager() + agent_id = h.spawn(mgr) + await _run_to_terminal(mgr, agent_id) + agent = mgr._agents[agent_id] + assert agent.context_char_ceiling is not None + + async def test_rescue_ladder_comes_from_the_frozen_plan(self, monkeypatch): + """Blocker #1: the ladder of the request that ACTUALLY overflowed — + the generation plan's snapshot — governs rescue, not the spawn + provider's advisory (his repro: a sol advisory ladder attached to a + gpt-5.5 request).""" + from src.llm.context_budget import resolve_context_budget + + plan_snapshot = resolve_context_budget("gpt-5.5") # ladder (399001,) + targets: list[int] = [] + + import src.llm.context_compressor as cc_mod + + real = cc_mod.emergency_compress_for_window + + def recording(messages, *, target_chars, stats=None): + targets.append(target_chars) + return real(messages, target_chars=target_chars) + + monkeypatch.setattr( + "src.llm.context_compressor.emergency_compress_for_window", recording + ) + + async def script_cb(messages, system_prompt, tools, generation_state=None): + if generation_state is not None and "plan" not in generation_state: + # The callback captures its frozen identity BEFORE sending — + # a 5.5 request whose overflow must rescue on 5.5's ladder. + generation_state["plan"] = { + "client": object(), + "effort": None, + "model": "gpt-5.5", + "snapshot": plan_snapshot, + } + raise _overflow_error() + return {"text": "DONE", "tool_calls": [], "stop_reason": "end_turn"} + + from src.agents.manager import AgentInfo, _call_llm_with_recovery + + agent = AgentInfo( + id="a-pin", label="t", goal="g", channel_id="c1", + requester_id="u1", requester_name="user", + ) + agent.messages = _messages(40, 30_000) + agent.iteration_timeout = 50.0 + sol_advisory = resolve_context_budget("gpt-5.6-sol").ladder # (894180, 400000) + result = await _call_llm_with_recovery( + agent, script_cb, "sys", [], rescue_ladder=sol_advisory, + generation_state={}, + ) + assert result is not None + assert targets == [399_001] # the PLAN's rung, not the sol advisory + + +class TestRound2AuthoritativePlanPins: + async def test_authoritative_empty_ladder_fails_without_legacy_fallback(self, monkeypatch): + """A zero-clamp plan has no positive rescue rung. That is an honest + terminal result, not permission to widen back to unknown-model math.""" + from src.agents.manager import AgentInfo, _call_llm_with_recovery + from src.llm.context_budget import resolve_context_budget + + calls = 0 + targets: list[int] = [] + + async def callback(messages, system, tools, *, generation_state): + nonlocal calls + calls += 1 + if "plan" not in generation_state: + generation_state["plan"] = { + "client": object(), + "effort": "xhigh", + "model": "gpt-5.6-sol", + "snapshot": resolve_context_budget("gpt-5.6-sol", observed_clamp=0), + } + raise _overflow_error() + + def should_not_compact(messages, *, target_chars, stats=None): + targets.append(target_chars) + raise AssertionError("empty authoritative ladder used fallback") + + monkeypatch.setattr( + "src.llm.context_compressor.emergency_compress_for_window", + should_not_compact, + ) + agent = AgentInfo( + id="zero", + label="z", + goal="g", + channel_id="c", + requester_id="u", + requester_name="user", + ) + agent.messages = _messages(2, 1_000) + agent.iteration_timeout = 10 + result = await _call_llm_with_recovery( + agent, + callback, + "sys", + [], + rescue_ladder=_fallback_budget_snapshot().ladder, + generation_state={}, + ) + assert result is None + assert calls == 1 + assert targets == [] + assert agent.state.name == "FAILED" + + async def test_three_argument_callback_is_explicitly_unsupported(self): + """The manager contract requires the frozen-generation channel; a + legacy three-argument callback fails loudly rather than half-working.""" + from src.agents.manager import AgentInfo, _call_llm_with_recovery + + async def legacy_callback(messages, system, tools): + return {"text": "wrong", "tool_calls": [], "stop_reason": "end_turn"} + + agent = AgentInfo( + id="legacy", + label="l", + goal="g", + channel_id="c", + requester_id="u", + requester_name="user", + ) + agent.iteration_timeout = 10 + result = await _call_llm_with_recovery( + agent, legacy_callback, "sys", [], generation_state={} + ) + assert result is None + assert agent.state.name == "FAILED" + assert "LLM error" in agent.error diff --git a/tests/test_agent_lifecycle.py b/tests/test_agent_lifecycle.py index 05408c24..5a262299 100644 --- a/tests/test_agent_lifecycle.py +++ b/tests/test_agent_lifecycle.py @@ -483,7 +483,7 @@ async def test_kill_sends_to_terminal(self): mgr = AgentManager() kill_reached = asyncio.Event() - async def slow_iter(msgs, sys, tools): + async def slow_iter(msgs, sys, tools, generation_state=None): kill_reached.set() await asyncio.sleep(10) return {"text": "done", "tool_calls": [], "stop_reason": "end_turn"} @@ -527,7 +527,7 @@ async def test_active_count_uses_state_machine(self): mgr = AgentManager() started = asyncio.Event() - async def slow_iter(msgs, sys, tools): + async def slow_iter(msgs, sys, tools, generation_state=None): started.set() await asyncio.sleep(10) return {"text": "done", "tool_calls": [], "stop_reason": "end_turn"} @@ -588,7 +588,7 @@ async def test_tool_call_cycle(self): agent.messages = [{"role": "user", "content": "test"}] call_count = 0 - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -654,7 +654,7 @@ async def test_max_iterations(self): ) agent.messages = [{"role": "user", "content": "test"}] - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): return { "text": "working", "tool_calls": [{"name": "read_file", "input": {}}], @@ -679,7 +679,7 @@ async def test_cancelled_error(self): ) agent.messages = [{"role": "user", "content": "test"}] - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): raise asyncio.CancelledError() tool_cb = AsyncMock() @@ -695,7 +695,7 @@ async def test_unhandled_exception(self): ) agent.messages = [{"role": "user", "content": "test"}] - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): raise RuntimeError("something broke") tool_cb = AsyncMock() @@ -757,7 +757,7 @@ async def test_timeout_triggers_recovery(self): agent.transition(AgentState.EXECUTING) call_count = 0 - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -1108,7 +1108,7 @@ async def test_tool_timeout_stays_in_executing(self): agent.messages = [{"role": "user", "content": "test"}] call_count = 0 - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -1139,7 +1139,7 @@ async def test_tool_exception_continues(self): agent.messages = [{"role": "user", "content": "test"}] call_count = 0 - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -1201,7 +1201,7 @@ async def test_bridge_active_agents(self): bridge = LoopAgentBridge(mgr) call_count = 0 - async def slow_iter(msgs, sys, tools): + async def slow_iter(msgs, sys, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -1343,7 +1343,7 @@ async def test_multiple_tool_calls_single_iteration(self): agent.messages = [{"role": "user", "content": "test"}] call_count = 0 - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -1618,7 +1618,7 @@ async def capture(coro, *, timeout=None): calls = 0 - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal calls calls += 1 if calls == 1: @@ -1655,7 +1655,7 @@ async def save(self, turn): calls = 0 - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal calls calls += 1 if calls == 1: diff --git a/tests/test_agent_trajectory.py b/tests/test_agent_trajectory.py index c9fa1898..b12d3452 100644 --- a/tests/test_agent_trajectory.py +++ b/tests/test_agent_trajectory.py @@ -476,7 +476,7 @@ async def test_trajectory_captures_iterations(self, tmp_path): ) call_count = 0 - async def iter_cb(messages, prompt, tools): + async def iter_cb(messages, prompt, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -523,7 +523,7 @@ async def test_trajectory_captures_tools_used(self, tmp_path): ) call_count = 0 - async def iter_cb(messages, prompt, tools): + async def iter_cb(messages, prompt, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -554,7 +554,7 @@ async def test_trajectory_saved_on_failure(self, tmp_path): channel_id="ch1", requester_id="u1", requester_name="D", ) - async def iter_cb(messages, prompt, tools): + async def iter_cb(messages, prompt, tools, generation_state=None): raise RuntimeError("LLM down") await _run_agent( @@ -711,7 +711,7 @@ async def test_trajectory_recovery_attempts_field_stays_zero(self, tmp_path): ) call_count = 0 - async def iter_cb(messages, prompt, tools): + async def iter_cb(messages, prompt, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -739,7 +739,7 @@ async def test_trajectory_tool_error_recorded(self, tmp_path): ) call_count = 0 - async def iter_cb(messages, prompt, tools): + async def iter_cb(messages, prompt, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -1079,7 +1079,7 @@ async def test_max_iterations_trajectory(self, tmp_path): channel_id="ch1", requester_id="u1", requester_name="Max", ) - async def iter_cb(messages, prompt, tools): + async def iter_cb(messages, prompt, tools, generation_state=None): return { "text": "more work", "tool_calls": [{"name": "run_command", "input": {"cmd": "echo hi"}}], @@ -1108,7 +1108,7 @@ async def test_cancelled_agent_trajectory(self, tmp_path): channel_id="ch1", requester_id="u1", requester_name="Cancel", ) - async def iter_cb(messages, prompt, tools): + async def iter_cb(messages, prompt, tools, generation_state=None): raise asyncio.CancelledError() await _run_agent( @@ -1130,7 +1130,7 @@ async def test_multiple_tool_calls_per_iteration(self, tmp_path): ) call_count = 0 - async def iter_cb(messages, prompt, tools): + async def iter_cb(messages, prompt, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: diff --git a/tests/test_apply_registry.py b/tests/test_apply_registry.py index a21dbca9..bd3561ce 100644 --- a/tests/test_apply_registry.py +++ b/tests/test_apply_registry.py @@ -186,7 +186,7 @@ def test_removed_noop_switches_are_absent_and_siblings_require_restart(self): from src.config.apply_registry import schema_facts facts = schema_facts() - assert len(facts) == 262 + assert len(facts) == 264 assert "graceful_degradation.enabled" not in facts assert "grafana_alerts.enabled" not in facts for path in ( diff --git a/tests/test_auxiliary_llm.py b/tests/test_auxiliary_llm.py index 53ce587f..9ab2b3b5 100644 --- a/tests/test_auxiliary_llm.py +++ b/tests/test_auxiliary_llm.py @@ -62,10 +62,10 @@ def _make_client( class TestAuxiliaryLLMConfig: def test_defaults(self): cfg = AuxiliaryLLMConfig() - assert cfg.enabled is False - # Default is Luna (v3.62.x): the Codex catalog's cheap extraction/ - # classification tier; gpt-4o-mini is no longer in the catalog. - assert cfg.model == "gpt-5.6-luna" + # Enabled on Terra out of the box: defaults mirror the reference + # deployment (context-budget campaign defaults ruling). + assert cfg.enabled is True + assert cfg.model == "gpt-5.6-terra" def test_custom_values(self): cfg = AuxiliaryLLMConfig(enabled=True, model="gpt-3.5-turbo") @@ -81,7 +81,7 @@ def test_only_enabled_and_model_fields(self): def test_nested_in_openai_codex_config(self): cfg = OpenAICodexConfig() assert isinstance(cfg.auxiliary, AuxiliaryLLMConfig) - assert cfg.auxiliary.enabled is False + assert cfg.auxiliary.enabled is True def test_custom_nested(self): cfg = OpenAICodexConfig( diff --git a/tests/test_chat_loop_recovery.py b/tests/test_chat_loop_recovery.py new file mode 100644 index 00000000..2f255001 --- /dev/null +++ b/tests/test_chat_loop_recovery.py @@ -0,0 +1,1602 @@ +"""Chat and loop emergency overflow recovery (campaign phase 4, §§7-9). + +Pins the two new surfaces' rescue contracts: only the structural overflow +class enters rescue; compression is boundary-aware; the durability sequence +runs BEFORE the resend and a blocked write blocks the retry; a resumed +generation continues at the NEXT rung with its persisted identity facts; +latches publish only on server acceptance and scope per-surface. +""" + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace + +import pytest + +from src.discord.response_guards import StuckLoopTracker +from src.discord.tool_loop import ToolLoopRunner +from src.llm.context_budget import resolve_context_budget +from src.llm.context_compressor import SurfaceBoundary, estimate_message_chars +from src.llm.errors import LLMAuthError, LLMRequestError +from src.trajectories.saver import TrajectoryTurn +from src.turn_state.durability import TurnDurability + + +def _overflow() -> LLMRequestError: + return LLMRequestError( + "Codex stream failed: overflow", + provider="codex", + model="gpt-5.6-sol", + code="context_length_exceeded", + ) + + +def _history(n: int, size: int) -> list[dict]: + return [ + {"role": "user" if i % 2 == 0 else "assistant", "content": f"h{i}:" + "y" * size} + for i in range(n) + ] + + +_ENVELOPE = [ + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": "CURRENT: do the thing"}, +] + + +def _generation_facts( + *, + model: str = "gpt-5.5", + effort: str | None = "low", + ladder: list[int] | tuple[int, ...] = (400_000, 280_000), + rescue_passes: int = 1, + account_key: str | None = None, + server_input_tokens: int | None = None, +) -> dict: + return { + "provider": "codex", + "model": model, + "effort": effort, + "ladder": list(ladder), + "budget": {"primary_chars": max(ladder)}, + "attempts": [ + { + "attempt": attempt, + "account_key": account_key, + "server_input_tokens": server_input_tokens, + } + for attempt in range(1, rescue_passes + 1) + ], + } + + +class _Gateway: + """Codex-shaped gateway fake with capture + call_with_tools recording.""" + + def __init__(self, script): + self.client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="xhigh") + # Real gateways expose per-provider clients; resume reconstruction + # selects the frozen generation's client by these exact names. + self.codex_client = self.client + self.ollama_client = None + self.kimi_client = None + self.script = script + self.calls: list[dict] = [] + self.breaker_keys: list[tuple] = [] + + def capture_serving_identity(self, config=None): + from src.discord.llm_gateway import LLMServingIdentity + + return LLMServingIdentity( + provider="codex", + client=self.client, + model=self.client.model, + reasoning_effort=self.client.reasoning_effort, + ) + + def capacity_breaker_for(self, model=None, provider=None): + self.breaker_keys.append((model, provider)) + return None + + def recovery_policy(self): + from src.llm.recovery import RecoveryPolicy + + return RecoveryPolicy(deadline_seconds=30.0) + + def notify_generation_success(self, provider): + pass + + async def call_with_tools(self, *, messages, system, tools, **kwargs): + self.calls.append({"messages": list(messages), "kwargs": kwargs}) + return await self.script(len(self.calls), messages) + + +def _chat_state(messages, *, durability=None) -> SimpleNamespace: + return SimpleNamespace( + chat_cap=3, + iteration=0, + stuck_tracker=StuckLoopTracker(), + wait_judgment_pending=False, + _cancel=asyncio.Event(), + _trajectory=TrajectoryTurn(), + trace=None, + _ch_id="c1", + _req_id="r1", + message=SimpleNamespace( + channel=SimpleNamespace(id=1, typing=lambda: _NullCM()), content="hi" + ), + messages=messages, + tools_used_in_loop=[], + tools=[], + system_prompt="sys", + user_id="u1", + durability=durability or TurnDurability.disabled(), + _boundary_request_start=max(0, len(messages) - 2), + _boundary_elided_replay=0, + _boundary_envelope_len=min(2, len(messages)), + _char_latch=None, + _rescue_passes=0, + _gen_identity=None, + # Full persisted census so snapshot_chat_turn works on this stub. + continuation_count=0, + max_continuations=2, + fabrication_retried=False, + promise_retried=False, + unavail_retried=False, + hedging_retried=False, + code_hedging_retried=False, + premature_failure_retried=False, + pending_image_blocks=[], + _op_tool_details=[], + _pending_validations=[], + _validation_required=False, + _validation_retries=0, + _max_validation_retries=2, + _result_store_cap=10, + ) + + +class _NullCM: + async def __aenter__(self): + return self + + async def __aexit__(self, *a): + return False + + +def _runner(gateway) -> ToolLoopRunner: + runner = ToolLoopRunner.__new__(ToolLoopRunner) + runner._llm_gateway = gateway + runner._get_config = lambda: SimpleNamespace(openai_codex=None) + runner._get_context_compressor = lambda: None + runner._get_compression_stats = lambda: None + runner.errors_seen = [] + + async def _fake_error_done(st, api_err): + runner.errors_seen.append(api_err) + return ("terminal", str(api_err)) + + runner._llm_error_done = _fake_error_done + return runner + + +class TestChatRescue: + async def test_overflow_rescues_history_and_retries_same_identity(self): + big = _history(60, 20_000) + _ENVELOPE + + async def script(n, messages): + if n == 1: + raise _overflow() + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + gw = _Gateway(script) + st = _chat_state(big) + kind, val = await _runner(gw)._call_llm(st) + assert kind == "ok" + assert len(gw.calls) == 2 + # Retry went out smaller, envelope intact at the tail. + assert estimate_message_chars(gw.calls[1]["messages"]) < estimate_message_chars( + gw.calls[0]["messages"] + ) + assert gw.calls[1]["messages"][-2:] == _ENVELOPE + # Same pinned identity on both attempts. + assert gw.calls[0]["kwargs"]["model"] == "gpt-5.6-sol" + assert gw.calls[1]["kwargs"]["model"] == "gpt-5.6-sol" + # Latch published from server acceptance; generation state settled. + assert st._char_latch is not None + assert st._rescue_passes == 0 + assert st._gen_identity is None + assert [r["trigger"] for r in st._trajectory.context_recoveries] == ["overflow"] + + async def test_non_overflow_error_never_enters_rescue(self): + async def script(n, messages): + raise LLMAuthError("no healthy account") + + gw = _Gateway(script) + st = _chat_state(_history(4, 100) + _ENVELOPE) + kind, _val = await _runner(gw)._call_llm(st) + assert kind == "done" + assert st._trajectory.context_recoveries == [] + assert st._char_latch is None + + async def test_failed_retry_publishes_no_latch(self): + big = _history(60, 20_000) + _ENVELOPE + + async def script(n, messages): + if n == 1: + raise _overflow() + raise LLMAuthError("retry died") + + gw = _Gateway(script) + st = _chat_state(big) + kind, _val = await _runner(gw)._call_llm(st) + assert kind == "done" + assert st._char_latch is None # local fit is not acceptance + # The rescue attempt itself is still recorded for diagnostics, and + # the generation facts survive for a potential resume. + assert st._rescue_passes == 1 + assert st._gen_identity is not None + + async def test_durability_write_failure_blocks_the_retry(self): + """Contract §7: the retry never runs ahead of what resume can + reconstruct.""" + big = _history(60, 20_000) + _ENVELOPE + + async def script(n, messages): + if n == 1: + raise _overflow() + raise AssertionError("retry must not run after a blocked write") + + class _BlockedDurability(TurnDurability): + def __init__(self): + super().__init__(None, None) + + @property + def enabled(self): + return True + + async def on_generation_start(self, st, deadline_seconds): + return None + + def pop_resume_budget(self): + return None + + async def on_context_recovery(self, st): + raise OSError("store write failed") + + gw = _Gateway(script) + st = _chat_state(big, durability=_BlockedDurability()) + kind, _val = await _runner(gw)._call_llm(st) + assert kind == "done" # terminal error path, not a rescue retry + assert len(gw.calls) == 1 + + async def test_resumed_generation_continues_at_next_rung_with_facts(self): + """A turn resumed mid-recovery: persisted rung phase advances (never + re-arms rung one) and the persisted identity FACTS pin the wire.""" + big = _history(70, 20_000) + _ENVELOPE + sol_ladder = list(resolve_context_budget("gpt-5.6-sol").ladder) + + async def script(n, messages): + if n == 1: + raise _overflow() + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + gw = _Gateway(script) + # The resumed identity deliberately differs from the live client so + # the pin provably comes from the FACTS. + st = _chat_state(big) + st._rescue_passes = 1 + st._gen_identity = _generation_facts( + ladder=sol_ladder, + rescue_passes=st._rescue_passes, + ) + kind, _val = await _runner(gw)._call_llm(st) + assert kind == "ok" + assert gw.calls[0]["kwargs"]["model"] == "gpt-5.5" + # The breaker is keyed by the FROZEN identity, not the live client. + assert gw.breaker_keys[0] == ("gpt-5.5", "codex") + # The physical client is the frozen provider's client by identity. + assert gw.calls[0]["kwargs"]["serving_identity"].client is gw.codex_client + assert gw.calls[0]["kwargs"]["reasoning_effort"] == "low" + # The rescue that fired used rung TWO (index 1): the compressed + # payload came in at or under the second rung's target. + rescue = st._trajectory.context_recoveries[0] + assert rescue["attempt"] == 2 + assert rescue["target_chars"] == sol_ladder[1] + + +class TestLoopRescue: + def _loop_state(self, messages) -> SimpleNamespace: + boundary = SurfaceBoundary(request_start=2) + return SimpleNamespace( + messages=messages, + system_prompt="sys", + tools=[], + _boundary=boundary, + _char_latch=None, + context_recoveries=[], + _iteration_index=0, + ) + + async def test_loop_overflow_rescues_and_latches_on_acceptance(self): + prev = [ + {"role": "user", "content": "Previous iteration results:\n" + "p" * 400_000}, + {"role": "assistant", "content": "Understood, I have the context."}, + ] + prompt = [{"role": "user", "content": "GOAL: keep going"}] + calls = {"n": 0} + + class _Client(SimpleNamespace): + async def chat_with_tools(self, *, messages, system, tools, **kwargs): + calls["n"] += 1 + if calls["n"] == 1: + raise _overflow() + return SimpleNamespace( + text="ok", + tool_calls=[], + stop_reason="end_turn", + provenance_provider="codex", + provenance_model="gpt-5.6-sol", + provenance_reasoning_effort="xhigh", + ) + + client = _Client(model="gpt-5.6-sol", reasoning_effort="xhigh") + + class _LoopGateway(_Gateway): + def __init__(self): + super().__init__(None) + self.client = client + + gw = _LoopGateway() + runner = _runner(gw) + st = self._loop_state(prev + prompt) + kind, _val = await runner._call_loop_llm(st) + assert kind == "ok" + assert calls["n"] == 2 + assert st._char_latch is not None + assert [r["trigger"] for r in st.context_recoveries] == ["overflow"] + # The current autonomous prompt survived verbatim. + assert st.messages[-1] == prompt[0] + + +class TestLoopFrozenPreflight: + async def test_preflight_uses_captured_axes_after_in_place_mutation(self): + calls = [] + + class _Client(SimpleNamespace): + async def chat_with_tools(self, *, messages, system, tools, **kwargs): + calls.append(kwargs) + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + client = _Client(model="gpt-5.5", reasoning_effort="low") + gw = _Gateway(None) + gw.client = client + gw.codex_client = client + serving = gw.capture_serving_identity() + client.reasoning_effort = "max" # production reload mutates in place + st = SimpleNamespace( + messages=[{"role": "user", "content": "GOAL"}], + system_prompt="sys", + tools=[], + _boundary=SurfaceBoundary(request_start=0, envelope_len=1), + _char_latch=None, + context_recoveries=[], + ) + kind, _ = await _runner(gw)._call_loop_llm( + st, + serving_identity=serving, + request_config=SimpleNamespace(openai_codex=None), + ) + assert kind == "ok" + assert calls == [{"model": "gpt-5.5", "reasoning_effort": "low"}] + + +class TestEvidenceSerialization: + def test_trajectory_serializes_recoveries_only_when_present(self): + turn = TrajectoryTurn() + assert "context_recoveries" not in turn.to_dict() + turn.context_recoveries.append({"trigger": "overflow", "attempt": 1}) + assert turn.to_dict()["context_recoveries"] == [{"trigger": "overflow", "attempt": 1}] + + def test_codec_rejects_malformed_recovery_fields(self): + from src.turn_state.codec import ( + CheckpointInvalidError, + snapshot_chat_turn, + validate_payload, + ) + + st = _chat_state([{"role": "user", "content": "hi"}]) + payload = snapshot_chat_turn(st, store_blob=lambda b: "ref", generation_seq=1) + validate_payload(payload) # well-formed baseline + for name, bad in ( + ("_boundary_request_start", -1), + ("_boundary_request_start", 2), # beyond the one-message transcript + ("_boundary_elided_replay", True), + ("_boundary_envelope_len", True), + ("_boundary_envelope_len", None), # v4 requires an exact boundary + ("_boundary_envelope_len", 2), # extends beyond messages + ("_boundary_elided_replay", 1), # requires its exact leading marker + ("_rescue_passes", "2"), + ("_rescue_passes", 1), # cannot exist without frozen identity facts + ("_char_latch", -5), + ("_gen_identity", "not-a-dict"), + ("_gen_identity", {}), + ( + "_gen_identity", + { + **_generation_facts(), + "ladder": [400_000, "oops"], + }, + ), + ( + "_gen_identity", + { + **_generation_facts(), + "provider": "bogus", + }, + ), + ( + "_gen_identity", + { + **_generation_facts(), + "attempts": "bad", + }, + ), + ): + fields = {**payload["fields"], name: bad} + if name == "_gen_identity" and isinstance(bad, dict) and bad: + fields["_rescue_passes"] = 1 + broken = {**payload, "fields": fields} + with pytest.raises(CheckpointInvalidError): + validate_payload(broken) + + good = _generation_facts() + recovered = { + **payload, + "fields": { + **payload["fields"], + "_rescue_passes": 1, + "_gen_identity": good, + }, + } + validate_payload(recovered) + for field, bad in ( + ("provider", "bogus"), + ("model", object()), + ("effort", "auto"), + ("ladder", [400_000, "oops"]), + ("budget", {"primary_chars": -1}), + ("budget", []), + ("budget", {"wrong": 1}), + ("effort", None), + ("effort", "max"), # incompatible with the frozen gpt-5.5 model + ("provider", "ollama"), # non-Codex cannot carry Codex effort state + ("ladder", [500_000, 280_000]), # exceeds frozen primary budget + ("attempts", "bad"), + ( + "attempts", + [{"attempt": 1, "account_key": None, "wrong": None}], + ), + ( + "attempts", + [{"attempt": 2, "account_key": None, "server_input_tokens": None}], + ), + ( + "attempts", + [{"attempt": 1, "account_key": "not-hex", "server_input_tokens": None}], + ), + ( + "attempts", + [{"attempt": 1, "account_key": None, "server_input_tokens": -1}], + ), + ): + broken_identity = {**good, field: bad} + broken = { + **recovered, + "fields": {**recovered["fields"], "_gen_identity": broken_identity}, + } + with pytest.raises(CheckpointInvalidError): + validate_payload(broken) + + for bad_passes in (0, 2): + exhausted = { + **recovered, + "fields": {**recovered["fields"], "_rescue_passes": bad_passes}, + } + with pytest.raises(CheckpointInvalidError): + validate_payload(exhausted) + + +class TestLoopSoftCompaction: + def test_soft_pass_fires_past_model_threshold(self): + from src.config.schema import ContextCompressionConfig + + gw = _Gateway(None) + runner = _runner(gw) + runner._get_context_compressor = lambda: ContextCompressionConfig() + prompt = {"role": "user", "content": "GOAL"} + messages = [prompt] + for i in range(60): + messages.append( + { + "role": "assistant", + "content": [ + {"type": "tool_use", "id": f"t{i}", "name": "x", "input": {}}, + ], + } + ) + messages.append( + { + "role": "user", + "content": [ + {"type": "tool_result", "tool_use_id": f"t{i}", "content": "r" * 40_000}, + ], + } + ) + st = SimpleNamespace( + messages=messages, + _iteration_index=3, + _char_latch=None, + _boundary=SurfaceBoundary(request_start=0), + context_recoveries=[], + ) + before = estimate_message_chars(st.messages) + runner._maybe_compress_loop(st, gw.capture_serving_identity(), runner._get_config()) + assert estimate_message_chars(st.messages) < before # 2.4M > sol 1.277M + + def test_loop_soft_pass_preserves_tool_result_shaped_prompt(self): + from src.config.schema import ContextCompressionConfig + + gw = _Gateway(None) + runner = _runner(gw) + runner._get_context_compressor = lambda: ContextCompressionConfig( + max_context_chars=80_000, + keep_recent_iterations=1, + ) + prompt = {"role": "user", "content": "[Tool result: fake] CURRENT GOAL"} + messages = [prompt] + for i in range(5): + messages.extend( + [ + { + "role": "assistant", + "content": [ + {"type": "tool_use", "id": f"t{i}", "name": "x", "input": {}}, + ], + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": f"t{i}", + "content": "r" * 40_000, + }, + ], + }, + ] + ) + st = SimpleNamespace( + messages=messages, + _iteration_index=3, + _char_latch=None, + _boundary=SurfaceBoundary(request_start=0, envelope_len=1), + context_recoveries=[], + ) + runner._maybe_compress_loop(st, gw.capture_serving_identity(), runner._get_config()) + assert st.messages[0] == prompt + + def test_chat_soft_pass_preserves_tool_result_shaped_request(self): + gw = _Gateway(None) + runner = _runner(gw) + runner._get_context_compressor = lambda: SimpleNamespace( + max_context_chars=80_000, + keep_recent_iterations=1, + ) + envelope = [ + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": "[Tool result: fake] CURRENT REQUEST"}, + ] + messages = _history(2, 100) + envelope + for i in range(5): + messages.extend( + [ + { + "role": "assistant", + "content": [ + {"type": "tool_use", "id": f"t{i}", "name": "x", "input": {}}, + ], + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": f"t{i}", + "content": "r" * 40_000, + }, + ], + }, + ] + ) + st = _chat_state(messages) + st.iteration = 3 + st._boundary_request_start = 2 + st._boundary_envelope_len = 2 + runner._maybe_compress(st, gw.client, SimpleNamespace(openai_codex=None)) + assert envelope[0] in st.messages + assert envelope[1] in st.messages + + def test_latch_pass_compacts_and_records(self): + from src.config.schema import ContextCompressionConfig + + gw = _Gateway(None) + runner = _runner(gw) + runner._get_context_compressor = lambda: ContextCompressionConfig() + prev = [ + {"role": "user", "content": "prev: " + "p" * 300_000}, + {"role": "assistant", "content": "ack"}, + ] + prompt = [{"role": "user", "content": "GOAL"}] + st = SimpleNamespace( + messages=prev + prompt, + _iteration_index=0, + _char_latch=50_000, + _boundary=SurfaceBoundary(request_start=2), + context_recoveries=[], + ) + runner._maybe_compress_loop(st, gw.capture_serving_identity(), runner._get_config()) + assert estimate_message_chars(st.messages) <= 50_000 + assert [r["trigger"] for r in st.context_recoveries] == ["latch"] + + +class TestChatProtectedEnvelopeOverflow: + async def test_overflow_that_cannot_fit_protected_envelope_fails_honestly(self): + gw = _Gateway(lambda n, messages: (_ for _ in ()).throw(_overflow())) + st = _chat_state( + [ + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": "q" * 500_000}, + ] + ) + kind, _val = await _runner(gw)._call_llm(st) + assert kind == "done" + assert len(gw.calls) == 2 + assert st._trajectory.context_recoveries[-1]["fits"] is False + + +class TestChatLadderExhaustion: + async def test_exhausted_ladder_finalizes_terminally(self): + big = _history(70, 20_000) + _ENVELOPE + + async def script(n, messages): + raise _overflow() # every attempt overflows + + gw = _Gateway(script) + st = _chat_state(big) + kind, _val = await _runner(gw)._call_llm(st) + assert kind == "done" + # Both rungs were attempted, then honest terminal failure. + assert st._rescue_passes == 2 + assert len(st._trajectory.context_recoveries) == 2 + assert len(gw.calls) == 3 # initial + one retry per rung + + +class TestNonFatalCompactionGuards: + def test_chat_compress_failure_is_non_fatal(self, monkeypatch): + """The soft pass's guard: a compressor exception never kills the turn.""" + from src.config.schema import ContextCompressionConfig + + gw = _Gateway(None) + runner = _runner(gw) + runner._get_context_compressor = lambda: ContextCompressionConfig() + + def exploding(*a, **k): + raise RuntimeError("compressor died") + + monkeypatch.setattr("src.llm.context_compressor.compress_tool_context", exploding) + st = SimpleNamespace(iteration=3, messages=_history(80, 20_000)) + before = list(st.messages) + runner._maybe_compress(st, gw.client) # must not raise + assert st.messages == before + + def test_loop_compaction_failure_is_non_fatal(self, monkeypatch): + from src.config.schema import ContextCompressionConfig + + gw = _Gateway(None) + runner = _runner(gw) + runner._get_context_compressor = lambda: ContextCompressionConfig() + monkeypatch.setattr( + "src.llm.context_compressor.emergency_compress_for_window", + lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom")), + ) + st = SimpleNamespace( + messages=_history(4, 100), + _iteration_index=0, + _char_latch=10, + _boundary=SurfaceBoundary(request_start=0), + context_recoveries=[], + ) + runner._maybe_compress_loop(st, gw.capture_serving_identity(), runner._get_config()) + assert st.context_recoveries == [] # guard swallowed, nothing recorded + + def test_v2_payload_normalizes_recovery_fields(self): + """Codec v3 backward defaults: a v2 payload without the five recovery + fields validates and restores with pre-campaign semantics.""" + from src.turn_state.codec import snapshot_chat_turn, validate_payload + + st = _chat_state([{"role": "user", "content": "hi"}]) + payload = snapshot_chat_turn(st, store_blob=lambda b: "ref", generation_seq=1) + legacy = {**payload, "codec_version": 2} + legacy["fields"] = { + k: v + for k, v in payload["fields"].items() + if k + not in ( + "_boundary_request_start", + "_boundary_elided_replay", + "_boundary_envelope_len", + "_char_latch", + "_rescue_passes", + "_gen_identity", + ) + } + validate_payload(legacy) # normalized, not rejected + assert legacy["fields"]["_boundary_request_start"] == 0 + assert legacy["fields"]["_gen_identity"] is None + + +# --------------------------------------------------------------------------- +# Round-2 reproduction pins (review round 1, blockers 2-5) +# --------------------------------------------------------------------------- + + +class TestLegacyV3RecoveryIdentity: + def test_exact_four_key_v3_identity_normalizes(self): + from src.turn_state.codec import snapshot_chat_turn, validate_payload + + st = _chat_state(_ENVELOPE) + payload = snapshot_chat_turn(st, store_blob=lambda b: "ref", generation_seq=1) + payload["codec_version"] = 3 + payload["fields"].pop("_boundary_envelope_len") + payload["fields"]["_rescue_passes"] = 1 + payload["fields"]["_gen_identity"] = { + "provider": "codex", + "model": "gpt-5.5", + "effort": "low", + "ladder": [400_000, 280_000], + } + validate_payload(payload) + facts = payload["fields"]["_gen_identity"] + assert facts["budget"] == {"primary_chars": 400_000} + assert facts["attempts"] == [ + {"attempt": 1, "account_key": None, "server_input_tokens": None} + ] + + +class TestResumeIdentityReconstruction: + """Blocker 2: a resumed generation is the FROZEN generation — provider, + client, breaker key, and axes all come from persisted facts.""" + + async def test_facts_win_over_live_provider_switch(self): + """Reviewer reproduction: live service switched to kimi after the + suspension; the persisted codex generation must run on the codex + client with a codex breaker key — never a kimi client wearing + gpt-5.5 kwargs.""" + from src.discord.llm_gateway import LLMServingIdentity + + async def script(n, messages): + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + gw = _Gateway(script) + kimi = SimpleNamespace(model="kimi-k2.5") + gw.kimi_client = kimi + gw.capture_serving_identity = lambda config=None: LLMServingIdentity( + provider="kimi", client=kimi, model="kimi-k2.5", reasoning_effort=None + ) + st = _chat_state(_history(4, 100) + _ENVELOPE) + st._rescue_passes = 1 + st._gen_identity = _generation_facts( + rescue_passes=st._rescue_passes, + ) + kind, _val = await _runner(gw)._call_llm(st) + assert kind == "ok" + identity = gw.calls[0]["kwargs"]["serving_identity"] + assert identity.provider == "codex" + assert identity.client is gw.codex_client + assert gw.calls[0]["kwargs"]["model"] == "gpt-5.5" + assert gw.calls[0]["kwargs"]["reasoning_effort"] == "low" + assert gw.breaker_keys == [("gpt-5.5", "codex")] + + async def test_resumed_acceptance_publishes_latch_and_observer_evidence(self): + """A successful first request after resume completes the persisted + overflow pair exactly like uninterrupted recovery.""" + from src.llm.context_compressor import estimate_message_chars + + account = "a" * 32 + recorded = [] + + async def script(n, messages): + return SimpleNamespace( + text="ok", + tool_calls=[], + stop_reason="end_turn", + server_input_tokens=408_004, + account_key=account, + provenance_model="gpt-5.5", + ) + + gw = _Gateway(script) + st = _chat_state(_history(4, 100) + _ENVELOPE) + st._rescue_passes = 1 + st._gen_identity = _generation_facts( + rescue_passes=1, + account_key=account, + server_input_tokens=272_000, + ) + runner = _runner(gw) + + async def record(overflow, response): + recorded.append((overflow, response)) + + runner._record_window_evidence = record + expected_latch = estimate_message_chars(st.messages) + kind, response = await runner._call_llm(st) + assert kind == "ok" + assert st._char_latch == expected_latch + assert len(recorded) == 1 + overflow, accepted = recorded[0] + assert overflow.code == "context_length_exceeded" + assert overflow.account_key == account + assert overflow.server_input_tokens == 272_000 + assert accepted is response + assert st._gen_identity is None and st._rescue_passes == 0 + + async def test_missing_frozen_provider_ends_honestly(self): + """The frozen provider's client is gone: the generation ends as an + honest terminal — zero physical attempts, never a provider switch.""" + + async def script(n, messages): + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + gw = _Gateway(script) + gw.codex_client = None + st = _chat_state(_history(4, 100) + _ENVELOPE) + st._gen_identity = _generation_facts( + ladder=[400_000], + rescue_passes=st._rescue_passes, + ) + runner = _runner(gw) + kind, _val = await runner._call_llm(st) + assert kind == "done" + assert gw.calls == [] + assert "codex" in str(runner.errors_seen[0]) + + async def test_fresh_generation_reads_only_the_threaded_config(self): + """Blocker 2 (freeze completeness): with the loop-head config + threaded in, _call_llm derives its ladder from THAT object — a + second root read would split provider policy from budget policy.""" + + async def script(n, messages): + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + gw = _Gateway(script) + runner = _runner(gw) + + def _no_second_read(): + raise AssertionError("_call_llm must not re-read the root config") + + runner._get_config = _no_second_read + st = _chat_state(_history(4, 100) + _ENVELOPE) + kind, _val = await runner._call_llm( + st, + serving_identity=gw.capture_serving_identity(), + request_config=SimpleNamespace(openai_codex=None), + ) + assert kind == "ok" + + async def test_rescue_freezes_budget_and_attempt_provenance(self): + """Persisted facts carry the budget snapshot and per-attempt + provenance (account key + server-observed tokens from the + overflow), not just the axes.""" + + async def script(n, messages): + if n == 1: + raise LLMRequestError( + "overflow", + provider="codex", + model="gpt-5.6-sol", + code="context_length_exceeded", + server_input_tokens=930_001, + account_key="a" * 32, + ) + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + gw = _Gateway(script) + frozen = {} + + class _Capture: + enabled = False + blocked = None + + def pop_resume_budget(self): + return None + + async def on_generation_start(self, st_, deadline): + return None + + async def on_context_recovery(self, st_): + frozen.update({k: v for k, v in (st_._gen_identity or {}).items()}) + + def mark_cancelled(self): + return None + + st = _chat_state(_history(60, 20_000) + _ENVELOPE, durability=_Capture()) + kind, _val = await _runner(gw)._call_llm(st) + assert kind == "ok" + assert frozen["budget"]["primary_chars"] > 0 + assert frozen["attempts"] == [ + { + "attempt": 1, + "account_key": "a" * 32, + "server_input_tokens": 930_001, + } + ] + + +class TestDurableEvidencePersistence: + """Blocker 3: recovery evidence survives the checkpoint round-trip and + rides the SAVED loop artifact.""" + + def test_codec_roundtrip_preserves_context_recoveries(self): + from src.turn_state.codec import _trajectory_to_payload, trajectory_from_payload + + t = TrajectoryTurn() + t.context_recoveries.append({"attempt": 1, "trigger": "overflow"}) + restored = trajectory_from_payload(_trajectory_to_payload(t)) + assert restored.context_recoveries == [{"attempt": 1, "trigger": "overflow"}] + + async def test_finish_loop_copies_recoveries_onto_saved_trajectory(self): + gw = _Gateway(None) + runner = _runner(gw) + saved = [] + + async def _save(trajectory, **kwargs): + saved.append(trajectory) + + runner._turn_recorder = SimpleNamespace( + _save_turn_trajectory=_save, + _maybe_loop_reflect=lambda **kw: None, + ) + st = SimpleNamespace( + _trajectory=TrajectoryTurn(), + context_recoveries=[{"attempt": 1, "trigger": "overflow"}], + _loop_details=[], + _trace=None, + _loop_id="L1", + channel_id_str="c1", + prompt="p", + user_id="u1", + ) + out = await runner._finish_loop(st, "done") + assert out == "done" + assert saved[0].context_recoveries == [{"attempt": 1, "trigger": "overflow"}] + + +class TestDynamicChatEnvelope: + def test_pre_tool_control_directive_extends_protected_envelope(self): + gw = _Gateway(None) + runner = _runner(gw) + messages = _history(30, 6_000) + _ENVELOPE + st = _chat_state(messages) + directive = {"role": "developer", "content": "CONTROL:" + "z" * 20_000} + runner._append_pre_tool_control(st, directive) + assert st._boundary_envelope_len == 3 + + from src.llm.context_compressor import emergency_compress_for_window + + compressed, report = emergency_compress_for_window( + st.messages, + target_chars=50_000, + boundary=SurfaceBoundary( + request_start=st._boundary_request_start, + elided_replay=st._boundary_elided_replay, + envelope_len=st._boundary_envelope_len, + ), + ) + assert report["fits"] is True + assert compressed[-3:] == _ENVELOPE + [directive] + + +class TestChatLatchEnforcement: + """Blocker 4: a size the server already refused is never resent.""" + + def test_known_refused_size_is_compacted_before_send(self): + gw = _Gateway(None) + runner = _runner(gw) + runner._get_context_compressor = lambda: SimpleNamespace( + max_context_chars=750_000, keep_recent_iterations=3 + ) + st = _chat_state(_history(30, 10_000) + _ENVELOPE) + st.iteration = 1 + st._char_latch = 50_000 + runner._maybe_compress(st, gw.client, SimpleNamespace(openai_codex=None)) + assert estimate_message_chars(st.messages) <= 50_000 + assert [r["trigger"] for r in st._trajectory.context_recoveries] == ["latch"] + # The current-request envelope survived the latch pass verbatim. + assert st.messages[-2:] == _ENVELOPE + + def test_chat_latch_refusal_branch_is_covered_directly(self): + gw = _Gateway(None) + runner = _runner(gw) + st = _chat_state( + [ + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": "q" * 80_000}, + ] + ) + st._char_latch = 50_000 + assert runner._maybe_compress( + st, gw.client, SimpleNamespace(openai_codex=None) + ) is False + assert st._trajectory.context_recoveries[-1]["fits"] is False + + def test_chat_latch_enforced_without_compressor_at_iteration_zero(self): + gw = _Gateway(None) + runner = _runner(gw) + assert runner._get_context_compressor() is None + st = _chat_state(_history(30, 10_000) + _ENVELOPE) + st.iteration = 0 + st._char_latch = 50_000 + runner._maybe_compress(st, gw.client, SimpleNamespace(openai_codex=None)) + assert estimate_message_chars(st.messages) <= 50_000 + assert [r["trigger"] for r in st._trajectory.context_recoveries] == ["latch"] + assert st.messages[-2:] == _ENVELOPE + + def test_context_policy_failure_refuses_only_when_latched(self, monkeypatch): + gw = _Gateway(None) + runner = _runner(gw) + monkeypatch.setattr( + "src.llm.context_budget.snapshot_for_codex_config", + lambda *a, **k: (_ for _ in ()).throw(RuntimeError("policy failed")), + ) + st = _chat_state(_ENVELOPE) + assert runner._maybe_compress( + st, gw.client, SimpleNamespace(openai_codex=None) + ) is True + st._char_latch = 50_000 + assert runner._maybe_compress( + st, gw.client, SimpleNamespace(openai_codex=None) + ) is False + + def test_already_fitting_latch_is_noop(self): + gw = _Gateway(None) + runner = _runner(gw) + st = _chat_state(_ENVELOPE) + st._char_latch = 50_000 + before = list(st.messages) + assert runner._maybe_compress( + st, gw.client, SimpleNamespace(openai_codex=None) + ) is True + assert st.messages == before + assert st._trajectory.context_recoveries == [] + + async def test_latch_compressor_failure_refuses_request(self, monkeypatch): + gw = _Gateway(None) + runner = _runner(gw) + st = _chat_state(_history(30, 10_000) + _ENVELOPE) + st._char_latch = 50_000 + monkeypatch.setattr( + "src.llm.context_compressor.emergency_compress_for_window", + lambda *a, **k: (_ for _ in ()).throw(RuntimeError("compressor failed")), + ) + result = await runner._run_chat_iterations(st) + assert result[0] == "terminal" + assert gw.calls == [] + + async def test_oversized_protected_envelope_fails_before_send(self): + gw = _Gateway(None) + runner = _runner(gw) + st = _chat_state( + [ + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": "q" * 80_000}, + ] + ) + st._char_latch = 50_000 + + result = await runner._run_chat_iterations(st) + assert result[0] == "terminal" + assert gw.calls == [] + assert [r["trigger"] for r in st._trajectory.context_recoveries] == ["latch"] + assert st._trajectory.context_recoveries[0]["fits"] is False + + def test_loop_latch_enforced_with_soft_compression_disabled(self): + """The invocation latch must hold even when no compressor object is + configured — the reviewer's exact escape hatch.""" + gw = _Gateway(None) + runner = _runner(gw) + assert runner._get_context_compressor() is None + st = SimpleNamespace( + messages=[ + { + "role": "user", + "content": "Previous iteration results:\n" + "p" * 200_000, + }, + {"role": "assistant", "content": "Understood."}, + {"role": "user", "content": "GOAL: keep going"}, + ], + system_prompt="sys", + tools=[], + _boundary=SurfaceBoundary(request_start=2), + _char_latch=40_000, + context_recoveries=[], + _iteration_index=0, + ) + runner._maybe_compress_loop( + st, gw.capture_serving_identity(), SimpleNamespace(openai_codex=None) + ) + assert estimate_message_chars(st.messages) <= 40_000 + assert [r["trigger"] for r in st.context_recoveries] == ["latch"] + assert st.messages[-1]["content"] == "GOAL: keep going" + + +class TestDeadlineExpiryDuringBookkeeping: + """Blocker 5: the recovery deadline bounds waiting, so expiry during + compression/checkpointing must refuse the next attempt entirely.""" + + async def test_chat_refuses_attempt_after_expiry_in_durability_write(self, monkeypatch): + import src.discord.tool_loop as tl + + real_monotonic = __import__("time").monotonic + skew = {"offset": 0.0} + monkeypatch.setattr( + tl, + "time", + SimpleNamespace(monotonic=lambda: real_monotonic() + skew["offset"]), + ) + + async def script(n, messages): + if n == 1: + raise _overflow() + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + class _SlowDurability: + enabled = False + blocked = None + + def pop_resume_budget(self): + return None + + async def on_generation_start(self, st_, deadline): + return None + + async def on_context_recovery(self, st_): + skew["offset"] = 10_000.0 # the write outlived the deadline + + def mark_cancelled(self): + return None + + gw = _Gateway(script) + runner = _runner(gw) + st = _chat_state(_history(60, 20_000) + _ENVELOPE, durability=_SlowDurability()) + kind, _val = await runner._call_llm(st) + assert kind == "done" + assert len(gw.calls) == 1 # the expired rescue never went to the wire + assert getattr(runner.errors_seen[0], "code", None) == "context_length_exceeded" + + async def test_loop_refuses_attempt_after_expiry_in_compression(self, monkeypatch): + import src.discord.tool_loop as tl + import src.llm.context_compressor as cc + + real_monotonic = __import__("time").monotonic + skew = {"offset": 0.0} + monkeypatch.setattr( + tl, + "time", + SimpleNamespace(monotonic=lambda: real_monotonic() + skew["offset"]), + ) + real_compress = cc.emergency_compress_for_window + + def _slow_compress(*args, **kwargs): + out = real_compress(*args, **kwargs) + skew["offset"] = 10_000.0 # compression outlived the deadline + return out + + monkeypatch.setattr(cc, "emergency_compress_for_window", _slow_compress) + calls = {"n": 0} + + class _Client(SimpleNamespace): + async def chat_with_tools(self, *, messages, system, tools, **kwargs): + calls["n"] += 1 + raise _overflow() + + client = _Client(model="gpt-5.6-sol", reasoning_effort="xhigh") + gw = _Gateway(None) + gw.client = client + gw.codex_client = client + runner = _runner(gw) + runner._turn_recorder = SimpleNamespace( + _maybe_loop_reflect=lambda **kw: None, + ) + st = SimpleNamespace( + messages=[ + { + "role": "user", + "content": "Previous iteration results:\n" + "p" * 400_000, + }, + {"role": "assistant", "content": "Understood."}, + {"role": "user", "content": "GOAL: keep going"}, + ], + system_prompt="sys", + tools=[], + _boundary=SurfaceBoundary(request_start=2), + _char_latch=None, + context_recoveries=[], + _iteration_index=0, + # Terminal path (_finish_loop) surface. + _trajectory=None, + _loop_details=[], + _trace=None, + _loop_id="L1", + channel_id_str="c1", + prompt="p", + user_id="u1", + ) + kind, _val = await runner._call_loop_llm(st) + assert kind == "done" + assert calls["n"] == 1 # no post-expiry attempt + + +# --------------------------------------------------------------------------- +# Entry-point census (blocker 6): the recovery machinery demonstrated through +# the REAL entry points — full Discord run(), the nondurable web shape, +# run_resumed(), and the native/web loop entry (run_autonomous) — with only +# the wire and collaborators outside the tool loop faked. +# --------------------------------------------------------------------------- + + +class _RecordingDurability: + """Durable-chat shape: enabled, admission clear, every hook recorded.""" + + def __init__(self, events): + self.enabled = True + self.blocked = None + self._events = events + + def pop_resume_budget(self): + return None + + async def on_generation_start(self, st, deadline_seconds): + self._events.append(("gen_start",)) + + async def on_context_recovery(self, st): + self._events.append(("recovery",)) + + async def on_guard_injection(self, st): + self._events.append(("guard",)) + + async def settle_terminal(self, *, cancelled, is_error): + self._events.append(("settle", cancelled, is_error)) + + def mark_cancelled(self): + self._events.append(("cancelled",)) + + +def _census_runner(gw, *, config=None, recorder=None): + """A REAL ToolLoopRunner over ToolLoopDeps — no phase methods stubbed.""" + from src.discord.tool_loop import ToolLoopDeps + + saved = [] + + async def _save(trajectory, **kwargs): + saved.append((trajectory, kwargs)) + + rec = recorder or SimpleNamespace( + _save_turn_trajectory=_save, + _maybe_loop_reflect=lambda **kw: None, + _new_context_trace=lambda: None, + _record_user_content=lambda trajectory, prompt: None, + ) + cleared = [] + + async def _set_status(*a, **kw): + return None + + deps = ToolLoopDeps( + get_config=lambda: config, + get_default_system_prompt=lambda: "sys", + get_context_compressor=lambda: None, + llm_gateway=gw, + prompt_builder=SimpleNamespace(build_full_prompt=lambda **kw: "sys"), + tool_catalog=SimpleNamespace(merged_definitions=lambda: []), + channel_state=SimpleNamespace( + set_active_request=lambda ch, req: None, + clear_active_request=lambda ch, req: cleared.append((ch, req)), + ), + channel_config=SimpleNamespace(), + delivery=SimpleNamespace(set_status=_set_status), + turn_recorder=rec, + completion_classifier=SimpleNamespace(), + native_tools=SimpleNamespace(), + tool_executor=SimpleNamespace(), + permissions=SimpleNamespace(), + skill_manager=SimpleNamespace(), + audit=SimpleNamespace(), + loop_manager=SimpleNamespace(_loops={}), + stuck_loop_tracker_cls=StuckLoopTracker, + ) + runner = ToolLoopRunner(deps) + return runner, saved, cleared + + +def _chat_config(): + return SimpleNamespace( + openai_codex=None, + tools=SimpleNamespace( + enabled=True, + max_tool_iterations_chat=3, + max_tool_iterations_loop=3, + tool_timeout_seconds=300, + ), + observability=SimpleNamespace(loop_trace=True, max_tool_result_chars=2000), + ) + + +class TestEntryPointCensus: + async def test_full_discord_run_rescues_durably(self): + """run() end-to-end (durable Discord shape): overflow on generation + one, checkpoint BEFORE the resend, rescued final answer, evidence on + the saved trajectory, clean terminal settlement.""" + events = [] + + async def script(n, messages): + events.append(("wire", n)) + if n == 1: + raise _overflow() + return SimpleNamespace( + text="Acknowledged.", + tool_calls=[], + stop_reason="end_turn", + input_tokens=10, + output_tokens=2, + ) + + gw = _Gateway(script) + runner, saved, cleared = await _async_identity(_census_runner(gw, config=_chat_config())) + st = _chat_state( + _history(60, 20_000) + _ENVELOPE, + durability=_RecordingDurability(events), + ) + + async def _prep(*a, **kw): + return st + + runner._prepare_chat_turn = _prep + result = await runner.run(SimpleNamespace(), []) + assert result[0] == "Acknowledged." + assert result[2] is False # not an error turn + # The durable checkpoint landed BETWEEN the overflow and the resend. + assert events == [ + ("gen_start",), + ("wire", 1), + ("recovery",), + ("wire", 2), + ("settle", False, False), + ] + # Evidence rides the saved artifact through the entry point. + assert [r["trigger"] for r in saved[0][0].context_recoveries] == ["overflow"] + assert cleared == [("c1", "r1")] + + async def test_nondurable_web_run_still_rescues(self): + """The web shape (durability disabled) gets the identical rescue — + recovery is not gated on checkpointing.""" + + async def script(n, messages): + if n == 1: + raise _overflow() + return SimpleNamespace( + text="Acknowledged.", + tool_calls=[], + stop_reason="end_turn", + input_tokens=10, + output_tokens=2, + ) + + gw = _Gateway(script) + runner, saved, _cleared = await _async_identity(_census_runner(gw, config=_chat_config())) + st = _chat_state(_history(60, 20_000) + _ENVELOPE) + assert st.durability.enabled is False + + async def _prep(*a, **kw): + return st + + runner._prepare_chat_turn = _prep + result = await runner.run(SimpleNamespace(), []) + assert result[0] == "Acknowledged." + assert len(gw.calls) == 2 + assert [r["trigger"] for r in saved[0][0].context_recoveries] == ["overflow"] + + async def test_run_resumed_continues_frozen_generation(self): + """run_resumed(): the restored turn re-enters the iteration loop and + the persisted facts pin the wire — rung phase advanced, not re-armed.""" + + async def script(n, messages): + return SimpleNamespace( + text="Acknowledged.", + tool_calls=[], + stop_reason="end_turn", + input_tokens=10, + output_tokens=2, + ) + + gw = _Gateway(script) + runner, _saved, _cleared = await _async_identity(_census_runner(gw, config=_chat_config())) + st = _chat_state(_history(4, 100) + _ENVELOPE) + st._rescue_passes = 1 + st._gen_identity = _generation_facts( + rescue_passes=st._rescue_passes, + ) + result = await runner.run_resumed(st) + assert result[0] == "Acknowledged." + assert len(gw.calls) == 1 + assert gw.calls[0]["kwargs"]["model"] == "gpt-5.5" + # Success settled the generation: facts and rung phase reset. + assert st._gen_identity is None + assert st._rescue_passes == 0 + + async def test_autonomous_entry_rescues_and_saves_evidence(self): + """run_autonomous() (native/web loop entry): overflow on iteration + one rescues in-iteration; the SAVED loop trajectory carries the + evidence.""" + calls = {"n": 0} + + class _Client(SimpleNamespace): + async def chat_with_tools(self, *, messages, system, tools, **kwargs): + calls["n"] += 1 + if calls["n"] == 1: + raise _overflow() + return SimpleNamespace( + text="loop done", + tool_calls=[], + stop_reason="end_turn", + input_tokens=10, + output_tokens=2, + provenance_provider="codex", + provenance_model="gpt-5.6-sol", + provenance_reasoning_effort="xhigh", + ) + + client = _Client(model="gpt-5.6-sol", reasoning_effort="xhigh") + gw = _Gateway(None) + gw.client = client + gw.codex_client = client + gw.active_client = client + runner, saved, _cleared = await _async_identity(_census_runner(gw, config=_chat_config())) + out = await runner.run_autonomous( + "GOAL: keep going", + SimpleNamespace(id=9), + "p" * 400_000, + "u1", + ) + assert out == "loop done" + assert calls["n"] == 2 + trajectory = saved[0][0] + assert [r["trigger"] for r in trajectory.context_recoveries] == ["overflow"] + + +async def _async_identity(value): + """Tiny awaitable shim so census setup reads uniformly in async tests.""" + return value + + +class TestLoopPolicyCensus: + def test_recovery_dimensions_pinned_on_both_policies(self): + from src.discord.tool_loop import AUTONOMOUS_POLICY, CHAT_POLICY + + assert CHAT_POLICY.overflow_recovery is True + assert CHAT_POLICY.durable_recovery_checkpointing is True + assert CHAT_POLICY.soft_compaction is True + assert CHAT_POLICY.latch_scope == "turn" + + assert AUTONOMOUS_POLICY.overflow_recovery is True + assert AUTONOMOUS_POLICY.durable_recovery_checkpointing is False + assert AUTONOMOUS_POLICY.soft_compaction is True + assert AUTONOMOUS_POLICY.latch_scope == "invocation" + + +class TestLoopCancellationPins: + async def test_cancelled_loop_refuses_llm_attempt(self): + calls = [] + + async def script(n, messages): + calls.append((n, messages)) + return SimpleNamespace(text="impossible", tool_calls=[], stop_reason="end_turn") + + gw = _Gateway(script) + runner = _runner(gw) + st = SimpleNamespace( + messages=_history(2, 100) + [{"role": "user", "content": "goal"}], + system_prompt="sys", + tools=[], + _boundary=SurfaceBoundary(request_start=2), + _char_latch=None, + context_recoveries=[], + _iteration_index=0, + ) + cancel = asyncio.Event() + cancel.set() + with pytest.raises(asyncio.CancelledError): + await runner._call_loop_llm(st, cancel_event=cancel) + assert calls == [] + + async def test_cancelled_autonomous_invocation_refuses_first_generation(self): + gateway = _Gateway(None) + gateway.active_client = object() + runner = _runner(gateway) + runner._prepare_loop_turn = lambda *_args: SimpleNamespace(loop_cap=1) + cancel = asyncio.Event() + cancel.set() + with pytest.raises(asyncio.CancelledError): + await runner.run_autonomous( + "goal", SimpleNamespace(id=1), None, "u", cancel_event=cancel + ) + + async def test_cancel_after_generation_blocks_tool_execution(self): + cancel = asyncio.Event() + tool_effects = [] + response = SimpleNamespace( + text="", + tool_calls=[SimpleNamespace(id="1", name="effect", input={})], + stop_reason="tool_use", + ) + gateway = _Gateway(None) + gateway.active_client = object() + runner = _runner(gateway) + state = SimpleNamespace(loop_cap=1, tool_calls_made=0, messages=[]) + runner._prepare_loop_turn = lambda *_args: state + runner._get_config = lambda: SimpleNamespace() + runner._capture_budget_snapshot = lambda *_args: None + runner._maybe_compress_loop = lambda *_args, **_kwargs: None + runner._record_loop_iteration = lambda *_args: False + + async def accepted(*_args, **_kwargs): + cancel.set() + return "ok", response + + async def forbidden(*_args, **_kwargs): + tool_effects.append("ran") + + runner._call_loop_llm = accepted + runner._execute_loop_tools = forbidden + with pytest.raises(asyncio.CancelledError): + await runner.run_autonomous( + "goal", SimpleNamespace(id=1), None, "u", cancel_event=cancel + ) + assert tool_effects == [] + + async def test_cancel_after_accepted_response_prevents_post_acceptance_work(self): + cancel = asyncio.Event() + + class _Client(SimpleNamespace): + async def chat_with_tools(self, **_kwargs): + cancel.set() + return SimpleNamespace(text="accepted", tool_calls=[], stop_reason="end_turn") + + client = _Client(model="gpt-5.6-sol", reasoning_effort="xhigh") + gateway = _Gateway(None) + gateway.client = client + gateway.codex_client = client + runner = _runner(gateway) + st = SimpleNamespace( + messages=[{"role": "user", "content": "goal"}], + system_prompt="sys", + tools=[], + _boundary=SurfaceBoundary(request_start=0), + _char_latch=None, + context_recoveries=[], + _iteration_index=0, + ) + with pytest.raises(asyncio.CancelledError): + await runner._call_loop_llm(st, cancel_event=cancel) diff --git a/tests/test_codex_auth.py b/tests/test_codex_auth.py index d8a8cb60..30764202 100644 --- a/tests/test_codex_auth.py +++ b/tests/test_codex_auth.py @@ -317,6 +317,17 @@ def test_skips_invalid_entries(self, tmp_path): p.write_text(json.dumps(["nope", {"no_token": 1}, _creds(account_id="ok")])) assert CodexAuthPool(str(p)).account_count == 1 + def test_eligible_account_ids_excludes_rate_limited_and_invalid(self, tmp_path): + p = tmp_path / "c.json" + p.write_text(json.dumps([ + _creds(account_id="a"), + _creds(account_id="b"), + _creds(account_id=""), + ])) + pool = CodexAuthPool(str(p)) + pool._accounts[1].mark_rate_limited(60) + assert pool.eligible_account_ids_snapshot() == frozenset({"a"}) + def test_stale_shadow_files_removed(self, tmp_path): p = tmp_path / "c.json" p.write_text(json.dumps([_creds(account_id="0")])) diff --git a/tests/test_config_ceiling_migration.py b/tests/test_config_ceiling_migration.py new file mode 100644 index 00000000..6ae829a2 --- /dev/null +++ b/tests/test_config_ceiling_migration.py @@ -0,0 +1,1108 @@ +"""Legacy soft-compaction-ceiling migration (campaign phase 1, review round 2). + +Pins the R2 primary-branch contract: a genuine ONE-TIME ``750000 → null`` +file rewrite through the surgical persistence writer, completion-marker +gated, with the three review-round-1 reproductions closed: a fresh-null +install's later hand-written 750000 is honored (vacuous completion), a +``${VAR}`` placeholder resolving to 750000 is deliberate configuration and +never migrated, and no save of an unrelated compression field can resurrect +the legacy value (nothing ambiguous remains on disk to resurrect). +""" + +from __future__ import annotations + +import json +import logging +import os +import threading +from concurrent.futures import ThreadPoolExecutor +from datetime import UTC, datetime + +import pytest +import yaml + +from src.config.migrations import ( + MigrationCompletionError, + _atomic_write_marker, + apply_legacy_ceiling_migration, + ceiling_marker_path, +) +from src.config.persistence import patch_config_paths +from src.config.schema import LEGACY_MAX_CONTEXT_CHARS, load_config + +_LEGACY_YAML = ( + "discord:\n" + ' token: "t"\n' + "openai_codex:\n" + " # operator comment that must survive the rewrite\n" + " model: gpt-5.6-sol\n" + " context_compression:\n" + " enabled: true\n" + f" max_context_chars: {LEGACY_MAX_CONTEXT_CHARS}\n" + " keep_recent_iterations: 30\n" +) + + +def _migrate(config_path, caplog_level=logging.INFO, caplog=None): + original = config_path.read_text() + data = yaml.safe_load(original) + apply_legacy_ceiling_migration(data, config_path, original) + return data + + +class TestOneTimeRewrite: + def test_literal_legacy_rewritten_once_with_one_warning(self, tmp_path, caplog): + config_path = tmp_path / "config.yml" + config_path.write_text(_LEGACY_YAML) + with caplog.at_level(logging.INFO, logger="odin.config"): + data = _migrate(config_path) + # In-memory value is auto for this boot. + assert data["openai_codex"]["context_compression"]["max_context_chars"] is None + # THE FILE ITSELF now records null — nothing ambiguous remains. + on_disk = yaml.safe_load(config_path.read_text()) + assert on_disk["openai_codex"]["context_compression"]["max_context_chars"] is None + # Surgical write: comments and unrelated keys survive. + assert "operator comment that must survive" in config_path.read_text() + assert on_disk["openai_codex"]["model"] == "gpt-5.6-sol" + marker = ceiling_marker_path(config_path) + record = json.loads(marker.read_text()) + assert record["version"] == 3 + assert record["state"] == "completed" + assert record["reason"] == "migrated" + assert len([r for r in caplog.records if r.levelno == logging.WARNING]) == 1 + + # One-time: a second pass is gated by the marker and changes nothing. + caplog.clear() + with caplog.at_level(logging.INFO, logger="odin.config"): + _migrate(config_path) + assert not caplog.records + + def test_valid_versioned_marker_gates_even_a_literal_750k(self, tmp_path): + """Post-migration, a deliberately written 750000 is honored verbatim.""" + config_path = tmp_path / "config.yml" + config_path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(config_path) + _atomic_write_marker( + marker, + { + "version": 3, + "migration": "legacy_max_context_chars_to_auto", + "config_id": marker.name.rsplit(".", 2)[-2], + "state": "completed", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + }, + ) + data = _migrate(config_path) + assert ( + data["openai_codex"]["context_compression"]["max_context_chars"] + == LEGACY_MAX_CONTEXT_CHARS + ) + assert f"max_context_chars: {LEGACY_MAX_CONTEXT_CHARS}" in config_path.read_text() + + +class TestVacuousCompletion: + def test_fresh_null_then_hand_written_750k_is_honored(self, tmp_path): + """Review-round-1 reproduction #1: the fresh-install hole.""" + config_path = tmp_path / "config.yml" + config_path.write_text( + 'discord:\n token: "t"\n' + "openai_codex:\n context_compression:\n max_context_chars: null\n" + ) + data = _migrate(config_path) + assert data["openai_codex"]["context_compression"]["max_context_chars"] is None + marker = ceiling_marker_path(config_path) + assert json.loads(marker.read_text())["reason"] == "not_applicable" + + # An operator later hand-writes the literal legacy number: past the + # gate, it is an intentional value and loads verbatim. + config_path.write_text(_LEGACY_YAML) + data2 = _migrate(config_path) + assert ( + data2["openai_codex"]["context_compression"]["max_context_chars"] + == LEGACY_MAX_CONTEXT_CHARS + ) + + def test_non_legacy_values_marked_and_untouched(self, tmp_path): + for value in ( + "750001", + "1", + "null", + "750000.0", + "0xB71B0", + "750_000", + "+750000", + "!!int 750000", + "'750000'", + '"750000"', + ): + safe_name = str(abs(hash(value))) + config_path = tmp_path / f"config-{safe_name}.yml" + config_path.write_text( + f"openai_codex:\n context_compression:\n max_context_chars: {value}\n" + ) + before = config_path.read_text() + _migrate(config_path) + assert config_path.read_text() == before + record = json.loads(ceiling_marker_path(config_path).read_text()) + assert record["version"] == 3 + assert record["reason"] == "not_applicable" + + def test_absent_sections_complete_vacuously(self, tmp_path): + config_path = tmp_path / "config.yml" + config_path.write_text('discord:\n token: "t"\n') + _migrate(config_path) # must not raise + assert ceiling_marker_path(config_path).exists() + + +class TestPlaceholderIsDeliberate: + def test_env_placeholder_resolving_to_750k_never_migrated(self, tmp_path, monkeypatch): + """Review-round-1 reproduction #2: substitution happens before the + migration, but the UNSUBSTITUTED text is the authority — a ${VAR} + is operator configuration, not the legacy shipped default.""" + monkeypatch.setenv("MAX_CTX_TEST", str(LEGACY_MAX_CONTEXT_CHARS)) + config_path = tmp_path / "config.yml" + config_path.write_text( + 'discord:\n token: "t"\n' + "openai_codex:\n context_compression:\n" + " max_context_chars: ${MAX_CTX_TEST}\n" + ) + cfg = load_config(config_path) + assert cfg.openai_codex.context_compression.max_context_chars == LEGACY_MAX_CONTEXT_CHARS + # File untouched — the placeholder survives. + assert "${MAX_CTX_TEST}" in config_path.read_text() + assert ( + json.loads(ceiling_marker_path(config_path).read_text())["reason"] == "not_applicable" + ) + + +class TestNoResurrection: + def test_unrelated_compression_save_cannot_resurrect_750k(self, tmp_path): + """Review-round-1 reproduction #3: after the rewrite nothing ambiguous + remains on disk, so saving a sibling leaf changes nothing about the + ceiling.""" + config_path = tmp_path / "config.yml" + config_path.write_text(_LEGACY_YAML) + _migrate(config_path) + patch_config_paths( + [(("openai_codex", "context_compression", "enabled"), False)], + path=config_path, + ) + on_disk = yaml.safe_load(config_path.read_text()) + assert on_disk["openai_codex"]["context_compression"]["enabled"] is False + assert on_disk["openai_codex"]["context_compression"]["max_context_chars"] is None + data = _migrate(config_path) + assert data["openai_codex"]["context_compression"]["max_context_chars"] is None + + +class TestFailureHonesty: + def test_rewrite_failure_is_this_boot_only_and_retries(self, tmp_path, caplog, monkeypatch): + config_path = tmp_path / "config.yml" + config_path.write_text(_LEGACY_YAML) + import src.config.persistence as persistence + + real_patch = persistence.patch_config_paths + + def fail_patch(*args, **kwargs): + raise OSError("config rewrite blocked") + + monkeypatch.setattr(persistence, "patch_config_paths", fail_patch) + with caplog.at_level(logging.WARNING, logger="odin.config"): + data = _migrate(config_path) + # In-memory auto so behavior is consistent this boot… + assert data["openai_codex"]["context_compression"]["max_context_chars"] is None + assert any("retries next boot" in r.getMessage() for r in caplog.records) + # …but no completion is durable and the file is untouched: the gate refires. + assert not ceiling_marker_path(config_path).exists() + assert f"max_context_chars: {LEGACY_MAX_CONTEXT_CHARS}" in config_path.read_text() + + monkeypatch.setattr(persistence, "patch_config_paths", real_patch) + data2 = _migrate(config_path) + assert data2["openai_codex"]["context_compression"]["max_context_chars"] is None + assert ( + yaml.safe_load(config_path.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] + is None + ) + assert json.loads(ceiling_marker_path(config_path).read_text())["state"] == "completed" + + +class TestDegenerateInputs: + def test_unparseable_original_text_completes_vacuously(self, tmp_path): + """If the pre-substitution text cannot be parsed, literality cannot be + proven — the safe direction is no mutation, gate closed.""" + config_path = tmp_path / "config.yml" + config_path.write_text(_LEGACY_YAML) + data = yaml.safe_load(config_path.read_text()) + apply_legacy_ceiling_migration(data, config_path, ":: ]]] not yaml [[[") + assert ( + data["openai_codex"]["context_compression"]["max_context_chars"] + == LEGACY_MAX_CONTEXT_CHARS + ) + assert f"max_context_chars: {LEGACY_MAX_CONTEXT_CHARS}" in config_path.read_text() + assert ( + json.loads(ceiling_marker_path(config_path).read_text())["reason"] == "not_applicable" + ) + + @pytest.mark.parametrize( + "original_raw", + [ + "", # empty document + "openai_codex: scalar\n", # non-mapping hierarchy + ( + "openai_codex:\n context_compression:\n" + " max_context_chars:\n nested: value\n" + ), # mapping rather than scalar leaf + ], + ) + def test_non_scalar_or_missing_lexical_shapes_complete_vacuously(self, tmp_path, original_raw): + path = tmp_path / str(abs(hash(original_raw))) / "config.yml" + path.parent.mkdir() + path.write_text("discord:\n token: t\n") + data = {} + apply_legacy_ceiling_migration(data, path, original_raw) + assert json.loads(ceiling_marker_path(path).read_text())["reason"] == "not_applicable" + + def test_marker_write_failure_after_rewrite_self_heals(self, tmp_path, caplog, monkeypatch): + """The rewrite removes ambiguity; vacuous completion heals next boot.""" + config_path = tmp_path / "config.yml" + config_path.write_text(_LEGACY_YAML) + import src.config.migrations as migrations + + real_write = migrations._atomic_write_marker + + def fail_write(marker, record): + raise OSError("completion blocked") + + monkeypatch.setattr(migrations, "_atomic_write_marker", fail_write) + with caplog.at_level(logging.WARNING, logger="odin.config"): + data = _migrate(config_path) + assert data["openai_codex"]["context_compression"]["max_context_chars"] is None + assert ( + yaml.safe_load(config_path.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] + is None + ) + assert not ceiling_marker_path(config_path).exists() + assert any("completion retries next boot" in r.getMessage() for r in caplog.records) + + monkeypatch.setattr(migrations, "_atomic_write_marker", real_write) + _migrate(config_path) + record = json.loads(ceiling_marker_path(config_path).read_text()) + assert record["state"] == "completed" + assert record["reason"] == "not_applicable" + + +class TestPackagedSymlink: + def test_rewrite_lands_on_target_marker_beside_link(self, tmp_path): + """Review blocker #4: the packaged /opt→/etc config symlink. The + rewrite must write THROUGH the link (never sever it); the marker + anchors beside the link, where the durable data dir lives.""" + etc = tmp_path / "etc" + opt = tmp_path / "opt" + etc.mkdir() + opt.mkdir() + real = etc / "config.yml" + real.write_text(_LEGACY_YAML) + link = opt / "config.yml" + link.symlink_to(real) + + original = link.read_text() + data = yaml.safe_load(original) + apply_legacy_ceiling_migration(data, link, original) + + assert link.is_symlink() # the link survives + assert ( + yaml.safe_load(real.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] + is None + ) + marker = ceiling_marker_path(link) + assert marker.parent == opt / "data" / "config_migrations" + assert marker.is_file() + assert not (etc / "data").exists() + + +class TestLoadConfigIntegration: + def test_load_migrates_then_honors_explicit_750k(self, tmp_path, caplog): + config_path = tmp_path / "config.yml" + config_path.write_text(_LEGACY_YAML) + with caplog.at_level(logging.INFO, logger="odin.config"): + cfg = load_config(config_path) + cc = cfg.openai_codex.context_compression + assert cc.max_context_chars is None + assert cc.resolved_max_context_chars == LEGACY_MAX_CONTEXT_CHARS + assert ( + yaml.safe_load(config_path.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] + is None + ) + + # A deliberate explicit set of the SAME literal value now persists: + # the one-time gate, not eternal coercion. + patch_config_paths( + [ + ( + ("openai_codex", "context_compression", "max_context_chars"), + LEGACY_MAX_CONTEXT_CHARS, + ) + ], + path=config_path, + ) + cfg2 = load_config(config_path) + assert cfg2.openai_codex.context_compression.max_context_chars == LEGACY_MAX_CONTEXT_CHARS + + +class TestLexicalLiteralGate: + def test_only_shipped_plain_decimal_scalar_is_rewritten(self, tmp_path): + forms = { + "750000": True, + "750000.0": False, + "0xB71B0": False, + "750_000": False, + "+750000": False, + "!!int 750000": False, + "'750000'": False, + '"750000"': False, + } + for index, (value, migrates) in enumerate(forms.items()): + path = tmp_path / str(index) / "config.yml" + path.parent.mkdir() + path.write_text( + f"openai_codex:\n context_compression:\n max_context_chars: {value}\n" + ) + before = path.read_text() + data = _migrate(path) + if migrates: + assert data["openai_codex"]["context_compression"]["max_context_chars"] is None + assert ( + yaml.safe_load(path.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] + is None + ) + else: + assert path.read_text() == before + + +class TestCompletionRecordProvenance: + @pytest.mark.parametrize("kind", ["empty", "corrupt", "directory"]) + def test_invalid_marker_never_counts_as_completion_or_gets_overwritten(self, tmp_path, kind): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + if kind == "empty": + marker.write_text("") + elif kind == "corrupt": + marker.write_text("{broken") + else: + marker.mkdir() + with pytest.raises(MigrationCompletionError): + _migrate(path) + assert f"max_context_chars: {LEGACY_MAX_CONTEXT_CHARS}" in path.read_text() + if kind == "empty": + assert marker.read_text() == "" + elif kind == "corrupt": + assert marker.read_text() == "{broken" + else: + assert marker.is_dir() + + def test_unknown_record_fails_closed_without_overwrite(self, tmp_path): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + raw = '{"version": 99, "migration": "future"}\n' + marker.write_text(raw) + with pytest.raises(MigrationCompletionError): + _migrate(path) + assert marker.read_text() == raw + assert f"max_context_chars: {LEGACY_MAX_CONTEXT_CHARS}" in path.read_text() + + def test_round1_legacy_marker_does_not_suppress_rewrite(self, tmp_path): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + marker.write_text( + json.dumps( + { + "migration": "legacy_max_context_chars_to_auto", + "legacy_value": LEGACY_MAX_CONTEXT_CHARS, + "migrated_at": datetime.now(UTC).isoformat(), + } + ) + ) + _migrate(path) + assert ( + yaml.safe_load(path.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] + is None + ) + assert json.loads(marker.read_text())["version"] == 3 + + def test_round1_operator_marker_preserves_and_upgrades(self, tmp_path): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + marker.write_text( + json.dumps( + { + "migration": "legacy_max_context_chars_to_auto", + "operator_saved": True, + "saved_at": datetime.now(UTC).isoformat(), + } + ) + ) + data = _migrate(path) + assert ( + data["openai_codex"]["context_compression"]["max_context_chars"] + == LEGACY_MAX_CONTEXT_CHARS + ) + record = json.loads(marker.read_text()) + assert record["version"] == 3 + assert record["reason"] == "prior_operator_saved" + + def test_preversioned_round2_completion_is_validated_then_upgraded(self, tmp_path): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + marker.write_text( + json.dumps( + { + "migration": "legacy_max_context_chars_to_auto", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + } + ) + ) + data = _migrate(path) + assert ( + data["openai_codex"]["context_compression"]["max_context_chars"] + == LEGACY_MAX_CONTEXT_CHARS + ) + assert json.loads(marker.read_text())["version"] == 3 + + @pytest.mark.parametrize( + "record", + [ + [], + { + "version": 2, + "migration": "legacy_max_context_chars_to_auto", + "state": "completed", + "reason": "not_applicable", + "completed_at": 123, + }, + { + "version": 2, + "migration": "legacy_max_context_chars_to_auto", + "state": "completed", + "reason": "not_applicable", + "completed_at": "not-a-date", + }, + { + "version": 2, + "migration": "legacy_max_context_chars_to_auto", + "state": "completed", + "reason": [], + "completed_at": datetime.now(UTC).isoformat(), + }, + { + "migration": "legacy_max_context_chars_to_auto", + "reason": {}, + "completed_at": datetime.now(UTC).isoformat(), + }, + ], + ) + def test_malformed_record_fields_are_unknown(self, tmp_path, record): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + marker.write_text(json.dumps(record)) + with pytest.raises(MigrationCompletionError): + _migrate(path) + + def test_unreadable_marker_is_not_completion(self, tmp_path, monkeypatch): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + marker.write_text("placeholder") + real_read_text = type(marker).read_text + + def unreadable(self, *args, **kwargs): + if self == marker: + raise OSError("unreadable") + return real_read_text(self, *args, **kwargs) + + monkeypatch.setattr(type(marker), "read_text", unreadable) + with pytest.raises(MigrationCompletionError): + _migrate(path) + + +class TestVacuousFailureSequence: + def test_blocked_vacuous_record_prevents_later_750k_erasure(self, tmp_path): + """Exact round-2 blocker: failure must stop this boot truthfully.""" + path = tmp_path / "config.yml" + path.write_text("openai_codex:\n context_compression:\n max_context_chars: null\n") + (tmp_path / "data").write_text("blocks marker directory") + with pytest.raises(MigrationCompletionError): + _migrate(path) + assert "max_context_chars: null" in path.read_text() + + # No boot may have continued and handed control to an operator. Once + # persistence is repaired, the original null completes vacuously; only + # then is a later literal 750000 unambiguously intentional. + (tmp_path / "data").unlink() + _migrate(path) + path.write_text(_LEGACY_YAML) + data = _migrate(path) + assert ( + data["openai_codex"]["context_compression"]["max_context_chars"] + == LEGACY_MAX_CONTEXT_CHARS + ) + assert f"max_context_chars: {LEGACY_MAX_CONTEXT_CHARS}" in path.read_text() + + def test_load_config_reports_truthful_vacuous_completion_failure(self, tmp_path): + path = tmp_path / "config.yml" + path.write_text( + 'discord:\n token: "t"\n' + "openai_codex:\n context_compression:\n" + " max_context_chars: null\n" + ) + (tmp_path / "data").write_text("blocks marker directory") + with pytest.raises(SystemExit, match="Configuration migration failed"): + load_config(path) + + +class TestAtomicMarkerPersistence: + def test_marker_replace_is_fsynced_and_mode_0600(self, tmp_path, monkeypatch): + marker = tmp_path / "data" / "marker.json" + import src.config.migrations as migrations + + calls = [] + real_fsync = os.fsync + real_replace = os.replace + + def spy_fsync(fd): + calls.append("fsync") + return real_fsync(fd) + + def spy_replace(source, destination): + calls.append("replace") + return real_replace(source, destination) + + monkeypatch.setattr(migrations.os, "fsync", spy_fsync) + monkeypatch.setattr(migrations.os, "replace", spy_replace) + _atomic_write_marker(marker, {"ok": True}) + assert calls[0] == "fsync" + assert "replace" in calls + assert marker.stat().st_mode & 0o777 == 0o600 + assert not list(marker.parent.glob("*.tmp")) + + +class TestAtomicMarkerFailures: + def test_replace_failure_cleans_temporary_file(self, tmp_path, monkeypatch): + marker = tmp_path / "data" / "marker.json" + import src.config.migrations as migrations + + def fail_replace(*args, **kwargs): + raise OSError("replace failed") + + monkeypatch.setattr(migrations.os, "replace", fail_replace) + with pytest.raises(OSError, match="replace failed"): + _atomic_write_marker(marker, {"ok": True}) + assert not marker.exists() + assert not list(marker.parent.glob("*.tmp")) + + +class TestRemainingMigrationBranches: + def test_required_marker_write_error_is_truthful(self, tmp_path, monkeypatch): + path = tmp_path / "config.yml" + path.write_text("discord:\n token: t\n") + import src.config.migrations as migrations + + def fail_write(*args, **kwargs): + raise OSError("marker blocked") + + monkeypatch.setattr(migrations, "_atomic_write_marker", fail_write) + with pytest.raises(MigrationCompletionError, match="configuration was left unchanged"): + _migrate(path) + + def test_runtime_auto_tolerates_missing_or_non_mapping_sections(self): + import src.config.migrations as migrations + + missing = {} + migrations._set_runtime_auto(missing) + assert missing == {} + + non_mapping = {"openai_codex": {"context_compression": "disabled"}} + migrations._set_runtime_auto(non_mapping) + assert non_mapping["openai_codex"]["context_compression"] == "disabled" + + def test_open_temp_stream_is_closed_and_removed_on_write_failure(self, tmp_path, monkeypatch): + marker = tmp_path / "marker.json" + import src.config.migrations as migrations + + class BrokenStream: + closed = False + + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def write(self, value): + raise OSError("write failed") + + def close(self): + self.closed = True + + broken = BrokenStream() + monkeypatch.setattr(migrations.os, "fdopen", lambda *args, **kwargs: broken) + with pytest.raises(OSError, match="write failed"): + _atomic_write_marker(marker, {"ok": True}) + assert broken.closed is True + assert not list(tmp_path.glob("*.tmp")) + + def test_fd_is_closed_and_temp_removed_when_fdopen_fails(self, tmp_path, monkeypatch): + marker = tmp_path / "marker.json" + import src.config.migrations as migrations + + closed = [] + real_close = os.close + + def fail_fdopen(*args, **kwargs): + raise OSError("fdopen failed") + + def spy_close(fd): + closed.append(fd) + return real_close(fd) + + monkeypatch.setattr(migrations.os, "fdopen", fail_fdopen) + monkeypatch.setattr(migrations.os, "close", spy_close) + with pytest.raises(OSError, match="fdopen failed"): + _atomic_write_marker(marker, {"ok": True}) + assert closed + assert not list(tmp_path.glob("*.tmp")) + +class TestLegacyMarkerClaim: + def test_invalid_and_unreadable_claims_fail_closed(self, tmp_path, monkeypatch): + import src.config.migrations as migrations + + legacy = tmp_path / "context_ceiling_migration.json" + claim = migrations._legacy_claim_path(legacy) + claim.write_text("not-a-config-id\n") + with pytest.raises(MigrationCompletionError, match="claim is invalid"): + migrations._read_claim_owner(claim) + + real_read = type(claim).read_text + + def unreadable(self, *args, **kwargs): + if self == claim: + raise OSError("unreadable") + return real_read(self, *args, **kwargs) + + monkeypatch.setattr(type(claim), "read_text", unreadable) + with pytest.raises(MigrationCompletionError, match="claim is invalid"): + migrations._read_claim_owner(claim) + + def test_claim_fchmod_failure_closes_fd_and_removes_temp(self, tmp_path, monkeypatch): + import src.config.migrations as migrations + + legacy = tmp_path / "context_ceiling_migration.json" + closed = [] + real_close = os.close + + def fail_fchmod(_fd, _mode): + raise OSError("fchmod failed") + + def spy_close(fd): + closed.append(fd) + return real_close(fd) + + monkeypatch.setattr(migrations.os, "fchmod", fail_fchmod) + monkeypatch.setattr(migrations.os, "close", spy_close) + with pytest.raises(MigrationCompletionError, match="could not claim"): + migrations._claim_legacy_marker(legacy, "a" * 64) + assert closed + assert not list(tmp_path.glob("*.tmp")) + + def test_claim_stream_failure_closes_stream_and_removes_temp(self, tmp_path, monkeypatch): + import src.config.migrations as migrations + + legacy = tmp_path / "context_ceiling_migration.json" + + class BrokenStream: + def __init__(self, fd): + self.fd = fd + self.closed = False + + def __enter__(self): + return self + + def __exit__(self, *_args): + return None + + def write(self, _value): + raise OSError("write failed") + + def close(self): + if not self.closed: + os.close(self.fd) + self.closed = True + + streams = [] + + def broken_fdopen(fd, *_args, **_kwargs): + stream = BrokenStream(fd) + streams.append(stream) + return stream + + monkeypatch.setattr(migrations.os, "fdopen", broken_fdopen) + with pytest.raises(MigrationCompletionError, match="could not claim"): + migrations._claim_legacy_marker(legacy, "a" * 64) + assert streams[0].closed is True + assert not list(tmp_path.glob("*.tmp")) + + def test_foreign_claim_does_not_launder_corrupt_legacy_marker(self, tmp_path): + import src.config.migrations as migrations + + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.parent.mkdir() + legacy.write_text("{broken") + assert migrations._claim_legacy_marker(legacy, "f" * 64) + + with pytest.raises(MigrationCompletionError, match="legacy ceiling-migration"): + _migrate(path) + + def test_existing_foreign_claim_forces_own_literal_migration(self, tmp_path): + import src.config.migrations as migrations + + first = tmp_path / "first.yml" + second = tmp_path / "second.yml" + first.write_text(_LEGACY_YAML) + second.write_text(_LEGACY_YAML) + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.parent.mkdir() + legacy.write_text( + json.dumps( + { + "version": 2, + "migration": "legacy_max_context_chars_to_auto", + "state": "completed", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + } + ) + ) + assert migrations._claim_legacy_marker( + legacy, migrations._config_identity(first) + ) + + second_data = _migrate(second) + + assert second_data["openai_codex"]["context_compression"]["max_context_chars"] is None + assert yaml.safe_load(second.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] is None + + +class TestConfigIdentityBinding: + def test_symlink_aliases_in_different_directories_share_completion(self, tmp_path): + real_dir = tmp_path / "real" + alias_a_dir = tmp_path / "alias-a" + alias_b_dir = tmp_path / "alias-b" + for directory in (real_dir, alias_a_dir, alias_b_dir): + directory.mkdir() + target = real_dir / "odin.yml" + target.write_text(_LEGACY_YAML) + alias_a = alias_a_dir / "config.yml" + alias_b = alias_b_dir / "other.yml" + alias_a.symlink_to(target) + alias_b.symlink_to(target) + + _migrate(alias_a) + assert yaml.safe_load(target.read_text())["openai_codex"][ + "context_compression" + ]["max_context_chars"] is None + + # A deliberate post-migration value must survive loading through a + # different launch alias whose local marker did not exist yet. + patch_config_paths( + [(("openai_codex", "context_compression", "max_context_chars"), 750_000)], + path=target, + ) + data = _migrate(alias_b) + assert data["openai_codex"]["context_compression"]["max_context_chars"] == 750_000 + assert yaml.safe_load(target.read_text())["openai_codex"][ + "context_compression" + ]["max_context_chars"] == 750_000 + assert ceiling_marker_path(alias_a).is_file() + assert ceiling_marker_path(alias_b).is_file() + assert json.loads(ceiling_marker_path(alias_a).read_text())["config_id"] == json.loads( + ceiling_marker_path(alias_b).read_text() + )["config_id"] + + def test_distinct_configs_in_one_directory_do_not_share_completion(self, tmp_path): + first = tmp_path / "first.yml" + second = tmp_path / "second.yml" + first.write_text(_LEGACY_YAML) + second.write_text(_LEGACY_YAML) + + _migrate(first) + _migrate(second) + + assert yaml.safe_load(first.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] is None + assert yaml.safe_load(second.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] is None + assert ceiling_marker_path(first) != ceiling_marker_path(second) + first_record = json.loads(ceiling_marker_path(first).read_text()) + second_record = json.loads(ceiling_marker_path(second).read_text()) + assert first_record["config_id"] != second_record["config_id"] + + def test_concurrent_siblings_cannot_both_adopt_one_legacy_marker( + self, tmp_path, monkeypatch + ): + """The exact integration race: both readers reach claim together.""" + import src.config.migrations as migrations + + first = tmp_path / "first.yml" + second = tmp_path / "second.yml" + first.write_text(_LEGACY_YAML) + second.write_text(_LEGACY_YAML) + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.parent.mkdir() + legacy.write_text( + json.dumps( + { + "version": 2, + "migration": "legacy_max_context_chars_to_auto", + "state": "completed", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + } + ) + ) + barrier = threading.Barrier(2) + real_claim = migrations._claim_legacy_marker + + def synchronized_claim(marker, config_id): + barrier.wait(timeout=2) + return real_claim(marker, config_id) + + monkeypatch.setattr(migrations, "_claim_legacy_marker", synchronized_claim) + with ThreadPoolExecutor(max_workers=2) as executor: + first_future = executor.submit(_migrate, first) + second_future = executor.submit(_migrate, second) + results = [first_future.result(timeout=3), second_future.result(timeout=3)] + + values = sorted( + result["openai_codex"]["context_compression"]["max_context_chars"] + if result["openai_codex"]["context_compression"]["max_context_chars"] is not None + else -1 + for result in results + ) + assert values == [-1, 750_000] + disk_values = { + yaml.safe_load(path.read_text())["openai_codex"]["context_compression"][ + "max_context_chars" + ] + for path in (first, second) + } + assert disk_values == {None, 750_000} + first_record = json.loads(ceiling_marker_path(first).read_text()) + second_record = json.loads(ceiling_marker_path(second).read_text()) + assert first_record["config_id"] != second_record["config_id"] + bound = json.loads(legacy.read_text()) + assert bound["config_id"] in {first_record["config_id"], second_record["config_id"]} + + def test_preidentity_directory_marker_is_bound_not_shared(self, tmp_path): + first = tmp_path / "first.yml" + second = tmp_path / "second.yml" + first.write_text(_LEGACY_YAML) + second.write_text(_LEGACY_YAML) + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.parent.mkdir() + legacy.write_text( + json.dumps( + { + "version": 2, + "migration": "legacy_max_context_chars_to_auto", + "state": "completed", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + } + ) + ) + + # The first config adopts the old provenance and therefore preserves a + # value the old migration had already classified as operator-authored. + first_data = _migrate(first) + assert first_data["openai_codex"]["context_compression"][ + "max_context_chars" + ] == 750_000 + + # The legacy marker is now identity-bound. A distinct sibling no longer + # inherits it and performs its own one-time rewrite. + second_data = _migrate(second) + assert second_data["openai_codex"]["context_compression"][ + "max_context_chars" + ] is None + assert json.loads(ceiling_marker_path(first).read_text())["config_id"] != json.loads( + ceiling_marker_path(second).read_text() + )["config_id"] + +class TestIdentityMarkerAdversarialBranches: + def test_identity_marker_reread_failure_is_fail_closed(self, tmp_path, monkeypatch): + import src.config.migrations as migrations + + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + config_id = marker.name.rsplit(".", 2)[-2] + migrations._atomic_write_marker( + marker, + { + "version": 3, + "migration": "legacy_max_context_chars_to_auto", + "config_id": config_id, + "state": "completed", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + }, + ) + real_read = type(marker).read_text + reads = 0 + + def fail_second_read(self, *args, **kwargs): + nonlocal reads + if self == marker: + reads += 1 + if reads == 2: + raise OSError("reread failed") + return real_read(self, *args, **kwargs) + + monkeypatch.setattr(type(marker), "read_text", fail_second_read) + with pytest.raises(MigrationCompletionError): + _migrate(path) + + def test_unrelated_identity_directory_debris_does_not_change_adoption(self, tmp_path): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + marker = ceiling_marker_path(path) + marker.parent.mkdir(parents=True) + (marker.parent / "unrelated.tmp").write_text("debris") + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.write_text( + json.dumps( + { + "version": 2, + "migration": "legacy_max_context_chars_to_auto", + "state": "completed", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + } + ) + ) + + data = _migrate(path) + + assert data["openai_codex"]["context_compression"]["max_context_chars"] == 750_000 + + def test_corrupt_bound_legacy_marker_is_fail_closed(self, tmp_path): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.parent.mkdir() + legacy.write_text("{broken") + with pytest.raises(MigrationCompletionError, match="legacy ceiling-migration"): + _migrate(path) + + def test_bound_legacy_marker_for_other_identity_is_ignored(self, tmp_path): + import src.config.migrations as migrations + + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.parent.mkdir() + migrations._atomic_write_marker( + legacy, + { + "version": 3, + "migration": "legacy_max_context_chars_to_auto", + "config_id": "f" * 64, + "state": "completed", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + }, + ) + data = _migrate(path) + assert data["openai_codex"]["context_compression"]["max_context_chars"] is None + + def test_round1_legacy_at_old_path_rewrites_and_binds(self, tmp_path): + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.parent.mkdir() + legacy.write_text( + json.dumps( + { + "migration": "legacy_max_context_chars_to_auto", + "legacy_value": 750_000, + "migrated_at": datetime.now(UTC).isoformat(), + } + ) + ) + data = _migrate(path) + assert data["openai_codex"]["context_compression"]["max_context_chars"] is None + + def test_bound_legacy_marker_reread_failure_is_fail_closed(self, tmp_path, monkeypatch): + import src.config.migrations as migrations + + path = tmp_path / "config.yml" + path.write_text(_LEGACY_YAML) + legacy = tmp_path / "data" / "context_ceiling_migration.json" + legacy.parent.mkdir() + migrations._atomic_write_marker( + legacy, + { + "version": 3, + "migration": "legacy_max_context_chars_to_auto", + "config_id": migrations._config_identity(path), + "state": "completed", + "reason": "not_applicable", + "completed_at": datetime.now(UTC).isoformat(), + }, + ) + real_read = type(legacy).read_text + reads = 0 + + def fail_second_read(self, *args, **kwargs): + nonlocal reads + if self == legacy: + reads += 1 + if reads == 2: + raise OSError("reread failed") + return real_read(self, *args, **kwargs) + + monkeypatch.setattr(type(legacy), "read_text", fail_second_read) + with pytest.raises(MigrationCompletionError, match="legacy ceiling-migration"): + _migrate(path) diff --git a/tests/test_config_persistence.py b/tests/test_config_persistence.py index be63eaa4..745d9899 100644 --- a/tests/test_config_persistence.py +++ b/tests/test_config_persistence.py @@ -318,6 +318,31 @@ def test_without_the_schema_an_alias_is_still_dropped(self): {"search": {"search_db_path": "/old", "enabled": True}}, ) == [] + def test_schema_owned_mapping_persists_canonicalized_keys_as_one_leaf(self): + from src.config.schema import Config + + current = Config(discord={"token": "test"}).model_dump() + current["openai_codex"]["context_budget_overrides"] = { + "gpt-5.6-luna": 600_000, + } + leaves = submitted_leaves( + { + "openai_codex": { + "context_budget_overrides": { + "codex-auto-review": 600_000, + } + } + }, + current, + Config, + ) + assert leaves == [ + ( + ("openai_codex", "context_budget_overrides"), + {"gpt-5.6-luna": 600_000}, + ) + ] + def test_canonical_key_still_resolves_normally(self): from src.config.schema import Config diff --git a/tests/test_config_schema_validators.py b/tests/test_config_schema_validators.py index c239b71e..73fb5239 100644 --- a/tests/test_config_schema_validators.py +++ b/tests/test_config_schema_validators.py @@ -168,9 +168,10 @@ def test_validation_failure_is_systemexit(self, tmp_path): class TestCodexReasoningEffort: - def test_default_is_medium(self): + def test_default_is_xhigh(self): + # Defaults mirror the reference deployment (defaults ruling). from src.config.schema import OpenAICodexConfig - assert OpenAICodexConfig().reasoning_effort == "medium" + assert OpenAICodexConfig().reasoning_effort == "xhigh" def test_all_enum_values_accepted(self): from src.config.schema import CODEX_REASONING_EFFORTS, OpenAICodexConfig @@ -253,9 +254,11 @@ def test_max_lifetime_bounds(self): class TestAgentReasoningEffortConfig: - def test_default_is_inherit(self): + def test_default_is_auto(self): + # Defaults mirror the reference deployment: per-spawn Auto/Dynamic. from src.config.schema import OpenAICodexConfig - assert OpenAICodexConfig().agent_reasoning_effort is None + assert OpenAICodexConfig().agent_reasoning_effort == "auto" + assert OpenAICodexConfig(agent_reasoning_effort=None).agent_reasoning_effort is None def test_valid_values_accepted(self): from src.config.schema import CODEX_REASONING_EFFORTS, OpenAICodexConfig @@ -279,9 +282,11 @@ def test_legacy_minimal_coerced_to_low(self): class TestAgentModelConfig: - def test_default_is_inherit(self): + def test_default_is_auto(self): + # Defaults mirror the reference deployment: per-spawn Auto/Dynamic. from src.config.schema import OpenAICodexConfig - assert OpenAICodexConfig().agent_model is None + assert OpenAICodexConfig().agent_model == "auto" + assert OpenAICodexConfig(agent_model=None).agent_model is None def test_value_round_trips(self): from src.config.schema import OpenAICodexConfig diff --git a/tests/test_context_budget.py b/tests/test_context_budget.py new file mode 100644 index 00000000..bc84703c --- /dev/null +++ b/tests/test_context_budget.py @@ -0,0 +1,310 @@ +"""Contract battery for the per-model context-budget kernel (campaign phase 1). + +Covers the pure resolver (src/llm/context_budget.py), the schema-owned +registry/canonicalizer, and the new configuration fields. The settled +numbers here are the plan-of-record examples — a formula change that moves +any of them is a design change, not a refactor. +""" + +from __future__ import annotations + +import dataclasses + +import pytest + +from src.config.schema import ( + CODEX_MODEL_INPUT_BUDGETS, + CODEX_UNKNOWN_MODEL_INPUT_BUDGET, + CONTEXT_BUDGET_OVERRIDE_MAX, + CONTEXT_BUDGET_OVERRIDE_MIN, + LEGACY_MAX_CONTEXT_CHARS, + ContextCompressionConfig, + OpenAICodexConfig, + canonical_codex_model, + input_budget_floor_for_model, +) +from src.llm.context_budget import ( + FIXED_ENVELOPE_RESERVE_TOKENS, + LEGACY_UTILIZATION_FLOOR_TOKENS, + RESCUE_CEILING_CHARS, + resolve_context_budget, +) + + +# --------------------------------------------------------------------------- +# Canonicalizer + registry +# --------------------------------------------------------------------------- +class TestCanonicalizer: + def test_trims_and_preserves_unknown_spelling(self): + assert canonical_codex_model(" gpt-5.6-sol ") == "gpt-5.6-sol" + # Unknown models pass through spelling-preserved — no case folding. + assert canonical_codex_model(" Some-Future-Model ") == "Some-Future-Model" + + def test_alias_maps_before_lookup(self): + assert canonical_codex_model("codex-auto-review") == "gpt-5.6-luna" + assert ( + input_budget_floor_for_model("codex-auto-review") + == CODEX_MODEL_INPUT_BUDGETS["gpt-5.6-luna"] + ) + + def test_none_and_empty(self): + assert canonical_codex_model(None) == "" + assert canonical_codex_model(" ") == "" + assert input_budget_floor_for_model(None) == CODEX_UNKNOWN_MODEL_INPUT_BUDGET + + def test_alias_is_not_a_registry_row(self): + assert "codex-auto-review" not in CODEX_MODEL_INPUT_BUDGETS + + +class TestRegistryFloors: + def test_floors_match_probe_evidence(self): + # A floor never exceeds its own accepted observation: only sol got + # the fine-refinement acceptances (921,601); its window-mates proved + # 917,506 (plan of record R2). + assert CODEX_MODEL_INPUT_BUDGETS == { + "gpt-5.6-sol": 921_601, + "gpt-5.6-terra": 917_506, + "gpt-5.6-luna": 917_506, + "gpt-5.4": 917_506, + "gpt-5.5": 270_001, + "gpt-5.4-mini": 262_146, + "gpt-5.3-codex-spark": 124_001, + } + assert CODEX_UNKNOWN_MODEL_INPUT_BUDGET == 272_000 + + +# --------------------------------------------------------------------------- +# Resolver characterization — the settled plan-of-record numbers +# --------------------------------------------------------------------------- +class TestResolverDefaults: + def test_sol_at_defaults(self): + snap = resolve_context_budget("gpt-5.6-sol") + assert snap.base_budget == 921_601 + assert snap.base_source == "floor" + assert snap.working_budget == 552_960 + assert snap.compactable_tokens == 510_960 + assert snap.primary_chars == 1_277_400 + assert snap.ladder == (894_180, 400_000) + + def test_terra_luna_54_at_defaults(self): + for model in ("gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.4"): + snap = resolve_context_budget(model) + assert snap.base_budget == 917_506 + assert snap.working_budget == 550_503 + assert snap.primary_chars == 1_271_257 + assert snap.ladder == (889_879, 400_000) + + def test_55_legacy_floor_keeps_full_target(self): + snap = resolve_context_budget("gpt-5.5") + # 60% of 270,001 is 162,000 — the 272K legacy floor wins, so the + # working budget stays the full budget and the derived target matches + # the pre-campaign per-model result exactly. + assert snap.working_budget == 270_001 + assert snap.primary_chars == 570_002 + assert snap.ladder == (399_001,) + + def test_54_mini_at_defaults(self): + snap = resolve_context_budget("gpt-5.4-mini") + assert snap.working_budget == 262_146 + assert snap.primary_chars == 550_365 + assert snap.ladder == (385_255,) + + def test_spark_not_lifted_by_legacy_floor(self): + snap = resolve_context_budget("gpt-5.3-codex-spark") + # min(budget, max(272K, …)) can never RAISE a small budget. + assert snap.working_budget == 124_001 + assert snap.primary_chars == 205_002 + assert snap.ladder == (143_501,) + + def test_unknown_model_reproduces_precampaign_constants(self): + snap = resolve_context_budget("some-new-model") + assert snap.base_source == "unknown_default" + assert snap.working_budget == 272_000 + # Today's shipped emergency targets were 575,000 / 400,000: the + # unknown-model primary is exactly the old first target and the + # rescue ceiling is exactly the old aggressive target. + assert snap.primary_chars == 575_000 + assert snap.ladder == (402_500, 400_000) + + def test_alias_resolves_identically_to_luna(self): + assert resolve_context_budget("codex-auto-review") == resolve_context_budget( + "gpt-5.6-luna" + ) + + def test_snapshot_is_frozen(self): + snap = resolve_context_budget("gpt-5.6-sol") + with pytest.raises(dataclasses.FrozenInstanceError): + snap.primary_chars = 1 # type: ignore[misc] + + +# --------------------------------------------------------------------------- +# Overrides, clamps, utilization, ceiling +# --------------------------------------------------------------------------- +class TestResolverInputs: + def test_override_beats_floor(self): + snap = resolve_context_budget( + "gpt-5.5", overrides={"gpt-5.5": 400_000} + ) + assert snap.base_budget == 400_000 + assert snap.base_source == "override" + # 60% of 400,000 = 240,000 < 272,000 legacy floor → floor wins, + # capped by the budget itself. + assert snap.working_budget == 272_000 + + def test_override_for_unknown_model(self): + snap = resolve_context_budget("future-x", overrides={"future-x": 600_000}) + assert snap.base_source == "override" + assert snap.working_budget == 360_000 + + def test_clamp_below_base_applies(self): + snap = resolve_context_budget("gpt-5.6-sol", observed_clamp=372_000) + assert snap.clamp_applied is True + assert snap.effective_budget == 372_000 + # 60% of 372,000 = 223,200 → legacy floor 272,000 wins. + assert snap.working_budget == 272_000 + assert snap.primary_chars == 575_000 + + def test_clamp_at_or_above_base_ignored(self): + for clamp in (921_601, 1_000_000): + snap = resolve_context_budget("gpt-5.6-sol", observed_clamp=clamp) + assert snap.clamp_applied is False + assert snap.effective_budget == 921_601 + + def test_utilization_100_uses_full_budget(self): + snap = resolve_context_budget("gpt-5.6-sol", utilization=100) + assert snap.working_budget == 921_601 + assert snap.primary_chars == 2_199_002 + + def test_utilization_30_bites_only_large_budgets(self): + sol = resolve_context_budget("gpt-5.6-sol", utilization=30) + assert sol.working_budget == 276_480 + five5 = resolve_context_budget("gpt-5.5", utilization=30) + assert five5.working_budget == 270_001 # legacy floor: unchanged + + def test_explicit_ceiling_only_lowers(self): + lowered = resolve_context_budget("gpt-5.6-sol", max_context_chars=800_000) + assert lowered.ceiling_applied is True + assert lowered.primary_chars == 800_000 + raised = resolve_context_budget("gpt-5.6-sol", max_context_chars=9_999_999) + assert raised.ceiling_applied is False + assert raised.primary_chars == 1_277_400 + + def test_ladder_derives_from_final_primary_after_ceiling(self): + # The R5-settled ordering rule: a low explicit ceiling must pull the + # rescue rungs down with it — never an emergency target above primary. + snap = resolve_context_budget("gpt-5.6-sol", max_context_chars=300_000) + assert snap.primary_chars == 300_000 + assert snap.ladder == (210_000,) + assert all(rung <= snap.primary_chars for rung in snap.ladder) + + +# --------------------------------------------------------------------------- +# Totality — evidence clamps bypass override bounds by design +# --------------------------------------------------------------------------- +class TestResolverTotality: + @pytest.mark.parametrize("clamp", [0, 41_999, FIXED_ENVELOPE_RESERVE_TOKENS]) + def test_clamp_at_or_below_envelope_yields_empty_ladder(self, clamp): + snap = resolve_context_budget("gpt-5.6-sol", observed_clamp=clamp) + assert snap.compactable_tokens == 0 + assert snap.derived_chars == 0 + assert snap.primary_chars == 0 + assert snap.ladder == () + + def test_negative_clamp_never_goes_negative(self): + snap = resolve_context_budget("gpt-5.6-sol", observed_clamp=-5) + assert snap.compactable_tokens == 0 + assert snap.derived_chars == 0 + assert snap.ladder == () + + def test_clamp_at_override_minimum_boundary(self): + snap = resolve_context_budget( + "gpt-5.6-sol", observed_clamp=CONTEXT_BUDGET_OVERRIDE_MIN + ) + # 50,192 − 42,000 = 8,192 tokens → 20,480 chars → single 14,336 rung. + assert snap.compactable_tokens == 8_192 + assert snap.derived_chars == 20_480 + assert snap.ladder == (14_336,) + + def test_ladder_always_positive_monotonic_deduped(self): + for clamp in (None, 43_000, 50_192, 100_000, 372_000, 921_601): + for util in (30, 60, 100): + for ceiling in (None, 1, 100_000, 500_000, 5_000_000): + snap = resolve_context_budget( + "gpt-5.6-sol", + observed_clamp=clamp, + utilization=util, + max_context_chars=ceiling, + ) + assert all(rung > 0 for rung in snap.ladder) + assert list(snap.ladder) == sorted(snap.ladder, reverse=True) + assert len(set(snap.ladder)) == len(snap.ladder) + if snap.ladder: + # The dedupe guarantees this: a lone surviving rung is + # one that already sat at or below the rescue ceiling. + assert snap.ladder[-1] <= RESCUE_CEILING_CHARS + assert all(r <= snap.primary_chars for r in snap.ladder) + + def test_rescue_ceiling_never_enlarges(self): + # Models whose own rung is below 400K keep it (min semantics). + snap = resolve_context_budget("gpt-5.3-codex-spark") + assert snap.ladder == (143_501,) + assert LEGACY_UTILIZATION_FLOOR_TOKENS == 272_000 + + +# --------------------------------------------------------------------------- +# Configuration surface +# --------------------------------------------------------------------------- +class TestBudgetConfig: + def test_override_keys_canonicalized_and_deduped(self): + cfg = OpenAICodexConfig( + context_budget_overrides={" codex-auto-review ": 900_000} + ) + assert cfg.context_budget_overrides == {"gpt-5.6-luna": 900_000} + with pytest.raises(ValueError, match="duplicates"): + OpenAICodexConfig( + context_budget_overrides={ + "codex-auto-review": 900_000, + "gpt-5.6-luna": 800_000, + } + ) + + @pytest.mark.parametrize( + "value", [0, CONTEXT_BUDGET_OVERRIDE_MIN - 1, CONTEXT_BUDGET_OVERRIDE_MAX + 1] + ) + def test_override_bounds_enforced(self, value): + with pytest.raises(ValueError): + OpenAICodexConfig(context_budget_overrides={"gpt-5.5": value}) + + def test_override_bound_edges_accepted(self): + cfg = OpenAICodexConfig( + context_budget_overrides={ + "gpt-5.5": CONTEXT_BUDGET_OVERRIDE_MIN, + "gpt-5.6-sol": CONTEXT_BUDGET_OVERRIDE_MAX, + } + ) + assert cfg.context_budget_overrides["gpt-5.5"] == CONTEXT_BUDGET_OVERRIDE_MIN + + def test_empty_override_key_rejected(self): + with pytest.raises(ValueError, match="non-empty"): + OpenAICodexConfig(context_budget_overrides={" ": 900_000}) + + @pytest.mark.parametrize("value", [29, 101, True]) + def test_utilization_bounds(self, value): + with pytest.raises(ValueError): + OpenAICodexConfig(context_utilization=value) + + def test_utilization_default_and_edges(self): + assert OpenAICodexConfig().context_utilization == 60 + assert OpenAICodexConfig(context_utilization=30).context_utilization == 30 + assert OpenAICodexConfig(context_utilization=100).context_utilization == 100 + + def test_ceiling_null_is_auto_and_positive_required(self): + assert ContextCompressionConfig().max_context_chars is None + assert ( + ContextCompressionConfig().resolved_max_context_chars + == LEGACY_MAX_CONTEXT_CHARS + ) + explicit = ContextCompressionConfig(max_context_chars=500_000) + assert explicit.resolved_max_context_chars == 500_000 + with pytest.raises(ValueError): + ContextCompressionConfig(max_context_chars=0) diff --git a/tests/test_context_budget_activation.py b/tests/test_context_budget_activation.py new file mode 100644 index 00000000..ddd9558d --- /dev/null +++ b/tests/test_context_budget_activation.py @@ -0,0 +1,688 @@ +"""Budget-policy activation (context-budget campaign phase 3). + +Pins the wiring that makes the per-model resolver real: agents resolve +their EFFECTIVE model's snapshot per generation, chat's soft threshold +follows the serving model, rescue ladders come from the snapshot, and the +no-provider fallback reproduces the pre-campaign conservative math. +""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from src.agents.manager import _fallback_budget_snapshot +from src.config.schema import ContextCompressionConfig, OpenAICodexConfig +from src.discord.llm_gateway import LLMGateway +from src.discord.native_tools.agents_tasks import ( + _capture_agent_generation_plan, + _make_budget_snapshot_provider, +) +from src.discord.tool_loop import ToolLoopRunner +from src.llm.context_budget import snapshot_for_codex_config +from src.llm.context_compressor import estimate_message_chars + + +class _CodexClient(SimpleNamespace): + """Codex-shaped: has reasoning_effort, so agent policy resolves models.""" + + +def _codex_client(model="gpt-5.6-sol"): + return _CodexClient(model=model, reasoning_effort="xhigh") + + +# --------------------------------------------------------------------------- +# snapshot_for_codex_config +# --------------------------------------------------------------------------- +class TestSnapshotForCodexConfig: + def test_reads_live_policy_fields_and_passed_ceiling(self): + cfg = OpenAICodexConfig( + context_budget_overrides={"gpt-5.6-sol": 800_000}, + context_utilization=100, + ) + snap = snapshot_for_codex_config("gpt-5.6-sol", cfg, max_context_chars=1_000_000) + assert snap.base_budget == 800_000 + assert snap.working_budget == 800_000 # utilization 100 + assert snap.primary_chars == 1_000_000 # explicit ceiling lowers + assert snap.ceiling_applied is True + + def test_getattr_safe_on_none_config(self): + snap = snapshot_for_codex_config("gpt-5.6-sol", None, max_context_chars=None) + assert snap.primary_chars == 1_277_400 # defaults: 60% utilization + + def test_fallback_snapshot_is_unknown_model_math(self): + snap = _fallback_budget_snapshot() + assert snap.base_source == "unknown_default" + assert snap.primary_chars == 575_000 + assert snap.ladder == (402_500, 400_000) + + +# --------------------------------------------------------------------------- +# Agent spawn-path provider +# --------------------------------------------------------------------------- +class TestAgentSnapshotProvider: + def test_inherit_tracks_live_model_next_generation(self): + config = SimpleNamespace( + openai_codex=OpenAICodexConfig(model="gpt-5.6-sol", agent_model=None) + ) + client = _codex_client("gpt-5.6-sol") + compressor = ContextCompressionConfig() + provider = _make_budget_snapshot_provider( + lambda: config, lambda: client, lambda: compressor, None + ) + assert provider().canonical_model == "gpt-5.6-sol" + assert provider().primary_chars == 1_277_400 + # A live model change reaches the NEXT resolution. + config.openai_codex.model = "gpt-5.5" + client.model = "gpt-5.5" + after = provider() + assert after.canonical_model == "gpt-5.5" + assert after.primary_chars == 570_002 + + def test_fixed_override_wins_over_live_config(self): + config = SimpleNamespace( + openai_codex=OpenAICodexConfig(model="gpt-5.6-sol", agent_model=None) + ) + provider = _make_budget_snapshot_provider( + lambda: config, lambda: _codex_client(), lambda: ContextCompressionConfig(), + "gpt-5.5", + ) + snap = provider() + assert snap.canonical_model == "gpt-5.5" + assert snap.primary_chars == 570_002 + + def test_non_codex_client_uses_its_own_model_name(self): + config = SimpleNamespace(openai_codex=OpenAICodexConfig()) + ollama = SimpleNamespace(model="qwen3:14b") # no reasoning_effort attr + provider = _make_budget_snapshot_provider( + lambda: config, lambda: ollama, lambda: ContextCompressionConfig(), None + ) + snap = provider() + assert snap.base_source == "unknown_default" + assert snap.primary_chars == 575_000 + + def test_non_codex_collision_gets_unknown_math(self): + """Review blocker #2 pin (agents): an Ollama client named after a + Codex slug never inherits the Codex capability floor.""" + config = SimpleNamespace(openai_codex=OpenAICodexConfig()) + impostor = SimpleNamespace(model="gpt-5.6-sol") # not codex-shaped + provider = _make_budget_snapshot_provider( + lambda: config, lambda: impostor, lambda: ContextCompressionConfig(), None + ) + snap = provider() + assert snap.base_source == "unknown_default" + assert snap.primary_chars == 575_000 + + def test_frozen_ceiling_comes_from_compressor_object(self): + config = SimpleNamespace(openai_codex=OpenAICodexConfig()) + compressor = ContextCompressionConfig(max_context_chars=500_000) + provider = _make_budget_snapshot_provider( + lambda: config, lambda: _codex_client(), lambda: compressor, None + ) + snap = provider() + assert snap.primary_chars == 500_000 + assert snap.ceiling_applied is True + + +# --------------------------------------------------------------------------- +# Chat soft threshold follows the serving model +# --------------------------------------------------------------------------- +def _chat_runner(model, compressor, *, codex_shaped=True): + runner = ToolLoopRunner.__new__(ToolLoopRunner) + runner._get_context_compressor = lambda: compressor + runner._get_compression_stats = lambda: None + runner._get_config = lambda: SimpleNamespace(openai_codex=OpenAICodexConfig()) + client = ( + _CodexClient(model=model, reasoning_effort="xhigh") + if codex_shaped + else SimpleNamespace(model=model) + ) + runner._llm_gateway = SimpleNamespace(active_client=client) + return runner + + +def _bulk_messages(char_target: int) -> list[dict]: + # A prefix plus enough structurally complete tool iterations to cross + # any threshold under test; sizes dominated by tool_result payloads. + messages: list[dict] = [{"role": "user", "content": "task"}] + # Small chunks so every fixture comfortably exceeds keep_recent=30 + # iterations — the soft pass only summarizes OLDER-than-recent ones. + chunk = "y" * 12_000 + while estimate_message_chars(messages) < char_target: + messages.append( + {"role": "assistant", "content": [ + {"type": "tool_use", "id": f"t{len(messages)}", "name": "read_file", + "input": {"path": "x"}}, + ]} + ) + messages.append( + {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": f"t{len(messages) - 1}", + "content": chunk}, + ]} + ) + return messages + + +class TestChatSoftThreshold: + def test_sol_headroom_no_compress_where_legacy_would_have(self): + """850K chars: over the legacy 750K, comfortably under sol's 1.277M — + the whole point of the campaign is that this stays uncompressed.""" + st = SimpleNamespace(iteration=3, messages=_bulk_messages(850_000)) + before = list(st.messages) + _chat_runner("gpt-5.6-sol", ContextCompressionConfig())._maybe_compress(st) + assert st.messages == before + + def test_sol_compresses_past_model_threshold(self): + st = SimpleNamespace(iteration=3, messages=_bulk_messages(1_400_000)) + _chat_runner("gpt-5.6-sol", ContextCompressionConfig())._maybe_compress(st) + assert estimate_message_chars(st.messages) < 1_400_000 + + def test_unknown_model_keeps_conservative_threshold(self): + st = SimpleNamespace(iteration=3, messages=_bulk_messages(700_000)) + _chat_runner("some-local-model", ContextCompressionConfig())._maybe_compress(st) + # 700K > the 575K unknown-model target: compressed. + assert estimate_message_chars(st.messages) < 700_000 + + def test_explicit_ceiling_still_lowers(self): + st = SimpleNamespace(iteration=3, messages=_bulk_messages(600_000)) + _chat_runner( + "gpt-5.6-sol", ContextCompressionConfig(max_context_chars=500_000) + )._maybe_compress(st) + assert estimate_message_chars(st.messages) < 600_000 + + def test_non_codex_client_named_like_codex_slug_gets_unknown_math(self): + """Review blocker #2 pin (chat): provider identity gates the registry. + A non-Codex client literally named gpt-5.6-sol compacts at the + conservative 575K unknown-model target, never sol's 1.277M floor.""" + st = SimpleNamespace(iteration=3, messages=_bulk_messages(700_000)) + _chat_runner( + "gpt-5.6-sol", ContextCompressionConfig(), codex_shaped=False + )._maybe_compress(st) + assert estimate_message_chars(st.messages) < 700_000 + + def test_first_iteration_never_compresses(self): + st = SimpleNamespace(iteration=0, messages=_bulk_messages(2_000_000)) + before = list(st.messages) + _chat_runner("gpt-5.6-sol", ContextCompressionConfig())._maybe_compress(st) + assert st.messages == before + + +# --------------------------------------------------------------------------- +# Round-2 blocker regression pins +# --------------------------------------------------------------------------- +class TestRound2GenerationIdentityPins: + async def test_agent_plan_freezes_effective_effort_before_in_place_mutation(self): + """The production live-reload shape mutates the SAME client object. + A frozen plan must contain xhigh, never the None inherit sentinel that + would re-read the now-max client during rescue.""" + cfg = SimpleNamespace( + openai_codex=OpenAICodexConfig( + model="gpt-5.6-sol", + agent_model=None, + agent_reasoning_effort=None, + ) + ) + client = _codex_client("gpt-5.6-sol") + plan = _capture_agent_generation_plan( + lambda: cfg, + lambda _cfg: client, + lambda: ContextCompressionConfig(), + model_override=None, + effort_override=None, + ) + assert plan["effort"] == "xhigh" + client.reasoning_effort = "max" + assert plan["client"] is client + assert plan["effort"] == "xhigh" + + def test_agent_capture_reads_root_config_once(self): + configs = [ + SimpleNamespace( + openai_codex=OpenAICodexConfig( + model="gpt-5.6-sol", + context_budget_overrides={"gpt-5.6-sol": 800_000}, + ) + ), + SimpleNamespace( + openai_codex=OpenAICodexConfig( + model="gpt-5.5", + context_budget_overrides={"gpt-5.6-sol": 270_001}, + ) + ), + ] + reads = 0 + + def get_config(): + nonlocal reads + value = configs[min(reads, 1)] + reads += 1 + return value + + plan = _capture_agent_generation_plan( + get_config, + lambda _cfg: _codex_client("gpt-5.6-sol"), + lambda: ContextCompressionConfig(), + model_override=None, + effort_override=None, + ) + assert reads == 1 + assert plan["model"] == "gpt-5.6-sol" + assert plan["snapshot"].base_budget == 800_000 + + async def test_chat_capture_and_budget_share_one_root_config_read(self): + configs = [ + SimpleNamespace( + llm_provider=SimpleNamespace(active_provider="codex"), + openai_codex=OpenAICodexConfig( + model="gpt-5.6-sol", + context_budget_overrides={"gpt-5.6-sol": 800_000}, + ), + ), + SimpleNamespace( + llm_provider=SimpleNamespace(active_provider="codex"), + openai_codex=OpenAICodexConfig( + model="gpt-5.5", + context_budget_overrides={"gpt-5.6-sol": 270_001}, + ), + ), + ] + reads = 0 + + def get_config(): + nonlocal reads + value = configs[min(reads, 1)] + reads += 1 + return value + + client = _codex_client("gpt-5.6-sol") + gateway = LLMGateway( + get_config=get_config, + codex_client=client, + ollama_client=None, + kimi_client=None, + subsystem_guard=None, + auxiliary_llm_client=None, + cost_tracker=None, + sessions=SimpleNamespace(), + reflector=SimpleNamespace(), + ) + from tests.test_typing_resilience import _make_runner, _stub_state + + runner, _saved, _cleared = _make_runner() + runner._get_config = get_config + runner._llm_gateway = gateway + runner._judge_entry_stuck = AsyncMock(return_value=None) + captures = [] + runner._maybe_compress = lambda st, client, config: ( + captures.append((client, config)) or True + ) + done = ("done", False, False, [], False) + runner._call_llm = AsyncMock(return_value=("done", done)) + assert await runner._run_chat_iterations(_stub_state()) == done + assert reads == 1 + assert captures == [(client, configs[0])] + serving = runner._call_llm.await_args.kwargs["serving_identity"] + assert serving.model == "gpt-5.6-sol" + + async def test_chat_soft_and_rescue_share_one_observer_snapshot(self): + from src.llm.errors import LLMRequestError + from src.llm.recovery import RecoveryPolicy + from tests.test_typing_resilience import FakeChannel, _make_runner, _stub_state + + class ChangingObserver: + def __init__(self): + self.calls = 0 + + def active_clamp(self, _model): + self.calls += 1 + return 500_000 if self.calls == 1 else 100_000 + + class Client: + model = "gpt-5.6-sol" + reasoning_effort = "xhigh" + + async def chat_with_tools(self, **_kwargs): + raise LLMRequestError( + "overflow", + provider="codex", + model=self.model, + code="context_length_exceeded", + ) + + config = SimpleNamespace( + llm_provider=SimpleNamespace(active_provider="codex"), + openai_codex=OpenAICodexConfig(), + ) + client = Client() + gateway = LLMGateway( + get_config=lambda: config, + codex_client=client, + ollama_client=None, + kimi_client=None, + subsystem_guard=None, + auxiliary_llm_client=None, + cost_tracker=None, + sessions=SimpleNamespace(), + reflector=SimpleNamespace(), + recovery_policy_source=lambda: RecoveryPolicy(deadline_seconds=1), + ) + runner, _saved, _cleared = _make_runner() + runner._get_config = lambda: config + runner._llm_gateway = gateway + runner._window_observer = ChangingObserver() + runner._judge_entry_stuck = AsyncMock(return_value=None) + runner._get_context_compressor = lambda: None + st = _stub_state(channel=FakeChannel()) + st._trajectory.context_recoveries = [] + st.messages = [ + {"role": "user", "content": "history" * 80_000}, + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": "current"}, + ] + st._boundary_request_start = 1 + st._boundary_envelope_len = 2 + result = await runner._run_chat_iterations(st) + assert result[2] is True + assert runner._window_observer.calls == 1 + # Clamp 500K at 60% utilization yields this frozen ladder. A second + # read's 100K clamp would instead produce a much smaller ladder. + assert st._trajectory.context_recoveries[0]["target_chars"] == 451_500 + + async def test_chat_physical_retries_keep_captured_identity(self): + from src.llm.errors import LLMTransportError + from src.llm.recovery import RecoveryPolicy + from tests.test_typing_resilience import FakeChannel, _make_runner, _stub_state + + calls: list[tuple[object, str | None, str | None]] = [] + configs = [SimpleNamespace(llm_provider=SimpleNamespace(active_provider="codex"))] + + class Client: + model = "gpt-5.6-sol" + reasoning_effort = "xhigh" + + async def chat_with_tools(self, *, model=None, reasoning_effort=None, **_kwargs): + calls.append((self, model, reasoning_effort)) + if len(calls) == 1: + self.model = "gpt-5.5" + self.reasoning_effort = "max" + configs[0] = SimpleNamespace( + llm_provider=SimpleNamespace(active_provider="kimi") + ) + raise LLMTransportError("retry") + return SimpleNamespace(text="ok", tool_calls=[]) + + client = Client() + gateway = LLMGateway( + get_config=lambda: configs[0], + codex_client=client, + ollama_client=None, + kimi_client=None, + subsystem_guard=None, + auxiliary_llm_client=None, + cost_tracker=None, + sessions=SimpleNamespace(), + reflector=SimpleNamespace(), + recovery_policy_source=lambda: RecoveryPolicy( + deadline_seconds=1, + backoff_base=0, + backoff_cap=0, + ), + ) + serving = gateway.capture_serving_identity() + breaker = gateway.capacity_breaker_for(serving.model, provider=serving.provider) + runner, _saved, _cleared = _make_runner() + runner._llm_gateway = gateway + st = _stub_state(channel=FakeChannel()) + kind, response = await runner._call_llm(st, serving_identity=serving) + assert kind == "ok" and response.text == "ok" + assert calls == [ + (client, "gpt-5.6-sol", "xhigh"), + (client, "gpt-5.6-sol", "xhigh"), + ] + assert gateway.capacity_breaker_for("gpt-5.6-sol", provider="codex") is breaker + assert gateway.capacity_breaker_for("gpt-5.5", provider="kimi") is not breaker + + async def test_gateway_call_uses_captured_provider_for_guard_and_client(self): + guards = [] + + class Guard: + def check(self, key): + guards.append(("check", key)) + return None + + def record_success(self, key): + guards.append(("success", key)) + + def record_failure(self, key, error): + guards.append(("failure", key)) + + class Client: + model = "gpt-5.6-sol" + reasoning_effort = "xhigh" + + async def chat_with_tools(self, **_kwargs): + return SimpleNamespace(text="ok", input_tokens=0, output_tokens=0) + + client = Client() + config = SimpleNamespace(llm_provider=SimpleNamespace(active_provider="codex")) + gateway = LLMGateway( + get_config=lambda: config, + codex_client=client, + ollama_client=None, + kimi_client=SimpleNamespace(model="kimi-live", reasoning_effort=None), + subsystem_guard=Guard(), + auxiliary_llm_client=None, + cost_tracker=None, + sessions=SimpleNamespace(), + reflector=SimpleNamespace(), + ) + serving = gateway.capture_serving_identity() + config.llm_provider.active_provider = "kimi" + assert gateway.active_client is gateway.kimi_client + response = await gateway.call_with_tools( + messages=[], system="s", tools=[], serving_identity=serving + ) + assert response.text == "ok" + assert guards == [("check", "llm_codex"), ("success", "llm_codex")] + + async def test_chat_preflight_uses_captured_pair_before_open_breaker(self): + from src.llm.errors import LLMRequestError + from src.llm.recovery import RecoveryPolicy + from tests.test_typing_resilience import FakeChannel, _make_runner, _stub_state + + calls = 0 + + class Client: + model = "gpt-5.5" + reasoning_effort = "xhigh" + + async def chat_with_tools(self, **_kwargs): + nonlocal calls + calls += 1 + return SimpleNamespace(text="wrong", tool_calls=[]) + + client = Client() + gateway = LLMGateway( + get_config=lambda: SimpleNamespace( + llm_provider=SimpleNamespace(active_provider="codex") + ), + codex_client=client, + ollama_client=None, + kimi_client=None, + subsystem_guard=None, + auxiliary_llm_client=None, + cost_tracker=None, + sessions=SimpleNamespace(), + reflector=SimpleNamespace(), + recovery_policy_source=lambda: RecoveryPolicy(deadline_seconds=0.1), + ) + serving = gateway.capture_serving_identity() + breaker = gateway.capacity_breaker_for(serving.model, provider=serving.provider) + while breaker.snapshot()["state"] != "open": + breaker.record_generation_failure() + client.reasoning_effort = "max" # live drift after capture + frozen = type(serving)( + provider=serving.provider, + client=serving.client, + model=serving.model, + reasoning_effort="max", + ) + assert gateway.capacity_breaker_for( + frozen.model, provider=frozen.provider + ) is breaker + runner, _saved, _cleared = _make_runner() + runner._llm_gateway = gateway + st = _stub_state(channel=FakeChannel()) + with pytest.raises(LLMRequestError): + await runner._call_llm(st, serving_identity=frozen) + assert calls == 0 + assert breaker.snapshot()["state"] == "open" + +class TestSingleSnapshotAcrossLoopGeneration: + async def test_loop_soft_and_rescue_share_one_observer_snapshot(self): + from src.discord.llm_gateway import LLMServingIdentity + from src.llm.context_compressor import SurfaceBoundary + from src.llm.errors import LLMRequestError + from tests.test_chat_loop_recovery import _Gateway, _runner + + class ChangingObserver: + def __init__(self): + self.calls = 0 + + def active_clamp(self, _model): + self.calls += 1 + return 500_000 if self.calls == 1 else 100_000 + + class Client(SimpleNamespace): + calls = 0 + + async def chat_with_tools(self, **_kwargs): + self.calls += 1 + if self.calls == 1: + raise LLMRequestError( + "overflow", + provider="codex", + model=self.model, + code="context_length_exceeded", + ) + return SimpleNamespace( + text="ok", + tool_calls=[], + provenance_provider="codex", + provenance_model=self.model, + ) + + client = Client(model="gpt-5.6-sol", reasoning_effort="xhigh") + gw = _Gateway(None) + gw.client = client + gw.codex_client = client + runner = _runner(gw) + observer = ChangingObserver() + runner._window_observer = observer + config = SimpleNamespace(openai_codex=OpenAICodexConfig()) + serving = LLMServingIdentity("codex", client, client.model, client.reasoning_effort) + snapshot = runner._capture_budget_snapshot(serving, config) + st = SimpleNamespace( + messages=[ + {"role": "user", "content": "prior" * 100_000}, + {"role": "assistant", "content": "ok"}, + {"role": "user", "content": "goal"}, + ], + system_prompt="sys", + tools=[], + _boundary=SurfaceBoundary(request_start=2, envelope_len=1), + _char_latch=None, + context_recoveries=[], + _iteration_index=0, + _trajectory=None, + _loop_details=[], + _trace=None, + _loop_id="L", + channel_id_str="c", + prompt="p", + user_id="u", + ) + runner._maybe_compress_loop(st, serving, config, budget_snapshot=snapshot) + kind, _ = await runner._call_loop_llm( + st, + serving_identity=serving, + request_config=config, + budget_snapshot=snapshot, + ) + assert kind == "ok" + assert observer.calls == 1 + assert st.context_recoveries[0]["target_chars"] == 451_500 + +class TestIntegrationAgentGenerationSeams: + async def test_agent_soft_compaction_and_request_share_one_generation_snapshot( + self, monkeypatch + ): + from src.agents.manager import AgentManager + from src.llm.context_budget import resolve_context_budget + + snapshots = [ + resolve_context_budget("gpt-5.6-sol"), + resolve_context_budget("gpt-5.5", observed_clamp=200_000), + ] + reads = 0 + + def plan_provider(): + nonlocal reads + snap = snapshots[min(reads, 1)] + reads += 1 + return { + "provider": "codex", + "client": object(), + "model": snap.canonical_model, + "effort": "xhigh", + "snapshot": snap, + } + + seen_targets = [] + + def fake_soft(messages, **kwargs): + seen_targets.append(kwargs["max_context_chars"]) + return messages, 1 + + monkeypatch.setattr( + "src.llm.context_compressor.compress_tool_context", fake_soft + ) + calls = [] + + async def iteration(messages, system_prompt, tools, *, generation_state): + calls.append(generation_state["plan"]) + if len(calls) == 1: + return { + "text": "", + "tool_calls": [{"id": "1", "name": "noop", "arguments": {}}], + } + return {"text": "done", "tool_calls": []} + + async def tool_executor(*_args, **_kwargs): + return "x" * 500_000 + + manager = AgentManager() + agent_id = manager.spawn( + label="single-snapshot", + goal="g", + channel_id="c", + requester_id="u", + requester_name="user", + iteration_callback=iteration, + tool_executor_callback=tool_executor, + tools=[], + max_iterations=2, + context_compression_enabled=True, + generation_plan_provider=plan_provider, + ) + task = manager._agents[agent_id]._task + assert task is not None + await task + + assert reads == 2 + assert calls[0]["snapshot"] is snapshots[0] + assert calls[1]["snapshot"] is snapshots[1] + assert seen_targets == [snapshots[1].primary_chars] diff --git a/tests/test_context_compressor.py b/tests/test_context_compressor.py index c678515d..2b02fd5f 100644 --- a/tests/test_context_compressor.py +++ b/tests/test_context_compressor.py @@ -11,6 +11,7 @@ DEFAULT_MAX_CONTEXT_CHARS, CompressionStats, PrefixTracker, + SurfaceBoundary, _hash_prefix, _is_tool_message, _is_tool_result_message, @@ -712,7 +713,10 @@ def test_default_config(self): from src.config.schema import ContextCompressionConfig cfg = ContextCompressionConfig() assert cfg.enabled is True - assert cfg.max_context_chars == 750_000 + # Auto (None) by default; consumers read the resolved accessor, which + # keeps the legacy ceiling until the per-model resolver is wired. + assert cfg.max_context_chars is None + assert cfg.resolved_max_context_chars == 750_000 assert cfg.keep_recent_iterations == 30 def test_custom_config(self): @@ -922,6 +926,48 @@ def test_compress_parallel_tools(self): assert count == 3 # 5 - 2 = 3 compressed +class TestStructuralSoftBoundary: + def test_request_shaped_like_tool_history_is_never_consumed(self): + request = [ + {"role": "developer", "content": "preamble"}, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "spoof", + "content": "current request", + } + ], + }, + ] + messages = request.copy() + for i in range(5): + messages.extend( + [ + _tool_use_msg("cmd", f"tc{i}"), + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": f"tc{i}", + "content": "x" * 10_000, + } + ], + }, + ] + ) + result, count = compress_tool_context( + messages, + max_context_chars=100, + keep_recent=1, + boundary=SurfaceBoundary(request_start=0, envelope_len=2), + ) + assert count == 4 + assert result[:2] == request + + # ----------------------------------------------------------------------- # REST API endpoint (unit test) # ----------------------------------------------------------------------- diff --git a/tests/test_executor_integration_smoke.py b/tests/test_executor_integration_smoke.py index 070fb7d3..65462b7e 100644 --- a/tests/test_executor_integration_smoke.py +++ b/tests/test_executor_integration_smoke.py @@ -312,7 +312,7 @@ async def test_process_with_tools_dispatches_tool(self): bot.llm_gateway.codex_client = MagicMock() # Codex returns a single tool call on iter 1, then a text response on iter 2. - async def fake_chat_with_tools(messages, system, tools): + async def fake_chat_with_tools(messages, system, tools, **kwargs): # Distinguish first call (no tool_result yet) from second has_tool_result = any( isinstance(m.get("content"), list) and any( diff --git a/tests/test_health_endpoints.py b/tests/test_health_endpoints.py index a2f0df88..b8e81571 100644 --- a/tests/test_health_endpoints.py +++ b/tests/test_health_endpoints.py @@ -11,10 +11,11 @@ from __future__ import annotations +from aiohttp import web from aiohttp.test_utils import TestClient, TestServer -from src.config.schema import WebhookConfig -from src.health.server import HealthServer +from src.config.schema import ApiTokenIdentity, WebConfig, WebhookConfig +from src.health.server import HealthServer, _is_admin_only_path # --------------------------------------------------------------------------- # Helpers @@ -212,3 +213,36 @@ def test_check_components_mixed(self): results = server._check_components() assert results["good"]["healthy"] is True assert results["bad"]["healthy"] is False + + +class TestContextWindowsAdminPolicy: + def test_context_surface_is_centrally_admin_only(self): + assert _is_admin_only_path("/api/context/windows") + assert _is_admin_only_path("/api/context/windows/clear") + + async def test_get_and_post_reject_user_and_guest_but_allow_admin(self): + web_config = WebConfig( + api_tokens=[ + ApiTokenIdentity(token="admin-token", user_id="a", tier="admin"), + ApiTokenIdentity(token="user-token", user_id="u", tier="user"), + ApiTokenIdentity(token="guest-token", user_id="g", tier="guest"), + ] + ) + server = HealthServer(port=0, webhook_config=WebhookConfig(), web_config=web_config) + server._app.router.add_get( + "/api/context/windows", lambda _r: web.json_response({"ok": True}) + ) + server._app.router.add_post( + "/api/context/windows/clear", + lambda _r: web.json_response({"ok": True}), + ) + async with TestClient(TestServer(server._app)) as client: + for token in ("user-token", "guest-token"): + headers = {"Authorization": f"Bearer {token}"} + assert (await client.get("/api/context/windows", headers=headers)).status == 403 + assert ( + await client.post("/api/context/windows/clear", headers=headers) + ).status == 403 + headers = {"Authorization": "Bearer admin-token"} + assert (await client.get("/api/context/windows", headers=headers)).status == 200 + assert (await client.post("/api/context/windows/clear", headers=headers)).status == 200 diff --git a/tests/test_llm_gateway.py b/tests/test_llm_gateway.py index afe2a830..000fb766 100644 --- a/tests/test_llm_gateway.py +++ b/tests/test_llm_gateway.py @@ -310,6 +310,31 @@ async def test_success_records_cost(self): cost.record.assert_called_once() assert gw.inflight_requests == 0 # decremented in finally + async def test_capacity_failure_marks_only_transient_degradation(self): + from src.llm.errors import LLMCapacityError + + client = SimpleNamespace( + chat_with_tools=AsyncMock(side_effect=LLMCapacityError("full")) + ) + guard = MagicMock() + guard.check.return_value = None + gw = _gw(codex=client, guard=guard) + with pytest.raises(LLMCapacityError): + await gw.call_with_tools(messages=[], system="s", tools=[]) + guard.mark_degraded_transient.assert_called_once_with( + "llm_codex", "full", expires_in=120.0 + ) + guard.record_failure.assert_not_called() + + def test_bypass_success_uses_response_provenance_or_noops(self): + guard = MagicMock() + gw = _gw(codex=object(), guard=guard) + gw.notify_generation_success(None) + guard.record_success.assert_not_called() + gw.notify_generation_success("codex") + guard.record_success.assert_called_once_with("llm_codex") + _gw(codex=object()).notify_generation_success("codex") + async def test_failure_records_and_raises(self): client = SimpleNamespace(chat_with_tools=AsyncMock(side_effect=RuntimeError("api"))) guard = MagicMock() @@ -1184,3 +1209,52 @@ async def test_no_primary_is_reported_without_building(self): result = await gw.reload_auxiliary(plan=plan) assert result["committed"] is False assert "no primary" in result["reason"] + + +class TestServingIdentityFreeze: + def test_capture_reuses_supplied_config_and_fallback_identity(self): + cfg = _cfg("ollama") + codex = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="xhigh") + gw = _gw(cfg, codex=codex, ollama=None) + serving = gw.capture_serving_identity(cfg) + assert serving.provider == "codex" + assert serving.client is codex + assert serving.model == "gpt-5.6-sol" + assert serving.reasoning_effort == "xhigh" + + def test_capture_covers_each_available_provider(self): + ollama = SimpleNamespace(model="qwen") + kimi = SimpleNamespace(model="kimi") + ollama_serving = _gw(_cfg("ollama"), codex=None, ollama=ollama).capture_serving_identity() + kimi_serving = _gw(_cfg("kimi"), codex=None, kimi=kimi).capture_serving_identity() + assert (ollama_serving.provider, ollama_serving.client) == ("ollama", ollama) + assert (kimi_serving.provider, kimi_serving.client) == ("kimi", kimi) + assert ollama_serving.reasoning_effort is None + assert kimi_serving.reasoning_effort is None + + def test_codex_identity_is_provider_fact_not_attribute_collision(self): + from src.discord.llm_gateway import LLMServingIdentity + + collision = SimpleNamespace(model="local", reasoning_effort="decorative") + assert not LLMServingIdentity("ollama", collision, "local", None).is_codex + + async def test_explicit_empty_identity_never_re_resolves_live_provider(self): + from src.discord.llm_gateway import LLMServingIdentity + + client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="xhigh") + gw = _gw(_cfg("codex"), codex=client) + with pytest.raises(RuntimeError, match="No LLM provider"): + await gw.call_with_tools( + messages=[], + system="s", + tools=[], + serving_identity=LLMServingIdentity("codex", None, None, None), + ) + + def test_breaker_defaults_from_one_serving_capture(self): + client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="xhigh") + gw = _gw(_cfg("codex"), codex=client) + assert gw.capacity_breaker_for() is gw.model_breakers.for_model("codex", "gpt-5.6-sol") + assert gw.capacity_breaker_for("explicit", provider="kimi") is gw.model_breakers.for_model( + "kimi", "explicit" + ) diff --git a/tests/test_loop_error_sanitization.py b/tests/test_loop_error_sanitization.py index c7f5163e..b805fcdc 100644 --- a/tests/test_loop_error_sanitization.py +++ b/tests/test_loop_error_sanitization.py @@ -32,7 +32,7 @@ async def test_history_and_channel_posts_carry_no_markup(self, monkeypatch): mgr = al.LoopManager() channel = _FakeChannel() - async def exploding_iteration(prompt, ch, prev_context): + async def exploding_iteration(prompt, ch, prev_context, cancel_event): raise RuntimeError("@everyone edge page") loop_id = mgr.start_loop( diff --git a/tests/test_max_reasoning_effort.py b/tests/test_max_reasoning_effort.py index 3b070a76..2bee9e5d 100644 --- a/tests/test_max_reasoning_effort.py +++ b/tests/test_max_reasoning_effort.py @@ -102,6 +102,7 @@ def test_agent_effort_inheriting_bad_main_model_rejected(self): OpenAICodexConfig( model="gpt-5.5", reasoning_effort="xhigh", + agent_model=None, # explicit inherit (default is now "auto") agent_reasoning_effort="max", ) @@ -112,6 +113,7 @@ def test_agent_model_inheriting_max_effort_rejected(self): model="gpt-5.6-sol", reasoning_effort="max", agent_model="gpt-5.5", + agent_reasoning_effort=None, # explicit inherit (default is now "auto") ) def test_auto_model_axis_exempt(self): @@ -253,7 +255,7 @@ async def _cwt(**kwargs): # must never run runner._llm_gateway = SimpleNamespace( call_with_tools=_cwt, - capacity_breaker_for=lambda model=None: breaker, + capacity_breaker_for=lambda model=None, provider=None: breaker, recovery_policy=lambda: RecoveryPolicy( deadline_seconds=0.3, backoff_base=0.01, backoff_cap=0.02, retry_after_cap=0.05), @@ -282,7 +284,7 @@ async def test_bad_pair_finishes_loop_instead_of_raising(self): runner, _saved, _cleared = _make_runner() registry = ModelBreakerRegistry() runner._llm_gateway = SimpleNamespace( - capacity_breaker_for=lambda model=None: registry.for_model("codex", "m"), + capacity_breaker_for=lambda model=None, provider=None: registry.for_model("codex", "m"), recovery_policy=lambda: RecoveryPolicy( deadline_seconds=0.3, backoff_base=0.01, backoff_cap=0.02, retry_after_cap=0.05), diff --git a/tests/test_native_agents_tasks.py b/tests/test_native_agents_tasks.py index d7688947..9cc8e010 100644 --- a/tests/test_native_agents_tasks.py +++ b/tests/test_native_agents_tasks.py @@ -15,10 +15,15 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch +import pytest + +from src.config.schema import ContextCompressionConfig, OpenAICodexConfig from src.discord.background_task import MAX_STEPS from src.discord.native_tools.agents_tasks import ( AgentTaskDeps, AgentTaskTools, + _capture_agent_generation_plan, + _gateway_serving_for_config, _parse_spawn_overrides, ) from src.llm.model_breaker import ModelBreakerRegistry @@ -34,8 +39,8 @@ def _fake_gateway(client): """ registry = ModelBreakerRegistry() gw = SimpleNamespace(active_client=client) - gw.capacity_breaker_for = lambda model=None: registry.for_model( - "codex", str(model or getattr(client, "model", None) or "unknown") + gw.capacity_breaker_for = lambda model=None, *, provider=None: registry.for_model( + str(provider or "codex"), str(model or getattr(client, "model", None) or "unknown") ) gw.recovery_policy = lambda: RecoveryPolicy( deadline_seconds=0.2, backoff_base=0.01, backoff_cap=0.02, retry_after_cap=0.05 @@ -229,9 +234,9 @@ async def test_start_loop_success(self): async def test_stop_loop(self): t = _tools() - assert "'loop_id' is required" in t._handle_stop_loop({}) - t._loop_manager.stop_loop.return_value = "Loop stopped." - assert "stopped" in t._handle_stop_loop({"loop_id": "L1"}) + assert "'loop_id' is required" in await t._handle_stop_loop({}) + t._loop_manager.stop_loop = AsyncMock(return_value="Loop stopped.") + assert "stopped" in await t._handle_stop_loop({"loop_id": "L1"}) await asyncio.sleep(0) def test_list_loops(self): @@ -308,7 +313,7 @@ async def test_nested_tool_filter_uses_the_parent_depth_snapshot(self): assert {tool["name"] for tool in tools} == {"spawn_agent", "run_command"} async def test_nested_with_parent_and_compressor(self): - cc = SimpleNamespace(max_context_chars=500000, keep_recent_iterations=20) + cc = ContextCompressionConfig(max_context_chars=500000, keep_recent_iterations=20) t = _tools(get_context_compressor=lambda: cc) t._agent_manager.spawn.return_value = "child-1" t._agent_manager._agents = {"parent-1": SimpleNamespace(depth=0)} @@ -385,7 +390,7 @@ async def test_callback_passes_configured_override_and_stamps(self): t = self._spawned_callback("low", client) await t._handle_spawn_agent(_message(), {"label": "w", "goal": "g"}) cb = t._agent_manager.spawn.call_args.kwargs["iteration_callback"] - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["reasoning_effort"] == "low" assert out["reasoning_effort"] == "low" assert out["provider"] == "codex" @@ -396,7 +401,7 @@ async def test_callback_inherits_when_unset(self): t = self._spawned_callback(None, client) await t._handle_spawn_agent(_message(), {"label": "w", "goal": "g"}) cb = t._agent_manager.spawn.call_args.kwargs["iteration_callback"] - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) # Since the round-2 snapshot contract, the inherited effort is pinned # per generation and travels EXPLICITLY on the wire (preflight and the # outbound request must agree) — same effective request as the old @@ -415,12 +420,12 @@ async def test_callback_reads_config_at_call_time(self): t._agent_manager._agents = {} await t._handle_spawn_agent(_message(), {"label": "w", "goal": "g"}) cb = t._agent_manager.spawn.call_args.kwargs["iteration_callback"] - await cb([{"role": "user", "content": "x"}], "sys", []) + await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) # inherit resolves to the client's own effort, snapshotted per # generation (round-2 contract) — explicit on the wire, not None assert client.captured["reasoning_effort"] == "high" cfg.openai_codex.agent_reasoning_effort = "xhigh" # live WebUI change - await cb([{"role": "user", "content": "x"}], "sys", []) + await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["reasoning_effort"] == "xhigh" async def test_callback_stamps_none_for_effortless_provider(self): @@ -446,7 +451,7 @@ async def chat_with_tools(self, **kw): t = self._spawned_callback("low", client) await t._handle_spawn_agent(_message(), {"label": "w", "goal": "g"}) cb = t._agent_manager.spawn.call_args.kwargs["iteration_callback"] - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert out["reasoning_effort"] is None assert out["provider"] == "ollama" @@ -464,7 +469,7 @@ async def test_loop_spawn_callback_passes_effort(self): await t._handle_spawn_loop_agents( _message(), {"loop_id": "L1", "tasks": [{"label": "x", "goal": "g"}]}) cb = t._loop_agent_bridge.spawn_agents_for_loop.call_args.kwargs["iteration_callback"] - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["reasoning_effort"] == "medium" assert out["reasoning_effort"] == "medium" @@ -494,7 +499,7 @@ async def test_override_passed_and_stamped(self): agent_model="gpt-5.6-luna", model="gpt-5.6-sol"), client) cb = await self._callback(t) - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-luna" assert out["model"] == "gpt-5.6-luna" @@ -508,7 +513,7 @@ async def test_inherit_passes_resolved_chat_model(self): agent_model=None, model="gpt-5.6-sol"), client) cb = await self._callback(t) - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-sol" assert out["model"] == "gpt-5.6-sol" @@ -518,7 +523,7 @@ async def test_whitespace_override_means_inherit(self): agent_model=" ", model="gpt-5.6-sol"), client) cb = await self._callback(t) - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-sol" assert out["model"] == "gpt-5.6-sol" @@ -530,10 +535,10 @@ async def test_live_config_change_tracks_per_iteration(self): agent_model=None, model="gpt-5.6-sol") t = self._spawned(cfg_codex, client) cb = await self._callback(t) - out1 = await cb([{"role": "user", "content": "x"}], "sys", []) + out1 = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-sol" == out1["model"] cfg_codex.agent_model = "gpt-5.6-luna" # live WebUI change - out2 = await cb([{"role": "user", "content": "x"}], "sys", []) + out2 = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-luna" == out2["model"] async def test_non_codex_provider_stamps_actual_model(self): @@ -561,7 +566,7 @@ async def chat_with_tools(self, **kw): agent_model="gpt-5.6-luna", model="gpt-5.6-sol"), client) cb = await self._callback(t) - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] is None # override not forwarded assert out["model"] == "qwen3" @@ -590,7 +595,7 @@ async def chat_with_tools(self, **kw): agent_model="gpt-5.6-luna", model="gpt-5.6-sol"), client) cb = await self._callback(t) - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-luna" # resolver → request assert out["model"] == "proof-from-response" # response → stamp assert out["reasoning_effort"] == "low" @@ -616,7 +621,7 @@ async def chat_with_tools(self, **kw): agent_model="gpt-5.6-luna", model="gpt-5.6-sol"), client) cb = await self._callback(t) - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert out["provider"] == "" assert out["model"] == "" assert out["reasoning_effort"] is None @@ -636,7 +641,7 @@ async def test_loop_spawn_callback_same_treatment(self): await t._handle_spawn_loop_agents( _message(), {"loop_id": "L1", "tasks": [{"label": "x", "goal": "g"}]}) cb = t._loop_agent_bridge.spawn_agents_for_loop.call_args.kwargs["iteration_callback"] - out = await cb([{"role": "user", "content": "x"}], "sys", []) + out = await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-luna" assert out["model"] == "gpt-5.6-luna" @@ -697,7 +702,7 @@ async def test_spawn_override_wins_over_config(self): assert kwargs["reasoning_effort_override"] == "low" # and the iteration callback ASKS the client for luna@low, not sol@medium cb = kwargs["iteration_callback"] - await cb([{"role": "user", "content": "x"}], "sys", []) + await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-luna" assert client.captured["reasoning_effort"] == "low" @@ -714,8 +719,8 @@ async def test_no_override_inherits_config(self): assert kwargs["model_override"] is None assert kwargs["reasoning_effort_override"] is None cb = kwargs["iteration_callback"] - await cb([{"role": "user", "content": "x"}], "sys", []) - assert client.captured["model"] == "gpt-5.6-terra" # agent_model + await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) + assert client.captured["model"] == "gpt-5.6-terra" # agent_model assert client.captured["reasoning_effort"] == "high" async def test_invalid_effort_rejects_without_spawning(self): @@ -754,7 +759,7 @@ async def test_loop_per_task_overrides_and_batch_rejection(self): factory = t._loop_agent_bridge.spawn_agents_for_loop.call_args.kwargs[ "iteration_callback_factory"] cb = factory("gpt-5.6-luna", None) - await cb([{"role": "user", "content": "x"}], "sys", []) + await cb([{"role": "user", "content": "x"}], "sys", [], generation_state={}) assert client.captured["model"] == "gpt-5.6-luna" # one bad effort rejects the WHOLE batch — nothing spawns t._loop_agent_bridge.spawn_agents_for_loop.reset_mock() @@ -1014,8 +1019,6 @@ class TestAgentGeneratePreflight: must not move the breaker's failure count.""" async def test_bad_pair_fails_fast_with_open_breaker(self): - import pytest - from src.llm.errors import LLMRequestError client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="medium") @@ -1039,11 +1042,8 @@ async def test_bad_pair_fails_fast_with_open_breaker(self): assert breaker.snapshot()["failed_generations"] == failures_before assert breaker.snapshot()["state"] == "open" - async def test_inherited_client_effort_pair_fails_fast(self): - """agent_effort None inherits the client's live effort at preflight — - the drift shape: fixed gpt-5.5 model override + live effort now max.""" - import pytest - + async def test_resolved_client_effort_pair_fails_fast(self): + """The plan's resolved effort is validated at preflight for its fixed model.""" from src.llm.errors import LLMRequestError client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="max") @@ -1051,7 +1051,7 @@ async def test_inherited_client_effort_pair_fails_fast(self): with pytest.raises(LLMRequestError): await t._agent_generate( client, messages=[], sys_prompt="s", tool_defs=[], - agent_effort=None, resolved_model="gpt-5.5", + agent_effort="max", resolved_model="gpt-5.5", ) @@ -1073,6 +1073,42 @@ async def test_kimi_shaped_client_named_gpt55_spawns_under_max_config(self): assert "spawned" in out +class TestResolvedAgentEffortBoundary: + async def test_codex_generation_rejects_unresolved_effort_sentinel(self): + client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="xhigh") + t = _tools(llm_gateway=_fake_gateway(client)) + with pytest.raises(ValueError, match="unresolved reasoning effort"): + await t._agent_generate( + client, + messages=[], + sys_prompt="s", + tool_defs=[], + agent_effort=None, + resolved_model="gpt-5.6-sol", + ) + + async def test_effortless_provider_may_carry_none(self): + client = SimpleNamespace( + model="local", + chat_with_tools=AsyncMock( + return_value=SimpleNamespace( + text="ok", tool_calls=[], provenance_provider="ollama" + ) + ), + ) + t = _tools(llm_gateway=_fake_gateway(client)) + response = await t._agent_generate( + client, + messages=[], + sys_prompt="s", + tool_defs=[], + agent_effort=None, + resolved_model=None, + ) + assert response.text == "ok" + assert client.chat_with_tools.await_args.kwargs["reasoning_effort"] is None + + class TestAgentEffortSnapshot: """Review round 2 (High): the inherited agent effort is snapshotted ONCE per generation — preflight approves the SAME immutable value every @@ -1080,6 +1116,47 @@ class TestAgentEffortSnapshot: open-breaker or transport-retry wait) must not rewrite the outbound request; it reaches the agent on its next iteration.""" + async def test_plan_capture_drives_attempt_after_in_place_client_mutation(self): + from src.config.schema import OpenAICodexConfig + from src.discord.native_tools.agents_tasks import _capture_agent_generation_plan + + calls = [] + + class _Client: + model = "gpt-5.6-sol" + reasoning_effort = "xhigh" + + async def chat_with_tools(self, *, reasoning_effort=None, model=None, **_kw): + calls.append((reasoning_effort, model)) + return SimpleNamespace( + text="ok", tool_calls=[], provenance_provider="codex" + ) + + client = _Client() + cfg = SimpleNamespace( + openai_codex=OpenAICodexConfig( + model="gpt-5.6-sol", agent_reasoning_effort=None + ) + ) + plan = _capture_agent_generation_plan( + lambda: cfg, + lambda _cfg: client, + lambda: ContextCompressionConfig(), + model_override=None, + effort_override=None, + ) + client.reasoning_effort = "max" + t = _tools(llm_gateway=_fake_gateway(client)) + await t._agent_generate( + plan["client"], + messages=[], + sys_prompt="s", + tool_defs=[], + agent_effort=plan["effort"], + resolved_model=plan["model"], + ) + assert calls == [("xhigh", "gpt-5.6-sol")] + async def test_attempts_carry_the_snapshot_across_retries(self): from src.llm.errors import LLMTransportError @@ -1102,9 +1179,143 @@ async def chat_with_tools(self, *, reasoning_effort=None, model=None, **kw): t = _tools(llm_gateway=_fake_gateway(client)) resp = await t._agent_generate( client, messages=[], sys_prompt="s", tool_defs=[], - agent_effort=None, resolved_model="gpt-5.5", + agent_effort="xhigh", resolved_model="gpt-5.5", ) # both attempts carried the PRE-CHANGE snapshot, never None and never # the mid-generation "max" (which would 400 against gpt-5.5) assert calls == ["xhigh", "xhigh"] assert resp.text == "ok" + +class TestFrozenGenerationIdentity: + """PR #273 round-1 blocker #1 pin: client/model/effort/budget come from + ONE capture and stay fixed across rescue retries of the same generation; + a live reload or client swap reaches only the NEXT generation.""" + + async def test_rescue_retry_reuses_the_first_attempt_plan(self): + from src.config.schema import OpenAICodexConfig + + cfg = _cfg() + cfg.openai_codex = OpenAICodexConfig( + model="gpt-5.6-sol", agent_model=None, agent_reasoning_effort=None, + ) + sol_client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="xhigh") + gateway = SimpleNamespace(active_client=sol_client) + t = _tools(get_config=lambda: cfg, llm_gateway=gateway) + t._agent_manager.spawn = MagicMock(return_value="agent-1") + t._agent_generate = AsyncMock( + return_value=SimpleNamespace( + text="ok", tool_calls=[], stop_reason="end_turn", + provenance_provider="codex", provenance_model="gpt-5.6-sol", + provenance_reasoning_effort="xhigh", + ) + ) + await t._handle_spawn_agent(_message(), {"label": "x", "goal": "g"}) + cb = t._agent_manager.spawn.call_args.kwargs["iteration_callback"] + + generation_state: dict = {} + await cb([], "sys", [], generation_state=generation_state) + plan = generation_state["plan"] + assert plan["client"] is sol_client + assert plan["model"] == "gpt-5.6-sol" + assert plan["snapshot"].primary_chars == 1_277_400 + + # Mid-generation reload: live config and the active client both flip + # to 5.5. The rescue retry MUST still use the frozen sol identity. + cfg.openai_codex.model = "gpt-5.5" + gateway.active_client = SimpleNamespace( + model="gpt-5.5", reasoning_effort="xhigh" + ) + await cb([], "sys", [], generation_state=generation_state) + first = t._agent_generate.await_args_list[0] + second = t._agent_generate.await_args_list[1] + assert second.args[0] is sol_client # same client object + assert second.kwargs["resolved_model"] == first.kwargs["resolved_model"] + assert generation_state["plan"] is plan # nothing re-resolved + + # A FRESH generation state (the next iteration) sees the new world. + fresh: dict = {} + await cb([], "sys", [], generation_state=fresh) + assert fresh["plan"]["model"] == "gpt-5.5" + assert fresh["plan"]["snapshot"].primary_chars == 570_002 + +class TestIntegrationFrozenProviderBreaker: + async def test_rescue_after_provider_switch_uses_frozen_provider_breaker(self): + registry = ModelBreakerRegistry() + + class _Gateway: + def __init__(self): + self.live_provider = "codex" + self.active_client = SimpleNamespace( + model="gpt-5.6-sol", reasoning_effort="xhigh" + ) + self.recovery_policy = lambda: RecoveryPolicy( + deadline_seconds=0.2, + backoff_base=0.01, + backoff_cap=0.02, + retry_after_cap=0.05, + ) + + def capture_serving_identity(self, _config): + return SimpleNamespace( + provider=self.live_provider, + client=self.active_client, + model=self.active_client.model, + reasoning_effort=getattr(self.active_client, "reasoning_effort", None), + is_codex=self.live_provider == "codex", + ) + + def capacity_breaker_for(self, model=None, *, provider=None): + return registry.for_model(provider or self.live_provider, model or "unknown") + + def notify_generation_success(self, _response): + return None + + calls = [] + + class _Client: + model = "gpt-5.6-sol" + reasoning_effort = "xhigh" + + async def chat_with_tools(self, **_kwargs): + calls.append("called") + return SimpleNamespace( + text="ok", tool_calls=[], provenance_provider="codex" + ) + + gateway = _Gateway() + gateway.active_client = _Client() + cfg = SimpleNamespace( + openai_codex=OpenAICodexConfig( + model="gpt-5.6-sol", agent_reasoning_effort=None + ) + ) + plan = _capture_agent_generation_plan( + lambda: cfg, + lambda root: _gateway_serving_for_config(gateway, root), + lambda: ContextCompressionConfig(), + model_override=None, + effort_override=None, + ) + assert plan["provider"] == "codex" + + # Open only the newly-live Ollama breaker, then switch providers. A + # rescue governed by live identity would now fail before reaching the + # frozen Codex client. + live = registry.for_model("ollama", "gpt-5.6-sol") + while live.snapshot()["state"] != "open": + live.record_generation_failure() + gateway.live_provider = "ollama" + + tools = _tools(llm_gateway=gateway) + response = await tools._agent_generate( + plan["client"], + messages=[], + sys_prompt="s", + tool_defs=[], + agent_effort=plan["effort"], + resolved_model=plan["model"], + provider=plan["provider"], + ) + assert response.text == "ok" + assert calls == ["called"] + assert registry.for_model("codex", "gpt-5.6-sol").snapshot()["state"] == "closed" diff --git a/tests/test_openai_codex_client.py b/tests/test_openai_codex_client.py index c27b624f..3d2c05f7 100644 --- a/tests/test_openai_codex_client.py +++ b/tests/test_openai_codex_client.py @@ -42,6 +42,20 @@ def _client(auth=None): return CodexChatClient(auth=auth or _BareAuth(), model="gpt-5.5") +class TestEligibleAccountKeys: + def test_bare_auth_key_snapshot(self, monkeypatch): + monkeypatch.setattr( + "src.llm.account_key.opaque_account_key", lambda account_id: f"key-{account_id}" + ) + assert _client().eligible_account_keys_snapshot() == frozenset({"key-acct"}) + + def test_key_derivation_failure_is_conservative(self, monkeypatch): + monkeypatch.setattr( + "src.llm.account_key.opaque_account_key", lambda _account_id: None + ) + assert _client().eligible_account_keys_snapshot() == frozenset() + + class TestConvertMessages: def test_plain_string_roles(self): c = _client() diff --git a/tests/test_provider_truth.py b/tests/test_provider_truth.py new file mode 100644 index 00000000..f14d93b3 --- /dev/null +++ b/tests/test_provider_truth.py @@ -0,0 +1,699 @@ +"""Provider truth plumbing (context-budget campaign phase 2). + +Pins the evidence contract: ``server_input_tokens`` parsed STRICTLY from +the server's own usage echoes (success and failure events; never the client +estimate), and an opaque installation-local account key stamped on both the +successful response and the structural overflow exception — per attempt, +never a raw identifier. Key trouble degrades evidence, never requests. +""" + +from __future__ import annotations + +import json +import logging +import stat + +import pytest + +from src.llm.account_key import _key_cache, opaque_account_key +from src.llm.errors import LLMRequestError +from src.llm.openai_codex import ( + _server_input_tokens_from_usage, + _stream_error_from_event, +) +from src.llm.types import LLMResponse +from tests.test_codex_reliability import ( + FakeResp, + FakeSession, + FakeSingleAuth, + _async_return, + _client, + _sse, +) + + +@pytest.fixture(autouse=True) +def _fresh_key_cache(): + _key_cache.clear() + yield + _key_cache.clear() + + +# --------------------------------------------------------------------------- +# Strict usage parsing +# --------------------------------------------------------------------------- +class TestStrictUsageParse: + @pytest.mark.parametrize( + "usage", + [ + None, + "not a dict", + {}, + {"input_tokens": None}, + {"input_tokens": "12345"}, + {"input_tokens": 12.5}, + {"input_tokens": -1}, + {"input_tokens": True}, # bool is not evidence + {"output_tokens": 5}, + ], + ) + def test_rejects_everything_not_a_nonnegative_int(self, usage): + assert _server_input_tokens_from_usage(usage) is None + + def test_accepts_exact_ints(self): + assert _server_input_tokens_from_usage({"input_tokens": 0}) == 0 + assert _server_input_tokens_from_usage({"input_tokens": 921_601}) == 921_601 + + +# --------------------------------------------------------------------------- +# Opaque account key +# --------------------------------------------------------------------------- +class TestOpaqueAccountKey: + def test_deterministic_and_stable_across_cache_reset(self, tmp_path): + key_path = tmp_path / "k.secret" + first = opaque_account_key("acct-a", key_path=key_path) + _key_cache.clear() # simulate a process restart: re-read from disk + second = opaque_account_key("acct-a", key_path=key_path) + assert first is not None and first == second + + def test_distinct_accounts_distinct_keys(self, tmp_path): + key_path = tmp_path / "k.secret" + a = opaque_account_key("acct-a", key_path=key_path) + b = opaque_account_key("acct-b", key_path=key_path) + assert a != b + + def test_never_reversible_or_raw(self, tmp_path): + key_path = tmp_path / "k.secret" + account = "user-account-uuid-1234" + key = opaque_account_key(account, key_path=key_path) + assert key is not None + assert account not in key + assert key != account + + def test_installations_never_correlate(self, tmp_path): + a = opaque_account_key("acct-a", key_path=tmp_path / "one.secret") + b = opaque_account_key("acct-a", key_path=tmp_path / "two.secret") + assert a != b + + def test_missing_identity_disqualifies(self, tmp_path): + key_path = tmp_path / "k.secret" + assert opaque_account_key(None, key_path=key_path) is None + assert opaque_account_key("", key_path=key_path) is None + assert opaque_account_key(" ", key_path=key_path) is None + assert not key_path.exists() # no identity ⇒ no key material created + + def test_key_file_created_0600(self, tmp_path): + key_path = tmp_path / "k.secret" + opaque_account_key("acct-a", key_path=key_path) + assert stat.S_IMODE(key_path.stat().st_mode) == 0o600 + + def test_unwritable_directory_degrades_to_none(self, tmp_path, caplog): + blocked = tmp_path / "data" + blocked.write_text("not a directory") + with caplog.at_level(logging.WARNING, logger="odin.llm"): + key = opaque_account_key("acct-a", key_path=blocked / "k.secret") + assert key is None + assert any("account key" in r.getMessage().lower() for r in caplog.records) + + def test_publication_failure_cleans_temp_and_degrades(self, tmp_path, caplog, monkeypatch): + """A failure after the temp file exists must not leave debris or a key.""" + + def failing_link(src, dst): + raise OSError("simulated link failure") + + monkeypatch.setattr("src.llm.account_key.os.link", failing_link) + key_path = tmp_path / "k.secret" + with caplog.at_level(logging.WARNING, logger="odin.llm"): + key = opaque_account_key("acct-a", key_path=key_path) + assert key is None + assert not key_path.exists() + assert list(tmp_path.glob(".k.secret.*")) == [] # temp cleaned up + assert any("Could not create account key" in r.getMessage() for r in caplog.records) + + def test_weak_material_refused_never_overwritten(self, tmp_path, caplog): + key_path = tmp_path / "k.secret" + key_path.write_bytes(b"short") + key_path.chmod(0o600) + with caplog.at_level(logging.WARNING, logger="odin.llm"): + key = opaque_account_key("acct-a", key_path=key_path) + assert key is None + # Replacing weak material would decorrelate all prior evidence. + assert key_path.read_bytes() == b"short" + + +class TestPersistedKeyContract: + """Round-1 blocker #3: only the exact generated shape is trusted.""" + + def _valid_key(self, tmp_path): + key_path = tmp_path / "k.secret" + first = opaque_account_key("acct-a", key_path=key_path) + assert first is not None + _key_cache.clear() + return key_path, first + + def test_exact_32_bytes_accepted_but_not_33(self, tmp_path): + key_path, first = self._valid_key(tmp_path) + assert opaque_account_key("acct-a", key_path=key_path) == first + _key_cache.clear() + key_path.write_bytes(key_path.read_bytes() + b"x") + key_path.chmod(0o600) + assert opaque_account_key("acct-a", key_path=key_path) is None + assert len(key_path.read_bytes()) == 33 # refused, never repaired + + def test_group_readable_material_fails_closed_and_stays(self, tmp_path, caplog): + key_path, _ = self._valid_key(tmp_path) + key_path.chmod(0o644) + with caplog.at_level(logging.WARNING, logger="odin.llm"): + assert opaque_account_key("acct-a", key_path=key_path) is None + assert stat.S_IMODE(key_path.stat().st_mode) == 0o644 # untouched + assert any("not 0600" in r.getMessage() for r in caplog.records) + + def test_symlink_final_component_refused(self, tmp_path): + real = tmp_path / "real.key" + opaque_account_key("acct-a", key_path=real) + _key_cache.clear() + link = tmp_path / "k.secret" + link.symlink_to(real) + assert opaque_account_key("acct-a", key_path=link) is None + assert link.is_symlink() # never replaced or followed + + def test_foreign_owner_refused(self, tmp_path, monkeypatch, caplog): + import os as _os + + key_path, _ = self._valid_key(tmp_path) + real_uid = _os.getuid() + monkeypatch.setattr("src.llm.account_key.os.getuid", lambda: real_uid + 1) + with caplog.at_level(logging.WARNING, logger="odin.llm"): + assert opaque_account_key("acct-a", key_path=key_path) is None + assert any("not owned" in r.getMessage() for r in caplog.records) + + def test_parent_directory_fsynced_on_establish(self, tmp_path, monkeypatch): + synced: list = [] + monkeypatch.setattr( + "src.llm.account_key._fsync_parent", lambda p: synced.append(p) + ) + key_path = tmp_path / "k.secret" + assert opaque_account_key("acct-a", key_path=key_path) is not None + assert synced == [key_path] + + def test_fsync_parent_syncs_a_directory_fd(self, tmp_path, monkeypatch): + import os as _os + + from src.llm.account_key import _fsync_parent + + seen: list[int] = [] + real_fsync = _os.fsync + + def recording_fsync(fd): + seen.append(fd) + return real_fsync(fd) + + monkeypatch.setattr("src.llm.account_key.os.fsync", recording_fsync) + (tmp_path / "k.secret").write_bytes(b"x") + _fsync_parent(tmp_path / "k.secret") + assert len(seen) == 1 + + +def _fifo_worker(path_str, queue): + from src.llm.account_key import opaque_account_key as derive + + queue.put(derive("acct-a", key_path=path_str)) + + +class TestKeyEdgeBranches: + def test_directory_at_key_path_refused(self, tmp_path, caplog): + key_path = tmp_path / "k.secret" + key_path.mkdir() + with caplog.at_level(logging.WARNING, logger="odin.llm"): + assert opaque_account_key("acct-a", key_path=key_path) is None + assert any("not a regular file" in r.getMessage() for r in caplog.records) + + def test_fifo_refused_promptly_and_untouched(self, tmp_path): + """The open itself must not block before fstat can reject a FIFO.""" + import multiprocessing as mp + import os + + key_path = tmp_path / "k.secret" + os.mkfifo(key_path, 0o600) + ctx = mp.get_context("fork") + queue = ctx.Queue() + worker = ctx.Process( + target=_fifo_worker, args=(str(key_path), queue) + ) + worker.start() + worker.join(timeout=2) + if worker.is_alive(): + worker.terminate() + worker.join(timeout=5) + pytest.fail("account-key read blocked while opening a FIFO") + assert worker.exitcode == 0 + assert queue.get(timeout=2) is None + assert stat.S_ISFIFO(key_path.lstat().st_mode) + + def test_fchmod_failure_closes_owned_descriptor( + self, tmp_path, monkeypatch + ): + import errno + import os + import tempfile + + captured: list[int] = [] + real_mkstemp = tempfile.mkstemp + + def recording_mkstemp(*args, **kwargs): + fd, name = real_mkstemp(*args, **kwargs) + captured.append(fd) + return fd, name + + def failing_fchmod(fd, mode): + raise OSError("simulated fchmod failure") + + monkeypatch.setattr( + "src.llm.account_key.tempfile.mkstemp", recording_mkstemp + ) + monkeypatch.setattr("src.llm.account_key.os.fchmod", failing_fchmod) + assert opaque_account_key( + "acct-a", key_path=tmp_path / "k.secret" + ) is None + assert len(captured) == 1 + with pytest.raises(OSError) as excinfo: + os.fstat(captured[0]) + assert excinfo.value.errno == errno.EBADF + assert list(tmp_path.glob(".k.secret.*")) == [] + + def test_fdopen_failure_closes_owned_descriptor( + self, tmp_path, monkeypatch + ): + import errno + import os + import tempfile + + captured: list[int] = [] + real_mkstemp = tempfile.mkstemp + + def recording_mkstemp(*args, **kwargs): + fd, name = real_mkstemp(*args, **kwargs) + captured.append(fd) + return fd, name + + def failing_fdopen(fd, mode): + raise OSError("simulated fdopen failure") + + monkeypatch.setattr( + "src.llm.account_key.tempfile.mkstemp", recording_mkstemp + ) + monkeypatch.setattr("src.llm.account_key.os.fdopen", failing_fdopen) + assert opaque_account_key( + "acct-a", key_path=tmp_path / "k.secret" + ) is None + assert len(captured) == 1 + with pytest.raises(OSError) as excinfo: + os.fstat(captured[0]) + assert excinfo.value.errno == errno.EBADF + assert list(tmp_path.glob(".k.secret.*")) == [] + + def test_oversized_file_refused_before_short_read( + self, tmp_path, monkeypatch + ): + key_path = tmp_path / "k.secret" + original = b"x" * 33 + key_path.write_bytes(original) + key_path.chmod(0o600) + reads: list[tuple[int, int]] = [] + + def deceptive_short_read(fd, count): + reads.append((fd, count)) + return b"x" * 32 + + monkeypatch.setattr( + "src.llm.account_key.os.read", deceptive_short_read + ) + assert opaque_account_key("acct-a", key_path=key_path) is None + assert reads == [] # fstat size rejects it before any read + assert key_path.read_bytes() == original + + def test_short_read_of_exact_size_file_is_refused( + self, tmp_path, monkeypatch + ): + key_path = tmp_path / "k.secret" + original = b"x" * 32 + key_path.write_bytes(original) + key_path.chmod(0o600) + + monkeypatch.setattr( + "src.llm.account_key.os.read", lambda fd, count: b"x" * 31 + ) + assert opaque_account_key("acct-a", key_path=key_path) is None + assert key_path.read_bytes() == original + + def test_enoent_then_publish_race_adopts_winner( + self, tmp_path, monkeypatch + ): + """A witnessed miss goes straight to fail-if-exists publication. + + A winner appearing between ENOENT and os.link must be adopted; no + exists() snapshot may suppress or redirect the protocol. + """ + import hmac + import os + from hashlib import sha256 + + key_path = tmp_path / "k.secret" + winner_material = b"w" * 32 + real_open = os.open + real_link = os.link + first_key_open = True + publication_attempts: list[tuple[object, object]] = [] + + def missing_then_winner(path, flags, *args, **kwargs): + nonlocal first_key_open + if first_key_open and path == key_path: + first_key_open = False + # Model another process publishing immediately after this + # open observed ENOENT, but before our caller can perform any + # non-atomic exists() recheck. + key_path.write_bytes(winner_material) + key_path.chmod(0o600) + raise FileNotFoundError(str(key_path)) + return real_open(path, flags, *args, **kwargs) + + def recording_link(src, dst): + publication_attempts.append((src, dst)) + return real_link(src, dst) + + monkeypatch.setattr( + "src.llm.account_key.os.open", missing_then_winner + ) + monkeypatch.setattr("src.llm.account_key.os.link", recording_link) + result = opaque_account_key("acct-a", key_path=key_path) + expected = hmac.new( + winner_material, b"acct-a", sha256 + ).hexdigest()[:32] + assert result == expected + assert len(publication_attempts) == 1 + assert publication_attempts[0][1] == key_path + assert key_path.read_bytes() == winner_material + + def test_enoent_race_invalid_winner_is_refused_untouched( + self, tmp_path, monkeypatch + ): + """EEXIST adoption applies the same strict shape validation.""" + import os + + key_path = tmp_path / "k.secret" + invalid_winner = b"invalid-race-winner" + real_open = os.open + real_link = os.link + first_key_open = True + publication_attempts: list[tuple[object, object]] = [] + + def missing_then_invalid_winner(path, flags, *args, **kwargs): + nonlocal first_key_open + if first_key_open and path == key_path: + first_key_open = False + key_path.write_bytes(invalid_winner) + key_path.chmod(0o600) + raise FileNotFoundError(str(key_path)) + return real_open(path, flags, *args, **kwargs) + + def recording_link(src, dst): + publication_attempts.append((src, dst)) + return real_link(src, dst) + + monkeypatch.setattr( + "src.llm.account_key.os.open", missing_then_invalid_winner + ) + monkeypatch.setattr("src.llm.account_key.os.link", recording_link) + assert opaque_account_key("acct-a", key_path=key_path) is None + assert len(publication_attempts) == 1 + assert publication_attempts[0][1] == key_path + assert key_path.read_bytes() == invalid_winner + assert list(tmp_path.glob(".k.secret.*")) == [] + + def test_read_oserror_degrades(self, tmp_path, monkeypatch, caplog): + key_path = tmp_path / "k.secret" + opaque_account_key("acct-a", key_path=key_path) + _key_cache.clear() + + def failing_read(fd, n): + raise OSError("simulated read failure") + + monkeypatch.setattr("src.llm.account_key.os.read", failing_read) + with caplog.at_level(logging.WARNING, logger="odin.llm"): + assert opaque_account_key("acct-a", key_path=key_path) is None + assert any("Could not read account key" in r.getMessage() for r in caplog.records) + + def test_link_race_loser_converges_deterministically(self, tmp_path, monkeypatch): + """Single-process pin of the loser branch: publication finds a winner + already in place and adopts the winner's material.""" + key_path = tmp_path / "k.secret" + winner = opaque_account_key("acct-a", key_path=key_path) + winner_material = key_path.read_bytes() + key_path.unlink() + _key_cache.clear() + + def racing_link(src, dst): + # The "other process" wins between our temp write and publication. + key_path.write_bytes(winner_material) + key_path.chmod(0o600) + raise FileExistsError("winner already published") + + monkeypatch.setattr("src.llm.account_key.os.link", racing_link) + assert opaque_account_key("acct-a", key_path=key_path) == winner + assert key_path.read_bytes() == winner_material # winner untouched + + def test_totality_net_catches_derivation_failure(self, tmp_path, monkeypatch, caplog): + def exploding_hmac(*args, **kwargs): + raise RuntimeError("simulated derivation failure") + + monkeypatch.setattr("src.llm.account_key.hmac.new", exploding_hmac) + with caplog.at_level(logging.ERROR, logger="odin.llm"): + assert opaque_account_key("acct-a", key_path=tmp_path / "k.secret") is None + assert any("evidence forfeited" in r.getMessage() for r in caplog.records) + + +class TestIdentityNormalization: + """Round-1 blocker #2: identities that cannot be UTF-8 encoded disqualify.""" + + def test_unpaired_surrogate_returns_none_without_raising(self, tmp_path, caplog): + with caplog.at_level(logging.WARNING, logger="odin.llm"): + key = opaque_account_key( + "acct-\ud800", key_path=tmp_path / "k.secret" + ) + assert key is None + assert not (tmp_path / "k.secret").exists() # no material minted + assert any("not UTF-8" in r.getMessage() for r in caplog.records) + + +def _concurrent_worker(path_str, barrier, queue): + from src.llm.account_key import opaque_account_key as derive + + barrier.wait() + queue.put(derive("acct-a", key_path=path_str)) + + +class TestConcurrentFirstUse: + """Round-1 blocker #1: the exclusive-winner protocol converges.""" + + def test_eight_processes_one_key(self, tmp_path): + import multiprocessing as mp + + ctx = mp.get_context("fork") + barrier = ctx.Barrier(8) + queue = ctx.Queue() + key_path = tmp_path / "k.secret" + workers = [ + ctx.Process( + target=_concurrent_worker, args=(str(key_path), barrier, queue) + ) + for _ in range(8) + ] + for worker in workers: + worker.start() + keys = [queue.get(timeout=30) for _ in range(8)] + for worker in workers: + worker.join(timeout=30) + assert worker.exitcode == 0 + assert None not in keys + assert len(set(keys)) == 1 + # A fresh process (simulated: cold cache) reads the same winner. + _key_cache.clear() + assert opaque_account_key("acct-a", key_path=key_path) == keys[0] + assert len(key_path.read_bytes()) == 32 + assert stat.S_IMODE(key_path.stat().st_mode) == 0o600 + # No stray temp files survive the race. + assert list(tmp_path.glob(".k.secret.*")) == [] + + +# --------------------------------------------------------------------------- +# Stream-event evidence +# --------------------------------------------------------------------------- +class TestFailureEventUsage: + def test_failure_event_usage_parsed_when_present(self): + exc = _stream_error_from_event( + "response.failed", + { + "response": { + "error": {"type": "invalid_request_error", "code": "context_length_exceeded"}, + "usage": {"input_tokens": 372_101}, + } + }, + ) + assert exc.server_input_tokens == 372_101 + + def test_failure_event_without_usage_is_none(self): + exc = _stream_error_from_event( + "error", + {"error": {"type": "invalid_request_error", "code": "context_length_exceeded"}}, + ) + assert exc.server_input_tokens is None + + def test_malformed_failure_usage_is_none(self): + exc = _stream_error_from_event( + "response.failed", + { + "response": { + "error": {"code": "context_length_exceeded"}, + "usage": {"input_tokens": "372101"}, + } + }, + ) + assert exc.server_input_tokens is None + + +class TestCompletedEventUsage: + async def test_completed_usage_stamped_on_response(self): + client = _client() + resp = FakeResp(200, sse_lines=_sse([ + {"type": "response.output_text.delta", "delta": "hello"}, + {"type": "response.completed", + "response": {"output": [], "usage": {"input_tokens": 917_506}}}, + ])) + result = await client._read_tool_stream(resp) + assert result.server_input_tokens == 917_506 + + async def test_absent_usage_stays_none_and_estimate_untouched(self): + client = _client() + resp = FakeResp(200, sse_lines=_sse([ + {"type": "response.output_text.delta", "delta": "hello"}, + {"type": "response.completed", "response": {"output": []}}, + ])) + result = await client._read_tool_stream(resp) + assert result.server_input_tokens is None + # The estimate field is a separate concern with unchanged semantics. + assert result.input_tokens == 0 + + +# --------------------------------------------------------------------------- +# End-to-end stamping through the retry engine +# --------------------------------------------------------------------------- +class _AccountAuth(FakeSingleAuth): + def get_account_id(self): + return "acct-uuid-1" + + +class _SurrogateAuth(FakeSingleAuth): + def get_account_id(self): + return "acct-\ud800" # unpaired surrogate: json.loads accepts these + + +class TestSendWithRetriesStamping: + async def test_success_carries_account_key_and_server_usage( + self, tmp_path, monkeypatch + ): + monkeypatch.setattr( + "src.llm.account_key.DEFAULT_KEY_PATH", tmp_path / "k.secret" + ) + client = _client(auth=_AccountAuth()) + session = FakeSession([ + FakeResp(200, sse_lines=_sse([ + {"type": "response.output_text.delta", "delta": "ok"}, + {"type": "response.completed", + "response": {"output": [], "usage": {"input_tokens": 1234}}}, + ])), + ]) + monkeypatch.setattr(client, "_get_session", lambda: _async_return(session)) + result = await client._stream_tool_request({"model": "m"}) + assert isinstance(result, LLMResponse) + assert result.server_input_tokens == 1234 + expected = opaque_account_key("acct-uuid-1", key_path=tmp_path / "k.secret") + assert result.account_key == expected + assert "acct-uuid-1" not in json.dumps(result.account_key) + + async def test_overflow_exception_carries_evidence(self, tmp_path, monkeypatch): + monkeypatch.setattr( + "src.llm.account_key.DEFAULT_KEY_PATH", tmp_path / "k.secret" + ) + client = _client(auth=_AccountAuth(), max_retries=1) + session = FakeSession([ + FakeResp(200, sse_lines=_sse([ + {"type": "response.failed", + "response": { + "error": {"type": "invalid_request_error", + "code": "context_length_exceeded"}, + "usage": {"input_tokens": 922_000}, + }}, + ], done=False)), + ]) + monkeypatch.setattr(client, "_get_session", lambda: _async_return(session)) + with pytest.raises(LLMRequestError) as excinfo: + await client._stream_tool_request({"model": "m"}) + exc = excinfo.value + assert exc.code == "context_length_exceeded" + assert exc.server_input_tokens == 922_000 + assert exc.account_key == opaque_account_key( + "acct-uuid-1", key_path=tmp_path / "k.secret" + ) + assert exc.model == "m" + assert "acct-uuid-1" not in str(exc) + + async def test_surrogate_identity_none_on_success_path(self, tmp_path, monkeypatch): + """Blocker #2 end-to-end: a healthy response is never replaced by + UnicodeEncodeError — the stamp degrades to None.""" + monkeypatch.setattr( + "src.llm.account_key.DEFAULT_KEY_PATH", tmp_path / "k.secret" + ) + client = _client(auth=_SurrogateAuth()) + session = FakeSession([ + FakeResp(200, sse_lines=_sse([ + {"type": "response.output_text.delta", "delta": "ok"}, + {"type": "response.completed", + "response": {"output": [], "usage": {"input_tokens": 10}}}, + ])), + ]) + monkeypatch.setattr(client, "_get_session", lambda: _async_return(session)) + result = await client._stream_tool_request({"model": "m"}) + assert result.text == "ok" + assert result.account_key is None + + async def test_surrogate_identity_none_on_overflow_path(self, tmp_path, monkeypatch): + """Blocker #2 end-to-end: the intended structural overflow is raised, + not a UnicodeEncodeError from evidence stamping.""" + monkeypatch.setattr( + "src.llm.account_key.DEFAULT_KEY_PATH", tmp_path / "k.secret" + ) + client = _client(auth=_SurrogateAuth(), max_retries=1) + session = FakeSession([ + FakeResp(200, sse_lines=_sse([ + {"type": "response.failed", + "response": {"error": {"type": "invalid_request_error", + "code": "context_length_exceeded"}}}, + ], done=False)), + ]) + monkeypatch.setattr(client, "_get_session", lambda: _async_return(session)) + with pytest.raises(LLMRequestError) as excinfo: + await client._stream_tool_request({"model": "m"}) + assert excinfo.value.code == "context_length_exceeded" + assert excinfo.value.account_key is None + + async def test_missing_account_identity_stamps_none(self, monkeypatch): + client = _client() # FakeSingleAuth: account id None + session = FakeSession([ + FakeResp(200, sse_lines=_sse([ + {"type": "response.output_text.delta", "delta": "ok"}, + {"type": "response.completed", "response": {"output": []}}, + ])), + ]) + monkeypatch.setattr(client, "_get_session", lambda: _async_return(session)) + result = await client._stream_tool_request({"model": "m"}) + assert result.account_key is None diff --git a/tests/test_recovery.py b/tests/test_recovery.py index c2db2487..a9a54af2 100644 --- a/tests/test_recovery.py +++ b/tests/test_recovery.py @@ -701,7 +701,7 @@ async def test_recovery_attempts_reset_each_iteration(self): iteration_count = 0 recovery_values = [] - async def mock_iteration_cb(messages, system_prompt, tools): + async def mock_iteration_cb(messages, system_prompt, tools, generation_state=None): nonlocal iteration_count iteration_count += 1 recovery_values.append(agent.recovery_attempts) @@ -738,7 +738,7 @@ async def test_callback_failure_fails_agent_no_manager_ladder(self): call_count = 0 - async def mock_iteration_cb(messages, system_prompt, tools): + async def mock_iteration_cb(messages, system_prompt, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: diff --git a/tests/test_resume_admission.py b/tests/test_resume_admission.py index 459280ec..5a09fa97 100644 --- a/tests/test_resume_admission.py +++ b/tests/test_resume_admission.py @@ -601,6 +601,29 @@ def test_validate_payload_rejects_each_structural_deviation(self): validate_payload(broken) +class TestRecoveryCodecPreLeaseRejection: + async def test_malformed_generation_identity_rejects_before_lease(self, tmp_path): + h, original = await suspend_turn(tmp_path) + (payload_text,) = h.store._conn.execute("SELECT payload FROM turns").fetchone() + payload = json.loads(payload_text) + payload["fields"]["_rescue_passes"] = 1 + payload["fields"]["_gen_identity"] = { + "provider": "codex", "model": "gpt-5.5", "effort": "low", + "ladder": [400_000, "oops"], + "budget": {"primary_chars": 500_000}, "attempts": "bad", + } + rewrite_payload_with_valid_digest(h.store, json.dumps(payload, sort_keys=True)) + result = await h.manager.try_explicit_resume(resume_msg(original)) + assert result is not None + assert "could not be restored" in result[0] + (status,) = h.store._conn.execute("SELECT status FROM turns").fetchone() + assert status == TurnStatus.TERMINAL_REJECTED + (active,) = h.store._conn.execute( + "SELECT COUNT(*) FROM turns WHERE status='ACTIVE'" + ).fetchone() + assert active == 0 + + class TestExplicitResumeOrdering: async def test_resume_trigger_skips_history_compaction(self, tmp_path): """Round-3 deviation #6 (PR #242): the explicit-resume check runs @@ -1121,10 +1144,10 @@ async def test_pre_pr244_v1_checkpoint_resumes_with_default_pending(self, tmp_pa assert result[0] == "resumed a legacy checkpoint" assert h.row()[0] == TurnStatus.TERMINAL_COMPLETED - async def test_new_writers_emit_v2(self, tmp_path): + async def test_new_writers_emit_current_version(self, tmp_path): h, _original = await suspend_turn(tmp_path) payload = json.loads(h.row()[1]) - assert payload["codec_version"] == 2 + assert payload["codec_version"] == 4 async def test_v2_payload_missing_the_field_is_malformed(self, tmp_path): """Round-5 blocker #3: version scoping makes the two cases @@ -1135,7 +1158,7 @@ async def test_v2_payload_missing_the_field_is_malformed(self, tmp_path): make_breaker_probe_ready(h) (payload_text,) = h.store._conn.execute("SELECT payload FROM turns").fetchone() payload = json.loads(payload_text) - assert payload["codec_version"] == 2 + assert payload["codec_version"] == 4 del payload["fields"]["wait_judgment_pending"] rewrite_payload_with_valid_digest(h.store, json.dumps(payload, sort_keys=True)) calls_before = len(h.fake.calls) diff --git a/tests/test_scheduler_agents_reliability.py b/tests/test_scheduler_agents_reliability.py index 53a90423..5ec4b5c2 100644 --- a/tests/test_scheduler_agents_reliability.py +++ b/tests/test_scheduler_agents_reliability.py @@ -59,7 +59,7 @@ async def _record_wait(_info, seconds): monkeypatch.setattr(manager, "_interruptible_wait", _record_wait) - async def _always_fail(_prompt, _channel, _prev): + async def _always_fail(_prompt, _channel, _prev, _cancel): raise RuntimeError("boom") await manager._run_loop(info, _SilentChannel(), _always_fail) @@ -79,7 +79,7 @@ async def _record_wait(_info, seconds): monkeypatch.setattr(manager, "_interruptible_wait", _record_wait) - async def _ok(_prompt, _channel, _prev): + async def _ok(_prompt, _channel, _prev, _cancel): return "done" await manager._run_loop(info, _SilentChannel(), _ok) @@ -94,7 +94,7 @@ async def test_shutdown_cancels_and_awaits_loop_tasks(): manager = LoopManager() started = asyncio.Event() - async def _forever(_prompt, _channel, _prev): + async def _forever(_prompt, _channel, _prev, _cancel): started.set() await asyncio.sleep(3600) @@ -356,3 +356,151 @@ def test_cleanup_removes_untriggered_loop_even_on_fresh_boot(monkeypatch): manager.cleanup_finished() assert info.id not in manager._loops + +async def test_stop_waits_until_inflight_iteration_is_cancelled_and_settled(): + """Once stop_loop reports stopped, no recovery retry or tool effect may + begin. Cancellation must reach the in-flight callback, not merely set the + manager's status for the next scheduled iteration.""" + manager = LoopManager() + started = asyncio.Event() + cancelled = asyncio.Event() + effects: list[str] = [] + + async def _recovering(_prompt, _channel, _prev, cancel_event): + started.set() + try: + await asyncio.sleep(3600) + effects.append("retry") + effects.append("tool") + return "impossible" + except asyncio.CancelledError: + assert cancel_event.is_set() + cancelled.set() + raise + + loop_id = manager.start_loop( + goal="g", + channel=_SilentChannel(), + requester_id="u", + requester_name="U", + iteration_callback=_recovering, + interval_seconds=10, + mode="silent", + max_iterations=100, + ) + await asyncio.wait_for(started.wait(), timeout=2) + result = await asyncio.wait_for(manager.stop_loop(loop_id), timeout=2) + + assert result == f"Loop `{loop_id}` stopped." + assert cancelled.is_set() + assert manager._loops[loop_id]._task.done() + await asyncio.sleep(0) + assert effects == [] + +async def test_self_stop_single_direct_callback_uses_logical_owner(): + manager = LoopManager() + outcome: list[str] = [] + holder: dict[str, str] = {} + + async def _self_stop(_prompt, _channel, _prev, cancel_event): + assert not cancel_event.is_set() + outcome.append(await manager.stop_loop(holder["id"])) + return "settled" + + loop_id = manager.start_loop( + goal="g", + channel=_SilentChannel(), + requester_id="u", + requester_name="U", + iteration_callback=_self_stop, + interval_seconds=10, + mode="silent", + max_iterations=2, + ) + holder["id"] = loop_id + task = manager._loops[loop_id]._task + assert task is not None + await asyncio.wait_for(task, timeout=2) + + assert outcome == [f"Loop `{loop_id}` stop requested."] + assert manager._loops[loop_id].status == "stopped" + + +async def test_stop_all_from_one_loop_cancels_and_awaits_other_loops(): + manager = LoopManager() + other_started = asyncio.Event() + other_cancelled = asyncio.Event() + release_self = asyncio.Event() + outcome: list[str] = [] + + async def _other(_prompt, _channel, _prev, cancel_event): + other_started.set() + try: + await asyncio.sleep(3600) + except asyncio.CancelledError: + assert cancel_event.is_set() + other_cancelled.set() + raise + + other_id = manager.start_loop( + goal="other", + channel=_SilentChannel(), + requester_id="u", + requester_name="U", + iteration_callback=_other, + interval_seconds=10, + mode="silent", + max_iterations=2, + ) + await asyncio.wait_for(other_started.wait(), timeout=2) + + async def _self(_prompt, _channel, _prev, _cancel): + await release_self.wait() + outcome.append(await manager.stop_loop("all")) + return "settled" + + self_id = manager.start_loop( + goal="self", + channel=_SilentChannel(), + requester_id="u", + requester_name="U", + iteration_callback=_self, + interval_seconds=10, + mode="silent", + max_iterations=2, + ) + release_self.set() + self_task = manager._loops[self_id]._task + assert self_task is not None + await asyncio.wait_for(self_task, timeout=2) + + assert other_cancelled.is_set() + assert manager._loops[other_id].status == "stopped" + assert manager._loops[self_id].status == "stopped" + assert outcome == [f"Stop requested for 2 loop(s): {other_id}, {self_id}"] + + +async def test_self_stop_all_never_awaits_its_own_manager_task(): + manager = LoopManager() + outcome: list[str] = [] + + async def _self_stop(_prompt, _channel, _prev, cancel_event): + assert not cancel_event.is_set() + outcome.append(await manager.stop_loop("all")) + return "settled" + + loop_id = manager.start_loop( + goal="g", + channel=_SilentChannel(), + requester_id="u", + requester_name="U", + iteration_callback=_self_stop, + interval_seconds=10, + mode="silent", + max_iterations=2, + ) + task = manager._loops[loop_id]._task + assert task is not None + await asyncio.wait_for(task, timeout=2) + assert outcome == [f"Stop requested for 1 loop(s): {loop_id}"] + assert manager._loops[loop_id].status == "stopped" diff --git a/tests/test_setup_helpers.py b/tests/test_setup_helpers.py index f37aa3dd..4411d274 100644 --- a/tests/test_setup_helpers.py +++ b/tests/test_setup_helpers.py @@ -40,6 +40,24 @@ def test_returns_fresh_dict_with_defaults(self): cfg["discord"]["token"] = "mutated" assert build_config()["discord"]["token"] != "mutated" + def test_generated_codex_defaults_match_reference_deployment(self): + """The scaffold is the one supported first-boot writer: an explicit + legacy value here silently overrides the schema defaults, so the + GENERATED and the PARSED codex default tuple are pinned together.""" + from src.config.schema import OpenAICodexConfig + + generated = build_config()["openai_codex"] + assert generated["model"] == "gpt-5.6-sol" + parsed = OpenAICodexConfig(**generated) + assert ( + parsed.model, + parsed.reasoning_effort, + parsed.agent_model, + parsed.agent_reasoning_effort, + parsed.auxiliary.enabled, + parsed.auxiliary.model, + ) == ("gpt-5.6-sol", "xhigh", "auto", "auto", True, "gpt-5.6-terra") + class TestBuildEnv: def test_contains_discord_token_line(self): diff --git a/tests/test_spawn_loop_agents_config.py b/tests/test_spawn_loop_agents_config.py index 8588ccd0..e9d25861 100644 --- a/tests/test_spawn_loop_agents_config.py +++ b/tests/test_spawn_loop_agents_config.py @@ -16,6 +16,7 @@ import pytest +from src.config.schema import ContextCompressionConfig from tests.fakes import FakeChannel, FakeLLM, FakeMessage, make_bot @@ -58,7 +59,10 @@ async def test_no_attribute_error_with_default_config(self): assert "AttributeError" not in result assert "agent-a" in result or "spawned" in result.lower() assert captured["context_compression_enabled"] is True - assert captured["max_context_chars"] == bot.context_compressor.max_context_chars + assert ( + captured["max_context_chars"] + == bot.context_compressor.resolved_max_context_chars + ) assert captured["keep_recent_iterations"] == bot.context_compressor.keep_recent_iterations async def test_defaults_used_when_compression_disabled(self): @@ -74,7 +78,7 @@ async def test_defaults_used_when_compression_disabled(self): async def test_compressor_values_forwarded_when_enabled(self): bot, captured = _bot_with_running_loop() - bot.context_compressor = SimpleNamespace( + bot.context_compressor = ContextCompressionConfig( enabled=True, max_context_chars=123456, keep_recent_iterations=7 ) await bot.agent_task_tools._handle_spawn_loop_agents( diff --git a/tests/test_surface_boundary.py b/tests/test_surface_boundary.py new file mode 100644 index 00000000..1c494431 --- /dev/null +++ b/tests/test_surface_boundary.py @@ -0,0 +1,268 @@ +"""Surface-aware compressor boundary (campaign phase 4, contract §6). + +Pins the settled partition: replayed context (chat history / loop +prev_context) elides oldest-first in whole messages behind a count marker +regenerated from BOUNDARY STATE (never text matching); the current-request +envelope is protected verbatim; tool iterations keep the existing +newest-first emergency rules; a first-generation overflow with zero tool +iterations recovers by replay elision alone; and when the envelope itself +cannot fit a rung the failure is honest. +""" + +from __future__ import annotations + +from src.llm.context_compressor import ( + SurfaceBoundary, + emergency_compress_for_window, + estimate_message_chars, +) + + +def _history(n: int, size: int) -> list[dict]: + return [ + {"role": "user" if i % 2 == 0 else "assistant", "content": f"h{i}:" + "y" * size} + for i in range(n) + ] + + +def _envelope(size: int = 2_000) -> list[dict]: + return [ + {"role": "developer", "content": "per-request directives"}, + {"role": "user", "content": "CURRENT REQUEST: " + "q" * size}, + ] + + +def _iterations(n: int, size: int) -> list[dict]: + out: list[dict] = [] + for i in range(n): + out.append( + { + "role": "assistant", + "content": [ + {"type": "tool_use", "id": f"t{i}", "name": "read_file", "input": {"p": i}}, + ], + } + ) + out.append( + { + "role": "user", + "content": [ + {"type": "tool_result", "tool_use_id": f"t{i}", "content": "r" * size}, + ], + } + ) + return out + + +class TestFirstGenerationChatOverflow: + def test_recovers_by_replay_elision_alone(self): + """The round-1 structural gap: no tool iterations at all, yet the + payload must come under target by spending old history.""" + history = _history(40, 10_000) + envelope = _envelope() + messages = history + envelope + boundary = SurfaceBoundary(request_start=len(history)) + compressed, report = emergency_compress_for_window( + messages, target_chars=120_000, boundary=boundary + ) + assert report["fits"] is True + assert estimate_message_chars(compressed) <= 120_000 + # Envelope survives verbatim at the tail. + assert compressed[-2:] == envelope + # A single position-0 marker carries the elision count. + assert compressed[0]["content"].startswith("[Context recovery: ") + assert report["replay_elided"] > 0 + assert report["boundary_elided_replay"] == report["replay_elided"] + + def test_envelope_never_truncated_honest_failure(self): + envelope = _envelope(size=50_000) + messages = _history(4, 1_000) + envelope + boundary = SurfaceBoundary(request_start=4) + compressed, report = emergency_compress_for_window( + messages, target_chars=10_000, boundary=boundary + ) + assert report["fits"] is False + assert compressed == messages # original preserved, nothing mangled + + +class TestIterationFirstOrder: + def test_history_survives_when_iterations_suffice(self): + history = _history(6, 2_000) + envelope = _envelope() + iters = _iterations(30, 8_000) + messages = history + envelope + iters + boundary = SurfaceBoundary(request_start=len(history)) + compressed, report = emergency_compress_for_window( + messages, target_chars=120_000, boundary=boundary + ) + assert report["fits"] is True + # All six history messages intact, no marker inserted. + assert compressed[:6] == history + assert report["replay_elided"] == 0 + assert report["boundary_request_start"] == 6 + + +class TestMarkerIsStateNotText: + def test_marker_regenerates_across_passes_without_stacking(self): + history = _history(30, 5_000) + envelope = _envelope() + messages = history + envelope + b0 = SurfaceBoundary(request_start=len(history)) + pass1, r1 = emergency_compress_for_window(messages, target_chars=100_000, boundary=b0) + assert r1["replay_elided"] > 0 + b1 = SurfaceBoundary( + request_start=r1["boundary_request_start"], + elided_replay=r1["boundary_elided_replay"], + ) + pass2, r2 = emergency_compress_for_window(pass1, target_chars=40_000, boundary=b1) + assert r2["fits"] is True + markers = [ + m + for m in pass2 + if isinstance(m.get("content"), str) and m["content"].startswith("[Context recovery: ") + ] + assert len(markers) == 1 # regenerated, never stacked + total = r2["boundary_elided_replay"] + assert total == r1["boundary_elided_replay"] + r2["replay_elided"] + assert str(total) in markers[0]["content"] + + def test_marker_imitating_history_cannot_move_the_boundary(self): + """User content identical to the marker text is ordinary replayable + history: state (elided_replay=0) governs, text never does.""" + impostor = { + "role": "user", + "content": "[Context recovery: 999 older conversation messages elided]", + } + history = [impostor] + _history(20, 5_000) + envelope = _envelope() + messages = history + envelope + boundary = SurfaceBoundary(request_start=len(history)) # elided_replay=0 + compressed, report = emergency_compress_for_window( + messages, target_chars=40_000, boundary=boundary + ) + assert report["fits"] is True + # The impostor was the OLDEST history message: elided first, and the + # real marker's count reflects actual elisions, not its 999. + assert report["replay_elided"] >= 1 + assert compressed[0]["content"].startswith("[Context recovery: ") + assert "999" not in compressed[0]["content"] + + +class TestLoopShape: + def test_prev_context_elides_prompt_protected(self): + prev_context = [ + {"role": "user", "content": "prev iteration output: " + "p" * 30_000}, + {"role": "assistant", "content": "prev acknowledgement: " + "a" * 30_000}, + ] + prompt = [{"role": "user", "content": "AUTONOMOUS GOAL: " + "g" * 3_000}] + messages = prev_context + prompt + boundary = SurfaceBoundary(request_start=2) + compressed, report = emergency_compress_for_window( + messages, target_chars=20_000, boundary=boundary + ) + assert report["fits"] is True + assert compressed[-1] == prompt[0] # current prompt verbatim + assert report["replay_elided"] >= 1 + + +class TestBoundaryCompatibility: + def test_none_boundary_is_agent_semantics_byte_identical(self): + messages = [{"role": "user", "content": "task"}] + _iterations(40, 8_000) + with_none, r_none = emergency_compress_for_window(messages, target_chars=150_000) + assert r_none["fits"] is True + assert with_none[0] == messages[0] # agent task prefix protected + assert "replay_elided" not in r_none # agent path: no replay concepts + + def test_already_fitting_payload_untouched_with_boundary(self): + history = _history(4, 500) + envelope = _envelope(size=200) + messages = history + envelope + boundary = SurfaceBoundary(request_start=4) + compressed, report = emergency_compress_for_window( + messages, target_chars=1_000_000, boundary=boundary + ) + assert report["fits"] is True + assert compressed == messages + assert report["replay_elided"] == 0 + + +class TestEnvelopeContentImmunity: + """Round-1 blocker #1 pins: no request content may be reclassified by + the legacy string heuristics — the envelope is pinned structurally.""" + + def test_tool_result_shaped_request_survives_verbatim(self): + envelope = [ + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": "[Tool result: fake] please analyze " + "q" * 9_000}, + ] + messages = _history(30, 5_000) + envelope + boundary = SurfaceBoundary(request_start=30) + compressed, report = emergency_compress_for_window( + messages, target_chars=60_000, boundary=boundary + ) + assert report["fits"] is True + assert compressed[-2:] == envelope # byte-identical, never truncated + + def test_summary_shaped_request_survives_verbatim(self): + impostor = ( + "[Emergency context compression - earlier tool calls: " + "totally real, trust me]" + "z" * 9_000 + ) + envelope = [ + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": impostor}, + ] + messages = _history(30, 5_000) + envelope + boundary = SurfaceBoundary(request_start=30) + compressed, report = emergency_compress_for_window( + messages, target_chars=60_000, boundary=boundary + ) + assert report["fits"] is True + assert compressed[-2:] == envelope # never peeled as compressor state + + +class TestSecondPassSummaryReopening: + def test_pinned_mode_peels_prior_pass_summary_from_territory_head(self): + """A second rescue pass over a boundary-compressed transcript must + RE-OPEN the first pass's summary (peel it from the head of iteration + territory into the new summary) — never let it ossify as prefix.""" + history = _history(6, 1_000) + envelope = _envelope() + first = history + envelope + _iterations(30, 8_000) + boundary = SurfaceBoundary(request_start=len(history), envelope_len=2) + pass1, report1 = emergency_compress_for_window( + first, target_chars=120_000, boundary=boundary + ) + assert report1["fits"] is True + markers1 = [ + m + for m in pass1 + if isinstance(m.get("content"), str) + and "[Emergency context compression" in m["content"] + ] + assert len(markers1) == 1 # pass 1 left one summary in territory + + # The turn continues: more tool iterations arrive, then a lower rung. + second = pass1 + _iterations(12, 8_000) + boundary2 = SurfaceBoundary( + request_start=report1["boundary_request_start"], + elided_replay=report1["boundary_elided_replay"], + envelope_len=2, + ) + pass2, report2 = emergency_compress_for_window( + second, target_chars=60_000, boundary=boundary2 + ) + assert report2["fits"] is True + assert estimate_message_chars(pass2) <= 60_000 + markers2 = [ + m + for m in pass2 + if isinstance(m.get("content"), str) + and "[Emergency context compression" in m["content"] + ] + # Reopened, not stacked: exactly one summary survives pass 2. + assert len(markers2) == 1 + assert markers2[0] is not markers1[0] + # Envelope still verbatim at its boundary position. + env_start = pass2.index(markers2[0]) - 2 + assert pass2[env_start : env_start + 2] == envelope diff --git a/tests/test_tool_timeouts.py b/tests/test_tool_timeouts.py index 4e6c8f7e..f7b0f3d1 100644 --- a/tests/test_tool_timeouts.py +++ b/tests/test_tool_timeouts.py @@ -222,7 +222,7 @@ async def test_agent_uses_per_tool_timeout(self): call_count = 0 tool_timeouts_used = [] - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: @@ -265,7 +265,7 @@ async def test_agent_default_timeout_without_override(self): call_count = 0 tool_timeouts_used = [] - async def iter_cb(msgs, sys, tools): + async def iter_cb(msgs, sys, tools, generation_state=None): nonlocal call_count call_count += 1 if call_count == 1: diff --git a/tests/test_trajectory_completeness.py b/tests/test_trajectory_completeness.py index d0e790c3..972984f9 100644 --- a/tests/test_trajectory_completeness.py +++ b/tests/test_trajectory_completeness.py @@ -317,7 +317,9 @@ async def _log_execution(**kwargs): _registry = ModelBreakerRegistry() self._fake_gateway = SimpleNamespace( active_client=self.llm_client, - capacity_breaker_for=lambda model=None: _registry.for_model("codex", "m"), + capacity_breaker_for=lambda model=None, provider=None: _registry.for_model( + "codex", "m" + ), recovery_policy=RecoveryPolicy, notify_generation_success=lambda provider: None, ) @@ -468,7 +470,7 @@ async def send(self, *a, **k): chan = _Chan() - async def callback(prompt, channel, prev_context): + async def callback(prompt, channel, prev_context, cancel_event): stamps.append(dict(get_turn() or {})) mgr._loops[holder["lid"]]._cancel_event.set() return "ok" @@ -524,5 +526,7 @@ def test_chat_spawn_passes_saver(self): # P5c: body moved to native_tools/agents_tasks.py (host-based) from src.discord.native_tools.agents_tasks import AgentTaskTools - src = inspect.getsource(AgentTaskTools._handle_spawn_agent) - assert "trajectory_saver=self._agent_trajectory_saver" in src + assert ( + "trajectory_saver=self._agent_trajectory_saver" + in inspect.getsource(AgentTaskTools._handle_spawn_agent) + ) diff --git a/tests/test_turn_checkpoint_codec.py b/tests/test_turn_checkpoint_codec.py index 42e2badb..d3dc03ff 100644 --- a/tests/test_turn_checkpoint_codec.py +++ b/tests/test_turn_checkpoint_codec.py @@ -126,6 +126,8 @@ def _full_turn(): _cancel=SimpleNamespace(is_set=lambda: False), # RECONSTRUCTED _ch_id="c1", _req_id="abcd1234", + _boundary_request_start=0, + _boundary_envelope_len=1, iteration=3, tools_used_in_loop=["run_command", "read_file"], continuation_count=2, diff --git a/tests/test_typing_resilience.py b/tests/test_typing_resilience.py index d9e59c14..60b79f4b 100644 --- a/tests/test_typing_resilience.py +++ b/tests/test_typing_resilience.py @@ -238,7 +238,13 @@ async def _default_save(trajectory, **kwargs): loop_manager=SimpleNamespace(), stuck_loop_tracker_cls=object, ) - return ToolLoopRunner(deps), saved, cleared + runner = ToolLoopRunner(deps) + from src.discord.llm_gateway import LLMServingIdentity + + runner._llm_gateway.capture_serving_identity = lambda config=None: LLMServingIdentity( + provider="codex", client=None, model=None, reasoning_effort=None + ) + return runner, saved, cleared def _stub_state(channel=None): @@ -258,6 +264,12 @@ def _stub_state(channel=None): message=SimpleNamespace(channel=channel or FakeChannel(), content="hi"), messages=[], tools_used_in_loop=[], + _boundary_request_start=0, + _boundary_elided_replay=0, + _boundary_envelope_len=0, + _char_latch=None, + _rescue_passes=0, + _gen_identity=None, system_prompt="sys", tools=[], user_id="u1", @@ -270,7 +282,7 @@ async def _prep(*args, **kwargs): return st runner._prepare_chat_turn = _prep - runner._maybe_compress = lambda st: None + runner._maybe_compress = lambda st, request_client=None, request_config=None: True runner._call_llm = call_llm @@ -279,7 +291,7 @@ async def test_escape_records_bounded_error_clears_and_reraises(self): runner, saved, cleared = _make_runner() st = _stub_state() - async def _boom(_st): + async def _boom(_st, _request_client=None, **_kwargs): raise RuntimeError(CF_HTML) _wire(runner, st, _boom) @@ -296,7 +308,7 @@ async def test_cancellation_cleans_up_without_error_trajectory(self): runner, saved, cleared = _make_runner() st = _stub_state() - async def _cancel(_st): + async def _cancel(_st, _request_client=None, **_kwargs): raise asyncio.CancelledError() _wire(runner, st, _cancel) @@ -312,7 +324,7 @@ async def _bad_save(trajectory, **kwargs): runner, _saved, cleared = _make_runner(recorder_save=_bad_save) st = _stub_state() - async def _boom(_st): + async def _boom(_st, _request_client=None, **_kwargs): raise RuntimeError("original failure") _wire(runner, st, _boom) @@ -327,7 +339,7 @@ async def test_success_path_gets_no_extra_clear_from_guard(self): st = _stub_state() done = ("all good", False, False, [], False) - async def _done(_st): + async def _done(_st, _request_client=None, **_kwargs): return ("done", done) _wire(runner, st, _done) @@ -374,7 +386,7 @@ async def _cwt(**kwargs): registry = ModelBreakerRegistry() runner._llm_gateway = SimpleNamespace( call_with_tools=_cwt, - capacity_breaker_for=lambda model=None: registry.for_model("codex", "m"), + capacity_breaker_for=lambda model=None, provider=None: registry.for_model("codex", "m"), recovery_policy=RecoveryPolicy, # consumed by the pre-admission effort preflight (no-op for None) active_client=None, diff --git a/tests/test_web_api_agents_loops.py b/tests/test_web_api_agents_loops.py index 15d1c9e2..4833f20c 100644 --- a/tests/test_web_api_agents_loops.py +++ b/tests/test_web_api_agents_loops.py @@ -177,7 +177,9 @@ async def test_start_loop_manager_error(self): @pytest.mark.asyncio async def test_stop_loop_found_and_missing(self): bot = MagicMock() - bot.loop_manager.stop_loop.side_effect = ["Stopped loop.", "Loop not found."] + bot.loop_manager.stop_loop = AsyncMock( + side_effect=["Stopped loop.", "Loop not found."] + ) async with TestClient(TestServer(_app(register_loops, bot=bot))) as c: assert (await c.delete("/api/loops/L1")).status == 200 assert (await c.delete("/api/loops/L1")).status == 404 @@ -193,6 +195,7 @@ async def test_restart_missing_loop(self): async def test_restart_success(self): bot = MagicMock() bot.loop_manager._loops = {"L1": _loop_info(status="running", channel_id="123")} + bot.loop_manager.stop_loop = AsyncMock(return_value="Loop stopped.") bot.get_channel.return_value = MagicMock() bot.loop_manager.start_loop.return_value = "loop-new" async with TestClient(TestServer(_app(register_loops, bot=bot))) as c: @@ -214,6 +217,7 @@ async def test_restart_channel_gone(self): async def test_restart_manager_error(self): bot = MagicMock() bot.loop_manager._loops = {"L1": _loop_info(status="running", channel_id="123")} + bot.loop_manager.stop_loop = AsyncMock(return_value="Loop stopped.") bot.get_channel.return_value = MagicMock() bot.loop_manager.start_loop.return_value = "Error: too many loops" async with TestClient(TestServer(_app(register_loops, bot=bot))) as c: diff --git a/tests/test_web_api_config_admin.py b/tests/test_web_api_config_admin.py index 119cc654..68353ba5 100644 --- a/tests/test_web_api_config_admin.py +++ b/tests/test_web_api_config_admin.py @@ -333,6 +333,47 @@ async def test_update_config_persists_normalized_dropping_removed_keys(self, _ac assert "enabled" not in saved["grafana_alerts"] assert saved["grafana_alerts"]["cooldown_seconds"] == 612 + @pytest.mark.asyncio + async def test_context_budget_alias_persists_canonical_key_through_restart( + self, _active_config + ): + from pathlib import Path + + from ruamel.yaml import YAML + + from src.config.schema import Config as _Config + from src.config.schema import active_config_path, set_active_config_path + + path = Path("config.yml") + path.write_text("discord:\n token: fake\n") + previous = active_config_path() + set_active_config_path(path) + try: + app, bot = _app(register_discord_config) + async with TestClient(TestServer(app)) as c: + r = await c.put( + "/api/config", + json={ + "openai_codex": { + "context_budget_overrides": { + "codex-auto-review": 600_000, + } + } + }, + ) + assert r.status == 200 + assert (await r.json())["openai_codex"][ + "context_budget_overrides" + ] == {"gpt-5.6-luna": 600_000} + finally: + set_active_config_path(previous) + + expected = {"gpt-5.6-luna": 600_000} + assert bot.config.openai_codex.context_budget_overrides == expected + document = YAML().load(path.read_text()) + assert document["openai_codex"]["context_budget_overrides"] == expected + assert _Config(**document).openai_codex.context_budget_overrides == expected + @pytest.mark.asyncio async def test_blanking_the_workspace_normalizes_everywhere(self): """PR #239 round-8 follow-up: the persisted-config path, for real. diff --git a/tests/test_web_api_llm_admin.py b/tests/test_web_api_llm_admin.py index 44f6991e..168921ad 100644 --- a/tests/test_web_api_llm_admin.py +++ b/tests/test_web_api_llm_admin.py @@ -144,7 +144,7 @@ async def test_llm_status_reports_providers(self): body = await (await c.get("/api/llm/status")).json() assert body["codex"]["configured"] is True assert "max_tokens" not in body["codex"] - assert body["codex"]["reasoning_effort"] == "medium" + assert body["codex"]["reasoning_effort"] == "xhigh" assert body["codex"]["active_reasoning_effort"] is None # object() has no attr assert body["ollama"]["configured"] is False assert body["active_model"] == "gpt-5.5" @@ -156,8 +156,9 @@ async def test_llm_status_agent_effort_fields(self): bot.llm_gateway.ollama_client = None bot.llm_gateway.kimi_client = None bot.llm_gateway.active_client = None + bot.config.openai_codex.agent_reasoning_effort = None # explicit inherit (default: "auto") async with TestClient(TestServer(app)) as c: - # inherit (default): effective mirrors the live client's effort + # inherit: effective mirrors the live client's effort body = await (await c.get("/api/llm/status")).json() assert body["codex"]["agent_reasoning_effort"] is None assert body["codex"]["effective_agent_reasoning_effort"] == "high" @@ -231,6 +232,7 @@ async def test_llm_status_agent_model_fields(self): bot.llm_gateway.ollama_client = None bot.llm_gateway.kimi_client = None bot.llm_gateway.active_client = None + bot.config.openai_codex.agent_model = None # explicit inherit (default is "auto") async with TestClient(TestServer(app)) as c: body = await (await c.get("/api/llm/status")).json() assert body["codex"]["agent_model"] is None @@ -477,7 +479,7 @@ async def test_codex_config_invalid_reasoning_rejected_before_mutation(self): await c.put("/api/llm/codex/config", json={"reasoning_effort": "minimal"}) ).status == 400 # nothing mutated, nothing reloaded - assert bot.config.openai_codex.reasoning_effort == "medium" + assert bot.config.openai_codex.reasoning_effort == "xhigh" assert bot.config.openai_codex.model != "changed-model" bot.llm_gateway.reload_codex_inner.assert_not_awaited() @@ -687,7 +689,7 @@ async def test_codex_agent_effort_invalid_rejected_before_mutation(self): ) assert r.status == 400 assert "agent_reasoning_effort" in (await r.json())["error"] - assert bot.config.openai_codex.agent_reasoning_effort is None + assert bot.config.openai_codex.agent_reasoning_effort == "auto" assert bot.config.openai_codex.model != "changed-model" bot.llm_gateway.reload_codex_inner.assert_not_awaited() @@ -1330,7 +1332,7 @@ async def test_effort_direction_rejected_with_allowed_list(self): assert "gpt-5.5" in data["error"] and "'max'" in data["error"] assert "max" not in data["allowed"] and "xhigh" in data["allowed"] # nothing mutated, nothing reloaded - assert bot.config.openai_codex.reasoning_effort == "medium" + assert bot.config.openai_codex.reasoning_effort == "xhigh" gw.reload_codex_inner.assert_not_awaited() @pytest.mark.asyncio @@ -1355,11 +1357,12 @@ async def test_agent_model_direction_rejected(self): _gw(bot) bot.config.openai_codex.model = "gpt-5.6-sol" bot.config.openai_codex.reasoning_effort = "max" + bot.config.openai_codex.agent_reasoning_effort = None # explicit inherit (default: "auto") async with TestClient(TestServer(app)) as c: r = await c.put("/api/llm/codex/config", json={"agent_model": "gpt-5.5"}) assert r.status == 400 assert "agent settings" in (await r.json())["error"] - assert bot.config.openai_codex.agent_model is None + assert bot.config.openai_codex.agent_model == "auto" @pytest.mark.asyncio async def test_agent_effort_direction_rejected(self): @@ -1369,7 +1372,7 @@ async def test_agent_effort_direction_rejected(self): async with TestClient(TestServer(app)) as c: r = await c.put("/api/llm/codex/config", json={"agent_reasoning_effort": "max"}) assert r.status == 400 - assert bot.config.openai_codex.agent_reasoning_effort is None + assert bot.config.openai_codex.agent_reasoning_effort == "auto" @pytest.mark.asyncio async def test_combined_valid_switch_in_one_put_accepted(self): @@ -1768,6 +1771,10 @@ async def test_advanced_keys_persist_and_apply(self): "max_context_chars": 500000, "keep_recent_iterations": 12, }, + "context_budget_overrides": { + "codex-auto-review": 800000, + }, + "context_utilization": 72, }) assert r.status == 200 cfg = bot.config.openai_codex @@ -1778,13 +1785,39 @@ async def test_advanced_keys_persist_and_apply(self): assert cfg.context_compression.max_context_chars == 500000 assert cfg.stream_stall_timeout_seconds == 240 assert cfg.context_compression.keep_recent_iterations == 12 + assert cfg.context_budget_overrides == {"gpt-5.6-luna": 800000} + assert cfg.context_utilization == 72 persisted = {change[0] for change in persist.call_args[0][0]} assert ("openai_codex", "request_timeout_seconds") in persisted assert ("openai_codex", "retry", "max_retries") in persisted assert ("openai_codex", "connection_pool", "max_connections") in persisted + assert ("openai_codex", "context_budget_overrides") in persisted + assert ("openai_codex", "context_utilization") in persisted # Transport/retry reach the live client through the reload path. bot.llm_gateway.reload_codex_inner.assert_awaited() + @pytest.mark.asyncio + @pytest.mark.parametrize( + ("body", "expected_overrides", "expected_utilization"), + [ + ({"context_budget_overrides": {"gpt-5.5": 300_000}}, {"gpt-5.5": 300_000}, 60), + ({"context_utilization": 75}, {}, 75), + ], + ) + async def test_each_context_policy_leaf_saves_independently( + self, body, expected_overrides, expected_utilization + ): + app, bot = self._harness() + with patch( + "src.web.api.llm_admin.persist_config_paths_locked", + new=AsyncMock(return_value=(None, False)), + ): + async with TestClient(TestServer(app)) as c: + response = await c.put("/api/llm/codex/config", json=body) + assert response.status == 200 + assert bot.config.openai_codex.context_budget_overrides == expected_overrides + assert bot.config.openai_codex.context_utilization == expected_utilization + @pytest.mark.asyncio async def test_pool_and_compression_alone_do_not_reload(self): """They are restart/rebuild-bound — persisting them must not churn @@ -1814,16 +1847,27 @@ async def test_pool_and_compression_alone_do_not_reload(self): ({"retry": {"bogus_knob": 1}}, "unknown retry field"), ({"request_timeout_seconds": True}, "must be an integer"), ({"stream_stall_timeout_seconds": 90.5}, "must be an integer"), + ({"context_utilization": 29}, "between 30 and 100"), + ({"context_budget_overrides": {"gpt-5.5": 50_191}}, "between 50192 and 2000000"), + ( + {"context_budget_overrides": {"gpt-5.6-luna": 800000, "codex-auto-review": 700000}}, + "duplicates", + ), ]) async def test_bounds_are_enforced_before_any_mutation(self, body, fragment): app, bot = self._harness() - before = bot.config.openai_codex.request_timeout_seconds - async with TestClient(TestServer(app)) as c: - r = await c.put("/api/llm/codex/config", json=body) - payload = await r.json() + before = bot.config.openai_codex.model_dump() + with patch( + "src.web.api.llm_admin.persist_config_paths_locked", + new=AsyncMock(side_effect=AssertionError("invalid policy reached persistence")), + ): + async with TestClient(TestServer(app)) as c: + r = await c.put("/api/llm/codex/config", json=body) + payload = await r.json() assert r.status == 400 assert fragment in payload["error"] - assert bot.config.openai_codex.request_timeout_seconds == before + assert bot.config.openai_codex.model_dump() == before + bot.llm_gateway.reload_codex_inner.assert_not_awaited() @pytest.mark.asyncio async def test_top_level_timeouts_use_schema_lax_integer_coercion(self): @@ -1945,6 +1989,8 @@ async def test_status_reports_desired_boot_effective_and_pending_restart(self): assert codex["context_compression"] != boot_compression assert codex["effective_context_compression"] == boot_compression assert codex["context_compression_pending_restart"] is True + assert codex["context_budget_overrides"] == {} + assert codex["context_utilization"] == 60 assert body["kimi"]["timeout"] == 123 @pytest.mark.asyncio diff --git a/tests/test_web_api_self_update.py b/tests/test_web_api_self_update.py index e15d28c3..913e23bd 100644 --- a/tests/test_web_api_self_update.py +++ b/tests/test_web_api_self_update.py @@ -11,7 +11,7 @@ from subprocess import CompletedProcess from types import SimpleNamespace -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import web from aiohttp.test_utils import TestClient, TestServer @@ -295,7 +295,7 @@ def _false(cmd, **kw): class TestStopAllLoops: async def test_stop_all(self): bot = MagicMock() - bot.loop_manager.stop_loop.return_value = "stopped 3 loops" + bot.loop_manager.stop_loop = AsyncMock(return_value="stopped 3 loops") async with TestClient(TestServer(_app(bot))) as c: body = await (await c.post("/api/loops/stop-all")).json() assert body["result"] == "stopped 3 loops" diff --git a/tests/test_window_observer.py b/tests/test_window_observer.py new file mode 100644 index 00000000..19e8b012 --- /dev/null +++ b/tests/test_window_observer.py @@ -0,0 +1,1136 @@ +"""Passive window observer + downward clamps (campaign phase 5, plan §11). + +Pins the evidence store's hostile-input safety and atomicity, the clamp +qualification matrix (same-account, same-request, server-authoritative +acceptance), downward-only merges under the 24h TTL, the forfeit-never-fail +invariant, resolver integration, all three surface hooks (chat, loop, +agent), and the management API. +""" + +from __future__ import annotations + +import asyncio +import json +import os +import threading +from datetime import timedelta +from types import SimpleNamespace + +import pytest +from aiohttp import web +from aiohttp.test_utils import TestClient, TestServer + +from src.config.schema import ContextCompressionConfig +from src.llm import window_observer as wo +from src.llm.errors import LLMRequestError +from src.llm.window_observer import WindowObserver, WindowObserverMutationError + +ACCT_A = "a" * 32 +ACCT_B = "b" * 32 + + +def _observer(tmp_path) -> WindowObserver: + return WindowObserver(tmp_path / "context_windows.json") + + +def _overflow( + *, tokens=930_001, key=ACCT_A, model="gpt-5.6-sol", code="context_length_exceeded" +) -> LLMRequestError: + return LLMRequestError( + "overflow", + provider="codex", + model=model, + code=code, + server_input_tokens=tokens, + account_key=key, + ) + + +def _acceptance(*, tokens=408_004, key=ACCT_A, model="gpt-5.6-sol") -> SimpleNamespace: + return SimpleNamespace(server_input_tokens=tokens, account_key=key, provenance_model=model) + + +class TestStoreLifecycle: + async def test_fresh_store_round_trips_atomically(self, tmp_path): + obs = _observer(tmp_path) + assert obs.active_clamp("gpt-5.6-sol") is None + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + assert obs.active_clamp("gpt-5.6-sol") == 408_004 + # Reload from disk: the persisted store carries the same evidence. + again = _observer(tmp_path) + assert again.active_clamp("gpt-5.6-sol") == 408_004 + record = again.view()["accounts"][ACCT_A]["models"]["gpt-5.6-sol"] + assert record["lowest_rejection_bound"] == 930_001 + assert record["highest_accepted_input"] == 408_004 + assert record["overflow_occurrences"] == 1 + # No stray temp files behind the atomic replacement. + names = {p.name for p in tmp_path.iterdir()} + assert names == {"context_windows.json"} + + async def test_alias_models_share_one_canonical_record(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue( + overflow=_overflow(model="codex-auto-review"), + response=_acceptance(model="codex-auto-review"), + ) + assert obs.active_clamp("gpt-5.6-luna") == 408_004 + assert obs.active_clamp("codex-auto-review") == 408_004 + assert "gpt-5.6-luna" in obs.view()["accounts"][ACCT_A]["models"] + + +class TestHostileStoreInputs: + """The filesystem-contract sweep: every hostile shape at the store path + loads empty (quarantined), never blocks, never crashes construction.""" + + def test_fifo_at_store_path_never_blocks(self, tmp_path): + path = tmp_path / "context_windows.json" + os.mkfifo(path) + obs = WindowObserver(path) + assert obs.active_clamp("gpt-5.6-sol") is None + # The FIFO was quarantined out of the store's way. + assert not path.exists() or not path.is_fifo() + assert any(p.name.startswith("context_windows.json.corrupt-") for p in tmp_path.iterdir()) + + def test_symlink_at_store_path_is_refused(self, tmp_path): + victim = tmp_path / "victim.json" + victim.write_text(json.dumps({"version": 1, "accounts": {}})) + path = tmp_path / "context_windows.json" + path.symlink_to(victim) + obs = WindowObserver(path) + assert obs.active_clamp("gpt-5.6-sol") is None + # The victim itself was never consumed as the store. + assert victim.read_text() + + def test_directory_at_store_path_is_survived(self, tmp_path): + path = tmp_path / "context_windows.json" + path.mkdir() + obs = WindowObserver(path) + assert obs.active_clamp("gpt-5.6-sol") is None + + def test_oversized_file_is_quarantined(self, tmp_path): + path = tmp_path / "context_windows.json" + path.write_bytes(b"x" * (wo._MAX_STORE_BYTES + 1)) + WindowObserver(path) + assert any(p.name.startswith("context_windows.json.corrupt-") for p in tmp_path.iterdir()) + + def test_corrupt_json_is_quarantined_not_repaired(self, tmp_path): + path = tmp_path / "context_windows.json" + path.write_text("{not json") + WindowObserver(path) + quarantined = [ + p for p in tmp_path.iterdir() if p.name.startswith("context_windows.json.corrupt-") + ] + assert len(quarantined) == 1 + assert quarantined[0].read_text() == "{not json" # preserved verbatim + + def test_off_schema_store_is_quarantined(self, tmp_path): + path = tmp_path / "context_windows.json" + path.write_text(json.dumps({"version": 99, "accounts": {}})) + obs = WindowObserver(path) + assert obs.view()["accounts"] == {} + + +class TestClampQualification: + async def test_cross_account_retry_records_both_but_derives_no_clamp(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(key=ACCT_A), response=_acceptance(key=ACCT_B)) + assert obs.active_clamp("gpt-5.6-sol") is None + view = obs.view()["accounts"] + assert view[ACCT_A]["models"]["gpt-5.6-sol"]["lowest_rejection_bound"] == 930_001 + assert view[ACCT_B]["models"]["gpt-5.6-sol"]["highest_accepted_input"] == 408_004 + + async def test_missing_acceptance_usage_records_occurrence_only(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(tokens=None), response=_acceptance(tokens=None)) + assert obs.active_clamp("gpt-5.6-sol") is None + record = obs.view()["accounts"][ACCT_A]["models"]["gpt-5.6-sol"] + assert record["overflow_occurrences"] == 1 + assert record["lowest_rejection_bound"] is None + assert record["highest_accepted_input"] is None + + async def test_model_mismatch_derives_no_clamp(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue( + overflow=_overflow(model="gpt-5.6-sol"), + response=_acceptance(model="gpt-5.5"), + ) + assert obs.active_clamp("gpt-5.6-sol") is None + assert obs.active_clamp("gpt-5.5") is None + + async def test_non_overflow_error_is_ignored(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(code="other"), response=_acceptance()) + assert obs.view()["accounts"] == {} + + async def test_missing_account_keys_disqualify_scoped_evidence(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(key=None), response=_acceptance(key=None)) + assert obs.view()["accounts"] == {} + + async def test_estimate_shaped_junk_never_qualifies(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue( + overflow=_overflow(tokens=930_001), + response=SimpleNamespace( + server_input_tokens="408004", # strings are not evidence + account_key=ACCT_A, + provenance_model="gpt-5.6-sol", + ), + ) + assert obs.active_clamp("gpt-5.6-sol") is None + + +class TestDownwardOnlyMergeAndTTL: + async def test_lower_evidence_replaces_with_fresh_ttl(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=500_000)) + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=400_000)) + assert obs.active_clamp("gpt-5.6-sol") == 400_000 + + async def test_higher_evidence_never_raises_a_live_clamp(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=400_000)) + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=500_000)) + assert obs.active_clamp("gpt-5.6-sol") == 400_000 + + async def test_expired_clamp_is_not_served_and_is_replaceable(self, tmp_path, monkeypatch): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=400_000)) + real_now = wo._utc_now + monkeypatch.setattr(wo, "_utc_now", lambda: real_now() + timedelta(hours=25)) + assert obs.active_clamp("gpt-5.6-sol") is None + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=500_000)) + assert obs.active_clamp("gpt-5.6-sol") == 500_000 + + async def test_active_clamp_is_minimum_across_accounts(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue( + overflow=_overflow(key=ACCT_A), response=_acceptance(key=ACCT_A, tokens=500_000) + ) + await obs.record_rescue( + overflow=_overflow(key=ACCT_B), response=_acceptance(key=ACCT_B, tokens=420_000) + ) + assert obs.active_clamp("gpt-5.6-sol") == 420_000 + + async def test_active_clamp_ignores_ineligible_accounts(self, tmp_path): + eligible = {ACCT_A} + obs = WindowObserver( + tmp_path / "context_windows.json", + eligible_account_keys=lambda: frozenset(eligible), + ) + await obs.record_rescue( + overflow=_overflow(key=ACCT_A), response=_acceptance(key=ACCT_A, tokens=500_000) + ) + await obs.record_rescue( + overflow=_overflow(key=ACCT_B), response=_acceptance(key=ACCT_B, tokens=420_000) + ) + assert obs.active_clamp("gpt-5.6-sol") == 500_000 + eligible.clear() + assert obs.active_clamp("gpt-5.6-sol") is None + + async def test_account_clamps_are_active_eligible_and_management_ready(self, tmp_path): + eligible = {ACCT_A} + obs = WindowObserver( + tmp_path / "context_windows.json", + eligible_account_keys=lambda: frozenset(eligible), + ) + await obs.record_rescue( + overflow=_overflow(key=ACCT_A), response=_acceptance(key=ACCT_A, tokens=500_000) + ) + await obs.record_rescue( + overflow=_overflow(key=ACCT_B), response=_acceptance(key=ACCT_B, tokens=420_000) + ) + rows = obs.account_clamps() + assert rows == [ + { + "account_key": ACCT_A, + "model": "gpt-5.6-sol", + "value": 500_000, + "set_at": rows[0]["set_at"], + "expires_at": rows[0]["expires_at"], + "source": "rescue", + } + ] + + async def test_eligible_provider_none_or_failure_fails_open_not_stale(self, tmp_path): + obs = WindowObserver( + tmp_path / "context_windows.json", + eligible_account_keys=lambda: None, + ) + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + assert obs.active_clamp("gpt-5.6-sol") is None + + obs.set_eligible_account_keys_provider( + lambda: (_ for _ in ()).throw(RuntimeError("pool unavailable")) + ) + assert obs.active_clamp("gpt-5.6-sol") is None + + +class TestForfeitInvariant: + async def test_write_failure_forfeits_durability_never_the_request(self, tmp_path, monkeypatch): + obs = _observer(tmp_path) + + def _boom(): + raise OSError("disk full") + + monkeypatch.setattr(obs, "_persist_locked", _boom) + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + # In-memory evidence still protects this process... + assert obs.active_clamp("gpt-5.6-sol") == 408_004 + # ...but nothing was persisted. + assert not (tmp_path / "context_windows.json").exists() + + async def test_every_entry_point_is_total_on_junk(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=object(), response=object()) + await obs.record_rescue(overflow=None, response=None) + assert obs.active_clamp(object()) is None + assert await obs.clear_account("not-a-key") == 0 + assert obs.view()["accounts"] == {} + + +class TestPersistFdDiscipline: + async def test_fdopen_failure_leaks_no_fd_and_no_temp_file(self, tmp_path, monkeypatch): + obs = _observer(tmp_path) + real_fdopen = os.fdopen + + def _boom(fd, *a, **kw): + raise OSError("fdopen refused") + + monkeypatch.setattr(os, "fdopen", _boom) + fd_dir = "/proc/self/fd" + before = len(os.listdir(fd_dir)) + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + monkeypatch.setattr(os, "fdopen", real_fdopen) + assert len(os.listdir(fd_dir)) == before # the raw fd was closed + assert not any(p.name.startswith(".context_windows") for p in tmp_path.iterdir()) + # The observation still serves this process from memory. + assert obs.active_clamp("gpt-5.6-sol") == 408_004 + + async def test_crashed_write_never_corrupts_the_published_store(self, tmp_path, monkeypatch): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=500_000)) + published = (tmp_path / "context_windows.json").read_bytes() + + def _boom(fd): + raise OSError("device error") + + monkeypatch.setattr(os, "fsync", _boom) + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=400_000)) + # The atomic-replacement contract: the prior published bytes survive + # a crashed write untouched, and no temp debris remains. + assert (tmp_path / "context_windows.json").read_bytes() == published + assert not any(p.name.startswith(".context_windows") for p in tmp_path.iterdir()) + + async def test_cancelled_writer_drains_before_second_transaction(self, tmp_path, monkeypatch): + obs = _observer(tmp_path) + real_persist = obs._persist_locked + first_entered = threading.Event() + release_first = threading.Event() + calls = 0 + + def controlled_persist(state): + nonlocal calls + calls += 1 + if calls == 1: + first_entered.set() + assert release_first.wait(5) + real_persist(state) + + monkeypatch.setattr(obs, "_persist_locked", controlled_persist) + writer_a = asyncio.create_task( + obs.record_rescue( + overflow=_overflow(key=ACCT_A), + response=_acceptance(key=ACCT_A, tokens=500_000), + ) + ) + assert await asyncio.to_thread(first_entered.wait, 5) + writer_a.cancel() + writer_b = asyncio.create_task( + obs.record_rescue( + overflow=_overflow(key=ACCT_B), + response=_acceptance(key=ACCT_B, tokens=420_000), + ) + ) + await asyncio.sleep(0.05) + assert calls == 1 # B cannot enter while A's worker still owns the transaction. + release_first.set() + with pytest.raises(asyncio.CancelledError): + await writer_a + await writer_b + on_disk = WindowObserver(tmp_path / "context_windows.json").view() + assert set(on_disk["accounts"]) == {ACCT_A, ACCT_B} + assert not any(p.name.startswith(".context_windows") for p in tmp_path.iterdir()) + + +class TestManualClear: + async def test_clear_is_account_scoped_and_preserves_bounds(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(key=ACCT_A), response=_acceptance(key=ACCT_A)) + await obs.record_rescue( + overflow=_overflow(key=ACCT_B), response=_acceptance(key=ACCT_B, tokens=420_000) + ) + assert await obs.clear_account(ACCT_A) == 1 + # B's clamp survives; A's bounds history survives its clamp. + assert obs.active_clamp("gpt-5.6-sol") == 420_000 + record = obs.view()["accounts"][ACCT_A]["models"]["gpt-5.6-sol"] + assert record["clamp"] is None + assert record["lowest_rejection_bound"] == 930_001 + + async def test_failed_clear_is_truthful_and_retains_state(self, tmp_path, monkeypatch): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + published = (tmp_path / "context_windows.json").read_bytes() + + def fail(_state=None): + raise OSError("disk full") + + monkeypatch.setattr(obs, "_persist_locked", fail) + with pytest.raises(WindowObserverMutationError): + await obs.clear_account(ACCT_A) + assert obs.active_clamp("gpt-5.6-sol") == 408_004 + assert (tmp_path / "context_windows.json").read_bytes() == published + + async def test_model_scoped_clear(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + await obs.record_rescue( + overflow=_overflow(model="gpt-5.5", tokens=272_000), + response=_acceptance(model="gpt-5.5", tokens=250_000), + ) + assert await obs.clear_account(ACCT_A, model="gpt-5.5") == 1 + assert obs.active_clamp("gpt-5.5") is None + assert obs.active_clamp("gpt-5.6-sol") == 408_004 + + async def test_view_is_a_defensive_copy_with_expiry_flags(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + view = obs.view() + assert view["accounts"][ACCT_A]["models"]["gpt-5.6-sol"]["clamp"]["expired"] is False + view["accounts"].clear() + assert obs.view()["accounts"] # internal state untouched + + +class TestSurfaceGuardArms: + """The non-fatal guard arms are load-bearing: a broken observer must + never break compaction, rescue, or a spawn.""" + + class _BrokenObserver: + def active_clamp(self, model): + raise RuntimeError("observer wedged") + + async def record_rescue(self, *, overflow, response): + raise RuntimeError("observer wedged") + + def test_chat_clamp_lookup_survives_a_broken_observer(self): + runner = _chat_runner(_ChatGateway(None), self._BrokenObserver()) + assert runner._observed_clamp("gpt-5.6-sol") is None + + async def test_chat_evidence_recording_survives_a_broken_observer(self): + runner = _chat_runner(_ChatGateway(None), self._BrokenObserver()) + await runner._record_window_evidence(_overflow(), SimpleNamespace()) + + def test_agent_clamp_lookup_survives_a_broken_observer(self): + from src.discord.native_tools.agents_tasks import _observer_clamp + + assert _observer_clamp(self._BrokenObserver(), "gpt-5.6-sol") is None + + async def test_agent_recorder_survives_a_broken_observer(self): + from src.discord.native_tools.agents_tasks import _make_evidence_recorder + + recorder = _make_evidence_recorder(self._BrokenObserver()) + await recorder(_overflow(), {"text": "ok"}) + + +class TestResolverIntegration: + def test_snapshot_for_codex_config_threads_the_clamp(self): + from src.llm.context_budget import snapshot_for_codex_config + + cfg = SimpleNamespace(context_budget_overrides=None, context_utilization=60) + unclamped = snapshot_for_codex_config("gpt-5.6-sol", cfg, max_context_chars=None) + clamped = snapshot_for_codex_config( + "gpt-5.6-sol", cfg, max_context_chars=None, observed_clamp=300_000 + ) + assert unclamped.clamp_applied is False + assert clamped.clamp_applied is True + assert clamped.effective_budget == 300_000 + assert clamped.primary_chars < unclamped.primary_chars + + +# --------------------------------------------------------------------------- +# Surface hooks: chat, loop, agent +# --------------------------------------------------------------------------- + + +class _CaptureObserver: + def __init__(self, clamp=None): + self._clamp = clamp + self.recorded: list[tuple] = [] + + def active_clamp(self, model): + return self._clamp + + async def record_rescue(self, *, overflow, response): + self.recorded.append((overflow, response)) + + +def _chat_runner(gateway, observer): + from src.discord.tool_loop import ToolLoopRunner + + runner = ToolLoopRunner.__new__(ToolLoopRunner) + runner._llm_gateway = gateway + runner._get_config = lambda: SimpleNamespace(openai_codex=None) + runner._get_context_compressor = lambda: None + runner._get_compression_stats = lambda: None + runner._window_observer = observer + + async def _fake_error_done(st, api_err): + return ("terminal", str(api_err)) + + runner._llm_error_done = _fake_error_done + return runner + + +class _ChatGateway: + def __init__(self, script): + self.client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="xhigh") + self.codex_client = self.client + self.ollama_client = None + self.kimi_client = None + self.script = script + self.calls = 0 + + def capture_serving_identity(self, config=None): + from src.discord.llm_gateway import LLMServingIdentity + + return LLMServingIdentity( + provider="codex", + client=self.client, + model=self.client.model, + reasoning_effort=self.client.reasoning_effort, + ) + + def capacity_breaker_for(self, model=None, provider=None): + return None + + def recovery_policy(self): + from src.llm.recovery import RecoveryPolicy + + return RecoveryPolicy(deadline_seconds=30.0) + + def notify_generation_success(self, provider): + pass + + async def call_with_tools(self, *, messages, system, tools, **kwargs): + self.calls += 1 + return await self.script(self.calls) + + +def _chat_st(messages): + from src.discord.response_guards import StuckLoopTracker + from src.trajectories.saver import TrajectoryTurn + from src.turn_state.durability import TurnDurability + + class _NullCM: + async def __aenter__(self): + return self + + async def __aexit__(self, *a): + return False + + return SimpleNamespace( + chat_cap=3, + iteration=0, + stuck_tracker=StuckLoopTracker(), + wait_judgment_pending=False, + _cancel=asyncio.Event(), + _trajectory=TrajectoryTurn(), + trace=None, + _ch_id="c1", + _req_id="r1", + message=SimpleNamespace( + channel=SimpleNamespace(id=1, typing=lambda: _NullCM()), content="hi" + ), + messages=messages, + tools_used_in_loop=[], + tools=[], + system_prompt="sys", + user_id="u1", + durability=TurnDurability.disabled(), + _boundary_request_start=max(0, len(messages) - 2), + _boundary_elided_replay=0, + _boundary_envelope_len=2, + _char_latch=None, + _rescue_passes=0, + _gen_identity=None, + ) + + +def _big_history(n, size): + return [ + {"role": "user" if i % 2 == 0 else "assistant", "content": f"h{i}:" + "y" * size} + for i in range(n) + ] + + +_ENVELOPE = [ + {"role": "developer", "content": "preamble"}, + {"role": "user", "content": "CURRENT: do the thing"}, +] + + +class TestChatSurfaceHooks: + async def test_rescued_chat_success_records_the_evidence_pair(self): + observer = _CaptureObserver() + overflow = _overflow() + + async def script(n): + if n == 1: + raise overflow + return SimpleNamespace( + text="ok", + tool_calls=[], + stop_reason="end_turn", + server_input_tokens=408_004, + account_key=ACCT_A, + provenance_model="gpt-5.6-sol", + ) + + gw = _ChatGateway(script) + runner = _chat_runner(gw, observer) + st = _chat_st(_big_history(60, 20_000) + list(_ENVELOPE)) + kind, val = await runner._call_llm(st) + assert kind == "ok" + assert len(observer.recorded) == 1 + got_overflow, got_response = observer.recorded[0] + assert got_overflow is overflow # the exact overflow error object + assert got_response is val + + async def test_unrescued_chat_success_records_nothing(self): + observer = _CaptureObserver() + + async def script(n): + return SimpleNamespace(text="ok", tool_calls=[], stop_reason="end_turn") + + runner = _chat_runner(_ChatGateway(script), observer) + st = _chat_st(_big_history(4, 100) + list(_ENVELOPE)) + kind, _val = await runner._call_llm(st) + assert kind == "ok" + assert observer.recorded == [] + + def test_chat_soft_pass_consumes_the_active_clamp(self): + """Same payload, only the clamp differs: 800K chars sits under sol's + unclamped 1.277M-char target (no compression) but far over the + clamped 575K target (compression fires).""" + from src.llm.context_compressor import estimate_message_chars + + payload = ( + _big_history(6, 1_000) + + list(_ENVELOPE) + + [ + m + for i in range(50) + for m in ( + { + "role": "assistant", + "content": [ + {"type": "tool_use", "id": f"t{i}", "name": "read_file", "input": {}}, + ], + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": f"t{i}", + "content": "r" * 16_000, + }, + ], + }, + ) + ] + ) + compressor_cfg = SimpleNamespace(max_context_chars=None, keep_recent_iterations=3) + + def run(observer): + gw = _ChatGateway(None) + runner = _chat_runner(gw, observer) + runner._get_context_compressor = lambda: compressor_cfg + st = _chat_st([dict(m) for m in payload]) + # The payload already carries iterations: the request envelope + # sits after the 6 history messages, not at the list tail. + st._boundary_request_start = 6 + st.iteration = 1 + assert runner._maybe_compress(st, gw.client, SimpleNamespace(openai_codex=None)) + return estimate_message_chars(st.messages) + + before = estimate_message_chars(payload) + unclamped = run(None) + clamped = run(_CaptureObserver(clamp=300_000)) + assert before > 575_000 # the payload genuinely exceeds the clamped target + assert unclamped == before # untouched without a clamp + assert clamped <= 575_000 # compressed to the clamped target + + +class TestLoopSurfaceHooks: + async def test_rescued_loop_success_records_the_evidence_pair(self): + from src.llm.context_compressor import SurfaceBoundary + + observer = _CaptureObserver() + overflow = _overflow() + calls = {"n": 0} + + class _Client(SimpleNamespace): + async def chat_with_tools(self, *, messages, system, tools, **kwargs): + calls["n"] += 1 + if calls["n"] == 1: + raise overflow + return SimpleNamespace( + text="ok", + tool_calls=[], + stop_reason="end_turn", + provenance_provider="codex", + provenance_model="gpt-5.6-sol", + provenance_reasoning_effort="xhigh", + server_input_tokens=408_004, + account_key=ACCT_A, + ) + + client = _Client(model="gpt-5.6-sol", reasoning_effort="xhigh") + gw = _ChatGateway(None) + gw.client = client + gw.codex_client = client + runner = _chat_runner(gw, observer) + st = SimpleNamespace( + messages=[ + {"role": "user", "content": "Previous iteration results:\n" + "p" * 400_000}, + {"role": "assistant", "content": "Understood."}, + {"role": "user", "content": "GOAL: keep going"}, + ], + system_prompt="sys", + tools=[], + _boundary=SurfaceBoundary(request_start=2, envelope_len=1), + _char_latch=None, + context_recoveries=[], + _iteration_index=0, + ) + kind, val = await runner._call_loop_llm(st) + assert kind == "ok" + assert len(observer.recorded) == 1 + assert observer.recorded[0][0] is overflow + assert observer.recorded[0][1] is val + + +class TestAgentSurfaceHooks: + def test_generation_budget_snapshot_consumes_the_clamp(self): + from src.discord.native_tools.agents_tasks import _generation_budget_snapshot + + client = SimpleNamespace(model="gpt-5.6-sol", reasoning_effort="xhigh") + cfg = SimpleNamespace(openai_codex=None) + unclamped = _generation_budget_snapshot(cfg, client, "gpt-5.6-sol", None) + clamped = _generation_budget_snapshot( + cfg, client, "gpt-5.6-sol", None, observer=_CaptureObserver(clamp=300_000) + ) + assert unclamped.clamp_applied is False + assert clamped.clamp_applied is True + assert clamped.primary_chars < unclamped.primary_chars + + async def test_evidence_recorder_adapts_the_callback_dict(self, tmp_path): + from src.discord.native_tools.agents_tasks import _make_evidence_recorder + + obs = _observer(tmp_path) + recorder = _make_evidence_recorder(obs) + await recorder( + _overflow(), + { + "text": "ok", + "tool_calls": [], + "server_input_tokens": 408_004, + "account_key": ACCT_A, + "model": "gpt-5.6-sol", + }, + ) + assert obs.active_clamp("gpt-5.6-sol") == 408_004 + + def test_recorder_for_absent_observer_is_none(self): + from src.discord.native_tools.agents_tasks import _make_evidence_recorder + + assert _make_evidence_recorder(None) is None + + async def test_manager_rescue_success_invokes_the_recorder(self): + from src.agents.manager import AgentInfo, _call_llm_with_recovery + + agent = AgentInfo( + id="a1", + label="t", + goal="g", + channel_id="c1", + requester_id="u1", + requester_name="u", + ) + agent.messages = [{"role": "user", "content": "task"}] + [ + {"role": "assistant", "content": f"[Tool result: step{i}]\n" + "x" * 20_000} + for i in range(30) + ] + overflow = _overflow() + recorded = [] + + async def recorder(err, response): + recorded.append((err, response)) + + calls = {"n": 0} + + async def cb(messages, system_prompt, tools, generation_state=None): + calls["n"] += 1 + if calls["n"] == 1: + raise overflow + return {"text": "done", "tool_calls": []} + + response = await _call_llm_with_recovery( + agent, + cb, + "sys", + [], + rescue_ladder=(120_000,), + evidence_recorder=recorder, + ) + assert response == {"text": "done", "tool_calls": []} + assert len(recorded) == 1 + assert recorded[0][0] is overflow + assert recorded[0][1] is response + + +# --------------------------------------------------------------------------- +# Management API +# --------------------------------------------------------------------------- + + +def _api_app( + observer, + *, + max_context_chars: int | None = None, + runtime_max_context_chars: int | None | object = Ellipsis, +): + from src.web.api.llm_admin import register_context_windows + + runtime_value = ( + max_context_chars + if runtime_max_context_chars is Ellipsis + else runtime_max_context_chars + ) + runtime_config = ContextCompressionConfig(max_context_chars=runtime_value) + bot = SimpleNamespace( + config=SimpleNamespace( + openai_codex=SimpleNamespace( + context_budget_overrides={"gpt-5.5": 250_000}, + context_utilization=60, + context_compression=ContextCompressionConfig(max_context_chars=max_context_chars), + ) + ), + context_compressor=SimpleNamespace( + config=runtime_config, + resolved_max_context_chars=( + runtime_config.max_context_chars + if runtime_config.max_context_chars is not None + else 750_000 + ), + ), + services=SimpleNamespace(window_observer=observer), + ) + routes = web.RouteTableDef() + register_context_windows(routes, bot) + app = web.Application() + app.router.add_routes(routes) + return app + + +class TestContextWindowsApi: + async def test_get_serves_floors_overrides_clamps_and_both_resolutions(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance(tokens=300_000)) + app = _api_app(obs) + async with TestClient(TestServer(app)) as c: + body = await (await c.get("/api/context/windows")).json() + sol = body["models"]["gpt-5.6-sol"] + assert sol["floor"] == 921_601 + assert sol["active_clamp"] == 300_000 + assert sol["configured"]["clamp_applied"] is False + assert sol["effective"]["clamp_applied"] is True + assert sol["effective"]["effective_budget"] == 300_000 + assert sol["effective"]["primary_chars"] < sol["configured"]["primary_chars"] + assert sol["provenance"] == "temporary learned clamp" + assert sol["clamp_expires_at"] == body["clamps"][0]["expires_at"] + assert body["clamps"][0]["account_key"] == ACCT_A + five = body["models"]["gpt-5.5"] + assert five["override"] == 250_000 + assert five["configured"]["base_source"] == "override" + assert five["provenance"] == "override" + assert body["models"]["gpt-5.6-terra"]["provenance"] == "built-in" + # Raw evidence rides along, opaque keys only. + assert ACCT_A in body["evidence"]["accounts"] + + async def test_get_clamp_expiry_describes_only_applied_effective_minimum( + self, tmp_path, monkeypatch + ): + obs = _observer(tmp_path) + now = wo._utc_now() + monkeypatch.setattr(wo, "_utc_now", lambda: now) + await obs.record_rescue( + overflow=_overflow(key=ACCT_A), + response=_acceptance(key=ACCT_A, tokens=300_000), + ) + first_expiry = obs.account_clamps()[0]["expires_at"] + monkeypatch.setattr(wo, "_utc_now", lambda: now + timedelta(hours=1)) + await obs.record_rescue( + overflow=_overflow(key=ACCT_B), + response=_acceptance(key=ACCT_B, tokens=300_000), + ) + later_expiry = [ + row["expires_at"] for row in obs.account_clamps() if row["account_key"] == ACCT_B + ][0] + assert later_expiry > first_expiry + # Equal minimum remains effective until the later account expiry. + app = _api_app(obs) + async with TestClient(TestServer(app)) as c: + body = await (await c.get("/api/context/windows")).json() + assert body["models"]["gpt-5.6-sol"]["clamp_expires_at"] == later_expiry + + # Evidence above the configured budget is visible account evidence but + # is not an applied clamp and must not put expiry copy on built-in truth. + high = _observer(tmp_path / "high") + await high.record_rescue(overflow=_overflow(), response=_acceptance(tokens=1_000_000)) + app = _api_app(high) + async with TestClient(TestServer(app)) as c: + body = await (await c.get("/api/context/windows")).json() + sol = body["models"]["gpt-5.6-sol"] + assert sol["provenance"] == "built-in" + assert sol["clamp_expires_at"] is None + assert body["clamps"] + + async def test_get_preserves_raw_auto_ceiling(self, tmp_path): + obs = _observer(tmp_path) + app = _api_app(obs) + async with TestClient(TestServer(app)) as c: + body = await (await c.get("/api/context/windows")).json() + assert body["max_context_chars"] is None + assert body["models"]["gpt-5.6-sol"]["configured"]["primary_chars"] == 1_277_400 + + async def test_get_preserves_explicit_legacy_ceiling_verbatim(self, tmp_path): + obs = _observer(tmp_path) + app = _api_app(obs, max_context_chars=750_000) + async with TestClient(TestServer(app)) as c: + body = await (await c.get("/api/context/windows")).json() + assert body["max_context_chars"] == 750_000 + assert body["models"]["gpt-5.6-sol"]["configured"]["primary_chars"] == 750_000 + + async def test_get_without_boot_snapshot_uses_configured_runtime_truth(self, tmp_path): + obs = _observer(tmp_path) + app = _api_app(obs, max_context_chars=500_000) + async with TestClient(TestServer(app)) as c: + body = await (await c.get("/api/context/windows")).json() + assert body["runtime_max_context_chars"] == 500_000 + assert body["max_context_chars_pending_restart"] is False + + async def test_disabled_at_boot_reports_model_derived_runtime_target(self, tmp_path): + """A saved ceiling is inert when boot disabled the compressor.""" + from src.web.api.llm_admin import register_context_windows + + observer = _observer(tmp_path) + saved = ContextCompressionConfig(enabled=False, max_context_chars=500_000) + bot = SimpleNamespace( + config=SimpleNamespace( + openai_codex=SimpleNamespace( + context_budget_overrides={}, + context_utilization=60, + context_compression=saved, + ) + ), + context_compressor=None, + services=SimpleNamespace(window_observer=observer), + ) + bot.boot_config_snapshot = { + "openai_codex": {"context_compression": saved.model_dump()} + } + routes = web.RouteTableDef() + register_context_windows(routes, bot) + app = web.Application() + app.router.add_routes(routes) + async with TestClient(TestServer(app)) as client: + body = await (await client.get("/api/context/windows")).json() + + assert body["max_context_chars"] == 500_000 + assert body["runtime_max_context_chars"] is None + assert body["max_context_chars_pending_restart"] is False + assert body["models"]["gpt-5.6-sol"]["configured"]["primary_chars"] == 500_000 + assert body["models"]["gpt-5.6-sol"]["effective"]["primary_chars"] == 1_277_400 + + async def test_disabled_without_boot_snapshot_still_reports_runtime_truth( + self, tmp_path + ): + """No compressor means model-derived runtime math, never saved math.""" + from src.web.api.llm_admin import register_context_windows + + observer = _observer(tmp_path) + saved = ContextCompressionConfig(enabled=False, max_context_chars=500_000) + bot = SimpleNamespace( + config=SimpleNamespace( + openai_codex=SimpleNamespace( + context_budget_overrides={}, + context_utilization=60, + context_compression=saved, + ) + ), + context_compressor=None, + services=SimpleNamespace(window_observer=observer), + ) + routes = web.RouteTableDef() + register_context_windows(routes, bot) + app = web.Application() + app.router.add_routes(routes) + async with TestClient(TestServer(app)) as client: + body = await (await client.get("/api/context/windows")).json() + + assert body["runtime_max_context_chars"] is None + assert body["max_context_chars_pending_restart"] is None + assert body["models"]["gpt-5.6-sol"]["effective"]["primary_chars"] == 1_277_400 + + async def test_get_prefers_boot_snapshot_for_runtime_ceiling(self, tmp_path): + from src.web.api.llm_admin import register_context_windows + + observer = _observer(tmp_path) + saved = ContextCompressionConfig(max_context_chars=500_000) + bot = SimpleNamespace( + config=SimpleNamespace( + openai_codex=SimpleNamespace( + context_budget_overrides={}, + context_utilization=60, + context_compression=saved, + ) + ), + context_compressor=saved, + services=SimpleNamespace(window_observer=observer), + ) + bot.boot_config_snapshot = { + "openai_codex": { + "context_compression": ContextCompressionConfig( + max_context_chars=750_000 + ).model_dump() + } + } + routes = web.RouteTableDef() + register_context_windows(routes, bot) + app = web.Application() + app.router.add_routes(routes) + async with TestClient(TestServer(app)) as client: + body = await (await client.get("/api/context/windows")).json() + + assert body["runtime_max_context_chars"] == 750_000 + assert body["max_context_chars_pending_restart"] is True + assert body["models"]["gpt-5.6-sol"]["effective"]["primary_chars"] == 750_000 + + async def test_get_uses_production_direct_boot_compressor_shape(self, tmp_path): + from src.web.api.llm_admin import register_context_windows + + observer = _observer(tmp_path) + saved = ContextCompressionConfig(max_context_chars=500_000) + boot = ContextCompressionConfig(max_context_chars=750_000) + bot = SimpleNamespace( + config=SimpleNamespace( + openai_codex=SimpleNamespace( + context_budget_overrides={}, + context_utilization=60, + context_compression=saved, + ) + ), + # OdinClient stores this config object directly, not under .config. + context_compressor=boot, + services=SimpleNamespace(window_observer=observer), + ) + routes = web.RouteTableDef() + register_context_windows(routes, bot) + app = web.Application() + app.router.add_routes(routes) + async with TestClient(TestServer(app)) as client: + body = await (await client.get("/api/context/windows")).json() + + assert body["runtime_max_context_chars"] == 750_000 + assert body["max_context_chars_pending_restart"] is True + assert body["models"]["gpt-5.6-sol"]["configured"]["primary_chars"] == 500_000 + assert body["models"]["gpt-5.6-sol"]["effective"]["primary_chars"] == 750_000 + + async def test_get_distinguishes_saved_ceiling_from_boot_frozen_runtime(self, tmp_path): + obs = _observer(tmp_path) + app = _api_app( + obs, + max_context_chars=500_000, + runtime_max_context_chars=750_000, + ) + async with TestClient(TestServer(app)) as c: + body = await (await c.get("/api/context/windows")).json() + + sol = body["models"]["gpt-5.6-sol"] + assert body["max_context_chars"] == 500_000 + assert body["runtime_max_context_chars"] == 750_000 + assert body["max_context_chars_pending_restart"] is True + assert sol["configured"]["primary_chars"] == 500_000 + assert sol["effective"]["primary_chars"] == 750_000 + + async def test_failed_clear_returns_503_and_truthful_state(self, tmp_path, monkeypatch): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + monkeypatch.setattr( + obs, + "_persist_locked", + lambda _state=None: (_ for _ in ()).throw(OSError("disk full")), + ) + app = _api_app(obs) + async with TestClient(TestServer(app)) as c: + resp = await c.post("/api/context/windows/clear", json={"account_key": ACCT_A}) + assert resp.status == 503 + assert obs.active_clamp("gpt-5.6-sol") == 408_004 + + async def test_clear_endpoint_is_account_scoped(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue(overflow=_overflow(), response=_acceptance()) + app = _api_app(obs) + async with TestClient(TestServer(app)) as c: + resp = await c.post("/api/context/windows/clear", json={"account_key": ACCT_A}) + assert (await resp.json())["cleared"] == 1 + resp = await c.post("/api/context/windows/clear", json={}) + assert resp.status == 400 + resp = await c.post( + "/api/context/windows/clear", + data=b"not json", + headers={"Content-Type": "application/json"}, + ) + assert resp.status == 400 # malformed body degrades to empty + for body in ([], None, "not an object"): + resp = await c.post("/api/context/windows/clear", json=body) + assert resp.status == 400 + assert obs.active_clamp("gpt-5.6-sol") is None + + async def test_clear_endpoint_preserves_other_model_for_same_account(self, tmp_path): + obs = _observer(tmp_path) + await obs.record_rescue( + overflow=_overflow(model="gpt-5.6-sol"), + response=_acceptance(model="gpt-5.6-sol"), + ) + await obs.record_rescue( + overflow=_overflow(model="gpt-5.6-terra"), + response=_acceptance(model="gpt-5.6-terra", tokens=390_000), + ) + app = _api_app(obs) + async with TestClient(TestServer(app)) as c: + response = await c.post( + "/api/context/windows/clear", + json={"account_key": ACCT_A, "model": "gpt-5.6-sol"}, + ) + assert (await response.json())["cleared"] == 1 + assert obs.active_clamp("gpt-5.6-sol") is None + assert obs.active_clamp("gpt-5.6-terra") == 390_000 + + async def test_clear_without_observer_is_503(self): + app = _api_app(None) + async with TestClient(TestServer(app)) as c: + resp = await c.post("/api/context/windows/clear", json={"account_key": ACCT_A}) + assert resp.status == 503 diff --git a/ui/css/style.css b/ui/css/style.css index 1d7bbe8a..2988599a 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -4258,3 +4258,226 @@ a:focus-visible { .cfgc-review-values { grid-template-columns: minmax(0, 1fr); } .cfgc-review-values .odin-icon { margin: -0.1rem auto; transform: rotate(90deg); } } + +.llm-context-summary { + min-height: 58px; + padding: 0.48rem 0.65rem; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 0.2rem var(--hm-space-2); + background: rgba(7, 10, 15, 0.38); + border: 1px solid var(--hm-border-subtle); + border-radius: var(--hm-radius-md); +} +.llm-context-summary > span:first-child { color: var(--hm-text-dim); font-size: var(--hm-text-xs); } +.llm-context-summary strong { color: #e7d39d; font-size: var(--hm-text-sm); font-variant-numeric: tabular-nums; } +.llm-context-summary strong small { color: var(--hm-text-dim); font-size: 0.58rem; font-weight: 500; } +.llm-context-summary .llm-budget-provenance { justify-self: end; } +.llm-context-summary > small { grid-column: 1 / -1; color: var(--hm-text-dim); font-size: 0.58rem; } + +/* Context budgets — capability, policy, and observed evidence remain visibly + separate. The browser renders backend derivations verbatim; this layer is + presentation only. */ +.llm-context-budget-panel { + grid-column: 1 / -1; + overflow: hidden; + background: + radial-gradient(circle at 100% 0, rgba(201, 162, 78, 0.075), transparent 32rem), + rgba(8, 12, 18, 0.78); + border: 1px solid rgba(116, 96, 57, 0.58); + border-radius: var(--hm-radius-md); +} +.llm-context-budget-heading { + padding: var(--hm-space-4); + display: flex; + align-items: end; + justify-content: space-between; + gap: var(--hm-space-5); + border-bottom: 1px solid var(--hm-border-subtle); +} +.llm-context-budget-heading > div { min-width: 0; display: grid; gap: 0.15rem; } +.llm-context-budget-heading strong { color: var(--hm-text); font-size: var(--hm-text-sm); } +.llm-context-budget-heading > div > span { color: var(--hm-text-dim); font-size: var(--hm-text-xs); } +.llm-utilization-field { + min-width: 176px; + display: grid; + gap: 0.35rem; + color: var(--hm-text-muted); + font-size: var(--hm-text-xs); +} +.llm-utilization-input { display: grid; grid-template-columns: minmax(0, 1fr) 2rem; align-items: center; } +.llm-utilization-input .hm-input { border-radius: var(--hm-radius-md) 0 0 var(--hm-radius-md); text-align: right; font-variant-numeric: tabular-nums; } +.llm-utilization-input small { + align-self: stretch; + display: grid; + place-items: center; + color: var(--hm-accent); + background: rgba(201, 162, 78, 0.08); + border: 1px solid var(--hm-border); + border-left: 0; + border-radius: 0 var(--hm-radius-md) var(--hm-radius-md) 0; +} +.llm-context-budget-copy { + margin: 0; + padding: var(--hm-space-3) var(--hm-space-4); + color: var(--hm-text-dim); + background: rgba(7, 10, 15, 0.55); + border-bottom: 1px solid var(--hm-border-subtle); + font-size: var(--hm-text-xs); + line-height: 1.55; +} +.llm-context-budget-loading, +.llm-context-budget-error { + min-height: 112px; + padding: var(--hm-space-5); + display: flex; + align-items: center; + justify-content: center; + gap: var(--hm-space-3); + color: var(--hm-text-muted); + font-size: var(--hm-text-xs); +} +.llm-context-budget-loading .spinner { width: 16px; height: 16px; } +.llm-context-budget-error { color: var(--hm-danger); } +.llm-context-budget-table-wrap { overflow-x: auto; } +.llm-context-budget-table { min-width: 1120px; table-layout: fixed; } +.llm-context-budget-table th { padding: 0 var(--hm-space-3); font-size: 0.58rem; } +.llm-context-budget-table td { padding: var(--hm-space-3); font-size: var(--hm-text-xs); } +.llm-context-budget-table th:nth-child(1) { width: 15%; } +.llm-context-budget-table th:nth-child(2) { width: 11%; } +.llm-context-budget-table th:nth-child(3) { width: 19%; } +.llm-context-budget-table th:nth-child(4) { width: 12%; } +.llm-context-budget-table th:nth-child(5) { width: 13%; } +.llm-context-budget-table th:nth-child(6) { width: 14%; } +.llm-context-budget-table th:nth-child(7) { width: 16%; } +.llm-context-budget-table tbody tr.has-clamp td { background: rgba(211, 148, 49, 0.035); } +.llm-context-budget-table code, +.llm-clamp-card code { + color: #e4e8ef; + font-size: 0.72rem; + letter-spacing: -0.015em; +} +.llm-context-budget-table td > small { display: block; margin-top: 0.2rem; color: var(--hm-text-dim); font-size: 0.58rem; line-height: 1.3; } +.llm-budget-value { color: var(--hm-text-muted); font-variant-numeric: tabular-nums; } +.llm-budget-effective { color: #e7d39d; font-weight: 650; } +.llm-budget-override { display: flex; align-items: center; gap: var(--hm-space-2); } +.llm-budget-override .hm-input { min-width: 0; height: 34px; padding: 0 var(--hm-space-2); font-size: var(--hm-text-xs); font-variant-numeric: tabular-nums; } +.llm-budget-reset { + padding: 0; + color: var(--hm-text-dim); + border: 0; + background: transparent; + font-size: 0.62rem; + cursor: pointer; +} +.llm-budget-reset:hover { color: var(--hm-accent); } +.llm-budget-warning { color: var(--hm-warning-text) !important; } +.llm-budget-pending { + display: inline-flex; + margin-top: 0.3rem; + padding: 0.12rem 0.42rem; + color: var(--hm-warning-text); + background: rgba(217, 119, 6, 0.1); + border: 1px solid rgba(217, 119, 6, 0.23); + border-radius: var(--hm-radius-full); + font-size: 0.56rem; + font-weight: 650; +} +.llm-budget-provenance { + display: inline-flex; + align-items: center; + min-height: 22px; + padding: 0.16rem 0.48rem; + border: 1px solid transparent; + border-radius: var(--hm-radius-full); + font-size: 0.58rem; + font-weight: 650; + white-space: nowrap; +} +.llm-budget-provenance.is-built-in { color: #94a0b0; background: rgba(126, 136, 152, 0.09); border-color: rgba(126, 136, 152, 0.18); } +.llm-budget-provenance.is-override { color: #ccb474; background: rgba(201, 162, 78, 0.09); border-color: rgba(201, 162, 78, 0.2); } +.llm-budget-provenance.is-clamp { color: var(--hm-warning-text); background: rgba(217, 119, 6, 0.1); border-color: rgba(217, 119, 6, 0.23); } +.llm-clamp-list { padding: var(--hm-space-4); border-top: 1px solid rgba(217, 119, 6, 0.2); } +.llm-clamp-list-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--hm-space-4); margin-bottom: var(--hm-space-3); } +.llm-clamp-list-heading > div { display: grid; gap: 0.1rem; } +.llm-clamp-list-heading strong { color: var(--hm-warning-text); font-size: var(--hm-text-xs); } +.llm-clamp-list-heading > div > span { color: var(--hm-text-dim); font-size: 0.6rem; } +.llm-clamp-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--hm-space-3); } +.llm-clamp-card { + min-width: 0; + padding: var(--hm-space-3); + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 0.25rem var(--hm-space-3); + background: rgba(217, 119, 6, 0.045); + border: 1px solid rgba(217, 119, 6, 0.16); + border-radius: var(--hm-radius-md); +} +.llm-clamp-card > div { min-width: 0; display: flex; align-items: baseline; gap: var(--hm-space-2); overflow: hidden; } +.llm-clamp-card > div span { color: var(--hm-warning-text); font-size: var(--hm-text-xs); font-variant-numeric: tabular-nums; white-space: nowrap; } +.llm-clamp-card p { grid-column: 1; margin: 0; color: var(--hm-text-dim); font-size: 0.58rem; } +.llm-clamp-card .btn { grid-column: 2; grid-row: 1 / span 2; } +@media (max-width: 1180px) { + .llm-context-budget-table { min-width: 900px; } + .llm-clamp-grid { grid-template-columns: minmax(0, 1fr); } +} +@media (max-width: 900px) { + .llm-context-budget-panel + .llm-advanced-footer { align-items: stretch; flex-direction: column; } + .llm-context-budget-panel + .llm-advanced-footer .btn { width: 100%; justify-content: center; } + .llm-context-budget-heading { align-items: stretch; flex-direction: column; } + .llm-utilization-field { min-width: 0; } + .llm-context-budget-table-wrap { overflow: visible; } + .llm-context-budget-table, + .llm-context-budget-table tbody, + .llm-context-budget-table tr, + .llm-context-budget-table td { display: block; width: 100%; min-width: 0; } + .llm-context-budget-table thead { display: none; } + .llm-context-budget-table tbody { + padding: var(--hm-space-3); + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--hm-space-3); + } + .llm-context-budget-table tr { + margin-bottom: 0; + overflow: hidden; + background: rgba(17, 23, 32, 0.7); + border: 1px solid var(--hm-border-subtle); + border-radius: var(--hm-radius-md); + } + .llm-context-budget-table td { + min-height: 44px; + padding: var(--hm-space-2) var(--hm-space-3); + display: grid; + grid-template-columns: minmax(104px, 0.85fr) minmax(0, 1.15fr); + align-items: center; + gap: var(--hm-space-3); + text-align: right; + } + .llm-context-budget-table td::before { + content: attr(data-label); + color: var(--hm-text-dim); + font-size: 0.58rem; + font-weight: 650; + letter-spacing: 0.045em; + text-align: left; + text-transform: uppercase; + } + .llm-context-budget-table td:first-child { min-height: 48px; background: rgba(7, 10, 15, 0.45); } + .llm-context-budget-table td > small { margin: -0.1rem 0 0; grid-column: 2; } + .llm-budget-override { justify-content: flex-end; } + .llm-budget-override .hm-input { width: 150px; } + .llm-clamp-list-heading { align-items: flex-start; } + .llm-clamp-card { grid-template-columns: minmax(0, 1fr); } + .llm-clamp-card p, + .llm-clamp-card .btn { grid-column: 1; grid-row: auto; } + .llm-clamp-card .btn { width: 100%; justify-content: center; margin-top: var(--hm-space-2); } +} + +@media (max-width: 600px) { + .llm-context-budget-table tbody { grid-template-columns: minmax(0, 1fr); } + .llm-budget-override { gap: 0.35rem; } + .llm-budget-override .hm-input { width: min(105px, 100%); } +} diff --git a/ui/dist/assets/index-BuhcRniy.js b/ui/dist/assets/index-0WAOnBOa.js similarity index 68% rename from ui/dist/assets/index-BuhcRniy.js rename to ui/dist/assets/index-0WAOnBOa.js index 05c13a0f..43ce8914 100644 --- a/ui/dist/assets/index-BuhcRniy.js +++ b/ui/dist/assets/index-0WAOnBOa.js @@ -1,40 +1,40 @@ -var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s;var it=(e,t,s)=>Km(e,typeof t!="symbol"?t+"":t,s);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const i of a)if(i.type==="childList")for(const l of i.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&n(l)}).observe(document,{childList:!0,subtree:!0});function s(a){const i={};return a.integrity&&(i.integrity=a.integrity),a.referrerPolicy&&(i.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?i.credentials="include":a.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(a){if(a.ep)return;a.ep=!0;const i=s(a);fetch(a.href,i)}})();class Wm{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 n=this._persist?localStorage:sessionStorage;n.setItem("odin_token",t),this._persist&&localStorage.setItem("odin_persist","1"),s>0?n.setItem("odin_session_timeout",String(s)):n.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,n=null,{signal:a}={}){this._lastActivity=Date.now();const i={method:t,headers:this._headers(),signal:a};n!==null&&(i.body=JSON.stringify(n));const l=await fetch(s,i);if(l.status===401)throw new rl("Unauthorized");const r=await l.json().catch(()=>null);if(!l.ok){const o=(r==null?void 0:r.error)||`HTTP ${l.status}`;throw new ld(o,l.status,r)}return r}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 rl("Unauthorized");if(!s.ok){const n=await s.json().catch(()=>null);throw new ld((n==null?void 0:n.error)||`HTTP ${s.status}`,s.status,n)}return s.blob()}post(t,s){return this._request("POST",t,s)}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})}),n=await s.json().catch(()=>null);if(!s.ok)throw new rl((n==null?void 0:n.error)||"Login failed");return this.setToken(n.session_id,n.timeout_seconds||0),n}async logout(){try{await this.post("/api/auth/logout",{})}catch{}this.setToken("")}async check(){try{return await this.get("/api/status"),{ok:!0,needsAuth:!1}}catch(t){return t instanceof rl?{ok:!1,needsAuth:!0}:{ok:!1,needsAuth:!1,error:t.message}}}}class rl extends Error{constructor(t){super(t),this.name="AuthError"}}class ld extends Error{constructor(t,s,n){super(t),this.name="ApiError",this.status=s,this.data=n}}class Zm{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._lastPongTime=0,this._pingInterval=null,this._latency=-1,this._chatPending=!1,this._state="disconnected",this.onStatusChange=null,this.onStateChange=null,this.onLatency=null}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}_resetLatency(){if(this._latency=-1,this.onLatency)try{this.onLatency(-1)}catch{}}connect(){this._shouldConnect=!0,this._setState("connecting"),this._open()}disconnect(){this._shouldConnect=!1,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.onStateChange&&this.onStateChange(t,{attempt:this._reconnectAttempt,latency:this._latency}))}_startPing(){this._stopPing(),this._pingInterval=setInterval(()=>{if(this.connected)try{this._ws.send(JSON.stringify({type:"ping",ts:Date.now()}))}catch{}},15e3)}_stopPing(){this._pingInterval&&(clearInterval(this._pingInterval),this._pingInterval=null)}subscribe(t,s){this._handlers[t]||(this._handlers[t]=[]),this._handlers[t].push(s),t!=="chat"&&(this._subscriptions.add(t),this.connected&&this._ws.send(JSON.stringify({subscribe:t})))}unsubscribe(t,s){const n=this._handlers[t];if(n){const a=n.indexOf(s);a>=0&&n.splice(a,1),n.length===0&&t!=="chat"&&(this._subscriptions.delete(t),this.connected&&this._ws.send(JSON.stringify({unsubscribe:t})))}}on(t,s){return this.subscribe(t,s)}off(t,s){return this.unsubscribe(t,s)}sendChat(t,{channelId:s,userId:n,username:a}={}){return this.connected?(this._ws.send(JSON.stringify({type:"chat",content:t,channel_id:s||"web-default",user_id:n||void 0,username:a||void 0})),this._chatPending=!0,!0):!1}_open(){if(this._ws)return;let s=`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/api/ws`;this._api.token&&(s+=`?token=${encodeURIComponent(this._api.token)}`);const n=new WebSocket(s);this._ws=n;const a=()=>this._ws===n;n.onopen=()=>{if(a()){this._reconnectDelay=1e3,this._reconnectAttempt=0;for(const i of this._subscriptions)n.send(JSON.stringify({subscribe:i}));this._startPing(),this._setState("connected"),this.onStatusChange&&this.onStatusChange(!0)}},n.onmessage=i=>{if(!a())return;let l;try{l=JSON.parse(i.data)}catch{return}const r=l.type;if(r==="pong"){if(l.ts&&(this._latency=Date.now()-l.ts,this._lastPongTime=Date.now(),this.onLatency))try{this.onLatency(this._latency)}catch{}return}if(r==="log")for(const o of this._handlers.logs||[])o(l);else if(r==="event")for(const o of this._handlers.events||[])o(l);else if(r==="chat_response"||r==="chat_error"){this._chatPending=!1;for(const o of this._handlers.chat||[])o(l)}},n.onclose=()=>{if(a()){if(this._ws=null,this._stopPing(),this._resetLatency(),this._chatPending){this._chatPending=!1;const i={type:"chat_error",error:"Connection lost — the response may still complete; check session history."};for(const l of this._handlers.chat||[])l(i)}this.onStatusChange&&this.onStatusChange(!1),this._shouldConnect?(this._reconnectAttempt++,this._setState("reconnecting"),setTimeout(()=>this._open(),this._reconnectDelay),this._reconnectDelay=Math.min(this._reconnectDelay*2,this._maxReconnectDelay)):this._setState("disconnected")}},n.onerror=()=>{}}}const K=new Wm,Ke=new Zm(K);/** +var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s;var rt=(e,t,s)=>Km(e,typeof t!="symbol"?t+"":t,s);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const i of a)if(i.type==="childList")for(const l of i.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&n(l)}).observe(document,{childList:!0,subtree:!0});function s(a){const i={};return a.integrity&&(i.integrity=a.integrity),a.referrerPolicy&&(i.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?i.credentials="include":a.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(a){if(a.ep)return;a.ep=!0;const i=s(a);fetch(a.href,i)}})();class Wm{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 n=this._persist?localStorage:sessionStorage;n.setItem("odin_token",t),this._persist&&localStorage.setItem("odin_persist","1"),s>0?n.setItem("odin_session_timeout",String(s)):n.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,n=null,{signal:a}={}){this._lastActivity=Date.now();const i={method:t,headers:this._headers(),signal:a};n!==null&&(i.body=JSON.stringify(n));const l=await fetch(s,i);if(l.status===401)throw new ol("Unauthorized");const r=await l.json().catch(()=>null);if(!l.ok){const o=(r==null?void 0:r.error)||`HTTP ${l.status}`;throw new ld(o,l.status,r)}return r}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 ol("Unauthorized");if(!s.ok){const n=await s.json().catch(()=>null);throw new ld((n==null?void 0:n.error)||`HTTP ${s.status}`,s.status,n)}return s.blob()}post(t,s){return this._request("POST",t,s)}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})}),n=await s.json().catch(()=>null);if(!s.ok)throw new ol((n==null?void 0:n.error)||"Login failed");return this.setToken(n.session_id,n.timeout_seconds||0),n}async logout(){try{await this.post("/api/auth/logout",{})}catch{}this.setToken("")}async check(){try{return await this.get("/api/status"),{ok:!0,needsAuth:!1}}catch(t){return t instanceof ol?{ok:!1,needsAuth:!0}:{ok:!1,needsAuth:!1,error:t.message}}}}class ol extends Error{constructor(t){super(t),this.name="AuthError"}}class ld extends Error{constructor(t,s,n){super(t),this.name="ApiError",this.status=s,this.data=n}}class Zm{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._lastPongTime=0,this._pingInterval=null,this._latency=-1,this._chatPending=!1,this._state="disconnected",this.onStatusChange=null,this.onStateChange=null,this.onLatency=null}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}_resetLatency(){if(this._latency=-1,this.onLatency)try{this.onLatency(-1)}catch{}}connect(){this._shouldConnect=!0,this._setState("connecting"),this._open()}disconnect(){this._shouldConnect=!1,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.onStateChange&&this.onStateChange(t,{attempt:this._reconnectAttempt,latency:this._latency}))}_startPing(){this._stopPing(),this._pingInterval=setInterval(()=>{if(this.connected)try{this._ws.send(JSON.stringify({type:"ping",ts:Date.now()}))}catch{}},15e3)}_stopPing(){this._pingInterval&&(clearInterval(this._pingInterval),this._pingInterval=null)}subscribe(t,s){this._handlers[t]||(this._handlers[t]=[]),this._handlers[t].push(s),t!=="chat"&&(this._subscriptions.add(t),this.connected&&this._ws.send(JSON.stringify({subscribe:t})))}unsubscribe(t,s){const n=this._handlers[t];if(n){const a=n.indexOf(s);a>=0&&n.splice(a,1),n.length===0&&t!=="chat"&&(this._subscriptions.delete(t),this.connected&&this._ws.send(JSON.stringify({unsubscribe:t})))}}on(t,s){return this.subscribe(t,s)}off(t,s){return this.unsubscribe(t,s)}sendChat(t,{channelId:s,userId:n,username:a}={}){return this.connected?(this._ws.send(JSON.stringify({type:"chat",content:t,channel_id:s||"web-default",user_id:n||void 0,username:a||void 0})),this._chatPending=!0,!0):!1}_open(){if(this._ws)return;let s=`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/api/ws`;this._api.token&&(s+=`?token=${encodeURIComponent(this._api.token)}`);const n=new WebSocket(s);this._ws=n;const a=()=>this._ws===n;n.onopen=()=>{if(a()){this._reconnectDelay=1e3,this._reconnectAttempt=0;for(const i of this._subscriptions)n.send(JSON.stringify({subscribe:i}));this._startPing(),this._setState("connected"),this.onStatusChange&&this.onStatusChange(!0)}},n.onmessage=i=>{if(!a())return;let l;try{l=JSON.parse(i.data)}catch{return}const r=l.type;if(r==="pong"){if(l.ts&&(this._latency=Date.now()-l.ts,this._lastPongTime=Date.now(),this.onLatency))try{this.onLatency(this._latency)}catch{}return}if(r==="log")for(const o of this._handlers.logs||[])o(l);else if(r==="event")for(const o of this._handlers.events||[])o(l);else if(r==="chat_response"||r==="chat_error"){this._chatPending=!1;for(const o of this._handlers.chat||[])o(l)}},n.onclose=()=>{if(a()){if(this._ws=null,this._stopPing(),this._resetLatency(),this._chatPending){this._chatPending=!1;const i={type:"chat_error",error:"Connection lost — the response may still complete; check session history."};for(const l of this._handlers.chat||[])l(i)}this.onStatusChange&&this.onStatusChange(!1),this._shouldConnect?(this._reconnectAttempt++,this._setState("reconnecting"),setTimeout(()=>this._open(),this._reconnectDelay),this._reconnectDelay=Math.min(this._reconnectDelay*2,this._maxReconnectDelay)):this._setState("disconnected")}},n.onerror=()=>{}}}const G=new Wm,Ke=new Zm(G);/** * @vue/shared v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/function vs(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const qe={},Ea=[],Ft=()=>{},Ta=()=>!1,ra=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),or=e=>e.startsWith("onUpdate:"),je=Object.assign,Jo=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},Jm=Object.prototype.hasOwnProperty,et=(e,t)=>Jm.call(e,t),ge=Array.isArray,Aa=e=>Za(e)==="[object Map]",oa=e=>Za(e)==="[object Set]",rd=e=>Za(e)==="[object Date]",Ym=e=>Za(e)==="[object RegExp]",Ie=e=>typeof e=="function",Me=e=>typeof e=="string",Gt=e=>typeof e=="symbol",Qe=e=>e!==null&&typeof e=="object",Yo=e=>(Qe(e)||Ie(e))&&Ie(e.then)&&Ie(e.catch),df=Object.prototype.toString,Za=e=>df.call(e),Qm=e=>Za(e).slice(8,-1),cr=e=>Za(e)==="[object Object]",dr=e=>Me(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,gn=vs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Xm=vs("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),ur=e=>{const t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},eg=/-\w/g,at=ur(e=>e.replace(eg,t=>t.slice(1).toUpperCase())),tg=/\B([A-Z])/g,os=ur(e=>e.replace(tg,"-$1").toLowerCase()),ca=ur(e=>e.charAt(0).toUpperCase()+e.slice(1)),Ra=ur(e=>e?`on${ca(e)}`:""),It=(e,t)=>!Object.is(e,t),Ia=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},fr=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Nl=e=>{const t=Me(e)?Number(e):NaN;return isNaN(t)?e:t};let od;const pr=()=>od||(od=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function sg(e,t){return e+JSON.stringify(t,(s,n)=>typeof n=="function"?n.toString():n)}const ng="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",ag=vs(ng);function zi(e){if(ge(e)){const t={};for(let s=0;s{if(s){const n=s.split(lg);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function qi(e){let t="";if(Me(e))t=e;else if(ge(e))for(let s=0;sxn(s,t))}const hf=e=>!!(e&&e.__v_isRef===!0),mf=e=>Me(e)?e:e==null?"":ge(e)||Qe(e)&&(e.toString===df||!Ie(e.toString))?hf(e)?mf(e.value):JSON.stringify(e,gf,2):String(e),gf=(e,t)=>hf(t)?gf(e,t.value):Aa(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[n,a],i)=>(s[Br(n,i)+" =>"]=a,s),{})}:oa(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>Br(s))}:Gt(t)?Br(t):Qe(t)&&!ge(t)&&!cr(t)?String(t):t,Br=(e,t="")=>{var s;return Gt(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};function xg(e){return e==null?"initial":typeof e=="string"?e===""?" ":e:String(e)}/** +**/function ks(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const je={},Na=[],Ht=()=>{},Ia=()=>!1,ca=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),cr=e=>e.startsWith("onUpdate:"),ze=Object.assign,Jo=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},Jm=Object.prototype.hasOwnProperty,tt=(e,t)=>Jm.call(e,t),be=Array.isArray,La=e=>ei(e)==="[object Map]",da=e=>ei(e)==="[object Set]",rd=e=>ei(e)==="[object Date]",Ym=e=>ei(e)==="[object RegExp]",Ie=e=>typeof e=="function",Me=e=>typeof e=="string",Jt=e=>typeof e=="symbol",Xe=e=>e!==null&&typeof e=="object",Yo=e=>(Xe(e)||Ie(e))&&Ie(e.then)&&Ie(e.catch),df=Object.prototype.toString,ei=e=>df.call(e),Qm=e=>ei(e).slice(8,-1),dr=e=>ei(e)==="[object Object]",ur=e=>Me(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,bn=ks(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Xm=ks("bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"),fr=e=>{const t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},eg=/-\w/g,it=fr(e=>e.replace(eg,t=>t.slice(1).toUpperCase())),tg=/\B([A-Z])/g,ps=fr(e=>e.replace(tg,"-$1").toLowerCase()),ua=fr(e=>e.charAt(0).toUpperCase()+e.slice(1)),Da=fr(e=>e?`on${ua(e)}`:""),Lt=(e,t)=>!Object.is(e,t),Ma=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},pr=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Ll=e=>{const t=Me(e)?Number(e):NaN;return isNaN(t)?e:t};let od;const hr=()=>od||(od=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function sg(e,t){return e+JSON.stringify(t,(s,n)=>typeof n=="function"?n.toString():n)}const ng="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",ag=ks(ng);function Ji(e){if(be(e)){const t={};for(let s=0;s{if(s){const n=s.split(lg);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function Yi(e){let t="";if(Me(e))t=e;else if(be(e))for(let s=0;skn(s,t))}const hf=e=>!!(e&&e.__v_isRef===!0),mf=e=>Me(e)?e:e==null?"":be(e)||Xe(e)&&(e.toString===df||!Ie(e.toString))?hf(e)?mf(e.value):JSON.stringify(e,gf,2):String(e),gf=(e,t)=>hf(t)?gf(e,t.value):La(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[n,a],i)=>(s[Ur(n,i)+" =>"]=a,s),{})}:da(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>Ur(s))}:Jt(t)?Ur(t):Xe(t)&&!be(t)&&!dr(t)?String(t):t,Ur=(e,t="")=>{var s;return Jt(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};function xg(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 Et;class Qo{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&&Et&&(Et.active?(this.parent=Et,this.index=(Et.scopes||(Et.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(Et===this)Et=this.prevScope;else{let t=Et;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,n;for(s=0,n=this.effects.length;s0)return;if(gi){let t=gi;for(gi=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;mi;){let t=mi;for(mi=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function xf(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function _f(e){let t,s=e.depsTail,n=s;for(;n;){const a=n.prevDep;n.version===-1?(n===s&&(s=a),tc(n),wg(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=a}e.deps=t,e.depsTail=s}function mo(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(kf(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function kf(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ei)||(e.globalVersion=Ei,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!mo(e))))return;e.flags|=2;const t=e.dep,s=ot,n=Ds;ot=e,Ds=!0;try{xf(e);const a=e.fn(e._value);(t.version===0||It(a,e._value))&&(e.flags|=128,e._value=a,t.version++)}catch(a){throw t.version++,a}finally{ot=s,Ds=n,_f(e),e.flags&=-3}}function tc(e,t=!1){const{dep:s,prevSub:n,nextSub:a}=e;if(n&&(n.nextSub=a,e.prevSub=void 0),a&&(a.prevSub=n,e.nextSub=void 0),s.subs===e&&(s.subs=n,!n&&s.computed)){s.computed.flags&=-5;for(let i=s.computed.deps;i;i=i.nextDep)tc(i,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function wg(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}function Sg(e,t){e.effect instanceof Ci&&(e=e.effect.fn);const s=new Ci(e);t&&je(s,t);try{s.run()}catch(a){throw s.stop(),a}const n=s.run.bind(s);return n.effect=s,n}function Tg(e){e.effect.stop()}let Ds=!0;const wf=[];function _n(){wf.push(Ds),Ds=!1}function kn(){const e=wf.pop();Ds=e===void 0?!0:e}function cd(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=ot;ot=void 0;try{t()}finally{ot=s}}}let Ei=0;class Cg{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 mr{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(!ot||!Ds||ot===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==ot)s=this.activeLink=new Cg(ot,this),ot.deps?(s.prevDep=ot.depsTail,ot.depsTail.nextDep=s,ot.depsTail=s):ot.deps=ot.depsTail=s,Sf(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=ot.depsTail,s.nextDep=void 0,ot.depsTail.nextDep=s,ot.depsTail=s,ot.deps===s&&(ot.deps=n)}return s}trigger(t){this.version++,Ei++,this.notify(t)}notify(t){Xo();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{ec()}}}function Sf(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)Sf(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const Ll=new WeakMap,Qn=Symbol(""),go=Symbol(""),Ai=Symbol("");function jt(e,t,s){if(Ds&&ot){let n=Ll.get(e);n||Ll.set(e,n=new Map);let a=n.get(s);a||(n.set(s,a=new mr),a.map=n,a.key=s),a.track()}}function un(e,t,s,n,a,i){const l=Ll.get(e);if(!l){Ei++;return}const r=o=>{o&&o.trigger()};if(Xo(),t==="clear")l.forEach(r);else{const o=ge(e),c=o&&dr(s);if(o&&s==="length"){const d=Number(n);l.forEach((u,f)=>{(f==="length"||f===Ai||!Gt(f)&&f>=d)&&r(u)})}else switch((s!==void 0||l.has(void 0))&&r(l.get(s)),c&&r(l.get(Ai)),t){case"add":o?c&&r(l.get("length")):(r(l.get(Qn)),Aa(e)&&r(l.get(go)));break;case"delete":o||(r(l.get(Qn)),Aa(e)&&r(l.get(go)));break;case"set":Aa(e)&&r(l.get(Qn));break}}ec()}function Eg(e,t){const s=Ll.get(e);return s&&s.get(t)}function ma(e){const t=Ze(e);return t===e?t:(jt(t,"iterate",Ai),ds(e)?t:t.map(Ps))}function gr(e){return jt(e=Ze(e),"iterate",Ai),e}function Ws(e,t){return Js(e)?Fa(vn(e)?Ps(t):t):Ps(t)}const Ag={__proto__:null,[Symbol.iterator](){return Vr(this,Symbol.iterator,e=>Ws(this,e))},concat(...e){return ma(this).concat(...e.map(t=>ge(t)?ma(t):t))},entries(){return Vr(this,"entries",e=>(e[1]=Ws(this,e[1]),e))},every(e,t){return sn(this,"every",e,t,void 0,arguments)},filter(e,t){return sn(this,"filter",e,t,s=>s.map(n=>Ws(this,n)),arguments)},find(e,t){return sn(this,"find",e,t,s=>Ws(this,s),arguments)},findIndex(e,t){return sn(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return sn(this,"findLast",e,t,s=>Ws(this,s),arguments)},findLastIndex(e,t){return sn(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return sn(this,"forEach",e,t,void 0,arguments)},includes(...e){return jr(this,"includes",e)},indexOf(...e){return jr(this,"indexOf",e)},join(e){return ma(this).join(e)},lastIndexOf(...e){return jr(this,"lastIndexOf",e)},map(e,t){return sn(this,"map",e,t,void 0,arguments)},pop(){return ti(this,"pop")},push(...e){return ti(this,"push",e)},reduce(e,...t){return dd(this,"reduce",e,t)},reduceRight(e,...t){return dd(this,"reduceRight",e,t)},shift(){return ti(this,"shift")},some(e,t){return sn(this,"some",e,t,void 0,arguments)},splice(...e){return ti(this,"splice",e)},toReversed(){return ma(this).toReversed()},toSorted(e){return ma(this).toSorted(e)},toSpliced(...e){return ma(this).toSpliced(...e)},unshift(...e){return ti(this,"unshift",e)},values(){return Vr(this,"values",e=>Ws(this,e))}};function Vr(e,t,s){const n=gr(e),a=n[t]();return n!==e&&!ds(e)&&(a._next=a.next,a.next=()=>{const i=a._next();return i.done||(i.value=s(i.value)),i}),a}const Rg=Array.prototype;function sn(e,t,s,n,a,i){const l=gr(e),r=l!==e&&!ds(e),o=l[t];if(o!==Rg[t]){const u=o.apply(e,i);return r?Ps(u):u}let c=s;l!==e&&(r?c=function(u,f){return s.call(this,Ws(e,u),f,e)}:s.length>2&&(c=function(u,f){return s.call(this,u,f,e)}));const d=o.call(l,c,n);return r&&a?a(d):d}function dd(e,t,s,n){const a=gr(e),i=a!==e&&!ds(e);let l=s,r=!1;a!==e&&(i?(r=n.length===0,l=function(c,d,u){return r&&(r=!1,c=Ws(e,c)),s.call(this,c,Ws(e,d),u,e)}):s.length>3&&(l=function(c,d,u){return s.call(this,c,d,u,e)}));const o=a[t](l,...n);return r?Ws(e,o):o}function jr(e,t,s){const n=Ze(e);jt(n,"iterate",Ai);const a=n[t](...s);return(a===-1||a===!1)&&Gi(s[0])?(s[0]=Ze(s[0]),n[t](...s)):a}function ti(e,t,s=[]){_n(),Xo();const n=Ze(e)[t].apply(e,s);return ec(),kn(),n}const Ig=vs("__proto__,__v_isRef,__isVue"),Tf=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Gt));function Og(e){Gt(e)||(e=String(e));const t=Ze(this);return jt(t,"has",e),t.hasOwnProperty(e)}class Cf{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){if(s==="__v_skip")return t.__v_skip;const a=this._isReadonly,i=this._isShallow;if(s==="__v_isReactive")return!a;if(s==="__v_isReadonly")return a;if(s==="__v_isShallow")return i;if(s==="__v_raw")return n===(a?i?Nf:Of:i?If:Rf).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const l=ge(t);if(!a){let o;if(l&&(o=Ag[s]))return o;if(s==="hasOwnProperty")return Og}const r=Reflect.get(t,s,St(t)?t:n);if((Gt(s)?Tf.has(s):Ig(s))||(a||jt(t,"get",s),i))return r;if(St(r)){const o=l&&dr(s)?r:r.value;return a&&Qe(o)?Dl(o):o}return Qe(r)?a?Dl(r):Un(r):r}}class Ef extends Cf{constructor(t=!1){super(!1,t)}set(t,s,n,a){let i=t[s];const l=ge(t)&&dr(s);if(!this._isShallow){const c=Js(i);if(!ds(n)&&!Js(n)&&(i=Ze(i),n=Ze(n)),!l&&St(i)&&!St(n))return c||(i.value=n),!0}const r=l?Number(s)e,ol=e=>Reflect.getPrototypeOf(e);function Pg(e,t,s){return function(...n){const a=this.__v_raw,i=Ze(a),l=Aa(i),r=e==="entries"||e===Symbol.iterator&&l,o=e==="keys"&&l,c=a[e](...n),d=s?vo:t?Fa:Ps;return!t&&jt(i,"iterate",o?go:Qn),je(Object.create(c),{next(){const{value:u,done:f}=c.next();return f?{value:u,done:f}:{value:r?[d(u[0]),d(u[1])]:d(u),done:f}}})}}function cl(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Fg(e,t){const s={get(a){const i=this.__v_raw,l=Ze(i),r=Ze(a);e||(It(a,r)&&jt(l,"get",a),jt(l,"get",r));const{has:o}=ol(l),c=t?vo:e?Fa:Ps;if(o.call(l,a))return c(i.get(a));if(o.call(l,r))return c(i.get(r));i!==l&&i.get(a)},get size(){const a=this.__v_raw;return!e&&jt(Ze(a),"iterate",Qn),a.size},has(a){const i=this.__v_raw,l=Ze(i),r=Ze(a);return e||(It(a,r)&&jt(l,"has",a),jt(l,"has",r)),a===r?i.has(a):i.has(a)||i.has(r)},forEach(a,i){const l=this,r=l.__v_raw,o=Ze(r),c=t?vo:e?Fa:Ps;return!e&&jt(o,"iterate",Qn),r.forEach((d,u)=>a.call(i,c(d),c(u),l))}};return je(s,e?{add:cl("add"),set:cl("set"),delete:cl("delete"),clear:cl("clear")}:{add(a){const i=Ze(this),l=ol(i),r=Ze(a),o=!t&&!ds(a)&&!Js(a)?r:a;return l.has.call(i,o)||It(a,o)&&l.has.call(i,a)||It(r,o)&&l.has.call(i,r)||(i.add(o),un(i,"add",o,o)),this},set(a,i){!t&&!ds(i)&&!Js(i)&&(i=Ze(i));const l=Ze(this),{has:r,get:o}=ol(l);let c=r.call(l,a);c||(a=Ze(a),c=r.call(l,a));const d=o.call(l,a);return l.set(a,i),c?It(i,d)&&un(l,"set",a,i):un(l,"add",a,i),this},delete(a){const i=Ze(this),{has:l,get:r}=ol(i);let o=l.call(i,a);o||(a=Ze(a),o=l.call(i,a)),r&&r.call(i,a);const c=i.delete(a);return o&&un(i,"delete",a,void 0),c},clear(){const a=Ze(this),i=a.size!==0,l=a.clear();return i&&un(a,"clear",void 0,void 0),l}}),["keys","values","entries",Symbol.iterator].forEach(a=>{s[a]=Pg(a,e,t)}),s}function vr(e,t){const s=Fg(e,t);return(n,a,i)=>a==="__v_isReactive"?!e:a==="__v_isReadonly"?e:a==="__v_raw"?n:Reflect.get(et(s,a)&&a in n?s:n,a,i)}const $g={get:vr(!1,!1)},Ug={get:vr(!1,!0)},Bg={get:vr(!0,!1)},Hg={get:vr(!0,!0)},Rf=new WeakMap,If=new WeakMap,Of=new WeakMap,Nf=new WeakMap;function Vg(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Un(e){return Js(e)?e:br(e,!1,Ng,$g,Rf)}function sc(e){return br(e,!1,Dg,Ug,If)}function Dl(e){return br(e,!0,Lg,Bg,Of)}function jg(e){return br(e,!0,Mg,Hg,Nf)}function br(e,t,s,n,a){if(!Qe(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const i=a.get(e);if(i)return i;const l=Vg(Qm(e));if(l===0)return e;const r=new Proxy(e,l===2?n:s);return a.set(e,r),r}function vn(e){return Js(e)?vn(e.__v_raw):!!(e&&e.__v_isReactive)}function Js(e){return!!(e&&e.__v_isReadonly)}function ds(e){return!!(e&&e.__v_isShallow)}function Gi(e){return e?!!e.__v_raw:!1}function Ze(e){const t=e&&e.__v_raw;return t?Ze(t):e}function Lf(e){return!et(e,"__v_skip")&&Object.isExtensible(e)&&uf(e,"__v_skip",!0),e}const Ps=e=>Qe(e)?Un(e):e,Fa=e=>Qe(e)?Dl(e):e;function St(e){return e?e.__v_isRef===!0:!1}function h(e){return Df(e,!1)}function nc(e){return Df(e,!0)}function Df(e,t){return St(e)?e:new zg(e,t)}class zg{constructor(t,s){this.dep=new mr,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=s?t:Ze(t),this._value=s?t:Ps(t),this.__v_isShallow=s}get value(){return this.dep.track(),this._value}set value(t){const s=this._rawValue,n=this.__v_isShallow||ds(t)||Js(t);t=n?t:Ze(t),It(t,s)&&(this._rawValue=t,this._value=n?t:Ps(t),this.dep.trigger())}}function qg(e){e.dep&&e.dep.trigger()}function Zs(e){return St(e)?e.value:e}function Gg(e){return Ie(e)?e():Zs(e)}const Kg={get:(e,t,s)=>t==="__v_raw"?e:Zs(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const a=e[t];return St(a)&&!St(s)?(a.value=s,!0):Reflect.set(e,t,s,n)}};function ac(e){return vn(e)?e:new Proxy(e,Kg)}class Wg{constructor(t){this.__v_isRef=!0,this._value=void 0;const s=this.dep=new mr,{get:n,set:a}=t(s.track.bind(s),s.trigger.bind(s));this._get=n,this._set=a}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Mf(e){return new Wg(e)}function Zg(e){const t=ge(e)?new Array(e.length):{};for(const s in e)t[s]=Pf(e,s);return t}class Jg{constructor(t,s,n){this._object=t,this._defaultValue=n,this.__v_isRef=!0,this._value=void 0,this._key=Gt(s)?s:String(s),this._raw=Ze(t);let a=!0,i=t;if(!ge(t)||Gt(this._key)||!dr(this._key))do a=!Gi(i)||ds(i);while(a&&(i=i.__v_raw));this._shallow=a}get value(){let t=this._object[this._key];return this._shallow&&(t=Zs(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&St(this._raw[this._key])){const s=this._object[this._key];if(St(s)){s.value=t;return}}this._object[this._key]=t}get dep(){return Eg(this._raw,this._key)}}class Yg{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 Qg(e,t,s){return St(e)?e:Ie(e)?new Yg(e):Qe(e)&&arguments.length>1?Pf(e,t,s):h(e)}function Pf(e,t,s){return new Jg(e,t,s)}class Xg{constructor(t,s,n){this.fn=t,this.setter=s,this._value=void 0,this.dep=new mr(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ei-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&ot!==this)return yf(this,!0),!0}get value(){const t=this.dep.track();return kf(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function ev(e,t,s=!1){let n,a;return Ie(e)?n=e:(n=e.get,a=e.set),new Xg(n,a,s)}const tv={GET:"get",HAS:"has",ITERATE:"iterate"},sv={SET:"set",ADD:"add",DELETE:"delete",CLEAR:"clear"},dl={},Ml=new WeakMap;let Nn;function nv(){return Nn}function Ff(e,t=!1,s=Nn){if(s){let n=Ml.get(s);n||Ml.set(s,n=[]),n.push(e)}}function av(e,t,s=qe){const{immediate:n,deep:a,once:i,scheduler:l,augmentJob:r,call:o}=s,c=_=>a?_:ds(_)||a===!1||a===0?fn(_,1):fn(_);let d,u,f,p,b=!1,y=!1;if(St(e)?(u=()=>e.value,b=ds(e)):vn(e)?(u=()=>c(e),b=!0):ge(e)?(y=!0,b=e.some(_=>vn(_)||ds(_)),u=()=>e.map(_=>{if(St(_))return _.value;if(vn(_))return c(_);if(Ie(_))return o?o(_,2):_()})):Ie(e)?t?u=o?()=>o(e,2):e:u=()=>{if(f){_n();try{f()}finally{kn()}}const _=Nn;Nn=d;try{return o?o(e,3,[p]):e(p)}finally{Nn=_}}:u=Ft,t&&a){const _=u,S=a===!0?1/0:a;u=()=>fn(_(),S)}const A=vf(),O=()=>{d.stop(),A&&A.active&&Jo(A.effects,d)};if(i&&t){const _=t;t=(...S)=>{const g=_(...S);return O(),g}}let x=y?new Array(e.length).fill(dl):dl;const m=_=>{if(!(!(d.flags&1)||!d.dirty&&!_))if(t){const S=d.run();if(_||a||b||(y?S.some((g,w)=>It(g,x[w])):It(S,x))){f&&f();const g=Nn;Nn=d;try{const w=[S,x===dl?void 0:y&&x[0]===dl?[]:x,p];x=S,o?o(t,3,w):t(...w)}finally{Nn=g}}}else d.run()};return r&&r(m),d=new Ci(u),d.scheduler=l?()=>l(m,!1):m,p=_=>Ff(_,!1,d),f=d.onStop=()=>{const _=Ml.get(d);if(_){if(o)o(_,4);else for(const S of _)S();Ml.delete(d)}},t?n?m(!0):x=d.run():l?l(m.bind(null,!0),!0):d.run(),O.pause=d.pause.bind(d),O.resume=d.resume.bind(d),O.stop=O,O}function fn(e,t=1/0,s){if(t<=0||!Qe(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,St(e))fn(e.value,t,s);else if(ge(e))for(let n=0;n{fn(n,t,s)});else if(cr(e)){for(const n in e)fn(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&fn(e[n],t,s)}return e}/** +**/let At;class Qo{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&&At&&(At.active?(this.parent=At,this.index=(At.scopes||(At.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(At===this)At=this.prevScope;else{let t=At;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,n;for(s=0,n=this.effects.length;s0)return;if(ki){let t=ki;for(ki=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;_i;){let t=_i;for(_i=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function xf(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function _f(e){let t,s=e.depsTail,n=s;for(;n;){const a=n.prevDep;n.version===-1?(n===s&&(s=a),tc(n),wg(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=a}e.deps=t,e.depsTail=s}function mo(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(kf(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function kf(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Li)||(e.globalVersion=Li,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!mo(e))))return;e.flags|=2;const t=e.dep,s=ot,n=zs;ot=e,zs=!0;try{xf(e);const a=e.fn(e._value);(t.version===0||Lt(a,e._value))&&(e.flags|=128,e._value=a,t.version++)}catch(a){throw t.version++,a}finally{ot=s,zs=n,_f(e),e.flags&=-3}}function tc(e,t=!1){const{dep:s,prevSub:n,nextSub:a}=e;if(n&&(n.nextSub=a,e.prevSub=void 0),a&&(a.prevSub=n,e.nextSub=void 0),s.subs===e&&(s.subs=n,!n&&s.computed)){s.computed.flags&=-5;for(let i=s.computed.deps;i;i=i.nextDep)tc(i,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function wg(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}function Sg(e,t){e.effect instanceof Ni&&(e=e.effect.fn);const s=new Ni(e);t&&ze(s,t);try{s.run()}catch(a){throw s.stop(),a}const n=s.run.bind(s);return n.effect=s,n}function Tg(e){e.effect.stop()}let zs=!0;const wf=[];function wn(){wf.push(zs),zs=!1}function Sn(){const e=wf.pop();zs=e===void 0?!0:e}function cd(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=ot;ot=void 0;try{t()}finally{ot=s}}}let Li=0;class Cg{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(!ot||!zs||ot===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==ot)s=this.activeLink=new Cg(ot,this),ot.deps?(s.prevDep=ot.depsTail,ot.depsTail.nextDep=s,ot.depsTail=s):ot.deps=ot.depsTail=s,Sf(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=ot.depsTail,s.nextDep=void 0,ot.depsTail.nextDep=s,ot.depsTail=s,ot.deps===s&&(ot.deps=n)}return s}trigger(t){this.version++,Li++,this.notify(t)}notify(t){Xo();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{ec()}}}function Sf(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)Sf(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const Dl=new WeakMap,ea=Symbol(""),go=Symbol(""),Di=Symbol("");function Kt(e,t,s){if(zs&&ot){let n=Dl.get(e);n||Dl.set(e,n=new Map);let a=n.get(s);a||(n.set(s,a=new gr),a.map=n,a.key=s),a.track()}}function pn(e,t,s,n,a,i){const l=Dl.get(e);if(!l){Li++;return}const r=o=>{o&&o.trigger()};if(Xo(),t==="clear")l.forEach(r);else{const o=be(e),c=o&&ur(s);if(o&&s==="length"){const d=Number(n);l.forEach((u,f)=>{(f==="length"||f===Di||!Jt(f)&&f>=d)&&r(u)})}else switch((s!==void 0||l.has(void 0))&&r(l.get(s)),c&&r(l.get(Di)),t){case"add":o?c&&r(l.get("length")):(r(l.get(ea)),La(e)&&r(l.get(go)));break;case"delete":o||(r(l.get(ea)),La(e)&&r(l.get(go)));break;case"set":La(e)&&r(l.get(ea));break}}ec()}function Eg(e,t){const s=Dl.get(e);return s&&s.get(t)}function xa(e){const t=Je(e);return t===e?t:(Kt(t,"iterate",Di),ms(e)?t:t.map(js))}function vr(e){return Kt(e=Je(e),"iterate",Di),e}function Xs(e,t){return tn(e)?za(yn(e)?js(t):t):js(t)}const Ag={__proto__:null,[Symbol.iterator](){return zr(this,Symbol.iterator,e=>Xs(this,e))},concat(...e){return xa(this).concat(...e.map(t=>be(t)?xa(t):t))},entries(){return zr(this,"entries",e=>(e[1]=Xs(this,e[1]),e))},every(e,t){return an(this,"every",e,t,void 0,arguments)},filter(e,t){return an(this,"filter",e,t,s=>s.map(n=>Xs(this,n)),arguments)},find(e,t){return an(this,"find",e,t,s=>Xs(this,s),arguments)},findIndex(e,t){return an(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return an(this,"findLast",e,t,s=>Xs(this,s),arguments)},findLastIndex(e,t){return an(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return an(this,"forEach",e,t,void 0,arguments)},includes(...e){return Vr(this,"includes",e)},indexOf(...e){return Vr(this,"indexOf",e)},join(e){return xa(this).join(e)},lastIndexOf(...e){return Vr(this,"lastIndexOf",e)},map(e,t){return an(this,"map",e,t,void 0,arguments)},pop(){return ri(this,"pop")},push(...e){return ri(this,"push",e)},reduce(e,...t){return dd(this,"reduce",e,t)},reduceRight(e,...t){return dd(this,"reduceRight",e,t)},shift(){return ri(this,"shift")},some(e,t){return an(this,"some",e,t,void 0,arguments)},splice(...e){return ri(this,"splice",e)},toReversed(){return xa(this).toReversed()},toSorted(e){return xa(this).toSorted(e)},toSpliced(...e){return xa(this).toSpliced(...e)},unshift(...e){return ri(this,"unshift",e)},values(){return zr(this,"values",e=>Xs(this,e))}};function zr(e,t,s){const n=vr(e),a=n[t]();return n!==e&&!ms(e)&&(a._next=a.next,a.next=()=>{const i=a._next();return i.done||(i.value=s(i.value)),i}),a}const Rg=Array.prototype;function an(e,t,s,n,a,i){const l=vr(e),r=l!==e&&!ms(e),o=l[t];if(o!==Rg[t]){const u=o.apply(e,i);return r?js(u):u}let c=s;l!==e&&(r?c=function(u,f){return s.call(this,Xs(e,u),f,e)}:s.length>2&&(c=function(u,f){return s.call(this,u,f,e)}));const d=o.call(l,c,n);return r&&a?a(d):d}function dd(e,t,s,n){const a=vr(e),i=a!==e&&!ms(e);let l=s,r=!1;a!==e&&(i?(r=n.length===0,l=function(c,d,u){return r&&(r=!1,c=Xs(e,c)),s.call(this,c,Xs(e,d),u,e)}):s.length>3&&(l=function(c,d,u){return s.call(this,c,d,u,e)}));const o=a[t](l,...n);return r?Xs(e,o):o}function Vr(e,t,s){const n=Je(e);Kt(n,"iterate",Di);const a=n[t](...s);return(a===-1||a===!1)&&Qi(s[0])?(s[0]=Je(s[0]),n[t](...s)):a}function ri(e,t,s=[]){wn(),Xo();const n=Je(e)[t].apply(e,s);return ec(),Sn(),n}const Ig=ks("__proto__,__v_isRef,__isVue"),Tf=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Jt));function Og(e){Jt(e)||(e=String(e));const t=Je(this);return Kt(t,"has",e),t.hasOwnProperty(e)}class Cf{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){if(s==="__v_skip")return t.__v_skip;const a=this._isReadonly,i=this._isShallow;if(s==="__v_isReactive")return!a;if(s==="__v_isReadonly")return a;if(s==="__v_isShallow")return i;if(s==="__v_raw")return n===(a?i?Nf:Of:i?If:Rf).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const l=be(t);if(!a){let o;if(l&&(o=Ag[s]))return o;if(s==="hasOwnProperty")return Og}const r=Reflect.get(t,s,St(t)?t:n);if((Jt(s)?Tf.has(s):Ig(s))||(a||Kt(t,"get",s),i))return r;if(St(r)){const o=l&&ur(s)?r:r.value;return a&&Xe(o)?Ml(o):o}return Xe(r)?a?Ml(r):Hn(r):r}}class Ef extends Cf{constructor(t=!1){super(!1,t)}set(t,s,n,a){let i=t[s];const l=be(t)&&ur(s);if(!this._isShallow){const c=tn(i);if(!ms(n)&&!tn(n)&&(i=Je(i),n=Je(n)),!l&&St(i)&&!St(n))return c||(i.value=n),!0}const r=l?Number(s)e,cl=e=>Reflect.getPrototypeOf(e);function Pg(e,t,s){return function(...n){const a=this.__v_raw,i=Je(a),l=La(i),r=e==="entries"||e===Symbol.iterator&&l,o=e==="keys"&&l,c=a[e](...n),d=s?vo:t?za:js;return!t&&Kt(i,"iterate",o?go:ea),ze(Object.create(c),{next(){const{value:u,done:f}=c.next();return f?{value:u,done:f}:{value:r?[d(u[0]),d(u[1])]:d(u),done:f}}})}}function dl(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Fg(e,t){const s={get(a){const i=this.__v_raw,l=Je(i),r=Je(a);e||(Lt(a,r)&&Kt(l,"get",a),Kt(l,"get",r));const{has:o}=cl(l),c=t?vo:e?za:js;if(o.call(l,a))return c(i.get(a));if(o.call(l,r))return c(i.get(r));i!==l&&i.get(a)},get size(){const a=this.__v_raw;return!e&&Kt(Je(a),"iterate",ea),a.size},has(a){const i=this.__v_raw,l=Je(i),r=Je(a);return e||(Lt(a,r)&&Kt(l,"has",a),Kt(l,"has",r)),a===r?i.has(a):i.has(a)||i.has(r)},forEach(a,i){const l=this,r=l.__v_raw,o=Je(r),c=t?vo:e?za:js;return!e&&Kt(o,"iterate",ea),r.forEach((d,u)=>a.call(i,c(d),c(u),l))}};return ze(s,e?{add:dl("add"),set:dl("set"),delete:dl("delete"),clear:dl("clear")}:{add(a){const i=Je(this),l=cl(i),r=Je(a),o=!t&&!ms(a)&&!tn(a)?r:a;return l.has.call(i,o)||Lt(a,o)&&l.has.call(i,a)||Lt(r,o)&&l.has.call(i,r)||(i.add(o),pn(i,"add",o,o)),this},set(a,i){!t&&!ms(i)&&!tn(i)&&(i=Je(i));const l=Je(this),{has:r,get:o}=cl(l);let c=r.call(l,a);c||(a=Je(a),c=r.call(l,a));const d=o.call(l,a);return l.set(a,i),c?Lt(i,d)&&pn(l,"set",a,i):pn(l,"add",a,i),this},delete(a){const i=Je(this),{has:l,get:r}=cl(i);let o=l.call(i,a);o||(a=Je(a),o=l.call(i,a)),r&&r.call(i,a);const c=i.delete(a);return o&&pn(i,"delete",a,void 0),c},clear(){const a=Je(this),i=a.size!==0,l=a.clear();return i&&pn(a,"clear",void 0,void 0),l}}),["keys","values","entries",Symbol.iterator].forEach(a=>{s[a]=Pg(a,e,t)}),s}function br(e,t){const s=Fg(e,t);return(n,a,i)=>a==="__v_isReactive"?!e:a==="__v_isReadonly"?e:a==="__v_raw"?n:Reflect.get(tt(s,a)&&a in n?s:n,a,i)}const $g={get:br(!1,!1)},Bg={get:br(!1,!0)},Ug={get:br(!0,!1)},Hg={get:br(!0,!0)},Rf=new WeakMap,If=new WeakMap,Of=new WeakMap,Nf=new WeakMap;function zg(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Hn(e){return tn(e)?e:yr(e,!1,Ng,$g,Rf)}function sc(e){return yr(e,!1,Dg,Bg,If)}function Ml(e){return yr(e,!0,Lg,Ug,Of)}function Vg(e){return yr(e,!0,Mg,Hg,Nf)}function yr(e,t,s,n,a){if(!Xe(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const i=a.get(e);if(i)return i;const l=zg(Qm(e));if(l===0)return e;const r=new Proxy(e,l===2?n:s);return a.set(e,r),r}function yn(e){return tn(e)?yn(e.__v_raw):!!(e&&e.__v_isReactive)}function tn(e){return!!(e&&e.__v_isReadonly)}function ms(e){return!!(e&&e.__v_isShallow)}function Qi(e){return e?!!e.__v_raw:!1}function Je(e){const t=e&&e.__v_raw;return t?Je(t):e}function Lf(e){return!tt(e,"__v_skip")&&Object.isExtensible(e)&&uf(e,"__v_skip",!0),e}const js=e=>Xe(e)?Hn(e):e,za=e=>Xe(e)?Ml(e):e;function St(e){return e?e.__v_isRef===!0:!1}function h(e){return Df(e,!1)}function nc(e){return Df(e,!0)}function Df(e,t){return St(e)?e:new jg(e,t)}class jg{constructor(t,s){this.dep=new gr,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=s?t:Je(t),this._value=s?t:js(t),this.__v_isShallow=s}get value(){return this.dep.track(),this._value}set value(t){const s=this._rawValue,n=this.__v_isShallow||ms(t)||tn(t);t=n?t:Je(t),Lt(t,s)&&(this._rawValue=t,this._value=n?t:js(t),this.dep.trigger())}}function qg(e){e.dep&&e.dep.trigger()}function en(e){return St(e)?e.value:e}function Gg(e){return Ie(e)?e():en(e)}const Kg={get:(e,t,s)=>t==="__v_raw"?e:en(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const a=e[t];return St(a)&&!St(s)?(a.value=s,!0):Reflect.set(e,t,s,n)}};function ac(e){return yn(e)?e:new Proxy(e,Kg)}class Wg{constructor(t){this.__v_isRef=!0,this._value=void 0;const s=this.dep=new gr,{get:n,set:a}=t(s.track.bind(s),s.trigger.bind(s));this._get=n,this._set=a}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Mf(e){return new Wg(e)}function Zg(e){const t=be(e)?new Array(e.length):{};for(const s in e)t[s]=Pf(e,s);return t}class Jg{constructor(t,s,n){this._object=t,this._defaultValue=n,this.__v_isRef=!0,this._value=void 0,this._key=Jt(s)?s:String(s),this._raw=Je(t);let a=!0,i=t;if(!be(t)||Jt(this._key)||!ur(this._key))do a=!Qi(i)||ms(i);while(a&&(i=i.__v_raw));this._shallow=a}get value(){let t=this._object[this._key];return this._shallow&&(t=en(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&St(this._raw[this._key])){const s=this._object[this._key];if(St(s)){s.value=t;return}}this._object[this._key]=t}get dep(){return Eg(this._raw,this._key)}}class Yg{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 Qg(e,t,s){return St(e)?e:Ie(e)?new Yg(e):Xe(e)&&arguments.length>1?Pf(e,t,s):h(e)}function Pf(e,t,s){return new Jg(e,t,s)}class Xg{constructor(t,s,n){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=Li-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&ot!==this)return yf(this,!0),!0}get value(){const t=this.dep.track();return kf(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function ev(e,t,s=!1){let n,a;return Ie(e)?n=e:(n=e.get,a=e.set),new Xg(n,a,s)}const tv={GET:"get",HAS:"has",ITERATE:"iterate"},sv={SET:"set",ADD:"add",DELETE:"delete",CLEAR:"clear"},ul={},Pl=new WeakMap;let Dn;function nv(){return Dn}function Ff(e,t=!1,s=Dn){if(s){let n=Pl.get(s);n||Pl.set(s,n=[]),n.push(e)}}function av(e,t,s=je){const{immediate:n,deep:a,once:i,scheduler:l,augmentJob:r,call:o}=s,c=_=>a?_:ms(_)||a===!1||a===0?hn(_,1):hn(_);let d,u,f,p,b=!1,y=!1;if(St(e)?(u=()=>e.value,b=ms(e)):yn(e)?(u=()=>c(e),b=!0):be(e)?(y=!0,b=e.some(_=>yn(_)||ms(_)),u=()=>e.map(_=>{if(St(_))return _.value;if(yn(_))return c(_);if(Ie(_))return o?o(_,2):_()})):Ie(e)?t?u=o?()=>o(e,2):e:u=()=>{if(f){wn();try{f()}finally{Sn()}}const _=Dn;Dn=d;try{return o?o(e,3,[p]):e(p)}finally{Dn=_}}:u=Ht,t&&a){const _=u,S=a===!0?1/0:a;u=()=>hn(_(),S)}const E=vf(),I=()=>{d.stop(),E&&E.active&&Jo(E.effects,d)};if(i&&t){const _=t;t=(...S)=>{const g=_(...S);return I(),g}}let x=y?new Array(e.length).fill(ul):ul;const m=_=>{if(!(!(d.flags&1)||!d.dirty&&!_))if(t){const S=d.run();if(_||a||b||(y?S.some((g,w)=>Lt(g,x[w])):Lt(S,x))){f&&f();const g=Dn;Dn=d;try{const w=[S,x===ul?void 0:y&&x[0]===ul?[]:x,p];x=S,o?o(t,3,w):t(...w)}finally{Dn=g}}}else d.run()};return r&&r(m),d=new Ni(u),d.scheduler=l?()=>l(m,!1):m,p=_=>Ff(_,!1,d),f=d.onStop=()=>{const _=Pl.get(d);if(_){if(o)o(_,4);else for(const S of _)S();Pl.delete(d)}},t?n?m(!0):x=d.run():l?l(m.bind(null,!0),!0):d.run(),I.pause=d.pause.bind(d),I.resume=d.resume.bind(d),I.stop=I,I}function hn(e,t=1/0,s){if(t<=0||!Xe(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,St(e))hn(e.value,t,s);else if(be(e))for(let n=0;n{hn(n,t,s)});else if(dr(e)){for(const n in e)hn(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&hn(e[n],t,s)}return e}/** * @vue/runtime-core v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const $f=[];function iv(e){$f.push(e)}function lv(){$f.pop()}function rv(e,t){}const ov={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"},cv={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 Ja(e,t,s,n){try{return n?e(...n):e()}catch(a){da(a,t,s)}}function ms(e,t,s,n){if(Ie(e)){const a=Ja(e,t,s,n);return a&&Yo(a)&&a.catch(i=>{da(i,t,s)}),a}if(ge(e)){const a=[];for(let i=0;i>>1,a=Qt[n],i=Ii(a);i=Ii(s)?Qt.push(e):Qt.splice(uv(t),0,e),e.flags|=1,Bf()}}function Bf(){Pl||(Pl=Uf.then(Hf))}function Ri(e){ge(e)?Oa.push(...e):Ln&&e.id===-1?Ln.splice(_a+1,0,e):e.flags&1||(Oa.push(e),e.flags|=1),Bf()}function ud(e,t,s=Gs+1){for(;sIi(s)-Ii(n));if(Oa.length=0,Ln){Ln.push(...t);return}for(Ln=t,_a=0;_ae.id==null?e.flags&2?-1:1/0:e.id;function Hf(e){try{for(Gs=0;Gska.emit(a,...i)),ul=[]):typeof window<"u"&&window.HTMLElement&&!((n=(s=window.navigator)==null?void 0:s.userAgent)!=null&&n.includes("jsdom"))?((t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push(i=>{Vf(i,t)}),setTimeout(()=>{ka||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,ul=[])},3e3)):ul=[]}let Pt=null,yr=null;function Oi(e){const t=Pt;return Pt=e,yr=e&&e.type.__scopeId||null,t}function fv(e){yr=e}function pv(){yr=null}const hv=e=>lc;function lc(e,t=Pt,s){if(!t||e._n)return e;const n=(...a)=>{n._d&&Mi(-1);const i=Oi(t);let l;try{l=e(...a)}finally{Oi(i),n._d&&Mi(1)}return l};return n._n=!0,n._c=!0,n._d=!0,n}function mv(e,t){if(Pt===null)return e;const s=Ji(Pt),n=e.dirs||(e.dirs=[]);for(let a=0;a1)return s&&Ie(t)?t.call(n&&n.proxy):t}}function gv(){return!!(ts()||Xn)}const jf=Symbol.for("v-scx"),zf=()=>ws(jf);function vv(e,t){return Ki(e,null,t)}function bv(e,t){return Ki(e,null,{flush:"post"})}function qf(e,t){return Ki(e,null,{flush:"sync"})}function es(e,t,s){return Ki(e,t,s)}function Ki(e,t,s=qe){const{immediate:n,deep:a,flush:i,once:l}=s,r=je({},s),o=t&&n||!t&&i!=="post";let c;if(aa){if(i==="sync"){const p=zf();c=p.__watcherHandles||(p.__watcherHandles=[])}else if(!o){const p=()=>{};return p.stop=Ft,p.resume=Ft,p.pause=Ft,p}}const d=Mt;r.call=(p,b,y)=>ms(p,d,b,y);let u=!1;i==="post"?r.scheduler=p=>{kt(p,d&&d.suspense)}:i!=="sync"&&(u=!0,r.scheduler=(p,b)=>{b?p():ic(p)}),r.augmentJob=p=>{t&&(p.flags|=4),u&&(p.flags|=2,d&&(p.id=d.uid,p.i=d))};const f=av(e,t,r);return aa&&(c?c.push(f):o&&f()),f}function yv(e,t,s){const n=this.proxy,a=Me(e)?e.includes(".")?Gf(n,e):()=>n[e]:e.bind(n,n);let i;Ie(t)?i=t:(i=t.handler,s=t);const l=Ya(this),r=Ki(a,i.bind(n),s);return l(),r}function Gf(e,t){const s=t.split(".");return()=>{let n=e;for(let a=0;ae.__isTeleport,Wn=e=>e&&(e.disabled||e.disabled===""),xv=e=>e&&(e.defer||e.defer===""),fd=e=>typeof SVGElement<"u"&&e instanceof SVGElement,pd=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,bo=(e,t)=>{const s=e&&e.to;return Me(s)?t?t(s):null:s},_v={name:"Teleport",__isTeleport:!0,process(e,t,s,n,a,i,l,r,o,c){const{mc:d,pc:u,pbc:f,o:{insert:p,querySelector:b,createText:y,createComment:A,parentNode:O}}=c,x=Wn(t.props);let{dynamicChildren:m}=t;const _=(w,T,C)=>{w.shapeFlag&16&&d(w.children,T,C,a,i,l,r,o)},S=(w=t)=>{const T=Wn(w.props),C=w.target=bo(w.props,b),M=yo(C,w,y,p);C&&(l!=="svg"&&fd(C)?l="svg":l!=="mathml"&&pd(C)&&(l="mathml"),a&&a.isCE&&(a.ce._teleportTargets||(a.ce._teleportTargets=new Set)).add(C),T||(_(w,C,M),di(w,!1)))},g=w=>{const T=()=>{if(In.get(w)===T){if(In.delete(w),Wn(w.props)){const C=O(w.el)||s;_(w,C,w.anchor),di(w,!0)}S(w)}};In.set(w,T),kt(T,i)};if(e==null){const w=t.el=y(""),T=t.anchor=y("");if(p(w,s,n),p(T,s,n),xv(t.props)||i&&i.pendingBranch){g(t);return}x&&(_(t,s,T),di(t,!0)),S()}else{t.el=e.el;const w=t.anchor=e.anchor,T=In.get(e);if(T){T.flags|=8,In.delete(e),g(t);return}t.targetStart=e.targetStart;const C=t.target=e.target,M=t.targetAnchor=e.targetAnchor,B=Wn(e.props),$=B?s:C,I=B?w:M;if(l==="svg"||fd(C)?l="svg":(l==="mathml"||pd(C))&&(l="mathml"),m?(f(e.dynamicChildren,m,$,a,i,l,r),vc(e,t,!0)):o||u(e,t,$,I,a,i,l,r,!1),x)B?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):fl(t,s,w,c,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const j=t.target=bo(t.props,b);j&&fl(t,j,null,c,0)}else B&&fl(t,C,M,c,1);di(t,x)}},remove(e,t,s,{um:n,o:{remove:a}},i){const{shapeFlag:l,children:r,anchor:o,targetStart:c,targetAnchor:d,target:u,props:f}=e,p=i||!Wn(f),b=In.get(e);if(b&&(b.flags|=8,In.delete(e)),u&&(a(c),a(d)),i&&a(o),!b&&l&16)for(let y=0;y{e.isMounted=!0}),wr(()=>{e.isUnmounting=!0}),e}const ys=[Function,Array],oc={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:ys,onEnter:ys,onAfterEnter:ys,onEnterCancelled:ys,onBeforeLeave:ys,onLeave:ys,onAfterLeave:ys,onLeaveCancelled:ys,onBeforeAppear:ys,onAppear:ys,onAfterAppear:ys,onAppearCancelled:ys},Zf=e=>{const t=e.subTree;return t.component?Zf(t.component):t},Sv={name:"BaseTransition",props:oc,setup(e,{slots:t}){const s=ts(),n=rc();return()=>{const a=t.default&&xr(t.default(),!0),i=a&&a.length?Jf(a):s.subTree?Np():void 0;if(!i)return;const l=Ze(e),{mode:r}=l;if(n.isLeaving)return zr(i);const o=hd(i);if(!o)return zr(i);let c=$a(o,l,n,s,u=>c=u);o.type!==yt&&wn(o,c);let d=s.subTree&&hd(s.subTree);if(d&&d.type!==yt&&!Ls(d,o)&&Zf(s).type!==yt){let u=$a(d,l,n,s);if(wn(d,u),r==="out-in"&&o.type!==yt)return n.isLeaving=!0,u.afterLeave=()=>{n.isLeaving=!1,s.job.flags&8||s.update(),delete u.afterLeave,d=void 0},zr(i);r==="in-out"&&o.type!==yt?u.delayLeave=(f,p,b)=>{const y=Qf(n,d);y[String(d.key)]=d,f[xs]=()=>{p(),f[xs]=void 0,delete c.delayedLeave,d=void 0},c.delayedLeave=()=>{b(),delete c.delayedLeave,d=void 0}}:d=void 0}else d&&(d=void 0);return i}}};function Jf(e){let t=e[0];if(e.length>1){for(const s of e)if(s.type!==yt){t=s;break}}return t}const Yf=Sv;function Qf(e,t){const{leavingVNodes:s}=e;let n=s.get(t.type);return n||(n=Object.create(null),s.set(t.type,n)),n}function $a(e,t,s,n,a){const{appear:i,mode:l,persisted:r=!1,onBeforeEnter:o,onEnter:c,onAfterEnter:d,onEnterCancelled:u,onBeforeLeave:f,onLeave:p,onAfterLeave:b,onLeaveCancelled:y,onBeforeAppear:A,onAppear:O,onAfterAppear:x,onAppearCancelled:m}=t,_=String(e.key),S=Qf(s,e),g=(C,M)=>{C&&ms(C,n,9,M)},w=(C,M)=>{const B=M[1];g(C,M),ge(C)?C.every($=>$.length<=1)&&B():C.length<=1&&B()},T={mode:l,persisted:r,beforeEnter(C){let M=o;if(!s.isMounted)if(i)M=A||o;else return;C[xs]&&C[xs](!0);const B=S[_];B&&Ls(e,B)&&B.el[xs]&&B.el[xs](),g(M,[C])},enter(C){if(S[_]===e)return;let M=c,B=d,$=u;if(!s.isMounted)if(i)M=O||c,B=x||d,$=m||u;else return;let I=!1;C[si]=Y=>{I||(I=!0,Y?g($,[C]):g(B,[C]),T.delayedLeave&&T.delayedLeave(),C[si]=void 0)};const j=C[si].bind(null,!1);M?w(M,[C,j]):j()},leave(C,M){const B=String(e.key);if(C[si]&&C[si](!0),s.isUnmounting)return M();g(f,[C]);let $=!1;C[xs]=j=>{$||($=!0,M(),j?g(y,[C]):g(b,[C]),C[xs]=void 0,S[B]===e&&delete S[B])};const I=C[xs].bind(null,!1);S[B]=e,p?w(p,[C,I]):I()},clone(C){const M=$a(C,t,s,n,a);return a&&a(M),M}};return T}function zr(e){if(Zi(e))return e=Ys(e),e.children=null,e}function hd(e){if(!Zi(e))return Wf(e.type)&&e.children?Jf(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&&Ie(s.default))return s.default()}}function wn(e,t){e.shapeFlag&6&&e.component?(e.transition=t,wn(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 xr(e,t=!1,s){let n=[],a=0;for(let i=0;i1)for(let i=0;is.value,set:i=>s.value=i})}return s}function md(e,t){let s;return!!((s=Object.getOwnPropertyDescriptor(e,t))&&!s.configurable)}const $l=new WeakMap;function Na(e,t,s,n,a=!1){if(ge(e)){e.forEach((y,A)=>Na(y,t&&(ge(t)?t[A]:t),s,n,a));return}if(bn(n)&&!a){n.shapeFlag&512&&n.type.__asyncResolved&&n.component.subTree.component&&Na(e,t,s,n.component.subTree);return}const i=n.shapeFlag&4?Ji(n.component):n.el,l=a?null:i,{i:r,r:o}=e,c=t&&t.r,d=r.refs===qe?r.refs={}:r.refs,u=r.setupState,f=Ze(u),p=u===qe?Ta:y=>md(d,y)?!1:et(f,y),b=(y,A)=>!(A&&md(d,A));if(c!=null&&c!==o){if(gd(t),Me(c))d[c]=null,p(c)&&(u[c]=null);else if(St(c)){const y=t;b(c,y.k)&&(c.value=null),y.k&&(d[y.k]=null)}}if(Ie(o))Ja(o,r,12,[l,d]);else{const y=Me(o),A=St(o);if(y||A){const O=()=>{if(e.f){const x=y?p(o)?u[o]:d[o]:b()||!e.k?o.value:d[e.k];if(a)ge(x)&&Jo(x,i);else if(ge(x))x.includes(i)||x.push(i);else if(y)d[o]=[i],p(o)&&(u[o]=d[o]);else{const m=[i];b(o,e.k)&&(o.value=m),e.k&&(d[e.k]=m)}}else y?(d[o]=l,p(o)&&(u[o]=l)):A&&(b(o,e.k)&&(o.value=l),e.k&&(d[e.k]=l))};if(l){const x=()=>{O(),$l.delete(e)};x.id=-1,$l.set(e,x),kt(x,s)}else gd(e),O()}}}function gd(e){const t=$l.get(e);t&&(t.flags|=8,$l.delete(e))}let vd=!1;const ga=()=>{vd||(console.error("Hydration completed but contains mismatches."),vd=!0)},Ev=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",Av=e=>e.namespaceURI.includes("MathML"),pl=e=>{if(e.nodeType===1){if(Ev(e))return"svg";if(Av(e))return"mathml"}},Ca=e=>e.nodeType===8;function Rv(e){const{mt:t,p:s,o:{patchProp:n,createText:a,nextSibling:i,parentNode:l,remove:r,insert:o,createComment:c}}=e,d=(m,_)=>{if(!_.hasChildNodes()){s(null,m,_),Fl(),_._vnode=m;return}u(_.firstChild,m,null,null,null),Fl(),_._vnode=m},u=(m,_,S,g,w,T=!1)=>{T=T||!!_.dynamicChildren;const C=Ca(m)&&m.data==="[",M=()=>y(m,_,S,g,w,C),{type:B,ref:$,shapeFlag:I,patchFlag:j}=_;let Y=m.nodeType;_.el=m,j===-2&&(T=!1,_.dynamicChildren=null);let H=null;switch(B){case Pn:Y!==3?_.children===""?(o(_.el=a(""),l(m),m),H=m):H=M():(m.data!==_.children&&(ga(),m.data=_.children),H=i(m));break;case yt:x(m)?(H=i(m),O(_.el=m.content.firstChild,m,S)):Y!==8||C?H=M():H=i(m);break;case ea:if(C&&(m=i(m),Y=m.nodeType),Y===1||Y===3){H=m;const N=!_.children.length;for(let L=0;L<_.staticCount;L++)N&&(_.children+=H.nodeType===1?H.outerHTML:H.data),L===_.staticCount-1&&(_.anchor=H),H=i(H);return C?i(H):H}else M();break;case Ot:C?H=b(m,_,S,g,w,T):H=M();break;default:if(I&1)(Y!==1||_.type.toLowerCase()!==m.tagName.toLowerCase())&&!x(m)?H=M():H=f(m,_,S,g,w,T);else if(I&6){_.slotScopeIds=w;const N=l(m);if(C?H=A(m):Ca(m)&&m.data==="teleport start"?H=A(m,m.data,"teleport end"):H=i(m),t(_,N,null,S,g,pl(N),T),bn(_)&&!_.type.__asyncResolved){let L;C?(L=ft(Ot),L.anchor=H?H.previousSibling:N.lastChild):L=m.nodeType===3?yc(""):ft("div"),L.el=m,_.component.subTree=L}}else I&64?Y!==8?H=M():H=_.type.hydrate(m,_,S,g,w,T,e,p):I&128&&(H=_.type.hydrate(m,_,S,g,pl(l(m)),w,T,e,u))}return $!=null&&Na($,null,g,_),H},f=(m,_,S,g,w,T)=>{T=T||!!_.dynamicChildren;const{type:C,props:M,patchFlag:B,shapeFlag:$,dirs:I,transition:j}=_,Y=C==="input"||C==="option";if(Y||B!==-1){I&&Ks(_,null,S,"created");let H=!1;if(x(m)){H=wp(null,j)&&S&&S.vnode.props&&S.vnode.props.appear;const L=m.content.firstChild;if(H){const Z=L.getAttribute("class");Z&&(L.$cls=Z),j.beforeEnter(L)}O(L,m,S),_.el=m=L}if($&16&&!(M&&(M.innerHTML||M.textContent))){let L=p(m.firstChild,_,m,S,g,w,T);for(L&&!hl(m,1)&&ga();L;){const Z=L;L=L.nextSibling,r(Z)}}else if($&8){let L=_.children;L[0]===` -`&&(m.tagName==="PRE"||m.tagName==="TEXTAREA")&&(L=L.slice(1));const{textContent:Z}=m;Z!==L&&Z!==L.replace(/\r\n|\r/g,` -`)&&(hl(m,0)||ga(),m.textContent=_.children)}if(M){if(Y||!T||B&48){const L=m.tagName.includes("-");for(const Z in M)(Y&&(Z.endsWith("value")||Z==="indeterminate")||ra(Z)&&!gn(Z)||Z[0]==="."||L&&!gn(Z))&&n(m,Z,null,M[Z],void 0,S)}else if(M.onClick)n(m,"onClick",null,M.onClick,void 0,S);else if(B&4&&vn(M.style))for(const L in M.style)M.style[L]}let N;(N=M&&M.onVnodeBeforeMount)&&is(N,S,_),I&&Ks(_,null,S,"beforeMount"),((N=M&&M.onVnodeMounted)||I||H)&&Ep(()=>{N&&is(N,S,_),H&&j.enter(m),I&&Ks(_,null,S,"mounted")},g)}return m.nextSibling},p=(m,_,S,g,w,T,C)=>{C=C||!!_.dynamicChildren;const M=_.children,B=M.length;let $=!1;for(let I=0;I{const{slotScopeIds:C}=_;C&&(w=w?w.concat(C):C);const M=l(m),B=p(i(m),_,M,S,g,w,T);return B&&Ca(B)&&B.data==="]"?i(_.anchor=B):(ga(),o(_.anchor=c("]"),M,B),B)},y=(m,_,S,g,w,T)=>{if(hl(m.parentElement,1)||ga(),_.el=null,T){const B=A(m);for(;;){const $=i(m);if($&&$!==B)r($);else break}}const C=i(m),M=l(m);return r(m),s(null,_,M,C,S,g,pl(M),w),S&&(S.vnode.el=_.el,Tr(S,_.el)),C},A=(m,_="[",S="]")=>{let g=0;for(;m;)if(m=i(m),m&&Ca(m)&&(m.data===_&&g++,m.data===S)){if(g===0)return i(m);g--}return m},O=(m,_,S)=>{const g=_.parentNode;g&&g.replaceChild(m,_);let w=S;for(;w;)w.vnode.el===_&&(w.vnode.el=w.subTree.el=m),w=w.parent},x=m=>m.nodeType===1&&m.tagName==="TEMPLATE";return[d,u]}const bd="data-allow-mismatch",Iv={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function hl(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(bd);)e=e.parentElement;const s=e&&e.getAttribute(bd);if(s==null)return!1;if(s==="")return!0;{const n=s.split(",");return t===0&&n.includes("children")?!0:n.includes(Iv[t])}}const Ov=pr().requestIdleCallback||(e=>setTimeout(e,1)),Nv=pr().cancelIdleCallback||(e=>clearTimeout(e)),Lv=(e=1e4)=>t=>{const s=Ov(t,{timeout:e});return()=>Nv(s)};function Dv(e){const{top:t,left:s,bottom:n,right:a}=e.getBoundingClientRect(),{innerHeight:i,innerWidth:l}=window;return(t>0&&t0&&n0&&s0&&a(t,s)=>{const n=new IntersectionObserver(a=>{for(const i of a)if(i.isIntersecting){n.disconnect(),t();break}},e);return s(a=>{if(a instanceof Element){if(Dv(a))return t(),n.disconnect(),!1;n.observe(a)}}),()=>n.disconnect()},Pv=e=>t=>{if(e){const s=matchMedia(e);if(s.matches)t();else return s.addEventListener("change",t,{once:!0}),()=>s.removeEventListener("change",t)}},Fv=(e=[])=>(t,s)=>{Me(e)&&(e=[e]);let n=!1;const a=l=>{n||(n=!0,i(),t(),l.target.dispatchEvent(new l.constructor(l.type,l)))},i=()=>{s(l=>{for(const r of e)l.removeEventListener(r,a)})};return s(l=>{for(const r of e)l.addEventListener(r,a,{once:!0})}),i};function $v(e,t){if(Ca(e)&&e.data==="["){let s=1,n=e.nextSibling;for(;n;){if(n.nodeType===1){if(t(n)===!1)break}else if(Ca(n))if(n.data==="]"){if(--s===0)break}else n.data==="["&&s++;n=n.nextSibling}}else t(e)}const bn=e=>!!e.type.__asyncLoader;function Uv(e){Ie(e)&&(e={loader:e});const{loader:t,loadingComponent:s,errorComponent:n,delay:a=200,hydrate:i,timeout:l,suspensible:r=!0,onError:o}=e;let c=null,d,u=0;const f=()=>(u++,c=null,p()),p=()=>{let b;return c||(b=c=t().catch(y=>{if(y=y instanceof Error?y:new Error(String(y)),o)return new Promise((A,O)=>{o(y,()=>A(f()),()=>O(y),u+1)});throw y}).then(y=>b!==c&&c?c:(y&&(y.__esModule||y[Symbol.toStringTag]==="Module")&&(y=y.default),d=y,y)))};return Wi({name:"AsyncComponentWrapper",__asyncLoader:p,__asyncHydrate(b,y,A){let O=!1;(y.bu||(y.bu=[])).push(()=>O=!0);const x=()=>{O||A()},m=i?()=>{const _=i(x,S=>$v(b,S));_&&(y.bum||(y.bum=[])).push(_)}:x;d?m():p().then(()=>!y.isUnmounted&&m())},get __asyncResolved(){return d},setup(){const b=Mt;if(cc(b),d)return()=>ml(d,b);const y=S=>{c=null,da(S,b,13,!n)};if(r&&b.suspense||aa)return p().then(S=>()=>ml(S,b)).catch(S=>(y(S),()=>n?ft(n,{error:S}):null));const A=h(!1),O=h(),x=h(!!a);let m,_;return xt(()=>{m!=null&&clearTimeout(m),_!=null&&clearTimeout(_)}),a&&(_=setTimeout(()=>{b.isUnmounted||(x.value=!1)},a)),l!=null&&(m=setTimeout(()=>{if(!b.isUnmounted&&!A.value&&!O.value){const S=new Error(`Async component timed out after ${l}ms.`);y(S),O.value=S}},l)),p().then(()=>{b.isUnmounted||(A.value=!0,b.parent&&Zi(b.parent.vnode)&&b.parent.update())}).catch(S=>{if(b.isUnmounted){c=null;return}y(S),O.value=S}),()=>{if(A.value&&d)return ml(d,b);if(O.value&&n)return ft(n,{error:O.value});if(s&&!x.value)return ml(s,b)}}})}function ml(e,t){const{ref:s,props:n,children:a,ce:i}=t.vnode,l=ft(e,n,a);return l.ref=s,l.ce=i,delete t.vnode.ce,l}const Zi=e=>e.type.__isKeepAlive,Bv={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){const s=ts(),n=s.ctx;if(!n.renderer)return()=>{const x=t.default&&t.default();return x&&x.length===1?x[0]:x};const a=new Map,i=new Set;let l=null;const r=s.suspense,{renderer:{p:o,m:c,um:d,o:{createElement:u}}}=n,f=u("div");n.activate=(x,m,_,S,g)=>{const w=x.component;c(x,m,_,0,r),o(w.vnode,x,m,_,w,r,S,x.slotScopeIds,g),kt(()=>{w.isDeactivated=!1,w.a&&Ia(w.a);const T=x.props&&x.props.onVnodeMounted;T&&is(T,w.parent,x)},r)},n.deactivate=x=>{const m=x.component;Bl(m.m),Bl(m.a),c(x,f,null,1,r),kt(()=>{m.da&&Ia(m.da);const _=x.props&&x.props.onVnodeUnmounted;_&&is(_,m.parent,x),m.isDeactivated=!0},r)};function p(x){qr(x),d(x,s,r,!0)}function b(x){a.forEach((m,_)=>{const S=Ao(bn(m)?m.type.__asyncResolved||{}:m.type);S&&!x(S)&&y(_)})}function y(x){const m=a.get(x);m&&(!l||!Ls(m,l))?p(m):l&&qr(l),a.delete(x),i.delete(x)}es(()=>[e.include,e.exclude],([x,m])=>{x&&b(_=>ui(x,_)),m&&b(_=>!ui(m,_))},{flush:"post",deep:!0});let A=null;const O=()=>{A!=null&&(Hl(s.subTree.type)?kt(()=>{a.set(A,gl(s.subTree))},s.subTree.suspense):a.set(A,gl(s.subTree)))};return We(O),kr(O),wr(()=>{a.forEach(x=>{const{subTree:m,suspense:_}=s,S=gl(m);if(x.type===S.type&&x.key===S.key){qr(S);const g=S.component.da;g&&kt(g,_);return}p(x)})}),()=>{if(A=null,!t.default)return l=null;const x=t.default(),m=x[0];if(x.length>1)return l=null,x;if(!Sn(m)||!(m.shapeFlag&4)&&!(m.shapeFlag&128))return l=null,m;let _=gl(m);if(_.type===yt)return l=null,_;const S=_.type,g=Ao(bn(_)?_.type.__asyncResolved||{}:S),{include:w,exclude:T,max:C}=e;if(w&&(!g||!ui(w,g))||T&&g&&ui(T,g))return _.shapeFlag&=-257,l=_,m;const M=_.key==null?S:_.key,B=a.get(M);return _.el&&(_=Ys(_),m.shapeFlag&128&&(m.ssContent=_)),A=M,B?(_.el=B.el,_.component=B.component,_.transition&&wn(_,_.transition),_.shapeFlag|=512,i.delete(M),i.add(M)):(i.add(M),C&&i.size>parseInt(C,10)&&y(i.values().next().value)),_.shapeFlag|=256,l=_,Hl(m.type)?m:_}}},Hv=Bv;function ui(e,t){return ge(e)?e.some(s=>ui(s,t)):Me(e)?e.split(",").includes(t):Ym(e)?(e.lastIndex=0,e.test(t)):!1}function Cs(e,t){Xf(e,"a",t)}function Es(e,t){Xf(e,"da",t)}function Xf(e,t,s=Mt){const n=e.__wdc||(e.__wdc=()=>{let a=s;for(;a;){if(a.isDeactivated)return;a=a.parent}return e()});if(_r(t,n,s),s){let a=s.parent;for(;a&&a.parent;)Zi(a.parent.vnode)&&Vv(n,t,s,a),a=a.parent}}function Vv(e,t,s,n){const a=_r(t,e,n,!0);xt(()=>{Jo(n[t],a)},s)}function qr(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function gl(e){return e.shapeFlag&128?e.ssContent:e}function _r(e,t,s=Mt,n=!1){if(s){const a=s[e]||(s[e]=[]),i=t.__weh||(t.__weh=(...l)=>{_n();const r=Ya(s),o=ms(t,s,e,l);return r(),kn(),o});return n?a.unshift(i):a.push(i),i}}const Tn=e=>(t,s=Mt)=>{(!aa||e==="sp")&&_r(e,(...n)=>t(...n),s)},ep=Tn("bm"),We=Tn("m"),dc=Tn("bu"),kr=Tn("u"),wr=Tn("bum"),xt=Tn("um"),tp=Tn("sp"),sp=Tn("rtg"),np=Tn("rtc");function ap(e,t=Mt){_r("ec",e,t)}const uc="components",jv="directives";function zv(e,t){return fc(uc,e,!0,t)||e}const ip=Symbol.for("v-ndc");function qv(e){return Me(e)?fc(uc,e,!1)||e:e||ip}function Gv(e){return fc(jv,e)}function fc(e,t,s=!0,n=!1){const a=Pt||Mt;if(a){const i=a.type;if(e===uc){const r=Ao(i,!1);if(r&&(r===t||r===at(t)||r===ca(at(t))))return i}const l=yd(a[e]||i[e],t)||yd(a.appContext[e],t);return!l&&n?i:l}}function yd(e,t){return e&&(e[t]||e[at(t)]||e[ca(at(t))])}function Kv(e,t,s,n){let a;const i=s&&s[n],l=ge(e);if(l||Me(e)){const r=l&&vn(e);let o=!1,c=!1;r&&(o=!ds(e),c=Js(e),e=gr(e)),a=new Array(e.length);for(let d=0,u=e.length;dt(r,o,void 0,i&&i[o]));else{const r=Object.keys(e);a=new Array(r.length);for(let o=0,c=r.length;o{const i=n.fn(...a);return i&&(i.key=n.key),i}:n.fn)}return e}function Zv(e,t,s={},n,a){if(Pt.ce||Pt.parent&&bn(Pt.parent)&&Pt.parent.ce){const c=Object.keys(s).length>0;return t!=="default"&&(s.name=t),Di(),Vl(Ot,null,[ft("slot",s,n&&n())],c?-2:64)}let i=e[t];i&&i._c&&(i._d=!1),Di();const l=i&&pc(i(s)),r=s.key||l&&l.key,o=Vl(Ot,{key:(r&&!Gt(r)?r:`_${t}`)+(!l&&n?"_fb":"")},l||(n?n():[]),l&&e._===1?64:-2);return!a&&o.scopeId&&(o.slotScopeIds=[o.scopeId+"-s"]),i&&i._c&&(i._d=!0),o}function pc(e){return e.some(t=>Sn(t)?!(t.type===yt||t.type===Ot&&!pc(t.children)):!0)?e:null}function Jv(e,t){const s={};for(const n in e)s[t&&/[A-Z]/.test(n)?`on:${n}`:Ra(n)]=e[n];return s}const xo=e=>e?Mp(e)?Ji(e):xo(e.parent):null,bi=je(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=>xo(e.parent),$root:e=>xo(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>hc(e),$forceUpdate:e=>e.f||(e.f=()=>{ic(e.update)}),$nextTick:e=>e.n||(e.n=At.bind(e.proxy)),$watch:e=>yv.bind(e)}),Gr=(e,t)=>e!==qe&&!e.__isScriptSetup&&et(e,t),_o={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:a,props:i,accessCache:l,type:r,appContext:o}=e;if(t[0]!=="$"){const f=l[t];if(f!==void 0)switch(f){case 1:return n[t];case 2:return a[t];case 4:return s[t];case 3:return i[t]}else{if(Gr(n,t))return l[t]=1,n[t];if(a!==qe&&et(a,t))return l[t]=2,a[t];if(et(i,t))return l[t]=3,i[t];if(s!==qe&&et(s,t))return l[t]=4,s[t];ko&&(l[t]=0)}}const c=bi[t];let d,u;if(c)return t==="$attrs"&&jt(e.attrs,"get",""),c(e);if((d=r.__cssModules)&&(d=d[t]))return d;if(s!==qe&&et(s,t))return l[t]=4,s[t];if(u=o.config.globalProperties,et(u,t))return u[t]},set({_:e},t,s){const{data:n,setupState:a,ctx:i}=e;return Gr(a,t)?(a[t]=s,!0):n!==qe&&et(n,t)?(n[t]=s,!0):et(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:a,props:i,type:l}},r){let o;return!!(s[r]||e!==qe&&r[0]!=="$"&&et(e,r)||Gr(t,r)||et(i,r)||et(n,r)||et(bi,r)||et(a.config.globalProperties,r)||(o=l.__cssModules)&&o[r])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:et(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}},Yv=je({},_o,{get(e,t){if(t!==Symbol.unscopables)return _o.get(e,t,e)},has(e,t){return t[0]!=="_"&&!ag(t)}});function Qv(){return null}function Xv(){return null}function eb(e){}function tb(e){}function sb(){return null}function nb(){}function ab(e,t){return null}function ib(){return lp().slots}function lb(){return lp().attrs}function lp(e){const t=ts();return t.setupContext||(t.setupContext=Up(t))}function Ni(e){return ge(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}function rb(e,t){const s=Ni(e);for(const n in t){if(n.startsWith("__skip"))continue;let a=s[n];a?ge(a)||Ie(a)?a=s[n]={type:a,default:t[n]}:a.default=t[n]:a===null&&(a=s[n]={default:t[n]}),a&&t[`__skip_${n}`]&&(a.skipFactory=!0)}return s}function ob(e,t){return!e||!t?e||t:ge(e)&&ge(t)?e.concat(t):je({},Ni(e),Ni(t))}function cb(e,t){const s={};for(const n in e)t.includes(n)||Object.defineProperty(s,n,{enumerable:!0,get:()=>e[n]});return s}function db(e){const t=ts(),s=aa;let n=e();Pi(),s&&Da(!1);const a=()=>{Ya(t),s&&Da(!0)},i=()=>{ts()!==t&&t.scope.off(),Pi(),s&&Da(!1)};return Yo(n)&&(n=n.catch(l=>{throw a(),Promise.resolve().then(()=>Promise.resolve().then(i)),l})),[n,()=>{a(),Promise.resolve().then(i)}]}let ko=!0;function ub(e){const t=hc(e),s=e.proxy,n=e.ctx;ko=!1,t.beforeCreate&&xd(t.beforeCreate,e,"bc");const{data:a,computed:i,methods:l,watch:r,provide:o,inject:c,created:d,beforeMount:u,mounted:f,beforeUpdate:p,updated:b,activated:y,deactivated:A,beforeDestroy:O,beforeUnmount:x,destroyed:m,unmounted:_,render:S,renderTracked:g,renderTriggered:w,errorCaptured:T,serverPrefetch:C,expose:M,inheritAttrs:B,components:$,directives:I,filters:j}=t;if(c&&fb(c,n,null),l)for(const N in l){const L=l[N];Ie(L)&&(n[N]=L.bind(s))}if(a){const N=a.call(s,s);Qe(N)&&(e.data=Un(N))}if(ko=!0,i)for(const N in i){const L=i[N],Z=Ie(L)?L.bind(s,s):Ie(L.get)?L.get.bind(s,s):Ft,xe=!Ie(L)&&Ie(L.set)?L.set.bind(s):Ft,_e=J({get:Z,set:xe});Object.defineProperty(n,N,{enumerable:!0,configurable:!0,get:()=>_e.value,set:ae=>_e.value=ae})}if(r)for(const N in r)rp(r[N],n,s,N);if(o){const N=Ie(o)?o.call(s):o;Reflect.ownKeys(N).forEach(L=>{vi(L,N[L])})}d&&xd(d,e,"c");function H(N,L){ge(L)?L.forEach(Z=>N(Z.bind(s))):L&&N(L.bind(s))}if(H(ep,u),H(We,f),H(dc,p),H(kr,b),H(Cs,y),H(Es,A),H(ap,T),H(np,g),H(sp,w),H(wr,x),H(xt,_),H(tp,C),ge(M))if(M.length){const N=e.exposed||(e.exposed={});M.forEach(L=>{Object.defineProperty(N,L,{get:()=>s[L],set:Z=>s[L]=Z,enumerable:!0})})}else e.exposed||(e.exposed={});S&&e.render===Ft&&(e.render=S),B!=null&&(e.inheritAttrs=B),$&&(e.components=$),I&&(e.directives=I),C&&cc(e)}function fb(e,t,s=Ft){ge(e)&&(e=wo(e));for(const n in e){const a=e[n];let i;Qe(a)?"default"in a?i=ws(a.from||n,a.default,!0):i=ws(a.from||n):i=ws(a),St(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:l=>i.value=l}):t[n]=i}}function xd(e,t,s){ms(ge(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function rp(e,t,s,n){let a=n.includes(".")?Gf(s,n):()=>s[n];if(Me(e)){const i=t[e];Ie(i)&&es(a,i)}else if(Ie(e))es(a,e.bind(s));else if(Qe(e))if(ge(e))e.forEach(i=>rp(i,t,s,n));else{const i=Ie(e.handler)?e.handler.bind(s):t[e.handler];Ie(i)&&es(a,i,e)}}function hc(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:a,optionsCache:i,config:{optionMergeStrategies:l}}=e.appContext,r=i.get(t);let o;return r?o=r:!a.length&&!s&&!n?o=t:(o={},a.length&&a.forEach(c=>Ul(o,c,l,!0)),Ul(o,t,l)),Qe(t)&&i.set(t,o),o}function Ul(e,t,s,n=!1){const{mixins:a,extends:i}=t;i&&Ul(e,i,s,!0),a&&a.forEach(l=>Ul(e,l,s,!0));for(const l in t)if(!(n&&l==="expose")){const r=pb[l]||s&&s[l];e[l]=r?r(e[l],t[l]):t[l]}return e}const pb={data:_d,props:kd,emits:kd,methods:fi,computed:fi,beforeCreate:Zt,created:Zt,beforeMount:Zt,mounted:Zt,beforeUpdate:Zt,updated:Zt,beforeDestroy:Zt,beforeUnmount:Zt,destroyed:Zt,unmounted:Zt,activated:Zt,deactivated:Zt,errorCaptured:Zt,serverPrefetch:Zt,components:fi,directives:fi,watch:mb,provide:_d,inject:hb};function _d(e,t){return t?e?function(){return je(Ie(e)?e.call(this,this):e,Ie(t)?t.call(this,this):t)}:t:e}function hb(e,t){return fi(wo(e),wo(t))}function wo(e){if(ge(e)){const t={};for(let s=0;s{let d,u=qe,f;return qf(()=>{const p=e[a];It(d,p)&&(d=p,c())}),{get(){return o(),s.get?s.get(d):d},set(p){const b=s.set?s.set(p):p;if(!It(b,d)&&!(u!==qe&&It(p,u)))return;const y=n.vnode.props,A=!!(y&&(t in y||a in y||i in y)&&(`onUpdate:${t}`in y||`onUpdate:${a}`in y||`onUpdate:${i}`in y));A||(d=p,c()),n.emit(`update:${t}`,b),It(p,u)&&(It(p,b)&&!It(b,f)||A&&u!==qe&&!It(b,d))&&c(),u=p,f=b}}});return r[Symbol.iterator]=()=>{let o=0;return{next(){return o<2?{value:o++?l||qe:r,done:!1}:{done:!0}}}},r}const cp=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${at(t)}Modifiers`]||e[`${os(t)}Modifiers`];function yb(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||qe;let a=s;const i=t.startsWith("update:"),l=i&&cp(n,t.slice(7));l&&(l.trim&&(a=s.map(d=>Me(d)?d.trim():d)),l.number&&(a=s.map(fr)));let r,o=n[r=Ra(t)]||n[r=Ra(at(t))];!o&&i&&(o=n[r=Ra(os(t))]),o&&ms(o,e,6,a);const c=n[r+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[r])return;e.emitted[r]=!0,ms(c,e,6,a)}}const xb=new WeakMap;function dp(e,t,s=!1){const n=s?xb:t.emitsCache,a=n.get(e);if(a!==void 0)return a;const i=e.emits;let l={},r=!1;if(!Ie(e)){const o=c=>{const d=dp(c,t,!0);d&&(r=!0,je(l,d))};!s&&t.mixins.length&&t.mixins.forEach(o),e.extends&&o(e.extends),e.mixins&&e.mixins.forEach(o)}return!i&&!r?(Qe(e)&&n.set(e,null),null):(ge(i)?i.forEach(o=>l[o]=null):je(l,i),Qe(e)&&n.set(e,l),l)}function Sr(e,t){return!e||!ra(t)?!1:(t=t.slice(2).replace(/Once$/,""),et(e,t[0].toLowerCase()+t.slice(1))||et(e,os(t))||et(e,t))}function Tl(e){const{type:t,vnode:s,proxy:n,withProxy:a,propsOptions:[i],slots:l,attrs:r,emit:o,render:c,renderCache:d,props:u,data:f,setupState:p,ctx:b,inheritAttrs:y}=e,A=Oi(e);let O,x;try{if(s.shapeFlag&4){const _=a||n,S=_;O=rs(c.call(S,_,d,u,p,f,b)),x=r}else{const _=t;O=rs(_.length>1?_(u,{attrs:r,slots:l,emit:o}):_(u,null)),x=t.props?r:kb(r)}}catch(_){yi.length=0,da(_,e,1),O=ft(yt)}let m=O;if(x&&y!==!1){const _=Object.keys(x),{shapeFlag:S}=m;_.length&&S&7&&(i&&_.some(or)&&(x=wb(x,i)),m=Ys(m,x,!1,!0))}return s.dirs&&(m=Ys(m,null,!1,!0),m.dirs=m.dirs?m.dirs.concat(s.dirs):s.dirs),s.transition&&wn(m,s.transition),O=m,Oi(A),O}function _b(e,t=!0){let s;for(let n=0;n{let t;for(const s in e)(s==="class"||s==="style"||ra(s))&&((t||(t={}))[s]=e[s]);return t},wb=(e,t)=>{const s={};for(const n in e)(!or(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function Sb(e,t,s){const{props:n,children:a,component:i}=e,{props:l,children:r,patchFlag:o}=t,c=i.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&o>=0){if(o&1024)return!0;if(o&16)return n?wd(n,l,c):!!l;if(o&8){const d=t.dynamicProps;for(let u=0;uObject.create(fp),hp=e=>Object.getPrototypeOf(e)===fp;function Tb(e,t,s,n=!1){const a={},i=pp();e.propsDefaults=Object.create(null),mp(e,t,a,i);for(const l in e.propsOptions[0])l in a||(a[l]=void 0);s?e.props=n?a:sc(a):e.type.props?e.props=a:e.props=i,e.attrs=i}function Cb(e,t,s,n){const{props:a,attrs:i,vnode:{patchFlag:l}}=e,r=Ze(a),[o]=e.propsOptions;let c=!1;if((n||l>0)&&!(l&16)){if(l&8){const d=e.vnode.dynamicProps;for(let u=0;u{o=!0;const[f,p]=gp(u,t,!0);je(l,f),p&&r.push(...p)};!s&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}if(!i&&!o)return Qe(e)&&n.set(e,Ea),Ea;if(ge(i))for(let d=0;de==="_"||e==="_ctx"||e==="$stable",gc=e=>ge(e)?e.map(rs):[rs(e)],Ab=(e,t,s)=>{if(t._n)return t;const n=lc((...a)=>gc(t(...a)),s);return n._c=!1,n},vp=(e,t,s)=>{const n=e._ctx;for(const a in e){if(mc(a))continue;const i=e[a];if(Ie(i))t[a]=Ab(a,i,n);else if(i!=null){const l=gc(i);t[a]=()=>l}}},bp=(e,t)=>{const s=gc(t);e.slots.default=()=>s},yp=(e,t,s)=>{for(const n in t)(s||!mc(n))&&(e[n]=t[n])},Rb=(e,t,s)=>{const n=e.slots=pp();if(e.vnode.shapeFlag&32){const a=t._;a?(yp(n,t,s),s&&uf(n,"_",a,!0)):vp(t,n)}else t&&bp(e,t)},Ib=(e,t,s)=>{const{vnode:n,slots:a}=e;let i=!0,l=qe;if(n.shapeFlag&32){const r=t._;r?s&&r===1?i=!1:yp(a,t,s):(i=!t.$stable,vp(t,a)),l=t}else t&&(bp(e,t),l={default:1});if(i)for(const r in a)!mc(r)&&l[r]==null&&delete a[r]},kt=Ep;function xp(e){return kp(e)}function _p(e){return kp(e,Rv)}function kp(e,t){const s=pr();s.__VUE__=!0;const{insert:n,remove:a,patchProp:i,createElement:l,createText:r,createComment:o,setText:c,setElementText:d,parentNode:u,nextSibling:f,setScopeId:p=Ft,insertStaticContent:b}=e,y=(k,E,U,X=null,q=null,Q=null,ie=void 0,re=null,le=!!E.dynamicChildren)=>{if(k===E)return;k&&!Ls(k,E)&&(X=V(k),ae(k,q,Q,!0),k=null),E.patchFlag===-2&&(le=!1,E.dynamicChildren=null);const{type:te,ref:be,shapeFlag:ue}=E;switch(te){case Pn:A(k,E,U,X);break;case yt:O(k,E,U,X);break;case ea:k==null&&x(E,U,X,ie);break;case Ot:$(k,E,U,X,q,Q,ie,re,le);break;default:ue&1?S(k,E,U,X,q,Q,ie,re,le):ue&6?I(k,E,U,X,q,Q,ie,re,le):(ue&64||ue&128)&&te.process(k,E,U,X,q,Q,ie,re,le,ve)}be!=null&&q?Na(be,k&&k.ref,Q,E||k,!E):be==null&&k&&k.ref!=null&&Na(k.ref,null,Q,k,!0)},A=(k,E,U,X)=>{if(k==null)n(E.el=r(E.children),U,X);else{const q=E.el=k.el;E.children!==k.children&&c(q,E.children)}},O=(k,E,U,X)=>{k==null?n(E.el=o(E.children||""),U,X):E.el=k.el},x=(k,E,U,X)=>{[k.el,k.anchor]=b(k.children,E,U,X,k.el,k.anchor)},m=({el:k,anchor:E},U,X)=>{let q;for(;k&&k!==E;)q=f(k),n(k,U,X),k=q;n(E,U,X)},_=({el:k,anchor:E})=>{let U;for(;k&&k!==E;)U=f(k),a(k),k=U;a(E)},S=(k,E,U,X,q,Q,ie,re,le)=>{if(E.type==="svg"?ie="svg":E.type==="math"&&(ie="mathml"),k==null)g(E,U,X,q,Q,ie,re,le);else{const te=k.el&&k.el._isVueCE?k.el:null;try{te&&te._beginPatch(),C(k,E,q,Q,ie,re,le)}finally{te&&te._endPatch()}}},g=(k,E,U,X,q,Q,ie,re)=>{let le,te;const{props:be,shapeFlag:ue,transition:he,dirs:we}=k;if(le=k.el=l(k.type,Q,be&&be.is,be),ue&8?d(le,k.children):ue&16&&T(k.children,le,null,X,q,Kr(k,Q),ie,re),we&&Ks(k,null,X,"created"),w(le,k,k.scopeId,ie,X),be){for(const Le in be)Le!=="value"&&!gn(Le)&&i(le,Le,null,be[Le],Q,X);"value"in be&&i(le,"value",null,be.value,Q),(te=be.onVnodeBeforeMount)&&is(te,X,k)}we&&Ks(k,null,X,"beforeMount");const Ee=wp(q,he);Ee&&he.beforeEnter(le),n(le,E,U),((te=be&&be.onVnodeMounted)||Ee||we)&&kt(()=>{try{te&&is(te,X,k),Ee&&he.enter(le),we&&Ks(k,null,X,"mounted")}finally{}},q)},w=(k,E,U,X,q)=>{if(U&&p(k,U),X)for(let Q=0;Q{for(let te=le;te{const re=E.el=k.el;let{patchFlag:le,dynamicChildren:te,dirs:be}=E;le|=k.patchFlag&16;const ue=k.props||qe,he=E.props||qe;let we;if(U&&jn(U,!1),(we=he.onVnodeBeforeUpdate)&&is(we,U,E,k),be&&Ks(E,k,U,"beforeUpdate"),U&&jn(U,!0),(ue.innerHTML&&he.innerHTML==null||ue.textContent&&he.textContent==null)&&d(re,""),te?M(k.dynamicChildren,te,re,U,X,Kr(E,q),Q):ie||L(k,E,re,null,U,X,Kr(E,q),Q,!1),le>0){if(le&16)B(re,ue,he,U,q);else if(le&2&&ue.class!==he.class&&i(re,"class",null,he.class,q),le&4&&i(re,"style",ue.style,he.style,q),le&8){const Ee=E.dynamicProps;for(let Le=0;Le{we&&is(we,U,E,k),be&&Ks(E,k,U,"updated")},X)},M=(k,E,U,X,q,Q,ie)=>{for(let re=0;re{if(E!==U){if(E!==qe)for(const Q in E)!gn(Q)&&!(Q in U)&&i(k,Q,E[Q],null,q,X);for(const Q in U){if(gn(Q))continue;const ie=U[Q],re=E[Q];ie!==re&&Q!=="value"&&i(k,Q,re,ie,q,X)}"value"in U&&i(k,"value",E.value,U.value,q)}},$=(k,E,U,X,q,Q,ie,re,le)=>{const te=E.el=k?k.el:r(""),be=E.anchor=k?k.anchor:r("");let{patchFlag:ue,dynamicChildren:he,slotScopeIds:we}=E;we&&(re=re?re.concat(we):we),k==null?(n(te,U,X),n(be,U,X),T(E.children||[],U,be,q,Q,ie,re,le)):ue>0&&ue&64&&he&&k.dynamicChildren&&k.dynamicChildren.length===he.length?(M(k.dynamicChildren,he,U,q,Q,ie,re),(E.key!=null||q&&E===q.subTree)&&vc(k,E,!0)):L(k,E,U,be,q,Q,ie,re,le)},I=(k,E,U,X,q,Q,ie,re,le)=>{E.slotScopeIds=re,k==null?E.shapeFlag&512?q.ctx.activate(E,U,X,ie,le):j(E,U,X,q,Q,ie,le):Y(k,E,le)},j=(k,E,U,X,q,Q,ie)=>{const re=k.component=Dp(k,X,q);if(Zi(k)&&(re.ctx.renderer=ve),Pp(re,!1,ie),re.asyncDep){if(q&&q.registerDep(re,H,ie),!k.el){const le=re.subTree=ft(yt);O(null,le,E,U),k.placeholder=le.el}}else H(re,k,E,U,q,Q,ie)},Y=(k,E,U)=>{const X=E.component=k.component;if(Sb(k,E,U))if(X.asyncDep&&!X.asyncResolved){N(X,E,U);return}else X.next=E,X.update();else E.el=k.el,X.vnode=E},H=(k,E,U,X,q,Q,ie)=>{const re=()=>{if(k.isMounted){let{next:ue,bu:he,u:we,parent:Ee,vnode:Le}=k;{const G=Sp(k);if(G){ue&&(ue.el=Le.el,N(k,ue,ie)),G.asyncDep.then(()=>{kt(()=>{k.isUnmounted||te()},q)});return}}let Oe=ue,Fe;jn(k,!1),ue?(ue.el=Le.el,N(k,ue,ie)):ue=Le,he&&Ia(he),(Fe=ue.props&&ue.props.onVnodeBeforeUpdate)&&is(Fe,Ee,ue,Le),jn(k,!0);const Ve=Tl(k),lt=k.subTree;k.subTree=Ve,y(lt,Ve,u(lt.el),V(lt),k,q,Q),ue.el=Ve.el,Oe===null&&Tr(k,Ve.el),we&&kt(we,q),(Fe=ue.props&&ue.props.onVnodeUpdated)&&kt(()=>is(Fe,Ee,ue,Le),q)}else{let ue;const{el:he,props:we}=E,{bm:Ee,m:Le,parent:Oe,root:Fe,type:Ve}=k,lt=bn(E);if(jn(k,!1),Ee&&Ia(Ee),!lt&&(ue=we&&we.onVnodeBeforeMount)&&is(ue,Oe,E),jn(k,!0),he&&He){const G=()=>{k.subTree=Tl(k),He(he,k.subTree,k,q,null)};lt&&Ve.__asyncHydrate?Ve.__asyncHydrate(he,k,G):G()}else{Fe.ce&&Fe.ce._hasShadowRoot()&&Fe.ce._injectChildStyle(Ve,k.parent?k.parent.type:void 0);const G=k.subTree=Tl(k);y(null,G,U,X,k,q,Q),E.el=G.el}if(Le&&kt(Le,q),!lt&&(ue=we&&we.onVnodeMounted)){const G=E;kt(()=>is(ue,Oe,G),q)}(E.shapeFlag&256||Oe&&bn(Oe.vnode)&&Oe.vnode.shapeFlag&256)&&k.a&&kt(k.a,q),k.isMounted=!0,E=U=X=null}};k.scope.on();const le=k.effect=new Ci(re);k.scope.off();const te=k.update=le.run.bind(le),be=k.job=le.runIfDirty.bind(le);be.i=k,be.id=k.uid,le.scheduler=()=>ic(be),jn(k,!0),te()},N=(k,E,U)=>{E.component=k;const X=k.vnode.props;k.vnode=E,k.next=null,Cb(k,E.props,X,U),Ib(k,E.children,U),_n(),ud(k),kn()},L=(k,E,U,X,q,Q,ie,re,le=!1)=>{const te=k&&k.children,be=k?k.shapeFlag:0,ue=E.children,{patchFlag:he,shapeFlag:we}=E;if(he>0){if(he&128){xe(te,ue,U,X,q,Q,ie,re,le);return}else if(he&256){Z(te,ue,U,X,q,Q,ie,re,le);return}}we&8?(be&16&&ke(te,q,Q),ue!==te&&d(U,ue)):be&16?we&16?xe(te,ue,U,X,q,Q,ie,re,le):ke(te,q,Q,!0):(be&8&&d(U,""),we&16&&T(ue,U,X,q,Q,ie,re,le))},Z=(k,E,U,X,q,Q,ie,re,le)=>{k=k||Ea,E=E||Ea;const te=k.length,be=E.length,ue=Math.min(te,be);let he;for(he=0;hebe?ke(k,q,Q,!0,!1,ue):T(E,U,X,q,Q,ie,re,le,ue)},xe=(k,E,U,X,q,Q,ie,re,le)=>{let te=0;const be=E.length;let ue=k.length-1,he=be-1;for(;te<=ue&&te<=he;){const we=k[te],Ee=E[te]=le?cn(E[te]):rs(E[te]);if(Ls(we,Ee))y(we,Ee,U,null,q,Q,ie,re,le);else break;te++}for(;te<=ue&&te<=he;){const we=k[ue],Ee=E[he]=le?cn(E[he]):rs(E[he]);if(Ls(we,Ee))y(we,Ee,U,null,q,Q,ie,re,le);else break;ue--,he--}if(te>ue){if(te<=he){const we=he+1,Ee=wehe)for(;te<=ue;)ae(k[te],q,Q,!0),te++;else{const we=te,Ee=te,Le=new Map;for(te=Ee;te<=he;te++){const Re=E[te]=le?cn(E[te]):rs(E[te]);Re.key!=null&&Le.set(Re.key,te)}let Oe,Fe=0;const Ve=he-Ee+1;let lt=!1,G=0;const ye=new Array(Ve);for(te=0;te=Ve){ae(Re,q,Q,!0);continue}let Be;if(Re.key!=null)Be=Le.get(Re.key);else for(Oe=Ee;Oe<=he;Oe++)if(ye[Oe-Ee]===0&&Ls(Re,E[Oe])){Be=Oe;break}Be===void 0?ae(Re,q,Q,!0):(ye[Be-Ee]=te+1,Be>=G?G=Be:lt=!0,y(Re,E[Be],U,null,q,Q,ie,re,le),Fe++)}const Ce=lt?Ob(ye):Ea;for(Oe=Ce.length-1,te=Ve-1;te>=0;te--){const Re=Ee+te,Be=E[Re],ze=E[Re+1],pt=Re+1{const{el:Q,type:ie,transition:re,children:le,shapeFlag:te}=k;if(te&6){_e(k.component.subTree,E,U,X);return}if(te&128){k.suspense.move(E,U,X);return}if(te&64){ie.move(k,E,U,ve);return}if(ie===Ot){n(Q,E,U);for(let ue=0;uere.enter(Q),q));else{const{leave:ue,delayLeave:he,afterLeave:we}=re,Ee=()=>{k.ctx.isUnmounted?a(Q):n(Q,E,U)},Le=()=>{const Oe=Q._isLeaving||!!Q[xs];Q._isLeaving&&Q[xs](!0),re.persisted&&!Oe?Ee():ue(Q,()=>{Ee(),we&&we()})};he?he(Q,Ee,Le):Le()}else n(Q,E,U)},ae=(k,E,U,X=!1,q=!1)=>{const{type:Q,props:ie,ref:re,children:le,dynamicChildren:te,shapeFlag:be,patchFlag:ue,dirs:he,cacheIndex:we,memo:Ee}=k;if(ue===-2&&(q=!1),re!=null&&(_n(),Na(re,null,U,k,!0),kn()),we!=null&&(E.renderCache[we]=void 0),be&256){E.ctx.deactivate(k);return}const Le=be&1&&he,Oe=!bn(k);let Fe;if(Oe&&(Fe=ie&&ie.onVnodeBeforeUnmount)&&is(Fe,E,k),be&6)se(k.component,U,X);else{if(be&128){k.suspense.unmount(U,X);return}Le&&Ks(k,null,E,"beforeUnmount"),be&64?k.type.remove(k,E,U,ve,X):te&&!te.hasOnce&&(Q!==Ot||ue>0&&ue&64)?ke(te,E,U,!1,!0):(Q===Ot&&ue&384||!q&&be&16)&&ke(le,E,U),X&&fe(k)}const Ve=Ee!=null&&we==null;(Oe&&(Fe=ie&&ie.onVnodeUnmounted)||Le||Ve)&&kt(()=>{Fe&&is(Fe,E,k),Le&&Ks(k,null,E,"unmounted"),Ve&&(k.el=null)},U)},fe=k=>{const{type:E,el:U,anchor:X,transition:q}=k;if(E===Ot){P(U,X);return}if(E===ea){_(k);return}const Q=()=>{a(U),q&&!q.persisted&&q.afterLeave&&q.afterLeave()};if(k.shapeFlag&1&&q&&!q.persisted){const{leave:ie,delayLeave:re}=q,le=()=>ie(U,Q);re?re(k.el,Q,le):le()}else Q()},P=(k,E)=>{let U;for(;k!==E;)U=f(k),a(k),k=U;a(E)},se=(k,E,U)=>{const{bum:X,scope:q,job:Q,subTree:ie,um:re,m:le,a:te}=k;Bl(le),Bl(te),X&&Ia(X),q.stop(),Q&&(Q.flags|=8,ae(ie,k,E,U)),re&&kt(re,E),kt(()=>{k.isUnmounted=!0},E)},ke=(k,E,U,X=!1,q=!1,Q=0)=>{for(let ie=Q;ie{if(k.shapeFlag&6)return V(k.component.subTree);if(k.shapeFlag&128)return k.suspense.next();const E=f(k.anchor||k.el),U=E&&E[Kf];return U?f(U):E};let ce=!1;const de=(k,E,U)=>{let X;k==null?E._vnode&&(ae(E._vnode,null,null,!0),X=E._vnode.component):y(E._vnode||null,k,E,null,null,null,U),E._vnode=k,ce||(ce=!0,ud(X),Fl(),ce=!1)},ve={p:y,um:ae,m:_e,r:fe,mt:j,mc:T,pc:L,pbc:M,n:V,o:e};let me,He;return t&&([me,He]=t(ve)),{render:de,hydrate:me,createApp:vb(de,me)}}function Kr({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 jn({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function wp(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function vc(e,t,s=!1){const n=e.children,a=t.children;if(ge(n)&&ge(a))for(let i=0;i>1,e[s[r]]0&&(t[n]=s[i-1]),s[i]=n)}}for(i=s.length,l=s[i-1];i-- >0;)s[i]=l,l=t[l];return s}function Sp(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Sp(t)}function Bl(e){if(e)for(let t=0;te.__isSuspense;let To=0;const Nb={name:"Suspense",__isSuspense:!0,process(e,t,s,n,a,i,l,r,o,c){if(e==null)Db(t,s,n,a,i,l,r,o,c);else{if(i&&i.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}Mb(e,t,s,n,a,l,r,o,c)}},hydrate:Pb,normalize:Fb},Lb=Nb;function Li(e,t){const s=e.props&&e.props[t];Ie(s)&&s()}function Db(e,t,s,n,a,i,l,r,o){const{p:c,o:{createElement:d}}=o,u=d("div"),f=e.suspense=Cp(e,a,n,t,u,s,i,l,r,o);c(null,f.pendingBranch=e.ssContent,u,null,n,f,i,l),f.deps>0?(Li(e,"onPending"),Li(e,"onFallback"),c(null,e.ssFallback,t,s,n,null,i,l),La(f,e.ssFallback)):f.resolve(!1,!0)}function Mb(e,t,s,n,a,i,l,r,{p:o,um:c,o:{createElement:d}}){const u=t.suspense=e.suspense;u.vnode=t,t.el=e.el;const f=t.ssContent,p=t.ssFallback,{activeBranch:b,pendingBranch:y,isInFallback:A,isHydrating:O}=u;if(y)u.pendingBranch=f,Ls(y,f)?(o(y,f,u.hiddenContainer,null,a,u,i,l,r),u.deps<=0?u.resolve():A&&(O||(o(b,p,s,n,a,null,i,l,r),La(u,p)))):(u.pendingId=To++,O?(u.isHydrating=!1,u.activeBranch=y):c(y,a,u),u.deps=0,u.effects.length=0,u.hiddenContainer=d("div"),A?(o(null,f,u.hiddenContainer,null,a,u,i,l,r),u.deps<=0?u.resolve():(o(b,p,s,n,a,null,i,l,r),La(u,p))):b&&Ls(b,f)?(o(b,f,s,n,a,u,i,l,r),u.resolve(!0)):(o(null,f,u.hiddenContainer,null,a,u,i,l,r),u.deps<=0&&u.resolve()));else if(b&&Ls(b,f))o(b,f,s,n,a,u,i,l,r),La(u,f);else if(Li(t,"onPending"),u.pendingBranch=f,f.shapeFlag&512?u.pendingId=f.component.suspenseId:u.pendingId=To++,o(null,f,u.hiddenContainer,null,a,u,i,l,r),u.deps<=0)u.resolve();else{const{timeout:x,pendingId:m}=u;x>0?setTimeout(()=>{u.pendingId===m&&u.fallback(p)},x):x===0&&u.fallback(p)}}function Cp(e,t,s,n,a,i,l,r,o,c,d=!1){const{p:u,m:f,um:p,n:b,o:{parentNode:y,remove:A}}=c;let O;const x=$b(e);x&&t&&t.pendingBranch&&(O=t.pendingId,t.deps++);const m=e.props?Nl(e.props.timeout):void 0,_=i,S={vnode:e,parent:t,parentComponent:s,namespace:l,container:n,hiddenContainer:a,deps:0,pendingId:To++,timeout:typeof m=="number"?m:-1,activeBranch:null,isFallbackMountPending:!1,pendingBranch:null,isInFallback:!d,isHydrating:d,isUnmounted:!1,effects:[],resolve(g=!1,w=!1){const{vnode:T,activeBranch:C,pendingBranch:M,pendingId:B,effects:$,parentComponent:I,container:j,isInFallback:Y}=S;let H=!1;if(S.isHydrating)S.isHydrating=!1;else if(!g){H=C&&M.transition&&M.transition.mode==="out-in";let Z=!1;H&&(C.transition.afterLeave=()=>{B===S.pendingId&&(f(M,j,i===_&&!Z?b(C):i,0),Ri($),Y&&T.ssFallback&&(T.ssFallback.el=null))}),C&&!S.isFallbackMountPending&&(y(C.el)===j&&(i=b(C),Z=!0),p(C,I,S,!0),!H&&Y&&T.ssFallback&&kt(()=>T.ssFallback.el=null,S)),H||f(M,j,i,0)}S.isFallbackMountPending=!1,La(S,M),S.pendingBranch=null,S.isInFallback=!1;let N=S.parent,L=!1;for(;N;){if(N.pendingBranch){N.effects.push(...$),L=!0;break}N=N.parent}!L&&!H&&Ri($),S.effects=[],x&&t&&t.pendingBranch&&O===t.pendingId&&(t.deps--,t.deps===0&&!w&&t.resolve()),Li(T,"onResolve")},fallback(g){if(!S.pendingBranch)return;const{vnode:w,activeBranch:T,parentComponent:C,container:M,namespace:B}=S;Li(w,"onFallback");const $=b(T),I=()=>{S.isFallbackMountPending=!1,S.isInFallback&&(u(null,g,M,$,C,null,B,r,o),La(S,g))},j=g.transition&&g.transition.mode==="out-in";j&&(S.isFallbackMountPending=!0,T.transition.afterLeave=I),S.isInFallback=!0,p(T,C,null,!0),j||I()},move(g,w,T){S.activeBranch&&f(S.activeBranch,g,w,T),S.container=g},next(){return S.activeBranch&&b(S.activeBranch)},registerDep(g,w,T){const C=!!S.pendingBranch;C&&S.deps++;const M=g.vnode.el;g.asyncDep.catch(B=>{da(B,g,0)}).then(B=>{if(g.isUnmounted||S.isUnmounted||S.pendingId!==g.suspenseId)return;Pi(),g.asyncResolved=!0;const{vnode:$}=g;Co(g,B,!1),M&&($.el=M);const I=!M&&g.subTree.el;w(g,$,y(M||g.subTree.el),M?null:b(g.subTree),S,l,T),I&&($.placeholder=null,A(I)),Tr(g,$.el),C&&--S.deps===0&&S.resolve()})},unmount(g,w){S.isUnmounted=!0,S.activeBranch&&p(S.activeBranch,s,g,w),S.pendingBranch&&p(S.pendingBranch,s,g,w)}};return S}function Pb(e,t,s,n,a,i,l,r,o){const c=t.suspense=Cp(t,n,s,e.parentNode,document.createElement("div"),null,a,i,l,r,!0),d=o(e,c.pendingBranch=t.ssContent,s,c,i,l);return c.deps===0&&c.resolve(!1,!0),d}function Fb(e){const{shapeFlag:t,children:s}=e,n=t&32;e.ssContent=Td(n?s.default:s),e.ssFallback=n?Td(s.fallback):ft(yt)}function Td(e){let t;if(Ie(e)){const s=na&&e._c;s&&(e._d=!1,Di()),e=e(),s&&(e._d=!0,t=zt,Ap())}return ge(e)&&(e=_b(e)),e=rs(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter(s=>s!==e)),e}function Ep(e,t){t&&t.pendingBranch?ge(e)?t.effects.push(...e):t.effects.push(e):Ri(e)}function La(e,t){e.activeBranch=t;const{vnode:s,parentComponent:n}=e;let a=t.el;for(;!a&&t.component;)t=t.component.subTree,a=t.el;s.el=a,n&&n.subTree===s&&(n.vnode.el=a,Tr(n,a))}function $b(e){const t=e.props&&e.props.suspensible;return t!=null&&t!==!1}const Ot=Symbol.for("v-fgt"),Pn=Symbol.for("v-txt"),yt=Symbol.for("v-cmt"),ea=Symbol.for("v-stc"),yi=[];let zt=null;function Di(e=!1){yi.push(zt=e?null:[])}function Ap(){yi.pop(),zt=yi[yi.length-1]||null}let na=1;function Mi(e,t=!1){na+=e,e<0&&zt&&t&&(zt.hasOnce=!0)}function Rp(e){return e.dynamicChildren=na>0?zt||Ea:null,Ap(),na>0&&zt&&zt.push(e),e}function Ub(e,t,s,n,a,i){return Rp(bc(e,t,s,n,a,i,!0))}function Vl(e,t,s,n,a){return Rp(ft(e,t,s,n,a,!0))}function Sn(e){return e?e.__v_isVNode===!0:!1}function Ls(e,t){return e.type===t.type&&e.key===t.key}function Bb(e){}const Ip=({key:e})=>e??null,Cl=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?Me(e)||St(e)||Ie(e)?{i:Pt,r:e,k:t,f:!!s}:e:null);function bc(e,t=null,s=null,n=0,a=null,i=e===Ot?0:1,l=!1,r=!1){const o={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Ip(t),ref:t&&Cl(t),scopeId:yr,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:n,dynamicProps:a,dynamicChildren:null,appContext:null,ctx:Pt};return r?(xc(o,s),i&128&&e.normalize(o)):s&&(o.shapeFlag|=Me(s)?8:16),na>0&&!l&&zt&&(o.patchFlag>0||i&6)&&o.patchFlag!==32&&zt.push(o),o}const ft=Hb;function Hb(e,t=null,s=null,n=0,a=null,i=!1){if((!e||e===ip)&&(e=yt),Sn(e)){const r=Ys(e,t,!0);return s&&xc(r,s),na>0&&!i&&zt&&(r.shapeFlag&6?zt[zt.indexOf(e)]=r:zt.push(r)),r.patchFlag=-2,r}if(Wb(e)&&(e=e.__vccOpts),t){t=Op(t);let{class:r,style:o}=t;r&&!Me(r)&&(t.class=qi(r)),Qe(o)&&(Gi(o)&&!ge(o)&&(o=je({},o)),t.style=zi(o))}const l=Me(e)?1:Hl(e)?128:Wf(e)?64:Qe(e)?4:Ie(e)?2:0;return bc(e,t,s,n,a,l,i,!0)}function Op(e){return e?Gi(e)||hp(e)?je({},e):e:null}function Ys(e,t,s=!1,n=!1){const{props:a,ref:i,patchFlag:l,children:r,transition:o}=e,c=t?Lp(a||{},t):a,d={__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&Ip(c),ref:t&&t.ref?s&&i?ge(i)?i.concat(Cl(t)):[i,Cl(t)]:Cl(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:r,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ot?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:o,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Ys(e.ssContent),ssFallback:e.ssFallback&&Ys(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return o&&n&&wn(d,o.clone(d)),d}function yc(e=" ",t=0){return ft(Pn,null,e,t)}function Vb(e,t){const s=ft(ea,null,e);return s.staticCount=t,s}function Np(e="",t=!1){return t?(Di(),Vl(yt,null,e)):ft(yt,null,e)}function rs(e){return e==null||typeof e=="boolean"?ft(yt):ge(e)?ft(Ot,null,e.slice()):Sn(e)?cn(e):ft(Pn,null,String(e))}function cn(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Ys(e)}function xc(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(ge(t))s=16;else if(typeof t=="object")if(n&65){const a=t.default;a&&(a._c&&(a._d=!1),xc(e,a()),a._c&&(a._d=!0));return}else{s=32;const a=t._;!a&&!hp(t)?t._ctx=Pt:a===3&&Pt&&(Pt.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else Ie(t)?(t={default:t,_ctx:Pt},s=32):(t=String(t),n&64?(s=16,t=[yc(t)]):s=8);e.children=t,e.shapeFlag|=s}function Lp(...e){const t={};for(let s=0;sMt||Pt;let jl,Da;{const e=pr(),t=(s,n)=>{let a;return(a=e[s])||(a=e[s]=[]),a.push(n),i=>{a.length>1?a.forEach(l=>l(i)):a[0](i)}};jl=t("__VUE_INSTANCE_SETTERS__",s=>Mt=s),Da=t("__VUE_SSR_SETTERS__",s=>aa=s)}const Ya=e=>{const t=Mt;return jl(e),e.scope.on(),()=>{e.scope.off(),jl(t)}},Pi=()=>{Mt&&Mt.scope.off(),jl(null)};function Mp(e){return e.vnode.shapeFlag&4}let aa=!1;function Pp(e,t=!1,s=!1){t&&Da(t);const{props:n,children:a}=e.vnode,i=Mp(e);Tb(e,n,i,t),Rb(e,a,s||t);const l=i?qb(e,t):void 0;return t&&Da(!1),l}function qb(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,_o);const{setup:n}=s;if(n){_n();const a=e.setupContext=n.length>1?Up(e):null,i=Ya(e),l=Ja(n,e,0,[e.props,a]),r=Yo(l);if(kn(),i(),(r||e.sp)&&!bn(e)&&cc(e),r){if(l.then(Pi,Pi),t)return l.then(o=>{Co(e,o,t)}).catch(o=>{da(o,e,0)});e.asyncDep=l}else Co(e,l,t)}else $p(e,t)}function Co(e,t,s){Ie(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Qe(t)&&(e.setupState=ac(t)),$p(e,s)}let zl,Eo;function Fp(e){zl=e,Eo=t=>{t.render._rc&&(t.withProxy=new Proxy(t.ctx,Yv))}}const Gb=()=>!zl;function $p(e,t,s){const n=e.type;if(!e.render){if(!t&&zl&&!n.render){const a=n.template||hc(e).template;if(a){const{isCustomElement:i,compilerOptions:l}=e.appContext.config,{delimiters:r,compilerOptions:o}=n,c=je(je({isCustomElement:i,delimiters:r},l),o);n.render=zl(a,c)}}e.render=n.render||Ft,Eo&&Eo(e)}{const a=Ya(e);_n();try{ub(e)}finally{kn(),a()}}}const Kb={get(e,t){return jt(e,"get",""),e[t]}};function Up(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,Kb),slots:e.slots,emit:e.emit,expose:t}}function Ji(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(ac(Lf(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in bi)return bi[s](e)},has(t,s){return s in t||s in bi}})):e.proxy}function Ao(e,t=!0){return Ie(e)?e.displayName||e.name:e.name||t&&e.__name}function Wb(e){return Ie(e)&&"__vccOpts"in e}const J=(e,t)=>ev(e,t,aa);function Ua(e,t,s){try{Mi(-1);const n=arguments.length;return n===2?Qe(t)&&!ge(t)?Sn(t)?ft(e,null,[t]):ft(e,t):ft(e,null,t):(n>3?s=Array.prototype.slice.call(arguments,2):n===3&&Sn(s)&&(s=[s]),ft(e,t,s))}finally{Mi(1)}}function Zb(){}function Jb(e,t,s,n){const a=s[n];if(a&&Bp(a,e))return a;const i=t();return i.memo=e.slice(),i.cacheIndex=n,s[n]=i}function Bp(e,t){const s=e.memo;if(s.length!=t.length)return!1;for(let n=0;n0&&zt&&zt.push(e),!0}const Hp="3.5.38",Yb=Ft,Qb=cv,Xb=ka,ey=Vf,ty={createComponentInstance:Dp,setupComponent:Pp,renderComponentRoot:Tl,setCurrentRenderingInstance:Oi,isVNode:Sn,normalizeVNode:rs,getComponentPublicInstance:Ji,ensureValidVNode:pc,pushWarningContext:iv,popWarningContext:lv},sy=ty,ny=null,ay=null,iy=null;/** +**/const $f=[];function iv(e){$f.push(e)}function lv(){$f.pop()}function rv(e,t){}const ov={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"},cv={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 ti(e,t,s,n){try{return n?e(...n):e()}catch(a){fa(a,t,s)}}function xs(e,t,s,n){if(Ie(e)){const a=ti(e,t,s,n);return a&&Yo(a)&&a.catch(i=>{fa(i,t,s)}),a}if(be(e)){const a=[];for(let i=0;i>>1,a=ts[n],i=Pi(a);i=Pi(s)?ts.push(e):ts.splice(uv(t),0,e),e.flags|=1,Uf()}}function Uf(){Fl||(Fl=Bf.then(Hf))}function Mi(e){be(e)?Pa.push(...e):Mn&&e.id===-1?Mn.splice(Ca+1,0,e):e.flags&1||(Pa.push(e),e.flags|=1),Uf()}function ud(e,t,s=Ys+1){for(;sPi(s)-Pi(n));if(Pa.length=0,Mn){Mn.push(...t);return}for(Mn=t,Ca=0;Cae.id==null?e.flags&2?-1:1/0:e.id;function Hf(e){try{for(Ys=0;YsEa.emit(a,...i)),fl=[]):typeof window<"u"&&window.HTMLElement&&!((n=(s=window.navigator)==null?void 0:s.userAgent)!=null&&n.includes("jsdom"))?((t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push(i=>{zf(i,t)}),setTimeout(()=>{Ea||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,fl=[])},3e3)):fl=[]}let Ut=null,xr=null;function Fi(e){const t=Ut;return Ut=e,xr=e&&e.type.__scopeId||null,t}function fv(e){xr=e}function pv(){xr=null}const hv=e=>lc;function lc(e,t=Ut,s){if(!t||e._n)return e;const n=(...a)=>{n._d&&Hi(-1);const i=Fi(t);let l;try{l=e(...a)}finally{Fi(i),n._d&&Hi(1)}return l};return n._n=!0,n._c=!0,n._d=!0,n}function mv(e,t){if(Ut===null)return e;const s=sl(Ut),n=e.dirs||(e.dirs=[]);for(let a=0;a1)return s&&Ie(t)?t.call(n&&n.proxy):t}}function gv(){return!!(as()||ta)}const Vf=Symbol.for("v-scx"),jf=()=>Os(Vf);function vv(e,t){return Xi(e,null,t)}function bv(e,t){return Xi(e,null,{flush:"post"})}function qf(e,t){return Xi(e,null,{flush:"sync"})}function ns(e,t,s){return Xi(e,t,s)}function Xi(e,t,s=je){const{immediate:n,deep:a,flush:i,once:l}=s,r=ze({},s),o=t&&n||!t&&i!=="post";let c;if(la){if(i==="sync"){const p=jf();c=p.__watcherHandles||(p.__watcherHandles=[])}else if(!o){const p=()=>{};return p.stop=Ht,p.resume=Ht,p.pause=Ht,p}}const d=Bt;r.call=(p,b,y)=>xs(p,d,b,y);let u=!1;i==="post"?r.scheduler=p=>{kt(p,d&&d.suspense)}:i!=="sync"&&(u=!0,r.scheduler=(p,b)=>{b?p():ic(p)}),r.augmentJob=p=>{t&&(p.flags|=4),u&&(p.flags|=2,d&&(p.id=d.uid,p.i=d))};const f=av(e,t,r);return la&&(c?c.push(f):o&&f()),f}function yv(e,t,s){const n=this.proxy,a=Me(e)?e.includes(".")?Gf(n,e):()=>n[e]:e.bind(n,n);let i;Ie(t)?i=t:(i=t.handler,s=t);const l=si(this),r=Xi(a,i.bind(n),s);return l(),r}function Gf(e,t){const s=t.split(".");return()=>{let n=e;for(let a=0;ae.__isTeleport,Jn=e=>e&&(e.disabled||e.disabled===""),xv=e=>e&&(e.defer||e.defer===""),fd=e=>typeof SVGElement<"u"&&e instanceof SVGElement,pd=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,bo=(e,t)=>{const s=e&&e.to;return Me(s)?t?t(s):null:s},_v={name:"Teleport",__isTeleport:!0,process(e,t,s,n,a,i,l,r,o,c){const{mc:d,pc:u,pbc:f,o:{insert:p,querySelector:b,createText:y,createComment:E,parentNode:I}}=c,x=Jn(t.props);let{dynamicChildren:m}=t;const _=(w,T,C)=>{w.shapeFlag&16&&d(w.children,T,C,a,i,l,r,o)},S=(w=t)=>{const T=Jn(w.props),C=w.target=bo(w.props,b),M=yo(C,w,y,p);C&&(l!=="svg"&&fd(C)?l="svg":l!=="mathml"&&pd(C)&&(l="mathml"),a&&a.isCE&&(a.ce._teleportTargets||(a.ce._teleportTargets=new Set)).add(C),T||(_(w,C,M),gi(w,!1)))},g=w=>{const T=()=>{if(Nn.get(w)===T){if(Nn.delete(w),Jn(w.props)){const C=I(w.el)||s;_(w,C,w.anchor),gi(w,!0)}S(w)}};Nn.set(w,T),kt(T,i)};if(e==null){const w=t.el=y(""),T=t.anchor=y("");if(p(w,s,n),p(T,s,n),xv(t.props)||i&&i.pendingBranch){g(t);return}x&&(_(t,s,T),gi(t,!0)),S()}else{t.el=e.el;const w=t.anchor=e.anchor,T=Nn.get(e);if(T){T.flags|=8,Nn.delete(e),g(t);return}t.targetStart=e.targetStart;const C=t.target=e.target,M=t.targetAnchor=e.targetAnchor,H=Jn(e.props),P=H?s:C,R=H?w:M;if(l==="svg"||fd(C)?l="svg":(l==="mathml"||pd(C))&&(l="mathml"),m?(f(e.dynamicChildren,m,P,a,i,l,r),vc(e,t,!0)):o||u(e,t,P,R,a,i,l,r,!1),x)H?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):pl(t,s,w,c,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const j=t.target=bo(t.props,b);j&&pl(t,j,null,c,0)}else H&&pl(t,C,M,c,1);gi(t,x)}},remove(e,t,s,{um:n,o:{remove:a}},i){const{shapeFlag:l,children:r,anchor:o,targetStart:c,targetAnchor:d,target:u,props:f}=e,p=i||!Jn(f),b=Nn.get(e);if(b&&(b.flags|=8,Nn.delete(e)),u&&(a(c),a(d)),i&&a(o),!b&&l&16)for(let y=0;y{e.isMounted=!0}),Sr(()=>{e.isUnmounting=!0}),e}const Es=[Function,Array],oc={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Es,onEnter:Es,onAfterEnter:Es,onEnterCancelled:Es,onBeforeLeave:Es,onLeave:Es,onAfterLeave:Es,onLeaveCancelled:Es,onBeforeAppear:Es,onAppear:Es,onAfterAppear:Es,onAppearCancelled:Es},Zf=e=>{const t=e.subTree;return t.component?Zf(t.component):t},Sv={name:"BaseTransition",props:oc,setup(e,{slots:t}){const s=as(),n=rc();return()=>{const a=t.default&&_r(t.default(),!0),i=a&&a.length?Jf(a):s.subTree?Np():void 0;if(!i)return;const l=Je(e),{mode:r}=l;if(n.isLeaving)return jr(i);const o=hd(i);if(!o)return jr(i);let c=Va(o,l,n,s,u=>c=u);o.type!==yt&&Tn(o,c);let d=s.subTree&&hd(s.subTree);if(d&&d.type!==yt&&!Hs(d,o)&&Zf(s).type!==yt){let u=Va(d,l,n,s);if(Tn(d,u),r==="out-in"&&o.type!==yt)return n.isLeaving=!0,u.afterLeave=()=>{n.isLeaving=!1,s.job.flags&8||s.update(),delete u.afterLeave,d=void 0},jr(i);r==="in-out"&&o.type!==yt?u.delayLeave=(f,p,b)=>{const y=Qf(n,d);y[String(d.key)]=d,f[As]=()=>{p(),f[As]=void 0,delete c.delayedLeave,d=void 0},c.delayedLeave=()=>{b(),delete c.delayedLeave,d=void 0}}:d=void 0}else d&&(d=void 0);return i}}};function Jf(e){let t=e[0];if(e.length>1){for(const s of e)if(s.type!==yt){t=s;break}}return t}const Yf=Sv;function Qf(e,t){const{leavingVNodes:s}=e;let n=s.get(t.type);return n||(n=Object.create(null),s.set(t.type,n)),n}function Va(e,t,s,n,a){const{appear:i,mode:l,persisted:r=!1,onBeforeEnter:o,onEnter:c,onAfterEnter:d,onEnterCancelled:u,onBeforeLeave:f,onLeave:p,onAfterLeave:b,onLeaveCancelled:y,onBeforeAppear:E,onAppear:I,onAfterAppear:x,onAppearCancelled:m}=t,_=String(e.key),S=Qf(s,e),g=(C,M)=>{C&&xs(C,n,9,M)},w=(C,M)=>{const H=M[1];g(C,M),be(C)?C.every(P=>P.length<=1)&&H():C.length<=1&&H()},T={mode:l,persisted:r,beforeEnter(C){let M=o;if(!s.isMounted)if(i)M=E||o;else return;C[As]&&C[As](!0);const H=S[_];H&&Hs(e,H)&&H.el[As]&&H.el[As](),g(M,[C])},enter(C){if(S[_]===e)return;let M=c,H=d,P=u;if(!s.isMounted)if(i)M=I||c,H=x||d,P=m||u;else return;let R=!1;C[oi]=Q=>{R||(R=!0,Q?g(P,[C]):g(H,[C]),T.delayedLeave&&T.delayedLeave(),C[oi]=void 0)};const j=C[oi].bind(null,!1);M?w(M,[C,j]):j()},leave(C,M){const H=String(e.key);if(C[oi]&&C[oi](!0),s.isUnmounting)return M();g(f,[C]);let P=!1;C[As]=j=>{P||(P=!0,M(),j?g(y,[C]):g(b,[C]),C[As]=void 0,S[H]===e&&delete S[H])};const R=C[As].bind(null,!1);S[H]=e,p?w(p,[C,R]):R()},clone(C){const M=Va(C,t,s,n,a);return a&&a(M),M}};return T}function jr(e){if(tl(e))return e=sn(e),e.children=null,e}function hd(e){if(!tl(e))return Wf(e.type)&&e.children?Jf(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&&Ie(s.default))return s.default()}}function Tn(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Tn(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 _r(e,t=!1,s){let n=[],a=0;for(let i=0;i1)for(let i=0;is.value,set:i=>s.value=i})}return s}function md(e,t){let s;return!!((s=Object.getOwnPropertyDescriptor(e,t))&&!s.configurable)}const Bl=new WeakMap;function Fa(e,t,s,n,a=!1){if(be(e)){e.forEach((y,E)=>Fa(y,t&&(be(t)?t[E]:t),s,n,a));return}if(xn(n)&&!a){n.shapeFlag&512&&n.type.__asyncResolved&&n.component.subTree.component&&Fa(e,t,s,n.component.subTree);return}const i=n.shapeFlag&4?sl(n.component):n.el,l=a?null:i,{i:r,r:o}=e,c=t&&t.r,d=r.refs===je?r.refs={}:r.refs,u=r.setupState,f=Je(u),p=u===je?Ia:y=>md(d,y)?!1:tt(f,y),b=(y,E)=>!(E&&md(d,E));if(c!=null&&c!==o){if(gd(t),Me(c))d[c]=null,p(c)&&(u[c]=null);else if(St(c)){const y=t;b(c,y.k)&&(c.value=null),y.k&&(d[y.k]=null)}}if(Ie(o))ti(o,r,12,[l,d]);else{const y=Me(o),E=St(o);if(y||E){const I=()=>{if(e.f){const x=y?p(o)?u[o]:d[o]:b()||!e.k?o.value:d[e.k];if(a)be(x)&&Jo(x,i);else if(be(x))x.includes(i)||x.push(i);else if(y)d[o]=[i],p(o)&&(u[o]=d[o]);else{const m=[i];b(o,e.k)&&(o.value=m),e.k&&(d[e.k]=m)}}else y?(d[o]=l,p(o)&&(u[o]=l)):E&&(b(o,e.k)&&(o.value=l),e.k&&(d[e.k]=l))};if(l){const x=()=>{I(),Bl.delete(e)};x.id=-1,Bl.set(e,x),kt(x,s)}else gd(e),I()}}}function gd(e){const t=Bl.get(e);t&&(t.flags|=8,Bl.delete(e))}let vd=!1;const _a=()=>{vd||(console.error("Hydration completed but contains mismatches."),vd=!0)},Ev=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",Av=e=>e.namespaceURI.includes("MathML"),hl=e=>{if(e.nodeType===1){if(Ev(e))return"svg";if(Av(e))return"mathml"}},Oa=e=>e.nodeType===8;function Rv(e){const{mt:t,p:s,o:{patchProp:n,createText:a,nextSibling:i,parentNode:l,remove:r,insert:o,createComment:c}}=e,d=(m,_)=>{if(!_.hasChildNodes()){s(null,m,_),$l(),_._vnode=m;return}u(_.firstChild,m,null,null,null),$l(),_._vnode=m},u=(m,_,S,g,w,T=!1)=>{T=T||!!_.dynamicChildren;const C=Oa(m)&&m.data==="[",M=()=>y(m,_,S,g,w,C),{type:H,ref:P,shapeFlag:R,patchFlag:j}=_;let Q=m.nodeType;_.el=m,j===-2&&(T=!1,_.dynamicChildren=null);let U=null;switch(H){case $n:Q!==3?_.children===""?(o(_.el=a(""),l(m),m),U=m):U=M():(m.data!==_.children&&(_a(),m.data=_.children),U=i(m));break;case yt:x(m)?(U=i(m),I(_.el=m.content.firstChild,m,S)):Q!==8||C?U=M():U=i(m);break;case sa:if(C&&(m=i(m),Q=m.nodeType),Q===1||Q===3){U=m;const O=!_.children.length;for(let N=0;N<_.staticCount;N++)O&&(_.children+=U.nodeType===1?U.outerHTML:U.data),N===_.staticCount-1&&(_.anchor=U),U=i(U);return C?i(U):U}else M();break;case Dt:C?U=b(m,_,S,g,w,T):U=M();break;default:if(R&1)(Q!==1||_.type.toLowerCase()!==m.tagName.toLowerCase())&&!x(m)?U=M():U=f(m,_,S,g,w,T);else if(R&6){_.slotScopeIds=w;const O=l(m);if(C?U=E(m):Oa(m)&&m.data==="teleport start"?U=E(m,m.data,"teleport end"):U=i(m),t(_,O,null,S,g,hl(O),T),xn(_)&&!_.type.__asyncResolved){let N;C?(N=ft(Dt),N.anchor=U?U.previousSibling:O.lastChild):N=m.nodeType===3?yc(""):ft("div"),N.el=m,_.component.subTree=N}}else R&64?Q!==8?U=M():U=_.type.hydrate(m,_,S,g,w,T,e,p):R&128&&(U=_.type.hydrate(m,_,S,g,hl(l(m)),w,T,e,u))}return P!=null&&Fa(P,null,g,_),U},f=(m,_,S,g,w,T)=>{T=T||!!_.dynamicChildren;const{type:C,props:M,patchFlag:H,shapeFlag:P,dirs:R,transition:j}=_,Q=C==="input"||C==="option";if(Q||H!==-1){R&&Qs(_,null,S,"created");let U=!1;if(x(m)){U=wp(null,j)&&S&&S.vnode.props&&S.vnode.props.appear;const N=m.content.firstChild;if(U){const Y=N.getAttribute("class");Y&&(N.$cls=Y),j.beforeEnter(N)}I(N,m,S),_.el=m=N}if(P&16&&!(M&&(M.innerHTML||M.textContent))){let N=p(m.firstChild,_,m,S,g,w,T);for(N&&!ml(m,1)&&_a();N;){const Y=N;N=N.nextSibling,r(Y)}}else if(P&8){let N=_.children;N[0]===` +`&&(m.tagName==="PRE"||m.tagName==="TEXTAREA")&&(N=N.slice(1));const{textContent:Y}=m;Y!==N&&Y!==N.replace(/\r\n|\r/g,` +`)&&(ml(m,0)||_a(),m.textContent=_.children)}if(M){if(Q||!T||H&48){const N=m.tagName.includes("-");for(const Y in M)(Q&&(Y.endsWith("value")||Y==="indeterminate")||ca(Y)&&!bn(Y)||Y[0]==="."||N&&!bn(Y))&&n(m,Y,null,M[Y],void 0,S)}else if(M.onClick)n(m,"onClick",null,M.onClick,void 0,S);else if(H&4&&yn(M.style))for(const N in M.style)M.style[N]}let O;(O=M&&M.onVnodeBeforeMount)&&ds(O,S,_),R&&Qs(_,null,S,"beforeMount"),((O=M&&M.onVnodeMounted)||R||U)&&Ep(()=>{O&&ds(O,S,_),U&&j.enter(m),R&&Qs(_,null,S,"mounted")},g)}return m.nextSibling},p=(m,_,S,g,w,T,C)=>{C=C||!!_.dynamicChildren;const M=_.children,H=M.length;let P=!1;for(let R=0;R{const{slotScopeIds:C}=_;C&&(w=w?w.concat(C):C);const M=l(m),H=p(i(m),_,M,S,g,w,T);return H&&Oa(H)&&H.data==="]"?i(_.anchor=H):(_a(),o(_.anchor=c("]"),M,H),H)},y=(m,_,S,g,w,T)=>{if(ml(m.parentElement,1)||_a(),_.el=null,T){const H=E(m);for(;;){const P=i(m);if(P&&P!==H)r(P);else break}}const C=i(m),M=l(m);return r(m),s(null,_,M,C,S,g,hl(M),w),S&&(S.vnode.el=_.el,Cr(S,_.el)),C},E=(m,_="[",S="]")=>{let g=0;for(;m;)if(m=i(m),m&&Oa(m)&&(m.data===_&&g++,m.data===S)){if(g===0)return i(m);g--}return m},I=(m,_,S)=>{const g=_.parentNode;g&&g.replaceChild(m,_);let w=S;for(;w;)w.vnode.el===_&&(w.vnode.el=w.subTree.el=m),w=w.parent},x=m=>m.nodeType===1&&m.tagName==="TEMPLATE";return[d,u]}const bd="data-allow-mismatch",Iv={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function ml(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(bd);)e=e.parentElement;const s=e&&e.getAttribute(bd);if(s==null)return!1;if(s==="")return!0;{const n=s.split(",");return t===0&&n.includes("children")?!0:n.includes(Iv[t])}}const Ov=hr().requestIdleCallback||(e=>setTimeout(e,1)),Nv=hr().cancelIdleCallback||(e=>clearTimeout(e)),Lv=(e=1e4)=>t=>{const s=Ov(t,{timeout:e});return()=>Nv(s)};function Dv(e){const{top:t,left:s,bottom:n,right:a}=e.getBoundingClientRect(),{innerHeight:i,innerWidth:l}=window;return(t>0&&t0&&n0&&s0&&a(t,s)=>{const n=new IntersectionObserver(a=>{for(const i of a)if(i.isIntersecting){n.disconnect(),t();break}},e);return s(a=>{if(a instanceof Element){if(Dv(a))return t(),n.disconnect(),!1;n.observe(a)}}),()=>n.disconnect()},Pv=e=>t=>{if(e){const s=matchMedia(e);if(s.matches)t();else return s.addEventListener("change",t,{once:!0}),()=>s.removeEventListener("change",t)}},Fv=(e=[])=>(t,s)=>{Me(e)&&(e=[e]);let n=!1;const a=l=>{n||(n=!0,i(),t(),l.target.dispatchEvent(new l.constructor(l.type,l)))},i=()=>{s(l=>{for(const r of e)l.removeEventListener(r,a)})};return s(l=>{for(const r of e)l.addEventListener(r,a,{once:!0})}),i};function $v(e,t){if(Oa(e)&&e.data==="["){let s=1,n=e.nextSibling;for(;n;){if(n.nodeType===1){if(t(n)===!1)break}else if(Oa(n))if(n.data==="]"){if(--s===0)break}else n.data==="["&&s++;n=n.nextSibling}}else t(e)}const xn=e=>!!e.type.__asyncLoader;function Bv(e){Ie(e)&&(e={loader:e});const{loader:t,loadingComponent:s,errorComponent:n,delay:a=200,hydrate:i,timeout:l,suspensible:r=!0,onError:o}=e;let c=null,d,u=0;const f=()=>(u++,c=null,p()),p=()=>{let b;return c||(b=c=t().catch(y=>{if(y=y instanceof Error?y:new Error(String(y)),o)return new Promise((E,I)=>{o(y,()=>E(f()),()=>I(y),u+1)});throw y}).then(y=>b!==c&&c?c:(y&&(y.__esModule||y[Symbol.toStringTag]==="Module")&&(y=y.default),d=y,y)))};return el({name:"AsyncComponentWrapper",__asyncLoader:p,__asyncHydrate(b,y,E){let I=!1;(y.bu||(y.bu=[])).push(()=>I=!0);const x=()=>{I||E()},m=i?()=>{const _=i(x,S=>$v(b,S));_&&(y.bum||(y.bum=[])).push(_)}:x;d?m():p().then(()=>!y.isUnmounted&&m())},get __asyncResolved(){return d},setup(){const b=Bt;if(cc(b),d)return()=>gl(d,b);const y=S=>{c=null,fa(S,b,13,!n)};if(r&&b.suspense||la)return p().then(S=>()=>gl(S,b)).catch(S=>(y(S),()=>n?ft(n,{error:S}):null));const E=h(!1),I=h(),x=h(!!a);let m,_;return xt(()=>{m!=null&&clearTimeout(m),_!=null&&clearTimeout(_)}),a&&(_=setTimeout(()=>{b.isUnmounted||(x.value=!1)},a)),l!=null&&(m=setTimeout(()=>{if(!b.isUnmounted&&!E.value&&!I.value){const S=new Error(`Async component timed out after ${l}ms.`);y(S),I.value=S}},l)),p().then(()=>{b.isUnmounted||(E.value=!0,b.parent&&tl(b.parent.vnode)&&b.parent.update())}).catch(S=>{if(b.isUnmounted){c=null;return}y(S),I.value=S}),()=>{if(E.value&&d)return gl(d,b);if(I.value&&n)return ft(n,{error:I.value});if(s&&!x.value)return gl(s,b)}}})}function gl(e,t){const{ref:s,props:n,children:a,ce:i}=t.vnode,l=ft(e,n,a);return l.ref=s,l.ce=i,delete t.vnode.ce,l}const tl=e=>e.type.__isKeepAlive,Uv={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){const s=as(),n=s.ctx;if(!n.renderer)return()=>{const x=t.default&&t.default();return x&&x.length===1?x[0]:x};const a=new Map,i=new Set;let l=null;const r=s.suspense,{renderer:{p:o,m:c,um:d,o:{createElement:u}}}=n,f=u("div");n.activate=(x,m,_,S,g)=>{const w=x.component;c(x,m,_,0,r),o(w.vnode,x,m,_,w,r,S,x.slotScopeIds,g),kt(()=>{w.isDeactivated=!1,w.a&&Ma(w.a);const T=x.props&&x.props.onVnodeMounted;T&&ds(T,w.parent,x)},r)},n.deactivate=x=>{const m=x.component;Hl(m.m),Hl(m.a),c(x,f,null,1,r),kt(()=>{m.da&&Ma(m.da);const _=x.props&&x.props.onVnodeUnmounted;_&&ds(_,m.parent,x),m.isDeactivated=!0},r)};function p(x){qr(x),d(x,s,r,!0)}function b(x){a.forEach((m,_)=>{const S=Ao(xn(m)?m.type.__asyncResolved||{}:m.type);S&&!x(S)&&y(_)})}function y(x){const m=a.get(x);m&&(!l||!Hs(m,l))?p(m):l&&qr(l),a.delete(x),i.delete(x)}ns(()=>[e.include,e.exclude],([x,m])=>{x&&b(_=>vi(x,_)),m&&b(_=>!vi(m,_))},{flush:"post",deep:!0});let E=null;const I=()=>{E!=null&&(zl(s.subTree.type)?kt(()=>{a.set(E,vl(s.subTree))},s.subTree.suspense):a.set(E,vl(s.subTree)))};return We(I),wr(I),Sr(()=>{a.forEach(x=>{const{subTree:m,suspense:_}=s,S=vl(m);if(x.type===S.type&&x.key===S.key){qr(S);const g=S.component.da;g&&kt(g,_);return}p(x)})}),()=>{if(E=null,!t.default)return l=null;const x=t.default(),m=x[0];if(x.length>1)return l=null,x;if(!Cn(m)||!(m.shapeFlag&4)&&!(m.shapeFlag&128))return l=null,m;let _=vl(m);if(_.type===yt)return l=null,_;const S=_.type,g=Ao(xn(_)?_.type.__asyncResolved||{}:S),{include:w,exclude:T,max:C}=e;if(w&&(!g||!vi(w,g))||T&&g&&vi(T,g))return _.shapeFlag&=-257,l=_,m;const M=_.key==null?S:_.key,H=a.get(M);return _.el&&(_=sn(_),m.shapeFlag&128&&(m.ssContent=_)),E=M,H?(_.el=H.el,_.component=H.component,_.transition&&Tn(_,_.transition),_.shapeFlag|=512,i.delete(M),i.add(M)):(i.add(M),C&&i.size>parseInt(C,10)&&y(i.values().next().value)),_.shapeFlag|=256,l=_,zl(m.type)?m:_}}},Hv=Uv;function vi(e,t){return be(e)?e.some(s=>vi(s,t)):Me(e)?e.split(",").includes(t):Ym(e)?(e.lastIndex=0,e.test(t)):!1}function Ds(e,t){Xf(e,"a",t)}function Ms(e,t){Xf(e,"da",t)}function Xf(e,t,s=Bt){const n=e.__wdc||(e.__wdc=()=>{let a=s;for(;a;){if(a.isDeactivated)return;a=a.parent}return e()});if(kr(t,n,s),s){let a=s.parent;for(;a&&a.parent;)tl(a.parent.vnode)&&zv(n,t,s,a),a=a.parent}}function zv(e,t,s,n){const a=kr(t,e,n,!0);xt(()=>{Jo(n[t],a)},s)}function qr(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function vl(e){return e.shapeFlag&128?e.ssContent:e}function kr(e,t,s=Bt,n=!1){if(s){const a=s[e]||(s[e]=[]),i=t.__weh||(t.__weh=(...l)=>{wn();const r=si(s),o=xs(t,s,e,l);return r(),Sn(),o});return n?a.unshift(i):a.push(i),i}}const En=e=>(t,s=Bt)=>{(!la||e==="sp")&&kr(e,(...n)=>t(...n),s)},ep=En("bm"),We=En("m"),dc=En("bu"),wr=En("u"),Sr=En("bum"),xt=En("um"),tp=En("sp"),sp=En("rtg"),np=En("rtc");function ap(e,t=Bt){kr("ec",e,t)}const uc="components",Vv="directives";function jv(e,t){return fc(uc,e,!0,t)||e}const ip=Symbol.for("v-ndc");function qv(e){return Me(e)?fc(uc,e,!1)||e:e||ip}function Gv(e){return fc(Vv,e)}function fc(e,t,s=!0,n=!1){const a=Ut||Bt;if(a){const i=a.type;if(e===uc){const r=Ao(i,!1);if(r&&(r===t||r===it(t)||r===ua(it(t))))return i}const l=yd(a[e]||i[e],t)||yd(a.appContext[e],t);return!l&&n?i:l}}function yd(e,t){return e&&(e[t]||e[it(t)]||e[ua(it(t))])}function Kv(e,t,s,n){let a;const i=s&&s[n],l=be(e);if(l||Me(e)){const r=l&&yn(e);let o=!1,c=!1;r&&(o=!ms(e),c=tn(e),e=vr(e)),a=new Array(e.length);for(let d=0,u=e.length;dt(r,o,void 0,i&&i[o]));else{const r=Object.keys(e);a=new Array(r.length);for(let o=0,c=r.length;o{const i=n.fn(...a);return i&&(i.key=n.key),i}:n.fn)}return e}function Zv(e,t,s={},n,a){if(Ut.ce||Ut.parent&&xn(Ut.parent)&&Ut.parent.ce){const c=Object.keys(s).length>0;return t!=="default"&&(s.name=t),Ui(),Vl(Dt,null,[ft("slot",s,n&&n())],c?-2:64)}let i=e[t];i&&i._c&&(i._d=!1),Ui();const l=i&&pc(i(s)),r=s.key||l&&l.key,o=Vl(Dt,{key:(r&&!Jt(r)?r:`_${t}`)+(!l&&n?"_fb":"")},l||(n?n():[]),l&&e._===1?64:-2);return!a&&o.scopeId&&(o.slotScopeIds=[o.scopeId+"-s"]),i&&i._c&&(i._d=!0),o}function pc(e){return e.some(t=>Cn(t)?!(t.type===yt||t.type===Dt&&!pc(t.children)):!0)?e:null}function Jv(e,t){const s={};for(const n in e)s[t&&/[A-Z]/.test(n)?`on:${n}`:Da(n)]=e[n];return s}const xo=e=>e?Mp(e)?sl(e):xo(e.parent):null,Si=ze(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=>xo(e.parent),$root:e=>xo(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>hc(e),$forceUpdate:e=>e.f||(e.f=()=>{ic(e.update)}),$nextTick:e=>e.n||(e.n=Rt.bind(e.proxy)),$watch:e=>yv.bind(e)}),Gr=(e,t)=>e!==je&&!e.__isScriptSetup&&tt(e,t),_o={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:a,props:i,accessCache:l,type:r,appContext:o}=e;if(t[0]!=="$"){const f=l[t];if(f!==void 0)switch(f){case 1:return n[t];case 2:return a[t];case 4:return s[t];case 3:return i[t]}else{if(Gr(n,t))return l[t]=1,n[t];if(a!==je&&tt(a,t))return l[t]=2,a[t];if(tt(i,t))return l[t]=3,i[t];if(s!==je&&tt(s,t))return l[t]=4,s[t];ko&&(l[t]=0)}}const c=Si[t];let d,u;if(c)return t==="$attrs"&&Kt(e.attrs,"get",""),c(e);if((d=r.__cssModules)&&(d=d[t]))return d;if(s!==je&&tt(s,t))return l[t]=4,s[t];if(u=o.config.globalProperties,tt(u,t))return u[t]},set({_:e},t,s){const{data:n,setupState:a,ctx:i}=e;return Gr(a,t)?(a[t]=s,!0):n!==je&&tt(n,t)?(n[t]=s,!0):tt(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:a,props:i,type:l}},r){let o;return!!(s[r]||e!==je&&r[0]!=="$"&&tt(e,r)||Gr(t,r)||tt(i,r)||tt(n,r)||tt(Si,r)||tt(a.config.globalProperties,r)||(o=l.__cssModules)&&o[r])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:tt(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}},Yv=ze({},_o,{get(e,t){if(t!==Symbol.unscopables)return _o.get(e,t,e)},has(e,t){return t[0]!=="_"&&!ag(t)}});function Qv(){return null}function Xv(){return null}function eb(e){}function tb(e){}function sb(){return null}function nb(){}function ab(e,t){return null}function ib(){return lp().slots}function lb(){return lp().attrs}function lp(e){const t=as();return t.setupContext||(t.setupContext=Bp(t))}function $i(e){return be(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}function rb(e,t){const s=$i(e);for(const n in t){if(n.startsWith("__skip"))continue;let a=s[n];a?be(a)||Ie(a)?a=s[n]={type:a,default:t[n]}:a.default=t[n]:a===null&&(a=s[n]={default:t[n]}),a&&t[`__skip_${n}`]&&(a.skipFactory=!0)}return s}function ob(e,t){return!e||!t?e||t:be(e)&&be(t)?e.concat(t):ze({},$i(e),$i(t))}function cb(e,t){const s={};for(const n in e)t.includes(n)||Object.defineProperty(s,n,{enumerable:!0,get:()=>e[n]});return s}function db(e){const t=as(),s=la;let n=e();zi(),s&&Ba(!1);const a=()=>{si(t),s&&Ba(!0)},i=()=>{as()!==t&&t.scope.off(),zi(),s&&Ba(!1)};return Yo(n)&&(n=n.catch(l=>{throw a(),Promise.resolve().then(()=>Promise.resolve().then(i)),l})),[n,()=>{a(),Promise.resolve().then(i)}]}let ko=!0;function ub(e){const t=hc(e),s=e.proxy,n=e.ctx;ko=!1,t.beforeCreate&&xd(t.beforeCreate,e,"bc");const{data:a,computed:i,methods:l,watch:r,provide:o,inject:c,created:d,beforeMount:u,mounted:f,beforeUpdate:p,updated:b,activated:y,deactivated:E,beforeDestroy:I,beforeUnmount:x,destroyed:m,unmounted:_,render:S,renderTracked:g,renderTriggered:w,errorCaptured:T,serverPrefetch:C,expose:M,inheritAttrs:H,components:P,directives:R,filters:j}=t;if(c&&fb(c,n,null),l)for(const O in l){const N=l[O];Ie(N)&&(n[O]=N.bind(s))}if(a){const O=a.call(s,s);Xe(O)&&(e.data=Hn(O))}if(ko=!0,i)for(const O in i){const N=i[O],Y=Ie(N)?N.bind(s,s):Ie(N.get)?N.get.bind(s,s):Ht,we=!Ie(N)&&Ie(N.set)?N.set.bind(s):Ht,ke=J({get:Y,set:we});Object.defineProperty(n,O,{enumerable:!0,configurable:!0,get:()=>ke.value,set:ie=>ke.value=ie})}if(r)for(const O in r)rp(r[O],n,s,O);if(o){const O=Ie(o)?o.call(s):o;Reflect.ownKeys(O).forEach(N=>{wi(N,O[N])})}d&&xd(d,e,"c");function U(O,N){be(N)?N.forEach(Y=>O(Y.bind(s))):N&&O(N.bind(s))}if(U(ep,u),U(We,f),U(dc,p),U(wr,b),U(Ds,y),U(Ms,E),U(ap,T),U(np,g),U(sp,w),U(Sr,x),U(xt,_),U(tp,C),be(M))if(M.length){const O=e.exposed||(e.exposed={});M.forEach(N=>{Object.defineProperty(O,N,{get:()=>s[N],set:Y=>s[N]=Y,enumerable:!0})})}else e.exposed||(e.exposed={});S&&e.render===Ht&&(e.render=S),H!=null&&(e.inheritAttrs=H),P&&(e.components=P),R&&(e.directives=R),C&&cc(e)}function fb(e,t,s=Ht){be(e)&&(e=wo(e));for(const n in e){const a=e[n];let i;Xe(a)?"default"in a?i=Os(a.from||n,a.default,!0):i=Os(a.from||n):i=Os(a),St(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:l=>i.value=l}):t[n]=i}}function xd(e,t,s){xs(be(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function rp(e,t,s,n){let a=n.includes(".")?Gf(s,n):()=>s[n];if(Me(e)){const i=t[e];Ie(i)&&ns(a,i)}else if(Ie(e))ns(a,e.bind(s));else if(Xe(e))if(be(e))e.forEach(i=>rp(i,t,s,n));else{const i=Ie(e.handler)?e.handler.bind(s):t[e.handler];Ie(i)&&ns(a,i,e)}}function hc(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:a,optionsCache:i,config:{optionMergeStrategies:l}}=e.appContext,r=i.get(t);let o;return r?o=r:!a.length&&!s&&!n?o=t:(o={},a.length&&a.forEach(c=>Ul(o,c,l,!0)),Ul(o,t,l)),Xe(t)&&i.set(t,o),o}function Ul(e,t,s,n=!1){const{mixins:a,extends:i}=t;i&&Ul(e,i,s,!0),a&&a.forEach(l=>Ul(e,l,s,!0));for(const l in t)if(!(n&&l==="expose")){const r=pb[l]||s&&s[l];e[l]=r?r(e[l],t[l]):t[l]}return e}const pb={data:_d,props:kd,emits:kd,methods:bi,computed:bi,beforeCreate:Qt,created:Qt,beforeMount:Qt,mounted:Qt,beforeUpdate:Qt,updated:Qt,beforeDestroy:Qt,beforeUnmount:Qt,destroyed:Qt,unmounted:Qt,activated:Qt,deactivated:Qt,errorCaptured:Qt,serverPrefetch:Qt,components:bi,directives:bi,watch:mb,provide:_d,inject:hb};function _d(e,t){return t?e?function(){return ze(Ie(e)?e.call(this,this):e,Ie(t)?t.call(this,this):t)}:t:e}function hb(e,t){return bi(wo(e),wo(t))}function wo(e){if(be(e)){const t={};for(let s=0;s{let d,u=je,f;return qf(()=>{const p=e[a];Lt(d,p)&&(d=p,c())}),{get(){return o(),s.get?s.get(d):d},set(p){const b=s.set?s.set(p):p;if(!Lt(b,d)&&!(u!==je&&Lt(p,u)))return;const y=n.vnode.props,E=!!(y&&(t in y||a in y||i in y)&&(`onUpdate:${t}`in y||`onUpdate:${a}`in y||`onUpdate:${i}`in y));E||(d=p,c()),n.emit(`update:${t}`,b),Lt(p,u)&&(Lt(p,b)&&!Lt(b,f)||E&&u!==je&&!Lt(b,d))&&c(),u=p,f=b}}});return r[Symbol.iterator]=()=>{let o=0;return{next(){return o<2?{value:o++?l||je:r,done:!1}:{done:!0}}}},r}const cp=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${it(t)}Modifiers`]||e[`${ps(t)}Modifiers`];function yb(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||je;let a=s;const i=t.startsWith("update:"),l=i&&cp(n,t.slice(7));l&&(l.trim&&(a=s.map(d=>Me(d)?d.trim():d)),l.number&&(a=s.map(pr)));let r,o=n[r=Da(t)]||n[r=Da(it(t))];!o&&i&&(o=n[r=Da(ps(t))]),o&&xs(o,e,6,a);const c=n[r+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[r])return;e.emitted[r]=!0,xs(c,e,6,a)}}const xb=new WeakMap;function dp(e,t,s=!1){const n=s?xb:t.emitsCache,a=n.get(e);if(a!==void 0)return a;const i=e.emits;let l={},r=!1;if(!Ie(e)){const o=c=>{const d=dp(c,t,!0);d&&(r=!0,ze(l,d))};!s&&t.mixins.length&&t.mixins.forEach(o),e.extends&&o(e.extends),e.mixins&&e.mixins.forEach(o)}return!i&&!r?(Xe(e)&&n.set(e,null),null):(be(i)?i.forEach(o=>l[o]=null):ze(l,i),Xe(e)&&n.set(e,l),l)}function Tr(e,t){return!e||!ca(t)?!1:(t=t.slice(2).replace(/Once$/,""),tt(e,t[0].toLowerCase()+t.slice(1))||tt(e,ps(t))||tt(e,t))}function Cl(e){const{type:t,vnode:s,proxy:n,withProxy:a,propsOptions:[i],slots:l,attrs:r,emit:o,render:c,renderCache:d,props:u,data:f,setupState:p,ctx:b,inheritAttrs:y}=e,E=Fi(e);let I,x;try{if(s.shapeFlag&4){const _=a||n,S=_;I=fs(c.call(S,_,d,u,p,f,b)),x=r}else{const _=t;I=fs(_.length>1?_(u,{attrs:r,slots:l,emit:o}):_(u,null)),x=t.props?r:kb(r)}}catch(_){Ti.length=0,fa(_,e,1),I=ft(yt)}let m=I;if(x&&y!==!1){const _=Object.keys(x),{shapeFlag:S}=m;_.length&&S&7&&(i&&_.some(cr)&&(x=wb(x,i)),m=sn(m,x,!1,!0))}return s.dirs&&(m=sn(m,null,!1,!0),m.dirs=m.dirs?m.dirs.concat(s.dirs):s.dirs),s.transition&&Tn(m,s.transition),I=m,Fi(E),I}function _b(e,t=!0){let s;for(let n=0;n{let t;for(const s in e)(s==="class"||s==="style"||ca(s))&&((t||(t={}))[s]=e[s]);return t},wb=(e,t)=>{const s={};for(const n in e)(!cr(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function Sb(e,t,s){const{props:n,children:a,component:i}=e,{props:l,children:r,patchFlag:o}=t,c=i.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&o>=0){if(o&1024)return!0;if(o&16)return n?wd(n,l,c):!!l;if(o&8){const d=t.dynamicProps;for(let u=0;uObject.create(fp),hp=e=>Object.getPrototypeOf(e)===fp;function Tb(e,t,s,n=!1){const a={},i=pp();e.propsDefaults=Object.create(null),mp(e,t,a,i);for(const l in e.propsOptions[0])l in a||(a[l]=void 0);s?e.props=n?a:sc(a):e.type.props?e.props=a:e.props=i,e.attrs=i}function Cb(e,t,s,n){const{props:a,attrs:i,vnode:{patchFlag:l}}=e,r=Je(a),[o]=e.propsOptions;let c=!1;if((n||l>0)&&!(l&16)){if(l&8){const d=e.vnode.dynamicProps;for(let u=0;u{o=!0;const[f,p]=gp(u,t,!0);ze(l,f),p&&r.push(...p)};!s&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}if(!i&&!o)return Xe(e)&&n.set(e,Na),Na;if(be(i))for(let d=0;de==="_"||e==="_ctx"||e==="$stable",gc=e=>be(e)?e.map(fs):[fs(e)],Ab=(e,t,s)=>{if(t._n)return t;const n=lc((...a)=>gc(t(...a)),s);return n._c=!1,n},vp=(e,t,s)=>{const n=e._ctx;for(const a in e){if(mc(a))continue;const i=e[a];if(Ie(i))t[a]=Ab(a,i,n);else if(i!=null){const l=gc(i);t[a]=()=>l}}},bp=(e,t)=>{const s=gc(t);e.slots.default=()=>s},yp=(e,t,s)=>{for(const n in t)(s||!mc(n))&&(e[n]=t[n])},Rb=(e,t,s)=>{const n=e.slots=pp();if(e.vnode.shapeFlag&32){const a=t._;a?(yp(n,t,s),s&&uf(n,"_",a,!0)):vp(t,n)}else t&&bp(e,t)},Ib=(e,t,s)=>{const{vnode:n,slots:a}=e;let i=!0,l=je;if(n.shapeFlag&32){const r=t._;r?s&&r===1?i=!1:yp(a,t,s):(i=!t.$stable,vp(t,a)),l=t}else t&&(bp(e,t),l={default:1});if(i)for(const r in a)!mc(r)&&l[r]==null&&delete a[r]},kt=Ep;function xp(e){return kp(e)}function _p(e){return kp(e,Rv)}function kp(e,t){const s=hr();s.__VUE__=!0;const{insert:n,remove:a,patchProp:i,createElement:l,createText:r,createComment:o,setText:c,setElementText:d,parentNode:u,nextSibling:f,setScopeId:p=Ht,insertStaticContent:b}=e,y=(k,L,$,ee=null,Z=null,X=null,ue=void 0,oe=null,le=!!L.dynamicChildren)=>{if(k===L)return;k&&!Hs(k,L)&&(ee=V(k),ie(k,Z,X,!0),k=null),L.patchFlag===-2&&(le=!1,L.dynamicChildren=null);const{type:te,ref:ne,shapeFlag:fe}=L;switch(te){case $n:E(k,L,$,ee);break;case yt:I(k,L,$,ee);break;case sa:k==null&&x(L,$,ee,ue);break;case Dt:P(k,L,$,ee,Z,X,ue,oe,le);break;default:fe&1?S(k,L,$,ee,Z,X,ue,oe,le):fe&6?R(k,L,$,ee,Z,X,ue,oe,le):(fe&64||fe&128)&&te.process(k,L,$,ee,Z,X,ue,oe,le,ye)}ne!=null&&Z?Fa(ne,k&&k.ref,X,L||k,!L):ne==null&&k&&k.ref!=null&&Fa(k.ref,null,X,k,!0)},E=(k,L,$,ee)=>{if(k==null)n(L.el=r(L.children),$,ee);else{const Z=L.el=k.el;L.children!==k.children&&c(Z,L.children)}},I=(k,L,$,ee)=>{k==null?n(L.el=o(L.children||""),$,ee):L.el=k.el},x=(k,L,$,ee)=>{[k.el,k.anchor]=b(k.children,L,$,ee,k.el,k.anchor)},m=({el:k,anchor:L},$,ee)=>{let Z;for(;k&&k!==L;)Z=f(k),n(k,$,ee),k=Z;n(L,$,ee)},_=({el:k,anchor:L})=>{let $;for(;k&&k!==L;)$=f(k),a(k),k=$;a(L)},S=(k,L,$,ee,Z,X,ue,oe,le)=>{if(L.type==="svg"?ue="svg":L.type==="math"&&(ue="mathml"),k==null)g(L,$,ee,Z,X,ue,oe,le);else{const te=k.el&&k.el._isVueCE?k.el:null;try{te&&te._beginPatch(),C(k,L,Z,X,ue,oe,le)}finally{te&&te._endPatch()}}},g=(k,L,$,ee,Z,X,ue,oe)=>{let le,te;const{props:ne,shapeFlag:fe,transition:ve,dirs:Te}=k;if(le=k.el=l(k.type,X,ne&&ne.is,ne),fe&8?d(le,k.children):fe&16&&T(k.children,le,null,ee,Z,Kr(k,X),ue,oe),Te&&Qs(k,null,ee,"created"),w(le,k,k.scopeId,ue,ee),ne){for(const Le in ne)Le!=="value"&&!bn(Le)&&i(le,Le,null,ne[Le],X,ee);"value"in ne&&i(le,"value",null,ne.value,X),(te=ne.onVnodeBeforeMount)&&ds(te,ee,k)}Te&&Qs(k,null,ee,"beforeMount");const Oe=wp(Z,ve);Oe&&ve.beforeEnter(le),n(le,L,$),((te=ne&&ne.onVnodeMounted)||Oe||Te)&&kt(()=>{try{te&&ds(te,ee,k),Oe&&ve.enter(le),Te&&Qs(k,null,ee,"mounted")}finally{}},Z)},w=(k,L,$,ee,Z)=>{if($&&p(k,$),ee)for(let X=0;X{for(let te=le;te{const oe=L.el=k.el;let{patchFlag:le,dynamicChildren:te,dirs:ne}=L;le|=k.patchFlag&16;const fe=k.props||je,ve=L.props||je;let Te;if($&&qn($,!1),(Te=ve.onVnodeBeforeUpdate)&&ds(Te,$,L,k),ne&&Qs(L,k,$,"beforeUpdate"),$&&qn($,!0),(fe.innerHTML&&ve.innerHTML==null||fe.textContent&&ve.textContent==null)&&d(oe,""),te?M(k.dynamicChildren,te,oe,$,ee,Kr(L,Z),X):ue||N(k,L,oe,null,$,ee,Kr(L,Z),X,!1),le>0){if(le&16)H(oe,fe,ve,$,Z);else if(le&2&&fe.class!==ve.class&&i(oe,"class",null,ve.class,Z),le&4&&i(oe,"style",fe.style,ve.style,Z),le&8){const Oe=L.dynamicProps;for(let Le=0;Le{Te&&ds(Te,$,L,k),ne&&Qs(L,k,$,"updated")},ee)},M=(k,L,$,ee,Z,X,ue)=>{for(let oe=0;oe{if(L!==$){if(L!==je)for(const X in L)!bn(X)&&!(X in $)&&i(k,X,L[X],null,Z,ee);for(const X in $){if(bn(X))continue;const ue=$[X],oe=L[X];ue!==oe&&X!=="value"&&i(k,X,oe,ue,Z,ee)}"value"in $&&i(k,"value",L.value,$.value,Z)}},P=(k,L,$,ee,Z,X,ue,oe,le)=>{const te=L.el=k?k.el:r(""),ne=L.anchor=k?k.anchor:r("");let{patchFlag:fe,dynamicChildren:ve,slotScopeIds:Te}=L;Te&&(oe=oe?oe.concat(Te):Te),k==null?(n(te,$,ee),n(ne,$,ee),T(L.children||[],$,ne,Z,X,ue,oe,le)):fe>0&&fe&64&&ve&&k.dynamicChildren&&k.dynamicChildren.length===ve.length?(M(k.dynamicChildren,ve,$,Z,X,ue,oe),(L.key!=null||Z&&L===Z.subTree)&&vc(k,L,!0)):N(k,L,$,ne,Z,X,ue,oe,le)},R=(k,L,$,ee,Z,X,ue,oe,le)=>{L.slotScopeIds=oe,k==null?L.shapeFlag&512?Z.ctx.activate(L,$,ee,ue,le):j(L,$,ee,Z,X,ue,le):Q(k,L,le)},j=(k,L,$,ee,Z,X,ue)=>{const oe=k.component=Dp(k,ee,Z);if(tl(k)&&(oe.ctx.renderer=ye),Pp(oe,!1,ue),oe.asyncDep){if(Z&&Z.registerDep(oe,U,ue),!k.el){const le=oe.subTree=ft(yt);I(null,le,L,$),k.placeholder=le.el}}else U(oe,k,L,$,Z,X,ue)},Q=(k,L,$)=>{const ee=L.component=k.component;if(Sb(k,L,$))if(ee.asyncDep&&!ee.asyncResolved){O(ee,L,$);return}else ee.next=L,ee.update();else L.el=k.el,ee.vnode=L},U=(k,L,$,ee,Z,X,ue)=>{const oe=()=>{if(k.isMounted){let{next:fe,bu:ve,u:Te,parent:Oe,vnode:Le}=k;{const K=Sp(k);if(K){fe&&(fe.el=Le.el,O(k,fe,ue)),K.asyncDep.then(()=>{kt(()=>{k.isUnmounted||te()},Z)});return}}let De=fe,Be;qn(k,!1),fe?(fe.el=Le.el,O(k,fe,ue)):fe=Le,ve&&Ma(ve),(Be=fe.props&&fe.props.onVnodeBeforeUpdate)&&ds(Be,Oe,fe,Le),qn(k,!0);const qe=Cl(k),ct=k.subTree;k.subTree=qe,y(ct,qe,u(ct.el),V(ct),k,Z,X),fe.el=qe.el,De===null&&Cr(k,qe.el),Te&&kt(Te,Z),(Be=fe.props&&fe.props.onVnodeUpdated)&&kt(()=>ds(Be,Oe,fe,Le),Z)}else{let fe;const{el:ve,props:Te}=L,{bm:Oe,m:Le,parent:De,root:Be,type:qe}=k,ct=xn(L);if(qn(k,!1),Oe&&Ma(Oe),!ct&&(fe=Te&&Te.onVnodeBeforeMount)&&ds(fe,De,L),qn(k,!0),ve&&He){const K=()=>{k.subTree=Cl(k),He(ve,k.subTree,k,Z,null)};ct&&qe.__asyncHydrate?qe.__asyncHydrate(ve,k,K):K()}else{Be.ce&&Be.ce._hasShadowRoot()&&Be.ce._injectChildStyle(qe,k.parent?k.parent.type:void 0);const K=k.subTree=Cl(k);y(null,K,$,ee,k,Z,X),L.el=K.el}if(Le&&kt(Le,Z),!ct&&(fe=Te&&Te.onVnodeMounted)){const K=L;kt(()=>ds(fe,De,K),Z)}(L.shapeFlag&256||De&&xn(De.vnode)&&De.vnode.shapeFlag&256)&&k.a&&kt(k.a,Z),k.isMounted=!0,L=$=ee=null}};k.scope.on();const le=k.effect=new Ni(oe);k.scope.off();const te=k.update=le.run.bind(le),ne=k.job=le.runIfDirty.bind(le);ne.i=k,ne.id=k.uid,le.scheduler=()=>ic(ne),qn(k,!0),te()},O=(k,L,$)=>{L.component=k;const ee=k.vnode.props;k.vnode=L,k.next=null,Cb(k,L.props,ee,$),Ib(k,L.children,$),wn(),ud(k),Sn()},N=(k,L,$,ee,Z,X,ue,oe,le=!1)=>{const te=k&&k.children,ne=k?k.shapeFlag:0,fe=L.children,{patchFlag:ve,shapeFlag:Te}=L;if(ve>0){if(ve&128){we(te,fe,$,ee,Z,X,ue,oe,le);return}else if(ve&256){Y(te,fe,$,ee,Z,X,ue,oe,le);return}}Te&8?(ne&16&&Se(te,Z,X),fe!==te&&d($,fe)):ne&16?Te&16?we(te,fe,$,ee,Z,X,ue,oe,le):Se(te,Z,X,!0):(ne&8&&d($,""),Te&16&&T(fe,$,ee,Z,X,ue,oe,le))},Y=(k,L,$,ee,Z,X,ue,oe,le)=>{k=k||Na,L=L||Na;const te=k.length,ne=L.length,fe=Math.min(te,ne);let ve;for(ve=0;vene?Se(k,Z,X,!0,!1,fe):T(L,$,ee,Z,X,ue,oe,le,fe)},we=(k,L,$,ee,Z,X,ue,oe,le)=>{let te=0;const ne=L.length;let fe=k.length-1,ve=ne-1;for(;te<=fe&&te<=ve;){const Te=k[te],Oe=L[te]=le?un(L[te]):fs(L[te]);if(Hs(Te,Oe))y(Te,Oe,$,null,Z,X,ue,oe,le);else break;te++}for(;te<=fe&&te<=ve;){const Te=k[fe],Oe=L[ve]=le?un(L[ve]):fs(L[ve]);if(Hs(Te,Oe))y(Te,Oe,$,null,Z,X,ue,oe,le);else break;fe--,ve--}if(te>fe){if(te<=ve){const Te=ve+1,Oe=Teve)for(;te<=fe;)ie(k[te],Z,X,!0),te++;else{const Te=te,Oe=te,Le=new Map;for(te=Oe;te<=ve;te++){const Re=L[te]=le?un(L[te]):fs(L[te]);Re.key!=null&&Le.set(Re.key,te)}let De,Be=0;const qe=ve-Oe+1;let ct=!1,K=0;const xe=new Array(qe);for(te=0;te=qe){ie(Re,Z,X,!0);continue}let Ve;if(Re.key!=null)Ve=Le.get(Re.key);else for(De=Oe;De<=ve;De++)if(xe[De-Oe]===0&&Hs(Re,L[De])){Ve=De;break}Ve===void 0?ie(Re,Z,X,!0):(xe[Ve-Oe]=te+1,Ve>=K?K=Ve:ct=!0,y(Re,L[Ve],$,null,Z,X,ue,oe,le),Be++)}const Ce=ct?Ob(xe):Na;for(De=Ce.length-1,te=qe-1;te>=0;te--){const Re=Oe+te,Ve=L[Re],Pe=L[Re+1],pt=Re+1{const{el:X,type:ue,transition:oe,children:le,shapeFlag:te}=k;if(te&6){ke(k.component.subTree,L,$,ee);return}if(te&128){k.suspense.move(L,$,ee);return}if(te&64){ue.move(k,L,$,ye);return}if(ue===Dt){n(X,L,$);for(let fe=0;feoe.enter(X),Z));else{const{leave:fe,delayLeave:ve,afterLeave:Te}=oe,Oe=()=>{k.ctx.isUnmounted?a(X):n(X,L,$)},Le=()=>{const De=X._isLeaving||!!X[As];X._isLeaving&&X[As](!0),oe.persisted&&!De?Oe():fe(X,()=>{Oe(),Te&&Te()})};ve?ve(X,Oe,Le):Le()}else n(X,L,$)},ie=(k,L,$,ee=!1,Z=!1)=>{const{type:X,props:ue,ref:oe,children:le,dynamicChildren:te,shapeFlag:ne,patchFlag:fe,dirs:ve,cacheIndex:Te,memo:Oe}=k;if(fe===-2&&(Z=!1),oe!=null&&(wn(),Fa(oe,null,$,k,!0),Sn()),Te!=null&&(L.renderCache[Te]=void 0),ne&256){L.ctx.deactivate(k);return}const Le=ne&1&&ve,De=!xn(k);let Be;if(De&&(Be=ue&&ue.onVnodeBeforeUnmount)&&ds(Be,L,k),ne&6)se(k.component,$,ee);else{if(ne&128){k.suspense.unmount($,ee);return}Le&&Qs(k,null,L,"beforeUnmount"),ne&64?k.type.remove(k,L,$,ye,ee):te&&!te.hasOnce&&(X!==Dt||fe>0&&fe&64)?Se(te,L,$,!1,!0):(X===Dt&&fe&384||!Z&&ne&16)&&Se(le,L,$),ee&&he(k)}const qe=Oe!=null&&Te==null;(De&&(Be=ue&&ue.onVnodeUnmounted)||Le||qe)&&kt(()=>{Be&&ds(Be,L,k),Le&&Qs(k,null,L,"unmounted"),qe&&(k.el=null)},$)},he=k=>{const{type:L,el:$,anchor:ee,transition:Z}=k;if(L===Dt){F($,ee);return}if(L===sa){_(k);return}const X=()=>{a($),Z&&!Z.persisted&&Z.afterLeave&&Z.afterLeave()};if(k.shapeFlag&1&&Z&&!Z.persisted){const{leave:ue,delayLeave:oe}=Z,le=()=>ue($,X);oe?oe(k.el,X,le):le()}else X()},F=(k,L)=>{let $;for(;k!==L;)$=f(k),a(k),k=$;a(L)},se=(k,L,$)=>{const{bum:ee,scope:Z,job:X,subTree:ue,um:oe,m:le,a:te}=k;Hl(le),Hl(te),ee&&Ma(ee),Z.stop(),X&&(X.flags|=8,ie(ue,k,L,$)),oe&&kt(oe,L),kt(()=>{k.isUnmounted=!0},L)},Se=(k,L,$,ee=!1,Z=!1,X=0)=>{for(let ue=X;ue{if(k.shapeFlag&6)return V(k.component.subTree);if(k.shapeFlag&128)return k.suspense.next();const L=f(k.anchor||k.el),$=L&&L[Kf];return $?f($):L};let de=!1;const ce=(k,L,$)=>{let ee;k==null?L._vnode&&(ie(L._vnode,null,null,!0),ee=L._vnode.component):y(L._vnode||null,k,L,null,null,null,$),L._vnode=k,de||(de=!0,ud(ee),$l(),de=!1)},ye={p:y,um:ie,m:ke,r:he,mt:j,mc:T,pc:N,pbc:M,n:V,o:e};let ge,He;return t&&([ge,He]=t(ye)),{render:ce,hydrate:ge,createApp:vb(ce,ge)}}function Kr({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 qn({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function wp(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function vc(e,t,s=!1){const n=e.children,a=t.children;if(be(n)&&be(a))for(let i=0;i>1,e[s[r]]0&&(t[n]=s[i-1]),s[i]=n)}}for(i=s.length,l=s[i-1];i-- >0;)s[i]=l,l=t[l];return s}function Sp(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Sp(t)}function Hl(e){if(e)for(let t=0;te.__isSuspense;let To=0;const Nb={name:"Suspense",__isSuspense:!0,process(e,t,s,n,a,i,l,r,o,c){if(e==null)Db(t,s,n,a,i,l,r,o,c);else{if(i&&i.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}Mb(e,t,s,n,a,l,r,o,c)}},hydrate:Pb,normalize:Fb},Lb=Nb;function Bi(e,t){const s=e.props&&e.props[t];Ie(s)&&s()}function Db(e,t,s,n,a,i,l,r,o){const{p:c,o:{createElement:d}}=o,u=d("div"),f=e.suspense=Cp(e,a,n,t,u,s,i,l,r,o);c(null,f.pendingBranch=e.ssContent,u,null,n,f,i,l),f.deps>0?(Bi(e,"onPending"),Bi(e,"onFallback"),c(null,e.ssFallback,t,s,n,null,i,l),$a(f,e.ssFallback)):f.resolve(!1,!0)}function Mb(e,t,s,n,a,i,l,r,{p:o,um:c,o:{createElement:d}}){const u=t.suspense=e.suspense;u.vnode=t,t.el=e.el;const f=t.ssContent,p=t.ssFallback,{activeBranch:b,pendingBranch:y,isInFallback:E,isHydrating:I}=u;if(y)u.pendingBranch=f,Hs(y,f)?(o(y,f,u.hiddenContainer,null,a,u,i,l,r),u.deps<=0?u.resolve():E&&(I||(o(b,p,s,n,a,null,i,l,r),$a(u,p)))):(u.pendingId=To++,I?(u.isHydrating=!1,u.activeBranch=y):c(y,a,u),u.deps=0,u.effects.length=0,u.hiddenContainer=d("div"),E?(o(null,f,u.hiddenContainer,null,a,u,i,l,r),u.deps<=0?u.resolve():(o(b,p,s,n,a,null,i,l,r),$a(u,p))):b&&Hs(b,f)?(o(b,f,s,n,a,u,i,l,r),u.resolve(!0)):(o(null,f,u.hiddenContainer,null,a,u,i,l,r),u.deps<=0&&u.resolve()));else if(b&&Hs(b,f))o(b,f,s,n,a,u,i,l,r),$a(u,f);else if(Bi(t,"onPending"),u.pendingBranch=f,f.shapeFlag&512?u.pendingId=f.component.suspenseId:u.pendingId=To++,o(null,f,u.hiddenContainer,null,a,u,i,l,r),u.deps<=0)u.resolve();else{const{timeout:x,pendingId:m}=u;x>0?setTimeout(()=>{u.pendingId===m&&u.fallback(p)},x):x===0&&u.fallback(p)}}function Cp(e,t,s,n,a,i,l,r,o,c,d=!1){const{p:u,m:f,um:p,n:b,o:{parentNode:y,remove:E}}=c;let I;const x=$b(e);x&&t&&t.pendingBranch&&(I=t.pendingId,t.deps++);const m=e.props?Ll(e.props.timeout):void 0,_=i,S={vnode:e,parent:t,parentComponent:s,namespace:l,container:n,hiddenContainer:a,deps:0,pendingId:To++,timeout:typeof m=="number"?m:-1,activeBranch:null,isFallbackMountPending:!1,pendingBranch:null,isInFallback:!d,isHydrating:d,isUnmounted:!1,effects:[],resolve(g=!1,w=!1){const{vnode:T,activeBranch:C,pendingBranch:M,pendingId:H,effects:P,parentComponent:R,container:j,isInFallback:Q}=S;let U=!1;if(S.isHydrating)S.isHydrating=!1;else if(!g){U=C&&M.transition&&M.transition.mode==="out-in";let Y=!1;U&&(C.transition.afterLeave=()=>{H===S.pendingId&&(f(M,j,i===_&&!Y?b(C):i,0),Mi(P),Q&&T.ssFallback&&(T.ssFallback.el=null))}),C&&!S.isFallbackMountPending&&(y(C.el)===j&&(i=b(C),Y=!0),p(C,R,S,!0),!U&&Q&&T.ssFallback&&kt(()=>T.ssFallback.el=null,S)),U||f(M,j,i,0)}S.isFallbackMountPending=!1,$a(S,M),S.pendingBranch=null,S.isInFallback=!1;let O=S.parent,N=!1;for(;O;){if(O.pendingBranch){O.effects.push(...P),N=!0;break}O=O.parent}!N&&!U&&Mi(P),S.effects=[],x&&t&&t.pendingBranch&&I===t.pendingId&&(t.deps--,t.deps===0&&!w&&t.resolve()),Bi(T,"onResolve")},fallback(g){if(!S.pendingBranch)return;const{vnode:w,activeBranch:T,parentComponent:C,container:M,namespace:H}=S;Bi(w,"onFallback");const P=b(T),R=()=>{S.isFallbackMountPending=!1,S.isInFallback&&(u(null,g,M,P,C,null,H,r,o),$a(S,g))},j=g.transition&&g.transition.mode==="out-in";j&&(S.isFallbackMountPending=!0,T.transition.afterLeave=R),S.isInFallback=!0,p(T,C,null,!0),j||R()},move(g,w,T){S.activeBranch&&f(S.activeBranch,g,w,T),S.container=g},next(){return S.activeBranch&&b(S.activeBranch)},registerDep(g,w,T){const C=!!S.pendingBranch;C&&S.deps++;const M=g.vnode.el;g.asyncDep.catch(H=>{fa(H,g,0)}).then(H=>{if(g.isUnmounted||S.isUnmounted||S.pendingId!==g.suspenseId)return;zi(),g.asyncResolved=!0;const{vnode:P}=g;Co(g,H,!1),M&&(P.el=M);const R=!M&&g.subTree.el;w(g,P,y(M||g.subTree.el),M?null:b(g.subTree),S,l,T),R&&(P.placeholder=null,E(R)),Cr(g,P.el),C&&--S.deps===0&&S.resolve()})},unmount(g,w){S.isUnmounted=!0,S.activeBranch&&p(S.activeBranch,s,g,w),S.pendingBranch&&p(S.pendingBranch,s,g,w)}};return S}function Pb(e,t,s,n,a,i,l,r,o){const c=t.suspense=Cp(t,n,s,e.parentNode,document.createElement("div"),null,a,i,l,r,!0),d=o(e,c.pendingBranch=t.ssContent,s,c,i,l);return c.deps===0&&c.resolve(!1,!0),d}function Fb(e){const{shapeFlag:t,children:s}=e,n=t&32;e.ssContent=Td(n?s.default:s),e.ssFallback=n?Td(s.fallback):ft(yt)}function Td(e){let t;if(Ie(e)){const s=ia&&e._c;s&&(e._d=!1,Ui()),e=e(),s&&(e._d=!0,t=Wt,Ap())}return be(e)&&(e=_b(e)),e=fs(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter(s=>s!==e)),e}function Ep(e,t){t&&t.pendingBranch?be(e)?t.effects.push(...e):t.effects.push(e):Mi(e)}function $a(e,t){e.activeBranch=t;const{vnode:s,parentComponent:n}=e;let a=t.el;for(;!a&&t.component;)t=t.component.subTree,a=t.el;s.el=a,n&&n.subTree===s&&(n.vnode.el=a,Cr(n,a))}function $b(e){const t=e.props&&e.props.suspensible;return t!=null&&t!==!1}const Dt=Symbol.for("v-fgt"),$n=Symbol.for("v-txt"),yt=Symbol.for("v-cmt"),sa=Symbol.for("v-stc"),Ti=[];let Wt=null;function Ui(e=!1){Ti.push(Wt=e?null:[])}function Ap(){Ti.pop(),Wt=Ti[Ti.length-1]||null}let ia=1;function Hi(e,t=!1){ia+=e,e<0&&Wt&&t&&(Wt.hasOnce=!0)}function Rp(e){return e.dynamicChildren=ia>0?Wt||Na:null,Ap(),ia>0&&Wt&&Wt.push(e),e}function Bb(e,t,s,n,a,i){return Rp(bc(e,t,s,n,a,i,!0))}function Vl(e,t,s,n,a){return Rp(ft(e,t,s,n,a,!0))}function Cn(e){return e?e.__v_isVNode===!0:!1}function Hs(e,t){return e.type===t.type&&e.key===t.key}function Ub(e){}const Ip=({key:e})=>e??null,El=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?Me(e)||St(e)||Ie(e)?{i:Ut,r:e,k:t,f:!!s}:e:null);function bc(e,t=null,s=null,n=0,a=null,i=e===Dt?0:1,l=!1,r=!1){const o={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Ip(t),ref:t&&El(t),scopeId:xr,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:n,dynamicProps:a,dynamicChildren:null,appContext:null,ctx:Ut};return r?(xc(o,s),i&128&&e.normalize(o)):s&&(o.shapeFlag|=Me(s)?8:16),ia>0&&!l&&Wt&&(o.patchFlag>0||i&6)&&o.patchFlag!==32&&Wt.push(o),o}const ft=Hb;function Hb(e,t=null,s=null,n=0,a=null,i=!1){if((!e||e===ip)&&(e=yt),Cn(e)){const r=sn(e,t,!0);return s&&xc(r,s),ia>0&&!i&&Wt&&(r.shapeFlag&6?Wt[Wt.indexOf(e)]=r:Wt.push(r)),r.patchFlag=-2,r}if(Wb(e)&&(e=e.__vccOpts),t){t=Op(t);let{class:r,style:o}=t;r&&!Me(r)&&(t.class=Yi(r)),Xe(o)&&(Qi(o)&&!be(o)&&(o=ze({},o)),t.style=Ji(o))}const l=Me(e)?1:zl(e)?128:Wf(e)?64:Xe(e)?4:Ie(e)?2:0;return bc(e,t,s,n,a,l,i,!0)}function Op(e){return e?Qi(e)||hp(e)?ze({},e):e:null}function sn(e,t,s=!1,n=!1){const{props:a,ref:i,patchFlag:l,children:r,transition:o}=e,c=t?Lp(a||{},t):a,d={__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&Ip(c),ref:t&&t.ref?s&&i?be(i)?i.concat(El(t)):[i,El(t)]:El(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:r,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Dt?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:o,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&sn(e.ssContent),ssFallback:e.ssFallback&&sn(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return o&&n&&Tn(d,o.clone(d)),d}function yc(e=" ",t=0){return ft($n,null,e,t)}function zb(e,t){const s=ft(sa,null,e);return s.staticCount=t,s}function Np(e="",t=!1){return t?(Ui(),Vl(yt,null,e)):ft(yt,null,e)}function fs(e){return e==null||typeof e=="boolean"?ft(yt):be(e)?ft(Dt,null,e.slice()):Cn(e)?un(e):ft($n,null,String(e))}function un(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:sn(e)}function xc(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(be(t))s=16;else if(typeof t=="object")if(n&65){const a=t.default;a&&(a._c&&(a._d=!1),xc(e,a()),a._c&&(a._d=!0));return}else{s=32;const a=t._;!a&&!hp(t)?t._ctx=Ut:a===3&&Ut&&(Ut.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else Ie(t)?(t={default:t,_ctx:Ut},s=32):(t=String(t),n&64?(s=16,t=[yc(t)]):s=8);e.children=t,e.shapeFlag|=s}function Lp(...e){const t={};for(let s=0;sBt||Ut;let jl,Ba;{const e=hr(),t=(s,n)=>{let a;return(a=e[s])||(a=e[s]=[]),a.push(n),i=>{a.length>1?a.forEach(l=>l(i)):a[0](i)}};jl=t("__VUE_INSTANCE_SETTERS__",s=>Bt=s),Ba=t("__VUE_SSR_SETTERS__",s=>la=s)}const si=e=>{const t=Bt;return jl(e),e.scope.on(),()=>{e.scope.off(),jl(t)}},zi=()=>{Bt&&Bt.scope.off(),jl(null)};function Mp(e){return e.vnode.shapeFlag&4}let la=!1;function Pp(e,t=!1,s=!1){t&&Ba(t);const{props:n,children:a}=e.vnode,i=Mp(e);Tb(e,n,i,t),Rb(e,a,s||t);const l=i?qb(e,t):void 0;return t&&Ba(!1),l}function qb(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,_o);const{setup:n}=s;if(n){wn();const a=e.setupContext=n.length>1?Bp(e):null,i=si(e),l=ti(n,e,0,[e.props,a]),r=Yo(l);if(Sn(),i(),(r||e.sp)&&!xn(e)&&cc(e),r){if(l.then(zi,zi),t)return l.then(o=>{Co(e,o,t)}).catch(o=>{fa(o,e,0)});e.asyncDep=l}else Co(e,l,t)}else $p(e,t)}function Co(e,t,s){Ie(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Xe(t)&&(e.setupState=ac(t)),$p(e,s)}let ql,Eo;function Fp(e){ql=e,Eo=t=>{t.render._rc&&(t.withProxy=new Proxy(t.ctx,Yv))}}const Gb=()=>!ql;function $p(e,t,s){const n=e.type;if(!e.render){if(!t&&ql&&!n.render){const a=n.template||hc(e).template;if(a){const{isCustomElement:i,compilerOptions:l}=e.appContext.config,{delimiters:r,compilerOptions:o}=n,c=ze(ze({isCustomElement:i,delimiters:r},l),o);n.render=ql(a,c)}}e.render=n.render||Ht,Eo&&Eo(e)}{const a=si(e);wn();try{ub(e)}finally{Sn(),a()}}}const Kb={get(e,t){return Kt(e,"get",""),e[t]}};function Bp(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,Kb),slots:e.slots,emit:e.emit,expose:t}}function sl(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(ac(Lf(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in Si)return Si[s](e)},has(t,s){return s in t||s in Si}})):e.proxy}function Ao(e,t=!0){return Ie(e)?e.displayName||e.name:e.name||t&&e.__name}function Wb(e){return Ie(e)&&"__vccOpts"in e}const J=(e,t)=>ev(e,t,la);function ja(e,t,s){try{Hi(-1);const n=arguments.length;return n===2?Xe(t)&&!be(t)?Cn(t)?ft(e,null,[t]):ft(e,t):ft(e,null,t):(n>3?s=Array.prototype.slice.call(arguments,2):n===3&&Cn(s)&&(s=[s]),ft(e,t,s))}finally{Hi(1)}}function Zb(){}function Jb(e,t,s,n){const a=s[n];if(a&&Up(a,e))return a;const i=t();return i.memo=e.slice(),i.cacheIndex=n,s[n]=i}function Up(e,t){const s=e.memo;if(s.length!=t.length)return!1;for(let n=0;n0&&Wt&&Wt.push(e),!0}const Hp="3.5.38",Yb=Ht,Qb=cv,Xb=Ea,ey=zf,ty={createComponentInstance:Dp,setupComponent:Pp,renderComponentRoot:Cl,setCurrentRenderingInstance:Fi,isVNode:Cn,normalizeVNode:fs,getComponentPublicInstance:sl,ensureValidVNode:pc,pushWarningContext:iv,popWarningContext:lv},sy=ty,ny=null,ay=null,iy=null;/** * @vue/runtime-dom v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/let Ro;const Cd=typeof window<"u"&&window.trustedTypes;if(Cd)try{Ro=Cd.createPolicy("vue",{createHTML:e=>e})}catch{}const Vp=Ro?e=>Ro.createHTML(e):e=>e,ly="http://www.w3.org/2000/svg",ry="http://www.w3.org/1998/Math/MathML",on=typeof document<"u"?document:null,Ed=on&&on.createElement("template"),jp={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const a=t==="svg"?on.createElementNS(ly,e):t==="mathml"?on.createElementNS(ry,e):s?on.createElement(e,{is:s}):on.createElement(e);return e==="select"&&n&&n.multiple!=null&&a.setAttribute("multiple",n.multiple),a},createText:e=>on.createTextNode(e),createComment:e=>on.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>on.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,a,i){const l=s?s.previousSibling:t.lastChild;if(a&&(a===i||a.nextSibling))for(;t.insertBefore(a.cloneNode(!0),s),!(a===i||!(a=a.nextSibling)););else{Ed.innerHTML=Vp(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const r=Ed.content;if(n==="svg"||n==="mathml"){const o=r.firstChild;for(;o.firstChild;)r.appendChild(o.firstChild);r.removeChild(o)}t.insertBefore(r,s)}return[l?l.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},En="transition",ni="animation",Ba=Symbol("_vtc"),zp={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},qp=je({},oc,zp),oy=e=>(e.displayName="Transition",e.props=qp,e),cy=oy((e,{slots:t})=>Ua(Yf,Gp(e),t)),zn=(e,t=[])=>{ge(e)?e.forEach(s=>s(...t)):e&&e(...t)},Ad=e=>e?ge(e)?e.some(t=>t.length>1):e.length>1:!1;function Gp(e){const t={};for(const $ in e)$ in zp||(t[$]=e[$]);if(e.css===!1)return t;const{name:s="v",type:n,duration:a,enterFromClass:i=`${s}-enter-from`,enterActiveClass:l=`${s}-enter-active`,enterToClass:r=`${s}-enter-to`,appearFromClass:o=i,appearActiveClass:c=l,appearToClass:d=r,leaveFromClass:u=`${s}-leave-from`,leaveActiveClass:f=`${s}-leave-active`,leaveToClass:p=`${s}-leave-to`}=e,b=dy(a),y=b&&b[0],A=b&&b[1],{onBeforeEnter:O,onEnter:x,onEnterCancelled:m,onLeave:_,onLeaveCancelled:S,onBeforeAppear:g=O,onAppear:w=x,onAppearCancelled:T=m}=t,C=($,I,j,Y)=>{$._enterCancelled=Y,On($,I?d:r),On($,I?c:l),j&&j()},M=($,I)=>{$._isLeaving=!1,On($,u),On($,p),On($,f),I&&I()},B=$=>(I,j)=>{const Y=$?w:x,H=()=>C(I,$,j);zn(Y,[I,H]),Rd(()=>{On(I,$?o:i),js(I,$?d:r),Ad(Y)||Id(I,n,y,H)})};return je(t,{onBeforeEnter($){zn(O,[$]),js($,i),js($,l)},onBeforeAppear($){zn(g,[$]),js($,o),js($,c)},onEnter:B(!1),onAppear:B(!0),onLeave($,I){$._isLeaving=!0;const j=()=>M($,I);js($,u),$._enterCancelled?(js($,f),Io($)):(Io($),js($,f)),Rd(()=>{$._isLeaving&&(On($,u),js($,p),Ad(_)||Id($,n,A,j))}),zn(_,[$,j])},onEnterCancelled($){C($,!1,void 0,!0),zn(m,[$])},onAppearCancelled($){C($,!0,void 0,!0),zn(T,[$])},onLeaveCancelled($){M($),zn(S,[$])}})}function dy(e){if(e==null)return null;if(Qe(e))return[Wr(e.enter),Wr(e.leave)];{const t=Wr(e);return[t,t]}}function Wr(e){return Nl(e)}function js(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.add(s)),(e[Ba]||(e[Ba]=new Set)).add(t)}function On(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.remove(n));const s=e[Ba];s&&(s.delete(t),s.size||(e[Ba]=void 0))}function Rd(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let uy=0;function Id(e,t,s,n){const a=e._endId=++uy,i=()=>{a===e._endId&&n()};if(s!=null)return setTimeout(i,s);const{type:l,timeout:r,propCount:o}=Kp(e,t);if(!l)return n();const c=l+"end";let d=0;const u=()=>{e.removeEventListener(c,f),i()},f=p=>{p.target===e&&++d>=o&&u()};setTimeout(()=>{d(s[b]||"").split(", "),a=n(`${En}Delay`),i=n(`${En}Duration`),l=Od(a,i),r=n(`${ni}Delay`),o=n(`${ni}Duration`),c=Od(r,o);let d=null,u=0,f=0;t===En?l>0&&(d=En,u=l,f=i.length):t===ni?c>0&&(d=ni,u=c,f=o.length):(u=Math.max(l,c),d=u>0?l>c?En:ni:null,f=d?d===En?i.length:o.length:0);const p=d===En&&/\b(?:transform|all)(?:,|$)/.test(n(`${En}Property`).toString());return{type:d,timeout:u,propCount:f,hasTransform:p}}function Od(e,t){for(;e.lengthNd(s)+Nd(e[n])))}function Nd(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Io(e){return(e?e.ownerDocument:document).body.offsetHeight}function fy(e,t,s){const n=e[Ba];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const ql=Symbol("_vod"),_c=Symbol("_vsh"),Wp={name:"show",beforeMount(e,{value:t},{transition:s}){e[ql]=e.style.display==="none"?"":e.style.display,s&&t?s.beforeEnter(e):ai(e,t)},mounted(e,{value:t},{transition:s}){s&&t&&s.enter(e)},updated(e,{value:t,oldValue:s},{transition:n}){!t!=!s&&(n?t?(n.beforeEnter(e),ai(e,!0),n.enter(e)):n.leave(e,()=>{ai(e,!1)}):ai(e,t))},beforeUnmount(e,{value:t}){ai(e,t)}};function ai(e,t){e.style.display=t?e[ql]:"none",e[_c]=!t}function py(){Wp.getSSRProps=({value:e})=>{if(!e)return{style:{display:"none"}}}}const Zp=Symbol("");function hy(e){const t=ts();if(!t)return;const s=t.ut=(a=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(i=>Gl(i,a))},n=()=>{const a=e(t.proxy);t.ce?Gl(t.ce,a):Oo(t.subTree,a),s(a)};dc(()=>{Ri(n)}),We(()=>{es(n,Ft,{flush:"post"});const a=new MutationObserver(n);a.observe(t.subTree.el.parentNode,{childList:!0}),xt(()=>a.disconnect())})}function Oo(e,t){if(e.shapeFlag&128){const s=e.suspense;e=s.activeBranch,s.pendingBranch&&!s.isHydrating&&s.effects.push(()=>{Oo(s.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)Gl(e.el,t);else if(e.type===Ot)e.children.forEach(s=>Oo(s,t));else if(e.type===ea){let{el:s,anchor:n}=e;for(;s&&(Gl(s,t),s!==n);)s=s.nextSibling}}function Gl(e,t){if(e.nodeType===1){const s=e.style;let n="";for(const a in t){const i=xg(t[a]);s.setProperty(`--${a}`,i),n+=`--${a}: ${i};`}s[Zp]=n}}const my=/(?:^|;)\s*display\s*:/;function gy(e,t,s){const n=e.style,a=Me(s);let i=!1;if(s&&!a){if(t)if(Me(t))for(const l of t.split(";")){const r=l.slice(0,l.indexOf(":")).trim();s[r]==null&&pi(n,r,"")}else for(const l in t)s[l]==null&&pi(n,l,"");for(const l in s){l==="display"&&(i=!0);const r=s[l];r!=null?by(e,l,!Me(t)&&t?t[l]:void 0,r)||pi(n,l,r):pi(n,l,"")}}else if(a){if(t!==s){const l=n[Zp];l&&(s+=";"+l),n.cssText=s,i=my.test(s)}}else t&&e.removeAttribute("style");ql in e&&(e[ql]=i?n.display:"",e[_c]&&(n.display="none"))}const Ld=/\s*!important$/;function pi(e,t,s){if(ge(s))s.forEach(n=>pi(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=vy(e,t);Ld.test(s)?e.setProperty(os(n),s.replace(Ld,""),"important"):e[n]=s}}const Dd=["Webkit","Moz","ms"],Zr={};function vy(e,t){const s=Zr[t];if(s)return s;let n=at(t);if(n!=="filter"&&n in e)return Zr[t]=n;n=ca(n);for(let a=0;aJr||(ky.then(()=>Jr=0),Jr=Date.now());function Sy(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;const a=s.value;if(ge(a)){const i=n.stopImmediatePropagation;n.stopImmediatePropagation=()=>{i.call(n),n._stopped=!0};const l=a.slice(),r=[n];for(let o=0;oe.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Jp=(e,t,s,n,a,i)=>{const l=a==="svg";t==="class"?fy(e,n,l):t==="style"?gy(e,s,n):ra(t)?or(t)||xy(e,t,s,n,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Ty(e,t,n,l))?(Fd(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Pd(e,t,n,l,i,t!=="value")):e._isVueCE&&(Cy(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!Me(n)))?Fd(e,at(t),n,i,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Pd(e,t,n,l))};function Ty(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&Bd(t)&&Ie(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 a=e.tagName;if(a==="IMG"||a==="VIDEO"||a==="CANVAS"||a==="SOURCE")return!1}return Bd(t)&&Me(s)?!1:t in e}function Cy(e,t){const s=e._def.props;if(!s)return!1;const n=at(t);return Array.isArray(s)?s.some(a=>at(a)===n):Object.keys(s).some(a=>at(a)===n)}const Hd={};function Yp(e,t,s){let n=Wi(e,t);cr(n)&&(n=je({},n,t));class a extends Cr{constructor(l){super(n,l,s)}}return a.def=n,a}const Ey=((e,t)=>Yp(e,t,dh)),Ay=typeof HTMLElement<"u"?HTMLElement:class{};class Cr extends Ay{constructor(t,s={},n=Zl){super(),this._def=t,this._props=s,this._createApp=n,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&&n!==Zl?this._root=this.shadowRoot:t.shadowRoot!==!1?(this.attachShadow(je({},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 Cr){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,At(()=>{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 n=0;n{this._resolved=!0,this._pendingResolve=void 0;const{props:i,styles:l}=n;let r;if(i&&!ge(i))for(const o in i){const c=i[o];(c===Number||c&&c.type===Number)&&(o in this._props&&(this._props[o]=Nl(this._props[o])),(r||(r=Object.create(null)))[at(o)]=!0)}this._numberProps=r,this._resolveProps(n),this.shadowRoot&&this._applyStyles(l),this._mount(n)},s=this._def.__asyncLoader;s?this._pendingResolve=s().then(n=>{n.configureApp=this._def.configureApp,t(this._def=n,!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 n in s)et(this,n)||Object.defineProperty(this,n,{get:()=>Zs(s[n])})}_resolveProps(t){const{props:s}=t,n=ge(s)?s:Object.keys(s||{});for(const a of Object.keys(this))a[0]!=="_"&&n.includes(a)&&this._setProp(a,this[a]);for(const a of n.map(at))Object.defineProperty(this,a,{get(){return this._getProp(a)},set(i){this._setProp(a,i,!0,!this._patching)}})}_setAttr(t){if(t.startsWith("data-v-"))return;const s=this.hasAttribute(t);let n=s?this.getAttribute(t):Hd;const a=at(t);s&&this._numberProps&&this._numberProps[a]&&(n=Nl(n)),this._setProp(a,n,!1,!0)}_getProp(t){return this._props[t]}_setProp(t,s,n=!0,a=!1){if(s!==this._props[t]&&(this._dirty=!0,s===Hd?delete this._props[t]:(this._props[t]=s,t==="key"&&this._app&&(this._app._ceVNode.key=s)),a&&this._instance&&this._update(),n)){const i=this._ob;i&&(this._processMutations(i.takeRecords()),i.disconnect()),s===!0?this.setAttribute(os(t),""):typeof s=="string"||typeof s=="number"?this.setAttribute(os(t),s+""):s||this.removeAttribute(os(t)),i&&i.observe(this,{attributes:!0})}}_update(){const t=this._createVNode();this._app&&(t.appContext=this._app._context),ch(t,this._root)}_createVNode(){const t={};this.shadowRoot||(t.onVnodeMounted=t.onVnodeUpdated=this._renderSlots.bind(this));const s=ft(this._def,je(t,this._props));return this._instance||(s.ce=n=>{this._instance=n,n.ce=this,n.isCE=!0;const a=(i,l)=>{this.dispatchEvent(new CustomEvent(i,cr(l[0])?je({detail:l},l[0]):{detail:l}))};n.emit=(i,...l)=>{a(i,l),os(i)!==i&&a(os(i),l)},this._setParent()}),s}_applyStyles(t,s,n){if(!t)return;if(s){if(s===this._def||this._styleChildren.has(s))return;this._styleChildren.add(s)}const a=this._nonce,i=this.shadowRoot,l=n?this._getStyleAnchor(n)||this._getStyleAnchor(this._def):this._getRootStyleInsertionAnchor(i);let r=null;for(let o=t.length-1;o>=0;o--){const c=document.createElement("style");a&&c.setAttribute("nonce",a),c.textContent=t[o],i.insertBefore(c,r||l),r=c,o===0&&(n||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),Ny=Oy({name:"TransitionGroup",props:je({},qp,{tag:String,moveClass:String}),setup(e,{slots:t}){const s=ts(),n=rc();let a,i;return kr(()=>{if(!a.length)return;const l=e.moveClass||`${e.name||"v"}-move`;if(!Fy(a[0].el,s.vnode.el,l)){a=[];return}a.forEach(Dy),a.forEach(My);const r=a.filter(Py);Io(s.vnode.el),r.forEach(o=>{const c=o.el,d=c.style;js(c,l),d.transform=d.webkitTransform=d.transitionDuration="";const u=c[Kl]=f=>{f&&f.target!==c||(!f||f.propertyName.endsWith("transform"))&&(c.removeEventListener("transitionend",u),c[Kl]=null,On(c,l))};c.addEventListener("transitionend",u)}),a=[]}),()=>{const l=Ze(e),r=Gp(l);let o=l.tag||Ot;if(a=[],i)for(let c=0;c{r.split(/\s+/).forEach(o=>o&&n.classList.remove(o))}),s.split(/\s+/).forEach(r=>r&&n.classList.add(r)),n.style.display="none";const i=t.nodeType===1?t:t.parentNode;i.appendChild(n);const{hasTransform:l}=Kp(n);return i.removeChild(n),l}const $n=e=>{const t=e.props["onUpdate:modelValue"]||!1;return ge(t)?s=>Ia(t,s):t};function $y(e){e.target.composing=!0}function jd(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Ss=Symbol("_assign");function zd(e,t,s){return t&&(e=e.trim()),s&&(e=fr(e)),e}const Wl={created(e,{modifiers:{lazy:t,trim:s,number:n}},a){e[Ss]=$n(a);const i=n||a.props&&a.props.type==="number";pn(e,t?"change":"input",l=>{l.target.composing||e[Ss](zd(e.value,s,i))}),(s||i)&&pn(e,"change",()=>{e.value=zd(e.value,s,i)}),t||(pn(e,"compositionstart",$y),pn(e,"compositionend",jd),pn(e,"change",jd))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:n,trim:a,number:i}},l){if(e[Ss]=$n(l),e.composing)return;const r=(i||e.type==="number")&&!/^0\d/.test(e.value)?fr(e.value):e.value,o=t??"";if(r===o)return;const c=e.getRootNode();(c instanceof Document||c instanceof ShadowRoot)&&c.activeElement===e&&e.type!=="range"&&(n&&t===s||a&&e.value.trim()===o)||(e.value=o)}},kc={deep:!0,created(e,t,s){e[Ss]=$n(s),pn(e,"change",()=>{const n=e._modelValue,a=Ha(e),i=e.checked,l=e[Ss];if(ge(n)){const r=hr(n,a),o=r!==-1;if(i&&!o)l(n.concat(a));else if(!i&&o){const c=[...n];c.splice(r,1),l(c)}}else if(oa(n)){const r=new Set(n);i?r.add(a):r.delete(a),l(r)}else l(nh(e,i))})},mounted:qd,beforeUpdate(e,t,s){e[Ss]=$n(s),qd(e,t,s)}};function qd(e,{value:t,oldValue:s},n){e._modelValue=t;let a;if(ge(t))a=hr(t,n.props.value)>-1;else if(oa(t))a=t.has(n.props.value);else{if(t===s)return;a=xn(t,nh(e,!0))}e.checked!==a&&(e.checked=a)}const wc={created(e,{value:t},s){e.checked=xn(t,s.props.value),e[Ss]=$n(s),pn(e,"change",()=>{e[Ss](Ha(e))})},beforeUpdate(e,{value:t,oldValue:s},n){e[Ss]=$n(n),t!==s&&(e.checked=xn(t,n.props.value))}},sh={deep:!0,created(e,{value:t,modifiers:{number:s}},n){const a=oa(t);pn(e,"change",()=>{const i=Array.prototype.filter.call(e.options,l=>l.selected).map(l=>s?fr(Ha(l)):Ha(l));e[Ss](e.multiple?a?new Set(i):i:i[0]),e._assigning=!0,At(()=>{e._assigning=!1})}),e[Ss]=$n(n)},mounted(e,{value:t}){Gd(e,t)},beforeUpdate(e,t,s){e[Ss]=$n(s)},updated(e,{value:t}){e._assigning||Gd(e,t)}};function Gd(e,t){const s=e.multiple,n=ge(t);if(!(s&&!n&&!oa(t))){for(let a=0,i=e.options.length;aString(c)===String(r)):l.selected=hr(t,r)>-1}else l.selected=t.has(r);else if(xn(Ha(l),t)){e.selectedIndex!==a&&(e.selectedIndex=a);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Ha(e){return"_value"in e?e._value:e.value}function nh(e,t){const s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}const ah={created(e,t,s){vl(e,t,s,null,"created")},mounted(e,t,s){vl(e,t,s,null,"mounted")},beforeUpdate(e,t,s,n){vl(e,t,s,n,"beforeUpdate")},updated(e,t,s,n){vl(e,t,s,n,"updated")}};function ih(e,t){switch(e){case"SELECT":return sh;case"TEXTAREA":return Wl;default:switch(t){case"checkbox":return kc;case"radio":return wc;default:return Wl}}}function vl(e,t,s,n,a){const l=ih(e.tagName,s.props&&s.props.type)[a];l&&l(e,t,s,n)}function Uy(){Wl.getSSRProps=({value:e})=>({value:e}),wc.getSSRProps=({value:e},t)=>{if(t.props&&xn(t.props.value,e))return{checked:!0}},kc.getSSRProps=({value:e},t)=>{if(ge(e)){if(t.props&&hr(e,t.props.value)>-1)return{checked:!0}}else if(oa(e)){if(t.props&&e.has(t.props.value))return{checked:!0}}else if(e)return{checked:!0}},ah.getSSRProps=(e,t)=>{if(typeof t.type!="string")return;const s=ih(t.type.toUpperCase(),t.props&&t.props.type);if(s.getSSRProps)return s.getSSRProps(e,t)}}const By=["ctrl","shift","alt","meta"],Hy={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)=>By.some(s=>e[`${s}Key`]&&!t.includes(s))},Vy=(e,t)=>{if(!e)return e;const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=((a,...i)=>{for(let l=0;l{const s=e._withKeys||(e._withKeys={}),n=t.join(".");return s[n]||(s[n]=(a=>{if(!("key"in a))return;const i=os(a.key);if(t.some(l=>l===i||jy[l]===i))return e(a)}))},lh=je({patchProp:Jp},jp);let xi,Kd=!1;function rh(){return xi||(xi=xp(lh))}function oh(){return xi=Kd?xi:_p(lh),Kd=!0,xi}const ch=((...e)=>{rh().render(...e)}),qy=((...e)=>{oh().hydrate(...e)}),Zl=((...e)=>{const t=rh().createApp(...e),{mount:s}=t;return t.mount=n=>{const a=fh(n);if(!a)return;const i=t._component;!Ie(i)&&!i.render&&!i.template&&(i.template=a.innerHTML),a.nodeType===1&&(a.textContent="");const l=s(a,!1,uh(a));return a instanceof Element&&(a.removeAttribute("v-cloak"),a.setAttribute("data-v-app","")),l},t}),dh=((...e)=>{const t=oh().createApp(...e),{mount:s}=t;return t.mount=n=>{const a=fh(n);if(a)return s(a,!0,uh(a))},t});function uh(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function fh(e){return Me(e)?document.querySelector(e):e}let Wd=!1;const Gy=()=>{Wd||(Wd=!0,Uy(),py())},Ky=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:Yf,BaseTransitionPropsValidators:oc,Comment:yt,DeprecationTypes:iy,EffectScope:Qo,ErrorCodes:ov,ErrorTypeStrings:Qb,Fragment:Ot,KeepAlive:Hv,ReactiveEffect:Ci,Static:ea,Suspense:Lb,Teleport:wv,Text:Pn,TrackOpTypes:tv,Transition:cy,TransitionGroup:Ly,TriggerOpTypes:sv,VueElement:Cr,assertNumber:rv,callWithAsyncErrorHandling:ms,callWithErrorHandling:Ja,camelize:at,capitalize:ca,cloneVNode:Ys,compatUtils:ay,computed:J,createApp:Zl,createBlock:Vl,createCommentVNode:Np,createElementBlock:Ub,createElementVNode:bc,createHydrationRenderer:_p,createPropsRestProxy:cb,createRenderer:xp,createSSRApp:dh,createSlots:Wv,createStaticVNode:Vb,createTextVNode:yc,createVNode:ft,customRef:Mf,defineAsyncComponent:Uv,defineComponent:Wi,defineCustomElement:Yp,defineEmits:Xv,defineExpose:eb,defineModel:nb,defineOptions:tb,defineProps:Qv,defineSSRCustomElement:Ey,defineSlots:sb,devtools:Xb,effect:Sg,effectScope:_g,getCurrentInstance:ts,getCurrentScope:vf,getCurrentWatcher:nv,getTransitionRawChildren:xr,guardReactiveProps:Op,h:Ua,handleError:da,hasInjectionContext:gv,hydrate:qy,hydrateOnIdle:Lv,hydrateOnInteraction:Fv,hydrateOnMediaQuery:Pv,hydrateOnVisible:Mv,initCustomFormatter:Zb,initDirectivesForSSR:Gy,inject:ws,isMemoSame:Bp,isProxy:Gi,isReactive:vn,isReadonly:Js,isRef:St,isRuntimeOnly:Gb,isShallow:ds,isVNode:Sn,markRaw:Lf,mergeDefaults:rb,mergeModels:ob,mergeProps:Lp,nextTick:At,nodeOps:jp,normalizeClass:qi,normalizeProps:og,normalizeStyle:zi,onActivated:Cs,onBeforeMount:ep,onBeforeUnmount:wr,onBeforeUpdate:dc,onDeactivated:Es,onErrorCaptured:ap,onMounted:We,onRenderTracked:np,onRenderTriggered:sp,onScopeDispose:kg,onServerPrefetch:tp,onUnmounted:xt,onUpdated:kr,onWatcherCleanup:Ff,openBlock:Di,patchProp:Jp,popScopeId:pv,provide:vi,proxyRefs:ac,pushScopeId:fv,queuePostFlushCb:Ri,reactive:Un,readonly:Dl,ref:h,registerRuntimeCompiler:Fp,render:ch,renderList:Kv,renderSlot:Zv,resolveComponent:zv,resolveDirective:Gv,resolveDynamicComponent:qv,resolveFilter:ny,resolveTransitionHooks:$a,setBlockTracking:Mi,setDevtoolsHook:ey,setTransitionHooks:wn,shallowReactive:sc,shallowReadonly:jg,shallowRef:nc,ssrContextKey:jf,ssrUtils:sy,stop:Tg,toDisplayString:mf,toHandlerKey:Ra,toHandlers:Jv,toRaw:Ze,toRef:Qg,toRefs:Zg,toValue:Gg,transformVNodeArgs:Bb,triggerRef:qg,unref:Zs,useAttrs:lb,useCssModule:Iy,useCssVars:hy,useHost:Qp,useId:Tv,useModel:bb,useSSRContext:zf,useShadowRoot:Ry,useSlots:ib,useTemplateRef:Cv,useTransitionState:rc,vModelCheckbox:kc,vModelDynamic:ah,vModelRadio:wc,vModelSelect:sh,vModelText:Wl,vShow:Wp,version:Hp,warn:Yb,watch:es,watchEffect:vv,watchPostEffect:bv,watchSyncEffect:qf,withAsyncContext:db,withCtx:lc,withDefaults:ab,withDirectives:mv,withKeys:zy,withMemo:Jb,withModifiers:Vy,withScopeId:hv},Symbol.toStringTag,{value:"Module"}));/** +**/let Ro;const Cd=typeof window<"u"&&window.trustedTypes;if(Cd)try{Ro=Cd.createPolicy("vue",{createHTML:e=>e})}catch{}const zp=Ro?e=>Ro.createHTML(e):e=>e,ly="http://www.w3.org/2000/svg",ry="http://www.w3.org/1998/Math/MathML",dn=typeof document<"u"?document:null,Ed=dn&&dn.createElement("template"),Vp={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const a=t==="svg"?dn.createElementNS(ly,e):t==="mathml"?dn.createElementNS(ry,e):s?dn.createElement(e,{is:s}):dn.createElement(e);return e==="select"&&n&&n.multiple!=null&&a.setAttribute("multiple",n.multiple),a},createText:e=>dn.createTextNode(e),createComment:e=>dn.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>dn.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,a,i){const l=s?s.previousSibling:t.lastChild;if(a&&(a===i||a.nextSibling))for(;t.insertBefore(a.cloneNode(!0),s),!(a===i||!(a=a.nextSibling)););else{Ed.innerHTML=zp(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const r=Ed.content;if(n==="svg"||n==="mathml"){const o=r.firstChild;for(;o.firstChild;)r.appendChild(o.firstChild);r.removeChild(o)}t.insertBefore(r,s)}return[l?l.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},Rn="transition",ci="animation",qa=Symbol("_vtc"),jp={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},qp=ze({},oc,jp),oy=e=>(e.displayName="Transition",e.props=qp,e),cy=oy((e,{slots:t})=>ja(Yf,Gp(e),t)),Gn=(e,t=[])=>{be(e)?e.forEach(s=>s(...t)):e&&e(...t)},Ad=e=>e?be(e)?e.some(t=>t.length>1):e.length>1:!1;function Gp(e){const t={};for(const P in e)P in jp||(t[P]=e[P]);if(e.css===!1)return t;const{name:s="v",type:n,duration:a,enterFromClass:i=`${s}-enter-from`,enterActiveClass:l=`${s}-enter-active`,enterToClass:r=`${s}-enter-to`,appearFromClass:o=i,appearActiveClass:c=l,appearToClass:d=r,leaveFromClass:u=`${s}-leave-from`,leaveActiveClass:f=`${s}-leave-active`,leaveToClass:p=`${s}-leave-to`}=e,b=dy(a),y=b&&b[0],E=b&&b[1],{onBeforeEnter:I,onEnter:x,onEnterCancelled:m,onLeave:_,onLeaveCancelled:S,onBeforeAppear:g=I,onAppear:w=x,onAppearCancelled:T=m}=t,C=(P,R,j,Q)=>{P._enterCancelled=Q,Ln(P,R?d:r),Ln(P,R?c:l),j&&j()},M=(P,R)=>{P._isLeaving=!1,Ln(P,u),Ln(P,p),Ln(P,f),R&&R()},H=P=>(R,j)=>{const Q=P?w:x,U=()=>C(R,P,j);Gn(Q,[R,U]),Rd(()=>{Ln(R,P?o:i),Ws(R,P?d:r),Ad(Q)||Id(R,n,y,U)})};return ze(t,{onBeforeEnter(P){Gn(I,[P]),Ws(P,i),Ws(P,l)},onBeforeAppear(P){Gn(g,[P]),Ws(P,o),Ws(P,c)},onEnter:H(!1),onAppear:H(!0),onLeave(P,R){P._isLeaving=!0;const j=()=>M(P,R);Ws(P,u),P._enterCancelled?(Ws(P,f),Io(P)):(Io(P),Ws(P,f)),Rd(()=>{P._isLeaving&&(Ln(P,u),Ws(P,p),Ad(_)||Id(P,n,E,j))}),Gn(_,[P,j])},onEnterCancelled(P){C(P,!1,void 0,!0),Gn(m,[P])},onAppearCancelled(P){C(P,!0,void 0,!0),Gn(T,[P])},onLeaveCancelled(P){M(P),Gn(S,[P])}})}function dy(e){if(e==null)return null;if(Xe(e))return[Wr(e.enter),Wr(e.leave)];{const t=Wr(e);return[t,t]}}function Wr(e){return Ll(e)}function Ws(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.add(s)),(e[qa]||(e[qa]=new Set)).add(t)}function Ln(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.remove(n));const s=e[qa];s&&(s.delete(t),s.size||(e[qa]=void 0))}function Rd(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let uy=0;function Id(e,t,s,n){const a=e._endId=++uy,i=()=>{a===e._endId&&n()};if(s!=null)return setTimeout(i,s);const{type:l,timeout:r,propCount:o}=Kp(e,t);if(!l)return n();const c=l+"end";let d=0;const u=()=>{e.removeEventListener(c,f),i()},f=p=>{p.target===e&&++d>=o&&u()};setTimeout(()=>{d(s[b]||"").split(", "),a=n(`${Rn}Delay`),i=n(`${Rn}Duration`),l=Od(a,i),r=n(`${ci}Delay`),o=n(`${ci}Duration`),c=Od(r,o);let d=null,u=0,f=0;t===Rn?l>0&&(d=Rn,u=l,f=i.length):t===ci?c>0&&(d=ci,u=c,f=o.length):(u=Math.max(l,c),d=u>0?l>c?Rn:ci:null,f=d?d===Rn?i.length:o.length:0);const p=d===Rn&&/\b(?:transform|all)(?:,|$)/.test(n(`${Rn}Property`).toString());return{type:d,timeout:u,propCount:f,hasTransform:p}}function Od(e,t){for(;e.lengthNd(s)+Nd(e[n])))}function Nd(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Io(e){return(e?e.ownerDocument:document).body.offsetHeight}function fy(e,t,s){const n=e[qa];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const Gl=Symbol("_vod"),_c=Symbol("_vsh"),Wp={name:"show",beforeMount(e,{value:t},{transition:s}){e[Gl]=e.style.display==="none"?"":e.style.display,s&&t?s.beforeEnter(e):di(e,t)},mounted(e,{value:t},{transition:s}){s&&t&&s.enter(e)},updated(e,{value:t,oldValue:s},{transition:n}){!t!=!s&&(n?t?(n.beforeEnter(e),di(e,!0),n.enter(e)):n.leave(e,()=>{di(e,!1)}):di(e,t))},beforeUnmount(e,{value:t}){di(e,t)}};function di(e,t){e.style.display=t?e[Gl]:"none",e[_c]=!t}function py(){Wp.getSSRProps=({value:e})=>{if(!e)return{style:{display:"none"}}}}const Zp=Symbol("");function hy(e){const t=as();if(!t)return;const s=t.ut=(a=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(i=>Kl(i,a))},n=()=>{const a=e(t.proxy);t.ce?Kl(t.ce,a):Oo(t.subTree,a),s(a)};dc(()=>{Mi(n)}),We(()=>{ns(n,Ht,{flush:"post"});const a=new MutationObserver(n);a.observe(t.subTree.el.parentNode,{childList:!0}),xt(()=>a.disconnect())})}function Oo(e,t){if(e.shapeFlag&128){const s=e.suspense;e=s.activeBranch,s.pendingBranch&&!s.isHydrating&&s.effects.push(()=>{Oo(s.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)Kl(e.el,t);else if(e.type===Dt)e.children.forEach(s=>Oo(s,t));else if(e.type===sa){let{el:s,anchor:n}=e;for(;s&&(Kl(s,t),s!==n);)s=s.nextSibling}}function Kl(e,t){if(e.nodeType===1){const s=e.style;let n="";for(const a in t){const i=xg(t[a]);s.setProperty(`--${a}`,i),n+=`--${a}: ${i};`}s[Zp]=n}}const my=/(?:^|;)\s*display\s*:/;function gy(e,t,s){const n=e.style,a=Me(s);let i=!1;if(s&&!a){if(t)if(Me(t))for(const l of t.split(";")){const r=l.slice(0,l.indexOf(":")).trim();s[r]==null&&yi(n,r,"")}else for(const l in t)s[l]==null&&yi(n,l,"");for(const l in s){l==="display"&&(i=!0);const r=s[l];r!=null?by(e,l,!Me(t)&&t?t[l]:void 0,r)||yi(n,l,r):yi(n,l,"")}}else if(a){if(t!==s){const l=n[Zp];l&&(s+=";"+l),n.cssText=s,i=my.test(s)}}else t&&e.removeAttribute("style");Gl in e&&(e[Gl]=i?n.display:"",e[_c]&&(n.display="none"))}const Ld=/\s*!important$/;function yi(e,t,s){if(be(s))s.forEach(n=>yi(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=vy(e,t);Ld.test(s)?e.setProperty(ps(n),s.replace(Ld,""),"important"):e[n]=s}}const Dd=["Webkit","Moz","ms"],Zr={};function vy(e,t){const s=Zr[t];if(s)return s;let n=it(t);if(n!=="filter"&&n in e)return Zr[t]=n;n=ua(n);for(let a=0;aJr||(ky.then(()=>Jr=0),Jr=Date.now());function Sy(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;const a=s.value;if(be(a)){const i=n.stopImmediatePropagation;n.stopImmediatePropagation=()=>{i.call(n),n._stopped=!0};const l=a.slice(),r=[n];for(let o=0;oe.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Jp=(e,t,s,n,a,i)=>{const l=a==="svg";t==="class"?fy(e,n,l):t==="style"?gy(e,s,n):ca(t)?cr(t)||xy(e,t,s,n,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Ty(e,t,n,l))?(Fd(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Pd(e,t,n,l,i,t!=="value")):e._isVueCE&&(Cy(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!Me(n)))?Fd(e,it(t),n,i,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Pd(e,t,n,l))};function Ty(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&Ud(t)&&Ie(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 a=e.tagName;if(a==="IMG"||a==="VIDEO"||a==="CANVAS"||a==="SOURCE")return!1}return Ud(t)&&Me(s)?!1:t in e}function Cy(e,t){const s=e._def.props;if(!s)return!1;const n=it(t);return Array.isArray(s)?s.some(a=>it(a)===n):Object.keys(s).some(a=>it(a)===n)}const Hd={};function Yp(e,t,s){let n=el(e,t);dr(n)&&(n=ze({},n,t));class a extends Er{constructor(l){super(n,l,s)}}return a.def=n,a}const Ey=((e,t)=>Yp(e,t,dh)),Ay=typeof HTMLElement<"u"?HTMLElement:class{};class Er extends Ay{constructor(t,s={},n=Jl){super(),this._def=t,this._props=s,this._createApp=n,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&&n!==Jl?this._root=this.shadowRoot:t.shadowRoot!==!1?(this.attachShadow(ze({},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 Er){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,Rt(()=>{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 n=0;n{this._resolved=!0,this._pendingResolve=void 0;const{props:i,styles:l}=n;let r;if(i&&!be(i))for(const o in i){const c=i[o];(c===Number||c&&c.type===Number)&&(o in this._props&&(this._props[o]=Ll(this._props[o])),(r||(r=Object.create(null)))[it(o)]=!0)}this._numberProps=r,this._resolveProps(n),this.shadowRoot&&this._applyStyles(l),this._mount(n)},s=this._def.__asyncLoader;s?this._pendingResolve=s().then(n=>{n.configureApp=this._def.configureApp,t(this._def=n,!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 n in s)tt(this,n)||Object.defineProperty(this,n,{get:()=>en(s[n])})}_resolveProps(t){const{props:s}=t,n=be(s)?s:Object.keys(s||{});for(const a of Object.keys(this))a[0]!=="_"&&n.includes(a)&&this._setProp(a,this[a]);for(const a of n.map(it))Object.defineProperty(this,a,{get(){return this._getProp(a)},set(i){this._setProp(a,i,!0,!this._patching)}})}_setAttr(t){if(t.startsWith("data-v-"))return;const s=this.hasAttribute(t);let n=s?this.getAttribute(t):Hd;const a=it(t);s&&this._numberProps&&this._numberProps[a]&&(n=Ll(n)),this._setProp(a,n,!1,!0)}_getProp(t){return this._props[t]}_setProp(t,s,n=!0,a=!1){if(s!==this._props[t]&&(this._dirty=!0,s===Hd?delete this._props[t]:(this._props[t]=s,t==="key"&&this._app&&(this._app._ceVNode.key=s)),a&&this._instance&&this._update(),n)){const i=this._ob;i&&(this._processMutations(i.takeRecords()),i.disconnect()),s===!0?this.setAttribute(ps(t),""):typeof s=="string"||typeof s=="number"?this.setAttribute(ps(t),s+""):s||this.removeAttribute(ps(t)),i&&i.observe(this,{attributes:!0})}}_update(){const t=this._createVNode();this._app&&(t.appContext=this._app._context),ch(t,this._root)}_createVNode(){const t={};this.shadowRoot||(t.onVnodeMounted=t.onVnodeUpdated=this._renderSlots.bind(this));const s=ft(this._def,ze(t,this._props));return this._instance||(s.ce=n=>{this._instance=n,n.ce=this,n.isCE=!0;const a=(i,l)=>{this.dispatchEvent(new CustomEvent(i,dr(l[0])?ze({detail:l},l[0]):{detail:l}))};n.emit=(i,...l)=>{a(i,l),ps(i)!==i&&a(ps(i),l)},this._setParent()}),s}_applyStyles(t,s,n){if(!t)return;if(s){if(s===this._def||this._styleChildren.has(s))return;this._styleChildren.add(s)}const a=this._nonce,i=this.shadowRoot,l=n?this._getStyleAnchor(n)||this._getStyleAnchor(this._def):this._getRootStyleInsertionAnchor(i);let r=null;for(let o=t.length-1;o>=0;o--){const c=document.createElement("style");a&&c.setAttribute("nonce",a),c.textContent=t[o],i.insertBefore(c,r||l),r=c,o===0&&(n||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),Ny=Oy({name:"TransitionGroup",props:ze({},qp,{tag:String,moveClass:String}),setup(e,{slots:t}){const s=as(),n=rc();let a,i;return wr(()=>{if(!a.length)return;const l=e.moveClass||`${e.name||"v"}-move`;if(!Fy(a[0].el,s.vnode.el,l)){a=[];return}a.forEach(Dy),a.forEach(My);const r=a.filter(Py);Io(s.vnode.el),r.forEach(o=>{const c=o.el,d=c.style;Ws(c,l),d.transform=d.webkitTransform=d.transitionDuration="";const u=c[Wl]=f=>{f&&f.target!==c||(!f||f.propertyName.endsWith("transform"))&&(c.removeEventListener("transitionend",u),c[Wl]=null,Ln(c,l))};c.addEventListener("transitionend",u)}),a=[]}),()=>{const l=Je(e),r=Gp(l);let o=l.tag||Dt;if(a=[],i)for(let c=0;c{r.split(/\s+/).forEach(o=>o&&n.classList.remove(o))}),s.split(/\s+/).forEach(r=>r&&n.classList.add(r)),n.style.display="none";const i=t.nodeType===1?t:t.parentNode;i.appendChild(n);const{hasTransform:l}=Kp(n);return i.removeChild(n),l}const Un=e=>{const t=e.props["onUpdate:modelValue"]||!1;return be(t)?s=>Ma(t,s):t};function $y(e){e.target.composing=!0}function Vd(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Ns=Symbol("_assign");function jd(e,t,s){return t&&(e=e.trim()),s&&(e=pr(e)),e}const Zl={created(e,{modifiers:{lazy:t,trim:s,number:n}},a){e[Ns]=Un(a);const i=n||a.props&&a.props.type==="number";mn(e,t?"change":"input",l=>{l.target.composing||e[Ns](jd(e.value,s,i))}),(s||i)&&mn(e,"change",()=>{e.value=jd(e.value,s,i)}),t||(mn(e,"compositionstart",$y),mn(e,"compositionend",Vd),mn(e,"change",Vd))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:n,trim:a,number:i}},l){if(e[Ns]=Un(l),e.composing)return;const r=(i||e.type==="number")&&!/^0\d/.test(e.value)?pr(e.value):e.value,o=t??"";if(r===o)return;const c=e.getRootNode();(c instanceof Document||c instanceof ShadowRoot)&&c.activeElement===e&&e.type!=="range"&&(n&&t===s||a&&e.value.trim()===o)||(e.value=o)}},kc={deep:!0,created(e,t,s){e[Ns]=Un(s),mn(e,"change",()=>{const n=e._modelValue,a=Ga(e),i=e.checked,l=e[Ns];if(be(n)){const r=mr(n,a),o=r!==-1;if(i&&!o)l(n.concat(a));else if(!i&&o){const c=[...n];c.splice(r,1),l(c)}}else if(da(n)){const r=new Set(n);i?r.add(a):r.delete(a),l(r)}else l(nh(e,i))})},mounted:qd,beforeUpdate(e,t,s){e[Ns]=Un(s),qd(e,t,s)}};function qd(e,{value:t,oldValue:s},n){e._modelValue=t;let a;if(be(t))a=mr(t,n.props.value)>-1;else if(da(t))a=t.has(n.props.value);else{if(t===s)return;a=kn(t,nh(e,!0))}e.checked!==a&&(e.checked=a)}const wc={created(e,{value:t},s){e.checked=kn(t,s.props.value),e[Ns]=Un(s),mn(e,"change",()=>{e[Ns](Ga(e))})},beforeUpdate(e,{value:t,oldValue:s},n){e[Ns]=Un(n),t!==s&&(e.checked=kn(t,n.props.value))}},sh={deep:!0,created(e,{value:t,modifiers:{number:s}},n){const a=da(t);mn(e,"change",()=>{const i=Array.prototype.filter.call(e.options,l=>l.selected).map(l=>s?pr(Ga(l)):Ga(l));e[Ns](e.multiple?a?new Set(i):i:i[0]),e._assigning=!0,Rt(()=>{e._assigning=!1})}),e[Ns]=Un(n)},mounted(e,{value:t}){Gd(e,t)},beforeUpdate(e,t,s){e[Ns]=Un(s)},updated(e,{value:t}){e._assigning||Gd(e,t)}};function Gd(e,t){const s=e.multiple,n=be(t);if(!(s&&!n&&!da(t))){for(let a=0,i=e.options.length;aString(c)===String(r)):l.selected=mr(t,r)>-1}else l.selected=t.has(r);else if(kn(Ga(l),t)){e.selectedIndex!==a&&(e.selectedIndex=a);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Ga(e){return"_value"in e?e._value:e.value}function nh(e,t){const s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}const ah={created(e,t,s){bl(e,t,s,null,"created")},mounted(e,t,s){bl(e,t,s,null,"mounted")},beforeUpdate(e,t,s,n){bl(e,t,s,n,"beforeUpdate")},updated(e,t,s,n){bl(e,t,s,n,"updated")}};function ih(e,t){switch(e){case"SELECT":return sh;case"TEXTAREA":return Zl;default:switch(t){case"checkbox":return kc;case"radio":return wc;default:return Zl}}}function bl(e,t,s,n,a){const l=ih(e.tagName,s.props&&s.props.type)[a];l&&l(e,t,s,n)}function By(){Zl.getSSRProps=({value:e})=>({value:e}),wc.getSSRProps=({value:e},t)=>{if(t.props&&kn(t.props.value,e))return{checked:!0}},kc.getSSRProps=({value:e},t)=>{if(be(e)){if(t.props&&mr(e,t.props.value)>-1)return{checked:!0}}else if(da(e)){if(t.props&&e.has(t.props.value))return{checked:!0}}else if(e)return{checked:!0}},ah.getSSRProps=(e,t)=>{if(typeof t.type!="string")return;const s=ih(t.type.toUpperCase(),t.props&&t.props.type);if(s.getSSRProps)return s.getSSRProps(e,t)}}const Uy=["ctrl","shift","alt","meta"],Hy={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)=>Uy.some(s=>e[`${s}Key`]&&!t.includes(s))},zy=(e,t)=>{if(!e)return e;const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=((a,...i)=>{for(let l=0;l{const s=e._withKeys||(e._withKeys={}),n=t.join(".");return s[n]||(s[n]=(a=>{if(!("key"in a))return;const i=ps(a.key);if(t.some(l=>l===i||Vy[l]===i))return e(a)}))},lh=ze({patchProp:Jp},Vp);let Ci,Kd=!1;function rh(){return Ci||(Ci=xp(lh))}function oh(){return Ci=Kd?Ci:_p(lh),Kd=!0,Ci}const ch=((...e)=>{rh().render(...e)}),qy=((...e)=>{oh().hydrate(...e)}),Jl=((...e)=>{const t=rh().createApp(...e),{mount:s}=t;return t.mount=n=>{const a=fh(n);if(!a)return;const i=t._component;!Ie(i)&&!i.render&&!i.template&&(i.template=a.innerHTML),a.nodeType===1&&(a.textContent="");const l=s(a,!1,uh(a));return a instanceof Element&&(a.removeAttribute("v-cloak"),a.setAttribute("data-v-app","")),l},t}),dh=((...e)=>{const t=oh().createApp(...e),{mount:s}=t;return t.mount=n=>{const a=fh(n);if(a)return s(a,!0,uh(a))},t});function uh(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function fh(e){return Me(e)?document.querySelector(e):e}let Wd=!1;const Gy=()=>{Wd||(Wd=!0,By(),py())},Ky=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:Yf,BaseTransitionPropsValidators:oc,Comment:yt,DeprecationTypes:iy,EffectScope:Qo,ErrorCodes:ov,ErrorTypeStrings:Qb,Fragment:Dt,KeepAlive:Hv,ReactiveEffect:Ni,Static:sa,Suspense:Lb,Teleport:wv,Text:$n,TrackOpTypes:tv,Transition:cy,TransitionGroup:Ly,TriggerOpTypes:sv,VueElement:Er,assertNumber:rv,callWithAsyncErrorHandling:xs,callWithErrorHandling:ti,camelize:it,capitalize:ua,cloneVNode:sn,compatUtils:ay,computed:J,createApp:Jl,createBlock:Vl,createCommentVNode:Np,createElementBlock:Bb,createElementVNode:bc,createHydrationRenderer:_p,createPropsRestProxy:cb,createRenderer:xp,createSSRApp:dh,createSlots:Wv,createStaticVNode:zb,createTextVNode:yc,createVNode:ft,customRef:Mf,defineAsyncComponent:Bv,defineComponent:el,defineCustomElement:Yp,defineEmits:Xv,defineExpose:eb,defineModel:nb,defineOptions:tb,defineProps:Qv,defineSSRCustomElement:Ey,defineSlots:sb,devtools:Xb,effect:Sg,effectScope:_g,getCurrentInstance:as,getCurrentScope:vf,getCurrentWatcher:nv,getTransitionRawChildren:_r,guardReactiveProps:Op,h:ja,handleError:fa,hasInjectionContext:gv,hydrate:qy,hydrateOnIdle:Lv,hydrateOnInteraction:Fv,hydrateOnMediaQuery:Pv,hydrateOnVisible:Mv,initCustomFormatter:Zb,initDirectivesForSSR:Gy,inject:Os,isMemoSame:Up,isProxy:Qi,isReactive:yn,isReadonly:tn,isRef:St,isRuntimeOnly:Gb,isShallow:ms,isVNode:Cn,markRaw:Lf,mergeDefaults:rb,mergeModels:ob,mergeProps:Lp,nextTick:Rt,nodeOps:Vp,normalizeClass:Yi,normalizeProps:og,normalizeStyle:Ji,onActivated:Ds,onBeforeMount:ep,onBeforeUnmount:Sr,onBeforeUpdate:dc,onDeactivated:Ms,onErrorCaptured:ap,onMounted:We,onRenderTracked:np,onRenderTriggered:sp,onScopeDispose:kg,onServerPrefetch:tp,onUnmounted:xt,onUpdated:wr,onWatcherCleanup:Ff,openBlock:Ui,patchProp:Jp,popScopeId:pv,provide:wi,proxyRefs:ac,pushScopeId:fv,queuePostFlushCb:Mi,reactive:Hn,readonly:Ml,ref:h,registerRuntimeCompiler:Fp,render:ch,renderList:Kv,renderSlot:Zv,resolveComponent:jv,resolveDirective:Gv,resolveDynamicComponent:qv,resolveFilter:ny,resolveTransitionHooks:Va,setBlockTracking:Hi,setDevtoolsHook:ey,setTransitionHooks:Tn,shallowReactive:sc,shallowReadonly:Vg,shallowRef:nc,ssrContextKey:Vf,ssrUtils:sy,stop:Tg,toDisplayString:mf,toHandlerKey:Da,toHandlers:Jv,toRaw:Je,toRef:Qg,toRefs:Zg,toValue:Gg,transformVNodeArgs:Ub,triggerRef:qg,unref:en,useAttrs:lb,useCssModule:Iy,useCssVars:hy,useHost:Qp,useId:Tv,useModel:bb,useSSRContext:jf,useShadowRoot:Ry,useSlots:ib,useTemplateRef:Cv,useTransitionState:rc,vModelCheckbox:kc,vModelDynamic:ah,vModelRadio:wc,vModelSelect:sh,vModelText:Zl,vShow:Wp,version:Hp,warn:Yb,watch:ns,watchEffect:vv,watchPostEffect:bv,watchSyncEffect:qf,withAsyncContext:db,withCtx:lc,withDefaults:ab,withDirectives:mv,withKeys:jy,withMemo:Jb,withModifiers:zy,withScopeId:hv},Symbol.toStringTag,{value:"Module"}));/** * @vue/compiler-core v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const Fi=Symbol(""),_i=Symbol(""),Sc=Symbol(""),Jl=Symbol(""),ph=Symbol(""),ia=Symbol(""),hh=Symbol(""),mh=Symbol(""),Tc=Symbol(""),Cc=Symbol(""),Yi=Symbol(""),Ec=Symbol(""),gh=Symbol(""),Ac=Symbol(""),Rc=Symbol(""),Ic=Symbol(""),Oc=Symbol(""),Nc=Symbol(""),Lc=Symbol(""),vh=Symbol(""),bh=Symbol(""),Er=Symbol(""),Yl=Symbol(""),Dc=Symbol(""),Mc=Symbol(""),$i=Symbol(""),Qi=Symbol(""),Pc=Symbol(""),No=Symbol(""),Wy=Symbol(""),Lo=Symbol(""),Ql=Symbol(""),Zy=Symbol(""),Jy=Symbol(""),Fc=Symbol(""),Yy=Symbol(""),Qy=Symbol(""),$c=Symbol(""),yh=Symbol(""),Va={[Fi]:"Fragment",[_i]:"Teleport",[Sc]:"Suspense",[Jl]:"KeepAlive",[ph]:"BaseTransition",[ia]:"openBlock",[hh]:"createBlock",[mh]:"createElementBlock",[Tc]:"createVNode",[Cc]:"createElementVNode",[Yi]:"createCommentVNode",[Ec]:"createTextVNode",[gh]:"createStaticVNode",[Ac]:"resolveComponent",[Rc]:"resolveDynamicComponent",[Ic]:"resolveDirective",[Oc]:"resolveFilter",[Nc]:"withDirectives",[Lc]:"renderList",[vh]:"renderSlot",[bh]:"createSlots",[Er]:"toDisplayString",[Yl]:"mergeProps",[Dc]:"normalizeClass",[Mc]:"normalizeStyle",[$i]:"normalizeProps",[Qi]:"guardReactiveProps",[Pc]:"toHandlers",[No]:"camelize",[Wy]:"capitalize",[Lo]:"toHandlerKey",[Ql]:"setBlockTracking",[Zy]:"pushScopeId",[Jy]:"popScopeId",[Fc]:"withCtx",[Yy]:"unref",[Qy]:"isRef",[$c]:"withMemo",[yh]:"isMemoSame"};function Xy(e){Object.getOwnPropertySymbols(e).forEach(t=>{Va[t]=e[t]})}const bs={start:{line:1,column:1,offset:0},end:{line:1,column:1,offset:0},source:""};function ex(e,t=""){return{type:0,source:t,children:e,helpers:new Set,components:[],directives:[],hoists:[],imports:[],cached:[],temps:0,codegenNode:void 0,loc:bs}}function Ui(e,t,s,n,a,i,l,r=!1,o=!1,c=!1,d=bs){return e&&(r?(e.helper(ia),e.helper(qa(e.inSSR,c))):e.helper(za(e.inSSR,c)),l&&e.helper(Nc)),{type:13,tag:t,props:s,children:n,patchFlag:a,dynamicProps:i,directives:l,isBlock:r,disableTracking:o,isComponent:c,loc:d}}function ta(e,t=bs){return{type:17,loc:t,elements:e}}function ks(e,t=bs){return{type:15,loc:t,properties:e}}function wt(e,t){return{type:16,loc:bs,key:Me(e)?Pe(e,!0):e,value:t}}function Pe(e,t=!1,s=bs,n=0){return{type:4,loc:s,content:e,isStatic:t,constType:t?3:n}}function Ms(e,t=bs){return{type:8,loc:t,children:e}}function Rt(e,t=[],s=bs){return{type:14,loc:s,callee:e,arguments:t}}function ja(e,t=void 0,s=!1,n=!1,a=bs){return{type:18,params:e,returns:t,newline:s,isSlot:n,loc:a}}function Do(e,t,s,n=!0){return{type:19,test:e,consequent:t,alternate:s,newline:n,loc:bs}}function tx(e,t,s=!1,n=!1){return{type:20,index:e,value:t,needPauseTracking:s,inVOnce:n,needArraySpread:!1,loc:bs}}function sx(e){return{type:21,body:e,loc:bs}}function za(e,t){return e||t?Tc:Cc}function qa(e,t){return e||t?hh:mh}function Uc(e,{helper:t,removeHelper:s,inSSR:n}){e.isBlock||(e.isBlock=!0,s(za(n,e.isComponent)),t(ia),t(qa(n,e.isComponent)))}const Zd=new Uint8Array([123,123]),Jd=new Uint8Array([125,125]);function Yd(e){return e>=97&&e<=122||e>=65&&e<=90}function ps(e){return e===32||e===10||e===9||e===12||e===13}function An(e){return e===47||e===62||ps(e)}function Xl(e){const t=new Uint8Array(e.length);for(let s=0;s100){let l=-1,r=a;for(;l+1>>1;this.newlines[o]=0;l--)if(t>this.newlines[l]){i=l;break}return i>=0&&(s=i+2,n=t-this.newlines[i]),{column:n,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?An(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||ps(t)){const s=this.index-this.currentSequence.length;if(this.sectionStart=t||(this.state===28?this.currentSequence===Bt.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 Qd(e,{compatConfig:t}){const s=t&&t[e];return e==="MODE"?s||3:s}function sa(e,t){const s=Qd("MODE",t),n=Qd(e,t);return s===3?n===!0:n!==!1}function Bi(e,t,s,...n){return sa(e,t)}function Bc(e){throw e}function xh(e){}function ut(e,t,s,n){const a=`https://vuejs.org/error-reference/#compiler-${e}`,i=new SyntaxError(String(a));return i.code=e,i.loc=t,i}const cs=e=>e.type===4&&e.isStatic;function _h(e){switch(e){case"Teleport":case"teleport":return _i;case"Suspense":case"suspense":return Sc;case"KeepAlive":case"keep-alive":return Jl;case"BaseTransition":case"base-transition":return ph}}const ax=/^$|^\d|[^\$\w\xA0-\uFFFF]/,Hc=e=>!ax.test(e),kh=/[A-Za-z_$\xA0-\uFFFF]/,ix=/[\.\?\w$\xA0-\uFFFF]/,lx=/\s+[.[]\s*|\s*[.[]\s+/g,wh=e=>e.type===4?e.content:e.loc.source,rx=e=>{const t=wh(e).trim().replace(lx,r=>r.trim());let s=0,n=[],a=0,i=0,l=null;for(let r=0;r|^\s*(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/,cx=e=>ox.test(wh(e)),dx=cx;function _s(e,t,s=!1){for(let n=0;nt.type===7&&t.name==="bind"&&(!t.arg||t.arg.type!==4||!t.arg.isStatic))}function Yr(e){return e.type===5||e.type===2}function Xd(e){return e.type===7&&e.name==="pre"}function fx(e){return e.type===7&&e.name==="slot"}function er(e){return e.type===1&&e.tagType===3}function tr(e){return e.type===1&&e.tagType===2}const px=new Set([$i,Qi]);function Th(e,t=[]){if(e&&!Me(e)&&e.type===14){const s=e.callee;if(!Me(s)&&px.has(s))return Th(e.arguments[0],t.concat(e))}return[e,t]}function sr(e,t,s){let n,a=e.type===13?e.props:e.arguments[2],i=[],l;if(a&&!Me(a)&&a.type===14){const r=Th(a);a=r[0],i=r[1],l=i[i.length-1]}if(a==null||Me(a))n=ks([t]);else if(a.type===14){const r=a.arguments[0];!Me(r)&&r.type===15?eu(t,r)||r.properties.unshift(t):a.callee===Pc?n=Rt(s.helper(Yl),[ks([t]),a]):a.arguments.unshift(ks([t])),!n&&(n=a)}else a.type===15?(eu(t,a)||a.properties.unshift(t),n=a):(n=Rt(s.helper(Yl),[ks([t]),a]),l&&l.callee===Qi&&(l=i[i.length-2]));e.type===13?l?l.arguments[0]=n:e.props=n:l?l.arguments[0]=n:e.arguments[2]=n}function eu(e,t){let s=!1;if(e.key.type===4){const n=e.key.content;s=t.properties.some(a=>a.key.type===4&&a.key.content===n)}return s}function Hi(e,t){return`_${t}_${e.replace(/[^\w]/g,(s,n)=>s==="-"?"_":e.charCodeAt(n).toString())}`}function hx(e){return e.type===14&&e.callee===$c?e.arguments[1].returns:e}const mx=/([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/;function Ch(e){for(let t=0;t0,isVoidTag:Ta,isPreTag:Ta,isIgnoreNewlineTag:Ta,isCustomElement:Ta,onError:Bc,onWarn:xh,comments:!1,prefixIdentifiers:!1};let Ye=Ah,Vi=null,yn="",Vt=null,Ge=null,as="",rn=-1,Gn=-1,jc=0,Dn=!1,Mo=null;const dt=[],gt=new nx(dt,{onerr:nn,ontext(e,t){bl(Dt(e,t),e,t)},ontextentity(e,t,s){bl(e,t,s)},oninterpolation(e,t){if(Dn)return bl(Dt(e,t),e,t);let s=e+gt.delimiterOpen.length,n=t-gt.delimiterClose.length;for(;ps(yn.charCodeAt(s));)s++;for(;ps(yn.charCodeAt(n-1));)n--;let a=Dt(s,n);a.includes("&")&&(a=Ye.decodeEntities(a,!1)),Po({type:5,content:Al(a,!1,bt(s,n)),loc:bt(e,t)})},onopentagname(e,t){const s=Dt(e,t);Vt={type:1,tag:s,ns:Ye.getNamespace(s,dt[0],Ye.ns),tagType:0,props:[],children:[],loc:bt(e-1,t),codegenNode:void 0}},onopentagend(e){su(e)},onclosetag(e,t){const s=Dt(e,t);if(!Ye.isVoidTag(s)){let n=!1;for(let a=0;a0&&nn(24,dt[0].loc.start.offset);for(let l=0;l<=a;l++){const r=dt.shift();El(r,t,l(n.type===7?n.rawName:n.name)===s)&&nn(2,t)},onattribend(e,t){if(Vt&&Ge){if(Jn(Ge.loc,t),e!==0)if(as.includes("&")&&(as=Ye.decodeEntities(as,!0)),Ge.type===6)Ge.name==="class"&&(as=Oh(as).trim()),e===1&&!as&&nn(13,t),Ge.value={type:2,content:as,loc:e===1?bt(rn,Gn):bt(rn-1,Gn+1)},gt.inSFCRoot&&Vt.tag==="template"&&Ge.name==="lang"&&as&&as!=="html"&>.enterRCDATA(Xl("a.content==="sync"))>-1&&Bi("COMPILER_V_BIND_SYNC",Ye,Ge.loc,Ge.arg.loc.source)&&(Ge.name="model",Ge.modifiers.splice(n,1))}(Ge.type!==7||Ge.name!=="pre")&&Vt.props.push(Ge)}as="",rn=Gn=-1},oncomment(e,t){Ye.comments&&Po({type:3,content:Dt(e,t),loc:bt(e-4,t+3)})},onend(){const e=yn.length;for(let t=0;t{const b=t.start.offset+f,y=b+u.length;return Al(u,!1,bt(b,y),0,p?1:0)},r={source:l(i.trim(),s.indexOf(i,a.length)),value:void 0,key:void 0,index:void 0,finalized:!1};let o=a.trim().replace(gx,"").trim();const c=a.indexOf(o),d=o.match(tu);if(d){o=o.replace(tu,"").trim();const u=d[1].trim();let f;if(u&&(f=s.indexOf(u,c+o.length),r.key=l(u,f,!0)),d[2]){const p=d[2].trim();p&&(r.index=l(p,s.indexOf(p,r.key?f+u.length:c+o.length),!0))}}return o&&(r.value=l(o,c,!0)),r}function Dt(e,t){return yn.slice(e,t)}function su(e){gt.inSFCRoot&&(Vt.innerLoc=bt(e+1,e+1)),Po(Vt);const{tag:t,ns:s}=Vt;s===0&&Ye.isPreTag(t)&&jc++,Ye.isVoidTag(t)?El(Vt,e):(dt.unshift(Vt),(s===1||s===2)&&(gt.inXML=!0)),Vt=null}function bl(e,t,s){{const i=dt[0]&&dt[0].tag;i!=="script"&&i!=="style"&&e.includes("&")&&(e=Ye.decodeEntities(e,!1))}const n=dt[0]||Vi,a=n.children[n.children.length-1];a&&a.type===2?(a.content+=e,Jn(a.loc,s)):n.children.push({type:2,content:e,loc:bt(t,s)})}function El(e,t,s=!1){s?Jn(e.loc,Rh(t,60)):Jn(e.loc,bx(t,62)+1),gt.inSFCRoot&&(e.children.length?e.innerLoc.end=je({},e.children[e.children.length-1].loc.end):e.innerLoc.end=je({},e.innerLoc.start),e.innerLoc.source=Dt(e.innerLoc.start.offset,e.innerLoc.end.offset));const{tag:n,ns:a,children:i}=e;if(Dn||(n==="slot"?e.tagType=2:nu(e)?e.tagType=3:xx(e)&&(e.tagType=1)),gt.inRCDATA||(e.children=Ih(i)),a===0&&Ye.isIgnoreNewlineTag(n)){const l=i[0];l&&l.type===2&&(l.content=l.content.replace(/^\r?\n/,""))}a===0&&Ye.isPreTag(n)&&jc--,Mo===e&&(Dn=gt.inVPre=!1,Mo=null),gt.inXML&&(dt[0]?dt[0].ns:Ye.ns)===0&&(gt.inXML=!1);{const l=e.props;if(!gt.inSFCRoot&&sa("COMPILER_NATIVE_TEMPLATE",Ye)&&e.tag==="template"&&!nu(e)){const o=dt[0]||Vi,c=o.children.indexOf(e);o.children.splice(c,1,...e.children)}const r=l.find(o=>o.type===6&&o.name==="inline-template");r&&Bi("COMPILER_INLINE_TEMPLATE",Ye,r.loc)&&e.children.length&&(r.value={type:2,content:Dt(e.children[0].loc.start.offset,e.children[e.children.length-1].loc.end.offset),loc:r.loc})}}function bx(e,t){let s=e;for(;yn.charCodeAt(s)!==t&&s=0;)s--;return s}const yx=new Set(["if","else","else-if","for","slot"]);function nu({tag:e,props:t}){if(e==="template"){for(let s=0;s64&&e<91}const kx=/\r\n/g;function Ih(e){const t=Ye.whitespace!=="preserve";let s=!1;for(let n=0;ns.type!==3);return t.length===1&&t[0].type===1&&!tr(t[0])?t[0]:null}function Rl(e,t,s,n=!1,a=!1){const{children:i}=e,l=[];for(let d=0;d0){if(f>=2){u.codegenNode.patchFlag=-1,l.push(u);continue}}else{const p=u.codegenNode;if(p.type===13){const b=p.patchFlag;if((b===void 0||b===512||b===1)&&Dh(u,s)>=2){const y=Mh(u);y&&(p.props=s.hoist(y))}p.dynamicProps&&(p.dynamicProps=s.hoist(p.dynamicProps))}}}else if(u.type===12&&(n?0:hs(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++,Rl(u,e,s,!1,a),f&&s.scopes.vSlot--}else if(u.type===11)Rl(u,e,s,u.children.length===1,!0);else if(u.type===9)for(let f=0;fp.key===u||p.key.content===u);return f&&f.value}}l.length&&s.transformHoist&&s.transformHoist(i,s,e)}function hs(e,t){const{constantCache:s}=t;switch(e.type){case 1:if(e.tagType!==0)return 0;const n=s.get(e);if(n!==void 0)return n;const a=e.codegenNode;if(a.type!==13||a.isBlock&&e.tag!=="svg"&&e.tag!=="foreignObject"&&e.tag!=="math")return 0;if(a.patchFlag===void 0){let l=3;const r=Dh(e,t);if(r===0)return s.set(e,0),0;r1)for(let o=0;oB&&(T.childIndex--,T.onNodeRemoved()),T.parent.children.splice(B,1)},onNodeRemoved:Ft,addIdentifiers(C){},removeIdentifiers(C){},hoist(C){Me(C)&&(C=Pe(C)),T.hoists.push(C);const M=Pe(`_hoisted_${T.hoists.length}`,!1,C.loc,2);return M.hoisted=C,M},cache(C,M=!1,B=!1){const $=tx(T.cached.length,C,M,B);return T.cached.push($),$}};return T.filters=new Set,T}function Ox(e,t){const s=Ix(e,t);Rr(e,s),t.hoistStatic&&Ax(e,s),t.ssr||Nx(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 Nx(e,t){const{helper:s}=t,{children:n}=e;if(n.length===1){const a=Nh(e);if(a&&a.codegenNode){const i=a.codegenNode;i.type===13&&Uc(i,t),e.codegenNode=i}else e.codegenNode=n[0]}else if(n.length>1){let a=64;e.codegenNode=Ui(t,s(Fi),void 0,e.children,a,void 0,void 0,!0,void 0,!1)}}function Lx(e,t){let s=0;const n=()=>{s--};for(;sn===e:n=>e.test(n);return(n,a)=>{if(n.type===1){const{props:i}=n;if(n.tagType===3&&i.some(fx))return;const l=[];for(let r=0;r`${Va[e]}: _${Va[e]}`;function Dx(e,{mode:t="function",prefixIdentifiers:s=t==="module",sourceMap:n=!1,filename:a="template.vue.html",scopeId:i=null,optimizeImports:l=!1,runtimeGlobalName:r="Vue",runtimeModuleName:o="vue",ssrRuntimeModuleName:c="vue/server-renderer",ssr:d=!1,isTS:u=!1,inSSR:f=!1}){const p={mode:t,prefixIdentifiers:s,sourceMap:n,filename:a,scopeId:i,optimizeImports:l,runtimeGlobalName:r,runtimeModuleName:o,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(y){return`_${Va[y]}`},push(y,A=-2,O){p.code+=y},indent(){b(++p.indentLevel)},deindent(y=!1){y?--p.indentLevel:b(--p.indentLevel)},newline(){b(p.indentLevel)}};function b(y){p.push(` -`+" ".repeat(y),0)}return p}function Mx(e,t={}){const s=Dx(e,t);t.onContextCreated&&t.onContextCreated(s);const{mode:n,push:a,prefixIdentifiers:i,indent:l,deindent:r,newline:o,scopeId:c,ssr:d}=s,u=Array.from(e.helpers),f=u.length>0,p=!i&&n!=="module";Px(e,s);const y=d?"ssrRender":"render",O=(d?["_ctx","_push","_parent","_attrs"]:["_ctx","_cache"]).join(", ");if(a(`function ${y}(${O}) {`),l(),p&&(a("with (_ctx) {"),l(),f&&(a(`const { ${u.map(Fh).join(", ")} } = _Vue +**/const Vi=Symbol(""),Ei=Symbol(""),Sc=Symbol(""),Yl=Symbol(""),ph=Symbol(""),ra=Symbol(""),hh=Symbol(""),mh=Symbol(""),Tc=Symbol(""),Cc=Symbol(""),nl=Symbol(""),Ec=Symbol(""),gh=Symbol(""),Ac=Symbol(""),Rc=Symbol(""),Ic=Symbol(""),Oc=Symbol(""),Nc=Symbol(""),Lc=Symbol(""),vh=Symbol(""),bh=Symbol(""),Ar=Symbol(""),Ql=Symbol(""),Dc=Symbol(""),Mc=Symbol(""),ji=Symbol(""),al=Symbol(""),Pc=Symbol(""),No=Symbol(""),Wy=Symbol(""),Lo=Symbol(""),Xl=Symbol(""),Zy=Symbol(""),Jy=Symbol(""),Fc=Symbol(""),Yy=Symbol(""),Qy=Symbol(""),$c=Symbol(""),yh=Symbol(""),Ka={[Vi]:"Fragment",[Ei]:"Teleport",[Sc]:"Suspense",[Yl]:"KeepAlive",[ph]:"BaseTransition",[ra]:"openBlock",[hh]:"createBlock",[mh]:"createElementBlock",[Tc]:"createVNode",[Cc]:"createElementVNode",[nl]:"createCommentVNode",[Ec]:"createTextVNode",[gh]:"createStaticVNode",[Ac]:"resolveComponent",[Rc]:"resolveDynamicComponent",[Ic]:"resolveDirective",[Oc]:"resolveFilter",[Nc]:"withDirectives",[Lc]:"renderList",[vh]:"renderSlot",[bh]:"createSlots",[Ar]:"toDisplayString",[Ql]:"mergeProps",[Dc]:"normalizeClass",[Mc]:"normalizeStyle",[ji]:"normalizeProps",[al]:"guardReactiveProps",[Pc]:"toHandlers",[No]:"camelize",[Wy]:"capitalize",[Lo]:"toHandlerKey",[Xl]:"setBlockTracking",[Zy]:"pushScopeId",[Jy]:"popScopeId",[Fc]:"withCtx",[Yy]:"unref",[Qy]:"isRef",[$c]:"withMemo",[yh]:"isMemoSame"};function Xy(e){Object.getOwnPropertySymbols(e).forEach(t=>{Ka[t]=e[t]})}const ws={start:{line:1,column:1,offset:0},end:{line:1,column:1,offset:0},source:""};function ex(e,t=""){return{type:0,source:t,children:e,helpers:new Set,components:[],directives:[],hoists:[],imports:[],cached:[],temps:0,codegenNode:void 0,loc:ws}}function qi(e,t,s,n,a,i,l,r=!1,o=!1,c=!1,d=ws){return e&&(r?(e.helper(ra),e.helper(Ja(e.inSSR,c))):e.helper(Za(e.inSSR,c)),l&&e.helper(Nc)),{type:13,tag:t,props:s,children:n,patchFlag:a,dynamicProps:i,directives:l,isBlock:r,disableTracking:o,isComponent:c,loc:d}}function na(e,t=ws){return{type:17,loc:t,elements:e}}function Is(e,t=ws){return{type:15,loc:t,properties:e}}function wt(e,t){return{type:16,loc:ws,key:Me(e)?Fe(e,!0):e,value:t}}function Fe(e,t=!1,s=ws,n=0){return{type:4,loc:s,content:e,isStatic:t,constType:t?3:n}}function Vs(e,t=ws){return{type:8,loc:t,children:e}}function It(e,t=[],s=ws){return{type:14,loc:s,callee:e,arguments:t}}function Wa(e,t=void 0,s=!1,n=!1,a=ws){return{type:18,params:e,returns:t,newline:s,isSlot:n,loc:a}}function Do(e,t,s,n=!0){return{type:19,test:e,consequent:t,alternate:s,newline:n,loc:ws}}function tx(e,t,s=!1,n=!1){return{type:20,index:e,value:t,needPauseTracking:s,inVOnce:n,needArraySpread:!1,loc:ws}}function sx(e){return{type:21,body:e,loc:ws}}function Za(e,t){return e||t?Tc:Cc}function Ja(e,t){return e||t?hh:mh}function Bc(e,{helper:t,removeHelper:s,inSSR:n}){e.isBlock||(e.isBlock=!0,s(Za(n,e.isComponent)),t(ra),t(Ja(n,e.isComponent)))}const Zd=new Uint8Array([123,123]),Jd=new Uint8Array([125,125]);function Yd(e){return e>=97&&e<=122||e>=65&&e<=90}function bs(e){return e===32||e===10||e===9||e===12||e===13}function In(e){return e===47||e===62||bs(e)}function er(e){const t=new Uint8Array(e.length);for(let s=0;s100){let l=-1,r=a;for(;l+1>>1;this.newlines[o]=0;l--)if(t>this.newlines[l]){i=l;break}return i>=0&&(s=i+2,n=t-this.newlines[i]),{column:n,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?In(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||bs(t)){const s=this.index-this.currentSequence.length;if(this.sectionStart=t||(this.state===28?this.currentSequence===jt.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 Qd(e,{compatConfig:t}){const s=t&&t[e];return e==="MODE"?s||3:s}function aa(e,t){const s=Qd("MODE",t),n=Qd(e,t);return s===3?n===!0:n!==!1}function Gi(e,t,s,...n){return aa(e,t)}function Uc(e){throw e}function xh(e){}function ut(e,t,s,n){const a=`https://vuejs.org/error-reference/#compiler-${e}`,i=new SyntaxError(String(a));return i.code=e,i.loc=t,i}const hs=e=>e.type===4&&e.isStatic;function _h(e){switch(e){case"Teleport":case"teleport":return Ei;case"Suspense":case"suspense":return Sc;case"KeepAlive":case"keep-alive":return Yl;case"BaseTransition":case"base-transition":return ph}}const ax=/^$|^\d|[^\$\w\xA0-\uFFFF]/,Hc=e=>!ax.test(e),kh=/[A-Za-z_$\xA0-\uFFFF]/,ix=/[\.\?\w$\xA0-\uFFFF]/,lx=/\s+[.[]\s*|\s*[.[]\s+/g,wh=e=>e.type===4?e.content:e.loc.source,rx=e=>{const t=wh(e).trim().replace(lx,r=>r.trim());let s=0,n=[],a=0,i=0,l=null;for(let r=0;r|^\s*(?:async\s+)?function(?:\s+[\w$]+)?\s*\(/,cx=e=>ox.test(wh(e)),dx=cx;function Rs(e,t,s=!1){for(let n=0;nt.type===7&&t.name==="bind"&&(!t.arg||t.arg.type!==4||!t.arg.isStatic))}function Yr(e){return e.type===5||e.type===2}function Xd(e){return e.type===7&&e.name==="pre"}function fx(e){return e.type===7&&e.name==="slot"}function tr(e){return e.type===1&&e.tagType===3}function sr(e){return e.type===1&&e.tagType===2}const px=new Set([ji,al]);function Th(e,t=[]){if(e&&!Me(e)&&e.type===14){const s=e.callee;if(!Me(s)&&px.has(s))return Th(e.arguments[0],t.concat(e))}return[e,t]}function nr(e,t,s){let n,a=e.type===13?e.props:e.arguments[2],i=[],l;if(a&&!Me(a)&&a.type===14){const r=Th(a);a=r[0],i=r[1],l=i[i.length-1]}if(a==null||Me(a))n=Is([t]);else if(a.type===14){const r=a.arguments[0];!Me(r)&&r.type===15?eu(t,r)||r.properties.unshift(t):a.callee===Pc?n=It(s.helper(Ql),[Is([t]),a]):a.arguments.unshift(Is([t])),!n&&(n=a)}else a.type===15?(eu(t,a)||a.properties.unshift(t),n=a):(n=It(s.helper(Ql),[Is([t]),a]),l&&l.callee===al&&(l=i[i.length-2]));e.type===13?l?l.arguments[0]=n:e.props=n:l?l.arguments[0]=n:e.arguments[2]=n}function eu(e,t){let s=!1;if(e.key.type===4){const n=e.key.content;s=t.properties.some(a=>a.key.type===4&&a.key.content===n)}return s}function Ki(e,t){return`_${t}_${e.replace(/[^\w]/g,(s,n)=>s==="-"?"_":e.charCodeAt(n).toString())}`}function hx(e){return e.type===14&&e.callee===$c?e.arguments[1].returns:e}const mx=/([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/;function Ch(e){for(let t=0;t0,isVoidTag:Ia,isPreTag:Ia,isIgnoreNewlineTag:Ia,isCustomElement:Ia,onError:Uc,onWarn:xh,comments:!1,prefixIdentifiers:!1};let Qe=Ah,Wi=null,_n="",Gt=null,Ge=null,cs="",cn=-1,Wn=-1,Vc=0,Pn=!1,Mo=null;const dt=[],gt=new nx(dt,{onerr:ln,ontext(e,t){yl($t(e,t),e,t)},ontextentity(e,t,s){yl(e,t,s)},oninterpolation(e,t){if(Pn)return yl($t(e,t),e,t);let s=e+gt.delimiterOpen.length,n=t-gt.delimiterClose.length;for(;bs(_n.charCodeAt(s));)s++;for(;bs(_n.charCodeAt(n-1));)n--;let a=$t(s,n);a.includes("&")&&(a=Qe.decodeEntities(a,!1)),Po({type:5,content:Rl(a,!1,bt(s,n)),loc:bt(e,t)})},onopentagname(e,t){const s=$t(e,t);Gt={type:1,tag:s,ns:Qe.getNamespace(s,dt[0],Qe.ns),tagType:0,props:[],children:[],loc:bt(e-1,t),codegenNode:void 0}},onopentagend(e){su(e)},onclosetag(e,t){const s=$t(e,t);if(!Qe.isVoidTag(s)){let n=!1;for(let a=0;a0&&ln(24,dt[0].loc.start.offset);for(let l=0;l<=a;l++){const r=dt.shift();Al(r,t,l(n.type===7?n.rawName:n.name)===s)&&ln(2,t)},onattribend(e,t){if(Gt&&Ge){if(Qn(Ge.loc,t),e!==0)if(cs.includes("&")&&(cs=Qe.decodeEntities(cs,!0)),Ge.type===6)Ge.name==="class"&&(cs=Oh(cs).trim()),e===1&&!cs&&ln(13,t),Ge.value={type:2,content:cs,loc:e===1?bt(cn,Wn):bt(cn-1,Wn+1)},gt.inSFCRoot&&Gt.tag==="template"&&Ge.name==="lang"&&cs&&cs!=="html"&>.enterRCDATA(er("a.content==="sync"))>-1&&Gi("COMPILER_V_BIND_SYNC",Qe,Ge.loc,Ge.arg.loc.source)&&(Ge.name="model",Ge.modifiers.splice(n,1))}(Ge.type!==7||Ge.name!=="pre")&&Gt.props.push(Ge)}cs="",cn=Wn=-1},oncomment(e,t){Qe.comments&&Po({type:3,content:$t(e,t),loc:bt(e-4,t+3)})},onend(){const e=_n.length;for(let t=0;t{const b=t.start.offset+f,y=b+u.length;return Rl(u,!1,bt(b,y),0,p?1:0)},r={source:l(i.trim(),s.indexOf(i,a.length)),value:void 0,key:void 0,index:void 0,finalized:!1};let o=a.trim().replace(gx,"").trim();const c=a.indexOf(o),d=o.match(tu);if(d){o=o.replace(tu,"").trim();const u=d[1].trim();let f;if(u&&(f=s.indexOf(u,c+o.length),r.key=l(u,f,!0)),d[2]){const p=d[2].trim();p&&(r.index=l(p,s.indexOf(p,r.key?f+u.length:c+o.length),!0))}}return o&&(r.value=l(o,c,!0)),r}function $t(e,t){return _n.slice(e,t)}function su(e){gt.inSFCRoot&&(Gt.innerLoc=bt(e+1,e+1)),Po(Gt);const{tag:t,ns:s}=Gt;s===0&&Qe.isPreTag(t)&&Vc++,Qe.isVoidTag(t)?Al(Gt,e):(dt.unshift(Gt),(s===1||s===2)&&(gt.inXML=!0)),Gt=null}function yl(e,t,s){{const i=dt[0]&&dt[0].tag;i!=="script"&&i!=="style"&&e.includes("&")&&(e=Qe.decodeEntities(e,!1))}const n=dt[0]||Wi,a=n.children[n.children.length-1];a&&a.type===2?(a.content+=e,Qn(a.loc,s)):n.children.push({type:2,content:e,loc:bt(t,s)})}function Al(e,t,s=!1){s?Qn(e.loc,Rh(t,60)):Qn(e.loc,bx(t,62)+1),gt.inSFCRoot&&(e.children.length?e.innerLoc.end=ze({},e.children[e.children.length-1].loc.end):e.innerLoc.end=ze({},e.innerLoc.start),e.innerLoc.source=$t(e.innerLoc.start.offset,e.innerLoc.end.offset));const{tag:n,ns:a,children:i}=e;if(Pn||(n==="slot"?e.tagType=2:nu(e)?e.tagType=3:xx(e)&&(e.tagType=1)),gt.inRCDATA||(e.children=Ih(i)),a===0&&Qe.isIgnoreNewlineTag(n)){const l=i[0];l&&l.type===2&&(l.content=l.content.replace(/^\r?\n/,""))}a===0&&Qe.isPreTag(n)&&Vc--,Mo===e&&(Pn=gt.inVPre=!1,Mo=null),gt.inXML&&(dt[0]?dt[0].ns:Qe.ns)===0&&(gt.inXML=!1);{const l=e.props;if(!gt.inSFCRoot&&aa("COMPILER_NATIVE_TEMPLATE",Qe)&&e.tag==="template"&&!nu(e)){const o=dt[0]||Wi,c=o.children.indexOf(e);o.children.splice(c,1,...e.children)}const r=l.find(o=>o.type===6&&o.name==="inline-template");r&&Gi("COMPILER_INLINE_TEMPLATE",Qe,r.loc)&&e.children.length&&(r.value={type:2,content:$t(e.children[0].loc.start.offset,e.children[e.children.length-1].loc.end.offset),loc:r.loc})}}function bx(e,t){let s=e;for(;_n.charCodeAt(s)!==t&&s<_n.length-1;)s++;return s}function Rh(e,t){let s=e;for(;_n.charCodeAt(s)!==t&&s>=0;)s--;return s}const yx=new Set(["if","else","else-if","for","slot"]);function nu({tag:e,props:t}){if(e==="template"){for(let s=0;s64&&e<91}const kx=/\r\n/g;function Ih(e){const t=Qe.whitespace!=="preserve";let s=!1;for(let n=0;ns.type!==3);return t.length===1&&t[0].type===1&&!sr(t[0])?t[0]:null}function Il(e,t,s,n=!1,a=!1){const{children:i}=e,l=[];for(let d=0;d0){if(f>=2){u.codegenNode.patchFlag=-1,l.push(u);continue}}else{const p=u.codegenNode;if(p.type===13){const b=p.patchFlag;if((b===void 0||b===512||b===1)&&Dh(u,s)>=2){const y=Mh(u);y&&(p.props=s.hoist(y))}p.dynamicProps&&(p.dynamicProps=s.hoist(p.dynamicProps))}}}else if(u.type===12&&(n?0:ys(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++,Il(u,e,s,!1,a),f&&s.scopes.vSlot--}else if(u.type===11)Il(u,e,s,u.children.length===1,!0);else if(u.type===9)for(let f=0;fp.key===u||p.key.content===u);return f&&f.value}}l.length&&s.transformHoist&&s.transformHoist(i,s,e)}function ys(e,t){const{constantCache:s}=t;switch(e.type){case 1:if(e.tagType!==0)return 0;const n=s.get(e);if(n!==void 0)return n;const a=e.codegenNode;if(a.type!==13||a.isBlock&&e.tag!=="svg"&&e.tag!=="foreignObject"&&e.tag!=="math")return 0;if(a.patchFlag===void 0){let l=3;const r=Dh(e,t);if(r===0)return s.set(e,0),0;r1)for(let o=0;oH&&(T.childIndex--,T.onNodeRemoved()),T.parent.children.splice(H,1)},onNodeRemoved:Ht,addIdentifiers(C){},removeIdentifiers(C){},hoist(C){Me(C)&&(C=Fe(C)),T.hoists.push(C);const M=Fe(`_hoisted_${T.hoists.length}`,!1,C.loc,2);return M.hoisted=C,M},cache(C,M=!1,H=!1){const P=tx(T.cached.length,C,M,H);return T.cached.push(P),P}};return T.filters=new Set,T}function Ox(e,t){const s=Ix(e,t);Ir(e,s),t.hoistStatic&&Ax(e,s),t.ssr||Nx(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 Nx(e,t){const{helper:s}=t,{children:n}=e;if(n.length===1){const a=Nh(e);if(a&&a.codegenNode){const i=a.codegenNode;i.type===13&&Bc(i,t),e.codegenNode=i}else e.codegenNode=n[0]}else if(n.length>1){let a=64;e.codegenNode=qi(t,s(Vi),void 0,e.children,a,void 0,void 0,!0,void 0,!1)}}function Lx(e,t){let s=0;const n=()=>{s--};for(;sn===e:n=>e.test(n);return(n,a)=>{if(n.type===1){const{props:i}=n;if(n.tagType===3&&i.some(fx))return;const l=[];for(let r=0;r`${Ka[e]}: _${Ka[e]}`;function Dx(e,{mode:t="function",prefixIdentifiers:s=t==="module",sourceMap:n=!1,filename:a="template.vue.html",scopeId:i=null,optimizeImports:l=!1,runtimeGlobalName:r="Vue",runtimeModuleName:o="vue",ssrRuntimeModuleName:c="vue/server-renderer",ssr:d=!1,isTS:u=!1,inSSR:f=!1}){const p={mode:t,prefixIdentifiers:s,sourceMap:n,filename:a,scopeId:i,optimizeImports:l,runtimeGlobalName:r,runtimeModuleName:o,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(y){return`_${Ka[y]}`},push(y,E=-2,I){p.code+=y},indent(){b(++p.indentLevel)},deindent(y=!1){y?--p.indentLevel:b(--p.indentLevel)},newline(){b(p.indentLevel)}};function b(y){p.push(` +`+" ".repeat(y),0)}return p}function Mx(e,t={}){const s=Dx(e,t);t.onContextCreated&&t.onContextCreated(s);const{mode:n,push:a,prefixIdentifiers:i,indent:l,deindent:r,newline:o,scopeId:c,ssr:d}=s,u=Array.from(e.helpers),f=u.length>0,p=!i&&n!=="module";Px(e,s);const y=d?"ssrRender":"render",I=(d?["_ctx","_push","_parent","_attrs"]:["_ctx","_cache"]).join(", ");if(a(`function ${y}(${I}) {`),l(),p&&(a("with (_ctx) {"),l(),f&&(a(`const { ${u.map(Fh).join(", ")} } = _Vue `,-1),o())),e.components.length&&(Qr(e.components,"component",s),(e.directives.length||e.temps>0)&&o()),e.directives.length&&(Qr(e.directives,"directive",s),e.temps>0&&o()),e.filters&&e.filters.length&&(o(),Qr(e.filters,"filter",s),o()),e.temps>0){a("let ");for(let x=0;x0?", ":""}_temp${x}`)}return(e.components.length||e.directives.length||e.temps)&&(a(` -`,0),o()),d||a("return "),e.codegenNode?qt(e.codegenNode,s):a("null"),p&&(r(),a("}")),r(),a("}"),{ast:e,code:s.code,preamble:"",map:s.map?s.map.toJSON():void 0}}function Px(e,t){const{ssr:s,prefixIdentifiers:n,push:a,newline:i,runtimeModuleName:l,runtimeGlobalName:r,ssrRuntimeModuleName:o}=t,c=r,d=Array.from(e.helpers);if(d.length>0&&(a(`const _Vue = ${c} -`,-1),e.hoists.length)){const u=[Tc,Cc,Yi,Ec,gh].filter(f=>d.includes(f)).map(Fh).join(", ");a(`const { ${u} } = _Vue -`,-1)}Fx(e.hoists,t),i(),a("return ")}function Qr(e,t,{helper:s,push:n,newline:a,isTS:i}){const l=s(t==="filter"?Oc:t==="component"?Ac:Ic);for(let r=0;r3||!1;t.push("["),s&&t.indent(),Xi(e,t,s),s&&t.deindent(),t.push("]")}function Xi(e,t,s=!1,n=!0){const{push:a,newline:i}=t;for(let l=0;ls||"null")}function zx(e,t){const{push:s,helper:n,pure:a}=t,i=Me(e.callee)?e.callee:n(e.callee);a&&s(Ir),s(i+"(",-2,e),Xi(e.arguments,t),s(")")}function qx(e,t){const{push:s,indent:n,deindent:a,newline:i}=t,{properties:l}=e;if(!l.length){s("{}",-2,e);return}const r=l.length>1||!1;s(r?"{":"{ "),r&&n();for(let o=0;o "),(o||r)&&(s("{"),n()),l?(o&&s("return "),ge(l)?zc(l,t):qt(l,t)):r&&qt(r,t),(o||r)&&(a(),s("}")),c&&(e.isNonScopedSlot&&s(", undefined, true"),s(")"))}function Wx(e,t){const{test:s,consequent:n,alternate:a,newline:i}=e,{push:l,indent:r,deindent:o,newline:c}=t;if(s.type===4){const u=!Hc(s.content);u&&l("("),$h(s,t),u&&l(")")}else l("("),qt(s,t),l(")");i&&r(),t.indentLevel++,i||l(" "),l("? "),qt(n,t),t.indentLevel--,i&&c(),i||l(" "),l(": ");const d=a.type===19;d||t.indentLevel++,qt(a,t),d||t.indentLevel--,i&&o(!0)}function Zx(e,t){const{push:s,helper:n,indent:a,deindent:i,newline:l}=t,{needPauseTracking:r,needArraySpread:o}=e;o&&s("[...("),s(`_cache[${e.index}] || (`),r&&(a(),s(`${n(Ql)}(-1`),e.inVOnce&&s(", true"),s("),"),l(),s("(")),s(`_cache[${e.index}] = `),qt(e.value,t),r&&(s(`).cacheIndex = ${e.index},`),l(),s(`${n(Ql)}(1),`),l(),s(`_cache[${e.index}]`),i()),s(")"),o&&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 Jx=Ph(/^(?:if|else|else-if)$/,(e,t,s)=>Yx(e,t,s,(n,a,i)=>{const l=s.parent.children;let r=l.indexOf(n),o=0;for(;r-->=0;){const c=l[r];c&&c.type===9&&(o+=c.branches.length)}return()=>{if(i)n.codegenNode=iu(a,o,s);else{const c=Qx(n.codegenNode);c.alternate=iu(a,o+n.branches.length-1,s)}}}));function Yx(e,t,s,n){if(t.name!=="else"&&(!t.exp||!t.exp.content.trim())){const a=t.exp?t.exp.loc:e.loc;s.onError(ut(28,t.loc)),t.exp=Pe("true",!1,a)}if(t.name==="if"){const a=au(e,t),i={type:9,loc:Sx(e.loc),branches:[a]};if(s.replaceNode(i),n)return n(i,a,!0)}else{const a=s.parent.children;let i=a.indexOf(e);for(;i-->=-1;){const l=a[i];if(l&&Eh(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(ut(30,e.loc)),s.removeNode();const r=au(e,t);l.branches.push(r);const o=n&&n(l,r,!1);Rr(r,s),o&&o(),s.currentNode=null}else s.onError(ut(30,e.loc));break}}}function au(e,t){const s=e.tagType===3;return{type:10,loc:e.loc,condition:t.name==="else"?void 0:t.exp,children:s&&!_s(e,"for")?e.children:[e],userKey:Ar(e,"key"),isTemplateIf:s}}function iu(e,t,s){return e.condition?Do(e.condition,lu(e,t,s),Rt(s.helper(Yi),['""',"true"])):lu(e,t,s)}function lu(e,t,s){const{helper:n}=s,a=wt("key",Pe(`${t}`,!1,bs,2)),{children:i}=e,l=i[0];if(i.length!==1||l.type!==1)if(i.length===1&&l.type===11){const o=l.codegenNode;return sr(o,a,s),o}else return Ui(s,n(Fi),ks([a]),i,64,void 0,void 0,!0,!1,!1,e.loc);else{const o=l.codegenNode,c=hx(o);return c.type===13&&Uc(c,s),sr(c,a,s),o}}function Qx(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 Xx=Ph("for",(e,t,s)=>{const{helper:n,removeHelper:a}=s;return e0(e,t,s,i=>{const l=Rt(n(Lc),[i.source]),r=er(e),o=_s(e,"memo"),c=Ar(e,"key",!1,!0);c&&c.type;let d=c&&(c.type===6?c.value?Pe(c.value.content,!0):void 0:c.exp);const u=d?wt("key",d):null,f=i.source.type===4&&i.source.constType>0,p=f?64:c?128:256;return i.codegenNode=Ui(s,n(Fi),void 0,l,p,void 0,void 0,!0,!f,!1,e.loc),()=>{let b;const{children:y}=i,A=y.length!==1||y[0].type!==1,O=tr(e)?e:r&&e.children.length===1&&tr(e.children[0])?e.children[0]:null;if(O?(b=O.codegenNode,r&&u&&sr(b,u,s)):A?b=Ui(s,n(Fi),u?ks([u]):void 0,e.children,64,void 0,void 0,!0,void 0,!1):(b=y[0].codegenNode,r&&u&&sr(b,u,s),b.isBlock!==!f&&(b.isBlock?(a(ia),a(qa(s.inSSR,b.isComponent))):a(za(s.inSSR,b.isComponent))),b.isBlock=!f,b.isBlock?(n(ia),n(qa(s.inSSR,b.isComponent))):n(za(s.inSSR,b.isComponent))),o){const x=ja(Fo(i.parseResult,[Pe("_cached")]));x.body=sx([Ms(["const _memo = (",o.exp,")"]),Ms(["if (_cached && _cached.el",...d?[" && _cached.key === ",d]:[],` && ${s.helperString(yh)}(_cached, _memo)) return _cached`]),Ms(["const _item = ",b]),Pe("_item.memo = _memo"),Pe("return _item")]),l.arguments.push(x,Pe("_cache"),Pe(String(s.cached.length))),s.cached.push(null)}else l.arguments.push(ja(Fo(i.parseResult),b,!0))}})});function e0(e,t,s,n){if(!t.exp){s.onError(ut(31,t.loc));return}const a=t.forParseResult;if(!a){s.onError(ut(32,t.loc));return}Bh(a);const{addIdentifiers:i,removeIdentifiers:l,scopes:r}=s,{source:o,value:c,key:d,index:u}=a,f={type:11,loc:t.loc,source:o,valueAlias:c,keyAlias:d,objectIndexAlias:u,parseResult:a,children:er(e)?e.children:[e]};s.replaceNode(f),r.vFor++;const p=n&&n(f);return()=>{r.vFor--,p&&p()}}function Bh(e,t){e.finalized||(e.finalized=!0)}function Fo({value:e,key:t,index:s},n=[]){return t0([e,t,s,...n])}function t0(e){let t=e.length;for(;t--&&!e[t];);return e.slice(0,t+1).map((s,n)=>s||Pe("_".repeat(n+1),!1))}const ru=Pe("undefined",!1),s0=(e,t)=>{if(e.type===1&&(e.tagType===1||e.tagType===3)){const s=_s(e,"slot");if(s)return s.exp,t.scopes.vSlot++,()=>{t.scopes.vSlot--}}},n0=(e,t,s,n)=>ja(e,s,!1,!0,s.length?s[0].loc:n);function a0(e,t,s=n0){t.helper(Fc);const{children:n,loc:a}=e,i=[],l=[];let r=t.scopes.vSlot>0||t.scopes.vFor>0;const o=_s(e,"slot",!0);if(o){const{arg:A,exp:O}=o;A&&!cs(A)&&(r=!0),i.push(wt(A||Pe("default",!0),s(O,void 0,n,a)))}let c=!1,d=!1;const u=[],f=new Set;let p=0;for(let A=0;A{const m=s(O,void 0,x,a);return t.compatConfig&&(m.isNonScopedSlot=!0),wt("default",m)};c?u.length&&!u.every(Vc)&&(d?t.onError(ut(39,u[0].loc)):i.push(A(void 0,u))):i.push(A(void 0,n))}const b=r?2:Il(e.children)?3:1;let y=ks(i.concat(wt("_",Pe(b+"",!1))),a);return l.length&&(y=Rt(t.helper(bh),[y,ta(l)])),{slots:y,hasDynamicSlots:r}}function yl(e,t,s){const n=[wt("name",e),wt("fn",t)];return s!=null&&n.push(wt("key",Pe(String(s),!0))),ks(n)}function Il(e){for(let t=0;tfunction(){if(e=t.currentNode,!(e.type===1&&(e.tagType===0||e.tagType===1)))return;const{tag:n,props:a}=e,i=e.tagType===1;let l=i?l0(e,t):`"${n}"`;const r=Qe(l)&&l.callee===Rc;let o,c,d=0,u,f,p,b=r||l===_i||l===Sc||!i&&(n==="svg"||n==="foreignObject"||n==="math");if(a.length>0){const y=Vh(e,t,void 0,i,r);o=y.props,d=y.patchFlag,f=y.dynamicPropNames;const A=y.directives;p=A&&A.length?ta(A.map(O=>o0(O,t))):void 0,y.shouldUseBlock&&(b=!0)}if(e.children.length>0)if(l===Jl&&(b=!0,d|=1024),i&&l!==_i&&l!==Jl){const{slots:A,hasDynamicSlots:O}=a0(e,t);c=A,O&&(d|=1024)}else if(e.children.length===1&&l!==_i){const A=e.children[0],O=A.type,x=O===5||O===8;x&&hs(A,t)===0&&(d|=1),x||O===2?c=A:c=e.children}else c=e.children;f&&f.length&&(u=c0(f)),e.codegenNode=Ui(t,l,o,c,d===0?void 0:d,u,p,!!b,!1,i,e.loc)};function l0(e,t,s=!1){let{tag:n}=e;const a=$o(n),i=Ar(e,"is",!1,!0);if(i)if(a||sa("COMPILER_IS_ON_ELEMENT",t)){let r;if(i.type===6?r=i.value&&Pe(i.value.content,!0):(r=i.exp,r||(r=Pe("is",!1,i.arg.loc))),r)return Rt(t.helper(Rc),[r])}else i.type===6&&i.value.content.startsWith("vue:")&&(n=i.value.content.slice(4));const l=_h(n)||t.isBuiltInComponent(n);return l?(s||t.helper(l),l):(t.helper(Ac),t.components.add(n),Hi(n,"component"))}function Vh(e,t,s=e.props,n,a,i=!1){const{tag:l,loc:r,children:o}=e;let c=[];const d=[],u=[],f=o.length>0;let p=!1,b=0,y=!1,A=!1,O=!1,x=!1,m=!1,_=!1;const S=[],g=M=>{c.length&&(d.push(ks(ou(c),r)),c=[]),M&&d.push(M)},w=()=>{t.scopes.vFor>0&&c.push(wt(Pe("ref_for",!0),Pe("true")))},T=({key:M,value:B})=>{if(cs(M)){const $=M.content,I=ra($);if(I&&(!n||a)&&$.toLowerCase()!=="onclick"&&$!=="onUpdate:modelValue"&&!gn($)&&(x=!0),I&&gn($)&&(_=!0),I&&B.type===14&&(B=B.arguments[0]),B.type===20||(B.type===4||B.type===8)&&hs(B,t)>0)return;$==="ref"?y=!0:$==="class"?A=!0:$==="style"?O=!0:$!=="key"&&!S.includes($)&&S.push($),n&&($==="class"||$==="style")&&!S.includes($)&&S.push($)}else m=!0};for(let M=0;Mxe.content==="prop")&&(b|=32);const Z=t.directiveTransforms[$];if(Z){const{props:xe,needRuntime:_e}=Z(B,e,t);!i&&xe.forEach(T),L&&I&&!cs(I)?g(ks(xe,r)):c.push(...xe),_e&&(u.push(B),Gt(_e)&&Hh.set(B,_e))}else Xm($)||(u.push(B),f&&(p=!0))}}let C;if(d.length?(g(),d.length>1?C=Rt(t.helper(Yl),d,r):C=d[0]):c.length&&(C=ks(ou(c),r)),m?b|=16:(A&&!n&&(b|=2),O&&!n&&(b|=4),S.length&&(b|=8),x&&(b|=32)),!p&&(b===0||b===32)&&(y||_||u.length>0)&&(b|=512),!t.inSSR&&C)switch(C.type){case 15:let M=-1,B=-1,$=!1;for(let Y=0;Ywt(l,i)),a))}return ta(s,e.loc)}function c0(e){let t="[";for(let s=0,n=e.length;s{if(tr(e)){const{children:s,loc:n}=e,{slotName:a,slotProps:i}=u0(e,t),l=[t.prefixIdentifiers?"_ctx.$slots":"$slots",a,"{}","undefined","true"];let r=2;i&&(l[2]=i,r=3),s.length&&(l[3]=ja([],s,!1,!1,n),r=4),t.scopeId&&!t.slotted&&(r=5),l.splice(r),e.codegenNode=Rt(t.helper(vh),l,n)}};function u0(e,t){let s='"default"',n;const a=[];for(let i=0;i0){const{props:i,directives:l}=Vh(e,t,a,!1,!1);n=i,l.length&&t.onError(ut(36,l[0].loc))}return{slotName:s,slotProps:n}}const jh=(e,t,s,n)=>{const{loc:a,modifiers:i,arg:l}=e;!e.exp&&!i.length&&s.onError(ut(35,a));let r;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)?Ra(at(u)):`on:${u}`;r=Pe(f,!0,l.loc)}else r=Ms([`${s.helperString(Lo)}(`,l,")"]);else r=l,r.children.unshift(`${s.helperString(Lo)}(`),r.children.push(")");let o=e.exp;o&&!o.content.trim()&&(o=void 0);let c=s.cacheHandlers&&!o&&!s.inVOnce;if(o){const u=Sh(o),f=!(u||dx(o)),p=o.content.includes(";");(f||c&&u)&&(o=Ms([`${f?"$event":"(...args)"} => ${p?"{":"("}`,o,p?"}":")"]))}let d={props:[wt(r,o||Pe("() => {}",!1,a))]};return n&&(d=n(d)),c&&(d.props[0].value=s.cache(d.props[0].value)),d.props.forEach(u=>u.key.isHandlerKey=!0),d},f0=(e,t,s)=>{const{modifiers:n,loc:a}=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} || ""`:'""'),n.some(r=>r.content==="camel")&&(i.type===4?i.isStatic?i.content=at(i.content):i.content=`${s.helperString(No)}(${i.content})`:(i.children.unshift(`${s.helperString(No)}(`),i.children.push(")"))),s.inSSR||(n.some(r=>r.content==="prop")&&cu(i,"."),n.some(r=>r.content==="attr")&&cu(i,"^")),{props:[wt(i,l)]}},cu=(e,t)=>{e.type===4?e.isStatic?e.content=t+e.content:e.content=`\`${t}\${${e.content}}\``:(e.children.unshift(`'${t}' + (`),e.children.push(")"))},p0=(e,t)=>{if(e.type===0||e.type===1||e.type===11||e.type===10)return()=>{const s=e.children;let n,a=!1;for(let i=0;ii.type===7&&!t.directiveTransforms[i.name])&&e.tag!=="template")))for(let i=0;i{if(e.type===1&&_s(e,"once",!0))return du.has(e)||t.inVOnce||t.inSSR?void 0:(du.add(e),t.inVOnce=!0,t.helper(Ql),()=>{t.inVOnce=!1;const s=t.currentNode;s.codegenNode&&(s.codegenNode=t.cache(s.codegenNode,!0,!0))})},zh=(e,t,s)=>{const{exp:n,arg:a}=e;if(!n)return s.onError(ut(41,e.loc)),ii();const i=n.loc.source.trim(),l=n.type===4?n.content:i,r=s.bindingMetadata[i];if(r==="props"||r==="props-aliased")return s.onError(ut(44,n.loc)),ii();if(r==="literal-const"||r==="setup-const")return s.onError(ut(45,n.loc)),ii();if(!l.trim()||!Sh(n))return s.onError(ut(42,n.loc)),ii();const o=a||Pe("modelValue",!0),c=a?cs(a)?`onUpdate:${at(a.content)}`:Ms(['"onUpdate:" + ',a]):"onUpdate:modelValue";let d;const u=s.isTS?"($event: any)":"$event";d=Ms([`${u} => ((`,n,") = $event)"]);const f=[wt(o,e.exp),wt(c,d)];if(e.modifiers.length&&t.tagType===1){const p=e.modifiers.map(y=>y.content).map(y=>(Hc(y)?y:JSON.stringify(y))+": true").join(", "),b=a?cs(a)?`${a.content}Modifiers`:Ms([a,' + "Modifiers"']):"modelModifiers";f.push(wt(b,Pe(`{ ${p} }`,!1,e.loc,2)))}return ii(f)};function ii(e=[]){return{props:e}}const m0=/[\w).+\-_$\]]/,g0=(e,t)=>{sa("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)uu(e,t);else for(let s=0;s=0&&(x=s.charAt(O),x===" ");O--);(!x||!m0.test(x))&&(l=!0)}}b===void 0?b=s.slice(0,p).trim():d!==0&&A();function A(){y.push(s.slice(d,p).trim()),d=p+1}if(y.length){for(p=0;p{if(e.type===1){const s=_s(e,"memo");return!s||fu.has(e)||t.inSSR?void 0:(fu.add(e),()=>{const n=e.codegenNode||t.currentNode.codegenNode;n&&n.type===13&&(e.tagType!==1&&Uc(n,t),e.codegenNode=Rt(t.helper($c),[s.exp,ja(void 0,n),"_cache",String(t.cached.length)]),t.cached.push(null))})}},y0=(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 n=s.arg;if(n.type!==4||!n.isStatic)t.onError(ut(53,n.loc)),s.exp=Pe("",!0,n.loc);else{const a=at(n.content);(kh.test(a[0])||a[0]==="-")&&(s.exp=Pe(a,!1,n.loc))}}}};function x0(e){return[[y0,h0,Jx,b0,Xx,g0,d0,i0,s0,p0],{on:jh,bind:f0,model:zh}]}function _0(e,t={}){const s=t.onError||Bc,n=t.mode==="module";t.prefixIdentifiers===!0?s(ut(48)):n&&s(ut(49));const a=!1;t.cacheHandlers&&s(ut(50)),t.scopeId&&!n&&s(ut(51));const i=je({},t,{prefixIdentifiers:a}),l=Me(e)?Ex(e,i):e,[r,o]=x0();return Ox(l,je({},i,{nodeTransforms:[...r,...t.nodeTransforms||[]],directiveTransforms:je({},o,t.directiveTransforms||{})})),Mx(l,i)}const k0=()=>({props:[]});/** +`,0),o()),d||a("return "),e.codegenNode?Zt(e.codegenNode,s):a("null"),p&&(r(),a("}")),r(),a("}"),{ast:e,code:s.code,preamble:"",map:s.map?s.map.toJSON():void 0}}function Px(e,t){const{ssr:s,prefixIdentifiers:n,push:a,newline:i,runtimeModuleName:l,runtimeGlobalName:r,ssrRuntimeModuleName:o}=t,c=r,d=Array.from(e.helpers);if(d.length>0&&(a(`const _Vue = ${c} +`,-1),e.hoists.length)){const u=[Tc,Cc,nl,Ec,gh].filter(f=>d.includes(f)).map(Fh).join(", ");a(`const { ${u} } = _Vue +`,-1)}Fx(e.hoists,t),i(),a("return ")}function Qr(e,t,{helper:s,push:n,newline:a,isTS:i}){const l=s(t==="filter"?Oc:t==="component"?Ac:Ic);for(let r=0;r3||!1;t.push("["),s&&t.indent(),il(e,t,s),s&&t.deindent(),t.push("]")}function il(e,t,s=!1,n=!0){const{push:a,newline:i}=t;for(let l=0;ls||"null")}function jx(e,t){const{push:s,helper:n,pure:a}=t,i=Me(e.callee)?e.callee:n(e.callee);a&&s(Or),s(i+"(",-2,e),il(e.arguments,t),s(")")}function qx(e,t){const{push:s,indent:n,deindent:a,newline:i}=t,{properties:l}=e;if(!l.length){s("{}",-2,e);return}const r=l.length>1||!1;s(r?"{":"{ "),r&&n();for(let o=0;o "),(o||r)&&(s("{"),n()),l?(o&&s("return "),be(l)?jc(l,t):Zt(l,t)):r&&Zt(r,t),(o||r)&&(a(),s("}")),c&&(e.isNonScopedSlot&&s(", undefined, true"),s(")"))}function Wx(e,t){const{test:s,consequent:n,alternate:a,newline:i}=e,{push:l,indent:r,deindent:o,newline:c}=t;if(s.type===4){const u=!Hc(s.content);u&&l("("),$h(s,t),u&&l(")")}else l("("),Zt(s,t),l(")");i&&r(),t.indentLevel++,i||l(" "),l("? "),Zt(n,t),t.indentLevel--,i&&c(),i||l(" "),l(": ");const d=a.type===19;d||t.indentLevel++,Zt(a,t),d||t.indentLevel--,i&&o(!0)}function Zx(e,t){const{push:s,helper:n,indent:a,deindent:i,newline:l}=t,{needPauseTracking:r,needArraySpread:o}=e;o&&s("[...("),s(`_cache[${e.index}] || (`),r&&(a(),s(`${n(Xl)}(-1`),e.inVOnce&&s(", true"),s("),"),l(),s("(")),s(`_cache[${e.index}] = `),Zt(e.value,t),r&&(s(`).cacheIndex = ${e.index},`),l(),s(`${n(Xl)}(1),`),l(),s(`_cache[${e.index}]`),i()),s(")"),o&&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 Jx=Ph(/^(?:if|else|else-if)$/,(e,t,s)=>Yx(e,t,s,(n,a,i)=>{const l=s.parent.children;let r=l.indexOf(n),o=0;for(;r-->=0;){const c=l[r];c&&c.type===9&&(o+=c.branches.length)}return()=>{if(i)n.codegenNode=iu(a,o,s);else{const c=Qx(n.codegenNode);c.alternate=iu(a,o+n.branches.length-1,s)}}}));function Yx(e,t,s,n){if(t.name!=="else"&&(!t.exp||!t.exp.content.trim())){const a=t.exp?t.exp.loc:e.loc;s.onError(ut(28,t.loc)),t.exp=Fe("true",!1,a)}if(t.name==="if"){const a=au(e,t),i={type:9,loc:Sx(e.loc),branches:[a]};if(s.replaceNode(i),n)return n(i,a,!0)}else{const a=s.parent.children;let i=a.indexOf(e);for(;i-->=-1;){const l=a[i];if(l&&Eh(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(ut(30,e.loc)),s.removeNode();const r=au(e,t);l.branches.push(r);const o=n&&n(l,r,!1);Ir(r,s),o&&o(),s.currentNode=null}else s.onError(ut(30,e.loc));break}}}function au(e,t){const s=e.tagType===3;return{type:10,loc:e.loc,condition:t.name==="else"?void 0:t.exp,children:s&&!Rs(e,"for")?e.children:[e],userKey:Rr(e,"key"),isTemplateIf:s}}function iu(e,t,s){return e.condition?Do(e.condition,lu(e,t,s),It(s.helper(nl),['""',"true"])):lu(e,t,s)}function lu(e,t,s){const{helper:n}=s,a=wt("key",Fe(`${t}`,!1,ws,2)),{children:i}=e,l=i[0];if(i.length!==1||l.type!==1)if(i.length===1&&l.type===11){const o=l.codegenNode;return nr(o,a,s),o}else return qi(s,n(Vi),Is([a]),i,64,void 0,void 0,!0,!1,!1,e.loc);else{const o=l.codegenNode,c=hx(o);return c.type===13&&Bc(c,s),nr(c,a,s),o}}function Qx(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 Xx=Ph("for",(e,t,s)=>{const{helper:n,removeHelper:a}=s;return e0(e,t,s,i=>{const l=It(n(Lc),[i.source]),r=tr(e),o=Rs(e,"memo"),c=Rr(e,"key",!1,!0);c&&c.type;let d=c&&(c.type===6?c.value?Fe(c.value.content,!0):void 0:c.exp);const u=d?wt("key",d):null,f=i.source.type===4&&i.source.constType>0,p=f?64:c?128:256;return i.codegenNode=qi(s,n(Vi),void 0,l,p,void 0,void 0,!0,!f,!1,e.loc),()=>{let b;const{children:y}=i,E=y.length!==1||y[0].type!==1,I=sr(e)?e:r&&e.children.length===1&&sr(e.children[0])?e.children[0]:null;if(I?(b=I.codegenNode,r&&u&&nr(b,u,s)):E?b=qi(s,n(Vi),u?Is([u]):void 0,e.children,64,void 0,void 0,!0,void 0,!1):(b=y[0].codegenNode,r&&u&&nr(b,u,s),b.isBlock!==!f&&(b.isBlock?(a(ra),a(Ja(s.inSSR,b.isComponent))):a(Za(s.inSSR,b.isComponent))),b.isBlock=!f,b.isBlock?(n(ra),n(Ja(s.inSSR,b.isComponent))):n(Za(s.inSSR,b.isComponent))),o){const x=Wa(Fo(i.parseResult,[Fe("_cached")]));x.body=sx([Vs(["const _memo = (",o.exp,")"]),Vs(["if (_cached && _cached.el",...d?[" && _cached.key === ",d]:[],` && ${s.helperString(yh)}(_cached, _memo)) return _cached`]),Vs(["const _item = ",b]),Fe("_item.memo = _memo"),Fe("return _item")]),l.arguments.push(x,Fe("_cache"),Fe(String(s.cached.length))),s.cached.push(null)}else l.arguments.push(Wa(Fo(i.parseResult),b,!0))}})});function e0(e,t,s,n){if(!t.exp){s.onError(ut(31,t.loc));return}const a=t.forParseResult;if(!a){s.onError(ut(32,t.loc));return}Uh(a);const{addIdentifiers:i,removeIdentifiers:l,scopes:r}=s,{source:o,value:c,key:d,index:u}=a,f={type:11,loc:t.loc,source:o,valueAlias:c,keyAlias:d,objectIndexAlias:u,parseResult:a,children:tr(e)?e.children:[e]};s.replaceNode(f),r.vFor++;const p=n&&n(f);return()=>{r.vFor--,p&&p()}}function Uh(e,t){e.finalized||(e.finalized=!0)}function Fo({value:e,key:t,index:s},n=[]){return t0([e,t,s,...n])}function t0(e){let t=e.length;for(;t--&&!e[t];);return e.slice(0,t+1).map((s,n)=>s||Fe("_".repeat(n+1),!1))}const ru=Fe("undefined",!1),s0=(e,t)=>{if(e.type===1&&(e.tagType===1||e.tagType===3)){const s=Rs(e,"slot");if(s)return s.exp,t.scopes.vSlot++,()=>{t.scopes.vSlot--}}},n0=(e,t,s,n)=>Wa(e,s,!1,!0,s.length?s[0].loc:n);function a0(e,t,s=n0){t.helper(Fc);const{children:n,loc:a}=e,i=[],l=[];let r=t.scopes.vSlot>0||t.scopes.vFor>0;const o=Rs(e,"slot",!0);if(o){const{arg:E,exp:I}=o;E&&!hs(E)&&(r=!0),i.push(wt(E||Fe("default",!0),s(I,void 0,n,a)))}let c=!1,d=!1;const u=[],f=new Set;let p=0;for(let E=0;E{const m=s(I,void 0,x,a);return t.compatConfig&&(m.isNonScopedSlot=!0),wt("default",m)};c?u.length&&!u.every(zc)&&(d?t.onError(ut(39,u[0].loc)):i.push(E(void 0,u))):i.push(E(void 0,n))}const b=r?2:Ol(e.children)?3:1;let y=Is(i.concat(wt("_",Fe(b+"",!1))),a);return l.length&&(y=It(t.helper(bh),[y,na(l)])),{slots:y,hasDynamicSlots:r}}function xl(e,t,s){const n=[wt("name",e),wt("fn",t)];return s!=null&&n.push(wt("key",Fe(String(s),!0))),Is(n)}function Ol(e){for(let t=0;tfunction(){if(e=t.currentNode,!(e.type===1&&(e.tagType===0||e.tagType===1)))return;const{tag:n,props:a}=e,i=e.tagType===1;let l=i?l0(e,t):`"${n}"`;const r=Xe(l)&&l.callee===Rc;let o,c,d=0,u,f,p,b=r||l===Ei||l===Sc||!i&&(n==="svg"||n==="foreignObject"||n==="math");if(a.length>0){const y=zh(e,t,void 0,i,r);o=y.props,d=y.patchFlag,f=y.dynamicPropNames;const E=y.directives;p=E&&E.length?na(E.map(I=>o0(I,t))):void 0,y.shouldUseBlock&&(b=!0)}if(e.children.length>0)if(l===Yl&&(b=!0,d|=1024),i&&l!==Ei&&l!==Yl){const{slots:E,hasDynamicSlots:I}=a0(e,t);c=E,I&&(d|=1024)}else if(e.children.length===1&&l!==Ei){const E=e.children[0],I=E.type,x=I===5||I===8;x&&ys(E,t)===0&&(d|=1),x||I===2?c=E:c=e.children}else c=e.children;f&&f.length&&(u=c0(f)),e.codegenNode=qi(t,l,o,c,d===0?void 0:d,u,p,!!b,!1,i,e.loc)};function l0(e,t,s=!1){let{tag:n}=e;const a=$o(n),i=Rr(e,"is",!1,!0);if(i)if(a||aa("COMPILER_IS_ON_ELEMENT",t)){let r;if(i.type===6?r=i.value&&Fe(i.value.content,!0):(r=i.exp,r||(r=Fe("is",!1,i.arg.loc))),r)return It(t.helper(Rc),[r])}else i.type===6&&i.value.content.startsWith("vue:")&&(n=i.value.content.slice(4));const l=_h(n)||t.isBuiltInComponent(n);return l?(s||t.helper(l),l):(t.helper(Ac),t.components.add(n),Ki(n,"component"))}function zh(e,t,s=e.props,n,a,i=!1){const{tag:l,loc:r,children:o}=e;let c=[];const d=[],u=[],f=o.length>0;let p=!1,b=0,y=!1,E=!1,I=!1,x=!1,m=!1,_=!1;const S=[],g=M=>{c.length&&(d.push(Is(ou(c),r)),c=[]),M&&d.push(M)},w=()=>{t.scopes.vFor>0&&c.push(wt(Fe("ref_for",!0),Fe("true")))},T=({key:M,value:H})=>{if(hs(M)){const P=M.content,R=ca(P);if(R&&(!n||a)&&P.toLowerCase()!=="onclick"&&P!=="onUpdate:modelValue"&&!bn(P)&&(x=!0),R&&bn(P)&&(_=!0),R&&H.type===14&&(H=H.arguments[0]),H.type===20||(H.type===4||H.type===8)&&ys(H,t)>0)return;P==="ref"?y=!0:P==="class"?E=!0:P==="style"?I=!0:P!=="key"&&!S.includes(P)&&S.push(P),n&&(P==="class"||P==="style")&&!S.includes(P)&&S.push(P)}else m=!0};for(let M=0;Mwe.content==="prop")&&(b|=32);const Y=t.directiveTransforms[P];if(Y){const{props:we,needRuntime:ke}=Y(H,e,t);!i&&we.forEach(T),N&&R&&!hs(R)?g(Is(we,r)):c.push(...we),ke&&(u.push(H),Jt(ke)&&Hh.set(H,ke))}else Xm(P)||(u.push(H),f&&(p=!0))}}let C;if(d.length?(g(),d.length>1?C=It(t.helper(Ql),d,r):C=d[0]):c.length&&(C=Is(ou(c),r)),m?b|=16:(E&&!n&&(b|=2),I&&!n&&(b|=4),S.length&&(b|=8),x&&(b|=32)),!p&&(b===0||b===32)&&(y||_||u.length>0)&&(b|=512),!t.inSSR&&C)switch(C.type){case 15:let M=-1,H=-1,P=!1;for(let Q=0;Qwt(l,i)),a))}return na(s,e.loc)}function c0(e){let t="[";for(let s=0,n=e.length;s{if(sr(e)){const{children:s,loc:n}=e,{slotName:a,slotProps:i}=u0(e,t),l=[t.prefixIdentifiers?"_ctx.$slots":"$slots",a,"{}","undefined","true"];let r=2;i&&(l[2]=i,r=3),s.length&&(l[3]=Wa([],s,!1,!1,n),r=4),t.scopeId&&!t.slotted&&(r=5),l.splice(r),e.codegenNode=It(t.helper(vh),l,n)}};function u0(e,t){let s='"default"',n;const a=[];for(let i=0;i0){const{props:i,directives:l}=zh(e,t,a,!1,!1);n=i,l.length&&t.onError(ut(36,l[0].loc))}return{slotName:s,slotProps:n}}const Vh=(e,t,s,n)=>{const{loc:a,modifiers:i,arg:l}=e;!e.exp&&!i.length&&s.onError(ut(35,a));let r;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)?Da(it(u)):`on:${u}`;r=Fe(f,!0,l.loc)}else r=Vs([`${s.helperString(Lo)}(`,l,")"]);else r=l,r.children.unshift(`${s.helperString(Lo)}(`),r.children.push(")");let o=e.exp;o&&!o.content.trim()&&(o=void 0);let c=s.cacheHandlers&&!o&&!s.inVOnce;if(o){const u=Sh(o),f=!(u||dx(o)),p=o.content.includes(";");(f||c&&u)&&(o=Vs([`${f?"$event":"(...args)"} => ${p?"{":"("}`,o,p?"}":")"]))}let d={props:[wt(r,o||Fe("() => {}",!1,a))]};return n&&(d=n(d)),c&&(d.props[0].value=s.cache(d.props[0].value)),d.props.forEach(u=>u.key.isHandlerKey=!0),d},f0=(e,t,s)=>{const{modifiers:n,loc:a}=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} || ""`:'""'),n.some(r=>r.content==="camel")&&(i.type===4?i.isStatic?i.content=it(i.content):i.content=`${s.helperString(No)}(${i.content})`:(i.children.unshift(`${s.helperString(No)}(`),i.children.push(")"))),s.inSSR||(n.some(r=>r.content==="prop")&&cu(i,"."),n.some(r=>r.content==="attr")&&cu(i,"^")),{props:[wt(i,l)]}},cu=(e,t)=>{e.type===4?e.isStatic?e.content=t+e.content:e.content=`\`${t}\${${e.content}}\``:(e.children.unshift(`'${t}' + (`),e.children.push(")"))},p0=(e,t)=>{if(e.type===0||e.type===1||e.type===11||e.type===10)return()=>{const s=e.children;let n,a=!1;for(let i=0;ii.type===7&&!t.directiveTransforms[i.name])&&e.tag!=="template")))for(let i=0;i{if(e.type===1&&Rs(e,"once",!0))return du.has(e)||t.inVOnce||t.inSSR?void 0:(du.add(e),t.inVOnce=!0,t.helper(Xl),()=>{t.inVOnce=!1;const s=t.currentNode;s.codegenNode&&(s.codegenNode=t.cache(s.codegenNode,!0,!0))})},jh=(e,t,s)=>{const{exp:n,arg:a}=e;if(!n)return s.onError(ut(41,e.loc)),ui();const i=n.loc.source.trim(),l=n.type===4?n.content:i,r=s.bindingMetadata[i];if(r==="props"||r==="props-aliased")return s.onError(ut(44,n.loc)),ui();if(r==="literal-const"||r==="setup-const")return s.onError(ut(45,n.loc)),ui();if(!l.trim()||!Sh(n))return s.onError(ut(42,n.loc)),ui();const o=a||Fe("modelValue",!0),c=a?hs(a)?`onUpdate:${it(a.content)}`:Vs(['"onUpdate:" + ',a]):"onUpdate:modelValue";let d;const u=s.isTS?"($event: any)":"$event";d=Vs([`${u} => ((`,n,") = $event)"]);const f=[wt(o,e.exp),wt(c,d)];if(e.modifiers.length&&t.tagType===1){const p=e.modifiers.map(y=>y.content).map(y=>(Hc(y)?y:JSON.stringify(y))+": true").join(", "),b=a?hs(a)?`${a.content}Modifiers`:Vs([a,' + "Modifiers"']):"modelModifiers";f.push(wt(b,Fe(`{ ${p} }`,!1,e.loc,2)))}return ui(f)};function ui(e=[]){return{props:e}}const m0=/[\w).+\-_$\]]/,g0=(e,t)=>{aa("COMPILER_FILTERS",t)&&(e.type===5?ar(e.content,t):e.type===1&&e.props.forEach(s=>{s.type===7&&s.name!=="for"&&s.exp&&ar(s.exp,t)}))};function ar(e,t){if(e.type===4)uu(e,t);else for(let s=0;s=0&&(x=s.charAt(I),x===" ");I--);(!x||!m0.test(x))&&(l=!0)}}b===void 0?b=s.slice(0,p).trim():d!==0&&E();function E(){y.push(s.slice(d,p).trim()),d=p+1}if(y.length){for(p=0;p{if(e.type===1){const s=Rs(e,"memo");return!s||fu.has(e)||t.inSSR?void 0:(fu.add(e),()=>{const n=e.codegenNode||t.currentNode.codegenNode;n&&n.type===13&&(e.tagType!==1&&Bc(n,t),e.codegenNode=It(t.helper($c),[s.exp,Wa(void 0,n),"_cache",String(t.cached.length)]),t.cached.push(null))})}},y0=(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 n=s.arg;if(n.type!==4||!n.isStatic)t.onError(ut(53,n.loc)),s.exp=Fe("",!0,n.loc);else{const a=it(n.content);(kh.test(a[0])||a[0]==="-")&&(s.exp=Fe(a,!1,n.loc))}}}};function x0(e){return[[y0,h0,Jx,b0,Xx,g0,d0,i0,s0,p0],{on:Vh,bind:f0,model:jh}]}function _0(e,t={}){const s=t.onError||Uc,n=t.mode==="module";t.prefixIdentifiers===!0?s(ut(48)):n&&s(ut(49));const a=!1;t.cacheHandlers&&s(ut(50)),t.scopeId&&!n&&s(ut(51));const i=ze({},t,{prefixIdentifiers:a}),l=Me(e)?Ex(e,i):e,[r,o]=x0();return Ox(l,ze({},i,{nodeTransforms:[...r,...t.nodeTransforms||[]],directiveTransforms:ze({},o,t.directiveTransforms||{})})),Mx(l,i)}const k0=()=>({props:[]});/** * @vue/compiler-dom v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const qh=Symbol(""),Gh=Symbol(""),Kh=Symbol(""),Wh=Symbol(""),Uo=Symbol(""),Zh=Symbol(""),Jh=Symbol(""),Yh=Symbol(""),Qh=Symbol(""),Xh=Symbol("");Xy({[qh]:"vModelRadio",[Gh]:"vModelCheckbox",[Kh]:"vModelText",[Wh]:"vModelSelect",[Uo]:"vModelDynamic",[Zh]:"withModifiers",[Jh]:"withKeys",[Yh]:"vShow",[Qh]:"Transition",[Xh]:"TransitionGroup"});let va;function w0(e,t=!1){return va||(va=document.createElement("div")),t?(va.innerHTML=`
`,va.children[0].getAttribute("foo")):(va.innerHTML=e,va.textContent)}const S0={parseMode:"html",isVoidTag:gg,isNativeTag:e=>pg(e)||hg(e)||mg(e),isPreTag:e=>e==="pre",isIgnoreNewlineTag:e=>e==="pre"||e==="textarea",decodeEntities:w0,isBuiltInComponent:e=>{if(e==="Transition"||e==="transition")return Qh;if(e==="TransitionGroup"||e==="transition-group")return Xh},getNamespace(e,t,s){let n=t?t.ns:s;if(t&&n===2)if(t.tag==="annotation-xml"){if(e==="svg")return 1;t.props.some(a=>a.type===6&&a.name==="encoding"&&a.value!=null&&(a.value.content==="text/html"||a.value.content==="application/xhtml+xml"))&&(n=0)}else/^m(?:[ions]|text)$/.test(t.tag)&&e!=="mglyph"&&e!=="malignmark"&&(n=0);else t&&n===1&&(t.tag==="foreignObject"||t.tag==="desc"||t.tag==="title")&&(n=0);if(n===0){if(e==="svg")return 1;if(e==="math")return 2}return n}},T0=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:Pe("style",!0,t.loc),exp:C0(t.value.content,t.loc),modifiers:[],loc:t.loc})})},C0=(e,t)=>{const s=ff(e);return Pe(JSON.stringify(s),!1,t,3)};function Fn(e,t){return ut(e,t)}const E0=(e,t,s)=>{const{exp:n,loc:a}=e;return n||s.onError(Fn(54,a)),t.children.length&&(s.onError(Fn(55,a)),t.children.length=0),{props:[wt(Pe("innerHTML",!0,a),n||Pe("",!0))]}},A0=(e,t,s)=>{const{exp:n,loc:a}=e;return n||s.onError(Fn(56,a)),t.children.length&&(s.onError(Fn(57,a)),t.children.length=0),{props:[wt(Pe("textContent",!0),n?hs(n,s)>0?n:Rt(s.helperString(Er),[n],a):Pe("",!0))]}},R0=(e,t,s)=>{const n=zh(e,t,s);if(!n.props.length||t.tagType===1)return n;e.arg&&s.onError(Fn(59,e.arg.loc));const{tag:a}=t,i=s.isCustomElement(a);if(a==="input"||a==="textarea"||a==="select"||i){let l=Kh,r=!1;if(a==="input"||i){const o=Ar(t,"type");if(o){if(o.type===7)l=Uo;else if(o.value)switch(o.value.content){case"radio":l=qh;break;case"checkbox":l=Gh;break;case"file":r=!0,s.onError(Fn(60,e.loc));break}}else ux(t)&&(l=Uo)}else a==="select"&&(l=Wh);r||(n.needRuntime=s.helper(l))}else s.onError(Fn(58,e.loc));return n.props=n.props.filter(l=>!(l.key.type===4&&l.key.content==="modelValue")),n},I0=vs("passive,once,capture"),O0=vs("stop,prevent,self,ctrl,shift,alt,meta,exact,middle"),N0=vs("left,right"),em=vs("onkeyup,onkeydown,onkeypress"),L0=(e,t,s,n)=>{const a=[],i=[],l=[];for(let r=0;rcs(e)&&e.content.toLowerCase()==="onclick"?Pe(t,!0):e.type!==4?Ms(["(",e,`) === "onClick" ? "${t}" : (`,e,")"]):e,D0=(e,t,s)=>jh(e,t,s,n=>{const{modifiers:a}=e;if(!a.length)return n;let{key:i,value:l}=n.props[0];const{keyModifiers:r,nonKeyModifiers:o,eventOptionModifiers:c}=L0(i,a,s,e.loc);if(o.includes("right")&&(i=pu(i,"onContextmenu")),o.includes("middle")&&(i=pu(i,"onMouseup")),o.length&&(l=Rt(s.helper(Zh),[l,JSON.stringify(o)])),r.length&&(!cs(i)||em(i.content.toLowerCase()))&&(l=Rt(s.helper(Jh),[l,JSON.stringify(r)])),c.length){const d=c.map(ca).join("");i=cs(i)?Pe(`${i.content}${d}`,!0):Ms(["(",i,`) + "${d}"`])}return{props:[wt(i,l)]}}),M0=(e,t,s)=>{const{exp:n,loc:a}=e;return n||s.onError(Fn(62,a)),{props:[],needRuntime:s.helper(Yh)}},P0=(e,t)=>{e.type===1&&e.tagType===0&&(e.tag==="script"||e.tag==="style")&&t.removeNode()},F0=[T0],$0={cloak:k0,html:E0,text:A0,model:R0,on:D0,show:M0};function U0(e,t={}){return _0(e,je({},S0,t,{nodeTransforms:[P0,...F0,...t.nodeTransforms||[]],directiveTransforms:je({},$0,t.directiveTransforms||{}),transformHoist:null}))}/** +**/const qh=Symbol(""),Gh=Symbol(""),Kh=Symbol(""),Wh=Symbol(""),Bo=Symbol(""),Zh=Symbol(""),Jh=Symbol(""),Yh=Symbol(""),Qh=Symbol(""),Xh=Symbol("");Xy({[qh]:"vModelRadio",[Gh]:"vModelCheckbox",[Kh]:"vModelText",[Wh]:"vModelSelect",[Bo]:"vModelDynamic",[Zh]:"withModifiers",[Jh]:"withKeys",[Yh]:"vShow",[Qh]:"Transition",[Xh]:"TransitionGroup"});let ka;function w0(e,t=!1){return ka||(ka=document.createElement("div")),t?(ka.innerHTML=`
`,ka.children[0].getAttribute("foo")):(ka.innerHTML=e,ka.textContent)}const S0={parseMode:"html",isVoidTag:gg,isNativeTag:e=>pg(e)||hg(e)||mg(e),isPreTag:e=>e==="pre",isIgnoreNewlineTag:e=>e==="pre"||e==="textarea",decodeEntities:w0,isBuiltInComponent:e=>{if(e==="Transition"||e==="transition")return Qh;if(e==="TransitionGroup"||e==="transition-group")return Xh},getNamespace(e,t,s){let n=t?t.ns:s;if(t&&n===2)if(t.tag==="annotation-xml"){if(e==="svg")return 1;t.props.some(a=>a.type===6&&a.name==="encoding"&&a.value!=null&&(a.value.content==="text/html"||a.value.content==="application/xhtml+xml"))&&(n=0)}else/^m(?:[ions]|text)$/.test(t.tag)&&e!=="mglyph"&&e!=="malignmark"&&(n=0);else t&&n===1&&(t.tag==="foreignObject"||t.tag==="desc"||t.tag==="title")&&(n=0);if(n===0){if(e==="svg")return 1;if(e==="math")return 2}return n}},T0=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:Fe("style",!0,t.loc),exp:C0(t.value.content,t.loc),modifiers:[],loc:t.loc})})},C0=(e,t)=>{const s=ff(e);return Fe(JSON.stringify(s),!1,t,3)};function Bn(e,t){return ut(e,t)}const E0=(e,t,s)=>{const{exp:n,loc:a}=e;return n||s.onError(Bn(54,a)),t.children.length&&(s.onError(Bn(55,a)),t.children.length=0),{props:[wt(Fe("innerHTML",!0,a),n||Fe("",!0))]}},A0=(e,t,s)=>{const{exp:n,loc:a}=e;return n||s.onError(Bn(56,a)),t.children.length&&(s.onError(Bn(57,a)),t.children.length=0),{props:[wt(Fe("textContent",!0),n?ys(n,s)>0?n:It(s.helperString(Ar),[n],a):Fe("",!0))]}},R0=(e,t,s)=>{const n=jh(e,t,s);if(!n.props.length||t.tagType===1)return n;e.arg&&s.onError(Bn(59,e.arg.loc));const{tag:a}=t,i=s.isCustomElement(a);if(a==="input"||a==="textarea"||a==="select"||i){let l=Kh,r=!1;if(a==="input"||i){const o=Rr(t,"type");if(o){if(o.type===7)l=Bo;else if(o.value)switch(o.value.content){case"radio":l=qh;break;case"checkbox":l=Gh;break;case"file":r=!0,s.onError(Bn(60,e.loc));break}}else ux(t)&&(l=Bo)}else a==="select"&&(l=Wh);r||(n.needRuntime=s.helper(l))}else s.onError(Bn(58,e.loc));return n.props=n.props.filter(l=>!(l.key.type===4&&l.key.content==="modelValue")),n},I0=ks("passive,once,capture"),O0=ks("stop,prevent,self,ctrl,shift,alt,meta,exact,middle"),N0=ks("left,right"),em=ks("onkeyup,onkeydown,onkeypress"),L0=(e,t,s,n)=>{const a=[],i=[],l=[];for(let r=0;rhs(e)&&e.content.toLowerCase()==="onclick"?Fe(t,!0):e.type!==4?Vs(["(",e,`) === "onClick" ? "${t}" : (`,e,")"]):e,D0=(e,t,s)=>Vh(e,t,s,n=>{const{modifiers:a}=e;if(!a.length)return n;let{key:i,value:l}=n.props[0];const{keyModifiers:r,nonKeyModifiers:o,eventOptionModifiers:c}=L0(i,a,s,e.loc);if(o.includes("right")&&(i=pu(i,"onContextmenu")),o.includes("middle")&&(i=pu(i,"onMouseup")),o.length&&(l=It(s.helper(Zh),[l,JSON.stringify(o)])),r.length&&(!hs(i)||em(i.content.toLowerCase()))&&(l=It(s.helper(Jh),[l,JSON.stringify(r)])),c.length){const d=c.map(ua).join("");i=hs(i)?Fe(`${i.content}${d}`,!0):Vs(["(",i,`) + "${d}"`])}return{props:[wt(i,l)]}}),M0=(e,t,s)=>{const{exp:n,loc:a}=e;return n||s.onError(Bn(62,a)),{props:[],needRuntime:s.helper(Yh)}},P0=(e,t)=>{e.type===1&&e.tagType===0&&(e.tag==="script"||e.tag==="style")&&t.removeNode()},F0=[T0],$0={cloak:k0,html:E0,text:A0,model:R0,on:D0,show:M0};function B0(e,t={}){return _0(e,ze({},S0,t,{nodeTransforms:[P0,...F0,...t.nodeTransforms||[]],directiveTransforms:ze({},$0,t.directiveTransforms||{}),transformHoist:null}))}/** * vue v3.5.38 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/const hu=Object.create(null);function B0(e,t){if(!Me(e))if(e.nodeType)e=e.innerHTML;else return Ft;const s=sg(e,t),n=hu[s];if(n)return n;if(e[0]==="#"){const r=document.querySelector(e);e=r?r.innerHTML:""}const a=je({hoistStatic:!0,onError:void 0,onWarn:Ft},t);!a.isCustomElement&&typeof customElements<"u"&&(a.isCustomElement=r=>!!customElements.get(r));const{code:i}=U0(e,a),l=new Function("Vue",i)(Ky);return l._rc=!0,hu[s]=l}Fp(B0);const ar=Un({items:[]});let H0=1;function Or(e,t="info",s=3e3){const n=H0++;return ar.items.push({id:n,message:String(e),type:t}),s>0&&setTimeout(()=>qc(n),s),n}function qc(e){const t=ar.items.findIndex(s=>s.id===e);t>=0&&ar.items.splice(t,1)}function Te(e,t="info",s=3e3){return Or(e,t,s)}Te.success=(e,t=3e3)=>Or(e,"success",t);Te.error=(e,t=5e3)=>Or(e,"error",t);Te.info=(e,t=3e3)=>Or(e,"info",t);Te.dismiss=qc;const V0={setup(){return{state:ar,dismiss:qc}},template:` +**/const hu=Object.create(null);function U0(e,t){if(!Me(e))if(e.nodeType)e=e.innerHTML;else return Ht;const s=sg(e,t),n=hu[s];if(n)return n;if(e[0]==="#"){const r=document.querySelector(e);e=r?r.innerHTML:""}const a=ze({hoistStatic:!0,onError:void 0,onWarn:Ht},t);!a.isCustomElement&&typeof customElements<"u"&&(a.isCustomElement=r=>!!customElements.get(r));const{code:i}=B0(e,a),l=new Function("Vue",i)(Ky);return l._rc=!0,hu[s]=l}Fp(U0);const ir=Hn({items:[]});let H0=1;function Nr(e,t="info",s=3e3){const n=H0++;return ir.items.push({id:n,message:String(e),type:t}),s>0&&setTimeout(()=>qc(n),s),n}function qc(e){const t=ir.items.findIndex(s=>s.id===e);t>=0&&ir.items.splice(t,1)}function Ae(e,t="info",s=3e3){return Nr(e,t,s)}Ae.success=(e,t=3e3)=>Nr(e,"success",t);Ae.error=(e,t=5e3)=>Nr(e,"error",t);Ae.info=(e,t=3e3)=>Nr(e,"info",t);Ae.dismiss=qc;const z0={setup(){return{state:ir,dismiss:qc}},template:`
t in e?Gm(e,t,{enumerable:!0,config
- `},dn=Un({open:!1,title:"Confirm",message:"",confirmLabel:"Confirm",cancelLabel:"Cancel",danger:!1});let Ma=null;function gs({title:e="Confirm",message:t="",confirmLabel:s="Confirm",cancelLabel:n="Cancel",danger:a=!1}={}){return Ma&&Ma(!1),dn.title=e,dn.message=t,dn.confirmLabel=s,dn.cancelLabel=n,dn.danger=a,dn.open=!0,new Promise(i=>{Ma=i})}function mu(e){dn.open=!1,Ma&&(Ma(e),Ma=null)}const j0={setup(){function e(t){dn.open&&t.key==="Escape"&&(t.stopPropagation(),mu(!1))}return We(()=>document.addEventListener("keydown",e,!0)),xt(()=>document.removeEventListener("keydown",e,!0)),{state:dn,settle:mu}},template:` + `},fn=Hn({open:!1,title:"Confirm",message:"",confirmLabel:"Confirm",cancelLabel:"Cancel",danger:!1});let Ua=null;function _s({title:e="Confirm",message:t="",confirmLabel:s="Confirm",cancelLabel:n="Cancel",danger:a=!1}={}){return Ua&&Ua(!1),fn.title=e,fn.message=t,fn.confirmLabel=s,fn.cancelLabel=n,fn.danger=a,fn.open=!0,new Promise(i=>{Ua=i})}function mu(e){fn.open=!1,Ua&&(Ua(e),Ua=null)}const V0={setup(){function e(t){fn.open&&t.key==="Escape"&&(t.stopPropagation(),mu(!1))}return We(()=>document.addEventListener("keydown",e,!0)),xt(()=>document.removeEventListener("keydown",e,!0)),{state:fn,settle:mu}},template:`
@@ -71,11 +71,11 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config * vue-router v4.6.4 * (c) 2025 Eduardo San Martin Morote * @license MIT - */const wa=typeof document<"u";function tm(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function z0(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&tm(e.default)}const st=Object.assign;function Xr(e,t){const s={};for(const n in t){const a=t[n];s[n]=Fs(a)?a.map(e):e(a)}return s}const ki=()=>{},Fs=Array.isArray;function gu(e,t){const s={};for(const n in e)s[n]=n in t?t[n]:e[n];return s}const sm=/#/g,q0=/&/g,G0=/\//g,K0=/=/g,W0=/\?/g,nm=/\+/g,Z0=/%5B/g,J0=/%5D/g,am=/%5E/g,Y0=/%60/g,im=/%7B/g,Q0=/%7C/g,lm=/%7D/g,X0=/%20/g;function Gc(e){return e==null?"":encodeURI(""+e).replace(Q0,"|").replace(Z0,"[").replace(J0,"]")}function e_(e){return Gc(e).replace(im,"{").replace(lm,"}").replace(am,"^")}function Bo(e){return Gc(e).replace(nm,"%2B").replace(X0,"+").replace(sm,"%23").replace(q0,"%26").replace(Y0,"`").replace(im,"{").replace(lm,"}").replace(am,"^")}function t_(e){return Bo(e).replace(K0,"%3D")}function s_(e){return Gc(e).replace(sm,"%23").replace(W0,"%3F")}function n_(e){return s_(e).replace(G0,"%2F")}function ji(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const a_=/\/$/,i_=e=>e.replace(a_,"");function eo(e,t,s="/"){let n,a={},i="",l="";const r=t.indexOf("#");let o=t.indexOf("?");return o=r>=0&&o>r?-1:o,o>=0&&(n=t.slice(0,o),i=t.slice(o,r>0?r:t.length),a=e(i.slice(1))),r>=0&&(n=n||t.slice(0,r),l=t.slice(r,t.length)),n=c_(n??t,s),{fullPath:n+i+l,path:n,query:a,hash:ji(l)}}function l_(e,t){const s=t.query?e(t.query):"";return t.path+(s&&"?")+s+(t.hash||"")}function vu(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function r_(e,t,s){const n=t.matched.length-1,a=s.matched.length-1;return n>-1&&n===a&&Ga(t.matched[n],s.matched[a])&&rm(t.params,s.params)&&e(t.query)===e(s.query)&&t.hash===s.hash}function Ga(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function rm(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var s in e)if(!o_(e[s],t[s]))return!1;return!0}function o_(e,t){return Fs(e)?bu(e,t):Fs(t)?bu(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function bu(e,t){return Fs(t)?e.length===t.length&&e.every((s,n)=>s===t[n]):e.length===1&&e[0]===t}function c_(e,t){if(e.startsWith("/"))return e;if(!e)return t;const s=t.split("/"),n=e.split("/"),a=n[n.length-1];(a===".."||a===".")&&n.push("");let i=s.length-1,l,r;for(l=0;l1&&i--;else break;return s.slice(0,i).join("/")+"/"+n.slice(l).join("/")}const Rn={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let Ho=(function(e){return e.pop="pop",e.push="push",e})({}),to=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function d_(e){if(!e)if(wa){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),i_(e)}const u_=/^[^#]+#/;function f_(e,t){return e.replace(u_,"#")+t}function p_(e,t){const s=document.documentElement.getBoundingClientRect(),n=e.getBoundingClientRect();return{behavior:t.behavior,left:n.left-s.left-(t.left||0),top:n.top-s.top-(t.top||0)}}const Nr=()=>({left:window.scrollX,top:window.scrollY});function h_(e){let t;if("el"in e){const s=e.el,n=typeof s=="string"&&s.startsWith("#"),a=typeof s=="string"?n?document.getElementById(s.slice(1)):document.querySelector(s):s;if(!a)return;t=p_(a,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function yu(e,t){return(history.state?history.state.position-t:-1)+e}const Vo=new Map;function m_(e,t){Vo.set(e,t)}function g_(e){const t=Vo.get(e);return Vo.delete(e),t}function v_(e){return typeof e=="string"||e&&typeof e=="object"}function om(e){return typeof e=="string"||typeof e=="symbol"}let mt=(function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e})({});const cm=Symbol("");mt.MATCHER_NOT_FOUND+"",mt.NAVIGATION_GUARD_REDIRECT+"",mt.NAVIGATION_ABORTED+"",mt.NAVIGATION_CANCELLED+"",mt.NAVIGATION_DUPLICATED+"";function Ka(e,t){return st(new Error,{type:e,[cm]:!0},t)}function an(e,t){return e instanceof Error&&cm in e&&(t==null||!!(e.type&t))}const b_=["params","query","hash"];function y_(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const s of b_)s in e&&(t[s]=e[s]);return JSON.stringify(t,null,2)}function x_(e){const t={};if(e===""||e==="?")return t;const s=(e[0]==="?"?e.slice(1):e).split("&");for(let n=0;na&&Bo(a)):[n&&Bo(n)]).forEach(a=>{a!==void 0&&(t+=(t.length?"&":"")+s,a!=null&&(t+="="+a))})}return t}function __(e){const t={};for(const s in e){const n=e[s];n!==void 0&&(t[s]=Fs(n)?n.map(a=>a==null?null:""+a):n==null?n:""+n)}return t}const k_=Symbol(""),_u=Symbol(""),Lr=Symbol(""),Kc=Symbol(""),jo=Symbol("");function li(){let e=[];function t(n){return e.push(n),()=>{const a=e.indexOf(n);a>-1&&e.splice(a,1)}}function s(){e=[]}return{add:t,list:()=>e.slice(),reset:s}}function Mn(e,t,s,n,a,i=l=>l()){const l=n&&(n.enterCallbacks[a]=n.enterCallbacks[a]||[]);return()=>new Promise((r,o)=>{const c=f=>{f===!1?o(Ka(mt.NAVIGATION_ABORTED,{from:s,to:t})):f instanceof Error?o(f):v_(f)?o(Ka(mt.NAVIGATION_GUARD_REDIRECT,{from:t,to:f})):(l&&n.enterCallbacks[a]===l&&typeof f=="function"&&l.push(f),r())},d=i(()=>e.call(n&&n.instances[a],t,s,c));let u=Promise.resolve(d);e.length<3&&(u=u.then(c)),u.catch(f=>o(f))})}function so(e,t,s,n,a=i=>i()){const i=[];for(const l of e)for(const r in l.components){let o=l.components[r];if(!(t!=="beforeRouteEnter"&&!l.instances[r]))if(tm(o)){const c=(o.__vccOpts||o)[t];c&&i.push(Mn(c,s,n,l,r,a))}else{let c=o();i.push(()=>c.then(d=>{if(!d)throw new Error(`Couldn't resolve component "${r}" at "${l.path}"`);const u=z0(d)?d.default:d;l.mods[r]=d,l.components[r]=u;const f=(u.__vccOpts||u)[t];return f&&Mn(f,s,n,l,r,a)()}))}}return i}function w_(e,t){const s=[],n=[],a=[],i=Math.max(t.matched.length,e.matched.length);for(let l=0;lGa(c,r))?n.push(r):s.push(r));const o=e.matched[l];o&&(t.matched.find(c=>Ga(c,o))||a.push(o))}return[s,n,a]}/*! + */const Aa=typeof document<"u";function tm(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function j0(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&tm(e.default)}const nt=Object.assign;function Xr(e,t){const s={};for(const n in t){const a=t[n];s[n]=qs(a)?a.map(e):e(a)}return s}const Ai=()=>{},qs=Array.isArray;function gu(e,t){const s={};for(const n in e)s[n]=n in t?t[n]:e[n];return s}const sm=/#/g,q0=/&/g,G0=/\//g,K0=/=/g,W0=/\?/g,nm=/\+/g,Z0=/%5B/g,J0=/%5D/g,am=/%5E/g,Y0=/%60/g,im=/%7B/g,Q0=/%7C/g,lm=/%7D/g,X0=/%20/g;function Gc(e){return e==null?"":encodeURI(""+e).replace(Q0,"|").replace(Z0,"[").replace(J0,"]")}function e_(e){return Gc(e).replace(im,"{").replace(lm,"}").replace(am,"^")}function Uo(e){return Gc(e).replace(nm,"%2B").replace(X0,"+").replace(sm,"%23").replace(q0,"%26").replace(Y0,"`").replace(im,"{").replace(lm,"}").replace(am,"^")}function t_(e){return Uo(e).replace(K0,"%3D")}function s_(e){return Gc(e).replace(sm,"%23").replace(W0,"%3F")}function n_(e){return s_(e).replace(G0,"%2F")}function Zi(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const a_=/\/$/,i_=e=>e.replace(a_,"");function eo(e,t,s="/"){let n,a={},i="",l="";const r=t.indexOf("#");let o=t.indexOf("?");return o=r>=0&&o>r?-1:o,o>=0&&(n=t.slice(0,o),i=t.slice(o,r>0?r:t.length),a=e(i.slice(1))),r>=0&&(n=n||t.slice(0,r),l=t.slice(r,t.length)),n=c_(n??t,s),{fullPath:n+i+l,path:n,query:a,hash:Zi(l)}}function l_(e,t){const s=t.query?e(t.query):"";return t.path+(s&&"?")+s+(t.hash||"")}function vu(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function r_(e,t,s){const n=t.matched.length-1,a=s.matched.length-1;return n>-1&&n===a&&Ya(t.matched[n],s.matched[a])&&rm(t.params,s.params)&&e(t.query)===e(s.query)&&t.hash===s.hash}function Ya(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function rm(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var s in e)if(!o_(e[s],t[s]))return!1;return!0}function o_(e,t){return qs(e)?bu(e,t):qs(t)?bu(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function bu(e,t){return qs(t)?e.length===t.length&&e.every((s,n)=>s===t[n]):e.length===1&&e[0]===t}function c_(e,t){if(e.startsWith("/"))return e;if(!e)return t;const s=t.split("/"),n=e.split("/"),a=n[n.length-1];(a===".."||a===".")&&n.push("");let i=s.length-1,l,r;for(l=0;l1&&i--;else break;return s.slice(0,i).join("/")+"/"+n.slice(l).join("/")}const On={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let Ho=(function(e){return e.pop="pop",e.push="push",e})({}),to=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function d_(e){if(!e)if(Aa){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),i_(e)}const u_=/^[^#]+#/;function f_(e,t){return e.replace(u_,"#")+t}function p_(e,t){const s=document.documentElement.getBoundingClientRect(),n=e.getBoundingClientRect();return{behavior:t.behavior,left:n.left-s.left-(t.left||0),top:n.top-s.top-(t.top||0)}}const Lr=()=>({left:window.scrollX,top:window.scrollY});function h_(e){let t;if("el"in e){const s=e.el,n=typeof s=="string"&&s.startsWith("#"),a=typeof s=="string"?n?document.getElementById(s.slice(1)):document.querySelector(s):s;if(!a)return;t=p_(a,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function yu(e,t){return(history.state?history.state.position-t:-1)+e}const zo=new Map;function m_(e,t){zo.set(e,t)}function g_(e){const t=zo.get(e);return zo.delete(e),t}function v_(e){return typeof e=="string"||e&&typeof e=="object"}function om(e){return typeof e=="string"||typeof e=="symbol"}let mt=(function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e})({});const cm=Symbol("");mt.MATCHER_NOT_FOUND+"",mt.NAVIGATION_GUARD_REDIRECT+"",mt.NAVIGATION_ABORTED+"",mt.NAVIGATION_CANCELLED+"",mt.NAVIGATION_DUPLICATED+"";function Qa(e,t){return nt(new Error,{type:e,[cm]:!0},t)}function rn(e,t){return e instanceof Error&&cm in e&&(t==null||!!(e.type&t))}const b_=["params","query","hash"];function y_(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const s of b_)s in e&&(t[s]=e[s]);return JSON.stringify(t,null,2)}function x_(e){const t={};if(e===""||e==="?")return t;const s=(e[0]==="?"?e.slice(1):e).split("&");for(let n=0;na&&Uo(a)):[n&&Uo(n)]).forEach(a=>{a!==void 0&&(t+=(t.length?"&":"")+s,a!=null&&(t+="="+a))})}return t}function __(e){const t={};for(const s in e){const n=e[s];n!==void 0&&(t[s]=qs(n)?n.map(a=>a==null?null:""+a):n==null?n:""+n)}return t}const k_=Symbol(""),_u=Symbol(""),Dr=Symbol(""),Kc=Symbol(""),Vo=Symbol("");function fi(){let e=[];function t(n){return e.push(n),()=>{const a=e.indexOf(n);a>-1&&e.splice(a,1)}}function s(){e=[]}return{add:t,list:()=>e.slice(),reset:s}}function Fn(e,t,s,n,a,i=l=>l()){const l=n&&(n.enterCallbacks[a]=n.enterCallbacks[a]||[]);return()=>new Promise((r,o)=>{const c=f=>{f===!1?o(Qa(mt.NAVIGATION_ABORTED,{from:s,to:t})):f instanceof Error?o(f):v_(f)?o(Qa(mt.NAVIGATION_GUARD_REDIRECT,{from:t,to:f})):(l&&n.enterCallbacks[a]===l&&typeof f=="function"&&l.push(f),r())},d=i(()=>e.call(n&&n.instances[a],t,s,c));let u=Promise.resolve(d);e.length<3&&(u=u.then(c)),u.catch(f=>o(f))})}function so(e,t,s,n,a=i=>i()){const i=[];for(const l of e)for(const r in l.components){let o=l.components[r];if(!(t!=="beforeRouteEnter"&&!l.instances[r]))if(tm(o)){const c=(o.__vccOpts||o)[t];c&&i.push(Fn(c,s,n,l,r,a))}else{let c=o();i.push(()=>c.then(d=>{if(!d)throw new Error(`Couldn't resolve component "${r}" at "${l.path}"`);const u=j0(d)?d.default:d;l.mods[r]=d,l.components[r]=u;const f=(u.__vccOpts||u)[t];return f&&Fn(f,s,n,l,r,a)()}))}}return i}function w_(e,t){const s=[],n=[],a=[],i=Math.max(t.matched.length,e.matched.length);for(let l=0;lYa(c,r))?n.push(r):s.push(r));const o=e.matched[l];o&&(t.matched.find(c=>Ya(c,o))||a.push(o))}return[s,n,a]}/*! * vue-router v4.6.4 * (c) 2025 Eduardo San Martin Morote * @license MIT - */let S_=()=>location.protocol+"//"+location.host;function dm(e,t){const{pathname:s,search:n,hash:a}=t,i=e.indexOf("#");if(i>-1){let l=a.includes(e.slice(i))?e.slice(i).length:1,r=a.slice(l);return r[0]!=="/"&&(r="/"+r),vu(r,"")}return vu(s,e)+n+a}function T_(e,t,s,n){let a=[],i=[],l=null;const r=({state:f})=>{const p=dm(e,location),b=s.value,y=t.value;let A=0;if(f){if(s.value=p,t.value=f,l&&l===b){l=null;return}A=y?f.position-y.position:0}else n(p);a.forEach(O=>{O(s.value,b,{delta:A,type:Ho.pop,direction:A?A>0?to.forward:to.back:to.unknown})})};function o(){l=s.value}function c(f){a.push(f);const p=()=>{const b=a.indexOf(f);b>-1&&a.splice(b,1)};return i.push(p),p}function d(){if(document.visibilityState==="hidden"){const{history:f}=window;if(!f.state)return;f.replaceState(st({},f.state,{scroll:Nr()}),"")}}function u(){for(const f of i)f();i=[],window.removeEventListener("popstate",r),window.removeEventListener("pagehide",d),document.removeEventListener("visibilitychange",d)}return window.addEventListener("popstate",r),window.addEventListener("pagehide",d),document.addEventListener("visibilitychange",d),{pauseListeners:o,listen:c,destroy:u}}function ku(e,t,s,n=!1,a=!1){return{back:e,current:t,forward:s,replaced:n,position:window.history.length,scroll:a?Nr():null}}function C_(e){const{history:t,location:s}=window,n={value:dm(e,s)},a={value:t.state};a.value||i(n.value,{back:null,current:n.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(o,c,d){const u=e.indexOf("#"),f=u>-1?(s.host&&document.querySelector("base")?e:e.slice(u))+o:S_()+e+o;try{t[d?"replaceState":"pushState"](c,"",f),a.value=c}catch(p){console.error(p),s[d?"replace":"assign"](f)}}function l(o,c){i(o,st({},t.state,ku(a.value.back,o,a.value.forward,!0),c,{position:a.value.position}),!0),n.value=o}function r(o,c){const d=st({},a.value,t.state,{forward:o,scroll:Nr()});i(d.current,d,!0),i(o,st({},ku(n.value,o,null),{position:d.position+1},c),!1),n.value=o}return{location:n,state:a,push:r,replace:l}}function E_(e){e=d_(e);const t=C_(e),s=T_(e,t.state,t.location,t.replace);function n(i,l=!0){l||s.pauseListeners(),history.go(i)}const a=st({location:"",base:e,go:n,createHref:f_.bind(null,e)},t,s);return Object.defineProperty(a,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(a,"state",{enumerable:!0,get:()=>t.state.value}),a}function A_(e){return e=location.host?e||location.pathname+location.search:"",e.includes("#")||(e+="#"),E_(e)}let Yn=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e})({});var Ct=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e})(Ct||{});const R_={type:Yn.Static,value:""},I_=/[a-zA-Z0-9_]/;function O_(e){if(!e)return[[]];if(e==="/")return[[R_]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(p){throw new Error(`ERR (${s})/"${c}": ${p}`)}let s=Ct.Static,n=s;const a=[];let i;function l(){i&&a.push(i),i=[]}let r=0,o,c="",d="";function u(){c&&(s===Ct.Static?i.push({type:Yn.Static,value:c}):s===Ct.Param||s===Ct.ParamRegExp||s===Ct.ParamRegExpEnd?(i.length>1&&(o==="*"||o==="+")&&t(`A repeatable param (${c}) must be alone in its segment. eg: '/:ids+.`),i.push({type:Yn.Param,value:c,regexp:d,repeatable:o==="*"||o==="+",optional:o==="*"||o==="?"})):t("Invalid state to consume buffer"),c="")}function f(){c+=o}for(;rt.length?t.length===1&&t[0]===Yt.Static+Yt.Segment?1:-1:0}function um(e,t){let s=0;const n=e.score,a=t.score;for(;s0&&t[t.length-1]<0}const P_={strict:!1,end:!0,sensitive:!1};function F_(e,t,s){const n=D_(O_(e.path),s),a=st(n,{record:e,parent:t,children:[],alias:[]});return t&&!a.record.aliasOf==!t.record.aliasOf&&t.children.push(a),a}function $_(e,t){const s=[],n=new Map;t=gu(P_,t);function a(u){return n.get(u)}function i(u,f,p){const b=!p,y=Cu(u);y.aliasOf=p&&p.record;const A=gu(t,u),O=[y];if("alias"in u){const _=typeof u.alias=="string"?[u.alias]:u.alias;for(const S of _)O.push(Cu(st({},y,{components:p?p.record.components:y.components,path:S,aliasOf:p?p.record:y})))}let x,m;for(const _ of O){const{path:S}=_;if(f&&S[0]!=="/"){const g=f.record.path,w=g[g.length-1]==="/"?"":"/";_.path=f.record.path+(S&&w+S)}if(x=F_(_,f,A),p?p.alias.push(x):(m=m||x,m!==x&&m.alias.push(x),b&&u.name&&!Eu(x)&&l(u.name)),fm(x)&&o(x),y.children){const g=y.children;for(let w=0;w{l(m)}:ki}function l(u){if(om(u)){const f=n.get(u);f&&(n.delete(u),s.splice(s.indexOf(f),1),f.children.forEach(l),f.alias.forEach(l))}else{const f=s.indexOf(u);f>-1&&(s.splice(f,1),u.record.name&&n.delete(u.record.name),u.children.forEach(l),u.alias.forEach(l))}}function r(){return s}function o(u){const f=H_(u,s);s.splice(f,0,u),u.record.name&&!Eu(u)&&n.set(u.record.name,u)}function c(u,f){let p,b={},y,A;if("name"in u&&u.name){if(p=n.get(u.name),!p)throw Ka(mt.MATCHER_NOT_FOUND,{location:u});A=p.record.name,b=st(Tu(f.params,p.keys.filter(m=>!m.optional).concat(p.parent?p.parent.keys.filter(m=>m.optional):[]).map(m=>m.name)),u.params&&Tu(u.params,p.keys.map(m=>m.name))),y=p.stringify(b)}else if(u.path!=null)y=u.path,p=s.find(m=>m.re.test(y)),p&&(b=p.parse(y),A=p.record.name);else{if(p=f.name?n.get(f.name):s.find(m=>m.re.test(f.path)),!p)throw Ka(mt.MATCHER_NOT_FOUND,{location:u,currentLocation:f});A=p.record.name,b=st({},f.params,u.params),y=p.stringify(b)}const O=[];let x=p;for(;x;)O.unshift(x.record),x=x.parent;return{name:A,path:y,params:b,matched:O,meta:B_(O)}}e.forEach(u=>i(u));function d(){s.length=0,n.clear()}return{addRoute:i,resolve:c,removeRoute:l,clearRoutes:d,getRoutes:r,getRecordMatcher:a}}function Tu(e,t){const s={};for(const n of t)n in e&&(s[n]=e[n]);return s}function Cu(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:U_(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function U_(e){const t={},s=e.props||!1;if("component"in e)t.default=s;else for(const n in e.components)t[n]=typeof s=="object"?s[n]:s;return t}function Eu(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function B_(e){return e.reduce((t,s)=>st(t,s.meta),{})}function H_(e,t){let s=0,n=t.length;for(;s!==n;){const i=s+n>>1;um(e,t[i])<0?n=i:s=i+1}const a=V_(e);return a&&(n=t.lastIndexOf(a,n-1)),n}function V_(e){let t=e;for(;t=t.parent;)if(fm(t)&&um(e,t)===0)return t}function fm({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Au(e){const t=ws(Lr),s=ws(Kc),n=J(()=>{const o=Zs(e.to);return t.resolve(o)}),a=J(()=>{const{matched:o}=n.value,{length:c}=o,d=o[c-1],u=s.matched;if(!d||!u.length)return-1;const f=u.findIndex(Ga.bind(null,d));if(f>-1)return f;const p=Ru(o[c-2]);return c>1&&Ru(d)===p&&u[u.length-1].path!==p?u.findIndex(Ga.bind(null,o[c-2])):f}),i=J(()=>a.value>-1&&K_(s.params,n.value.params)),l=J(()=>a.value>-1&&a.value===s.matched.length-1&&rm(s.params,n.value.params));function r(o={}){if(G_(o)){const c=t[Zs(e.replace)?"replace":"push"](Zs(e.to)).catch(ki);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>c),c}return Promise.resolve()}return{route:n,href:J(()=>n.value.href),isActive:i,isExactActive:l,navigate:r}}function j_(e){return e.length===1?e[0]:e}const z_=Wi({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:Au,setup(e,{slots:t}){const s=Un(Au(e)),{options:n}=ws(Lr),a=J(()=>({[Iu(e.activeClass,n.linkActiveClass,"router-link-active")]:s.isActive,[Iu(e.exactActiveClass,n.linkExactActiveClass,"router-link-exact-active")]:s.isExactActive}));return()=>{const i=t.default&&j_(t.default(s));return e.custom?i:Ua("a",{"aria-current":s.isExactActive?e.ariaCurrentValue:null,href:s.href,onClick:s.navigate,class:a.value},i)}}}),q_=z_;function G_(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function K_(e,t){for(const s in t){const n=t[s],a=e[s];if(typeof n=="string"){if(n!==a)return!1}else if(!Fs(a)||a.length!==n.length||n.some((i,l)=>i.valueOf()!==a[l].valueOf()))return!1}return!0}function Ru(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Iu=(e,t,s)=>e??t??s,W_=Wi({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:s}){const n=ws(jo),a=J(()=>e.route||n.value),i=ws(_u,0),l=J(()=>{let c=Zs(i);const{matched:d}=a.value;let u;for(;(u=d[c])&&!u.components;)c++;return c}),r=J(()=>a.value.matched[l.value]);vi(_u,J(()=>l.value+1)),vi(k_,r),vi(jo,a);const o=h();return es(()=>[o.value,r.value,e.name],([c,d,u],[f,p,b])=>{d&&(d.instances[u]=c,p&&p!==d&&c&&c===f&&(d.leaveGuards.size||(d.leaveGuards=p.leaveGuards),d.updateGuards.size||(d.updateGuards=p.updateGuards))),c&&d&&(!p||!Ga(d,p)||!f)&&(d.enterCallbacks[u]||[]).forEach(y=>y(c))},{flush:"post"}),()=>{const c=a.value,d=e.name,u=r.value,f=u&&u.components[d];if(!f)return Ou(s.default,{Component:f,route:c});const p=u.props[d],b=p?p===!0?c.params:typeof p=="function"?p(c):p:null,A=Ua(f,st({},b,t,{onVnodeUnmounted:O=>{O.component.isUnmounted&&(u.instances[d]=null)},ref:o}));return Ou(s.default,{Component:A,route:c})||A}}});function Ou(e,t){if(!e)return null;const s=e(t);return s.length===1?s[0]:s}const Z_=W_;function J_(e){const t=$_(e.routes,e),s=e.parseQuery||x_,n=e.stringifyQuery||xu,a=e.history,i=li(),l=li(),r=li(),o=nc(Rn);let c=Rn;wa&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const d=Xr.bind(null,V=>""+V),u=Xr.bind(null,n_),f=Xr.bind(null,ji);function p(V,ce){let de,ve;return om(V)?(de=t.getRecordMatcher(V),ve=ce):ve=V,t.addRoute(ve,de)}function b(V){const ce=t.getRecordMatcher(V);ce&&t.removeRoute(ce)}function y(){return t.getRoutes().map(V=>V.record)}function A(V){return!!t.getRecordMatcher(V)}function O(V,ce){if(ce=st({},ce||o.value),typeof V=="string"){const E=eo(s,V,ce.path),U=t.resolve({path:E.path},ce),X=a.createHref(E.fullPath);return st(E,U,{params:f(U.params),hash:ji(E.hash),redirectedFrom:void 0,href:X})}let de;if(V.path!=null)de=st({},V,{path:eo(s,V.path,ce.path).path});else{const E=st({},V.params);for(const U in E)E[U]==null&&delete E[U];de=st({},V,{params:u(E)}),ce.params=u(ce.params)}const ve=t.resolve(de,ce),me=V.hash||"";ve.params=d(f(ve.params));const He=l_(n,st({},V,{hash:e_(me),path:ve.path})),k=a.createHref(He);return st({fullPath:He,hash:me,query:n===xu?__(V.query):V.query||{}},ve,{redirectedFrom:void 0,href:k})}function x(V){return typeof V=="string"?eo(s,V,o.value.path):st({},V)}function m(V,ce){if(c!==V)return Ka(mt.NAVIGATION_CANCELLED,{from:ce,to:V})}function _(V){return w(V)}function S(V){return _(st(x(V),{replace:!0}))}function g(V,ce){const de=V.matched[V.matched.length-1];if(de&&de.redirect){const{redirect:ve}=de;let me=typeof ve=="function"?ve(V,ce):ve;return typeof me=="string"&&(me=me.includes("?")||me.includes("#")?me=x(me):{path:me},me.params={}),st({query:V.query,hash:V.hash,params:me.path!=null?{}:V.params},me)}}function w(V,ce){const de=c=O(V),ve=o.value,me=V.state,He=V.force,k=V.replace===!0,E=g(de,ve);if(E)return w(st(x(E),{state:typeof E=="object"?st({},me,E.state):me,force:He,replace:k}),ce||de);const U=de;U.redirectedFrom=ce;let X;return!He&&r_(n,ve,de)&&(X=Ka(mt.NAVIGATION_DUPLICATED,{to:U,from:ve}),_e(ve,ve,!0,!1)),(X?Promise.resolve(X):M(U,ve)).catch(q=>an(q)?an(q,mt.NAVIGATION_GUARD_REDIRECT)?q:xe(q):L(q,U,ve)).then(q=>{if(q){if(an(q,mt.NAVIGATION_GUARD_REDIRECT))return w(st({replace:k},x(q.to),{state:typeof q.to=="object"?st({},me,q.to.state):me,force:He}),ce||U)}else q=$(U,ve,!0,k,me);return B(U,ve,q),q})}function T(V,ce){const de=m(V,ce);return de?Promise.reject(de):Promise.resolve()}function C(V){const ce=P.values().next().value;return ce&&typeof ce.runWithContext=="function"?ce.runWithContext(V):V()}function M(V,ce){let de;const[ve,me,He]=w_(V,ce);de=so(ve.reverse(),"beforeRouteLeave",V,ce);for(const E of ve)E.leaveGuards.forEach(U=>{de.push(Mn(U,V,ce))});const k=T.bind(null,V,ce);return de.push(k),ke(de).then(()=>{de=[];for(const E of i.list())de.push(Mn(E,V,ce));return de.push(k),ke(de)}).then(()=>{de=so(me,"beforeRouteUpdate",V,ce);for(const E of me)E.updateGuards.forEach(U=>{de.push(Mn(U,V,ce))});return de.push(k),ke(de)}).then(()=>{de=[];for(const E of He)if(E.beforeEnter)if(Fs(E.beforeEnter))for(const U of E.beforeEnter)de.push(Mn(U,V,ce));else de.push(Mn(E.beforeEnter,V,ce));return de.push(k),ke(de)}).then(()=>(V.matched.forEach(E=>E.enterCallbacks={}),de=so(He,"beforeRouteEnter",V,ce,C),de.push(k),ke(de))).then(()=>{de=[];for(const E of l.list())de.push(Mn(E,V,ce));return de.push(k),ke(de)}).catch(E=>an(E,mt.NAVIGATION_CANCELLED)?E:Promise.reject(E))}function B(V,ce,de){r.list().forEach(ve=>C(()=>ve(V,ce,de)))}function $(V,ce,de,ve,me){const He=m(V,ce);if(He)return He;const k=ce===Rn,E=wa?history.state:{};de&&(ve||k?a.replace(V.fullPath,st({scroll:k&&E&&E.scroll},me)):a.push(V.fullPath,me)),o.value=V,_e(V,ce,de,k),xe()}let I;function j(){I||(I=a.listen((V,ce,de)=>{if(!se.listening)return;const ve=O(V),me=g(ve,se.currentRoute.value);if(me){w(st(me,{replace:!0,force:!0}),ve).catch(ki);return}c=ve;const He=o.value;wa&&m_(yu(He.fullPath,de.delta),Nr()),M(ve,He).catch(k=>an(k,mt.NAVIGATION_ABORTED|mt.NAVIGATION_CANCELLED)?k:an(k,mt.NAVIGATION_GUARD_REDIRECT)?(w(st(x(k.to),{force:!0}),ve).then(E=>{an(E,mt.NAVIGATION_ABORTED|mt.NAVIGATION_DUPLICATED)&&!de.delta&&de.type===Ho.pop&&a.go(-1,!1)}).catch(ki),Promise.reject()):(de.delta&&a.go(-de.delta,!1),L(k,ve,He))).then(k=>{k=k||$(ve,He,!1),k&&(de.delta&&!an(k,mt.NAVIGATION_CANCELLED)?a.go(-de.delta,!1):de.type===Ho.pop&&an(k,mt.NAVIGATION_ABORTED|mt.NAVIGATION_DUPLICATED)&&a.go(-1,!1)),B(ve,He,k)}).catch(ki)}))}let Y=li(),H=li(),N;function L(V,ce,de){xe(V);const ve=H.list();return ve.length?ve.forEach(me=>me(V,ce,de)):console.error(V),Promise.reject(V)}function Z(){return N&&o.value!==Rn?Promise.resolve():new Promise((V,ce)=>{Y.add([V,ce])})}function xe(V){return N||(N=!V,j(),Y.list().forEach(([ce,de])=>V?de(V):ce()),Y.reset()),V}function _e(V,ce,de,ve){const{scrollBehavior:me}=e;if(!wa||!me)return Promise.resolve();const He=!de&&g_(yu(V.fullPath,0))||(ve||!de)&&history.state&&history.state.scroll||null;return At().then(()=>me(V,ce,He)).then(k=>k&&h_(k)).catch(k=>L(k,V,ce))}const ae=V=>a.go(V);let fe;const P=new Set,se={currentRoute:o,listening:!0,addRoute:p,removeRoute:b,clearRoutes:t.clearRoutes,hasRoute:A,getRoutes:y,resolve:O,options:e,push:_,replace:S,go:ae,back:()=>ae(-1),forward:()=>ae(1),beforeEach:i.add,beforeResolve:l.add,afterEach:r.add,onError:H.add,isReady:Z,install(V){V.component("RouterLink",q_),V.component("RouterView",Z_),V.config.globalProperties.$router=se,Object.defineProperty(V.config.globalProperties,"$route",{enumerable:!0,get:()=>Zs(o)}),wa&&!fe&&o.value===Rn&&(fe=!0,_(a.location).catch(ve=>{}));const ce={};for(const ve in Rn)Object.defineProperty(ce,ve,{get:()=>o.value[ve],enumerable:!0});V.provide(Lr,se),V.provide(Kc,sc(ce)),V.provide(jo,o);const de=V.unmount;P.add(V),V.unmount=function(){P.delete(V),P.size<1&&(c=Rn,I&&I(),I=null,o.value=Rn,fe=!1,N=!1),de()}}};function ke(V){return V.reduce((ce,de)=>ce.then(()=>C(de)),Promise.resolve())}return se}function pm(){return ws(Lr)}function Y_(e){return ws(Kc)}const Dr={props:{tabs:{type:Array,required:!0},defaultTab:{type:String,default:""},groupLabel:{type:String,default:""}},setup(e){const t=Y_(),s=pm(),n=J({get(){var o;const r=t.query.tab;return r&&e.tabs.some(c=>c.id===r)?r:e.defaultTab||((o=e.tabs[0])==null?void 0:o.id)||""},set(r){s.replace({query:{...t.query,tab:r}})}}),a=J(()=>{var r;return((r=e.tabs.find(o=>o.id===n.value))==null?void 0:r.component)||null}),i=J(()=>{var r;return((r=e.tabs.find(o=>o.id===n.value))==null?void 0:r.label)||""});es(i,r=>{e.groupLabel&&r&&(document.title=`Odin — ${e.groupLabel} › ${r}`)},{immediate:!0});function l(r,o){if(!["ArrowLeft","ArrowRight","Home","End"].includes(r.key))return;r.preventDefault();let c=o;r.key==="ArrowRight"&&(c=(o+1)%e.tabs.length),r.key==="ArrowLeft"&&(c=(o-1+e.tabs.length)%e.tabs.length),r.key==="Home"&&(c=0),r.key==="End"&&(c=e.tabs.length-1),n.value=e.tabs[c].id,requestAnimationFrame(()=>{var d;return(d=document.getElementById("tab-"+e.tabs[c].id))==null?void 0:d.focus()})}return{activeTab:n,activeComponent:a,activeLabel:i,onTabKeydown:l}},template:` + */let S_=()=>location.protocol+"//"+location.host;function dm(e,t){const{pathname:s,search:n,hash:a}=t,i=e.indexOf("#");if(i>-1){let l=a.includes(e.slice(i))?e.slice(i).length:1,r=a.slice(l);return r[0]!=="/"&&(r="/"+r),vu(r,"")}return vu(s,e)+n+a}function T_(e,t,s,n){let a=[],i=[],l=null;const r=({state:f})=>{const p=dm(e,location),b=s.value,y=t.value;let E=0;if(f){if(s.value=p,t.value=f,l&&l===b){l=null;return}E=y?f.position-y.position:0}else n(p);a.forEach(I=>{I(s.value,b,{delta:E,type:Ho.pop,direction:E?E>0?to.forward:to.back:to.unknown})})};function o(){l=s.value}function c(f){a.push(f);const p=()=>{const b=a.indexOf(f);b>-1&&a.splice(b,1)};return i.push(p),p}function d(){if(document.visibilityState==="hidden"){const{history:f}=window;if(!f.state)return;f.replaceState(nt({},f.state,{scroll:Lr()}),"")}}function u(){for(const f of i)f();i=[],window.removeEventListener("popstate",r),window.removeEventListener("pagehide",d),document.removeEventListener("visibilitychange",d)}return window.addEventListener("popstate",r),window.addEventListener("pagehide",d),document.addEventListener("visibilitychange",d),{pauseListeners:o,listen:c,destroy:u}}function ku(e,t,s,n=!1,a=!1){return{back:e,current:t,forward:s,replaced:n,position:window.history.length,scroll:a?Lr():null}}function C_(e){const{history:t,location:s}=window,n={value:dm(e,s)},a={value:t.state};a.value||i(n.value,{back:null,current:n.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(o,c,d){const u=e.indexOf("#"),f=u>-1?(s.host&&document.querySelector("base")?e:e.slice(u))+o:S_()+e+o;try{t[d?"replaceState":"pushState"](c,"",f),a.value=c}catch(p){console.error(p),s[d?"replace":"assign"](f)}}function l(o,c){i(o,nt({},t.state,ku(a.value.back,o,a.value.forward,!0),c,{position:a.value.position}),!0),n.value=o}function r(o,c){const d=nt({},a.value,t.state,{forward:o,scroll:Lr()});i(d.current,d,!0),i(o,nt({},ku(n.value,o,null),{position:d.position+1},c),!1),n.value=o}return{location:n,state:a,push:r,replace:l}}function E_(e){e=d_(e);const t=C_(e),s=T_(e,t.state,t.location,t.replace);function n(i,l=!0){l||s.pauseListeners(),history.go(i)}const a=nt({location:"",base:e,go:n,createHref:f_.bind(null,e)},t,s);return Object.defineProperty(a,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(a,"state",{enumerable:!0,get:()=>t.state.value}),a}function A_(e){return e=location.host?e||location.pathname+location.search:"",e.includes("#")||(e+="#"),E_(e)}let Xn=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e})({});var Et=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e})(Et||{});const R_={type:Xn.Static,value:""},I_=/[a-zA-Z0-9_]/;function O_(e){if(!e)return[[]];if(e==="/")return[[R_]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(p){throw new Error(`ERR (${s})/"${c}": ${p}`)}let s=Et.Static,n=s;const a=[];let i;function l(){i&&a.push(i),i=[]}let r=0,o,c="",d="";function u(){c&&(s===Et.Static?i.push({type:Xn.Static,value:c}):s===Et.Param||s===Et.ParamRegExp||s===Et.ParamRegExpEnd?(i.length>1&&(o==="*"||o==="+")&&t(`A repeatable param (${c}) must be alone in its segment. eg: '/:ids+.`),i.push({type:Xn.Param,value:c,regexp:d,repeatable:o==="*"||o==="+",optional:o==="*"||o==="?"})):t("Invalid state to consume buffer"),c="")}function f(){c+=o}for(;rt.length?t.length===1&&t[0]===es.Static+es.Segment?1:-1:0}function um(e,t){let s=0;const n=e.score,a=t.score;for(;s0&&t[t.length-1]<0}const P_={strict:!1,end:!0,sensitive:!1};function F_(e,t,s){const n=D_(O_(e.path),s),a=nt(n,{record:e,parent:t,children:[],alias:[]});return t&&!a.record.aliasOf==!t.record.aliasOf&&t.children.push(a),a}function $_(e,t){const s=[],n=new Map;t=gu(P_,t);function a(u){return n.get(u)}function i(u,f,p){const b=!p,y=Cu(u);y.aliasOf=p&&p.record;const E=gu(t,u),I=[y];if("alias"in u){const _=typeof u.alias=="string"?[u.alias]:u.alias;for(const S of _)I.push(Cu(nt({},y,{components:p?p.record.components:y.components,path:S,aliasOf:p?p.record:y})))}let x,m;for(const _ of I){const{path:S}=_;if(f&&S[0]!=="/"){const g=f.record.path,w=g[g.length-1]==="/"?"":"/";_.path=f.record.path+(S&&w+S)}if(x=F_(_,f,E),p?p.alias.push(x):(m=m||x,m!==x&&m.alias.push(x),b&&u.name&&!Eu(x)&&l(u.name)),fm(x)&&o(x),y.children){const g=y.children;for(let w=0;w{l(m)}:Ai}function l(u){if(om(u)){const f=n.get(u);f&&(n.delete(u),s.splice(s.indexOf(f),1),f.children.forEach(l),f.alias.forEach(l))}else{const f=s.indexOf(u);f>-1&&(s.splice(f,1),u.record.name&&n.delete(u.record.name),u.children.forEach(l),u.alias.forEach(l))}}function r(){return s}function o(u){const f=H_(u,s);s.splice(f,0,u),u.record.name&&!Eu(u)&&n.set(u.record.name,u)}function c(u,f){let p,b={},y,E;if("name"in u&&u.name){if(p=n.get(u.name),!p)throw Qa(mt.MATCHER_NOT_FOUND,{location:u});E=p.record.name,b=nt(Tu(f.params,p.keys.filter(m=>!m.optional).concat(p.parent?p.parent.keys.filter(m=>m.optional):[]).map(m=>m.name)),u.params&&Tu(u.params,p.keys.map(m=>m.name))),y=p.stringify(b)}else if(u.path!=null)y=u.path,p=s.find(m=>m.re.test(y)),p&&(b=p.parse(y),E=p.record.name);else{if(p=f.name?n.get(f.name):s.find(m=>m.re.test(f.path)),!p)throw Qa(mt.MATCHER_NOT_FOUND,{location:u,currentLocation:f});E=p.record.name,b=nt({},f.params,u.params),y=p.stringify(b)}const I=[];let x=p;for(;x;)I.unshift(x.record),x=x.parent;return{name:E,path:y,params:b,matched:I,meta:U_(I)}}e.forEach(u=>i(u));function d(){s.length=0,n.clear()}return{addRoute:i,resolve:c,removeRoute:l,clearRoutes:d,getRoutes:r,getRecordMatcher:a}}function Tu(e,t){const s={};for(const n of t)n in e&&(s[n]=e[n]);return s}function Cu(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:B_(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function B_(e){const t={},s=e.props||!1;if("component"in e)t.default=s;else for(const n in e.components)t[n]=typeof s=="object"?s[n]:s;return t}function Eu(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function U_(e){return e.reduce((t,s)=>nt(t,s.meta),{})}function H_(e,t){let s=0,n=t.length;for(;s!==n;){const i=s+n>>1;um(e,t[i])<0?n=i:s=i+1}const a=z_(e);return a&&(n=t.lastIndexOf(a,n-1)),n}function z_(e){let t=e;for(;t=t.parent;)if(fm(t)&&um(e,t)===0)return t}function fm({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function Au(e){const t=Os(Dr),s=Os(Kc),n=J(()=>{const o=en(e.to);return t.resolve(o)}),a=J(()=>{const{matched:o}=n.value,{length:c}=o,d=o[c-1],u=s.matched;if(!d||!u.length)return-1;const f=u.findIndex(Ya.bind(null,d));if(f>-1)return f;const p=Ru(o[c-2]);return c>1&&Ru(d)===p&&u[u.length-1].path!==p?u.findIndex(Ya.bind(null,o[c-2])):f}),i=J(()=>a.value>-1&&K_(s.params,n.value.params)),l=J(()=>a.value>-1&&a.value===s.matched.length-1&&rm(s.params,n.value.params));function r(o={}){if(G_(o)){const c=t[en(e.replace)?"replace":"push"](en(e.to)).catch(Ai);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>c),c}return Promise.resolve()}return{route:n,href:J(()=>n.value.href),isActive:i,isExactActive:l,navigate:r}}function V_(e){return e.length===1?e[0]:e}const j_=el({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:Au,setup(e,{slots:t}){const s=Hn(Au(e)),{options:n}=Os(Dr),a=J(()=>({[Iu(e.activeClass,n.linkActiveClass,"router-link-active")]:s.isActive,[Iu(e.exactActiveClass,n.linkExactActiveClass,"router-link-exact-active")]:s.isExactActive}));return()=>{const i=t.default&&V_(t.default(s));return e.custom?i:ja("a",{"aria-current":s.isExactActive?e.ariaCurrentValue:null,href:s.href,onClick:s.navigate,class:a.value},i)}}}),q_=j_;function G_(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function K_(e,t){for(const s in t){const n=t[s],a=e[s];if(typeof n=="string"){if(n!==a)return!1}else if(!qs(a)||a.length!==n.length||n.some((i,l)=>i.valueOf()!==a[l].valueOf()))return!1}return!0}function Ru(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Iu=(e,t,s)=>e??t??s,W_=el({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:s}){const n=Os(Vo),a=J(()=>e.route||n.value),i=Os(_u,0),l=J(()=>{let c=en(i);const{matched:d}=a.value;let u;for(;(u=d[c])&&!u.components;)c++;return c}),r=J(()=>a.value.matched[l.value]);wi(_u,J(()=>l.value+1)),wi(k_,r),wi(Vo,a);const o=h();return ns(()=>[o.value,r.value,e.name],([c,d,u],[f,p,b])=>{d&&(d.instances[u]=c,p&&p!==d&&c&&c===f&&(d.leaveGuards.size||(d.leaveGuards=p.leaveGuards),d.updateGuards.size||(d.updateGuards=p.updateGuards))),c&&d&&(!p||!Ya(d,p)||!f)&&(d.enterCallbacks[u]||[]).forEach(y=>y(c))},{flush:"post"}),()=>{const c=a.value,d=e.name,u=r.value,f=u&&u.components[d];if(!f)return Ou(s.default,{Component:f,route:c});const p=u.props[d],b=p?p===!0?c.params:typeof p=="function"?p(c):p:null,E=ja(f,nt({},b,t,{onVnodeUnmounted:I=>{I.component.isUnmounted&&(u.instances[d]=null)},ref:o}));return Ou(s.default,{Component:E,route:c})||E}}});function Ou(e,t){if(!e)return null;const s=e(t);return s.length===1?s[0]:s}const Z_=W_;function J_(e){const t=$_(e.routes,e),s=e.parseQuery||x_,n=e.stringifyQuery||xu,a=e.history,i=fi(),l=fi(),r=fi(),o=nc(On);let c=On;Aa&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const d=Xr.bind(null,V=>""+V),u=Xr.bind(null,n_),f=Xr.bind(null,Zi);function p(V,de){let ce,ye;return om(V)?(ce=t.getRecordMatcher(V),ye=de):ye=V,t.addRoute(ye,ce)}function b(V){const de=t.getRecordMatcher(V);de&&t.removeRoute(de)}function y(){return t.getRoutes().map(V=>V.record)}function E(V){return!!t.getRecordMatcher(V)}function I(V,de){if(de=nt({},de||o.value),typeof V=="string"){const L=eo(s,V,de.path),$=t.resolve({path:L.path},de),ee=a.createHref(L.fullPath);return nt(L,$,{params:f($.params),hash:Zi(L.hash),redirectedFrom:void 0,href:ee})}let ce;if(V.path!=null)ce=nt({},V,{path:eo(s,V.path,de.path).path});else{const L=nt({},V.params);for(const $ in L)L[$]==null&&delete L[$];ce=nt({},V,{params:u(L)}),de.params=u(de.params)}const ye=t.resolve(ce,de),ge=V.hash||"";ye.params=d(f(ye.params));const He=l_(n,nt({},V,{hash:e_(ge),path:ye.path})),k=a.createHref(He);return nt({fullPath:He,hash:ge,query:n===xu?__(V.query):V.query||{}},ye,{redirectedFrom:void 0,href:k})}function x(V){return typeof V=="string"?eo(s,V,o.value.path):nt({},V)}function m(V,de){if(c!==V)return Qa(mt.NAVIGATION_CANCELLED,{from:de,to:V})}function _(V){return w(V)}function S(V){return _(nt(x(V),{replace:!0}))}function g(V,de){const ce=V.matched[V.matched.length-1];if(ce&&ce.redirect){const{redirect:ye}=ce;let ge=typeof ye=="function"?ye(V,de):ye;return typeof ge=="string"&&(ge=ge.includes("?")||ge.includes("#")?ge=x(ge):{path:ge},ge.params={}),nt({query:V.query,hash:V.hash,params:ge.path!=null?{}:V.params},ge)}}function w(V,de){const ce=c=I(V),ye=o.value,ge=V.state,He=V.force,k=V.replace===!0,L=g(ce,ye);if(L)return w(nt(x(L),{state:typeof L=="object"?nt({},ge,L.state):ge,force:He,replace:k}),de||ce);const $=ce;$.redirectedFrom=de;let ee;return!He&&r_(n,ye,ce)&&(ee=Qa(mt.NAVIGATION_DUPLICATED,{to:$,from:ye}),ke(ye,ye,!0,!1)),(ee?Promise.resolve(ee):M($,ye)).catch(Z=>rn(Z)?rn(Z,mt.NAVIGATION_GUARD_REDIRECT)?Z:we(Z):N(Z,$,ye)).then(Z=>{if(Z){if(rn(Z,mt.NAVIGATION_GUARD_REDIRECT))return w(nt({replace:k},x(Z.to),{state:typeof Z.to=="object"?nt({},ge,Z.to.state):ge,force:He}),de||$)}else Z=P($,ye,!0,k,ge);return H($,ye,Z),Z})}function T(V,de){const ce=m(V,de);return ce?Promise.reject(ce):Promise.resolve()}function C(V){const de=F.values().next().value;return de&&typeof de.runWithContext=="function"?de.runWithContext(V):V()}function M(V,de){let ce;const[ye,ge,He]=w_(V,de);ce=so(ye.reverse(),"beforeRouteLeave",V,de);for(const L of ye)L.leaveGuards.forEach($=>{ce.push(Fn($,V,de))});const k=T.bind(null,V,de);return ce.push(k),Se(ce).then(()=>{ce=[];for(const L of i.list())ce.push(Fn(L,V,de));return ce.push(k),Se(ce)}).then(()=>{ce=so(ge,"beforeRouteUpdate",V,de);for(const L of ge)L.updateGuards.forEach($=>{ce.push(Fn($,V,de))});return ce.push(k),Se(ce)}).then(()=>{ce=[];for(const L of He)if(L.beforeEnter)if(qs(L.beforeEnter))for(const $ of L.beforeEnter)ce.push(Fn($,V,de));else ce.push(Fn(L.beforeEnter,V,de));return ce.push(k),Se(ce)}).then(()=>(V.matched.forEach(L=>L.enterCallbacks={}),ce=so(He,"beforeRouteEnter",V,de,C),ce.push(k),Se(ce))).then(()=>{ce=[];for(const L of l.list())ce.push(Fn(L,V,de));return ce.push(k),Se(ce)}).catch(L=>rn(L,mt.NAVIGATION_CANCELLED)?L:Promise.reject(L))}function H(V,de,ce){r.list().forEach(ye=>C(()=>ye(V,de,ce)))}function P(V,de,ce,ye,ge){const He=m(V,de);if(He)return He;const k=de===On,L=Aa?history.state:{};ce&&(ye||k?a.replace(V.fullPath,nt({scroll:k&&L&&L.scroll},ge)):a.push(V.fullPath,ge)),o.value=V,ke(V,de,ce,k),we()}let R;function j(){R||(R=a.listen((V,de,ce)=>{if(!se.listening)return;const ye=I(V),ge=g(ye,se.currentRoute.value);if(ge){w(nt(ge,{replace:!0,force:!0}),ye).catch(Ai);return}c=ye;const He=o.value;Aa&&m_(yu(He.fullPath,ce.delta),Lr()),M(ye,He).catch(k=>rn(k,mt.NAVIGATION_ABORTED|mt.NAVIGATION_CANCELLED)?k:rn(k,mt.NAVIGATION_GUARD_REDIRECT)?(w(nt(x(k.to),{force:!0}),ye).then(L=>{rn(L,mt.NAVIGATION_ABORTED|mt.NAVIGATION_DUPLICATED)&&!ce.delta&&ce.type===Ho.pop&&a.go(-1,!1)}).catch(Ai),Promise.reject()):(ce.delta&&a.go(-ce.delta,!1),N(k,ye,He))).then(k=>{k=k||P(ye,He,!1),k&&(ce.delta&&!rn(k,mt.NAVIGATION_CANCELLED)?a.go(-ce.delta,!1):ce.type===Ho.pop&&rn(k,mt.NAVIGATION_ABORTED|mt.NAVIGATION_DUPLICATED)&&a.go(-1,!1)),H(ye,He,k)}).catch(Ai)}))}let Q=fi(),U=fi(),O;function N(V,de,ce){we(V);const ye=U.list();return ye.length?ye.forEach(ge=>ge(V,de,ce)):console.error(V),Promise.reject(V)}function Y(){return O&&o.value!==On?Promise.resolve():new Promise((V,de)=>{Q.add([V,de])})}function we(V){return O||(O=!V,j(),Q.list().forEach(([de,ce])=>V?ce(V):de()),Q.reset()),V}function ke(V,de,ce,ye){const{scrollBehavior:ge}=e;if(!Aa||!ge)return Promise.resolve();const He=!ce&&g_(yu(V.fullPath,0))||(ye||!ce)&&history.state&&history.state.scroll||null;return Rt().then(()=>ge(V,de,He)).then(k=>k&&h_(k)).catch(k=>N(k,V,de))}const ie=V=>a.go(V);let he;const F=new Set,se={currentRoute:o,listening:!0,addRoute:p,removeRoute:b,clearRoutes:t.clearRoutes,hasRoute:E,getRoutes:y,resolve:I,options:e,push:_,replace:S,go:ie,back:()=>ie(-1),forward:()=>ie(1),beforeEach:i.add,beforeResolve:l.add,afterEach:r.add,onError:U.add,isReady:Y,install(V){V.component("RouterLink",q_),V.component("RouterView",Z_),V.config.globalProperties.$router=se,Object.defineProperty(V.config.globalProperties,"$route",{enumerable:!0,get:()=>en(o)}),Aa&&!he&&o.value===On&&(he=!0,_(a.location).catch(ye=>{}));const de={};for(const ye in On)Object.defineProperty(de,ye,{get:()=>o.value[ye],enumerable:!0});V.provide(Dr,se),V.provide(Kc,sc(de)),V.provide(Vo,o);const ce=V.unmount;F.add(V),V.unmount=function(){F.delete(V),F.size<1&&(c=On,R&&R(),R=null,o.value=On,he=!1,O=!1),ce()}}};function Se(V){return V.reduce((de,ce)=>de.then(()=>C(ce)),Promise.resolve())}return se}function pm(){return Os(Dr)}function Y_(e){return Os(Kc)}const Mr={props:{tabs:{type:Array,required:!0},defaultTab:{type:String,default:""},groupLabel:{type:String,default:""}},setup(e){const t=Y_(),s=pm(),n=J({get(){var o;const r=t.query.tab;return r&&e.tabs.some(c=>c.id===r)?r:e.defaultTab||((o=e.tabs[0])==null?void 0:o.id)||""},set(r){s.replace({query:{...t.query,tab:r}})}}),a=J(()=>{var r;return((r=e.tabs.find(o=>o.id===n.value))==null?void 0:r.component)||null}),i=J(()=>{var r;return((r=e.tabs.find(o=>o.id===n.value))==null?void 0:r.label)||""});ns(i,r=>{e.groupLabel&&r&&(document.title=`Odin — ${e.groupLabel} › ${r}`)},{immediate:!0});function l(r,o){if(!["ArrowLeft","ArrowRight","Home","End"].includes(r.key))return;r.preventDefault();let c=o;r.key==="ArrowRight"&&(c=(o+1)%e.tabs.length),r.key==="ArrowLeft"&&(c=(o-1+e.tabs.length)%e.tabs.length),r.key==="Home"&&(c=0),r.key==="End"&&(c=e.tabs.length-1),n.value=e.tabs[c].id,requestAnimationFrame(()=>{var d;return(d=document.getElementById("tab-"+e.tabs[c].id))==null?void 0:d.focus()})}return{activeTab:n,activeComponent:a,activeLabel:i,onTabKeydown:l}},template:`
@@ -90,9 +90,9 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `},Q_={setup(){const e=h([]),t=h([]),s=h({}),n=50;function a(f){var y,A,O,x,m;const p=f.payload||f,b=p.type||f.type;if(b==="tool_start"){const _=((y=p.metadata)==null?void 0:y.call_id)||null,S={callId:_,id:_||`${p.action}-${Date.now()}`,tool:p.action,actor:p.actor||"",channel:p.channel_id||"",iteration:((A=p.metadata)==null?void 0:A.iteration)??0,startTime:Date.now(),elapsed:0,status:"running",output:"",result:""};e.value.unshift(S);return}if(b==="tool_end"){const _=((O=p.metadata)==null?void 0:O.call_id)||null;let S=-1;if(_&&(S=e.value.findIndex(g=>g.callId===_&&g.status==="running")),S<0&&!_)for(let g=e.value.length-1;g>=0;g--){const w=e.value[g];if(w.tool===p.action&&w.status==="running"){S=g;break}}if(S>=0){const g=e.value[S];g.status=(x=p.metadata)!=null&&x.error?"error":"success",g.elapsed=((m=p.metadata)==null?void 0:m.elapsed_ms)||Date.now()-g.startTime,g.result=p.detail||"",g.fadingOut=!0,setTimeout(()=>{const w=e.value.indexOf(g);w>=0&&e.value.splice(w,1),t.value.unshift(g),t.value.length>n&&t.value.pop()},5e3)}return}if(b==="tool_stream"){const _=p.call_id||p.tool_name||"unknown";if(p.finished){const S={...s.value};delete S[_],s.value=S}else{const g=((s.value[_]||"")+(p.chunk||"")).split(` + `},Q_={setup(){const e=h([]),t=h([]),s=h({}),n=50;function a(f){var y,E,I,x,m;const p=f.payload||f,b=p.type||f.type;if(b==="tool_start"){const _=((y=p.metadata)==null?void 0:y.call_id)||null,S={callId:_,id:_||`${p.action}-${Date.now()}`,tool:p.action,actor:p.actor||"",channel:p.channel_id||"",iteration:((E=p.metadata)==null?void 0:E.iteration)??0,startTime:Date.now(),elapsed:0,status:"running",output:"",result:""};e.value.unshift(S);return}if(b==="tool_end"){const _=((I=p.metadata)==null?void 0:I.call_id)||null;let S=-1;if(_&&(S=e.value.findIndex(g=>g.callId===_&&g.status==="running")),S<0&&!_)for(let g=e.value.length-1;g>=0;g--){const w=e.value[g];if(w.tool===p.action&&w.status==="running"){S=g;break}}if(S>=0){const g=e.value[S];g.status=(x=p.metadata)!=null&&x.error?"error":"success",g.elapsed=((m=p.metadata)==null?void 0:m.elapsed_ms)||Date.now()-g.startTime,g.result=p.detail||"",g.fadingOut=!0,setTimeout(()=>{const w=e.value.indexOf(g);w>=0&&e.value.splice(w,1),t.value.unshift(g),t.value.length>n&&t.value.pop()},5e3)}return}if(b==="tool_stream"){const _=p.call_id||p.tool_name||"unknown";if(p.finished){const S={...s.value};delete S[_],s.value=S}else{const g=((s.value[_]||"")+(p.chunk||"")).split(` `);s.value={...s.value,[_]:g.slice(-30).join(` -`)}}return}}let i=null;function l(){const f=Date.now();e.value.forEach(p=>{p.status==="running"&&(p.elapsed=f-p.startTime)})}let r=!1;function o(){r||(r=!0,Ke.on("events",a),i||(i=setInterval(l,500)))}function c(){r&&(r=!1,Ke.off("events",a),i&&(clearInterval(i),i=null))}We(o),Cs(o),Es(c),xt(c);function d(f){return f<1e3?`${f}ms`:`${(f/1e3).toFixed(1)}s`}function u(f){return f==="running"?"clock":f==="success"?"success":f==="error"?"error":"info"}return{activeTasks:e,recentHistory:t,streamOutput:s,formatMs:d,statusIcon:u}},template:` +`)}}return}}let i=null;function l(){const f=Date.now();e.value.forEach(p=>{p.status==="running"&&(p.elapsed=f-p.startTime)})}let r=!1;function o(){r||(r=!0,Ke.on("events",a),i||(i=setInterval(l,500)))}function c(){r&&(r=!1,Ke.off("events",a),i&&(clearInterval(i),i=null))}We(o),Ds(o),Ms(c),xt(c);function d(f){return f<1e3?`${f}ms`:`${(f/1e3).toFixed(1)}s`}function u(f){return f==="running"?"clock":f==="success"?"success":f==="error"?"error":"info"}return{activeTasks:e,recentHistory:t,streamOutput:s,formatMs:d,statusIcon:u}},template:`

Execution Viewer @@ -155,8 +155,8 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config

- `};function Wc(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 ua(e){const t=Wc(e);return t?t.toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):"—"}function hm(e){const t=Wc(e);return t?t.toLocaleTimeString():"—"}function mm(e){const t=Wc(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 X_(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 Wa(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 a=Math.floor(t/60),i=t%60;return i?`${a}m ${i}s`:`${a}m`}const s=Math.floor(t/3600),n=Math.floor(t%3600/60);return n?`${s}h ${n}m`:`${s}h`}function Zc(e,t=200){const s=String(e??"");return s.length>t?s.slice(0,t)+"…":s}function gm(e,t=5e3){const s=String(e??"");return s.length>t?s.slice(0,t)+` -... (truncated)`:s}function Nu(e){return String(e??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function vm(e){return e==null||!isFinite(e)?"—":Number(e).toLocaleString()}function bm(e){return e==null||!isFinite(e)?"—":e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}const ym=Symbol("agent-detail-cancelled"),ek=15e3;function tk(e,{timeoutMs:t,timeoutLabel:s,scheduleTimeout:n,cancelTimeout:a}){const i=typeof AbortController=="function"?new AbortController:null;let l=null,r=!1,o,c;const d=new Promise((p,b)=>{o=p,c=b});function u(p,b){r||(r=!0,l!==null&&a(l),l=null,(p?o:c)(b))}let f;try{f=e(i==null?void 0:i.signal)}catch(p){u(!1,p)}return r||Promise.resolve(f).then(p=>u(!0,p),p=>u(!1,p)),!r&&Number.isFinite(t)&&t>0&&(l=n(()=>{const p=Math.max(1,Math.round(t/1e3));u(!1,new Error(`${s} request timed out after ${p}s`)),i==null||i.abort()},t)),{promise:d,cancel(){u(!0,ym),i==null||i.abort()}}}function xm({state:e,requestDetail:t,timeoutMs:s=ek,detailLabel:n="Agent detail",scheduleTimeout:a=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 r(){const f=l;l=null,f==null||f.cancel()}function o(f,{initial:p,coalesce:b}){if(!f)return Promise.resolve();if(b&&l&&l.agentId===f&&e.detailId===f)return l.promise;r();const y={agentId:f,cancel:null,promise:null};l=y,p?(e.detail=null,e.detailError=null,e.detailLoading=!0):e.detail===null&&e.detailError===null&&(e.detailLoading=!0);const A=tk(O=>t(f,{signal:O}),{timeoutMs:s,timeoutLabel:n,scheduleTimeout:a,cancelTimeout:i});return y.cancel=A.cancel,y.promise=(async()=>{let O=null,x=null;try{O=await A.promise}catch(m){x=m}O!==ym&&(l!==y||e.detailId!==f||(l=null,!x&&(O===null||typeof O!="object")&&(x=new Error(`${n} response was empty or invalid`)),x?e.detail===null&&(e.detailError=(x==null?void 0:x.message)||`Failed to load ${n.toLowerCase()}`):(e.detail=O,e.detailError=null),e.detailLoading=!1))})(),y.promise}function c(f){return e.detailId=f,o(f,{initial:!0,coalesce:!1})}function d(){const f=e.detailId;return f?o(f,{initial:!1,coalesce:!0}):Promise.resolve()}function u(){r(),e.detailId=null,e.detail=null,e.detailError=null,e.detailLoading=!1}return{open:c,refresh:d,close:u,hasInFlight:()=>l!==null}}function sk({isEnabled:e,refreshList:t,hasOpenDetail:s,refreshDetail:n,intervalMs:a=5e3,scheduleInterval:i=globalThis.setInterval.bind(globalThis),cancelInterval:l=globalThis.clearInterval.bind(globalThis)}){let r=null;function o(){e()&&(t(),s()&&n())}function c(){r!==null&&(l(r),r=null)}function d(){c(),e()&&(r=i(o,a))}function u(){e()?d():c()}return{start:d,stop:c,sync:u,isRunning:()=>r!==null}}const nk={template:` + `};function Wc(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 pa(e){const t=Wc(e);return t?t.toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):"—"}function hm(e){const t=Wc(e);return t?t.toLocaleTimeString():"—"}function mm(e){const t=Wc(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 X_(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 Xa(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 a=Math.floor(t/60),i=t%60;return i?`${a}m ${i}s`:`${a}m`}const s=Math.floor(t/3600),n=Math.floor(t%3600/60);return n?`${s}h ${n}m`:`${s}h`}function Zc(e,t=200){const s=String(e??"");return s.length>t?s.slice(0,t)+"…":s}function gm(e,t=5e3){const s=String(e??"");return s.length>t?s.slice(0,t)+` +... (truncated)`:s}function Nu(e){return String(e??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function vm(e){return e==null||!isFinite(e)?"—":Number(e).toLocaleString()}function bm(e){return e==null||!isFinite(e)?"—":e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}const ym=Symbol("agent-detail-cancelled"),ek=15e3;function tk(e,{timeoutMs:t,timeoutLabel:s,scheduleTimeout:n,cancelTimeout:a}){const i=typeof AbortController=="function"?new AbortController:null;let l=null,r=!1,o,c;const d=new Promise((p,b)=>{o=p,c=b});function u(p,b){r||(r=!0,l!==null&&a(l),l=null,(p?o:c)(b))}let f;try{f=e(i==null?void 0:i.signal)}catch(p){u(!1,p)}return r||Promise.resolve(f).then(p=>u(!0,p),p=>u(!1,p)),!r&&Number.isFinite(t)&&t>0&&(l=n(()=>{const p=Math.max(1,Math.round(t/1e3));u(!1,new Error(`${s} request timed out after ${p}s`)),i==null||i.abort()},t)),{promise:d,cancel(){u(!0,ym),i==null||i.abort()}}}function xm({state:e,requestDetail:t,timeoutMs:s=ek,detailLabel:n="Agent detail",scheduleTimeout:a=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 r(){const f=l;l=null,f==null||f.cancel()}function o(f,{initial:p,coalesce:b}){if(!f)return Promise.resolve();if(b&&l&&l.agentId===f&&e.detailId===f)return l.promise;r();const y={agentId:f,cancel:null,promise:null};l=y,p?(e.detail=null,e.detailError=null,e.detailLoading=!0):e.detail===null&&e.detailError===null&&(e.detailLoading=!0);const E=tk(I=>t(f,{signal:I}),{timeoutMs:s,timeoutLabel:n,scheduleTimeout:a,cancelTimeout:i});return y.cancel=E.cancel,y.promise=(async()=>{let I=null,x=null;try{I=await E.promise}catch(m){x=m}I!==ym&&(l!==y||e.detailId!==f||(l=null,!x&&(I===null||typeof I!="object")&&(x=new Error(`${n} response was empty or invalid`)),x?e.detail===null&&(e.detailError=(x==null?void 0:x.message)||`Failed to load ${n.toLowerCase()}`):(e.detail=I,e.detailError=null),e.detailLoading=!1))})(),y.promise}function c(f){return e.detailId=f,o(f,{initial:!0,coalesce:!1})}function d(){const f=e.detailId;return f?o(f,{initial:!1,coalesce:!0}):Promise.resolve()}function u(){r(),e.detailId=null,e.detail=null,e.detailError=null,e.detailLoading=!1}return{open:c,refresh:d,close:u,hasInFlight:()=>l!==null}}function sk({isEnabled:e,refreshList:t,hasOpenDetail:s,refreshDetail:n,intervalMs:a=5e3,scheduleInterval:i=globalThis.setInterval.bind(globalThis),cancelInterval:l=globalThis.clearInterval.bind(globalThis)}){let r=null;function o(){e()&&(t(),s()&&n())}function c(){r!==null&&(l(r),r=null)}function d(){c(),e()&&(r=i(o,a))}function u(){e()?d():c()}return{start:d,stop:c,sync:u,isRunning:()=>r!==null}}const nk={template:`

Agents

@@ -430,7 +430,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
-
`,setup(){const e=h([]),t=h(!0),s=h(null),n=h(null),a=h(!0),i=h("all");let l=!1;const r=J(()=>e.value.filter(L=>L.status==="running").length),o=J(()=>e.value.filter(L=>L.status==="completed").length),c=J(()=>e.value.filter(L=>["failed","timeout","killed"].includes(L.status)).length),d=J(()=>[{value:"all",label:"All",count:e.value.length},{value:"running",label:"Running",count:r.value},{value:"completed",label:"Completed",count:o.value},{value:"failed",label:"Failed",count:c.value}]),u=J(()=>i.value==="all"?e.value:i.value==="failed"?e.value.filter(L=>["failed","timeout","killed"].includes(L.status)):e.value.filter(L=>L.status===i.value));function f(L){const Z=Number(L.max_iterations)||0;return Z<=0?0:Math.min(100,Math.round(L.iteration_count/Z*100))}function p(L){return(Number(L.max_iterations)||0)>0}function b(L,Z){return L?L==="N/A"?"N/A":Z==="current_inheritance"?`inherit (currently ${L})`:L:"unknown"}function y(L){return b(L.display_model,L.display_model_source||L.display_source)}function A(L){return b(L.display_reasoning_effort,L.display_reasoning_effort_source||L.display_source)}function O(L){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"}[L]||""}const x=h(null),m=h(null),_=h(!1),S=h(null),g=h(""),T=xm({state:{get detail(){return x.value},set detail(L){x.value=L},get detailId(){return m.value},set detailId(L){m.value=L},get detailLoading(){return _.value},set detailLoading(L){_.value=L},get detailError(){return S.value},set detailError(L){S.value=L}},requestDetail:(L,{signal:Z})=>K.get(`/api/agents/${encodeURIComponent(L)}`,{signal:Z})});async function C(L){g.value="",await T.open(L.id)}function M(){T.close(),g.value=""}async function B(){await T.refresh()}async function $(L,Z){try{await navigator.clipboard.writeText(Z||""),g.value=L,setTimeout(()=>{g.value===L&&(g.value="")},1500)}catch{Te.error("Copy failed")}}async function I(L=!1){L=L===!0,L||(t.value=!0);try{const Z=await K.get("/api/agents");e.value=Array.isArray(Z)?Z:[],s.value=null}catch(Z){L||(s.value=Z.message)}L||(t.value=!1)}async function j(L){const Z=e.value.find(_e=>_e.id===L);if(await gs({title:"Kill agent",message:`Kill agent "${(Z==null?void 0:Z.label)||L}"? Its current work will be lost.`,confirmLabel:"Kill",danger:!0})){n.value=L;try{await K.del(`/api/agents/${encodeURIComponent(L)}`),Te.success("Agent killed"),await I()}catch(_e){Te.error(_e.message||"Failed to kill agent")}n.value=null}}const Y=sk({isEnabled:()=>a.value&&l,refreshList:()=>I(!0),hasOpenDetail:()=>!!m.value,refreshDetail:B});function H(){Y.start()}function N(){Y.stop()}return es(a,()=>Y.sync()),We(()=>{l=!0,I(),H()}),Cs(()=>{l=!0,I(!0),H()}),Es(()=>{l=!1,N()}),xt(()=>{l=!1,N(),T.close()}),{agents:e,loading:t,error:s,killing:n,autoRefresh:a,statusFilter:i,runningCount:r,completedCount:o,failedCount:c,statusFilters:d,filteredAgents:u,formatTs:ua,formatDuration:Wa,progressPercent:f,hasProgress:p,displayModelText:y,displayEffortText:A,displaySourceLabel:O,detail:x,detailId:m,detailLoading:_,detailError:S,copied:g,openDetail:C,closeDetail:M,copyText:$,fetchAgents:I,killAgent:j,startAutoRefresh:H,stopAutoRefresh:N}}},ak={template:` +
`,setup(){const e=h([]),t=h(!0),s=h(null),n=h(null),a=h(!0),i=h("all");let l=!1;const r=J(()=>e.value.filter(N=>N.status==="running").length),o=J(()=>e.value.filter(N=>N.status==="completed").length),c=J(()=>e.value.filter(N=>["failed","timeout","killed"].includes(N.status)).length),d=J(()=>[{value:"all",label:"All",count:e.value.length},{value:"running",label:"Running",count:r.value},{value:"completed",label:"Completed",count:o.value},{value:"failed",label:"Failed",count:c.value}]),u=J(()=>i.value==="all"?e.value:i.value==="failed"?e.value.filter(N=>["failed","timeout","killed"].includes(N.status)):e.value.filter(N=>N.status===i.value));function f(N){const Y=Number(N.max_iterations)||0;return Y<=0?0:Math.min(100,Math.round(N.iteration_count/Y*100))}function p(N){return(Number(N.max_iterations)||0)>0}function b(N,Y){return N?N==="N/A"?"N/A":Y==="current_inheritance"?`inherit (currently ${N})`:N:"unknown"}function y(N){return b(N.display_model,N.display_model_source||N.display_source)}function E(N){return b(N.display_reasoning_effort,N.display_reasoning_effort_source||N.display_source)}function I(N){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"}[N]||""}const x=h(null),m=h(null),_=h(!1),S=h(null),g=h(""),T=xm({state:{get detail(){return x.value},set detail(N){x.value=N},get detailId(){return m.value},set detailId(N){m.value=N},get detailLoading(){return _.value},set detailLoading(N){_.value=N},get detailError(){return S.value},set detailError(N){S.value=N}},requestDetail:(N,{signal:Y})=>G.get(`/api/agents/${encodeURIComponent(N)}`,{signal:Y})});async function C(N){g.value="",await T.open(N.id)}function M(){T.close(),g.value=""}async function H(){await T.refresh()}async function P(N,Y){try{await navigator.clipboard.writeText(Y||""),g.value=N,setTimeout(()=>{g.value===N&&(g.value="")},1500)}catch{Ae.error("Copy failed")}}async function R(N=!1){N=N===!0,N||(t.value=!0);try{const Y=await G.get("/api/agents");e.value=Array.isArray(Y)?Y:[],s.value=null}catch(Y){N||(s.value=Y.message)}N||(t.value=!1)}async function j(N){const Y=e.value.find(ke=>ke.id===N);if(await _s({title:"Kill agent",message:`Kill agent "${(Y==null?void 0:Y.label)||N}"? Its current work will be lost.`,confirmLabel:"Kill",danger:!0})){n.value=N;try{await G.del(`/api/agents/${encodeURIComponent(N)}`),Ae.success("Agent killed"),await R()}catch(ke){Ae.error(ke.message||"Failed to kill agent")}n.value=null}}const Q=sk({isEnabled:()=>a.value&&l,refreshList:()=>R(!0),hasOpenDetail:()=>!!m.value,refreshDetail:H});function U(){Q.start()}function O(){Q.stop()}return ns(a,()=>Q.sync()),We(()=>{l=!0,R(),U()}),Ds(()=>{l=!0,R(!0),U()}),Ms(()=>{l=!1,O()}),xt(()=>{l=!1,O(),T.close()}),{agents:e,loading:t,error:s,killing:n,autoRefresh:a,statusFilter:i,runningCount:r,completedCount:o,failedCount:c,statusFilters:d,filteredAgents:u,formatTs:pa,formatDuration:Xa,progressPercent:f,hasProgress:p,displayModelText:y,displayEffortText:E,displaySourceLabel:I,detail:x,detailId:m,detailLoading:_,detailError:S,copied:g,openDetail:C,closeDetail:M,copyText:P,fetchAgents:R,killAgent:j,startAutoRefresh:U,stopAutoRefresh:O}}},ak={template:`

Autonomous Loops

@@ -696,7 +696,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(!1),a=h({goal:"",interval_seconds:60,mode:"notify",max_iterations:50,stop_condition:"",channel_id:""}),i=h(!1),l=h(null),r=h(null),o=h(null),c=h(null),d=h(null),u=h(!1),f=h(null),p=h("");let b=!1;const A=xm({state:{get detail(){return c.value},set detail(N){c.value=N},get detailId(){return d.value},set detailId(N){d.value=N},get detailLoading(){return u.value},set detailLoading(N){u.value=N},get detailError(){return f.value},set detailError(N){f.value=N}},detailLabel:"Loop detail",requestDetail:(N,{signal:L})=>K.get(`/api/loops/${encodeURIComponent(N)}?limit=100`,{signal:L})});async function O(N){p.value="",await A.open(N.id)}function x(){A.close(),p.value=""}async function m(N,L){try{await navigator.clipboard.writeText(L||""),p.value=N,setTimeout(()=>{p.value===N&&(p.value="")},1500)}catch{Te.error("Copy failed")}}const _=J(()=>e.value.reduce((N,L)=>N+(L.iteration_count||0),0)),S=J(()=>e.value.filter(N=>N.status==="running").length);function g(N){return N==="running"?"loop-status-running":N==="error"?"loop-status-error":"loop-status-stopped"}function w(N){return N==="running"?"badge-success":N==="error"?"badge-danger":N==="completed"?"badge-info":"badge-warning"}function T(N){return N==="act"?"badge-warning":N==="silent"?"badge-info":"badge-success"}async function C(N=!1){N=N===!0,N||(t.value=!0);try{const L=await K.get("/api/loops");e.value=Array.isArray(L)?L:[],s.value=null}catch(L){N||(s.value=L.message)}N||(t.value=!1)}async function M(){l.value=null;const N=a.value;if(!N.goal.trim()){l.value="Goal is required";return}if(!N.channel_id.trim()){l.value="Channel ID is required";return}const L={goal:N.goal.trim(),channel_id:N.channel_id.trim(),interval_seconds:N.interval_seconds||60,mode:N.mode,max_iterations:N.max_iterations||50};N.stop_condition.trim()&&(L.stop_condition=N.stop_condition.trim()),i.value=!0;try{const Z=await K.post("/api/loops",L);Te.success(`Loop started: ${Z.loop_id}`),a.value={goal:"",interval_seconds:60,mode:"notify",max_iterations:50,stop_condition:"",channel_id:""},n.value=!1,await C()}catch(Z){l.value=Z.message}i.value=!1}async function B(N){if(await gs({title:"Stop loop",message:`Stop loop ${N}? The current iteration will finish before stopping.`,confirmLabel:"Stop Loop",danger:!0})){r.value=N;try{await K.del(`/api/loops/${encodeURIComponent(N)}`),Te.success("Loop stopped"),await C()}catch(Z){Te.error(Z.message||"Failed to stop loop")}r.value=null}}async function $(N){o.value=N;try{await K.post(`/api/loops/${encodeURIComponent(N)}/restart`),Te.success("Loop restarted"),await C()}catch(L){Te.error(L.message||"Failed to restart loop")}o.value=null}function I(N){b&&N.payload&&(N.payload.loop_id||N.payload.type==="loop")&&(C(!0),d.value&&A.refresh())}let j=null;function Y(){j!==null&&clearInterval(j),j=null}function H(){Y(),b&&(j=setInterval(()=>{C(!0),d.value&&A.refresh()},5e3))}return We(()=>{b=!0,C(),Ke.subscribe("events",I),H()}),Cs(()=>{b=!0,C(!0),H()}),Es(()=>{b=!1,Y()}),xt(()=>{b=!1,Ke.unsubscribe("events",I),Y(),A.close()}),{loops:e,loading:t,error:s,showCreate:n,form:a,creating:i,createError:l,stoppingId:r,restartingId:o,detail:c,detailId:d,detailLoading:u,detailError:f,copied:p,totalIterations:_,runningCount:S,statusDotClass:g,statusBadge:w,modeBadge:T,formatAge:mm,formatDuration:Wa,formatTs:ua,formatTokens:bm,openDetail:O,closeDetail:x,copyText:m,fetchLoops:C,doCreate:M,doStop:B,doRestart:$}}},ik={template:` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(!1),a=h({goal:"",interval_seconds:60,mode:"notify",max_iterations:50,stop_condition:"",channel_id:""}),i=h(!1),l=h(null),r=h(null),o=h(null),c=h(null),d=h(null),u=h(!1),f=h(null),p=h("");let b=!1;const E=xm({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:N})=>G.get(`/api/loops/${encodeURIComponent(O)}?limit=100`,{signal:N})});async function I(O){p.value="",await E.open(O.id)}function x(){E.close(),p.value=""}async function m(O,N){try{await navigator.clipboard.writeText(N||""),p.value=O,setTimeout(()=>{p.value===O&&(p.value="")},1500)}catch{Ae.error("Copy failed")}}const _=J(()=>e.value.reduce((O,N)=>O+(N.iteration_count||0),0)),S=J(()=>e.value.filter(O=>O.status==="running").length);function g(O){return O==="running"?"loop-status-running":O==="error"?"loop-status-error":"loop-status-stopped"}function w(O){return O==="running"?"badge-success":O==="error"?"badge-danger":O==="completed"?"badge-info":"badge-warning"}function T(O){return O==="act"?"badge-warning":O==="silent"?"badge-info":"badge-success"}async function C(O=!1){O=O===!0,O||(t.value=!0);try{const N=await G.get("/api/loops");e.value=Array.isArray(N)?N:[],s.value=null}catch(N){O||(s.value=N.message)}O||(t.value=!1)}async function M(){l.value=null;const O=a.value;if(!O.goal.trim()){l.value="Goal is required";return}if(!O.channel_id.trim()){l.value="Channel ID is required";return}const N={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()&&(N.stop_condition=O.stop_condition.trim()),i.value=!0;try{const Y=await G.post("/api/loops",N);Ae.success(`Loop started: ${Y.loop_id}`),a.value={goal:"",interval_seconds:60,mode:"notify",max_iterations:50,stop_condition:"",channel_id:""},n.value=!1,await C()}catch(Y){l.value=Y.message}i.value=!1}async function H(O){if(await _s({title:"Stop loop",message:`Stop loop ${O}? The current iteration will finish before stopping.`,confirmLabel:"Stop Loop",danger:!0})){r.value=O;try{await G.del(`/api/loops/${encodeURIComponent(O)}`),Ae.success("Loop stopped"),await C()}catch(Y){Ae.error(Y.message||"Failed to stop loop")}r.value=null}}async function P(O){o.value=O;try{await G.post(`/api/loops/${encodeURIComponent(O)}/restart`),Ae.success("Loop restarted"),await C()}catch(N){Ae.error(N.message||"Failed to restart loop")}o.value=null}function R(O){b&&O.payload&&(O.payload.loop_id||O.payload.type==="loop")&&(C(!0),d.value&&E.refresh())}let j=null;function Q(){j!==null&&clearInterval(j),j=null}function U(){Q(),b&&(j=setInterval(()=>{C(!0),d.value&&E.refresh()},5e3))}return We(()=>{b=!0,C(),Ke.subscribe("events",R),U()}),Ds(()=>{b=!0,C(!0),U()}),Ms(()=>{b=!1,Q()}),xt(()=>{b=!1,Ke.unsubscribe("events",R),Q(),E.close()}),{loops:e,loading:t,error:s,showCreate:n,form:a,creating:i,createError:l,stoppingId:r,restartingId:o,detail:c,detailId:d,detailLoading:u,detailError:f,copied:p,totalIterations:_,runningCount:S,statusDotClass:g,statusBadge:w,modeBadge:T,formatAge:mm,formatDuration:Xa,formatTs:pa,formatTokens:bm,openDetail:I,closeDetail:x,copyText:m,fetchLoops:C,doCreate:M,doStop:H,doRestart:P}}},ik={template:`
@@ -789,7 +789,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
-
`,setup(){const e=h([]),t=h(!0),s=h(null),n=h(!0);let a=null;const i=h(null),l=J(()=>e.value.filter(x=>x.status==="running").length),r=J(()=>e.value.filter(x=>x.status!=="running").length);function o(x){return x==="running"?"loop-status-running":x==="failed"||x==="error"?"loop-status-error":"loop-status-stopped"}function c(x){return x==="running"?"badge-success":x==="completed"||x==="exited"?"badge-info":x==="killed"||x==="error"||x==="failed"?"badge-danger":"badge-warning"}async function d(x=!1){x=x===!0,x||(t.value=!0);try{e.value=await K.get("/api/processes"),s.value=null}catch(m){x||(s.value=m.message)}x||(t.value=!1)}function u(){f(),n.value&&(a=setInterval(()=>{t.value||d(!0)},5e3))}function f(){a&&(clearInterval(a),a=null)}es(n,x=>{x?u():f()});async function p(x){if(await gs({title:"Kill process",message:`Kill process ${x}?`,confirmLabel:"Kill",danger:!0})){i.value=x;try{await K.del(`/api/processes/${x}`),Te.success(`Process ${x} killed`),await d()}catch(_){Te.error(_.message||"Failed to kill process")}i.value=null}}function b(x){x.payload&&(x.payload.pid||x.payload.type==="process")&&d(!0)}let y=!1;function A(){y||(y=!0,d(),Ke.subscribe("events",b),u())}function O(){y&&(y=!1,Ke.unsubscribe("events",b),f())}return We(A),Cs(A),Es(O),xt(O),{processes:e,loading:t,error:s,autoRefresh:n,killingPid:i,runningCount:l,completedCount:r,procStatusDot:o,statusBadge:c,formatDuration:Wa,fetchProcesses:d,doKill:p}}},lk=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;function Lu(e,t){return t==="cron"&&String(e.cron||"").trim()?e.run_at="":t==="run_at"&&String(e.run_at||"").trim()&&(e.cron=""),e}function rk(e,t=!1){const s=a=>String(a).padStart(2,"0"),n=`${e.getFullYear()}-${s(e.getMonth()+1)}-${s(e.getDate())}T${s(e.getHours())}:${s(e.getMinutes())}`;return t?`${n}:${s(e.getSeconds())}`:n}function ok(e){const t=-e.getTimezoneOffset(),s=t>=0?"+":"-",n=Math.abs(t),a=Math.floor(n/60),i=n%60;return`UTC${s}${a}${i?`:${String(i).padStart(2,"0")}`:""}`}function ck(e){const t=String(e||"").trim();if(!t)return{state:"empty"};const s=lk.exec(t);if(!s)return{state:"invalid",typed:t};const[,n,a,i,l,r]=s.slice(0,6).map(Number),o=s[6]===void 0?0:Number(s[6]);if(o>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(n,a-1,i,l,r,o),f=new Date(u-864e5).getTimezoneOffset(),p=new Date(u+864e5).getTimezoneOffset(),b=[];for(const A of new Set([f,p])){const O=new Date(u+A*6e4);rk(O,c)===d&&(b.some(x=>x.getTime()===O.getTime())||b.push(O))}if(b.sort((A,O)=>A.getTime()-O.getTime()),b.length===0)return{state:"nonexistent",typed:t};if(b.length>1)return{state:"ambiguous",typed:t,options:b.map(A=>({instant:A,offset:ok(A),iso:A.toISOString()}))};const y=b[0];return{state:"ok",typed:t,instant:y,iso:y.toISOString()}}const dk={template:` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(!0);let a=null;const i=h(null),l=J(()=>e.value.filter(x=>x.status==="running").length),r=J(()=>e.value.filter(x=>x.status!=="running").length);function o(x){return x==="running"?"loop-status-running":x==="failed"||x==="error"?"loop-status-error":"loop-status-stopped"}function c(x){return x==="running"?"badge-success":x==="completed"||x==="exited"?"badge-info":x==="killed"||x==="error"||x==="failed"?"badge-danger":"badge-warning"}async function d(x=!1){x=x===!0,x||(t.value=!0);try{e.value=await G.get("/api/processes"),s.value=null}catch(m){x||(s.value=m.message)}x||(t.value=!1)}function u(){f(),n.value&&(a=setInterval(()=>{t.value||d(!0)},5e3))}function f(){a&&(clearInterval(a),a=null)}ns(n,x=>{x?u():f()});async function p(x){if(await _s({title:"Kill process",message:`Kill process ${x}?`,confirmLabel:"Kill",danger:!0})){i.value=x;try{await G.del(`/api/processes/${x}`),Ae.success(`Process ${x} killed`),await d()}catch(_){Ae.error(_.message||"Failed to kill process")}i.value=null}}function b(x){x.payload&&(x.payload.pid||x.payload.type==="process")&&d(!0)}let y=!1;function E(){y||(y=!0,d(),Ke.subscribe("events",b),u())}function I(){y&&(y=!1,Ke.unsubscribe("events",b),f())}return We(E),Ds(E),Ms(I),xt(I),{processes:e,loading:t,error:s,autoRefresh:n,killingPid:i,runningCount:l,completedCount:r,procStatusDot:o,statusBadge:c,formatDuration:Xa,fetchProcesses:d,doKill:p}}},lk=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;function Lu(e,t){return t==="cron"&&String(e.cron||"").trim()?e.run_at="":t==="run_at"&&String(e.run_at||"").trim()&&(e.cron=""),e}function rk(e,t=!1){const s=a=>String(a).padStart(2,"0"),n=`${e.getFullYear()}-${s(e.getMonth()+1)}-${s(e.getDate())}T${s(e.getHours())}:${s(e.getMinutes())}`;return t?`${n}:${s(e.getSeconds())}`:n}function ok(e){const t=-e.getTimezoneOffset(),s=t>=0?"+":"-",n=Math.abs(t),a=Math.floor(n/60),i=n%60;return`UTC${s}${a}${i?`:${String(i).padStart(2,"0")}`:""}`}function ck(e){const t=String(e||"").trim();if(!t)return{state:"empty"};const s=lk.exec(t);if(!s)return{state:"invalid",typed:t};const[,n,a,i,l,r]=s.slice(0,6).map(Number),o=s[6]===void 0?0:Number(s[6]);if(o>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(n,a-1,i,l,r,o),f=new Date(u-864e5).getTimezoneOffset(),p=new Date(u+864e5).getTimezoneOffset(),b=[];for(const E of new Set([f,p])){const I=new Date(u+E*6e4);rk(I,c)===d&&(b.some(x=>x.getTime()===I.getTime())||b.push(I))}if(b.sort((E,I)=>E.getTime()-I.getTime()),b.length===0)return{state:"nonexistent",typed:t};if(b.length>1)return{state:"ambiguous",typed:t,options:b.map(E=>({instant:E,offset:ok(E),iso:E.toISOString()}))};const y=b[0];return{state:"ok",typed:t,instant:y,iso:y.toISOString()}}const dk={template:`
@@ -1103,7 +1103,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
-
`,setup(){const e=h([]),t=h(!0),s=h(null),n=h(!1),a=h({description:"",action:"reminder",channel_id:"",cron:"",run_at:"",message:"",tool_name:"",tool_input_str:""}),i=h(!1),l=h(null),r=h(null),o=J(()=>ck(a.value.run_at));es(()=>a.value.run_at,()=>{r.value=null});const c=J(()=>{var se;const P=o.value;return P.state==="ok"?P.instant:P.state==="ambiguous"&&r.value!==null&&((se=P.options[r.value])==null?void 0:se.instant)||null}),d=J(()=>{const P=c.value;return P?`${P.toLocaleString()} local — ${P.toISOString()} UTC`:""}),u=h(null),f=h(!1),p=[{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 * * * *"}],b=h(null),y=h(null),A=h(null),O=h(null),x=h(null),m=h([]),_=h(!1),S=h("");let g=0;const w=J(()=>e.value.filter(P=>P.cron&&!P.one_time).length),T=J(()=>e.value.filter(P=>P.one_time).length),C=J(()=>e.value.filter(P=>P.trigger).length),M=J(()=>e.value.filter(P=>P.paused).length),B=J(()=>e.value.filter(P=>P.consecutive_failures>0).length);function $(P){if(!P)return"-";const se=Date.now(),V=(new Date(P).getTime()-se)/1e3;if(V<0)return"overdue";if(V<60)return"in < 1 min";if(V<3600)return`in ${Math.floor(V/60)} min`;if(V<86400){const de=Math.floor(V/3600),ve=Math.floor(V%3600/60);return ve>0?`in ${de}h ${ve}m`:`in ${de}h`}const ce=Math.floor(V/86400);return`in ${ce} day${ce!==1?"s":""}`}function I(P){return P==null?"-":P<1e3?`${P}ms`:P<6e4?`${(P/1e3).toFixed(1)}s`:Wa(P/1e3)}function j(P=a.value.cron){a.value.cron=P,Lu(a.value,"cron"),u.value=null}function Y(P=a.value.run_at){a.value.run_at=P,Lu(a.value,"run_at"),u.value=null}async function H(){const P=a.value.cron.trim();if(P){f.value=!0;try{u.value=await K.post("/api/schedules/validate-cron",{expression:P})}catch(se){u.value={valid:!1,error:se.message}}f.value=!1}}async function N(){t.value=!0,s.value=null;try{e.value=await K.get("/api/schedules")}catch(P){s.value=P.message}t.value=!1}async function L(P){if(x.value===P){x.value=null,m.value=[];return}x.value=P,_.value=!0,m.value=[];const se=++g;try{const ke=await K.get(`/api/schedules/${encodeURIComponent(P)}/history?limit=10`);if(se!==g||x.value!==P)return;m.value=ke,S.value=""}catch(ke){if(se!==g||x.value!==P)return;m.value=[],S.value=ke.message||"Failed to load execution history"}se===g&&(_.value=!1)}async function Z(){l.value=null;const P=a.value;if(!P.description.trim()){l.value="Description is required";return}if(!P.channel_id.trim()){l.value="Channel ID is required";return}if(!P.cron.trim()&&!P.run_at.trim()){l.value="Cron expression or run_at time is required";return}if(P.cron.trim()&&P.run_at.trim()){l.value="Choose either Cron or One-Time, not both";return}const se={description:P.description.trim(),action:P.action,channel_id:P.channel_id.trim()};if(P.cron.trim()&&(se.cron=P.cron.trim()),P.run_at.trim()){const ke=o.value;if(ke.state==="nonexistent"){l.value="That local time does not exist (daylight saving gap)";return}if(ke.state==="invalid"){l.value="One-time run time is not a valid date";return}const V=c.value;if(ke.state==="ambiguous"&&r.value===null){l.value="That local time happens twice — choose which occurrence to use";return}if(!V){l.value="One-time run time could not be resolved";return}se.run_at=V.toISOString()}if(P.action==="reminder"&&P.message.trim()&&(se.message=P.message.trim()),P.action==="check"&&(P.tool_name.trim()&&(se.tool_name=P.tool_name.trim()),P.tool_input_str.trim()))try{se.tool_input=JSON.parse(P.tool_input_str.trim())}catch{l.value="Tool input must be valid JSON";return}i.value=!0;try{await K.post("/api/schedules",se),Te.success("Schedule created"),a.value={description:"",action:"reminder",channel_id:"",cron:"",run_at:"",message:"",tool_name:"",tool_input_str:""},u.value=null,n.value=!1,await N()}catch(ke){l.value=ke.message}i.value=!1}async function xe(P){b.value=P;try{const se=await K.post(`/api/schedules/${encodeURIComponent(P)}/run`);if(se.status==="failure")Te.error(`Execution failed: ${se.error||"unknown error"}`);else{const ke=se.warning?`Executed (${se.warning})`:"Executed successfully";Te.success(ke)}await N()}catch(se){Te.error(se.message||"Failed to trigger")}b.value=null}async function _e(P){A.value=P.id;const se=!P.paused;try{await K.put(`/api/schedules/${encodeURIComponent(P.id)}`,{paused:se}),Te.success(se?"Schedule paused":"Schedule resumed"),await N()}catch(ke){Te.error(ke.message||"Failed to update schedule")}A.value=null}async function ae(P){O.value=P;try{await K.post(`/api/schedules/${encodeURIComponent(P)}/reset-failures`),Te.success("Failure counters reset"),await N()}catch(se){Te.error(se.message||"Failed to reset")}O.value=null}async function fe(P){const se=e.value.find(V=>V.id===P);if(await gs({title:"Delete schedule",message:`Delete "${(se==null?void 0:se.description)||P}"? This cannot be undone.`,confirmLabel:"Delete",danger:!0})){y.value=P;try{await K.del(`/api/schedules/${encodeURIComponent(P)}`),Te.success("Schedule deleted"),await N()}catch(V){Te.error(V.message||"Failed to delete schedule")}y.value=null}}return We(()=>{N()}),{schedules:e,loading:t,error:s,showCreate:n,form:a,creating:i,createError:l,runAtUtcPreview:d,runAtAnalysis:o,runAtOccurrence:r,cronResult:u,validatingCron:f,cronPresets:p,runningId:b,deletingId:y,togglingId:A,resettingId:O,expandedId:x,history:m,historyLoading:_,historyError:S,cronCount:w,oneTimeCount:T,webhookCount:C,pausedCount:M,failingCount:B,formatTs:ua,formatAge:mm,formatFuture:$,formatMs:I,formatDuration:Wa,onCronInput:j,onRunAtInput:Y,validateCron:H,toggleExpand:L,fetchSchedules:N,doCreate:Z,doRunNow:xe,doTogglePause:_e,doResetFailures:ae,doDelete:fe}}},_m=[{id:"live",label:"Live",component:Q_},{id:"agents",label:"Agents",component:nk},{id:"loops",label:"Loops",component:ak},{id:"processes",label:"Processes",component:ik},{id:"schedules",label:"Schedules",component:dk}],uk={components:{TabbedPage:Dr},setup(){return{tabs:_m}},template:''},fk={template:` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(!1),a=h({description:"",action:"reminder",channel_id:"",cron:"",run_at:"",message:"",tool_name:"",tool_input_str:""}),i=h(!1),l=h(null),r=h(null),o=J(()=>ck(a.value.run_at));ns(()=>a.value.run_at,()=>{r.value=null});const c=J(()=>{var se;const F=o.value;return F.state==="ok"?F.instant:F.state==="ambiguous"&&r.value!==null&&((se=F.options[r.value])==null?void 0:se.instant)||null}),d=J(()=>{const F=c.value;return F?`${F.toLocaleString()} local — ${F.toISOString()} UTC`:""}),u=h(null),f=h(!1),p=[{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 * * * *"}],b=h(null),y=h(null),E=h(null),I=h(null),x=h(null),m=h([]),_=h(!1),S=h("");let g=0;const w=J(()=>e.value.filter(F=>F.cron&&!F.one_time).length),T=J(()=>e.value.filter(F=>F.one_time).length),C=J(()=>e.value.filter(F=>F.trigger).length),M=J(()=>e.value.filter(F=>F.paused).length),H=J(()=>e.value.filter(F=>F.consecutive_failures>0).length);function P(F){if(!F)return"-";const se=Date.now(),V=(new Date(F).getTime()-se)/1e3;if(V<0)return"overdue";if(V<60)return"in < 1 min";if(V<3600)return`in ${Math.floor(V/60)} min`;if(V<86400){const ce=Math.floor(V/3600),ye=Math.floor(V%3600/60);return ye>0?`in ${ce}h ${ye}m`:`in ${ce}h`}const de=Math.floor(V/86400);return`in ${de} day${de!==1?"s":""}`}function R(F){return F==null?"-":F<1e3?`${F}ms`:F<6e4?`${(F/1e3).toFixed(1)}s`:Xa(F/1e3)}function j(F=a.value.cron){a.value.cron=F,Lu(a.value,"cron"),u.value=null}function Q(F=a.value.run_at){a.value.run_at=F,Lu(a.value,"run_at"),u.value=null}async function U(){const F=a.value.cron.trim();if(F){f.value=!0;try{u.value=await G.post("/api/schedules/validate-cron",{expression:F})}catch(se){u.value={valid:!1,error:se.message}}f.value=!1}}async function O(){t.value=!0,s.value=null;try{e.value=await G.get("/api/schedules")}catch(F){s.value=F.message}t.value=!1}async function N(F){if(x.value===F){x.value=null,m.value=[];return}x.value=F,_.value=!0,m.value=[];const se=++g;try{const Se=await G.get(`/api/schedules/${encodeURIComponent(F)}/history?limit=10`);if(se!==g||x.value!==F)return;m.value=Se,S.value=""}catch(Se){if(se!==g||x.value!==F)return;m.value=[],S.value=Se.message||"Failed to load execution history"}se===g&&(_.value=!1)}async function Y(){l.value=null;const F=a.value;if(!F.description.trim()){l.value="Description is required";return}if(!F.channel_id.trim()){l.value="Channel ID is required";return}if(!F.cron.trim()&&!F.run_at.trim()){l.value="Cron expression or run_at time is required";return}if(F.cron.trim()&&F.run_at.trim()){l.value="Choose either Cron or One-Time, not both";return}const se={description:F.description.trim(),action:F.action,channel_id:F.channel_id.trim()};if(F.cron.trim()&&(se.cron=F.cron.trim()),F.run_at.trim()){const Se=o.value;if(Se.state==="nonexistent"){l.value="That local time does not exist (daylight saving gap)";return}if(Se.state==="invalid"){l.value="One-time run time is not a valid date";return}const V=c.value;if(Se.state==="ambiguous"&&r.value===null){l.value="That local time happens twice — choose which occurrence to use";return}if(!V){l.value="One-time run time could not be resolved";return}se.run_at=V.toISOString()}if(F.action==="reminder"&&F.message.trim()&&(se.message=F.message.trim()),F.action==="check"&&(F.tool_name.trim()&&(se.tool_name=F.tool_name.trim()),F.tool_input_str.trim()))try{se.tool_input=JSON.parse(F.tool_input_str.trim())}catch{l.value="Tool input must be valid JSON";return}i.value=!0;try{await G.post("/api/schedules",se),Ae.success("Schedule created"),a.value={description:"",action:"reminder",channel_id:"",cron:"",run_at:"",message:"",tool_name:"",tool_input_str:""},u.value=null,n.value=!1,await O()}catch(Se){l.value=Se.message}i.value=!1}async function we(F){b.value=F;try{const se=await G.post(`/api/schedules/${encodeURIComponent(F)}/run`);if(se.status==="failure")Ae.error(`Execution failed: ${se.error||"unknown error"}`);else{const Se=se.warning?`Executed (${se.warning})`:"Executed successfully";Ae.success(Se)}await O()}catch(se){Ae.error(se.message||"Failed to trigger")}b.value=null}async function ke(F){E.value=F.id;const se=!F.paused;try{await G.put(`/api/schedules/${encodeURIComponent(F.id)}`,{paused:se}),Ae.success(se?"Schedule paused":"Schedule resumed"),await O()}catch(Se){Ae.error(Se.message||"Failed to update schedule")}E.value=null}async function ie(F){I.value=F;try{await G.post(`/api/schedules/${encodeURIComponent(F)}/reset-failures`),Ae.success("Failure counters reset"),await O()}catch(se){Ae.error(se.message||"Failed to reset")}I.value=null}async function he(F){const se=e.value.find(V=>V.id===F);if(await _s({title:"Delete schedule",message:`Delete "${(se==null?void 0:se.description)||F}"? This cannot be undone.`,confirmLabel:"Delete",danger:!0})){y.value=F;try{await G.del(`/api/schedules/${encodeURIComponent(F)}`),Ae.success("Schedule deleted"),await O()}catch(V){Ae.error(V.message||"Failed to delete schedule")}y.value=null}}return We(()=>{O()}),{schedules:e,loading:t,error:s,showCreate:n,form:a,creating:i,createError:l,runAtUtcPreview:d,runAtAnalysis:o,runAtOccurrence:r,cronResult:u,validatingCron:f,cronPresets:p,runningId:b,deletingId:y,togglingId:E,resettingId:I,expandedId:x,history:m,historyLoading:_,historyError:S,cronCount:w,oneTimeCount:T,webhookCount:C,pausedCount:M,failingCount:H,formatTs:pa,formatAge:mm,formatFuture:P,formatMs:R,formatDuration:Xa,onCronInput:j,onRunAtInput:Q,validateCron:U,toggleExpand:N,fetchSchedules:O,doCreate:Y,doRunNow:we,doTogglePause:ke,doResetFailures:ie,doDelete:he}}},_m=[{id:"live",label:"Live",component:Q_},{id:"agents",label:"Agents",component:nk},{id:"loops",label:"Loops",component:ak},{id:"processes",label:"Processes",component:ik},{id:"schedules",label:"Schedules",component:dk}],uk={components:{TabbedPage:Mr},setup(){return{tabs:_m}},template:''},fk={template:`

Audit Log

@@ -1243,7 +1243,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(null),a=h({tool:"",user:"",keyword:"",date:"",limit:50});function i(c){if(!c)return"";if(typeof c=="string")return c;try{return JSON.stringify(c,null,2)}catch{return String(c)}}function l(c){n.value=n.value===c?null:c}function r(){a.value={tool:"",user:"",keyword:"",date:"",limit:50},o()}async function o(){t.value=!0,s.value=null,n.value=null;try{const c=new URLSearchParams;a.value.tool&&c.set("tool",a.value.tool),a.value.user&&c.set("user",a.value.user),a.value.keyword&&c.set("q",a.value.keyword),a.value.date&&c.set("date",a.value.date),c.set("limit",String(a.value.limit));const d=c.toString(),u=await K.get(`/api/audit${d?"?"+d:""}`);e.value=Array.isArray(u)?u:[]}catch(c){s.value=c.message}t.value=!1}return We(()=>{o()}),{entries:e,loading:t,error:s,expandedIdx:n,filters:a,formatTs:ua,formatDetail:i,truncateBlock:gm,toggleExpand:l,clearFilters:r,fetchAudit:o}}},Du=[{id:"all",name:"All Sessions",icon:"list",filters:{}},{id:"active",name:"Recently Active",icon:"activity",filters:{minAge:0,maxAge:3600}},{id:"discord",name:"Discord Only",icon:"message",filters:{source:"discord"}},{id:"web",name:"Web Only",icon:"globe",filters:{source:"web"}},{id:"long",name:"Long Conversations",icon:"book",filters:{minMessages:10}},{id:"compacted",name:"Compacted",icon:"archive",filters:{hasCompaction:!0}}],pk=[{value:"last_active",label:"Last Active"},{value:"created_at",label:"Created"},{value:"message_count",label:"Message Count"}],hk={template:` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(null),a=h({tool:"",user:"",keyword:"",date:"",limit:50});function i(c){if(!c)return"";if(typeof c=="string")return c;try{return JSON.stringify(c,null,2)}catch{return String(c)}}function l(c){n.value=n.value===c?null:c}function r(){a.value={tool:"",user:"",keyword:"",date:"",limit:50},o()}async function o(){t.value=!0,s.value=null,n.value=null;try{const c=new URLSearchParams;a.value.tool&&c.set("tool",a.value.tool),a.value.user&&c.set("user",a.value.user),a.value.keyword&&c.set("q",a.value.keyword),a.value.date&&c.set("date",a.value.date),c.set("limit",String(a.value.limit));const d=c.toString(),u=await G.get(`/api/audit${d?"?"+d:""}`);e.value=Array.isArray(u)?u:[]}catch(c){s.value=c.message}t.value=!1}return We(()=>{o()}),{entries:e,loading:t,error:s,expandedIdx:n,filters:a,formatTs:pa,formatDetail:i,truncateBlock:gm,toggleExpand:l,clearFilters:r,fetchAudit:o}}},Du=[{id:"all",name:"All Sessions",icon:"list",filters:{}},{id:"active",name:"Recently Active",icon:"activity",filters:{minAge:0,maxAge:3600}},{id:"discord",name:"Discord Only",icon:"message",filters:{source:"discord"}},{id:"web",name:"Web Only",icon:"globe",filters:{source:"web"}},{id:"long",name:"Long Conversations",icon:"book",filters:{minMessages:10}},{id:"compacted",name:"Compacted",icon:"archive",filters:{hasCompaction:!0}}],pk=[{value:"last_active",label:"Last Active"},{value:"created_at",label:"Created"},{value:"message_count",label:"Message Count"}],hk={template:`
@@ -1572,8 +1572,8 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(null),a=h(null),i=h(!1);let l=0;const r=h(null),o=h(!1),c=h(new Set),d=h(!1),u=h("all"),f=h(""),p=h("last_active"),b=h(!1),y=Du,A=pk,O=h([]),x=h(!1),m=h(""),_=h("flat"),S=h(new Set),g=h(""),w=h(""),T=h(""),C=h(null),M=h(!1);function B(){try{const G=localStorage.getItem("odin-session-presets");G&&(O.value=JSON.parse(G))}catch{}}function $(){try{localStorage.setItem("odin-session-presets",JSON.stringify(O.value))}catch{}}const I=J(()=>f.value.trim()!==""||u.value!=="all"),j=J(()=>{let G=[...e.value];const ye=Du.find(ze=>ze.id===u.value),Ce=ye?ye.filters:{};if(Ce.source&&(G=G.filter(ze=>ze.source===Ce.source)),Ce.minMessages&&(G=G.filter(ze=>ze.message_count>=Ce.minMessages)),Ce.hasCompaction&&(G=G.filter(ze=>ze.has_summary)),Ce.maxAge!=null){const ze=Date.now()/1e3;G=G.filter(pt=>pt.last_active&&ze-pt.last_active<=Ce.maxAge)}if(f.value.trim()){const ze=f.value.toLowerCase().trim();G=G.filter(pt=>(pt.channel_id||"").toLowerCase().includes(ze)||(pt.last_user_id||"").toLowerCase().includes(ze)||(pt.source||"").toLowerCase().includes(ze))}const Re=p.value,Be=b.value?1:-1;return G.sort((ze,pt)=>{const ns=ze[Re]||0,As=pt[Re]||0;return(ns-As)*Be}),G}),Y=J(()=>{if(!a.value||!a.value.messages)return[];const G=a.value.messages;if(G.length===0)return[];const ye=[];let Ce=[];for(const Re of G)Re.role==="user"&&Ce.length>0&&(ye.push(Ce),Ce=[]),Ce.push(Re);return Ce.length>0&&ye.push(Ce),ye}),H=J(()=>j.value.length>0&&c.value.size===j.value.length);function N(G){const ye=G.find(Ce=>Ce.role==="user");if(ye&&ye.content){const Ce=ye.content.slice(0,120);return Ce.lengthye.id!==G),$(),u.value===G&&(u.value="all")}function fe(){u.value="all",f.value="",p.value="last_active",b.value=!1}function P(G){if(!G)return"—";const ye=Date.now()/1e3-G;if(ye<60)return"just now";if(ye<3600){const Re=Math.floor(ye/60);return`${Re} minute${Re!==1?"s":""} ago`}if(ye<86400){const Re=Math.floor(ye/3600);return`${Re} hour${Re!==1?"s":""} ago`}const Ce=Math.floor(ye/86400);return`${Ce} day${Ce!==1?"s":""} ago`}function se(G){if(!G)return"";try{return new Date(G*1e3).toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return""}}function ke(G){if(!G)return"";try{return new Date(G*1e3).toLocaleString()}catch{return""}}function V(G){return G==="user"?"bg-gray-900/50 border border-gray-800":G==="assistant"?"bg-indigo-950/30 border border-indigo-900/30":"bg-gray-900/30 border border-gray-800/50"}function ce(G){return G==="user"?"sess-msg-user":G==="assistant"?"sess-msg-assistant":"sess-msg-system"}function de(G){return G==="user"?"badge-info":G==="assistant"?"badge-success":"badge-warning"}function ve(G){return G==="user"?"sess-dot-user":G==="assistant"?"sess-dot-assistant":"sess-dot-system"}function me(G){return G==="user"?"text-cyan-400":G==="assistant"?"text-indigo-400":"text-gray-500"}function He(G){return G?G.length>2e3?G.slice(0,2e3)+` -... (truncated)`:G:""}async function k(){const G=g.value.trim();if(G){M.value=!0;try{let ye=`/api/sessions/search?q=${encodeURIComponent(G)}&limit=50`;w.value.trim()&&(ye+=`&channel_id=${encodeURIComponent(w.value.trim())}`),T.value.trim()&&(ye+=`&user_id=${encodeURIComponent(T.value.trim())}`);const Ce=await K.get(ye);C.value=Ce.results||[]}catch{C.value=[]}M.value=!1}}function E(){g.value="",w.value="",T.value="",C.value=null}function U(G){return G?G.replace(/&/g,"&").replace(//g,">").replace(/>>>/g,'').replace(/<<</g,""):""}function X(G){return G==="user"?"fts-result-user":G==="assistant"?"fts-result-assistant":G==="summary"?"fts-result-summary":G==="fts"?"fts-result-fts":G==="channel"?"fts-result-channel":"fts-result-default"}function q(G){return G==="user"?"badge-info":G==="assistant"?"badge-success":G==="summary"?"badge-warning":G==="fts"?"badge-success":"badge-info"}async function Q(){t.value=!0,s.value=null;try{e.value=await K.get("/api/sessions")}catch(G){s.value=G.message}t.value=!1}function ie(){s.value=null,Q()}async function re(G){if(n.value===G){n.value=null,a.value=null,S.value=new Set;return}n.value=G,a.value=null,i.value=!0,S.value=new Set;const ye=++l;try{const Ce=await K.get(`/api/sessions/${encodeURIComponent(G)}`);ye===l&&n.value===G&&(a.value=Ce)}catch(Ce){ye===l&&n.value===G&&(a.value={messages:[],summary:"",error:Ce.message||"Failed to load session"})}finally{ye===l&&(i.value=!1)}}function le(G){const ye=new Set(c.value);ye.has(G)?ye.delete(G):ye.add(G),c.value=ye}function te(){H.value?c.value=new Set:c.value=new Set(j.value.map(G=>G.channel_id))}function be(G){r.value=G}async function ue(){if(r.value){o.value=!0;try{await K.del(`/api/sessions/${encodeURIComponent(r.value)}`),n.value===r.value&&(n.value=null,a.value=null),c.value.delete(r.value),await Q()}catch(G){s.value=G.message||"Failed to clear session"}o.value=!1,r.value=null}}function he(){d.value=!0}async function we(){if(c.value.size!==0){o.value=!0;try{await K.post("/api/sessions/clear-bulk",{channel_ids:[...c.value]}),c.value.has(n.value)&&(n.value=null,a.value=null),c.value=new Set,await Q()}catch(G){s.value=G.message||"Failed to clear sessions"}o.value=!1,d.value=!1}}async function Ee(G,ye){const Ce=`/api/sessions/${encodeURIComponent(G)}/export?format=${ye}`;try{const Re=await K.getBlob(Ce),Be=URL.createObjectURL(Re),ze=document.createElement("a");ze.href=Be,ze.download=`session-${G}.${ye==="text"?"txt":"json"}`,ze.click(),URL.revokeObjectURL(Be)}catch(Re){s.value=Re.message||"Failed to export session"}}let Le=null;function Oe(G){G.payload&&G.payload.channel_id&&(clearTimeout(Le),Le=setTimeout(()=>{if(Q(),n.value&&G.payload.channel_id===n.value){const ye=n.value,Ce=l;K.get(`/api/sessions/${encodeURIComponent(ye)}`).then(Re=>{Ce!==l||n.value!==ye||(a.value=Re)}).catch(()=>{})}},2e3))}let Fe=!1;function Ve(){Fe||(Fe=!0,Q(),Ke.subscribe("events",Oe))}We(()=>{B(),Ve()}),Cs(()=>{Ve()});function lt(){Fe&&(Fe=!1,Ke.unsubscribe("events",Oe),clearTimeout(Le))}return Es(lt),xt(lt),{sessions:e,loading:t,error:s,expandedId:n,detail:a,detailLoading:i,clearTarget:r,clearing:o,selected:c,allSelected:H,bulkClearing:d,activePreset:u,searchQuery:f,sortBy:p,sortAsc:b,filterPresets:y,sortOptions:A,filteredSessions:j,hasActiveFilters:I,customPresets:O,showSavePreset:x,newPresetName:m,threadView:_,threads:Y,collapsedThreads:S,ftsQuery:g,ftsChannelId:w,ftsUserId:T,ftsResults:C,ftsSearching:M,formatAge:P,formatTimestamp:se,formatFullTimestamp:ke,messageClass:V,threadMsgClass:ce,roleBadge:de,roleDotClass:ve,roleLabelClass:me,truncateContent:He,threadSummary:N,fetchSessions:Q,retry:ie,toggleSession:re,toggleSelect:le,toggleSelectAll:te,confirmClear:be,clearSession:ue,confirmBulkClear:he,doBulkClear:we,exportSession:Ee,applyPreset:Z,applyCustomPreset:xe,saveCustomPreset:_e,removeCustomPreset:ae,resetFilters:fe,toggleThread:L,runFtsSearch:k,clearFtsSearch:E,highlightSnippet:U,ftsResultClass:X,ftsTypeBadge:q}}},mk={props:["trace"],template:` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(null),a=h(null),i=h(!1);let l=0;const r=h(null),o=h(!1),c=h(new Set),d=h(!1),u=h("all"),f=h(""),p=h("last_active"),b=h(!1),y=Du,E=pk,I=h([]),x=h(!1),m=h(""),_=h("flat"),S=h(new Set),g=h(""),w=h(""),T=h(""),C=h(null),M=h(!1);function H(){try{const K=localStorage.getItem("odin-session-presets");K&&(I.value=JSON.parse(K))}catch{}}function P(){try{localStorage.setItem("odin-session-presets",JSON.stringify(I.value))}catch{}}const R=J(()=>f.value.trim()!==""||u.value!=="all"),j=J(()=>{let K=[...e.value];const xe=Du.find(Pe=>Pe.id===u.value),Ce=xe?xe.filters:{};if(Ce.source&&(K=K.filter(Pe=>Pe.source===Ce.source)),Ce.minMessages&&(K=K.filter(Pe=>Pe.message_count>=Ce.minMessages)),Ce.hasCompaction&&(K=K.filter(Pe=>Pe.has_summary)),Ce.maxAge!=null){const Pe=Date.now()/1e3;K=K.filter(pt=>pt.last_active&&Pe-pt.last_active<=Ce.maxAge)}if(f.value.trim()){const Pe=f.value.toLowerCase().trim();K=K.filter(pt=>(pt.channel_id||"").toLowerCase().includes(Pe)||(pt.last_user_id||"").toLowerCase().includes(Pe)||(pt.source||"").toLowerCase().includes(Pe))}const Re=p.value,Ve=b.value?1:-1;return K.sort((Pe,pt)=>{const ls=Pe[Re]||0,Ps=pt[Re]||0;return(ls-Ps)*Ve}),K}),Q=J(()=>{if(!a.value||!a.value.messages)return[];const K=a.value.messages;if(K.length===0)return[];const xe=[];let Ce=[];for(const Re of K)Re.role==="user"&&Ce.length>0&&(xe.push(Ce),Ce=[]),Ce.push(Re);return Ce.length>0&&xe.push(Ce),xe}),U=J(()=>j.value.length>0&&c.value.size===j.value.length);function O(K){const xe=K.find(Ce=>Ce.role==="user");if(xe&&xe.content){const Ce=xe.content.slice(0,120);return Ce.lengthxe.id!==K),P(),u.value===K&&(u.value="all")}function he(){u.value="all",f.value="",p.value="last_active",b.value=!1}function F(K){if(!K)return"—";const xe=Date.now()/1e3-K;if(xe<60)return"just now";if(xe<3600){const Re=Math.floor(xe/60);return`${Re} minute${Re!==1?"s":""} ago`}if(xe<86400){const Re=Math.floor(xe/3600);return`${Re} hour${Re!==1?"s":""} ago`}const Ce=Math.floor(xe/86400);return`${Ce} day${Ce!==1?"s":""} ago`}function se(K){if(!K)return"";try{return new Date(K*1e3).toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return""}}function Se(K){if(!K)return"";try{return new Date(K*1e3).toLocaleString()}catch{return""}}function V(K){return K==="user"?"bg-gray-900/50 border border-gray-800":K==="assistant"?"bg-indigo-950/30 border border-indigo-900/30":"bg-gray-900/30 border border-gray-800/50"}function de(K){return K==="user"?"sess-msg-user":K==="assistant"?"sess-msg-assistant":"sess-msg-system"}function ce(K){return K==="user"?"badge-info":K==="assistant"?"badge-success":"badge-warning"}function ye(K){return K==="user"?"sess-dot-user":K==="assistant"?"sess-dot-assistant":"sess-dot-system"}function ge(K){return K==="user"?"text-cyan-400":K==="assistant"?"text-indigo-400":"text-gray-500"}function He(K){return K?K.length>2e3?K.slice(0,2e3)+` +... (truncated)`:K:""}async function k(){const K=g.value.trim();if(K){M.value=!0;try{let xe=`/api/sessions/search?q=${encodeURIComponent(K)}&limit=50`;w.value.trim()&&(xe+=`&channel_id=${encodeURIComponent(w.value.trim())}`),T.value.trim()&&(xe+=`&user_id=${encodeURIComponent(T.value.trim())}`);const Ce=await G.get(xe);C.value=Ce.results||[]}catch{C.value=[]}M.value=!1}}function L(){g.value="",w.value="",T.value="",C.value=null}function $(K){return K?K.replace(/&/g,"&").replace(//g,">").replace(/>>>/g,'').replace(/<<</g,""):""}function ee(K){return K==="user"?"fts-result-user":K==="assistant"?"fts-result-assistant":K==="summary"?"fts-result-summary":K==="fts"?"fts-result-fts":K==="channel"?"fts-result-channel":"fts-result-default"}function Z(K){return K==="user"?"badge-info":K==="assistant"?"badge-success":K==="summary"?"badge-warning":K==="fts"?"badge-success":"badge-info"}async function X(){t.value=!0,s.value=null;try{e.value=await G.get("/api/sessions")}catch(K){s.value=K.message}t.value=!1}function ue(){s.value=null,X()}async function oe(K){if(n.value===K){n.value=null,a.value=null,S.value=new Set;return}n.value=K,a.value=null,i.value=!0,S.value=new Set;const xe=++l;try{const Ce=await G.get(`/api/sessions/${encodeURIComponent(K)}`);xe===l&&n.value===K&&(a.value=Ce)}catch(Ce){xe===l&&n.value===K&&(a.value={messages:[],summary:"",error:Ce.message||"Failed to load session"})}finally{xe===l&&(i.value=!1)}}function le(K){const xe=new Set(c.value);xe.has(K)?xe.delete(K):xe.add(K),c.value=xe}function te(){U.value?c.value=new Set:c.value=new Set(j.value.map(K=>K.channel_id))}function ne(K){r.value=K}async function fe(){if(r.value){o.value=!0;try{await G.del(`/api/sessions/${encodeURIComponent(r.value)}`),n.value===r.value&&(n.value=null,a.value=null),c.value.delete(r.value),await X()}catch(K){s.value=K.message||"Failed to clear session"}o.value=!1,r.value=null}}function ve(){d.value=!0}async function Te(){if(c.value.size!==0){o.value=!0;try{await G.post("/api/sessions/clear-bulk",{channel_ids:[...c.value]}),c.value.has(n.value)&&(n.value=null,a.value=null),c.value=new Set,await X()}catch(K){s.value=K.message||"Failed to clear sessions"}o.value=!1,d.value=!1}}async function Oe(K,xe){const Ce=`/api/sessions/${encodeURIComponent(K)}/export?format=${xe}`;try{const Re=await G.getBlob(Ce),Ve=URL.createObjectURL(Re),Pe=document.createElement("a");Pe.href=Ve,Pe.download=`session-${K}.${xe==="text"?"txt":"json"}`,Pe.click(),URL.revokeObjectURL(Ve)}catch(Re){s.value=Re.message||"Failed to export session"}}let Le=null;function De(K){K.payload&&K.payload.channel_id&&(clearTimeout(Le),Le=setTimeout(()=>{if(X(),n.value&&K.payload.channel_id===n.value){const xe=n.value,Ce=l;G.get(`/api/sessions/${encodeURIComponent(xe)}`).then(Re=>{Ce!==l||n.value!==xe||(a.value=Re)}).catch(()=>{})}},2e3))}let Be=!1;function qe(){Be||(Be=!0,X(),Ke.subscribe("events",De))}We(()=>{H(),qe()}),Ds(()=>{qe()});function ct(){Be&&(Be=!1,Ke.unsubscribe("events",De),clearTimeout(Le))}return Ms(ct),xt(ct),{sessions:e,loading:t,error:s,expandedId:n,detail:a,detailLoading:i,clearTarget:r,clearing:o,selected:c,allSelected:U,bulkClearing:d,activePreset:u,searchQuery:f,sortBy:p,sortAsc:b,filterPresets:y,sortOptions:E,filteredSessions:j,hasActiveFilters:R,customPresets:I,showSavePreset:x,newPresetName:m,threadView:_,threads:Q,collapsedThreads:S,ftsQuery:g,ftsChannelId:w,ftsUserId:T,ftsResults:C,ftsSearching:M,formatAge:F,formatTimestamp:se,formatFullTimestamp:Se,messageClass:V,threadMsgClass:de,roleBadge:ce,roleDotClass:ye,roleLabelClass:ge,truncateContent:He,threadSummary:O,fetchSessions:X,retry:ue,toggleSession:oe,toggleSelect:le,toggleSelectAll:te,confirmClear:ne,clearSession:fe,confirmBulkClear:ve,doBulkClear:Te,exportSession:Oe,applyPreset:Y,applyCustomPreset:we,saveCustomPreset:ke,removeCustomPreset:ie,resetFilters:he,toggleThread:N,runFtsSearch:k,clearFtsSearch:L,highlightSnippet:$,ftsResultClass:ee,ftsTypeBadge:Z}}},mk={props:["trace"],template:`
Context Assembly
@@ -2013,7 +2013,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h([]),s=h(!0),n=h(null),a=h(null),i=h(null),l=h(""),r=h(""),o=h(0),c=h({}),d=h({channel_id:"",user_id:"",tool_name:"",errors_only:!1,limit:50});function u(w){if(!w)return"—";try{const T=new Date(w);return isNaN(T.getTime())?w:T.toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch{return w}}function f(w){return!w&&w!==0?"—":w<1e3?w+"ms":(w/1e3).toFixed(1)+"s"}function p(w){return!w&&w!==0?"—":w>=1e3?(w/1e3).toFixed(1)+"k":String(w)}function b(w){if(!w)return"";if(typeof w=="string")return w;try{return JSON.stringify(w,null,2)}catch{return String(w)}}function y(w){a.value===w?a.value=null:(a.value=w,c.value={})}function A(w,T){const C=w+"-"+T;c.value={...c.value,[C]:!c.value[C]}}function O(w,T){return!!c.value[w+"-"+T]}function x(){d.value={channel_id:"",user_id:"",tool_name:"",errors_only:!1,limit:50},r.value="",l.value="",i.value=null,S()}async function m(){try{const w=await K.get("/api/trajectories");e.value=w.files||[],o.value=w.count||0}catch{}}let _=0;async function S(){const w=++_;s.value=!0,n.value=null,a.value=null,i.value=null,c.value={};try{if(r.value){const T=await K.get(`/api/trajectories/${encodeURIComponent(r.value)}?limit=${d.value.limit}`);if(w!==_)return;let C=T.entries||[];d.value.tool_name&&(C=C.filter(M=>(M.tools_used||[]).includes(d.value.tool_name))),d.value.errors_only&&(C=C.filter(M=>M.is_error)),d.value.channel_id&&(C=C.filter(M=>M.channel_id===d.value.channel_id)),d.value.user_id&&(C=C.filter(M=>M.user_id===d.value.user_id)),t.value=C}else{const T=new URLSearchParams;d.value.channel_id&&T.set("channel_id",d.value.channel_id),d.value.user_id&&T.set("user_id",d.value.user_id),d.value.tool_name&&T.set("tool_name",d.value.tool_name),d.value.errors_only&&T.set("errors_only","true"),T.set("limit",String(d.value.limit));const C=T.toString(),M=await K.get(`/api/trajectories/search/query?${C}`);if(w!==_)return;t.value=M.results||[]}}catch(T){if(w!==_)return;n.value=T.message}w===_&&(s.value=!1)}async function g(){if(!l.value.trim())return;const w=++_;s.value=!0,n.value=null,c.value={};try{const T=await K.get(`/api/trajectories/message/${encodeURIComponent(l.value.trim())}`);if(w!==_)return;i.value=T.entry||null,i.value||(n.value="No trace found for this message ID")}catch(T){if(w!==_)return;T.status===404?(i.value=null,n.value="No trace found for message ID: "+l.value):n.value=T.message}w===_&&(s.value=!1)}return We(async()=>{await m(),await S()}),{files:e,entries:t,loading:s,error:n,expandedIdx:a,singleTrace:i,messageIdQuery:l,selectedFile:r,totalSaved:o,filters:d,expandedIterations:c,formatTs:u,formatDuration:f,formatTokens:p,formatJSON:b,truncateBlock:gm,toggleExpand:y,toggleIteration:A,isIterationExpanded:O,clearFilters:x,fetchFiles:m,fetchTraces:S,lookupMessage:g}}},vk={template:` + `,setup(){const e=h([]),t=h([]),s=h(!0),n=h(null),a=h(null),i=h(null),l=h(""),r=h(""),o=h(0),c=h({}),d=h({channel_id:"",user_id:"",tool_name:"",errors_only:!1,limit:50});function u(w){if(!w)return"—";try{const T=new Date(w);return isNaN(T.getTime())?w:T.toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch{return w}}function f(w){return!w&&w!==0?"—":w<1e3?w+"ms":(w/1e3).toFixed(1)+"s"}function p(w){return!w&&w!==0?"—":w>=1e3?(w/1e3).toFixed(1)+"k":String(w)}function b(w){if(!w)return"";if(typeof w=="string")return w;try{return JSON.stringify(w,null,2)}catch{return String(w)}}function y(w){a.value===w?a.value=null:(a.value=w,c.value={})}function E(w,T){const C=w+"-"+T;c.value={...c.value,[C]:!c.value[C]}}function I(w,T){return!!c.value[w+"-"+T]}function x(){d.value={channel_id:"",user_id:"",tool_name:"",errors_only:!1,limit:50},r.value="",l.value="",i.value=null,S()}async function m(){try{const w=await G.get("/api/trajectories");e.value=w.files||[],o.value=w.count||0}catch{}}let _=0;async function S(){const w=++_;s.value=!0,n.value=null,a.value=null,i.value=null,c.value={};try{if(r.value){const T=await G.get(`/api/trajectories/${encodeURIComponent(r.value)}?limit=${d.value.limit}`);if(w!==_)return;let C=T.entries||[];d.value.tool_name&&(C=C.filter(M=>(M.tools_used||[]).includes(d.value.tool_name))),d.value.errors_only&&(C=C.filter(M=>M.is_error)),d.value.channel_id&&(C=C.filter(M=>M.channel_id===d.value.channel_id)),d.value.user_id&&(C=C.filter(M=>M.user_id===d.value.user_id)),t.value=C}else{const T=new URLSearchParams;d.value.channel_id&&T.set("channel_id",d.value.channel_id),d.value.user_id&&T.set("user_id",d.value.user_id),d.value.tool_name&&T.set("tool_name",d.value.tool_name),d.value.errors_only&&T.set("errors_only","true"),T.set("limit",String(d.value.limit));const C=T.toString(),M=await G.get(`/api/trajectories/search/query?${C}`);if(w!==_)return;t.value=M.results||[]}}catch(T){if(w!==_)return;n.value=T.message}w===_&&(s.value=!1)}async function g(){if(!l.value.trim())return;const w=++_;s.value=!0,n.value=null,c.value={};try{const T=await G.get(`/api/trajectories/message/${encodeURIComponent(l.value.trim())}`);if(w!==_)return;i.value=T.entry||null,i.value||(n.value="No trace found for this message ID")}catch(T){if(w!==_)return;T.status===404?(i.value=null,n.value="No trace found for message ID: "+l.value):n.value=T.message}w===_&&(s.value=!1)}return We(async()=>{await m(),await S()}),{files:e,entries:t,loading:s,error:n,expandedIdx:a,singleTrace:i,messageIdQuery:l,selectedFile:r,totalSaved:o,filters:d,expandedIterations:c,formatTs:u,formatDuration:f,formatTokens:p,formatJSON:b,truncateBlock:gm,toggleExpand:y,toggleIteration:E,isIterationExpanded:I,clearFilters:x,fetchFiles:m,fetchTraces:S,lookupMessage:g}}},vk={template:`
@@ -2178,7 +2178,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h(!0),t=h(null),s=h(!1),n=h({by_user:{},by_channel:{},by_tool:{},recent:[],pricing:{}}),a=h({requests:0,input_tokens:0,output_tokens:0,total_tokens:0,cost_usd:0}),i=h("user");let l=null;const r=[{key:"user",label:"By User"},{key:"channel",label:"By Channel"},{key:"tool",label:"By Tool"},{key:"recent",label:"Recent"}],o=J(()=>[...n.value.recent||[]].reverse()),c=async()=>{try{const b=await K.get("/api/usage");n.value=b,a.value=b.totals||a.value,t.value=null,s.value=!0}catch(b){t.value=b.message}finally{e.value=!1}},d=()=>{e.value=!0,c()};let u=!1;function f(){u||(u=!0,c(),l||(l=setInterval(c,15e3)))}function p(){u&&(u=!1,l&&(clearInterval(l),l=null))}return We(f),Cs(f),Es(p),xt(p),{hasData:s,loading:e,error:t,data:n,totals:a,activeTab:i,tabs:r,recentReversed:o,fmtNum:vm,formatTime:hm,retry:d}}},km=[{id:"audit",label:"Audit",component:fk},{id:"sessions",label:"Sessions",component:hk},{id:"traces",label:"Traces",component:gk},{id:"usage",label:"Usage",component:vk}],bk={components:{TabbedPage:Dr},setup(){return{tabs:km}},template:''},no=[{id:"system",label:"System & Commands",icon:"terminal",match:e=>/^(run_command|run_script|read_file|write_file|list_directory|search_files|manage_process|file_|post_file)/.test(e)},{id:"devops",label:"DevOps & Infrastructure",icon:"server",match:e=>/^(git_ops|docker_ops|kubectl|terraform_ops|http_probe)/.test(e)},{id:"agents",label:"Agents & Orchestration",icon:"bot",match:e=>/^(spawn_agent|send_to_agent|wait_for_agents|get_agent_results|kill_agent|list_agents|spawn_loop_agents|collect_loop_agents)/.test(e)},{id:"workflow",label:"Workflows & Tasks",icon:"workflow",match:e=>/^(delegate_task|cancel_task|list_tasks|schedule_|start_loop|stop_loop|list_loops|delete_schedule|list_schedules|update_schedule|parse_time)/.test(e)},{id:"network",label:"Network & Web",icon:"globe",match:e=>/^(web_|browser_|search_web|fetch_url|http_)/.test(e)},{id:"knowledge",label:"Knowledge & Search",icon:"book",match:e=>/^(search_knowledge|ingest_|knowledge_|search_history|search_audit|bulk_ingest|delete_knowledge|list_knowledge)/.test(e)},{id:"discord",label:"Discord & Admin",icon:"message",match:e=>/^(send_|add_reaction|create_poll|purge_|discord_|embed_|read_channel|set_permission)/.test(e)},{id:"skills",label:"Skills",icon:"puzzle",match:e=>/^(create_skill|edit_skill|delete_skill|enable_skill|disable_skill|install_skill|export_skill|skill_status|invoke_skill|list_skills)/.test(e)},{id:"memory",label:"Memory & State",icon:"brain",match:e=>/^(memory_manage|list_manage)/.test(e)},{id:"ai",label:"AI & Generation",icon:"sparkles",match:e=>/^(generate_|analyze_|claude_|vision_|comfyui_)/.test(e)},{id:"integrations",label:"Integrations",icon:"link",match:e=>/^(issue_tracker|slack_|grafana_|mcp_)/.test(e)},{id:"other",label:"Other Tools",icon:"wrench",match:()=>!0}],yk={template:` + `,setup(){const e=h(!0),t=h(null),s=h(!1),n=h({by_user:{},by_channel:{},by_tool:{},recent:[],pricing:{}}),a=h({requests:0,input_tokens:0,output_tokens:0,total_tokens:0,cost_usd:0}),i=h("user");let l=null;const r=[{key:"user",label:"By User"},{key:"channel",label:"By Channel"},{key:"tool",label:"By Tool"},{key:"recent",label:"Recent"}],o=J(()=>[...n.value.recent||[]].reverse()),c=async()=>{try{const b=await G.get("/api/usage");n.value=b,a.value=b.totals||a.value,t.value=null,s.value=!0}catch(b){t.value=b.message}finally{e.value=!1}},d=()=>{e.value=!0,c()};let u=!1;function f(){u||(u=!0,c(),l||(l=setInterval(c,15e3)))}function p(){u&&(u=!1,l&&(clearInterval(l),l=null))}return We(f),Ds(f),Ms(p),xt(p),{hasData:s,loading:e,error:t,data:n,totals:a,activeTab:i,tabs:r,recentReversed:o,fmtNum:vm,formatTime:hm,retry:d}}},km=[{id:"audit",label:"Audit",component:fk},{id:"sessions",label:"Sessions",component:hk},{id:"traces",label:"Traces",component:gk},{id:"usage",label:"Usage",component:vk}],bk={components:{TabbedPage:Mr},setup(){return{tabs:km}},template:''},no=[{id:"system",label:"System & Commands",icon:"terminal",match:e=>/^(run_command|run_script|read_file|write_file|list_directory|search_files|manage_process|file_|post_file)/.test(e)},{id:"devops",label:"DevOps & Infrastructure",icon:"server",match:e=>/^(git_ops|docker_ops|kubectl|terraform_ops|http_probe)/.test(e)},{id:"agents",label:"Agents & Orchestration",icon:"bot",match:e=>/^(spawn_agent|send_to_agent|wait_for_agents|get_agent_results|kill_agent|list_agents|spawn_loop_agents|collect_loop_agents)/.test(e)},{id:"workflow",label:"Workflows & Tasks",icon:"workflow",match:e=>/^(delegate_task|cancel_task|list_tasks|schedule_|start_loop|stop_loop|list_loops|delete_schedule|list_schedules|update_schedule|parse_time)/.test(e)},{id:"network",label:"Network & Web",icon:"globe",match:e=>/^(web_|browser_|search_web|fetch_url|http_)/.test(e)},{id:"knowledge",label:"Knowledge & Search",icon:"book",match:e=>/^(search_knowledge|ingest_|knowledge_|search_history|search_audit|bulk_ingest|delete_knowledge|list_knowledge)/.test(e)},{id:"discord",label:"Discord & Admin",icon:"message",match:e=>/^(send_|add_reaction|create_poll|purge_|discord_|embed_|read_channel|set_permission)/.test(e)},{id:"skills",label:"Skills",icon:"puzzle",match:e=>/^(create_skill|edit_skill|delete_skill|enable_skill|disable_skill|install_skill|export_skill|skill_status|invoke_skill|list_skills)/.test(e)},{id:"memory",label:"Memory & State",icon:"brain",match:e=>/^(memory_manage|list_manage)/.test(e)},{id:"ai",label:"AI & Generation",icon:"sparkles",match:e=>/^(generate_|analyze_|claude_|vision_|comfyui_)/.test(e)},{id:"integrations",label:"Integrations",icon:"link",match:e=>/^(issue_tracker|slack_|grafana_|mcp_)/.test(e)},{id:"other",label:"Other Tools",icon:"wrench",match:()=>!0}],yk={template:`

Tools

@@ -2343,7 +2343,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config Try a different search term
- `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(""),a=h({}),i=h({}),l=h("cards"),r=h(null),o=J(()=>e.value.filter(x=>x.is_core).length),c=J(()=>e.value.filter(x=>!x.is_core).length),d=J(()=>Object.values(a.value).reduce((x,m)=>x+m,0));function u(x){for(const m of no)if(m.id!=="other"&&m.match(x))return m.id;return"other"}const f=J(()=>{let x=e.value;if(n.value){const m=n.value.toLowerCase();x=x.filter(_=>_.name.toLowerCase().includes(m)||(_.description||"").toLowerCase().includes(m))}return r.value&&(x=x.filter(m=>u(m.name)===r.value)),x}),p=J(()=>{const x=new Set;for(const m of e.value)x.add(u(m.name));return no.filter(m=>x.has(m.id))}),b=J(()=>{const x=f.value,m={};for(const S of x){const g=u(S.name);m[g]||(m[g]=[]),m[g].push(S)}const _=[];for(const S of no)m[S.id]&&m[S.id].length>0&&_.push({label:S.label,icon:S.icon,tools:m[S.id].sort((g,w)=>g.name.localeCompare(w.name))});return _});function y(x){i.value={...i.value,[x]:!i.value[x]}}async function A(){t.value=!0,s.value=null;try{const[x,m]=await Promise.all([K.get("/api/tools"),K.get("/api/tools/stats").catch(()=>({}))]);e.value=x,a.value=m||{};const _=Object.values(m||{}).filter(S=>S>0).sort((S,g)=>S-g)}catch(x){s.value=x.message}t.value=!1}function O(){A()}return We(()=>{A()}),{tools:e,loading:t,error:s,search:n,stats:a,expanded:i,viewMode:l,activeCategory:r,coreCount:o,skillCount:c,totalUsage:d,filteredTools:f,groupedTools:b,usedCategories:p,truncate:Zc,toggleExpand:y,refresh:O}}};function xk(e){if(!e)return"";let t=e.replace(/&/g,"&").replace(//g,">");t=t.replace(/("""[\s\S]*?"""|'''[\s\S]*?'''|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,'$1'),t=t.replace(/(#[^\n]*)/g,'$1');const s="\\b(def|class|return|if|elif|else|for|while|import|from|as|try|except|finally|raise|with|async|await|yield|pass|break|continue|and|or|not|in|is|None|True|False|self|lambda)\\b";t=t.replace(new RegExp(s,"g"),'$1');const n="\\b(print|len|range|str|int|float|list|dict|set|tuple|type|isinstance|hasattr|getattr|setattr|super|property|staticmethod|classmethod|enumerate|zip|map|filter|sorted|reversed|any|all|min|max|sum|abs|round|open|format)\\b";return t=t.replace(new RegExp(n,"g"),'$1'),t=t.replace(/(@\w+)/g,'$1'),t=t.replace(/\b(\d+\.?\d*)\b/g,'$1'),t}function _k(e){if(!e)return"1";const t=e.split(` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(""),a=h({}),i=h({}),l=h("cards"),r=h(null),o=J(()=>e.value.filter(x=>x.is_core).length),c=J(()=>e.value.filter(x=>!x.is_core).length),d=J(()=>Object.values(a.value).reduce((x,m)=>x+m,0));function u(x){for(const m of no)if(m.id!=="other"&&m.match(x))return m.id;return"other"}const f=J(()=>{let x=e.value;if(n.value){const m=n.value.toLowerCase();x=x.filter(_=>_.name.toLowerCase().includes(m)||(_.description||"").toLowerCase().includes(m))}return r.value&&(x=x.filter(m=>u(m.name)===r.value)),x}),p=J(()=>{const x=new Set;for(const m of e.value)x.add(u(m.name));return no.filter(m=>x.has(m.id))}),b=J(()=>{const x=f.value,m={};for(const S of x){const g=u(S.name);m[g]||(m[g]=[]),m[g].push(S)}const _=[];for(const S of no)m[S.id]&&m[S.id].length>0&&_.push({label:S.label,icon:S.icon,tools:m[S.id].sort((g,w)=>g.name.localeCompare(w.name))});return _});function y(x){i.value={...i.value,[x]:!i.value[x]}}async function E(){t.value=!0,s.value=null;try{const[x,m]=await Promise.all([G.get("/api/tools"),G.get("/api/tools/stats").catch(()=>({}))]);e.value=x,a.value=m||{};const _=Object.values(m||{}).filter(S=>S>0).sort((S,g)=>S-g)}catch(x){s.value=x.message}t.value=!1}function I(){E()}return We(()=>{E()}),{tools:e,loading:t,error:s,search:n,stats:a,expanded:i,viewMode:l,activeCategory:r,coreCount:o,skillCount:c,totalUsage:d,filteredTools:f,groupedTools:b,usedCategories:p,truncate:Zc,toggleExpand:y,refresh:I}}};function xk(e){if(!e)return"";let t=e.replace(/&/g,"&").replace(//g,">");t=t.replace(/("""[\s\S]*?"""|'''[\s\S]*?'''|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,'$1'),t=t.replace(/(#[^\n]*)/g,'$1');const s="\\b(def|class|return|if|elif|else|for|while|import|from|as|try|except|finally|raise|with|async|await|yield|pass|break|continue|and|or|not|in|is|None|True|False|self|lambda)\\b";t=t.replace(new RegExp(s,"g"),'$1');const n="\\b(print|len|range|str|int|float|list|dict|set|tuple|type|isinstance|hasattr|getattr|setattr|super|property|staticmethod|classmethod|enumerate|zip|map|filter|sorted|reversed|any|all|min|max|sum|abs|round|open|format)\\b";return t=t.replace(new RegExp(n,"g"),'$1'),t=t.replace(/(@\w+)/g,'$1'),t=t.replace(/\b(\d+\.?\d*)\b/g,'$1'),t}function _k(e){if(!e)return"1";const t=e.split(` `).length;return Array.from({length:t},(s,n)=>n+1).join(` `)}const kk={template:`
@@ -2545,10 +2545,10 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h(!0),s=h(null),n=h({}),a=h({}),i=h(null),l=h(""),r=h(null),o=h(!1),c=h("create"),d=h(""),u=h(""),f=h(null),p=h(null),b=h(!1),y=h(null),A=h(null),O=h(!1),x=J(()=>e.value.length),m=J(()=>e.value.reduce((P,se)=>P+(se.execution_count||0),0)),_=J(()=>e.value.reduce((P,se)=>P+M(se.code),0)),S=J(()=>{if(!l.value)return e.value;const P=l.value.toLowerCase();return e.value.filter(se=>se.name.toLowerCase().includes(P)||(se.description||"").toLowerCase().includes(P))}),g=J(()=>u.value?u.value.split(` -`).length:0),w=J(()=>{const P=Math.max(g.value,1);return Array.from({length:P},(se,ke)=>ke+1).join(` -`)}),T=J(()=>{const P=u.value.trim();return P?P.includes("SKILL_DEFINITION")?P.includes("async def execute")?{valid:!0,message:""}:{valid:!1,message:"Missing async def execute function"}:{valid:!1,message:"Missing SKILL_DEFINITION dict"}:null});function C(P){return xk(P)}function M(P){return P?P.split(` -`).length:0}function B(P){return _k(P)}function $(P){n.value={...n.value,[P]:!n.value[P]}}async function I(P){try{await navigator.clipboard.writeText(P);const se=e.value.find(ke=>ke.code===P);se&&(r.value=se.name,setTimeout(()=>{r.value=null},2e3))}catch{}}function j(P){if(P.key==="Tab"){P.preventDefault();const se=P.target,ke=se.selectionStart,V=se.selectionEnd;u.value=u.value.substring(0,ke)+" "+u.value.substring(V),At(()=>{se.selectionStart=se.selectionEnd=ke+4})}}function Y(P){const se=P.target.previousElementSibling;se&&(se.scrollTop=P.target.scrollTop)}async function H(){t.value=!0,s.value=null;try{e.value=await K.get("/api/skills")}catch(P){s.value=P.message}t.value=!1}async function N(P){i.value=P,delete a.value[P],a.value={...a.value};try{const se=await K.post(`/api/skills/${encodeURIComponent(P)}/test`);a.value={...a.value,[P]:se}}catch(se){a.value={...a.value,[P]:{result:se.message,is_error:!0}}}i.value=null}function L(){o.value=!0,c.value="create",d.value="",u.value="",f.value=null,p.value=null}function Z(P){o.value=!0,c.value="edit",d.value=P.name,u.value=P.code||"",f.value=null,p.value=null}function xe(){o.value=!1,f.value=null,p.value=null}async function _e(){f.value=null,p.value=null;const P=d.value.trim(),se=u.value.trim();if(!P){f.value="Name is required";return}if(!se){f.value="Code is required";return}b.value=!0;try{c.value==="create"?(await K.post("/api/skills",{name:P,code:se}),p.value="Skill created successfully"):(await K.put(`/api/skills/${encodeURIComponent(P)}`,{code:se}),p.value="Skill updated successfully"),await H(),setTimeout(()=>{o.value=!1},800)}catch(ke){f.value=ke.message}b.value=!1}function ae(P){A.value=P}async function fe(){if(A.value){O.value=!0;try{await K.del(`/api/skills/${encodeURIComponent(A.value)}`),await H()}catch(P){Te.error(`Failed to delete skill: ${P.message||"unknown error"}`)}O.value=!1,A.value=null}}return We(()=>{H()}),{skills:e,loading:t,error:s,showCode:n,testResults:a,testing:i,search:l,copied:r,editing:o,editMode:c,editName:d,editCode:u,editError:f,editSuccess:p,saving:b,editorRef:y,deleteTarget:A,deleting:O,enabledCount:x,totalExecutions:m,totalLines:_,displayedSkills:S,editLineCount:g,editorLineNums:w,editValidation:T,highlight:C,truncate:Zc,formatTs:ua,countLines:M,getLineNumbers:B,toggleCode:$,copyCode:I,handleEditorKey:j,syncScroll:Y,fetchSkills:H,testSkill:N,showCreate:L,editSkill:Z,cancelEdit:xe,saveSkill:_e,confirmDelete:ae,doDelete:fe}}};function wk(e,t){if(!e||!t)return Nu(e);const s=Nu(e),n=t.trim().split(/\s+/).filter(Boolean);if(!n.length)return s;const a=n.map(i=>i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|");try{return s.replace(new RegExp(`(${a})`,"gi"),'$1')}catch{return s}}const Sk={template:` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h({}),a=h({}),i=h(null),l=h(""),r=h(null),o=h(!1),c=h("create"),d=h(""),u=h(""),f=h(null),p=h(null),b=h(!1),y=h(null),E=h(null),I=h(!1),x=J(()=>e.value.length),m=J(()=>e.value.reduce((F,se)=>F+(se.execution_count||0),0)),_=J(()=>e.value.reduce((F,se)=>F+M(se.code),0)),S=J(()=>{if(!l.value)return e.value;const F=l.value.toLowerCase();return e.value.filter(se=>se.name.toLowerCase().includes(F)||(se.description||"").toLowerCase().includes(F))}),g=J(()=>u.value?u.value.split(` +`).length:0),w=J(()=>{const F=Math.max(g.value,1);return Array.from({length:F},(se,Se)=>Se+1).join(` +`)}),T=J(()=>{const F=u.value.trim();return F?F.includes("SKILL_DEFINITION")?F.includes("async def execute")?{valid:!0,message:""}:{valid:!1,message:"Missing async def execute function"}:{valid:!1,message:"Missing SKILL_DEFINITION dict"}:null});function C(F){return xk(F)}function M(F){return F?F.split(` +`).length:0}function H(F){return _k(F)}function P(F){n.value={...n.value,[F]:!n.value[F]}}async function R(F){try{await navigator.clipboard.writeText(F);const se=e.value.find(Se=>Se.code===F);se&&(r.value=se.name,setTimeout(()=>{r.value=null},2e3))}catch{}}function j(F){if(F.key==="Tab"){F.preventDefault();const se=F.target,Se=se.selectionStart,V=se.selectionEnd;u.value=u.value.substring(0,Se)+" "+u.value.substring(V),Rt(()=>{se.selectionStart=se.selectionEnd=Se+4})}}function Q(F){const se=F.target.previousElementSibling;se&&(se.scrollTop=F.target.scrollTop)}async function U(){t.value=!0,s.value=null;try{e.value=await G.get("/api/skills")}catch(F){s.value=F.message}t.value=!1}async function O(F){i.value=F,delete a.value[F],a.value={...a.value};try{const se=await G.post(`/api/skills/${encodeURIComponent(F)}/test`);a.value={...a.value,[F]:se}}catch(se){a.value={...a.value,[F]:{result:se.message,is_error:!0}}}i.value=null}function N(){o.value=!0,c.value="create",d.value="",u.value="",f.value=null,p.value=null}function Y(F){o.value=!0,c.value="edit",d.value=F.name,u.value=F.code||"",f.value=null,p.value=null}function we(){o.value=!1,f.value=null,p.value=null}async function ke(){f.value=null,p.value=null;const F=d.value.trim(),se=u.value.trim();if(!F){f.value="Name is required";return}if(!se){f.value="Code is required";return}b.value=!0;try{c.value==="create"?(await G.post("/api/skills",{name:F,code:se}),p.value="Skill created successfully"):(await G.put(`/api/skills/${encodeURIComponent(F)}`,{code:se}),p.value="Skill updated successfully"),await U(),setTimeout(()=>{o.value=!1},800)}catch(Se){f.value=Se.message}b.value=!1}function ie(F){E.value=F}async function he(){if(E.value){I.value=!0;try{await G.del(`/api/skills/${encodeURIComponent(E.value)}`),await U()}catch(F){Ae.error(`Failed to delete skill: ${F.message||"unknown error"}`)}I.value=!1,E.value=null}}return We(()=>{U()}),{skills:e,loading:t,error:s,showCode:n,testResults:a,testing:i,search:l,copied:r,editing:o,editMode:c,editName:d,editCode:u,editError:f,editSuccess:p,saving:b,editorRef:y,deleteTarget:E,deleting:I,enabledCount:x,totalExecutions:m,totalLines:_,displayedSkills:S,editLineCount:g,editorLineNums:w,editValidation:T,highlight:C,truncate:Zc,formatTs:pa,countLines:M,getLineNumbers:H,toggleCode:P,copyCode:R,handleEditorKey:j,syncScroll:Q,fetchSkills:U,testSkill:O,showCreate:N,editSkill:Y,cancelEdit:we,saveSkill:ke,confirmDelete:ie,doDelete:he}}};function wk(e,t){if(!e||!t)return Nu(e);const s=Nu(e),n=t.trim().split(/\s+/).filter(Boolean);if(!n.length)return s;const a=n.map(i=>i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|");try{return s.replace(new RegExp(`(${a})`,"gi"),'$1')}catch{return s}}const Sk={template:`

Knowledge

@@ -2739,7 +2739,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(""),a=h(null),i=h(!1),l=h(""),r=h(null),o=h(!1),c=h(""),d=h(""),u=h(null),f=h(null),p=h(!1),b=h(null),y=h(null);let A=null;const O=h(null),x=h(!1),m=h({}),_=h({}),S=h(null),g=h(null),w=J(()=>e.value.reduce((L,Z)=>L+(Z.chunks||0),0)),T=J(()=>new Set(e.value.map(Z=>Z.uploader).filter(Boolean)).size);function C(L,Z){const xe=_.value[Z];if(!xe||xe.length===0)return 0;const _e=Math.max(...xe.map(ae=>ae.char_count||0));return _e===0?0:Math.round(L.char_count/_e*100)}async function M(){t.value=!0,s.value=null;try{const L=await K.get("/api/knowledge");e.value=Array.isArray(L)?L:[]}catch(L){s.value=L.message}t.value=!1}async function B(L){if(m.value[L]){m.value[L]=!1,g.value=null;return}if(m.value[L]=!0,!(_.value[L]||S.value===L)){S.value=L;try{const Z=await K.get(`/api/knowledge/${encodeURIComponent(L)}/chunks`);_.value[L]=Array.isArray(Z)?Z:[]}catch(Z){_.value[L]=[],Te.error(`Failed to load chunks: ${Z.message}`)}S.value=null}}async function $(){const L=n.value.trim();if(L){i.value=!0,r.value=null,l.value=L;try{const Z=await K.get(`/api/knowledge/search?q=${encodeURIComponent(L)}`);a.value=Array.isArray(Z)?Z:[]}catch(Z){a.value=[],r.value=Z.message||"Search failed"}i.value=!1}}function I(){a.value=null,n.value="",r.value=null}async function j(){u.value=null,f.value=null;const L=c.value.trim(),Z=d.value.trim();if(!L){u.value="Source name is required";return}if(!Z){u.value="Content is required";return}p.value=!0;try{const xe=await K.post("/api/knowledge",{source:L,content:Z});f.value=`Ingested ${xe.chunks||0} chunks from "${L}"`,c.value="",d.value="",_.value={},await M(),setTimeout(()=>{o.value=!1,f.value=null},1500)}catch(xe){u.value=xe.message}p.value=!1}async function Y(L){b.value=L,y.value=null,A&&(clearTimeout(A),A=null);try{const Z=await K.post(`/api/knowledge/${encodeURIComponent(L)}/reingest`);y.value={source:L,error:!1,message:`Re-ingested ${Z.chunks||0} chunks`},delete _.value[L],await M(),A=setTimeout(()=>{y.value=null,A=null},3e3)}catch(Z){y.value={source:L,error:!0,message:Z.message}}b.value=null}function H(L){O.value=L}async function N(){if(O.value){x.value=!0;try{await K.del(`/api/knowledge/${encodeURIComponent(O.value)}`),delete _.value[O.value],await M()}catch(L){Te.error(`Failed to delete source: ${L.message||"unknown error"}`)}x.value=!1,O.value=null}}return We(()=>{M()}),{sources:e,loading:t,error:s,searchQuery:n,searchResults:a,searching:i,lastQuery:l,searchError:r,showIngest:o,ingestSource:c,ingestContent:d,ingestError:u,ingestSuccess:f,ingesting:p,reingesting:b,reingestResult:y,deleteTarget:O,deleting:x,expanded:m,sourceChunks:_,loadingChunks:S,selectedChunk:g,totalChunks:w,uploaderCount:T,truncate:Zc,formatTs:ua,highlightTerms:wk,chunkBarWidth:C,fetchSources:M,toggleSource:B,doSearch:$,clearSearch:I,doIngest:j,doReingest:Y,confirmDelete:H,doDelete:N}}},Tk={template:` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h(""),a=h(null),i=h(!1),l=h(""),r=h(null),o=h(!1),c=h(""),d=h(""),u=h(null),f=h(null),p=h(!1),b=h(null),y=h(null);let E=null;const I=h(null),x=h(!1),m=h({}),_=h({}),S=h(null),g=h(null),w=J(()=>e.value.reduce((N,Y)=>N+(Y.chunks||0),0)),T=J(()=>new Set(e.value.map(Y=>Y.uploader).filter(Boolean)).size);function C(N,Y){const we=_.value[Y];if(!we||we.length===0)return 0;const ke=Math.max(...we.map(ie=>ie.char_count||0));return ke===0?0:Math.round(N.char_count/ke*100)}async function M(){t.value=!0,s.value=null;try{const N=await G.get("/api/knowledge");e.value=Array.isArray(N)?N:[]}catch(N){s.value=N.message}t.value=!1}async function H(N){if(m.value[N]){m.value[N]=!1,g.value=null;return}if(m.value[N]=!0,!(_.value[N]||S.value===N)){S.value=N;try{const Y=await G.get(`/api/knowledge/${encodeURIComponent(N)}/chunks`);_.value[N]=Array.isArray(Y)?Y:[]}catch(Y){_.value[N]=[],Ae.error(`Failed to load chunks: ${Y.message}`)}S.value=null}}async function P(){const N=n.value.trim();if(N){i.value=!0,r.value=null,l.value=N;try{const Y=await G.get(`/api/knowledge/search?q=${encodeURIComponent(N)}`);a.value=Array.isArray(Y)?Y:[]}catch(Y){a.value=[],r.value=Y.message||"Search failed"}i.value=!1}}function R(){a.value=null,n.value="",r.value=null}async function j(){u.value=null,f.value=null;const N=c.value.trim(),Y=d.value.trim();if(!N){u.value="Source name is required";return}if(!Y){u.value="Content is required";return}p.value=!0;try{const we=await G.post("/api/knowledge",{source:N,content:Y});f.value=`Ingested ${we.chunks||0} chunks from "${N}"`,c.value="",d.value="",_.value={},await M(),setTimeout(()=>{o.value=!1,f.value=null},1500)}catch(we){u.value=we.message}p.value=!1}async function Q(N){b.value=N,y.value=null,E&&(clearTimeout(E),E=null);try{const Y=await G.post(`/api/knowledge/${encodeURIComponent(N)}/reingest`);y.value={source:N,error:!1,message:`Re-ingested ${Y.chunks||0} chunks`},delete _.value[N],await M(),E=setTimeout(()=>{y.value=null,E=null},3e3)}catch(Y){y.value={source:N,error:!0,message:Y.message}}b.value=null}function U(N){I.value=N}async function O(){if(I.value){x.value=!0;try{await G.del(`/api/knowledge/${encodeURIComponent(I.value)}`),delete _.value[I.value],await M()}catch(N){Ae.error(`Failed to delete source: ${N.message||"unknown error"}`)}x.value=!1,I.value=null}}return We(()=>{M()}),{sources:e,loading:t,error:s,searchQuery:n,searchResults:a,searching:i,lastQuery:l,searchError:r,showIngest:o,ingestSource:c,ingestContent:d,ingestError:u,ingestSuccess:f,ingesting:p,reingesting:b,reingestResult:y,deleteTarget:I,deleting:x,expanded:m,sourceChunks:_,loadingChunks:S,selectedChunk:g,totalChunks:w,uploaderCount:T,truncate:Zc,formatTs:pa,highlightTerms:wk,chunkBarWidth:C,fetchSources:M,toggleSource:H,doSearch:P,clearSearch:R,doIngest:j,doReingest:Q,confirmDelete:U,doDelete:O}}},Tk={template:`

Memory

@@ -2931,7 +2931,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h({}),s=h(!0),n=h(null),a=h({}),i=h(null),l=h(""),r=h(!1),o=h({scope:"global",key:"",value:""}),c=h(!1),d=h(null),u=h(null),f=h(null),p=h(""),b=h(!1),y=h(null),A=h(null),O=h(new Set),x=h(null),m=h(!1),_=h(!1),S=J(()=>e.value.reduce((ae,fe)=>ae+fe.count,0)),g=J(()=>O.value.size);function w(ae){const fe=t.value[ae];if(!fe)return[];if(!l.value.trim())return fe;const P=l.value.trim().toLowerCase();return fe.filter(se=>se.key.toLowerCase().includes(P)||se.value&&se.value.toLowerCase().includes(P))}function T(ae,fe){return O.value.has(ae+"/"+fe)}function C(ae,fe){const P=ae+"/"+fe,se=new Set(O.value);se.has(P)?se.delete(P):se.add(P),O.value=se}function M(ae){const fe=t.value[ae];return!fe||fe.length===0?!1:fe.every(P=>O.value.has(ae+"/"+P.key))}function B(ae,fe){const P=t.value[ae];if(!P)return;const se=new Set(O.value);for(const ke of P){const V=ae+"/"+ke.key;fe?se.add(V):se.delete(V)}O.value=se}async function $(){s.value=!0,n.value=null;try{const ae=await K.get("/api/memory");e.value=Object.entries(ae).map(([fe,P])=>({name:fe,keys:P.keys||[],count:P.count||0}))}catch(ae){n.value=ae.message}s.value=!1}async function I(ae){if(a.value[ae]){a.value[ae]=!1;return}a.value[ae]=!0;const fe=e.value.find(se=>se.name===ae);if(!fe||t.value[ae]||i.value===ae)return;i.value=ae;let P;try{const ke=(await K.get(`/api/memory/${encodeURIComponent(ae)}`)).entries||{};P=fe.keys.map(V=>Object.prototype.hasOwnProperty.call(ke,V)?{key:V,value:ke[V]||"",failed:!1}:{key:V,value:"",failed:!0,error:"Not found in scope"})}catch(se){P=fe.keys.map(ke=>({key:ke,value:"",failed:!0,error:se.message||"Failed to load"}))}t.value[ae]=P,i.value=null}function j(ae,fe,P){f.value=ae+"/"+fe,p.value=P}async function Y(ae,fe){b.value=!0,y.value=null;try{await K.put(`/api/memory/${encodeURIComponent(ae)}/${encodeURIComponent(fe)}`,{value:p.value});const P=t.value[ae];if(P){const se=P.find(ke=>ke.key===fe);se&&(se.value=p.value)}f.value=null}catch(P){y.value=`Failed to save: ${P.message||"unknown error"}`}b.value=!1}async function H(ae,fe){try{await navigator.clipboard.writeText(fe.value),A.value=ae+"/"+fe.key,setTimeout(()=>{A.value=null},1500)}catch{}}async function N(){d.value=null,u.value=null;const ae=o.value.scope.trim(),fe=o.value.key.trim(),P=o.value.value.trim();if(!ae){d.value="Scope is required";return}if(!fe){d.value="Key is required";return}if(!P){d.value="Value is required";return}c.value=!0;try{await K.put(`/api/memory/${encodeURIComponent(ae)}/${encodeURIComponent(fe)}`,{value:P}),u.value="Entry saved",o.value={scope:"global",key:"",value:""},t.value={},await $(),setTimeout(()=>{r.value=!1,u.value=null},800)}catch(se){d.value=se.message}c.value=!1}function L(ae,fe){x.value={scope:ae,key:fe}}async function Z(){if(!x.value)return;m.value=!0,y.value=null;const{scope:ae,key:fe}=x.value;try{await K.del(`/api/memory/${encodeURIComponent(ae)}/${encodeURIComponent(fe)}`);const P=t.value[ae];P&&(t.value[ae]=P.filter(V=>V.key!==fe));const se=e.value.find(V=>V.name===ae);se&&(se.count--,se.keys=se.keys.filter(V=>V!==fe));const ke=new Set(O.value);ke.delete(ae+"/"+fe),O.value=ke}catch(P){y.value=`Failed to delete: ${P.message||"unknown error"}`}m.value=!1,x.value=null}function xe(){_.value=!0}async function _e(){m.value=!0,y.value=null;const ae=[];for(const fe of O.value){const P=fe.indexOf("/");ae.push({scope:fe.slice(0,P),key:fe.slice(P+1)})}try{await K.post("/api/memory/bulk-delete",{entries:ae}),O.value=new Set,t.value={},await $()}catch(fe){y.value=`Bulk delete failed: ${fe.message||"unknown error"}`}m.value=!1,_.value=!1}return We(()=>{$()}),{scopes:e,scopeEntries:t,loading:s,error:n,expanded:a,loadingScope:i,filterQuery:l,showAdd:r,addForm:o,adding:c,addError:d,addSuccess:u,editingKey:f,editValue:p,saving:b,actionError:y,copied:A,selected:O,selectedCount:g,totalEntries:S,deleteTarget:x,deleting:m,showBulkDelete:_,fetchMemory:$,toggleScope:I,startEdit:j,doEdit:Y,copyValue:H,doAdd:N,confirmDelete:L,doDelete:Z,confirmBulkDelete:xe,doBulkDelete:_e,isSelected:T,toggleSelect:C,isScopeAllSelected:M,toggleSelectAll:B,filteredEntries:w}}},Ck={template:` + `,setup(){const e=h([]),t=h({}),s=h(!0),n=h(null),a=h({}),i=h(null),l=h(""),r=h(!1),o=h({scope:"global",key:"",value:""}),c=h(!1),d=h(null),u=h(null),f=h(null),p=h(""),b=h(!1),y=h(null),E=h(null),I=h(new Set),x=h(null),m=h(!1),_=h(!1),S=J(()=>e.value.reduce((ie,he)=>ie+he.count,0)),g=J(()=>I.value.size);function w(ie){const he=t.value[ie];if(!he)return[];if(!l.value.trim())return he;const F=l.value.trim().toLowerCase();return he.filter(se=>se.key.toLowerCase().includes(F)||se.value&&se.value.toLowerCase().includes(F))}function T(ie,he){return I.value.has(ie+"/"+he)}function C(ie,he){const F=ie+"/"+he,se=new Set(I.value);se.has(F)?se.delete(F):se.add(F),I.value=se}function M(ie){const he=t.value[ie];return!he||he.length===0?!1:he.every(F=>I.value.has(ie+"/"+F.key))}function H(ie,he){const F=t.value[ie];if(!F)return;const se=new Set(I.value);for(const Se of F){const V=ie+"/"+Se.key;he?se.add(V):se.delete(V)}I.value=se}async function P(){s.value=!0,n.value=null;try{const ie=await G.get("/api/memory");e.value=Object.entries(ie).map(([he,F])=>({name:he,keys:F.keys||[],count:F.count||0}))}catch(ie){n.value=ie.message}s.value=!1}async function R(ie){if(a.value[ie]){a.value[ie]=!1;return}a.value[ie]=!0;const he=e.value.find(se=>se.name===ie);if(!he||t.value[ie]||i.value===ie)return;i.value=ie;let F;try{const Se=(await G.get(`/api/memory/${encodeURIComponent(ie)}`)).entries||{};F=he.keys.map(V=>Object.prototype.hasOwnProperty.call(Se,V)?{key:V,value:Se[V]||"",failed:!1}:{key:V,value:"",failed:!0,error:"Not found in scope"})}catch(se){F=he.keys.map(Se=>({key:Se,value:"",failed:!0,error:se.message||"Failed to load"}))}t.value[ie]=F,i.value=null}function j(ie,he,F){f.value=ie+"/"+he,p.value=F}async function Q(ie,he){b.value=!0,y.value=null;try{await G.put(`/api/memory/${encodeURIComponent(ie)}/${encodeURIComponent(he)}`,{value:p.value});const F=t.value[ie];if(F){const se=F.find(Se=>Se.key===he);se&&(se.value=p.value)}f.value=null}catch(F){y.value=`Failed to save: ${F.message||"unknown error"}`}b.value=!1}async function U(ie,he){try{await navigator.clipboard.writeText(he.value),E.value=ie+"/"+he.key,setTimeout(()=>{E.value=null},1500)}catch{}}async function O(){d.value=null,u.value=null;const ie=o.value.scope.trim(),he=o.value.key.trim(),F=o.value.value.trim();if(!ie){d.value="Scope is required";return}if(!he){d.value="Key is required";return}if(!F){d.value="Value is required";return}c.value=!0;try{await G.put(`/api/memory/${encodeURIComponent(ie)}/${encodeURIComponent(he)}`,{value:F}),u.value="Entry saved",o.value={scope:"global",key:"",value:""},t.value={},await P(),setTimeout(()=>{r.value=!1,u.value=null},800)}catch(se){d.value=se.message}c.value=!1}function N(ie,he){x.value={scope:ie,key:he}}async function Y(){if(!x.value)return;m.value=!0,y.value=null;const{scope:ie,key:he}=x.value;try{await G.del(`/api/memory/${encodeURIComponent(ie)}/${encodeURIComponent(he)}`);const F=t.value[ie];F&&(t.value[ie]=F.filter(V=>V.key!==he));const se=e.value.find(V=>V.name===ie);se&&(se.count--,se.keys=se.keys.filter(V=>V!==he));const Se=new Set(I.value);Se.delete(ie+"/"+he),I.value=Se}catch(F){y.value=`Failed to delete: ${F.message||"unknown error"}`}m.value=!1,x.value=null}function we(){_.value=!0}async function ke(){m.value=!0,y.value=null;const ie=[];for(const he of I.value){const F=he.indexOf("/");ie.push({scope:he.slice(0,F),key:he.slice(F+1)})}try{await G.post("/api/memory/bulk-delete",{entries:ie}),I.value=new Set,t.value={},await P()}catch(he){y.value=`Bulk delete failed: ${he.message||"unknown error"}`}m.value=!1,_.value=!1}return We(()=>{P()}),{scopes:e,scopeEntries:t,loading:s,error:n,expanded:a,loadingScope:i,filterQuery:l,showAdd:r,addForm:o,adding:c,addError:d,addSuccess:u,editingKey:f,editValue:p,saving:b,actionError:y,copied:E,selected:I,selectedCount:g,totalEntries:S,deleteTarget:x,deleting:m,showBulkDelete:_,fetchMemory:P,toggleScope:R,startEdit:j,doEdit:Q,copyValue:U,doAdd:O,confirmDelete:N,doDelete:Y,confirmBulkDelete:we,doBulkDelete:ke,isSelected:T,toggleSelect:C,isScopeAllSelected:M,toggleSelectAll:H,filteredEntries:w}}},Ck={template:`
@@ -3000,7 +3000,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h(null),s=h(!0),n=h(null),a=h(null),i=h(null),l=h(""),r=J(()=>[...new Set(e.value.map(A=>A.category))].sort()),o=J(()=>{const y={};return e.value.forEach(A=>{y[A.category]=(y[A.category]||0)+1}),y}),c=J(()=>a.value?e.value.filter(y=>y.category===a.value):e.value);function d(y){return y==="correction"?"badge-warning":y==="operational"?"badge-info":y==="preference"?"badge-success":"badge-info"}function u(y){i.value=y.key,l.value=y.content}async function f(y){try{await K.put("/api/learned/"+encodeURIComponent(y),{content:l.value}),i.value=null,Te.success("Entry updated"),await b()}catch(A){Te.error(A.message||"Failed to save entry")}}async function p(y){if(await gs({title:"Delete learned entry",message:`Delete "${y}"? Odin will no longer apply this learned context.`,confirmLabel:"Delete",danger:!0}))try{await K.del("/api/learned/"+encodeURIComponent(y)),Te.success("Entry deleted"),await b()}catch(O){Te.error(O.message||"Failed to delete entry")}}async function b(){s.value=!0,n.value=null;try{const y=await K.get("/api/learned");e.value=y.entries||[],t.value={last_reflection:y.last_reflection,count:y.count}}catch(y){n.value=y.message}s.value=!1}return We(b),{entries:e,meta:t,loading:s,error:n,filterCat:a,editing:i,editContent:l,categories:r,catCounts:o,filtered:c,catBadge:d,formatTs:ua,startEdit:u,saveEdit:f,deleteEntry:p,fetchEntries:b}}},wm=[{id:"tools",label:"Tools",component:yk},{id:"skills",label:"Skills",component:kk},{id:"knowledge",label:"Knowledge",component:Sk},{id:"memory",label:"Memory",component:Tk},{id:"learned",label:"Learned",component:Ck}],Ek={components:{TabbedPage:Dr},setup(){return{tabs:wm}},template:''},Ak={ok:"text-green-400",degraded:"text-yellow-400",down:"text-red-400",unconfigured:"text-gray-500"},Rk={ok:"success",degraded:"warning",down:"error",unconfigured:"minus"},Ik={healthy:"text-green-400",degraded:"text-yellow-400",unhealthy:"text-red-400"},Ok={template:` + `,setup(){const e=h([]),t=h(null),s=h(!0),n=h(null),a=h(null),i=h(null),l=h(""),r=J(()=>[...new Set(e.value.map(E=>E.category))].sort()),o=J(()=>{const y={};return e.value.forEach(E=>{y[E.category]=(y[E.category]||0)+1}),y}),c=J(()=>a.value?e.value.filter(y=>y.category===a.value):e.value);function d(y){return y==="correction"?"badge-warning":y==="operational"?"badge-info":y==="preference"?"badge-success":"badge-info"}function u(y){i.value=y.key,l.value=y.content}async function f(y){try{await G.put("/api/learned/"+encodeURIComponent(y),{content:l.value}),i.value=null,Ae.success("Entry updated"),await b()}catch(E){Ae.error(E.message||"Failed to save entry")}}async function p(y){if(await _s({title:"Delete learned entry",message:`Delete "${y}"? Odin will no longer apply this learned context.`,confirmLabel:"Delete",danger:!0}))try{await G.del("/api/learned/"+encodeURIComponent(y)),Ae.success("Entry deleted"),await b()}catch(I){Ae.error(I.message||"Failed to delete entry")}}async function b(){s.value=!0,n.value=null;try{const y=await G.get("/api/learned");e.value=y.entries||[],t.value={last_reflection:y.last_reflection,count:y.count}}catch(y){n.value=y.message}s.value=!1}return We(b),{entries:e,meta:t,loading:s,error:n,filterCat:a,editing:i,editContent:l,categories:r,catCounts:o,filtered:c,catBadge:d,formatTs:pa,startEdit:u,saveEdit:f,deleteEntry:p,fetchEntries:b}}},wm=[{id:"tools",label:"Tools",component:yk},{id:"skills",label:"Skills",component:kk},{id:"knowledge",label:"Knowledge",component:Sk},{id:"memory",label:"Memory",component:Tk},{id:"learned",label:"Learned",component:Ck}],Ek={components:{TabbedPage:Mr},setup(){return{tabs:wm}},template:''},Ak={ok:"text-green-400",degraded:"text-yellow-400",down:"text-red-400",unconfigured:"text-gray-500"},Rk={ok:"success",degraded:"warning",down:"error",unconfigured:"minus"},Ik={healthy:"text-green-400",degraded:"text-yellow-400",unhealthy:"text-red-400"},Ok={template:`
@@ -3142,7 +3142,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h({}),t=h(!0),s=h(null),n=h(!1),a=h(!1),i=J(()=>e.value.components||[]),l=J(()=>Ik[e.value.overall]||"text-gray-400"),r=J(()=>e.value.overall==="healthy"?"success":e.value.overall==="degraded"?"warning":e.value.overall==="unhealthy"?"error":"minus"),o=J(()=>{const g=e.value.overall;return g==="healthy"?"All Systems Healthy":g==="degraded"?"Some Systems Degraded":g==="unhealthy"?"System Issues Detected":"Unknown"});function c(g){return Ak[g]||"text-gray-400"}function d(g){return Rk[g]||"info"}function u(g){return g==="ok"?"badge-success":g==="degraded"?"badge-warning":g==="down"?"badge-danger":"badge-info"}function f(g){return g==="closed"?"text-green-400":g==="half_open"?"text-yellow-400":g==="open"?"text-red-400":"text-gray-400"}function p(g){return g.replace(/_/g," ").replace(/\b\w/g,w=>w.toUpperCase())}function b(g){if(!g)return"—";try{return new Date(g).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch{return g}}function y(g){return g>=1e6?(g/1e6).toFixed(1)+"M":g>=1e3?(g/1e3).toFixed(1)+"K":String(g)}async function A(){a.value=!0;try{e.value=await K.get("/api/health/components"),s.value=null,n.value=!0}catch(g){s.value=g.message}finally{t.value=!1,a.value=!1}}function O(){t.value=!0,s.value=null,A()}let x=null,m=!1;function _(){m||(m=!0,A(),x||(x=setInterval(A,3e4)))}function S(){m&&(m=!1,x&&(clearInterval(x),x=null))}return We(_),Cs(_),Es(S),xt(S),{data:e,hasData:n,loading:t,error:s,refreshing:a,components:i,overallColor:l,overallIcon:r,overallLabel:o,statusColor:c,statusIcon:d,badgeClass:u,circuitColor:f,formatName:p,formatTime:b,formatNumber:y,fetchHealth:A,retry:O}}},Nk={template:` + `,setup(){const e=h({}),t=h(!0),s=h(null),n=h(!1),a=h(!1),i=J(()=>e.value.components||[]),l=J(()=>Ik[e.value.overall]||"text-gray-400"),r=J(()=>e.value.overall==="healthy"?"success":e.value.overall==="degraded"?"warning":e.value.overall==="unhealthy"?"error":"minus"),o=J(()=>{const g=e.value.overall;return g==="healthy"?"All Systems Healthy":g==="degraded"?"Some Systems Degraded":g==="unhealthy"?"System Issues Detected":"Unknown"});function c(g){return Ak[g]||"text-gray-400"}function d(g){return Rk[g]||"info"}function u(g){return g==="ok"?"badge-success":g==="degraded"?"badge-warning":g==="down"?"badge-danger":"badge-info"}function f(g){return g==="closed"?"text-green-400":g==="half_open"?"text-yellow-400":g==="open"?"text-red-400":"text-gray-400"}function p(g){return g.replace(/_/g," ").replace(/\b\w/g,w=>w.toUpperCase())}function b(g){if(!g)return"—";try{return new Date(g).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch{return g}}function y(g){return g>=1e6?(g/1e6).toFixed(1)+"M":g>=1e3?(g/1e3).toFixed(1)+"K":String(g)}async function E(){a.value=!0;try{e.value=await G.get("/api/health/components"),s.value=null,n.value=!0}catch(g){s.value=g.message}finally{t.value=!1,a.value=!1}}function I(){t.value=!0,s.value=null,E()}let x=null,m=!1;function _(){m||(m=!0,E(),x||(x=setInterval(E,3e4)))}function S(){m&&(m=!1,x&&(clearInterval(x),x=null))}return We(_),Ds(_),Ms(S),xt(S),{data:e,hasData:n,loading:t,error:s,refreshing:a,components:i,overallColor:l,overallIcon:r,overallLabel:o,statusColor:c,statusIcon:d,badgeClass:u,circuitColor:f,formatName:p,formatTime:b,formatNumber:y,fetchHealth:E,retry:I}}},Nk={template:`
@@ -3385,7 +3385,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h(!0),t=h(null),s=h(!1),n=h(!1),a=h("sessions"),i=h(null);let l=null;const r=[{key:"sessions",label:"Sessions"},{key:"knowledge",label:"Knowledge"},{key:"trajectories",label:"Trajectories"},{key:"storage",label:"Storage"}],o=J(()=>{if(!i.value||!i.value.collected_at)return"";try{return new Date(i.value.collected_at).toLocaleTimeString()}catch{return""}}),c=J(()=>{if(!i.value)return[];const A=i.value,O=A.storage_total_bytes||1;return[{label:"Session Persistence",mb:A.sessions.persist_dir.total_mb,bytes:A.sessions.persist_dir.total_bytes,files:A.sessions.persist_dir.file_count,pct:Math.min(100,Math.round(A.sessions.persist_dir.total_bytes/O*100)),color:"res-bar-blue"},{label:"Knowledge Database",mb:A.knowledge.db_file.total_mb,bytes:A.knowledge.db_file.total_bytes,files:A.knowledge.db_file.file_count,pct:Math.min(100,Math.round(A.knowledge.db_file.total_bytes/O*100)),color:"res-bar-purple"},{label:"Message Trajectories",mb:A.trajectories.message_dir.total_mb,bytes:A.trajectories.message_dir.total_bytes,files:A.trajectories.message_dir.file_count,pct:Math.min(100,Math.round(A.trajectories.message_dir.total_bytes/O*100)),color:"res-bar-emerald"},{label:"Agent Trajectories",mb:A.trajectories.agent_dir.total_mb,bytes:A.trajectories.agent_dir.total_bytes,files:A.trajectories.agent_dir.file_count,pct:Math.min(100,Math.round(A.trajectories.agent_dir.total_bytes/O*100)),color:"res-bar-amber"}]});async function d(){try{const A=await K.get("/api/resource-usage");i.value=A,t.value=null,s.value=!0}catch(A){t.value=A.message||"Failed to load resource usage"}finally{e.value=!1,n.value=!1}}async function u(){n.value=!0,await d()}function f(){e.value=!0,t.value=null,d()}let p=!1;function b(){p||(p=!0,d(),l||(l=setInterval(d,3e4)))}function y(){p&&(p=!1,l&&(clearInterval(l),l=null))}return We(b),Cs(b),Es(y),xt(y),{hasData:s,loading:e,error:t,refreshing:n,data:i,activeTab:a,tabs:r,collectedAt:o,storageItems:c,fmtNum:vm,refresh:u,retry:f}}},Lk=["INFO","WARNING","ERROR"],Dk=[{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"}}],ao=[{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}],Mk=[50,100,200,500],Pk={template:` + `,setup(){const e=h(!0),t=h(null),s=h(!1),n=h(!1),a=h("sessions"),i=h(null);let l=null;const r=[{key:"sessions",label:"Sessions"},{key:"knowledge",label:"Knowledge"},{key:"trajectories",label:"Trajectories"},{key:"storage",label:"Storage"}],o=J(()=>{if(!i.value||!i.value.collected_at)return"";try{return new Date(i.value.collected_at).toLocaleTimeString()}catch{return""}}),c=J(()=>{if(!i.value)return[];const E=i.value,I=E.storage_total_bytes||1;return[{label:"Session Persistence",mb:E.sessions.persist_dir.total_mb,bytes:E.sessions.persist_dir.total_bytes,files:E.sessions.persist_dir.file_count,pct:Math.min(100,Math.round(E.sessions.persist_dir.total_bytes/I*100)),color:"res-bar-blue"},{label:"Knowledge Database",mb:E.knowledge.db_file.total_mb,bytes:E.knowledge.db_file.total_bytes,files:E.knowledge.db_file.file_count,pct:Math.min(100,Math.round(E.knowledge.db_file.total_bytes/I*100)),color:"res-bar-purple"},{label:"Message Trajectories",mb:E.trajectories.message_dir.total_mb,bytes:E.trajectories.message_dir.total_bytes,files:E.trajectories.message_dir.file_count,pct:Math.min(100,Math.round(E.trajectories.message_dir.total_bytes/I*100)),color:"res-bar-emerald"},{label:"Agent Trajectories",mb:E.trajectories.agent_dir.total_mb,bytes:E.trajectories.agent_dir.total_bytes,files:E.trajectories.agent_dir.file_count,pct:Math.min(100,Math.round(E.trajectories.agent_dir.total_bytes/I*100)),color:"res-bar-amber"}]});async function d(){try{const E=await G.get("/api/resource-usage");i.value=E,t.value=null,s.value=!0}catch(E){t.value=E.message||"Failed to load resource usage"}finally{e.value=!1,n.value=!1}}async function u(){n.value=!0,await d()}function f(){e.value=!0,t.value=null,d()}let p=!1;function b(){p||(p=!0,d(),l||(l=setInterval(d,3e4)))}function y(){p&&(p=!1,l&&(clearInterval(l),l=null))}return We(b),Ds(b),Ms(y),xt(y),{hasData:s,loading:e,error:t,refreshing:n,data:i,activeTab:a,tabs:r,collectedAt:o,storageItems:c,fmtNum:vm,refresh:u,retry:f}}},Lk=["INFO","WARNING","ERROR"],Dk=[{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"}}],ao=[{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}],Mk=[50,100,200,500],Pk={template:`
@@ -3743,9 +3743,9 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h("live"),t=h([]),s=h(!1),n=h(!0),a=h(""),i=h(""),l=h(!1),r=h(!1),o=h(Ke.state||"disconnected"),c=J(()=>{switch(o.value){case"connected":return"Live";case"connecting":return"Connecting…";case"reconnecting":return"Reconnecting…";default:return"Disconnected"}}),d=h(null),u=h(!1),f=h(null),p=2e3,b=Lk,y=Dk,A=ao,O=h("all"),x=h(""),m=h([]),_=h(!1),S=h(""),g=h([]);function w(){try{const z=localStorage.getItem("odin-log-presets");z&&(m.value=JSON.parse(z))}catch{}}function T(){try{localStorage.setItem("odin-log-presets",JSON.stringify(m.value))}catch{}}const C=J(()=>a.value!==""||i.value.trim()!==""||x.value!==""),M=J(()=>{const z=ao.find(oe=>oe.value===x.value);return z?z.label:""}),B=J(()=>{if(!l.value||!i.value)return null;try{return new RegExp(i.value,"i"),null}catch(z){return z.message}}),$=24,I=J(()=>{if(xe.value.length===0)return[];const z=[],oe=new Date,Ae=3600*1e3;for(let Je=$-1;Je>=0;Je--){const ct=new Date(oe.getTime()-(Je+1)*Ae),$t=new Date(oe.getTime()-Je*Ae);z.push({start:ct,end:$t,label:N(ct,$t),shortLabel:$t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}),total:0,info:0,warnings:0,errors:0})}for(const Je of xe.value){if(!Je._time)continue;const ct=Je._time.getTime();for(const $t of z)if(ct>=$t.start.getTime()&&ct<$t.end.getTime()){$t.total++,Je.level==="ERROR"?$t.errors++:Je.level==="WARNING"?$t.warnings++:$t.info++;break}}return z}),j=J(()=>{let z=1;for(const oe of I.value)oe.total>z&&(z=oe.total);return z}),Y=J(()=>{if(I.value.length===0)return"";const z=xe.value.map(Je=>Je._time&&Je._time.getTime()).filter(Boolean);if(z.length===0)return"";const oe=new Date(Math.min(...z));return`${xe.value.length} shown, oldest ${oe.toLocaleTimeString()}`}),H=J(()=>Math.ceil($/8));function N(z,oe){const Ae={hour:"2-digit",minute:"2-digit"};return z.toLocaleTimeString([],Ae)+" - "+oe.toLocaleTimeString([],Ae)}function L(z,oe){return!oe||!z?"0px":Math.max(2,z/oe*100)+"%"}function Z(z){const oe=xe.value.findIndex(Ae=>Ae._time&&Ae._time.getTime()>=z.start.getTime()&&Ae._time.getTime()=0&&d.value){const Ae=d.value.querySelectorAll(".log-line");Ae[oe]&&(Ae[oe].scrollIntoView({behavior:"smooth",block:"center"}),n.value=!1)}}const xe=J(()=>{let z=t.value;if(a.value&&(z=z.filter(oe=>(oe.level||"INFO")===a.value)),x.value){const oe=ao.find(Ae=>Ae.value===x.value);if(oe&&oe.seconds){const Ae=new Date(Date.now()-oe.seconds*1e3);z=z.filter(Je=>Je._time&&Je._time>=Ae)}}if(i.value&&!B.value)if(l.value)try{const oe=new RegExp(i.value,"i");z=z.filter(Ae=>{const Je=Ae.text||Ae.raw||"",ct=Ae.tool||"";return oe.test(Je)||oe.test(ct)})}catch{}else{const oe=i.value.toLowerCase();z=z.filter(Ae=>{const Je=(Ae.text||Ae.raw||"").toLowerCase(),ct=(Ae.tool||"").toLowerCase();return Je.includes(oe)||ct.includes(oe)})}return z});function _e(z){if(z.type==="log"&&z.line)try{const oe=typeof z.line=="string"?JSON.parse(z.line):z.line,Ae=oe.timestamp?new Date(oe.timestamp):new Date;return{ts:Ae.toLocaleTimeString(),_time:Ae,level:oe.error?"ERROR":"INFO",text:oe.tool_name?`[${oe.tool_name}] ${oe.result_summary||""}`.trim():oe.message||JSON.stringify(oe),tool:oe.tool_name||"",raw:null}}catch{return{ts:new Date().toLocaleTimeString(),_time:new Date,level:"INFO",text:String(z.line),tool:"",raw:String(z.line)}}if(z.payload){const oe=z.payload,Ae=oe.timestamp?new Date(oe.timestamp):new Date;return{ts:Ae.toLocaleTimeString(),_time:Ae,level:oe.error?"ERROR":"INFO",text:oe.tool_name?`[${oe.tool_name}] ${oe.result_summary||""}`.trim():oe.message||JSON.stringify(oe),tool:oe.tool_name||"",raw:null}}return typeof z=="string"?{ts:new Date().toLocaleTimeString(),_time:new Date,level:"INFO",text:z,tool:"",raw:z}:{ts:new Date().toLocaleTimeString(),_time:new Date,level:"INFO",text:JSON.stringify(z),tool:"",raw:null}}function ae(z){const oe=_e(z);if(s.value){g.value.push(oe);return}fe(oe)}function fe(z){t.value.push(z),t.value.length>p&&(t.value=t.value.slice(-p)),n.value&&At(()=>P())}function P(z=!1){const oe=d.value;oe&&oe.scrollTo({top:oe.scrollHeight,behavior:z?"smooth":"instant"})}function se(){n.value=!0,u.value=!1,At(()=>P(!0))}const ke=new Set(["PageUp","PageDown","ArrowUp","ArrowDown","Home","End"," "]);function V(){const z=d.value;if(!z)return;const oe=z.scrollHeight-z.scrollTop-z.clientHeight<40;u.value=!n.value&&!oe&&t.value.length>0,me.value&&ce()}function ce(){const z=d.value;!z||!n.value||z.scrollHeight-z.scrollTop-z.clientHeight>=40&&(n.value=!1,u.value=t.value.length>0)}function de(){n.value&&requestAnimationFrame(ce)}function ve(z){ke.has(z.key)&&de()}const me=h(!1);function He(){n.value&&(me.value=!0,requestAnimationFrame(ce))}function k(){me.value&&(me.value=!1,ce())}function E(){n.value&&(u.value=!1,At(()=>P()))}function U(){if(s.value=!s.value,!s.value&&g.value.length>0){for(const z of g.value)fe(z);g.value=[]}}function X(){t.value=[],g.value=[],u.value=!1}function q(){let z;e.value==="search"?z=ze.value.map(ct=>{const $t=ct.error?"ERROR":"INFO",Wt=ct.tool_name?`[${ct.tool_name}] `:"";return`${ct.timestamp||""} ${$t} ${Wt}${ct.result_summary||ct.message||""}`}).join(` -`):z=xe.value.map(ct=>`${ct.ts} ${ct.level} ${ct.text}`).join(` -`);const oe=new Blob([z],{type:"text/plain"}),Ae=URL.createObjectURL(oe),Je=document.createElement("a");Je.href=Ae,Je.download=`odin-logs-${new Date().toISOString().slice(0,19).replace(/:/g,"-")}.txt`,Je.click(),URL.revokeObjectURL(Ae)}function Q(z,oe){const Ae=`${z.ts} ${z.level} ${z.text||z.raw||""}`;navigator.clipboard.writeText(Ae).then(()=>{f.value=oe,setTimeout(()=>{f.value=null},1500)}).catch(()=>{})}function ie(z){a.value=a.value===z?"":z,O.value="all"}function re(z){return z.level==="ERROR"?"log-line-error":z.level==="WARNING"?"log-line-warning":"text-gray-300"}function le(z){return z==="ERROR"?"text-red-500 font-semibold":z==="WARNING"?"text-yellow-500":"text-blue-500"}function te(z){return z==="ERROR"?"log-chip-error":z==="WARNING"?"log-chip-warning":"log-chip-info"}function be(z){O.value=z.id;const oe=z.filters;a.value=oe.level||"",x.value=oe.timeRange||"",i.value=oe.text||"",oe.levels&&(a.value=oe.levels[0]||""),oe.hasToolName&&(i.value="")}function ue(z){O.value=z.id,a.value=z.filters.level||"",x.value=z.filters.timeRange||"",i.value=z.filters.text||""}function he(){if(!S.value.trim())return;const z={id:"custom-"+Date.now(),name:S.value.trim(),filters:{level:a.value,timeRange:x.value,text:i.value}};m.value=[...m.value,z],T(),_.value=!1,S.value=""}function we(z){m.value=m.value.filter(oe=>oe.id!==z),T(),O.value===z&&(O.value="all")}const Ee=h("all"),Le=h(""),Oe=h(""),Fe=h(""),Ve=h(""),lt=h(""),G=h(100),ye=Mk,Ce=h(!1),Re=h(!1),Be=h(""),ze=h([]),pt=h(null),ns=h(null);function As(){e.value="search",pt.value||Qs()}async function Qs(){try{pt.value=await K.get("/api/logs/stats")}catch{}}function $s(){const z=lt.value;if(!z){Fe.value="",Ve.value="";return}const Ae={last_5m:300,last_15m:900,last_1h:3600,last_4h:14400,last_24h:86400,last_7d:604800}[z];if(Ae){const Je=new Date(Date.now()-Ae*1e3);Fe.value=Rs(Je),Ve.value=""}}function Rs(z){const oe=Ae=>String(Ae).padStart(2,"0");return`${z.getFullYear()}-${oe(z.getMonth()+1)}-${oe(z.getDate())}T${oe(z.getHours())}:${oe(z.getMinutes())}`}function Nt(z){if(!z)return"";const oe=new Date(z);return isNaN(oe.getTime())?"":oe.toISOString()}async function us(){Ce.value=!0,Be.value="",Re.value=!0,ns.value=null;try{const z=new URLSearchParams;Ee.value&&Ee.value!=="all"&&z.set("level",Ee.value),Le.value&&z.set("tool",Le.value),Oe.value&&z.set("q",Oe.value);const oe=Nt(Fe.value),Ae=Nt(Ve.value);oe&&z.set("start",oe),Ae&&z.set("end",Ae),z.set("limit",String(G.value));const Je=await K.get(`/api/logs/search?${z.toString()}`);ze.value=Je.entries||[]}catch(z){Be.value=z.message||"Search failed",ze.value=[]}finally{Ce.value=!1}}function Us(){Ee.value="all",Le.value="",Oe.value="",Fe.value="",Ve.value="",lt.value="",G.value=100,ze.value=[],Re.value=!1,Be.value="",ns.value=null}function Xs(z){ns.value=ns.value===z?null:z}function Hn(z){if(!z.timestamp)return"";try{return new Date(z.timestamp).toLocaleString()}catch{return z.timestamp}}function en(z){return z.type==="web_action"?`${z.status||""} (${z.execution_time_ms||0}ms)`:(z.result_summary||"").slice(0,200)}function Kt(z){return z.error?"log-line-error":"text-gray-300"}function ee(z){try{return JSON.stringify(z,null,2)}catch{return String(z)}}let Se=null,De=null,Bs=!1;function rt(){Bs||(Bs=!0,Ke.subscribe("logs",ae),r.value=Ke.connected,o.value=Ke.state||"disconnected",Se=Ke.onStateChange,De=(z,oe)=>{o.value=z,r.value=z==="connected",Se&&Se(z,oe)},Ke.onStateChange=De)}function Is(){Bs&&(Bs=!1,Ke.unsubscribe("logs",ae),Ke.onStateChange===De&&(Ke.onStateChange=Se),De=null,Se=null)}return We(()=>{w(),window.addEventListener("pointerup",k),window.addEventListener("pointercancel",k)}),Cs(rt),Es(Is),xt(()=>{Is(),window.removeEventListener("pointerup",k),window.removeEventListener("pointercancel",k)}),{mode:e,logs:t,paused:s,autoScroll:n,levelFilter:a,textFilter:i,useRegex:l,subscribed:r,wsState:o,wsStateLabel:c,logContainer:d,filteredLogs:xe,pauseBuffer:g,showJumpBottom:u,copiedIndex:f,regexError:B,levels:b,logPresets:y,timeRanges:A,timeRange:x,activeLogPreset:O,customLogPresets:m,showSaveLogPreset:_,newLogPresetName:S,hasActiveLogFilters:C,timeRangeLabel:M,timelineBuckets:I,timelineMax:j,timelineSpanLabel:Y,timelineLabelSkip:H,togglePause:U,clearLogs:X,exportLogs:q,logLineClass:re,levelClass:le,levelChipClass:te,toggleLevel:ie,copyLine:Q,jumpToBottom:se,onScroll:V,onUserScrollIntent:de,onUserScrollKey:ve,onAutoScrollToggle:E,onPointerDown:He,applyLogPreset:be,applyCustomLogPreset:ue,saveLogCustomPreset:he,removeLogCustomPreset:we,segmentHeight:L,jumpToTimelineBucket:Z,searchLevel:Ee,searchTool:Le,searchKeyword:Oe,searchStart:Fe,searchEnd:Ve,searchTimePreset:lt,searchLimit:G,searchLimits:ye,searching:Ce,searchRan:Re,searchError:Be,searchResults:ze,searchStats:pt,expandedSearch:ns,switchToSearch:As,runSearch:us,clearSearchFilters:Us,toggleSearchExpand:Xs,formatSearchTs:Hn,searchEntryText:en,searchLogLineClass:Kt,formatJson:ee,applySearchTimePreset:$s}}};function xl(e=[]){const t=[],s=new Set;function n(a){const i=[a.kind,a.label,a.apply_mode||"",a.code||"",a.text||""].join("\0");s.has(i)||(s.add(i),t.push({...a,key:i}))}for(const a of e)for(const i of(a==null?void 0:a.consumers)||[])n({kind:"consumer",label:i.name,apply_mode:i.apply_mode,text:i.detail});for(const a of e)a!=null&&a.apply_handler&&n({kind:"handler",label:"Apply handler",code:a.apply_handler});for(const a of e)a!=null&&a.restart_reason&&n({kind:"restart",label:"Why a restart is required",text:a.restart_reason});for(const a of e)a!=null&&a.activation_policy&&n({kind:"activation",label:"Activation policy",text:a.activation_policy});return t}const Fk=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 $k(e,t={}){var a,i;const s=t.getStyle||(l=>globalThis.getComputedStyle(l)),n=Object.hasOwn(t,"fallback")?t.fallback:(a=globalThis.document)==null?void 0:a.scrollingElement;for(let l=e;l;l=l.parentElement){const r=((i=s(l))==null?void 0:i.overflowY)||"";if(/^(auto|scroll|overlay)$/.test(r)&&l.scrollHeight>l.clientHeight)return l}return n&&n.scrollHeight>n.clientHeight?n:e||n||null}const Pa=[{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","comfyui","slack","mcp"]},{key:"automation",label:"Automation",icon:"workflow",sections:["message_triggers","reaction_triggers","grafana_alerts","outbound_webhooks","issue_tracker"]},{key:"infrastructure",label:"Infrastructure",icon:"server",sections:["tools","web"]}],Uk={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"},io=new Set(["llm_provider","openai_codex","ollama","kimi","personality","discord"]),Bk=Object.freeze(["web.api_tokens","outbound_webhooks.targets"]);function Mu(e){return Bk.some(t=>e===t||e.startsWith(`${t}.`))}const Sm="odin_config_center_expanded_v1",Tm="odin_config_center_category_v1",Hk=50,Vk=650,lo=()=>K.get("/api/config/meta");function Kn(e){return e===void 0?void 0:JSON.parse(JSON.stringify(e))}function wi(e,t){return JSON.stringify(e)===JSON.stringify(t)}function ba(e){return String(e).replace(/[_-]+/g," ").replace(/\b\w/g,t=>t.toUpperCase())}function jk(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 zk(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 Cm(e,t){if(wi(e,t))return;if(!(e&&t&&typeof e=="object"&&typeof t=="object"&&!Array.isArray(e)&&!Array.isArray(t)))return Kn(t);const n={};for(const[a,i]of Object.entries(t)){const l=Cm(e[a],i);l!==void 0&&(n[a]=l)}return Object.keys(n).length?n:void 0}function qk(e,t){const s={};for(const[n,a]of Object.entries(t||{})){const i=Cm(e==null?void 0:e[n],a);i!==void 0&&(s[n]=i)}return s}function Em(e,t,s,n){if(wi(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)Em(e[l],t[l],s?`${s}.${l}`:l,n);return}n.push({path:s,oldVal:e,newVal:t})}function Gk(){try{const e=JSON.parse(localStorage.getItem(Sm)||"{}");return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}function Kk(){try{const e=localStorage.getItem(Tm);return Pa.some(t=>t.key===e)?e:Pa[0].key}catch{return Pa[0].key}}const Wk={template:` + `,setup(){const e=h("live"),t=h([]),s=h(!1),n=h(!0),a=h(""),i=h(""),l=h(!1),r=h(!1),o=h(Ke.state||"disconnected"),c=J(()=>{switch(o.value){case"connected":return"Live";case"connecting":return"Connecting…";case"reconnecting":return"Reconnecting…";default:return"Disconnected"}}),d=h(null),u=h(!1),f=h(null),p=2e3,b=Lk,y=Dk,E=ao,I=h("all"),x=h(""),m=h([]),_=h(!1),S=h(""),g=h([]);function w(){try{const q=localStorage.getItem("odin-log-presets");q&&(m.value=JSON.parse(q))}catch{}}function T(){try{localStorage.setItem("odin-log-presets",JSON.stringify(m.value))}catch{}}const C=J(()=>a.value!==""||i.value.trim()!==""||x.value!==""),M=J(()=>{const q=ao.find(re=>re.value===x.value);return q?q.label:""}),H=J(()=>{if(!l.value||!i.value)return null;try{return new RegExp(i.value,"i"),null}catch(q){return q.message}}),P=24,R=J(()=>{if(we.value.length===0)return[];const q=[],re=new Date,Ee=3600*1e3;for(let Ze=P-1;Ze>=0;Ze--){const lt=new Date(re.getTime()-(Ze+1)*Ee),Ot=new Date(re.getTime()-Ze*Ee);q.push({start:lt,end:Ot,label:O(lt,Ot),shortLabel:Ot.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}),total:0,info:0,warnings:0,errors:0})}for(const Ze of we.value){if(!Ze._time)continue;const lt=Ze._time.getTime();for(const Ot of q)if(lt>=Ot.start.getTime()&<{let q=1;for(const re of R.value)re.total>q&&(q=re.total);return q}),Q=J(()=>{if(R.value.length===0)return"";const q=we.value.map(Ze=>Ze._time&&Ze._time.getTime()).filter(Boolean);if(q.length===0)return"";const re=new Date(Math.min(...q));return`${we.value.length} shown, oldest ${re.toLocaleTimeString()}`}),U=J(()=>Math.ceil(P/8));function O(q,re){const Ee={hour:"2-digit",minute:"2-digit"};return q.toLocaleTimeString([],Ee)+" - "+re.toLocaleTimeString([],Ee)}function N(q,re){return!re||!q?"0px":Math.max(2,q/re*100)+"%"}function Y(q){const re=we.value.findIndex(Ee=>Ee._time&&Ee._time.getTime()>=q.start.getTime()&&Ee._time.getTime()=0&&d.value){const Ee=d.value.querySelectorAll(".log-line");Ee[re]&&(Ee[re].scrollIntoView({behavior:"smooth",block:"center"}),n.value=!1)}}const we=J(()=>{let q=t.value;if(a.value&&(q=q.filter(re=>(re.level||"INFO")===a.value)),x.value){const re=ao.find(Ee=>Ee.value===x.value);if(re&&re.seconds){const Ee=new Date(Date.now()-re.seconds*1e3);q=q.filter(Ze=>Ze._time&&Ze._time>=Ee)}}if(i.value&&!H.value)if(l.value)try{const re=new RegExp(i.value,"i");q=q.filter(Ee=>{const Ze=Ee.text||Ee.raw||"",lt=Ee.tool||"";return re.test(Ze)||re.test(lt)})}catch{}else{const re=i.value.toLowerCase();q=q.filter(Ee=>{const Ze=(Ee.text||Ee.raw||"").toLowerCase(),lt=(Ee.tool||"").toLowerCase();return Ze.includes(re)||lt.includes(re)})}return q});function ke(q){if(q.type==="log"&&q.line)try{const re=typeof q.line=="string"?JSON.parse(q.line):q.line,Ee=re.timestamp?new Date(re.timestamp):new Date;return{ts:Ee.toLocaleTimeString(),_time:Ee,level:re.error?"ERROR":"INFO",text:re.tool_name?`[${re.tool_name}] ${re.result_summary||""}`.trim():re.message||JSON.stringify(re),tool:re.tool_name||"",raw:null}}catch{return{ts:new Date().toLocaleTimeString(),_time:new Date,level:"INFO",text:String(q.line),tool:"",raw:String(q.line)}}if(q.payload){const re=q.payload,Ee=re.timestamp?new Date(re.timestamp):new Date;return{ts:Ee.toLocaleTimeString(),_time:Ee,level:re.error?"ERROR":"INFO",text:re.tool_name?`[${re.tool_name}] ${re.result_summary||""}`.trim():re.message||JSON.stringify(re),tool:re.tool_name||"",raw:null}}return typeof q=="string"?{ts:new Date().toLocaleTimeString(),_time:new Date,level:"INFO",text:q,tool:"",raw:q}:{ts:new Date().toLocaleTimeString(),_time:new Date,level:"INFO",text:JSON.stringify(q),tool:"",raw:null}}function ie(q){const re=ke(q);if(s.value){g.value.push(re);return}he(re)}function he(q){t.value.push(q),t.value.length>p&&(t.value=t.value.slice(-p)),n.value&&Rt(()=>F())}function F(q=!1){const re=d.value;re&&re.scrollTo({top:re.scrollHeight,behavior:q?"smooth":"instant"})}function se(){n.value=!0,u.value=!1,Rt(()=>F(!0))}const Se=new Set(["PageUp","PageDown","ArrowUp","ArrowDown","Home","End"," "]);function V(){const q=d.value;if(!q)return;const re=q.scrollHeight-q.scrollTop-q.clientHeight<40;u.value=!n.value&&!re&&t.value.length>0,ge.value&&de()}function de(){const q=d.value;!q||!n.value||q.scrollHeight-q.scrollTop-q.clientHeight>=40&&(n.value=!1,u.value=t.value.length>0)}function ce(){n.value&&requestAnimationFrame(de)}function ye(q){Se.has(q.key)&&ce()}const ge=h(!1);function He(){n.value&&(ge.value=!0,requestAnimationFrame(de))}function k(){ge.value&&(ge.value=!1,de())}function L(){n.value&&(u.value=!1,Rt(()=>F()))}function $(){if(s.value=!s.value,!s.value&&g.value.length>0){for(const q of g.value)he(q);g.value=[]}}function ee(){t.value=[],g.value=[],u.value=!1}function Z(){let q;e.value==="search"?q=Pe.value.map(lt=>{const Ot=lt.error?"ERROR":"INFO",Pt=lt.tool_name?`[${lt.tool_name}] `:"";return`${lt.timestamp||""} ${Ot} ${Pt}${lt.result_summary||lt.message||""}`}).join(` +`):q=we.value.map(lt=>`${lt.ts} ${lt.level} ${lt.text}`).join(` +`);const re=new Blob([q],{type:"text/plain"}),Ee=URL.createObjectURL(re),Ze=document.createElement("a");Ze.href=Ee,Ze.download=`odin-logs-${new Date().toISOString().slice(0,19).replace(/:/g,"-")}.txt`,Ze.click(),URL.revokeObjectURL(Ee)}function X(q,re){const Ee=`${q.ts} ${q.level} ${q.text||q.raw||""}`;navigator.clipboard.writeText(Ee).then(()=>{f.value=re,setTimeout(()=>{f.value=null},1500)}).catch(()=>{})}function ue(q){a.value=a.value===q?"":q,I.value="all"}function oe(q){return q.level==="ERROR"?"log-line-error":q.level==="WARNING"?"log-line-warning":"text-gray-300"}function le(q){return q==="ERROR"?"text-red-500 font-semibold":q==="WARNING"?"text-yellow-500":"text-blue-500"}function te(q){return q==="ERROR"?"log-chip-error":q==="WARNING"?"log-chip-warning":"log-chip-info"}function ne(q){I.value=q.id;const re=q.filters;a.value=re.level||"",x.value=re.timeRange||"",i.value=re.text||"",re.levels&&(a.value=re.levels[0]||""),re.hasToolName&&(i.value="")}function fe(q){I.value=q.id,a.value=q.filters.level||"",x.value=q.filters.timeRange||"",i.value=q.filters.text||""}function ve(){if(!S.value.trim())return;const q={id:"custom-"+Date.now(),name:S.value.trim(),filters:{level:a.value,timeRange:x.value,text:i.value}};m.value=[...m.value,q],T(),_.value=!1,S.value=""}function Te(q){m.value=m.value.filter(re=>re.id!==q),T(),I.value===q&&(I.value="all")}const Oe=h("all"),Le=h(""),De=h(""),Be=h(""),qe=h(""),ct=h(""),K=h(100),xe=Mk,Ce=h(!1),Re=h(!1),Ve=h(""),Pe=h([]),pt=h(null),ls=h(null);function Ps(){e.value="search",pt.value||nn()}async function nn(){try{pt.value=await G.get("/api/logs/stats")}catch{}}function Ss(){const q=ct.value;if(!q){Be.value="",qe.value="";return}const Ee={last_5m:300,last_15m:900,last_1h:3600,last_4h:14400,last_24h:86400,last_7d:604800}[q];if(Ee){const Ze=new Date(Date.now()-Ee*1e3);Be.value=Fs(Ze),qe.value=""}}function Fs(q){const re=Ee=>String(Ee).padStart(2,"0");return`${q.getFullYear()}-${re(q.getMonth()+1)}-${re(q.getDate())}T${re(q.getHours())}:${re(q.getMinutes())}`}function Mt(q){if(!q)return"";const re=new Date(q);return isNaN(re.getTime())?"":re.toISOString()}async function Yt(){Ce.value=!0,Ve.value="",Re.value=!0,ls.value=null;try{const q=new URLSearchParams;Oe.value&&Oe.value!=="all"&&q.set("level",Oe.value),Le.value&&q.set("tool",Le.value),De.value&&q.set("q",De.value);const re=Mt(Be.value),Ee=Mt(qe.value);re&&q.set("start",re),Ee&&q.set("end",Ee),q.set("limit",String(K.value));const Ze=await G.get(`/api/logs/search?${q.toString()}`);Pe.value=Ze.entries||[]}catch(q){Ve.value=q.message||"Search failed",Pe.value=[]}finally{Ce.value=!1}}function $s(){Oe.value="all",Le.value="",De.value="",Be.value="",qe.value="",ct.value="",K.value=100,Pe.value=[],Re.value=!1,Ve.value="",ls.value=null}function Bs(q){ls.value=ls.value===q?null:q}function An(q){if(!q.timestamp)return"";try{return new Date(q.timestamp).toLocaleString()}catch{return q.timestamp}}function Us(q){return q.type==="web_action"?`${q.status||""} (${q.execution_time_ms||0}ms)`:(q.result_summary||"").slice(0,200)}function zt(q){return q.error?"log-line-error":"text-gray-300"}function Vn(q){try{return JSON.stringify(q,null,2)}catch{return String(q)}}let Ct=null,rs=null,os=!1;function Ye(){os||(os=!0,Ke.subscribe("logs",ie),r.value=Ke.connected,o.value=Ke.state||"disconnected",Ct=Ke.onStateChange,rs=(q,re)=>{o.value=q,r.value=q==="connected",Ct&&Ct(q,re)},Ke.onStateChange=rs)}function gs(){os&&(os=!1,Ke.unsubscribe("logs",ie),Ke.onStateChange===rs&&(Ke.onStateChange=Ct),rs=null,Ct=null)}return We(()=>{w(),window.addEventListener("pointerup",k),window.addEventListener("pointercancel",k)}),Ds(Ye),Ms(gs),xt(()=>{gs(),window.removeEventListener("pointerup",k),window.removeEventListener("pointercancel",k)}),{mode:e,logs:t,paused:s,autoScroll:n,levelFilter:a,textFilter:i,useRegex:l,subscribed:r,wsState:o,wsStateLabel:c,logContainer:d,filteredLogs:we,pauseBuffer:g,showJumpBottom:u,copiedIndex:f,regexError:H,levels:b,logPresets:y,timeRanges:E,timeRange:x,activeLogPreset:I,customLogPresets:m,showSaveLogPreset:_,newLogPresetName:S,hasActiveLogFilters:C,timeRangeLabel:M,timelineBuckets:R,timelineMax:j,timelineSpanLabel:Q,timelineLabelSkip:U,togglePause:$,clearLogs:ee,exportLogs:Z,logLineClass:oe,levelClass:le,levelChipClass:te,toggleLevel:ue,copyLine:X,jumpToBottom:se,onScroll:V,onUserScrollIntent:ce,onUserScrollKey:ye,onAutoScrollToggle:L,onPointerDown:He,applyLogPreset:ne,applyCustomLogPreset:fe,saveLogCustomPreset:ve,removeLogCustomPreset:Te,segmentHeight:N,jumpToTimelineBucket:Y,searchLevel:Oe,searchTool:Le,searchKeyword:De,searchStart:Be,searchEnd:qe,searchTimePreset:ct,searchLimit:K,searchLimits:xe,searching:Ce,searchRan:Re,searchError:Ve,searchResults:Pe,searchStats:pt,expandedSearch:ls,switchToSearch:Ps,runSearch:Yt,clearSearchFilters:$s,toggleSearchExpand:Bs,formatSearchTs:An,searchEntryText:Us,searchLogLineClass:zt,formatJson:Vn,applySearchTimePreset:Ss}}};function _l(e=[]){const t=[],s=new Set;function n(a){const i=[a.kind,a.label,a.apply_mode||"",a.code||"",a.text||""].join("\0");s.has(i)||(s.add(i),t.push({...a,key:i}))}for(const a of e)for(const i of(a==null?void 0:a.consumers)||[])n({kind:"consumer",label:i.name,apply_mode:i.apply_mode,text:i.detail});for(const a of e)a!=null&&a.apply_handler&&n({kind:"handler",label:"Apply handler",code:a.apply_handler});for(const a of e)a!=null&&a.restart_reason&&n({kind:"restart",label:"Why a restart is required",text:a.restart_reason});for(const a of e)a!=null&&a.activation_policy&&n({kind:"activation",label:"Activation policy",text:a.activation_policy});return t}const Fk=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 $k(e,t={}){var a,i;const s=t.getStyle||(l=>globalThis.getComputedStyle(l)),n=Object.hasOwn(t,"fallback")?t.fallback:(a=globalThis.document)==null?void 0:a.scrollingElement;for(let l=e;l;l=l.parentElement){const r=((i=s(l))==null?void 0:i.overflowY)||"";if(/^(auto|scroll|overlay)$/.test(r)&&l.scrollHeight>l.clientHeight)return l}return n&&n.scrollHeight>n.clientHeight?n:e||n||null}const Ha=[{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","comfyui","slack","mcp"]},{key:"automation",label:"Automation",icon:"workflow",sections:["message_triggers","reaction_triggers","grafana_alerts","outbound_webhooks","issue_tracker"]},{key:"infrastructure",label:"Infrastructure",icon:"server",sections:["tools","web"]}],Bk={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"},io=new Set(["llm_provider","openai_codex","ollama","kimi","personality","discord"]),Uk=Object.freeze(["web.api_tokens","outbound_webhooks.targets"]);function Mu(e){return Uk.some(t=>e===t||e.startsWith(`${t}.`))}const Sm="odin_config_center_expanded_v1",Tm="odin_config_center_category_v1",Hk=50,zk=650,lo=()=>G.get("/api/config/meta");function Zn(e){return e===void 0?void 0:JSON.parse(JSON.stringify(e))}function Ri(e,t){return JSON.stringify(e)===JSON.stringify(t)}function wa(e){return String(e).replace(/[_-]+/g," ").replace(/\b\w/g,t=>t.toUpperCase())}function Vk(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 jk(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 Cm(e,t){if(Ri(e,t))return;if(!(e&&t&&typeof e=="object"&&typeof t=="object"&&!Array.isArray(e)&&!Array.isArray(t)))return Zn(t);const n={};for(const[a,i]of Object.entries(t)){const l=Cm(e[a],i);l!==void 0&&(n[a]=l)}return Object.keys(n).length?n:void 0}function qk(e,t){const s={};for(const[n,a]of Object.entries(t||{})){const i=Cm(e==null?void 0:e[n],a);i!==void 0&&(s[n]=i)}return s}function Em(e,t,s,n){if(Ri(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)Em(e[l],t[l],s?`${s}.${l}`:l,n);return}n.push({path:s,oldVal:e,newVal:t})}function Gk(){try{const e=JSON.parse(localStorage.getItem(Sm)||"{}");return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}function Kk(){try{const e=localStorage.getItem(Tm);return Ha.some(t=>t.key===e)?e:Ha[0].key}catch{return Ha[0].key}}const Wk={template:`
@@ -4157,7 +4157,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h(null),t=h(null),s=h(!0),n=h(null),a=h(!1),i=h(null),l=h(null),r=h(null),o=h(!1),c=h(!1),d=h(null),u=h(""),f=h("all"),p=h(Kk()),b=h(Gk()),y=h({}),A=h({}),O=h(""),x=h({}),m=h({}),_=h([]),S=h([]),g=h(!1),w=h(!1),T=h(!1);let C=null,M=null,B={path:null,at:0},$=0;const I=J(()=>{var v;return(((v=t.value)==null?void 0:v.fields)||[]).filter(D=>!io.has(D.path.split(".")[0])&&!Mu(D.path))}),j=J(()=>new Map(I.value.map(v=>[v.path,v]))),Y=J(()=>xe.value.reduce((v,D)=>v+D.sections.length,0)),H=J(()=>I.value.length),N=J(()=>Fk),L=J(()=>_.value.length>0),Z=J(()=>S.value.length>0),xe=J(()=>{if(!e.value)return[];const v=new Set(Pa.flatMap(ne=>ne.sections)),D=Pa.map(ne=>({...ne,sections:ne.sections.filter(Ne=>Object.hasOwn(e.value,Ne)&&!io.has(Ne))})).filter(ne=>ne.sections.length),F=Object.keys(e.value).filter(ne=>!v.has(ne)&&!io.has(ne));return F.length&&D.push({key:"other",label:"Other",icon:"folder",sections:F}),D}),_e=J(()=>e.value?{...e.value,...y.value}:null),ae=J(()=>{if(!e.value)return[];const v=[];for(const[D,F]of Object.entries(y.value))Em(e.value[D],F,D,v);return v.filter(D=>!wi(D.oldVal,D.newVal)).map(D=>{const F=E(D.path);return{...D,label:(F==null?void 0:F.label)||ba(D.path.split(".").at(-1)),apply_mode:(F==null?void 0:F.apply_mode)||ie(D.path.split(".")[0])}})}),fe=J(()=>ae.value.length>0),P=J(()=>ae.value.length),se=J(()=>new Set(ae.value.map(v=>v.path.split(".")[0])).size),ke=J(()=>!!u.value||f.value!=="all"),V=J(()=>{const v={...m.value};for(const D of ae.value){const F=E(D.path),ne=$t(F,D.newVal);ne&&(v[D.path]=ne)}return v}),ce=J(()=>Object.keys(V.value).length>0),de=J(()=>e.value?(ke.value?xe.value:xe.value.filter(D=>D.key===p.value)).map(D=>({...D,sections:D.sections.filter(F=>G(F))})).filter(D=>D.sections.length):[]),ve=J(()=>{const v=["live_read","live_apply","live_for_new_work","restart","activation_required","legacy_control","dormant"],D=new Map(v.map(F=>[F,[]]));for(const F of ae.value){const ne=D.has(F.apply_mode)?F.apply_mode:"restart";D.get(ne).push(F)}return v.filter(F=>D.get(F).length).map(F=>({key:F,label:Cn(F),entries:D.get(F)}))}),me=J(()=>ae.value.filter(v=>v.apply_mode==="restart").length),He=J(()=>I.value.filter(v=>v.pending_restart)),k=J(()=>He.value.length);function E(v){const D=j.value.get(v);return D?{...D,apply_details:xl([D])}:null}function U(v){const D=`${v}.`;return I.value.filter(F=>F.path===v||F.path.startsWith(D))}function X(v){return U(v).length}function q(v){return ba(v)}function Q(v){const D=U(v);if(!D.length)return`${ba(v)} configuration.`;const F=D.find(Ue=>Ue.sensitivity==="public"&&Ue.description)||D.find(Ue=>Ue.description),ne=(F==null?void 0:F.description)||"";return ne.match(/setting for (.+)\.$/i)?`${ba(v)} settings and runtime behaviour.`:ne}function ie(v){const D=[...new Set(U(v).map(F=>F.apply_mode))];return D.length===1?D[0]:D.includes("restart")?"restart":D.includes("activation_required")?"activation_required":D[0]||"restart"}function re(v){const D=[...new Set(U(v).map(F=>Cn(F.apply_mode)))];return D.length?D.length===1?D[0]:`Mixed apply behaviour: ${D.join(" · ")}`:""}function le(v){return xl(U(v))}function te(v,D){return D.split(".").reduce((F,ne)=>F==null?void 0:F[ne],v)}function be(v){const D=_e.value;return U(v).filter(F=>Mu(F.path)?!1:F.path.split(".").length<=2?!0:!F.path.includes(".*")).map(F=>({...F,key:F.path.split(".").at(-1),value:te(D,F.path),apply_details:xl([F]),editor:F.path==="agents.final_warning_iterations"?"warning-chips":null}))}function ue(v){const D=v.path.split(".");return D.length>2?D.slice(0,2).join("."):null}function he(v){const D=new Map;for(const F of be(v)){const ne=ue(F),Ne=ne||`${v}.__root`;D.has(Ne)||D.set(Ne,{key:Ne,path:ne,entries:[]}),D.get(Ne).entries.push(F)}return[...D.values()].map(F=>{const ne=F.entries.find(Ne=>Ne.group_description);return{...F,label:F.path?ba(F.path.split(".").at(-1)):null,description:(ne==null?void 0:ne.group_description)||null,apply_details:xl(F.entries),runtime_summaries:Ee(F.entries)}})}function we(v){return{save:v.save_effect||(v.apply_mode==="dormant"?"Saving records this value in config.yml.":"Saving records this value and validates the section."),runtime:v.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."}[v.apply_mode]||"Effective runtime state is not currently observable."}}function Ee(v){const D=new Map;for(const F of v){const ne=we(F),Ne=`${F.apply_mode}|${ne.save}|${ne.runtime}`;D.has(Ne)||D.set(Ne,{key:Ne,label:Cn(F.apply_mode),save:ne.save,runtime:ne.runtime})}return[...D.values()]}function Le(v){if(Oe(v))return v.runtime_effect||v.activation_policy||"";if(v.apply_mode==="activation_required"){const D=v.activation_policy||v.runtime_effect;return D?`Not active after saving. No activation control exists in this release. ${D}`:"Not active after saving; no activation control exists in this release."}return""}function Oe(v){return v.action_available===!0&&!!(v.action_label&&v.action_endpoint)}async function Fe(v){if(Oe(v))try{if(ze(v.path))throw new Error("Save this setting before applying its action.");const D=String(v.action_method||"POST").toLowerCase(),F={post:K.post.bind(K),put:K.put.bind(K),delete:K.del.bind(K)}[D];if(!F)throw new Error("Unsupported configuration action");await F(v.action_endpoint,v.action_body||void 0),await pe(),tn("success",`${v.action_label} completed.`)}catch(D){tn("error",D.message||`${v.action_label} failed`)}}function Ve(v,D){return[v.label,v.path,v.description,...v.aliases||[]].filter(Boolean).join(" ").toLowerCase().includes(D)}function lt(v){const D=u.value.trim().toLowerCase();return D?U(v).filter(F=>Ve(F,D)):[]}function G(v){const D=U(v);if(f.value!=="all"&&!D.some(ne=>ne.apply_state===f.value))return!1;const F=u.value.trim().toLowerCase();return!F||`${q(v)} ${v}`.toLowerCase().includes(F)?!0:D.some(ne=>Ve(ne,F))}function ye(v,D){return U(v).filter(F=>F.apply_state===D).length}function Ce(v){return v==="all"?H.value:I.value.filter(D=>D.apply_state===v).length}function Re(v){const D=v.sections.flatMap(F=>U(F));return{fields:D.length,modified:ae.value.filter(F=>v.sections.includes(F.path.split(".")[0])).length,pending_restart:D.filter(F=>F.apply_state==="pending_restart").length,invalid:D.filter(F=>F.apply_state==="invalid").length,dormant:D.filter(F=>F.apply_state==="dormant").length}}function Be(v){var D;return Object.hasOwn(y.value,v)&&!wi((D=e.value)==null?void 0:D[v],y.value[v])}function ze(v){return ae.value.some(D=>D.path===v||D.path.startsWith(`${v}.`))}function pt(v){p.value=v,u.value="",f.value="all";try{localStorage.setItem(Tm,v)}catch{}}function ns(v){f.value=v}function As(){u.value="",f.value="all"}function Qs(v){var D;return((D=xe.value.find(F=>F.sections.includes(v)))==null?void 0:D.sections)||[]}function $s(v){const D=Qs(v),F=D.find(ne=>b.value[ne]===!0);return F||D.find(ne=>b.value[ne]!==!1)||null}function Rs(v){return u.value&&!T.value&&G(v)?!0:T.value?$s(v)===v:Object.hasOwn(b.value,v)?b.value[v]===!0:!0}function Nt(v){const D=!Rs(v);if(T.value){const F={...b.value};for(const ne of Qs(v))F[ne]===!0&&(F[ne]=!1);F[v]=D,b.value=F;return}b.value={...b.value,[v]:D}}function us(){_.value.push(Kn(y.value)),_.value.length>Hk&&_.value.shift(),S.value=[]}function Us(){fe.value&&(us(),y.value={},m.value={},g.value=!1)}function Xs(v,D=!1){const F=Date.now();if(D&&B.path===v&&F-B.atNe-ne);O.value="",Kt(v,F)}function z(v,D){Kt(v,(v.value||[]).filter(F=>F!==D))}function oe(v){return v.apply_mode==="live_read"?"Odin reads the saved file value on next use.":v.apply_mode==="live_for_new_work"?"New work uses the saved file value.":v.apply_mode==="live_apply"?v.apply_handler?`Apply the saved value through ${v.apply_handler}.`:"Apply it through its dedicated owner page or endpoint.":v.apply_mode==="restart"?"Restart Odin for the saved collection to take effect.":v.apply_mode==="activation_required"?"Saving does not enable it. No activation control exists in this release.":v.apply_mode==="dormant"?"This release does not use the saved collection.":"Follow the runtime details shown for this setting."}function Ae(v){return v.type==="array"&&Array.isArray(v.value)&&!v.structured_container&&!v.structured_container_child&&v.sensitivity==="public"&&v.value.every(D=>["string","number","boolean"].includes(typeof D))}function Je(v){const D=String(x.value[v.path]??"").trim();if(!D)return;const F=[...new Set([...v.value||[],D])];x.value={...x.value,[v.path]:""},Kt(v,F)}function ct(v,D){Kt(v,(v.value||[]).filter(F=>F!==D))}function $t(v,D){var ne;if(!v)return null;if((ne=v.enum)!=null&&ne.length&&!v.enum.includes(D))return`Choose one of: ${v.enum.join(", ")}`;if(v.path==="agents.final_warning_iterations"&&(!Array.isArray(D)||!D.length))return"Add at least one warning threshold.";const F=v.constraints||{};if((v.type==="integer"||v.type==="number")&&typeof D=="number"){if(F.minimum!==void 0&&DF.maximum)return`Must be at most ${F.maximum}${v.unit?` ${v.unit}`:""}`}return null}function Wt(v){return V.value[v.path]||null}function tl(v){const D=`${v}.`;return Object.keys(V.value).some(F=>F===v||F.startsWith(D))}function Hs(){_.value.length&&(S.value.push(Kn(y.value)),y.value=_.value.pop(),m.value={},A.value={},B={path:null,at:0})}function sl(){S.value.length&&(_.value.push(Kn(y.value)),y.value=S.value.pop(),m.value={},A.value={},B={path:null,at:0})}function $r(){!fe.value||ce.value||(g.value=!0,w.value=!1)}function nl(){g.value=!1}function al(){Us()}function Cn(v){return Uk[v]||ba(v||"unknown")}function pa(v){return`apply-${String(v||"unknown").replaceAll("_","-")}`}function Os(v){return`cfgc-field-${v.replace(/[^a-zA-Z0-9_-]/g,"-")}`}function Vn(v){return`${Os(v)}-input`}function Ns(v){const D=document.getElementById(Os(v))||document.getElementById(Os(v.split(".").slice(0,2).join(".")));D==null||D.scrollIntoView({behavior:"smooth",block:"center"})}function tn(v,D){l.value={type:v,message:D},window.setTimeout(()=>{var F;((F=l.value)==null?void 0:F.message)===D&&(l.value=null)},3500)}function il(){o.value=!1,f.value="pending_restart",u.value="";const v=$k(n.value);v&&(v.scrollTop=0)}function Ur(){o.value=!1}function Qa(v=1800){M&&window.clearTimeout(M),M=window.setTimeout(ll,v)}async function ll(){if(c.value){if($+=1,$>45){c.value=!1,d.value="Odin did not return with the new startup settings within 90 seconds.";return}try{if(t.value=await lo(),k.value===0){c.value=!1,d.value=null,tn("success","Odin restarted and the saved startup settings are active.");return}}catch{}Qa(2e3)}}async function ha(){if(!c.value){d.value=null;try{await K.post("/api/restart",{}),c.value=!0,$=0,o.value=!1,Qa()}catch(v){d.value=v.message||"Odin could not schedule a restart."}}}async function Xa(){if(!(!fe.value||ce.value||a.value)){a.value=!0;try{const v=qk(e.value,y.value),D=await K.put("/api/config",v);e.value=D,y.value={},_.value=[],S.value=[],m.value={},g.value=!1;try{t.value=await lo(),r.value=null,o.value=k.value>0,tn("success",k.value?`Configuration saved. ${k.value} setting${k.value===1?"":"s"} still use startup values.`:"Configuration saved. Apply status has been refreshed.")}catch(F){r.value=F.message||"Unknown metadata error.",tn("error",`Configuration saved, but apply status could not be refreshed: ${r.value}`)}}catch(v){tn("error",v.message||"Configuration could not be saved")}finally{a.value=!1}}}async function pe(){var v,D;if(!fe.value){s.value=!0,i.value=null;try{const F=await K.get("/api/config"),ne=await lo();e.value=F,t.value=ne,r.value=null;const Ne=xe.value;if(Ne.some(Ue=>Ue.key===p.value)||(p.value=((v=Ne[0])==null?void 0:v.key)||Pa[0].key),T.value){const Xe=(((D=Ne.find(Ut=>Ut.key===p.value))==null?void 0:D.sections)||[]).find(Ut=>b.value[Ut]===!0);b.value=Xe?{...b.value,[Xe]:!0}:{}}}catch(F){i.value=F.message||"Unknown configuration error"}finally{s.value=!1}}}function R(v){if(g.value||!(v.ctrlKey||v.metaKey))return;const D=v.target;D instanceof HTMLElement&&(D.matches("input, textarea, select")||D.isContentEditable)||(!v.shiftKey&&v.key.toLowerCase()==="z"?(v.preventDefault(),Hs()):(v.key.toLowerCase()==="y"||v.shiftKey&&v.key.toLowerCase()==="z")&&(v.preventDefault(),sl()))}function W(v){T.value=v.matches}return es(b,v=>{try{localStorage.setItem(Sm,JSON.stringify(v))}catch{}},{deep:!0}),We(()=>{var v;pe(),document.addEventListener("keydown",R),C=window.matchMedia("(max-width: 760px)"),W(C),(v=C.addEventListener)==null||v.call(C,"change",W)}),xt(()=>{var v;document.removeEventListener("keydown",R),(v=C==null?void 0:C.removeEventListener)==null||v.call(C,"change",W),M&&window.clearTimeout(M)}),{config:e,meta:t,loading:s,saving:a,error:i,toast:l,metaRefreshError:r,restartPromptOpen:o,restartScheduled:c,restartError:d,configMain:n,searchQuery:u,healthFilter:f,activeCategory:p,reviewOpen:g,mobileOverflowOpen:w,warningThresholdInput:O,arrayInputs:x,healthFilters:N,visibleCategories:xe,displayGroups:de,reviewGroups:ve,sectionCount:Y,fieldCount:H,hasChanges:fe,changeCount:P,changedSectionCount:se,hasDraftErrors:ce,canUndo:L,canRedo:Z,globalFilterActive:ke,reviewRestartCount:me,pendingRestartCount:k,pendingRestartFields:He,healthCount:Ce,categoryStats:Re,selectCategory:pt,selectHealthFilter:ns,clearFilters:As,sectionLabel:q,sectionDescription:Q,sectionFieldCount:X,sectionHealthCount:ye,sectionApplySummary:re,sectionApplyDetails:le,sectionEntries:be,fieldGroups:he,sectionSearchHits:lt,fieldRuntimeCopy:we,fieldSpecificRuntimeNote:Le,hasHonestAction:Oe,runFieldAction:Fe,sectionChanged:Be,fieldChanged:ze,isSectionExpanded:Rs,toggleSection:Nt,discardAllDrafts:Us,setFieldValue:Kt,setNumberFieldValue:rt,numberInputValue:Bs,beginInputEdit:ee,endTextInputEdit:Se,endInputEdit:De,addWarningThreshold:Is,removeWarningThreshold:z,isScalarArray:Ae,addScalarArrayItem:Je,removeScalarArrayItem:ct,fieldError:Wt,sectionHasErrors:tl,undo:Hs,redo:sl,openReview:$r,closeReview:nl,mobileCancel:al,applyModeLabel:Cn,applyClass:pa,compactValue:jk,formatValue:zk,structuredApplyCopy:oe,fieldId:Os,fieldInputId:Vn,focusField:Ns,fetchConfig:pe,saveConfig:Xa,restartOdin:ha,restartLater:Ur,reviewPendingRestart:il}}},Zk=/^\d{15,25}$/;function Am(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 Rm={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}},emits:["select"],template:` + `,setup(){const e=h(null),t=h(null),s=h(!0),n=h(null),a=h(!1),i=h(null),l=h(null),r=h(null),o=h(!1),c=h(!1),d=h(null),u=h(""),f=h("all"),p=h(Kk()),b=h(Gk()),y=h({}),E=h({}),I=h(""),x=h({}),m=h({}),_=h([]),S=h([]),g=h(!1),w=h(!1),T=h(!1);let C=null,M=null,H={path:null,at:0},P=0;const R=J(()=>{var v;return(((v=t.value)==null?void 0:v.fields)||[]).filter(D=>!io.has(D.path.split(".")[0])&&!Mu(D.path))}),j=J(()=>new Map(R.value.map(v=>[v.path,v]))),Q=J(()=>we.value.reduce((v,D)=>v+D.sections.length,0)),U=J(()=>R.value.length),O=J(()=>Fk),N=J(()=>_.value.length>0),Y=J(()=>S.value.length>0),we=J(()=>{if(!e.value)return[];const v=new Set(Ha.flatMap(ae=>ae.sections)),D=Ha.map(ae=>({...ae,sections:ae.sections.filter(Ne=>Object.hasOwn(e.value,Ne)&&!io.has(Ne))})).filter(ae=>ae.sections.length),B=Object.keys(e.value).filter(ae=>!v.has(ae)&&!io.has(ae));return B.length&&D.push({key:"other",label:"Other",icon:"folder",sections:B}),D}),ke=J(()=>e.value?{...e.value,...y.value}:null),ie=J(()=>{if(!e.value)return[];const v=[];for(const[D,B]of Object.entries(y.value))Em(e.value[D],B,D,v);return v.filter(D=>!Ri(D.oldVal,D.newVal)).map(D=>{const B=L(D.path);return{...D,label:(B==null?void 0:B.label)||wa(D.path.split(".").at(-1)),apply_mode:(B==null?void 0:B.apply_mode)||ue(D.path.split(".")[0])}})}),he=J(()=>ie.value.length>0),F=J(()=>ie.value.length),se=J(()=>new Set(ie.value.map(v=>v.path.split(".")[0])).size),Se=J(()=>!!u.value||f.value!=="all"),V=J(()=>{const v={...m.value};for(const D of ie.value){const B=L(D.path),ae=Ot(B,D.newVal);ae&&(v[D.path]=ae)}return v}),de=J(()=>Object.keys(V.value).length>0),ce=J(()=>e.value?(Se.value?we.value:we.value.filter(D=>D.key===p.value)).map(D=>({...D,sections:D.sections.filter(B=>K(B))})).filter(D=>D.sections.length):[]),ye=J(()=>{const v=["live_read","live_apply","live_for_new_work","restart","activation_required","legacy_control","dormant"],D=new Map(v.map(B=>[B,[]]));for(const B of ie.value){const ae=D.has(B.apply_mode)?B.apply_mode:"restart";D.get(ae).push(B)}return v.filter(B=>D.get(B).length).map(B=>({key:B,label:Gs(B),entries:D.get(B)}))}),ge=J(()=>ie.value.filter(v=>v.apply_mode==="restart").length),He=J(()=>R.value.filter(v=>v.pending_restart)),k=J(()=>He.value.length);function L(v){const D=j.value.get(v);return D?{...D,apply_details:_l([D])}:null}function $(v){const D=`${v}.`;return R.value.filter(B=>B.path===v||B.path.startsWith(D))}function ee(v){return $(v).length}function Z(v){return wa(v)}function X(v){const D=$(v);if(!D.length)return`${wa(v)} configuration.`;const B=D.find(Ue=>Ue.sensitivity==="public"&&Ue.description)||D.find(Ue=>Ue.description),ae=(B==null?void 0:B.description)||"";return ae.match(/setting for (.+)\.$/i)?`${wa(v)} settings and runtime behaviour.`:ae}function ue(v){const D=[...new Set($(v).map(B=>B.apply_mode))];return D.length===1?D[0]:D.includes("restart")?"restart":D.includes("activation_required")?"activation_required":D[0]||"restart"}function oe(v){const D=[...new Set($(v).map(B=>Gs(B.apply_mode)))];return D.length?D.length===1?D[0]:`Mixed apply behaviour: ${D.join(" · ")}`:""}function le(v){return _l($(v))}function te(v,D){return D.split(".").reduce((B,ae)=>B==null?void 0:B[ae],v)}function ne(v){const D=ke.value;return $(v).filter(B=>Mu(B.path)?!1:B.path.split(".").length<=2?!0:!B.path.includes(".*")).map(B=>({...B,key:B.path.split(".").at(-1),value:te(D,B.path),apply_details:_l([B]),editor:B.path==="agents.final_warning_iterations"?"warning-chips":null}))}function fe(v){const D=v.path.split(".");return D.length>2?D.slice(0,2).join("."):null}function ve(v){const D=new Map;for(const B of ne(v)){const ae=fe(B),Ne=ae||`${v}.__root`;D.has(Ne)||D.set(Ne,{key:Ne,path:ae,entries:[]}),D.get(Ne).entries.push(B)}return[...D.values()].map(B=>{const ae=B.entries.find(Ne=>Ne.group_description);return{...B,label:B.path?wa(B.path.split(".").at(-1)):null,description:(ae==null?void 0:ae.group_description)||null,apply_details:_l(B.entries),runtime_summaries:Oe(B.entries)}})}function Te(v){return{save:v.save_effect||(v.apply_mode==="dormant"?"Saving records this value in config.yml.":"Saving records this value and validates the section."),runtime:v.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."}[v.apply_mode]||"Effective runtime state is not currently observable."}}function Oe(v){const D=new Map;for(const B of v){const ae=Te(B),Ne=`${B.apply_mode}|${ae.save}|${ae.runtime}`;D.has(Ne)||D.set(Ne,{key:Ne,label:Gs(B.apply_mode),save:ae.save,runtime:ae.runtime})}return[...D.values()]}function Le(v){if(De(v))return v.runtime_effect||v.activation_policy||"";if(v.apply_mode==="activation_required"){const D=v.activation_policy||v.runtime_effect;return D?`Not active after saving. No activation control exists in this release. ${D}`:"Not active after saving; no activation control exists in this release."}return""}function De(v){return v.action_available===!0&&!!(v.action_label&&v.action_endpoint)}async function Be(v){if(De(v))try{if(Pe(v.path))throw new Error("Save this setting before applying its action.");const D=String(v.action_method||"POST").toLowerCase(),B={post:G.post.bind(G),put:G.put.bind(G),delete:G.del.bind(G)}[D];if(!B)throw new Error("Unsupported configuration action");await B(v.action_endpoint,v.action_body||void 0),await me(),Cs("success",`${v.action_label} completed.`)}catch(D){Cs("error",D.message||`${v.action_label} failed`)}}function qe(v,D){return[v.label,v.path,v.description,...v.aliases||[]].filter(Boolean).join(" ").toLowerCase().includes(D)}function ct(v){const D=u.value.trim().toLowerCase();return D?$(v).filter(B=>qe(B,D)):[]}function K(v){const D=$(v);if(f.value!=="all"&&!D.some(ae=>ae.apply_state===f.value))return!1;const B=u.value.trim().toLowerCase();return!B||`${Z(v)} ${v}`.toLowerCase().includes(B)?!0:D.some(ae=>qe(ae,B))}function xe(v,D){return $(v).filter(B=>B.apply_state===D).length}function Ce(v){return v==="all"?U.value:R.value.filter(D=>D.apply_state===v).length}function Re(v){const D=v.sections.flatMap(B=>$(B));return{fields:D.length,modified:ie.value.filter(B=>v.sections.includes(B.path.split(".")[0])).length,pending_restart:D.filter(B=>B.apply_state==="pending_restart").length,invalid:D.filter(B=>B.apply_state==="invalid").length,dormant:D.filter(B=>B.apply_state==="dormant").length}}function Ve(v){var D;return Object.hasOwn(y.value,v)&&!Ri((D=e.value)==null?void 0:D[v],y.value[v])}function Pe(v){return ie.value.some(D=>D.path===v||D.path.startsWith(`${v}.`))}function pt(v){p.value=v,u.value="",f.value="all";try{localStorage.setItem(Tm,v)}catch{}}function ls(v){f.value=v}function Ps(){u.value="",f.value="all"}function nn(v){var D;return((D=we.value.find(B=>B.sections.includes(v)))==null?void 0:D.sections)||[]}function Ss(v){const D=nn(v),B=D.find(ae=>b.value[ae]===!0);return B||D.find(ae=>b.value[ae]!==!1)||null}function Fs(v){return u.value&&!T.value&&K(v)?!0:T.value?Ss(v)===v:Object.hasOwn(b.value,v)?b.value[v]===!0:!0}function Mt(v){const D=!Fs(v);if(T.value){const B={...b.value};for(const ae of nn(v))B[ae]===!0&&(B[ae]=!1);B[v]=D,b.value=B;return}b.value={...b.value,[v]:D}}function Yt(){_.value.push(Zn(y.value)),_.value.length>Hk&&_.value.shift(),S.value=[]}function $s(){he.value&&(Yt(),y.value={},m.value={},g.value=!1)}function Bs(v,D=!1){const B=Date.now();if(D&&H.path===v&&B-H.atNe-ae);I.value="",zt(v,B)}function q(v,D){zt(v,(v.value||[]).filter(B=>B!==D))}function re(v){return v.apply_mode==="live_read"?"Odin reads the saved file value on next use.":v.apply_mode==="live_for_new_work"?"New work uses the saved file value.":v.apply_mode==="live_apply"?v.apply_handler?`Apply the saved value through ${v.apply_handler}.`:"Apply it through its dedicated owner page or endpoint.":v.apply_mode==="restart"?"Restart Odin for the saved collection to take effect.":v.apply_mode==="activation_required"?"Saving does not enable it. No activation control exists in this release.":v.apply_mode==="dormant"?"This release does not use the saved collection.":"Follow the runtime details shown for this setting."}function Ee(v){return v.type==="array"&&Array.isArray(v.value)&&!v.structured_container&&!v.structured_container_child&&v.sensitivity==="public"&&v.value.every(D=>["string","number","boolean"].includes(typeof D))}function Ze(v){const D=String(x.value[v.path]??"").trim();if(!D)return;const B=[...new Set([...v.value||[],D])];x.value={...x.value,[v.path]:""},zt(v,B)}function lt(v,D){zt(v,(v.value||[]).filter(B=>B!==D))}function Ot(v,D){var ae;if(!v)return null;if((ae=v.enum)!=null&&ae.length&&!v.enum.includes(D))return`Choose one of: ${v.enum.join(", ")}`;if(v.path==="agents.final_warning_iterations"&&(!Array.isArray(D)||!D.length))return"Add at least one warning threshold.";const B=v.constraints||{};if((v.type==="integer"||v.type==="number")&&typeof D=="number"){if(B.minimum!==void 0&&DB.maximum)return`Must be at most ${B.maximum}${v.unit?` ${v.unit}`:""}`}return null}function Pt(v){return V.value[v.path]||null}function ma(v){const D=`${v}.`;return Object.keys(V.value).some(B=>B===v||B.startsWith(D))}function Ts(){_.value.length&&(S.value.push(Zn(y.value)),y.value=_.value.pop(),m.value={},E.value={},H={path:null,at:0})}function ga(){S.value.length&&(_.value.push(Zn(y.value)),y.value=S.value.pop(),m.value={},E.value={},H={path:null,at:0})}function ni(){!he.value||de.value||(g.value=!0,w.value=!1)}function va(){g.value=!1}function ba(){$s()}function Gs(v){return Bk[v]||wa(v||"unknown")}function z(v){return`apply-${String(v||"unknown").replaceAll("_","-")}`}function pe(v){return`cfgc-field-${v.replace(/[^a-zA-Z0-9_-]/g,"-")}`}function _e(v){return`${pe(v)}-input`}function Nt(v){const D=document.getElementById(pe(v))||document.getElementById(pe(v.split(".").slice(0,2).join(".")));D==null||D.scrollIntoView({behavior:"smooth",block:"center"})}function Cs(v,D){l.value={type:v,message:D},window.setTimeout(()=>{var B;((B=l.value)==null?void 0:B.message)===D&&(l.value=null)},3500)}function jn(){o.value=!1,f.value="pending_restart",u.value="";const v=$k(n.value);v&&(v.scrollTop=0)}function Br(){o.value=!1}function ai(v=1800){M&&window.clearTimeout(M),M=window.setTimeout(rl,v)}async function rl(){if(c.value){if(P+=1,P>45){c.value=!1,d.value="Odin did not return with the new startup settings within 90 seconds.";return}try{if(t.value=await lo(),k.value===0){c.value=!1,d.value=null,Cs("success","Odin restarted and the saved startup settings are active.");return}}catch{}ai(2e3)}}async function ya(){if(!c.value){d.value=null;try{await G.post("/api/restart",{}),c.value=!0,P=0,o.value=!1,ai()}catch(v){d.value=v.message||"Odin could not schedule a restart."}}}async function ii(){if(!(!he.value||de.value||a.value)){a.value=!0;try{const v=qk(e.value,y.value),D=await G.put("/api/config",v);e.value=D,y.value={},_.value=[],S.value=[],m.value={},g.value=!1;try{t.value=await lo(),r.value=null,o.value=k.value>0,Cs("success",k.value?`Configuration saved. ${k.value} setting${k.value===1?"":"s"} still use startup values.`:"Configuration saved. Apply status has been refreshed.")}catch(B){r.value=B.message||"Unknown metadata error.",Cs("error",`Configuration saved, but apply status could not be refreshed: ${r.value}`)}}catch(v){Cs("error",v.message||"Configuration could not be saved")}finally{a.value=!1}}}async function me(){var v,D;if(!he.value){s.value=!0,i.value=null;try{const B=await G.get("/api/config"),ae=await lo();e.value=B,t.value=ae,r.value=null;const Ne=we.value;if(Ne.some(Ue=>Ue.key===p.value)||(p.value=((v=Ne[0])==null?void 0:v.key)||Ha[0].key),T.value){const et=(((D=Ne.find(Vt=>Vt.key===p.value))==null?void 0:D.sections)||[]).find(Vt=>b.value[Vt]===!0);b.value=et?{...b.value,[et]:!0}:{}}}catch(B){i.value=B.message||"Unknown configuration error"}finally{s.value=!1}}}function A(v){if(g.value||!(v.ctrlKey||v.metaKey))return;const D=v.target;D instanceof HTMLElement&&(D.matches("input, textarea, select")||D.isContentEditable)||(!v.shiftKey&&v.key.toLowerCase()==="z"?(v.preventDefault(),Ts()):(v.key.toLowerCase()==="y"||v.shiftKey&&v.key.toLowerCase()==="z")&&(v.preventDefault(),ga()))}function W(v){T.value=v.matches}return ns(b,v=>{try{localStorage.setItem(Sm,JSON.stringify(v))}catch{}},{deep:!0}),We(()=>{var v;me(),document.addEventListener("keydown",A),C=window.matchMedia("(max-width: 760px)"),W(C),(v=C.addEventListener)==null||v.call(C,"change",W)}),xt(()=>{var v;document.removeEventListener("keydown",A),(v=C==null?void 0:C.removeEventListener)==null||v.call(C,"change",W),M&&window.clearTimeout(M)}),{config:e,meta:t,loading:s,saving:a,error:i,toast:l,metaRefreshError:r,restartPromptOpen:o,restartScheduled:c,restartError:d,configMain:n,searchQuery:u,healthFilter:f,activeCategory:p,reviewOpen:g,mobileOverflowOpen:w,warningThresholdInput:I,arrayInputs:x,healthFilters:O,visibleCategories:we,displayGroups:ce,reviewGroups:ye,sectionCount:Q,fieldCount:U,hasChanges:he,changeCount:F,changedSectionCount:se,hasDraftErrors:de,canUndo:N,canRedo:Y,globalFilterActive:Se,reviewRestartCount:ge,pendingRestartCount:k,pendingRestartFields:He,healthCount:Ce,categoryStats:Re,selectCategory:pt,selectHealthFilter:ls,clearFilters:Ps,sectionLabel:Z,sectionDescription:X,sectionFieldCount:ee,sectionHealthCount:xe,sectionApplySummary:oe,sectionApplyDetails:le,sectionEntries:ne,fieldGroups:ve,sectionSearchHits:ct,fieldRuntimeCopy:Te,fieldSpecificRuntimeNote:Le,hasHonestAction:De,runFieldAction:Be,sectionChanged:Ve,fieldChanged:Pe,isSectionExpanded:Fs,toggleSection:Mt,discardAllDrafts:$s,setFieldValue:zt,setNumberFieldValue:Ye,numberInputValue:os,beginInputEdit:Vn,endTextInputEdit:Ct,endInputEdit:rs,addWarningThreshold:gs,removeWarningThreshold:q,isScalarArray:Ee,addScalarArrayItem:Ze,removeScalarArrayItem:lt,fieldError:Pt,sectionHasErrors:ma,undo:Ts,redo:ga,openReview:ni,closeReview:va,mobileCancel:ba,applyModeLabel:Gs,applyClass:z,compactValue:Vk,formatValue:jk,structuredApplyCopy:re,fieldId:pe,fieldInputId:_e,focusField:Nt,fetchConfig:me,saveConfig:ii,restartOdin:ya,restartLater:Br,reviewPendingRestart:jn}}},Zk=/^\d{15,25}$/;function Am(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 Rm={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}},emits:["select"],template:`
t in e?Gm(e,t,{enumerable:!0,config
- `,setup(e,{emit:t}){const s=h(""),n=h(!1),a=h(0),i=h(null),l=J(()=>new Set((e.excludedIds||[]).map(String))),r=J(()=>{const S=s.value.toLowerCase().trim();return(e.members||[]).filter(g=>l.value.has(String(g.id))?!1:S?u(g).toLowerCase().includes(S)||String(g.username||"").toLowerCase().includes(S)||String(g.id).includes(S):!0)}),o=J(()=>{const S=s.value.trim();return r.value.length===0&&Zk.test(S)&&!l.value.has(S)?S:""}),c=J(()=>r.value.length+(o.value?1:0)),d=J(()=>{if(n.value){if(r.value[a.value])return`${e.optionsId}-${a.value}`;if(o.value&&a.value===r.value.length)return`${e.optionsId}-raw`}});function u(S){return Am(S)}function f(){n.value=!0,a.value=0}function p(){f()}function b(){const S=Math.max(c.value-1,0);a.value=Math.min(a.value+1,S)}function y(){a.value=Math.max(a.value-1,0)}function A(){const S=r.value[a.value];S?O(S):o.value&&a.value===r.value.length&&x(o.value)}function O(S){x(String(S.id))}function x(S){t("select",S),s.value="",n.value=!1,a.value=0}function m(){n.value=!1}function _(){setTimeout(m,150)}return We(()=>{e.autofocus&&At(()=>{var S;return(S=i.value)==null?void 0:S.focus()})}),{query:s,open:n,highlightedIndex:a,input:i,filteredMembers:r,rawId:o,activeOptionId:d,memberName:u,openOptions:f,onInput:p,highlightNext:b,highlightPrevious:y,selectHighlighted:A,selectMember:O,selectId:x,closeOptions:m,onBlur:_}}};function Pu(e,t,s){var n;return((n=e==null?void 0:e.config)==null?void 0:n[t])!=null?e.config[t]:s==null?void 0:s[t]}const Jk={components:{DiscordUserCombobox:Rm},template:` + `,setup(e,{emit:t}){const s=h(""),n=h(!1),a=h(0),i=h(null),l=J(()=>new Set((e.excludedIds||[]).map(String))),r=J(()=>{const S=s.value.toLowerCase().trim();return(e.members||[]).filter(g=>l.value.has(String(g.id))?!1:S?u(g).toLowerCase().includes(S)||String(g.username||"").toLowerCase().includes(S)||String(g.id).includes(S):!0)}),o=J(()=>{const S=s.value.trim();return r.value.length===0&&Zk.test(S)&&!l.value.has(S)?S:""}),c=J(()=>r.value.length+(o.value?1:0)),d=J(()=>{if(n.value){if(r.value[a.value])return`${e.optionsId}-${a.value}`;if(o.value&&a.value===r.value.length)return`${e.optionsId}-raw`}});function u(S){return Am(S)}function f(){n.value=!0,a.value=0}function p(){f()}function b(){const S=Math.max(c.value-1,0);a.value=Math.min(a.value+1,S)}function y(){a.value=Math.max(a.value-1,0)}function E(){const S=r.value[a.value];S?I(S):o.value&&a.value===r.value.length&&x(o.value)}function I(S){x(String(S.id))}function x(S){t("select",S),s.value="",n.value=!1,a.value=0}function m(){n.value=!1}function _(){setTimeout(m,150)}return We(()=>{e.autofocus&&Rt(()=>{var S;return(S=i.value)==null?void 0:S.focus()})}),{query:s,open:n,highlightedIndex:a,input:i,filteredMembers:r,rawId:o,activeOptionId:d,memberName:u,openOptions:f,onInput:p,highlightNext:b,highlightPrevious:y,selectHighlighted:E,selectMember:I,selectId:x,closeOptions:m,onBlur:_}}};function Pu(e,t,s){var n;return((n=e==null?void 0:e.config)==null?void 0:n[t])!=null?e.config[t]:s==null?void 0:s[t]}const Jk={components:{DiscordUserCombobox:Rm},template:`

Discord Channels

@@ -4360,7 +4360,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h([]),t=h(!0),s=h(null),n=h({}),a=h(null),i=h(null),l=h(!1),r=h(null),o=h({}),c=h([]);let d=0;const u=Object.freeze([{key:"allowed_users",label:"Allowed users",description:"Absolute gate for ordinary conversational intake. Guild/channel settings cannot readmit blocked users; prefix commands use separate authorization and allowed test webhooks bypass this gate.",placeholder:"Search Discord users…",userAutocomplete:!0,fullWidth:!0},{key:"channels",label:"Allowed channels",description:"Absolute gate for ordinary conversational intake. Guild/channel settings cannot readmit blocked channels; prefix commands use separate authorization.",placeholder:"Discord channel ID",fullWidth:!0},{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:"Search Discord users or bots…",userAutocomplete:!0,fullWidth:!0}]),f=J(()=>JSON.stringify(a.value)!==JSON.stringify(i.value)),p=J(()=>new Map(c.value.map(I=>[String(I.id),I])));function b(I){return I.config&&I.config.enabled!==void 0?I.config.enabled:!0}function y(I){return Pu(I,"require_mention",a.value)}function A(I){return Pu(I,"respond_to_bots",a.value)}function O(I){return I.config&&Object.keys(I.config).length>0}function x(I){n.value[I]=!n.value[I]}function m(I){const j=I.discord||{};return{allowed_users:[...j.allowed_users||[]],channels:[...j.channels||[]],respond_to_bots:!!j.respond_to_bots,require_mention:!!j.require_mention,ignore_bot_ids:[...j.ignore_bot_ids||[]]}}async function _({showLoading:I=!0}={}){const j=++d;I&&(t.value=!0),s.value=null;try{const Y=await K.get("/api/discord/guilds");j===d&&(e.value=Y)}catch(Y){j===d&&(s.value=Y.message)}finally{I&&j===d&&(t.value=!1)}}async function S(){t.value=!0,s.value=null;try{const[I,j,Y]=await Promise.all([K.get("/api/discord/guilds"),K.get("/api/discord/members").catch(()=>[]),K.get("/api/config")]),H=m(Y),N=f.value;a.value=H,N||(i.value=JSON.parse(JSON.stringify(H))),c.value=j,e.value=I,r.value=null}catch(I){s.value=I.message}finally{t.value=!1}}async function g(I,j,Y){try{await K.put("/api/discord/guild/"+I+"/config",{[j]:Y}),await _({showLoading:!1})}catch(H){s.value=H.message}}async function w(I,j,Y,H){try{await K.put("/api/discord/channel/"+I+"/config",{[Y]:H}),await _({showLoading:!1})}catch(N){s.value=N.message}}async function T(I,j){try{await K.put("/api/discord/channel/"+I+"/config",{clear:!0}),await _({showLoading:!1})}catch(Y){s.value=Y.message}}function C(I,j){const Y=String(j);if(!I.userAutocomplete)return Y;const H=p.value.get(Y);return H?Am(H):Y}function M(I,j=null){const Y=String(j??o.value[I]??"").trim();!Y||i.value[I].includes(Y)||(i.value[I]=[...i.value[I],Y],o.value={...o.value,[I]:""})}function B(I,j){i.value[I]=i.value[I].filter(Y=>Y!==j)}async function $(){if(!(!f.value||l.value)){l.value=!0,r.value=null;try{const j=(await K.put("/api/config",{discord:i.value})).discord||i.value;a.value={allowed_users:[...j.allowed_users||[]],channels:[...j.channels||[]],respond_to_bots:!!j.respond_to_bots,require_mention:!!j.require_mention,ignore_bot_ids:[...j.ignore_bot_ids||[]]},i.value=JSON.parse(JSON.stringify(a.value))}catch(I){r.value=I.message||"Global defaults could not be saved."}finally{l.value=!1}}}return We(S),{guilds:e,loading:t,error:s,expanded:n,globalDraft:i,globalSaving:l,globalError:r,globalArrayInputs:o,globalMembers:c,globalListEditors:u,globalChanged:f,guildEnabled:b,guildMention:y,guildBots:A,hasOverride:O,toggleGuild:x,fetchAll:S,fetchGuilds:_,setGuildConfig:g,setChannelConfig:w,clearOverride:T,globalItemLabel:C,addGlobalItem:M,removeGlobalItem:B,saveGlobalDefaults:$}}},fs=e=>e==null?e:JSON.parse(JSON.stringify(e));function Yk({applyDefault:e,applyUser:t,applyDelete:s,onDefaultConfirmed:n=()=>{},onDefaultRollback:a=()=>{},onUserConfirmed:i=()=>{},onUserRollback:l=()=>{},onUserDeleted:r=()=>{},onError:o=()=>{}}){let c=Promise.resolve(),d=0,u=0;const f=new Map;let p=null;const b=new Map;function y(g){d+=1;const w=c.then(g,g);return c=w.catch(()=>{}),w}function A(g,w){p=fs(g),b.clear();for(const[T,C]of Object.entries(w||{}))b.set(T,fs(C))}function O(g){const w=fs(g),T=++u;return y(async()=>{try{await e(fs(w)),p=fs(w),T===u&&n(fs(w))}catch(C){T===u&&(a(fs(p)),o(C,{kind:"default"}))}})}function x(g,w){const T=fs(w),C=(f.get(g)||0)+1;return f.set(g,C),y(async()=>{try{await t(g,fs(T)),b.set(g,fs(T)),C===f.get(g)&&i(g,fs(T))}catch(M){C===f.get(g)&&(l(g,fs(b.get(g)??null)),o(M,{kind:"user",uid:g}))}})}function m(g){const w=(f.get(g)||0)+1;return f.set(g,w),y(async()=>{try{await s(g),b.delete(g),w===f.get(g)&&r(g)}catch(T){w===f.get(g)&&(l(g,fs(b.get(g)??null)),o(T,{kind:"delete",uid:g}))}})}async function _(){for(;;){const g=c;if(await g,g===c)return d}}async function S(g){for(;;){const w=await _(),T=await g();if(w===d)return T}}return{seed:A,saveDefault:O,saveUser:x,deleteUser:m,whenIdle:_,readSnapshot:S,get revision(){return d}}}const Qk={components:{DiscordUserCombobox:Rm},template:` + `,setup(){const e=h([]),t=h(!0),s=h(null),n=h({}),a=h(null),i=h(null),l=h(!1),r=h(null),o=h({}),c=h([]);let d=0;const u=Object.freeze([{key:"allowed_users",label:"Allowed users",description:"Absolute gate for ordinary conversational intake. Guild/channel settings cannot readmit blocked users; prefix commands use separate authorization and allowed test webhooks bypass this gate.",placeholder:"Search Discord users…",userAutocomplete:!0,fullWidth:!0},{key:"channels",label:"Allowed channels",description:"Absolute gate for ordinary conversational intake. Guild/channel settings cannot readmit blocked channels; prefix commands use separate authorization.",placeholder:"Discord channel ID",fullWidth:!0},{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:"Search Discord users or bots…",userAutocomplete:!0,fullWidth:!0}]),f=J(()=>JSON.stringify(a.value)!==JSON.stringify(i.value)),p=J(()=>new Map(c.value.map(R=>[String(R.id),R])));function b(R){return R.config&&R.config.enabled!==void 0?R.config.enabled:!0}function y(R){return Pu(R,"require_mention",a.value)}function E(R){return Pu(R,"respond_to_bots",a.value)}function I(R){return R.config&&Object.keys(R.config).length>0}function x(R){n.value[R]=!n.value[R]}function m(R){const j=R.discord||{};return{allowed_users:[...j.allowed_users||[]],channels:[...j.channels||[]],respond_to_bots:!!j.respond_to_bots,require_mention:!!j.require_mention,ignore_bot_ids:[...j.ignore_bot_ids||[]]}}async function _({showLoading:R=!0}={}){const j=++d;R&&(t.value=!0),s.value=null;try{const Q=await G.get("/api/discord/guilds");j===d&&(e.value=Q)}catch(Q){j===d&&(s.value=Q.message)}finally{R&&j===d&&(t.value=!1)}}async function S(){t.value=!0,s.value=null;try{const[R,j,Q]=await Promise.all([G.get("/api/discord/guilds"),G.get("/api/discord/members").catch(()=>[]),G.get("/api/config")]),U=m(Q),O=f.value;a.value=U,O||(i.value=JSON.parse(JSON.stringify(U))),c.value=j,e.value=R,r.value=null}catch(R){s.value=R.message}finally{t.value=!1}}async function g(R,j,Q){try{await G.put("/api/discord/guild/"+R+"/config",{[j]:Q}),await _({showLoading:!1})}catch(U){s.value=U.message}}async function w(R,j,Q,U){try{await G.put("/api/discord/channel/"+R+"/config",{[Q]:U}),await _({showLoading:!1})}catch(O){s.value=O.message}}async function T(R,j){try{await G.put("/api/discord/channel/"+R+"/config",{clear:!0}),await _({showLoading:!1})}catch(Q){s.value=Q.message}}function C(R,j){const Q=String(j);if(!R.userAutocomplete)return Q;const U=p.value.get(Q);return U?Am(U):Q}function M(R,j=null){const Q=String(j??o.value[R]??"").trim();!Q||i.value[R].includes(Q)||(i.value[R]=[...i.value[R],Q],o.value={...o.value,[R]:""})}function H(R,j){i.value[R]=i.value[R].filter(Q=>Q!==j)}async function P(){if(!(!f.value||l.value)){l.value=!0,r.value=null;try{const j=(await G.put("/api/config",{discord:i.value})).discord||i.value;a.value={allowed_users:[...j.allowed_users||[]],channels:[...j.channels||[]],respond_to_bots:!!j.respond_to_bots,require_mention:!!j.require_mention,ignore_bot_ids:[...j.ignore_bot_ids||[]]},i.value=JSON.parse(JSON.stringify(a.value))}catch(R){r.value=R.message||"Global defaults could not be saved."}finally{l.value=!1}}}return We(S),{guilds:e,loading:t,error:s,expanded:n,globalDraft:i,globalSaving:l,globalError:r,globalArrayInputs:o,globalMembers:c,globalListEditors:u,globalChanged:f,guildEnabled:b,guildMention:y,guildBots:E,hasOverride:I,toggleGuild:x,fetchAll:S,fetchGuilds:_,setGuildConfig:g,setChannelConfig:w,clearOverride:T,globalItemLabel:C,addGlobalItem:M,removeGlobalItem:H,saveGlobalDefaults:P}}},vs=e=>e==null?e:JSON.parse(JSON.stringify(e));function Yk({applyDefault:e,applyUser:t,applyDelete:s,onDefaultConfirmed:n=()=>{},onDefaultRollback:a=()=>{},onUserConfirmed:i=()=>{},onUserRollback:l=()=>{},onUserDeleted:r=()=>{},onError:o=()=>{}}){let c=Promise.resolve(),d=0,u=0;const f=new Map;let p=null;const b=new Map;function y(g){d+=1;const w=c.then(g,g);return c=w.catch(()=>{}),w}function E(g,w){p=vs(g),b.clear();for(const[T,C]of Object.entries(w||{}))b.set(T,vs(C))}function I(g){const w=vs(g),T=++u;return y(async()=>{try{await e(vs(w)),p=vs(w),T===u&&n(vs(w))}catch(C){T===u&&(a(vs(p)),o(C,{kind:"default"}))}})}function x(g,w){const T=vs(w),C=(f.get(g)||0)+1;return f.set(g,C),y(async()=>{try{await t(g,vs(T)),b.set(g,vs(T)),C===f.get(g)&&i(g,vs(T))}catch(M){C===f.get(g)&&(l(g,vs(b.get(g)??null)),o(M,{kind:"user",uid:g}))}})}function m(g){const w=(f.get(g)||0)+1;return f.set(g,w),y(async()=>{try{await s(g),b.delete(g),w===f.get(g)&&r(g)}catch(T){w===f.get(g)&&(l(g,vs(b.get(g)??null)),o(T,{kind:"delete",uid:g}))}})}async function _(){for(;;){const g=c;if(await g,g===c)return d}}async function S(g){for(;;){const w=await _(),T=await g();if(w===d)return T}}return{seed:E,saveDefault:I,saveUser:x,deleteUser:m,whenIdle:_,readSnapshot:S,get revision(){return d}}}const Qk={components:{DiscordUserCombobox:Rm},template:`

Host Access Control

@@ -4480,7 +4480,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h(!0),t=h(""),s=h(null),n=h([]),a=h({allowed_hosts:[],default_host:""}),i=h({}),l=h(!1),r=h([]),o=J(()=>{const g={};for(const w of r.value)g[w.id]=w;return g});function c(g){return o.value[g]||null}function d(g,w){return g?g.allowed_hosts===null||g.allowed_hosts===void 0?{allowed_hosts:[...w],default_host:g.default_host||"",allow_all:!0}:{allowed_hosts:g.allowed_hosts,default_host:g.default_host||"",allow_all:!1}:{allowed_hosts:[...w],default_host:w[0]||"",allow_all:!0}}const u=Yk({applyDefault:async g=>{const w=g.allow_all?null:g.allowed_hosts;await K.put("/api/host-access/default-policy",{allowed_hosts:w,default_host:g.default_host})},applyUser:async(g,w)=>{const T=w.allow_all?null:w.allowed_hosts;await K.put(`/api/host-access/user/${g}`,{allowed_hosts:T,default_host:w.default_host})},applyDelete:g=>K.del(`/api/host-access/user/${g}`),onDefaultConfirmed:()=>Te.success("Default policy updated"),onDefaultRollback:g=>{g&&(a.value=g)},onUserConfirmed:g=>{const w=c(g);Te.success(`Updated access for ${w?w.display_name:g}`)},onUserRollback:(g,w)=>{const T={...i.value};w?T[g]=w:delete T[g],i.value=T},onUserDeleted:g=>{const w={...i.value};delete w[g],i.value=w},onError:(g,w)=>{var C;const T=w.uid?` ${((C=c(w.uid))==null?void 0:C.display_name)||w.uid}`:"";Te.error(`${g.message||"Failed to save"} — reverted${T}`)}});let f=0;async function p(){const g=++f;e.value=!0,t.value="";try{const w=await u.readSnapshot(()=>K.get("/api/host-access"));if(g!==f)return;s.value=w,n.value=w.available_hosts||[],a.value=d(w.default_policy,n.value);const T=w.users||{},C={};for(const[M,B]of Object.entries(T))C[M]=d(B,n.value);i.value=C,u.seed(a.value,C)}catch(w){g===f&&(t.value=w.message||"Failed to fetch host access data")}finally{g===f&&(e.value=!1)}try{const w=await K.get("/api/discord/members")||[];g===f&&(r.value=w)}catch{g===f&&(r.value=[])}}function b(){u.saveDefault(a.value)}function y(g,w){a.value.allow_all=!1,w?a.value.allowed_hosts.includes(g)||a.value.allowed_hosts.push(g):(a.value.allowed_hosts=a.value.allowed_hosts.filter(T=>T!==g),a.value.default_host===g&&(a.value.default_host=a.value.allowed_hosts[0]||"")),b()}function A(g){const w=i.value[g];w&&u.saveUser(g,w)}function O(g,w,T){const C=i.value[g];C&&(C.allow_all=!1,T?C.allowed_hosts.includes(w)||C.allowed_hosts.push(w):(C.allowed_hosts=C.allowed_hosts.filter(M=>M!==w),C.default_host===w&&(C.default_host=C.allowed_hosts[0]||"")),A(g))}function x(g,w){const T=i.value[g];T&&(T.default_host=w,A(g))}function m(){l.value=!0}function _(g){!/^\d{15,25}$/.test(g)||i.value[g]||(i.value[g]={allowed_hosts:[...n.value],default_host:n.value[0]||"",allow_all:!1},A(g),l.value=!1)}async function S(g){const w=c(g);await gs({title:"Remove user override",message:`Remove the host access override for ${w?w.display_name:g}? They will fall back to the default policy.`,confirmLabel:"Remove",danger:!0})&&(await u.deleteUser(g),i.value[g]||Te.success(`Removed override for ${w?w.display_name:g}`))}return We(p),{loading:e,error:t,data:s,availableHosts:n,defaultPolicy:a,users:i,showAddUser:l,members:r,fetchData:p,saveDefaultPolicy:b,toggleDefaultHost:y,getMember:c,toggleUserHost:O,setUserDefault:x,openAddUser:m,addUserById:_,deleteUser:S}}},Xk={template:` + `,setup(){const e=h(!0),t=h(""),s=h(null),n=h([]),a=h({allowed_hosts:[],default_host:""}),i=h({}),l=h(!1),r=h([]),o=J(()=>{const g={};for(const w of r.value)g[w.id]=w;return g});function c(g){return o.value[g]||null}function d(g,w){return g?g.allowed_hosts===null||g.allowed_hosts===void 0?{allowed_hosts:[...w],default_host:g.default_host||"",allow_all:!0}:{allowed_hosts:g.allowed_hosts,default_host:g.default_host||"",allow_all:!1}:{allowed_hosts:[...w],default_host:w[0]||"",allow_all:!0}}const u=Yk({applyDefault:async g=>{const w=g.allow_all?null:g.allowed_hosts;await G.put("/api/host-access/default-policy",{allowed_hosts:w,default_host:g.default_host})},applyUser:async(g,w)=>{const T=w.allow_all?null:w.allowed_hosts;await G.put(`/api/host-access/user/${g}`,{allowed_hosts:T,default_host:w.default_host})},applyDelete:g=>G.del(`/api/host-access/user/${g}`),onDefaultConfirmed:()=>Ae.success("Default policy updated"),onDefaultRollback:g=>{g&&(a.value=g)},onUserConfirmed:g=>{const w=c(g);Ae.success(`Updated access for ${w?w.display_name:g}`)},onUserRollback:(g,w)=>{const T={...i.value};w?T[g]=w:delete T[g],i.value=T},onUserDeleted:g=>{const w={...i.value};delete w[g],i.value=w},onError:(g,w)=>{var C;const T=w.uid?` ${((C=c(w.uid))==null?void 0:C.display_name)||w.uid}`:"";Ae.error(`${g.message||"Failed to save"} — reverted${T}`)}});let f=0;async function p(){const g=++f;e.value=!0,t.value="";try{const w=await u.readSnapshot(()=>G.get("/api/host-access"));if(g!==f)return;s.value=w,n.value=w.available_hosts||[],a.value=d(w.default_policy,n.value);const T=w.users||{},C={};for(const[M,H]of Object.entries(T))C[M]=d(H,n.value);i.value=C,u.seed(a.value,C)}catch(w){g===f&&(t.value=w.message||"Failed to fetch host access data")}finally{g===f&&(e.value=!1)}try{const w=await G.get("/api/discord/members")||[];g===f&&(r.value=w)}catch{g===f&&(r.value=[])}}function b(){u.saveDefault(a.value)}function y(g,w){a.value.allow_all=!1,w?a.value.allowed_hosts.includes(g)||a.value.allowed_hosts.push(g):(a.value.allowed_hosts=a.value.allowed_hosts.filter(T=>T!==g),a.value.default_host===g&&(a.value.default_host=a.value.allowed_hosts[0]||"")),b()}function E(g){const w=i.value[g];w&&u.saveUser(g,w)}function I(g,w,T){const C=i.value[g];C&&(C.allow_all=!1,T?C.allowed_hosts.includes(w)||C.allowed_hosts.push(w):(C.allowed_hosts=C.allowed_hosts.filter(M=>M!==w),C.default_host===w&&(C.default_host=C.allowed_hosts[0]||"")),E(g))}function x(g,w){const T=i.value[g];T&&(T.default_host=w,E(g))}function m(){l.value=!0}function _(g){!/^\d{15,25}$/.test(g)||i.value[g]||(i.value[g]={allowed_hosts:[...n.value],default_host:n.value[0]||"",allow_all:!1},E(g),l.value=!1)}async function S(g){const w=c(g);await _s({title:"Remove user override",message:`Remove the host access override for ${w?w.display_name:g}? They will fall back to the default policy.`,confirmLabel:"Remove",danger:!0})&&(await u.deleteUser(g),i.value[g]||Ae.success(`Removed override for ${w?w.display_name:g}`))}return We(p),{loading:e,error:t,data:s,availableHosts:n,defaultPolicy:a,users:i,showAddUser:l,members:r,fetchData:p,saveDefaultPolicy:b,toggleDefaultHost:y,getMember:c,toggleUserHost:I,setUserDefault:x,openAddUser:m,addUserById:_,deleteUser:S}}},Xk={template:`

API Tokens

@@ -4723,7 +4723,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h(!0),t=h(""),s=h(null),n=h([]),a=h(!1),i=h(!1),l=h(null),r=h(null),o=h(!1),c=h({user_id:"",username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""}),d=h({username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""}),u=J(()=>c.value.host_mode==="select"?c.value.allowed_hosts:c.value.host_mode==="none"?[]:n.value),f=J(()=>d.value.host_mode==="select"?d.value.allowed_hosts:d.value.host_mode==="none"?[]:n.value);function p(T){return T==="admin"?"text-xs px-1.5 py-0.5 rounded bg-red-900/50 text-red-400":T==="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 b(){e.value=!0,t.value="";try{const T=await K.get("/api/tokens");s.value=T.tokens||[],n.value=T.available_hosts||[]}catch(T){t.value=T.message||"Failed to load tokens"}finally{e.value=!1}}function y(T){return!T||!T.trim()?[]:T.split(",").map(C=>C.trim()).filter(Boolean)}function A(T,C){const M=c.value.allowed_hosts;if(C&&!M.includes(T)&&M.push(T),!C){const B=M.indexOf(T);B>=0&&M.splice(B,1)}}function O(T,C){const M=d.value.allowed_hosts;if(C&&!M.includes(T)&&M.push(T),!C){const B=M.indexOf(T);B>=0&&M.splice(B,1)}}async function x(){var T;i.value=!0;try{const C=y(c.value.allowed_tools_str),M=c.value.host_mode,B=M==="none"?[]:M==="select"?c.value.allowed_hosts:null,$={user_id:c.value.user_id.trim(),username:c.value.username.trim()||"API",tier:c.value.tier,label:c.value.label.trim(),allowed_tools:C.length?C:[]};B!==null&&($.allowed_hosts=B),$.default_host=c.value.default_host||"";const I=await K.post("/api/tokens",$);l.value=I.token,c.value={user_id:"",username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""},a.value=!1,Te.success("Token created"),await b()}catch(C){Te.error(((T=C.data)==null?void 0:T.error)||C.message||"Failed to create token")}finally{i.value=!1}}function m(T){r.value=T;const C=T.allowed_hosts;let M="default";C==null?M="default":Array.isArray(C)&&C.length===0?M="none":Array.isArray(C)&&(M="select"),d.value={username:T.username||"",tier:T.tier||"admin",label:T.label||"",host_mode:M,allowed_hosts:Array.isArray(C)?[...C]:[],default_host:T.default_host||"",allowed_tools_str:(T.allowed_tools||[]).join(", ")}}async function _(){var T;if(r.value){o.value=!0;try{const C=y(d.value.allowed_tools_str),M=d.value.host_mode,B={username:d.value.username,tier:d.value.tier,label:d.value.label,allowed_tools:C};M==="none"?B.allowed_hosts=[]:M==="select"?B.allowed_hosts=d.value.allowed_hosts:B.allowed_hosts=null,B.default_host=d.value.default_host||"",await K.put("/api/tokens/"+encodeURIComponent(r.value.user_id),B),r.value=null,Te.success("Token updated"),await b()}catch(C){Te.error(((T=C.data)==null?void 0:T.error)||C.message||"Failed to update")}finally{o.value=!1}}}async function S(T){var M;if(await gs({title:"Regenerate token",message:`Regenerate token for ${T.username||T.user_id}? The old token will stop working immediately.`,confirmLabel:"Regenerate",danger:!0}))try{const B=await K.post("/api/tokens/"+encodeURIComponent(T.user_id)+"/regenerate");l.value=B.token,Te.success("Token regenerated")}catch(B){Te.error(((M=B.data)==null?void 0:M.error)||B.message||"Failed to regenerate")}}async function g(T){var M;if(await gs({title:"Delete token",message:`Delete token for ${T.username||T.user_id}? This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await K.del("/api/tokens/"+encodeURIComponent(T.user_id)),Te.success("Token deleted"),await b()}catch(B){Te.error(((M=B.data)==null?void 0:M.error)||B.message||"Failed to delete")}}async function w(){if(l.value)try{await navigator.clipboard.writeText(l.value),Te.success("Copied to clipboard")}catch{Te.error("Copy failed — select and copy manually")}}return We(b),{loading:e,error:t,tokens:s,availableHosts:n,showCreate:a,creating:i,newToken:l,editing:r,saving:o,createForm:c,editForm:d,createDefaultHostOptions:u,editDefaultHostOptions:f,fetchData:b,tierBadge:p,toggleCreateHost:A,toggleEditHost:O,createToken:x,startEdit:m,saveEdit:_,confirmRegenerate:S,confirmDelete:g,copyToken:w}}},ew=Object.freeze(["enabled","model","reasoning_effort","agent_reasoning_effort","agent_model"]),tw=Object.freeze(["request_timeout_seconds","stream_stall_timeout_seconds","retry","connection_pool","context_compression"]),sw=Object.freeze(["enabled","base_url","model","max_tokens"]),nw=Object.freeze(["enabled","model","max_tokens"]);function Mr(e,t){return Object.fromEntries(t.map(s=>[s,e[s]]))}function Fu(e){return Mr(e,ew)}function $u(e){return Mr(e,tw)}function aw(e,{includeApiKey:t=!1}={}){const s=Mr(e,sw);return t&&(s.api_key=e.api_key),s}function iw(e){return{timeout:e.timeout}}function lw(e,{includeApiKey:t=!1}={}){const s=Mr(e,nw);return t&&(s.api_key=e.api_key),s}function rw(e){return{timeout:e.timeout}}function _l(e,t=500){let s=null;const n=(...a)=>{s&&clearTimeout(s),s=setTimeout(()=>{s=null,e(...a)},t)};return n.pending=()=>s!==null,n.cancel=()=>{s&&(clearTimeout(s),s=null)},n}const ow={template:` + `,setup(){const e=h(!0),t=h(""),s=h(null),n=h([]),a=h(!1),i=h(!1),l=h(null),r=h(null),o=h(!1),c=h({user_id:"",username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""}),d=h({username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""}),u=J(()=>c.value.host_mode==="select"?c.value.allowed_hosts:c.value.host_mode==="none"?[]:n.value),f=J(()=>d.value.host_mode==="select"?d.value.allowed_hosts:d.value.host_mode==="none"?[]:n.value);function p(T){return T==="admin"?"text-xs px-1.5 py-0.5 rounded bg-red-900/50 text-red-400":T==="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 b(){e.value=!0,t.value="";try{const T=await G.get("/api/tokens");s.value=T.tokens||[],n.value=T.available_hosts||[]}catch(T){t.value=T.message||"Failed to load tokens"}finally{e.value=!1}}function y(T){return!T||!T.trim()?[]:T.split(",").map(C=>C.trim()).filter(Boolean)}function E(T,C){const M=c.value.allowed_hosts;if(C&&!M.includes(T)&&M.push(T),!C){const H=M.indexOf(T);H>=0&&M.splice(H,1)}}function I(T,C){const M=d.value.allowed_hosts;if(C&&!M.includes(T)&&M.push(T),!C){const H=M.indexOf(T);H>=0&&M.splice(H,1)}}async function x(){var T;i.value=!0;try{const C=y(c.value.allowed_tools_str),M=c.value.host_mode,H=M==="none"?[]:M==="select"?c.value.allowed_hosts:null,P={user_id:c.value.user_id.trim(),username:c.value.username.trim()||"API",tier:c.value.tier,label:c.value.label.trim(),allowed_tools:C.length?C:[]};H!==null&&(P.allowed_hosts=H),P.default_host=c.value.default_host||"";const R=await G.post("/api/tokens",P);l.value=R.token,c.value={user_id:"",username:"",tier:"admin",label:"",host_mode:"default",allowed_hosts:[],default_host:"",allowed_tools_str:""},a.value=!1,Ae.success("Token created"),await b()}catch(C){Ae.error(((T=C.data)==null?void 0:T.error)||C.message||"Failed to create token")}finally{i.value=!1}}function m(T){r.value=T;const C=T.allowed_hosts;let M="default";C==null?M="default":Array.isArray(C)&&C.length===0?M="none":Array.isArray(C)&&(M="select"),d.value={username:T.username||"",tier:T.tier||"admin",label:T.label||"",host_mode:M,allowed_hosts:Array.isArray(C)?[...C]:[],default_host:T.default_host||"",allowed_tools_str:(T.allowed_tools||[]).join(", ")}}async function _(){var T;if(r.value){o.value=!0;try{const C=y(d.value.allowed_tools_str),M=d.value.host_mode,H={username:d.value.username,tier:d.value.tier,label:d.value.label,allowed_tools:C};M==="none"?H.allowed_hosts=[]:M==="select"?H.allowed_hosts=d.value.allowed_hosts:H.allowed_hosts=null,H.default_host=d.value.default_host||"",await G.put("/api/tokens/"+encodeURIComponent(r.value.user_id),H),r.value=null,Ae.success("Token updated"),await b()}catch(C){Ae.error(((T=C.data)==null?void 0:T.error)||C.message||"Failed to update")}finally{o.value=!1}}}async function S(T){var M;if(await _s({title:"Regenerate token",message:`Regenerate token for ${T.username||T.user_id}? The old token will stop working immediately.`,confirmLabel:"Regenerate",danger:!0}))try{const H=await G.post("/api/tokens/"+encodeURIComponent(T.user_id)+"/regenerate");l.value=H.token,Ae.success("Token regenerated")}catch(H){Ae.error(((M=H.data)==null?void 0:M.error)||H.message||"Failed to regenerate")}}async function g(T){var M;if(await _s({title:"Delete token",message:`Delete token for ${T.username||T.user_id}? This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await G.del("/api/tokens/"+encodeURIComponent(T.user_id)),Ae.success("Token deleted"),await b()}catch(H){Ae.error(((M=H.data)==null?void 0:M.error)||H.message||"Failed to delete")}}async function w(){if(l.value)try{await navigator.clipboard.writeText(l.value),Ae.success("Copied to clipboard")}catch{Ae.error("Copy failed — select and copy manually")}}return We(b),{loading:e,error:t,tokens:s,availableHosts:n,showCreate:a,creating:i,newToken:l,editing:r,saving:o,createForm:c,editForm:d,createDefaultHostOptions:u,editDefaultHostOptions:f,fetchData:b,tierBadge:p,toggleCreateHost:E,toggleEditHost:I,createToken:x,startEdit:m,saveEdit:_,confirmRegenerate:S,confirmDelete:g,copyToken:w}}},ew=Object.freeze(["enabled","model","reasoning_effort","agent_reasoning_effort","agent_model"]),tw=Object.freeze(["request_timeout_seconds","stream_stall_timeout_seconds","retry","connection_pool","context_compression","context_budget_overrides","context_utilization"]),sw=Object.freeze(["enabled","base_url","model","max_tokens"]),nw=Object.freeze(["enabled","model","max_tokens"]);function Pr(e,t){return Object.fromEntries(t.map(s=>[s,e[s]]))}function Fu(e){return Pr(e,ew)}function $u(e){return Pr(e,tw)}function aw(e,{includeApiKey:t=!1}={}){const s=Pr(e,sw);return t&&(s.api_key=e.api_key),s}function iw(e){return{timeout:e.timeout}}function lw(e,{includeApiKey:t=!1}={}){const s=Pr(e,nw);return t&&(s.api_key=e.api_key),s}function rw(e){return{timeout:e.timeout}}function kl(e,t=500){let s=null;const n=(...a)=>{s&&clearTimeout(s),s=setTimeout(()=>{s=null,e(...a)},t)};return n.pending=()=>s!==null,n.cancel=()=>{s&&(clearTimeout(s),s=null)},n}const ow={template:`
@@ -4870,6 +4870,12 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
+
+ Effective context + {{ formatCount(activeContextBudget?.effective?.effective_budget) }} tokens + {{ activeContextBudget?.provenance || 'unavailable' }} + Expires {{ formatExpiry(activeContextBudget.clamp_expires_at) }} +

The Auxiliary Model runs the background jobs (compaction, reflection, consolidation, @@ -4884,7 +4890,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config

Advanced Settings - Transport, retries, connection pool, and context compression + Transport, retries, and model-aware context policy
@@ -4927,7 +4933,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
Context compressionLong-conversation compaction

- Saved values need a restart. This process still uses compression {{ llmStatus.codex.effective_context_compression?.enabled ? 'on' : 'off' }}, {{ Number(llmStatus.codex.effective_context_compression?.max_context_chars || 0).toLocaleString() }} characters, and {{ llmStatus.codex.effective_context_compression?.keep_recent_iterations }} recent iterations. + Saved values need a restart. This process still uses compression {{ llmStatus.codex.effective_context_compression?.enabled ? 'on' : 'off' }}, {{ formatContextCeiling(llmStatus.codex.effective_context_compression?.max_context_chars) }}, and {{ llmStatus.codex.effective_context_compression?.keep_recent_iterations }} recent iterations.

Saved values match this process. Future changes take effect after restart. @@ -4943,8 +4949,90 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config

+
+
+
+ Context budgets + Capability, working-set policy, and temporary evidence +
+ +
+

+ Overrides describe usable input capability. Utilization is the working-set policy applied to larger models; budgets at or below 272,000 tokens keep legacy behavior. Learned clamps are temporary evidence from successful overflow recovery, not operator policy. +

+
+ Loading context budgets… +
+
+ {{ contextWindowsError }} + +
+ +
-

Transport and retry changes apply to the primary client now. An existing auxiliary client keeps the transport and retry settings captured when it was built until it is rebuilt. The primary client’s connection pool and context compression are saved for the next restart.

+

Transport and retry changes apply to the primary client now. Context budgets and utilization apply to the next logical generation. An existing auxiliary client keeps the transport and retry settings captured when it was built until it is rebuilt. Connection-pool and context-compression changes are saved for the next restart.

@@ -5192,7 +5280,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h(!0),t=h(null),s=h("codex"),n=h({enabled:!1,model:"gpt-5.5",reasoning_effort:"medium",agent_reasoning_effort:"",agent_model:"",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:75e4,keep_recent_iterations:30}}),a=["gpt-5.6-sol","gpt-5.6-terra","gpt-5.6-luna","gpt-5.5"],i=J(()=>{const ee=n.value.model;return ee&&!a.includes(ee)?[ee,...a]:a}),l=J(()=>{const ee=n.value.agent_model;return ee&&ee!=="auto"&&!a.includes(ee)?[ee,...a]:a}),r=["gpt-5.5","gpt-5.4","gpt-5.4-mini"],o=J(()=>!r.includes(n.value.model)&&!(r.includes(n.value.agent_model)&&n.value.agent_reasoning_effort==="")),c=J(()=>{const ee=n.value.agent_model;return ee==="auto"?!0:!r.includes(ee||n.value.model)}),d=J(()=>{const ee=n.value.agent_reasoning_effort;return ee==="auto"?!1:(ee||n.value.reasoning_effort)==="max"}),u=ee=>r.includes(ee)&&(n.value.reasoning_effort==="max"||n.value.agent_model===""&&d.value),f=ee=>r.includes(ee)&&d.value,p=h({enabled:!1,model:"gpt-5.6-luna"}),b=h({unavailable_reason:null}),y=J(()=>{const ee=p.value.model;return ee&&!a.includes(ee)?[ee,...a]:a});function A(ee){const Se=ee.target.value;p.value.enabled=Se!=="",Se!==""&&(p.value.model=Se),Ce()}const O=h(!1),x=h({codex:!1,ollama:!1,kimi:!1}),m=h({enabled:!1,base_url:"",model:"",api_key:"",max_tokens:4096,timeout:300}),_=h({enabled:!1,api_key:"",model:"",max_tokens:4096,timeout:300}),S=h(!1),g=h(!1),w=h(!1),T=h(!1),C=h(!1),M=h(!1),B=h(!1),$=h({configured:!1}),I=h([]),j=h(""),Y=h(!1),H=h(!1),N=h({configured:!1}),L=h([]),Z=h(""),xe=h(!1),_e=h(!1),ae=h(!0),fe=h(""),P=h({configured:!1,accounts:[]}),se=h(null),ke=h(null),V=h(""),ce=h(null),de=h(!1),ve=h(null),me=h(null),He=h("");let k=null;function E(ee,Se="success"){Te(ee,Se==="error"?"error":"success")}function U(ee){if(!ee)return"?";const Se=ee/(1024*1024*1024);return Se>=1?Se.toFixed(1)+" GB":(ee/(1024*1024)).toFixed(0)+" MB"}async function X(){e.value=!0,await Promise.all([q(),Q(),ue(),ie()]),e.value=!1}async function q({preserveBasic:ee=!1,preserveAdvanced:Se=!1}={}){try{const De=await K.get("/api/llm/status");t.value=De,s.value=De.active_provider||"codex",De.codex&&!ye.pending()&&(ee||(n.value.enabled=De.codex.enabled,n.value.model=De.codex.model||"gpt-5.5",n.value.reasoning_effort=De.codex.reasoning_effort||"medium",n.value.agent_reasoning_effort=De.codex.agent_reasoning_effort||"",n.value.agent_model=De.codex.agent_model||""),Se||(n.value.request_timeout_seconds=De.codex.request_timeout_seconds??n.value.request_timeout_seconds,n.value.stream_stall_timeout_seconds=De.codex.stream_stall_timeout_seconds??n.value.stream_stall_timeout_seconds,n.value.retry={...n.value.retry,...De.codex.retry||{}},n.value.connection_pool={...n.value.connection_pool,...De.codex.connection_pool||{}},n.value.context_compression={...n.value.context_compression,...De.codex.context_compression||{}})),De.ollama&&!Re.pending()&&(ee||(m.value.enabled=De.ollama.enabled,m.value.base_url=De.ollama.base_url||"",m.value.model=De.ollama.model||"",m.value.max_tokens=De.ollama.max_tokens||4096),Se||(m.value.timeout=De.ollama.timeout??m.value.timeout)),De.kimi&&!Be.pending()&&(ee||(_.value.enabled=De.kimi.enabled,_.value.model=De.kimi.model||"",_.value.max_tokens=De.kimi.max_tokens||4096),Se||(_.value.timeout=De.kimi.timeout??_.value.timeout)),De.auxiliary&&(b.value=De.auxiliary,Ce.pending()||(p.value.enabled=De.auxiliary.enabled,p.value.model=De.auxiliary.model||"gpt-5.6-luna"))}catch{t.value={active_provider:"codex",codex:{configured:!1},ollama:{configured:!1},kimi:{configured:!1}}}}async function Q(){try{if($.value=await K.get("/api/ollama/status"),$.value.model&&(j.value=$.value.model),$.value.configured)try{const ee=await K.get("/api/ollama/models");I.value=ee.models||[]}catch{I.value=[]}else if(m.value.base_url)try{const ee=await K.post("/api/ollama/probe-models",{base_url:m.value.base_url});I.value=ee.models||[]}catch{I.value=[]}}catch{$.value={configured:!1}}}async function ie(){ae.value=!0,fe.value="";try{P.value=await K.get("/api/codex/status")}catch(ee){fe.value=ee.message||"Failed to fetch Codex status"}finally{ae.value=!1}}async function re(){const ee=t.value?t.value.active_provider:"codex";B.value=!0;try{const Se=await K.post("/api/llm/switch",{provider:s.value});Se.error?(s.value=ee,E(Se.error,"error")):(E("Switched to "+s.value+" ("+Se.model+")"),await X())}catch(Se){s.value=ee,E(Se.message||"Switch failed","error")}finally{B.value=!1}}async function le(){Y.value=!0;try{const ee=await K.post("/api/ollama/reload");E(ee.configured?"Ollama reloaded":ee.reason||"Ollama not configured",ee.configured?"success":"error"),await X()}catch(ee){E(ee.message||"Reload failed","error")}finally{Y.value=!1}}async function te(){H.value=!0;try{await K.post("/api/ollama/model",{model:j.value}),E("Model set to "+j.value),await X()}catch(ee){E(ee.message||"Failed","error")}finally{H.value=!1}}async function be(){const ee=m.value.base_url;if(!ee){E("Enter a base URL first","error");return}M.value=!0;try{const Se=await K.post("/api/ollama/probe-models",{base_url:ee});I.value=Se.models||[],I.value.length?(E(I.value.length+" model(s) found"),!m.value.model&&I.value.length&&(m.value.model=I.value[0].name)):E("No models found at "+ee,"error")}catch(Se){E(Se.message||"Could not reach Ollama","error")}finally{M.value=!1}}async function ue(){try{if(N.value=await K.get("/api/kimi/status"),N.value.model&&(Z.value=N.value.model),N.value.configured)try{const ee=await K.get("/api/kimi/models");L.value=ee.models||[]}catch{L.value=[]}}catch{N.value={configured:!1}}}async function he(){xe.value=!0;try{const ee=await K.post("/api/kimi/reload");E(ee.configured?"Kimi reloaded":ee.reason||"Kimi not configured",ee.configured?"success":"error"),await X()}catch(ee){E(ee.message||"Reload failed","error")}finally{xe.value=!1}}async function we(){_e.value=!0;try{await K.post("/api/kimi/model",{model:Z.value}),E("Model set to "+Z.value),await X()}catch(ee){E(ee.message||"Failed","error")}finally{_e.value=!1}}async function Ee(){if(w.value){ye();return}w.value=!0;const ee=Fu(n.value);try{await K.put("/api/llm/codex/config",ee),E("Codex config saved"),await Promise.all([q({preserveBasic:!0,preserveAdvanced:!0}),ie()])}catch(Se){E(Se.message||"Failed","error");const De=JSON.stringify(Fu(n.value))!==JSON.stringify(ee);await Promise.all([q({preserveBasic:De,preserveAdvanced:!0}),ie()])}finally{w.value=!1}}async function Le(){if(w.value)return;w.value=!0;const ee=$u(n.value);try{await K.put("/api/llm/codex/config",ee),E("Codex advanced settings saved"),await Promise.all([q({preserveBasic:!0,preserveAdvanced:!0}),ie()])}catch(Se){E(Se.message||"Failed","error");const De=JSON.stringify($u(n.value))!==JSON.stringify(ee);await Promise.all([q({preserveBasic:!0,preserveAdvanced:De}),ie()])}finally{w.value=!1}}async function Oe(){if(T.value){Re();return}T.value=!0;try{const ee=S.value?m.value.api_key:null,Se=aw(m.value,{includeApiKey:ee!==null});await K.put("/api/llm/ollama/config",Se),E("Ollama config saved"),ee!==null&&m.value.api_key===ee&&(m.value.api_key="",S.value=!1),await Promise.all([q({preserveBasic:!0,preserveAdvanced:!0}),Q()])}catch(ee){E(ee.message||"Failed","error")}finally{T.value=!1}}async function Fe(){if(!T.value){T.value=!0;try{await K.put("/api/llm/ollama/config",iw(m.value)),E("Ollama timeout saved"),await Promise.all([q({preserveBasic:!0,preserveAdvanced:!0}),Q()])}catch(ee){E(ee.message||"Failed","error")}finally{T.value=!1}}}async function Ve(){if(C.value){Be();return}C.value=!0;try{const ee=g.value?_.value.api_key:null,Se=lw(_.value,{includeApiKey:ee!==null});await K.put("/api/llm/kimi/config",Se),E("Kimi config saved"),ee!==null&&_.value.api_key===ee&&(_.value.api_key="",g.value=!1),await Promise.all([q({preserveBasic:!0,preserveAdvanced:!0}),ue()])}catch(ee){E(ee.message||"Failed","error")}finally{C.value=!1}}async function lt(){if(!C.value){C.value=!0;try{await K.put("/api/llm/kimi/config",rw(_.value)),E("Kimi timeout saved"),await Promise.all([q({preserveBasic:!0,preserveAdvanced:!0}),ue()])}catch(ee){E(ee.message||"Failed","error")}finally{C.value=!1}}}async function G(){if(O.value){Ce();return}O.value=!0;try{await K.put("/api/llm/auxiliary/config",p.value),E("Auxiliary config saved"),await q()}catch(ee){E(ee.message||"Failed","error"),await q()}finally{O.value=!1}}const ye=_l(Ee),Ce=_l(G),Re=_l(Oe),Be=_l(Ve),ze=()=>(ye.cancel(),Ee()),pt=()=>(Re.cancel(),Oe()),ns=()=>(Be.cancel(),Ve()),As=()=>Le(),Qs=()=>Fe(),$s=()=>lt();async function Rs(ee){try{await K.post("/api/codex/account/"+ee+"/activate"),E("Active account switched"),await ie()}catch(Se){E(Se.message||"Failed","error")}}async function Nt(ee){se.value=ee;try{await K.post("/api/codex/account/"+ee+"/refresh"),E("Token refreshed"),await ie()}catch(Se){E(Se.message||"Refresh failed","error")}finally{se.value=null}}function us(ee,Se){ke.value=ee,V.value=Se||""}async function Us(ee){try{await K.put("/api/codex/account/"+ee+"/label",{label:V.value}),E("Label updated"),ke.value=null,await ie()}catch(Se){E(Se.message||"Failed","error")}}async function Xs(ee,Se){if(await gs({title:"Delete Codex account",message:`Delete ${Se||"account #"+(ee+1)}? The pool will reload without it.`,confirmLabel:"Delete",danger:!0}))try{await K.del("/api/codex/account/"+ee),E("Deleted. Pool reloaded."),await ie()}catch(Bs){E(Bs.message||"Failed","error")}}async function Hn(){de.value=!0;try{const ee=await K.post("/api/codex/device-code");ve.value=ee,ce.value="pending",en(ee)}catch(ee){E(ee.message||"Failed","error")}finally{de.value=!1}}async function en(ee){k={cancelled:!1};const Se=k;try{const De=await K.post("/api/codex/device-poll",{device_auth_id:ee.device_auth_id,user_code:ee.user_code,interval:ee.interval});if(Se.cancelled)return;me.value=De,ce.value="success",await X()}catch(De){if(Se.cancelled)return;He.value=De.message||"Device login failed",ce.value="error"}}function Kt(){k&&(k.cancelled=!0),ce.value=null,ve.value=null}return We(X),xt(()=>{k&&(k.cancelled=!0),ye.cancel(),Ce.cancel(),Re.cancel(),Be.cancel()}),{loading:e,llmStatus:t,selectedProvider:s,switching:B,advancedOpen:x,codexForm:n,codexModelOptions:i,codexAgentModelOptions:l,mainMaxAllowed:o,agentMaxAllowed:c,mainModelOptionDisabled:u,agentModelOptionDisabled:f,auxForm:p,auxData:b,auxModelOptions:y,onAuxModelChange:A,savingAux:O,saveAuxConfigDebounced:Ce,ollamaForm:m,kimiForm:_,savingCodex:w,savingOllama:T,savingKimi:C,probingOllama:M,ollamaKeyDirty:S,kimiKeyDirty:g,ollamaStatus:$,ollamaModels:I,ollamaSelectedModel:j,reloading:Y,settingModel:H,kimiStatus:N,kimiModels:L,kimiSelectedModel:Z,reloadingKimi:xe,settingKimiModel:_e,codexLoading:ae,codexError:fe,codexData:P,refreshing:se,editingLabel:ke,labelValue:V,deviceState:ce,deviceLoading:de,deviceInfo:ve,deviceResult:me,deviceError:He,fetchAll:X,switchProvider:re,reloadOllama:le,setOllamaModel:te,reloadKimi:he,setKimiModel:we,probeOllamaModels:be,saveCodexConfig:Ee,saveOllamaConfig:Oe,saveKimiConfig:Ve,saveCodexAdvancedConfig:Le,saveOllamaAdvancedConfig:Fe,saveKimiAdvancedConfig:lt,saveCodexConfigDebounced:ye,saveOllamaConfigDebounced:Re,saveKimiConfigDebounced:Be,saveCodexConfigNow:ze,saveOllamaConfigNow:pt,saveKimiConfigNow:ns,saveCodexAdvancedConfigNow:As,saveOllamaAdvancedConfigNow:Qs,saveKimiAdvancedConfigNow:$s,activateAccount:Rs,refreshAccount:Nt,startEditLabel:us,saveLabel:Us,deleteAccount:Xs,startDeviceLogin:Hn,cancelDeviceLogin:Kt,formatSize:U}}},Uu={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 cw(e){return Uu[e]||Uu[(e||"").toLowerCase()]||"text-gray-400"}const dw={template:` + `,setup(){const e=h(!0),t=h(null),s=h("codex"),n=h({enabled:!1,model:"gpt-5.6-sol",reasoning_effort:"xhigh",agent_reasoning_effort:"auto",agent_model:"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}),a=["gpt-5.6-sol","gpt-5.6-terra","gpt-5.6-luna","gpt-5.5"],i=J(()=>{const z=n.value.model;return z&&!a.includes(z)?[z,...a]:a}),l=J(()=>{const z=n.value.agent_model;return z&&z!=="auto"&&!a.includes(z)?[z,...a]:a}),r=["gpt-5.5","gpt-5.4","gpt-5.4-mini"],o=J(()=>!r.includes(n.value.model)&&!(r.includes(n.value.agent_model)&&n.value.agent_reasoning_effort==="")),c=J(()=>{const z=n.value.agent_model;return z==="auto"?!0:!r.includes(z||n.value.model)}),d=J(()=>{const z=n.value.agent_reasoning_effort;return z==="auto"?!1:(z||n.value.reasoning_effort)==="max"}),u=z=>r.includes(z)&&(n.value.reasoning_effort==="max"||n.value.agent_model===""&&d.value),f=z=>r.includes(z)&&d.value,p=h({enabled:!1,model:"gpt-5.6-luna"}),b=h({unavailable_reason:null}),y=J(()=>{const z=p.value.model;return z&&!a.includes(z)?[z,...a]:a});function E(z){const pe=z.target.value;p.value.enabled=pe!=="",pe!==""&&(p.value.model=pe),rs()}const I=h(!1),x=h({codex:!1,ollama:!1,kimi:!1}),m=h(null),_=h(!1),S=h(""),g=h(null),w=h(!1);let T=0;const C=J(()=>{var z;return Object.entries(((z=m.value)==null?void 0:z.models)||{}).map(([pe,_e])=>{var Nt,Cs,jn;return{model:pe,floor:_e.floor,override:_e.override,effectiveBudget:(Nt=_e.effective)==null?void 0:Nt.effective_budget,configuredPrimaryChars:(Cs=_e.configured)==null?void 0:Cs.primary_chars,primaryChars:(jn=_e.effective)==null?void 0:jn.primary_chars,provenance:_e.provenance,clampExpiresAt:_e.clamp_expires_at}})}),M=J(()=>{var z;return((z=m.value)==null?void 0:z.clamps)||[]}),H=J(()=>{var z,pe;return((pe=(z=m.value)==null?void 0:z.models)==null?void 0:pe[n.value.model])||null}),P=h({enabled:!1,base_url:"",model:"",api_key:"",max_tokens:4096,timeout:300}),R=h({enabled:!1,api_key:"",model:"",max_tokens:4096,timeout:300}),j=h(!1),Q=h(!1),U=h(!1),O=h(!1),N=h(!1),Y=h(!1),we=h(!1),ke=h({configured:!1}),ie=h([]),he=h(""),F=h(!1),se=h(!1),Se=h({configured:!1}),V=h([]),de=h(""),ce=h(!1),ye=h(!1),ge=h(!0),He=h(""),k=h({configured:!1,accounts:[]}),L=h(null),$=h(null),ee=h(""),Z=h(null),X=h(!1),ue=h(null),oe=h(null),le=h("");let te=null;function ne(z,pe="success"){Ae(z,pe==="error"?"error":"success")}function fe(z){if(!z)return"?";const pe=z/(1024*1024*1024);return pe>=1?pe.toFixed(1)+" GB":(z/(1024*1024)).toFixed(0)+" MB"}function ve(z){return Number.isFinite(Number(z))?Number(z).toLocaleString():"—"}function Te(z){return z==null?"automatic (model-derived)":Number(z).toLocaleString()+" characters"}function Oe(z){const pe=new Date(z);return Number.isNaN(pe.getTime())?"unknown":pe.toLocaleString([],{dateStyle:"medium",timeStyle:"short"})}function Le(z){return typeof z=="string"&&z.length>12?z.slice(0,8)+"…"+z.slice(-4):z}function De(z){return z==="temporary learned clamp"?"is-clamp":z==="override"?"is-override":"is-built-in"}function Be(z){const pe=n.value.context_budget_overrides[z.model];return z.floor!=null&&Number.isFinite(Number(pe))&&Number(pe)>z.floor}function qe(z,pe){const _e={...n.value.context_budget_overrides};pe.target.value===""?delete _e[z]:_e[z]=Number(pe.target.value),n.value.context_budget_overrides=_e,w.value=!0}function ct(z){n.value.context_utilization=z.target.value===""?"":Number(z.target.value),w.value=!0}function K(z){const pe={...n.value.context_budget_overrides};delete pe[z],n.value.context_budget_overrides=pe,w.value=!0}async function xe(){e.value=!0,await Promise.all([Ce(),Ve(),Ss(),Pe(),Re()]),e.value=!1}async function Ce({preserveBasic:z=!1,preserveAdvanced:pe=!1}={}){try{const _e=await G.get("/api/llm/status");t.value=_e,s.value=_e.active_provider||"codex",_e.codex&&!Ct.pending()&&(z||(n.value.enabled=_e.codex.enabled,n.value.model=_e.codex.model||"gpt-5.6-sol",n.value.reasoning_effort=_e.codex.reasoning_effort||"medium",n.value.agent_reasoning_effort=_e.codex.agent_reasoning_effort||"",n.value.agent_model=_e.codex.agent_model||""),pe||(n.value.request_timeout_seconds=_e.codex.request_timeout_seconds??n.value.request_timeout_seconds,n.value.stream_stall_timeout_seconds=_e.codex.stream_stall_timeout_seconds??n.value.stream_stall_timeout_seconds,n.value.retry={...n.value.retry,..._e.codex.retry||{}},n.value.connection_pool={...n.value.connection_pool,..._e.codex.connection_pool||{}},n.value.context_compression={...n.value.context_compression,..._e.codex.context_compression||{}},!w.value&&!U.value&&(n.value.context_budget_overrides={..._e.codex.context_budget_overrides||{}},n.value.context_utilization=_e.codex.context_utilization??n.value.context_utilization))),_e.ollama&&!os.pending()&&(z||(P.value.enabled=_e.ollama.enabled,P.value.base_url=_e.ollama.base_url||"",P.value.model=_e.ollama.model||"",P.value.max_tokens=_e.ollama.max_tokens||4096),pe||(P.value.timeout=_e.ollama.timeout??P.value.timeout)),_e.kimi&&!Ye.pending()&&(z||(R.value.enabled=_e.kimi.enabled,R.value.model=_e.kimi.model||"",R.value.max_tokens=_e.kimi.max_tokens||4096),pe||(R.value.timeout=_e.kimi.timeout??R.value.timeout)),_e.auxiliary&&(b.value=_e.auxiliary,rs.pending()||(p.value.enabled=_e.auxiliary.enabled,p.value.model=_e.auxiliary.model||"gpt-5.6-luna"))}catch{t.value={active_provider:"codex",codex:{configured:!1},ollama:{configured:!1},kimi:{configured:!1}}}}async function Re(){const z=++T;_.value=!0,S.value="";try{const pe=await G.get("/api/context/windows");if(z!==T)return;m.value=pe,!U.value&&!w.value&&(n.value.context_budget_overrides=Object.fromEntries(Object.entries(pe.models||{}).filter(([,_e])=>_e.override!=null).map(([_e,Nt])=>[_e,Nt.override])),n.value.context_utilization=pe.utilization??n.value.context_utilization)}catch(pe){z===T&&(S.value=pe.message||"Failed to load context budgets")}finally{z===T&&(_.value=!1)}}async function Ve(){try{if(ke.value=await G.get("/api/ollama/status"),ke.value.model&&(he.value=ke.value.model),ke.value.configured)try{const z=await G.get("/api/ollama/models");ie.value=z.models||[]}catch{ie.value=[]}else if(P.value.base_url)try{const z=await G.post("/api/ollama/probe-models",{base_url:P.value.base_url});ie.value=z.models||[]}catch{ie.value=[]}}catch{ke.value={configured:!1}}}async function Pe(){ge.value=!0,He.value="";try{k.value=await G.get("/api/codex/status")}catch(z){He.value=z.message||"Failed to fetch Codex status"}finally{ge.value=!1}}async function pt(){const z=t.value?t.value.active_provider:"codex";we.value=!0;try{const pe=await G.post("/api/llm/switch",{provider:s.value});pe.error?(s.value=z,ne(pe.error,"error")):(ne("Switched to "+s.value+" ("+pe.model+")"),await xe())}catch(pe){s.value=z,ne(pe.message||"Switch failed","error")}finally{we.value=!1}}async function ls(){F.value=!0;try{const z=await G.post("/api/ollama/reload");ne(z.configured?"Ollama reloaded":z.reason||"Ollama not configured",z.configured?"success":"error"),await xe()}catch(z){ne(z.message||"Reload failed","error")}finally{F.value=!1}}async function Ps(){se.value=!0;try{await G.post("/api/ollama/model",{model:he.value}),ne("Model set to "+he.value),await xe()}catch(z){ne(z.message||"Failed","error")}finally{se.value=!1}}async function nn(){const z=P.value.base_url;if(!z){ne("Enter a base URL first","error");return}Y.value=!0;try{const pe=await G.post("/api/ollama/probe-models",{base_url:z});ie.value=pe.models||[],ie.value.length?(ne(ie.value.length+" model(s) found"),!P.value.model&&ie.value.length&&(P.value.model=ie.value[0].name)):ne("No models found at "+z,"error")}catch(pe){ne(pe.message||"Could not reach Ollama","error")}finally{Y.value=!1}}async function Ss(){try{if(Se.value=await G.get("/api/kimi/status"),Se.value.model&&(de.value=Se.value.model),Se.value.configured)try{const z=await G.get("/api/kimi/models");V.value=z.models||[]}catch{V.value=[]}}catch{Se.value={configured:!1}}}async function Fs(){ce.value=!0;try{const z=await G.post("/api/kimi/reload");ne(z.configured?"Kimi reloaded":z.reason||"Kimi not configured",z.configured?"success":"error"),await xe()}catch(z){ne(z.message||"Reload failed","error")}finally{ce.value=!1}}async function Mt(){ye.value=!0;try{await G.post("/api/kimi/model",{model:de.value}),ne("Model set to "+de.value),await xe()}catch(z){ne(z.message||"Failed","error")}finally{ye.value=!1}}async function Yt(){if(U.value){Ct();return}U.value=!0;const z=Fu(n.value);try{await G.put("/api/llm/codex/config",z),ne("Codex config saved"),await Promise.all([Ce({preserveBasic:!0,preserveAdvanced:!0}),Pe()])}catch(pe){ne(pe.message||"Failed","error");const _e=JSON.stringify(Fu(n.value))!==JSON.stringify(z);await Promise.all([Ce({preserveBasic:_e,preserveAdvanced:!0}),Pe()])}finally{U.value=!1}}async function $s(){if(U.value)return;U.value=!0;const z=$u(n.value);try{await G.put("/api/llm/codex/config",z),JSON.stringify({context_budget_overrides:n.value.context_budget_overrides,context_utilization:n.value.context_utilization})===JSON.stringify({context_budget_overrides:z.context_budget_overrides,context_utilization:z.context_utilization})&&(w.value=!1),ne("Codex advanced settings saved"),await Promise.all([Ce({preserveBasic:!0,preserveAdvanced:!0}),Pe(),Re()])}catch(pe){ne(pe.message||"Failed","error");const _e=JSON.stringify($u(n.value))!==JSON.stringify(z);await Promise.all([Ce({preserveBasic:!0,preserveAdvanced:_e}),Pe(),Re()])}finally{U.value=!1}}async function Bs(){if(O.value){os();return}O.value=!0;try{const z=j.value?P.value.api_key:null,pe=aw(P.value,{includeApiKey:z!==null});await G.put("/api/llm/ollama/config",pe),ne("Ollama config saved"),z!==null&&P.value.api_key===z&&(P.value.api_key="",j.value=!1),await Promise.all([Ce({preserveBasic:!0,preserveAdvanced:!0}),Ve()])}catch(z){ne(z.message||"Failed","error")}finally{O.value=!1}}async function An(){if(!O.value){O.value=!0;try{await G.put("/api/llm/ollama/config",iw(P.value)),ne("Ollama timeout saved"),await Promise.all([Ce({preserveBasic:!0,preserveAdvanced:!0}),Ve()])}catch(z){ne(z.message||"Failed","error")}finally{O.value=!1}}}async function Us(){if(N.value){Ye();return}N.value=!0;try{const z=Q.value?R.value.api_key:null,pe=lw(R.value,{includeApiKey:z!==null});await G.put("/api/llm/kimi/config",pe),ne("Kimi config saved"),z!==null&&R.value.api_key===z&&(R.value.api_key="",Q.value=!1),await Promise.all([Ce({preserveBasic:!0,preserveAdvanced:!0}),Ss()])}catch(z){ne(z.message||"Failed","error")}finally{N.value=!1}}async function zt(){if(!N.value){N.value=!0;try{await G.put("/api/llm/kimi/config",rw(R.value)),ne("Kimi timeout saved"),await Promise.all([Ce({preserveBasic:!0,preserveAdvanced:!0}),Ss()])}catch(z){ne(z.message||"Failed","error")}finally{N.value=!1}}}async function Vn(){if(I.value){rs();return}I.value=!0;try{await G.put("/api/llm/auxiliary/config",p.value),ne("Auxiliary config saved"),await Ce()}catch(z){ne(z.message||"Failed","error"),await Ce()}finally{I.value=!1}}const Ct=kl(Yt),rs=kl(Vn),os=kl(Bs),Ye=kl(Us),gs=()=>(Ct.cancel(),Yt()),q=()=>(os.cancel(),Bs()),re=()=>(Ye.cancel(),Us()),Ee=()=>$s(),Ze=()=>An(),lt=()=>zt();async function Ot(z){const pe=z.account_key+":"+z.model;g.value=pe;try{const _e=await G.post("/api/context/windows/clear",{account_key:z.account_key,model:z.model});ne(_e.cleared?"Temporary clamp cleared":"Clamp was already inactive"),await Re()}catch(_e){ne(_e.message||"Failed to clear clamp","error"),await Re()}finally{g.value=null}}async function Pt(z){try{await G.post("/api/codex/account/"+z+"/activate"),ne("Active account switched"),await Pe()}catch(pe){ne(pe.message||"Failed","error")}}async function ma(z){L.value=z;try{await G.post("/api/codex/account/"+z+"/refresh"),ne("Token refreshed"),await Pe()}catch(pe){ne(pe.message||"Refresh failed","error")}finally{L.value=null}}function Ts(z,pe){$.value=z,ee.value=pe||""}async function ga(z){try{await G.put("/api/codex/account/"+z+"/label",{label:ee.value}),ne("Label updated"),$.value=null,await Pe()}catch(pe){ne(pe.message||"Failed","error")}}async function ni(z,pe){if(await _s({title:"Delete Codex account",message:`Delete ${pe||"account #"+(z+1)}? The pool will reload without it.`,confirmLabel:"Delete",danger:!0}))try{await G.del("/api/codex/account/"+z),ne("Deleted. Pool reloaded."),await Pe()}catch(Nt){ne(Nt.message||"Failed","error")}}async function va(){X.value=!0;try{const z=await G.post("/api/codex/device-code");ue.value=z,Z.value="pending",ba(z)}catch(z){ne(z.message||"Failed","error")}finally{X.value=!1}}async function ba(z){te={cancelled:!1};const pe=te;try{const _e=await G.post("/api/codex/device-poll",{device_auth_id:z.device_auth_id,user_code:z.user_code,interval:z.interval});if(pe.cancelled)return;oe.value=_e,Z.value="success",await xe()}catch(_e){if(pe.cancelled)return;le.value=_e.message||"Device login failed",Z.value="error"}}function Gs(){te&&(te.cancelled=!0),Z.value=null,ue.value=null}return We(xe),xt(()=>{te&&(te.cancelled=!0),Ct.cancel(),rs.cancel(),os.cancel(),Ye.cancel()}),{loading:e,llmStatus:t,selectedProvider:s,switching:we,advancedOpen:x,codexForm:n,codexModelOptions:i,codexAgentModelOptions:l,mainMaxAllowed:o,agentMaxAllowed:c,mainModelOptionDisabled:u,agentModelOptionDisabled:f,auxForm:p,auxData:b,auxModelOptions:y,onAuxModelChange:E,savingAux:I,saveAuxConfigDebounced:rs,ollamaForm:P,kimiForm:R,savingCodex:U,savingOllama:O,savingKimi:N,probingOllama:Y,ollamaKeyDirty:j,kimiKeyDirty:Q,ollamaStatus:ke,ollamaModels:ie,ollamaSelectedModel:he,reloading:F,settingModel:se,kimiStatus:Se,kimiModels:V,kimiSelectedModel:de,reloadingKimi:ce,settingKimiModel:ye,codexLoading:ge,codexError:He,codexData:k,refreshing:L,editingLabel:$,labelValue:ee,contextWindows:m,contextWindowsLoading:_,contextWindowsError:S,contextBudgetRows:C,activeClampRows:M,activeContextBudget:H,clearingClamp:g,contextPolicyDirty:w,deviceState:Z,deviceLoading:X,deviceInfo:ue,deviceResult:oe,deviceError:le,fetchAll:xe,switchProvider:pt,reloadOllama:ls,setOllamaModel:Ps,reloadKimi:Fs,setKimiModel:Mt,probeOllamaModels:nn,saveCodexConfig:Yt,saveOllamaConfig:Bs,saveKimiConfig:Us,saveCodexAdvancedConfig:$s,saveOllamaAdvancedConfig:An,saveKimiAdvancedConfig:zt,saveCodexConfigDebounced:Ct,saveOllamaConfigDebounced:os,saveKimiConfigDebounced:Ye,saveCodexConfigNow:gs,saveOllamaConfigNow:q,saveKimiConfigNow:re,saveCodexAdvancedConfigNow:Ee,saveOllamaAdvancedConfigNow:Ze,saveKimiAdvancedConfigNow:lt,activateAccount:Pt,refreshAccount:ma,startEditLabel:Ts,saveLabel:ga,deleteAccount:ni,startDeviceLogin:va,cancelDeviceLogin:Gs,formatSize:fe,fetchContextWindows:Re,clearContextClamp:Ot,setContextOverride:qe,setContextUtilization:ct,resetContextOverride:K,overrideAboveFloor:Be,formatCount:ve,formatContextCeiling:Te,formatExpiry:Oe,shortAccountKey:Le,provenanceClass:De}}},Bu={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 cw(e){return Bu[e]||Bu[(e||"").toLowerCase()]||"text-gray-400"}const dw={template:`
@@ -5360,7 +5448,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h(!0),t=h({}),s=h([]),n=h({}),a=h({}),i=h(null),l=h(null),r=h(null),o=h(null),c=h(null),d=J(()=>{var g;return Object.values(((g=i.value)==null?void 0:g.totals)||{}).reduce((w,T)=>w+Number(T||0),0)}),u=h(""),f=h(0),p=h([]),b=J(()=>p.value.map(g=>`${g.label} (${g.path}${g.reason?`: ${g.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 A=null;async function O(){var M;const g=await Promise.allSettled(y.map(B=>K.get(B.path))),w=B=>g[B].status==="fulfilled"?g[B].value:null;t.value=w(0)||{};const T=w(1);s.value=Array.isArray(T)?T:T&&T.subsystems||[],n.value=w(2)||{},a.value=w(3)||{},i.value=w(4),l.value=w(5),r.value=w(6),o.value=w(7),c.value=w(8);const C=g.filter(B=>B.status==="rejected");if(p.value=g.flatMap((B,$)=>{var I;return B.status==="rejected"?[{...y[$],reason:((I=B.reason)==null?void 0:I.message)||"request failed"}]:[]}),f.value=p.value.length,C.length===g.length){const B=(M=C[0])==null?void 0:M.reason;u.value=(B==null?void 0:B.message)||"Failed to load internals"}else u.value="";e.value=!1}function x(){e.value=!0,u.value="",O()}let m=!1;function _(){m||(m=!0,O(),A||(A=setInterval(O,3e4)))}function S(){m&&(m=!1,A&&(clearInterval(A),A=null))}return We(_),Cs(_),Es(S),xt(S),{loading:e,error:u,failedCount:f,failedEndpoints:p,failedEndpointSummary:b,endpoints:y,retry:x,startup:t,subsystems:s,sshPool:n,httpPool:a,riskStats:i,riskTotal:d,recoveryStats:l,compressionStats:r,freshnessStats:o,governorStats:c,statusColor:cw,formatAgeSeconds:X_}}},uw={setup(){const e=h(""),t=h(""),s=h(!1),n=h(""),a=h(!1),i=h(!1),l=h(!1),r=h(null),o=h(!1);async function c(){a.value=!0,r.value=null,o.value=!1;try{const u=await K.get("/api/update/check");e.value=u.current||"",t.value=u.latest||"",s.value=u.update_available||!1,n.value=u.changelog||"",u.error&&(r.value=u.error),o.value=!0}catch(u){r.value=u.message}finally{a.value=!1}}async function d(){if(await gs({title:"Update & restart",message:"Update Odin and restart? Active tasks will be interrupted.",confirmLabel:"Update & Restart",danger:!0})){i.value=!0,r.value=null;try{await K.post("/api/update/apply",{version:"latest"}),l.value=!0,setTimeout(()=>location.reload(),8e3)}catch(f){r.value=f.message}finally{i.value=!1}}}return We(c),{current:e,latest:t,updateAvailable:s,changelog:n,checking:a,applying:i,applied:l,error:r,checkDone:o,checkUpdate:c,applyUpdate:d}},template:` + `,setup(){const e=h(!0),t=h({}),s=h([]),n=h({}),a=h({}),i=h(null),l=h(null),r=h(null),o=h(null),c=h(null),d=J(()=>{var g;return Object.values(((g=i.value)==null?void 0:g.totals)||{}).reduce((w,T)=>w+Number(T||0),0)}),u=h(""),f=h(0),p=h([]),b=J(()=>p.value.map(g=>`${g.label} (${g.path}${g.reason?`: ${g.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 E=null;async function I(){var M;const g=await Promise.allSettled(y.map(H=>G.get(H.path))),w=H=>g[H].status==="fulfilled"?g[H].value:null;t.value=w(0)||{};const T=w(1);s.value=Array.isArray(T)?T:T&&T.subsystems||[],n.value=w(2)||{},a.value=w(3)||{},i.value=w(4),l.value=w(5),r.value=w(6),o.value=w(7),c.value=w(8);const C=g.filter(H=>H.status==="rejected");if(p.value=g.flatMap((H,P)=>{var R;return H.status==="rejected"?[{...y[P],reason:((R=H.reason)==null?void 0:R.message)||"request failed"}]:[]}),f.value=p.value.length,C.length===g.length){const H=(M=C[0])==null?void 0:M.reason;u.value=(H==null?void 0:H.message)||"Failed to load internals"}else u.value="";e.value=!1}function x(){e.value=!0,u.value="",I()}let m=!1;function _(){m||(m=!0,I(),E||(E=setInterval(I,3e4)))}function S(){m&&(m=!1,E&&(clearInterval(E),E=null))}return We(_),Ds(_),Ms(S),xt(S),{loading:e,error:u,failedCount:f,failedEndpoints:p,failedEndpointSummary:b,endpoints:y,retry:x,startup:t,subsystems:s,sshPool:n,httpPool:a,riskStats:i,riskTotal:d,recoveryStats:l,compressionStats:r,freshnessStats:o,governorStats:c,statusColor:cw,formatAgeSeconds:X_}}},uw={setup(){const e=h(""),t=h(""),s=h(!1),n=h(""),a=h(!1),i=h(!1),l=h(!1),r=h(null),o=h(!1);async function c(){a.value=!0,r.value=null,o.value=!1;try{const u=await G.get("/api/update/check");e.value=u.current||"",t.value=u.latest||"",s.value=u.update_available||!1,n.value=u.changelog||"",u.error&&(r.value=u.error),o.value=!0}catch(u){r.value=u.message}finally{a.value=!1}}async function d(){if(await _s({title:"Update & restart",message:"Update Odin and restart? Active tasks will be interrupted.",confirmLabel:"Update & Restart",danger:!0})){i.value=!0,r.value=null;try{await G.post("/api/update/apply",{version:"latest"}),l.value=!0,setTimeout(()=>location.reload(),8e3)}catch(f){r.value=f.message}finally{i.value=!1}}}return We(c),{current:e,latest:t,updateAvailable:s,changelog:n,checking:a,applying:i,applied:l,error:r,checkDone:o,checkUpdate:c,applyUpdate:d}},template:`

Updates

@@ -5415,7 +5503,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config

{{ error }}

- `},Im=[{id:"health",label:"Health",component:Ok},{id:"resources",label:"Resources",component:Nk},{id:"logs",label:"Logs",component:Pk},{id:"config",label:"Config",component:Wk},{id:"discord",label:"Discord",component:Jk},{id:"host-access",label:"Host Access",component:Qk},{id:"api-tokens",label:"API Tokens",component:Xk},{id:"llm",label:"LLM Config",component:ow},{id:"internals",label:"Internals",component:dw},{id:"update",label:"Update",component:uw}],fw={components:{TabbedPage:Dr},setup(){return{tabs:Im}},template:''},kl=(e,t,s,n)=>n.map(({id:a,label:i})=>({group:e,label:i,icon:t,to:{path:s,query:{tab:a}}})),pw=[{group:"Workspace",label:"Dashboard",icon:"dashboard",to:{path:"/dashboard"}},{group:"Workspace",label:"Chat",icon:"chat",to:{path:"/chat"}},...kl("Operations","operations","/operations",_m),...kl("History","history","/history",km),...kl("Capabilities","capabilities","/capabilities",wm),{group:"Manage",label:"Personality",icon:"personality",to:{path:"/personality"}},...kl("System","system","/system",Im)],ls=Un({open:!1,query:"",selected:0});function Bu(){ls.query="",ls.selected=0,ls.open=!0}function ro(){ls.open=!1}function hw(e,t){const s=e.label.toLowerCase(),n=`${e.group} ${e.label}`.toLowerCase();return t?s.startsWith(t)?100:n.startsWith(t)?80:s.includes(t)?60:n.includes(t)?40:0:1}const mw={setup(){const e=pm(),t=h(null),s=J(()=>{const i=ls.query.trim().toLowerCase();return pw.map(l=>({...l,_score:hw(l,i)})).filter(l=>l._score>0).sort((l,r)=>r._score-l._score)});es(()=>ls.open,async i=>{var l;i&&(await At(),(l=t.value)==null||l.focus())}),es(()=>ls.query,()=>{ls.selected=0});function n(i){ro(),e.push(i.to)}function a(i){if(i.key==="Escape"){i.preventDefault(),ro();return}if(i.key==="ArrowDown")i.preventDefault(),ls.selected=Math.min(ls.selected+1,s.value.length-1);else if(i.key==="ArrowUp")i.preventDefault(),ls.selected=Math.max(ls.selected-1,0);else if(i.key==="Enter"){i.preventDefault();const l=s.value[ls.selected];l&&n(l)}}return{state:ls,results:s,inputEl:t,go:n,onKeydown:a,closePalette:ro}},template:` + `},Im=[{id:"health",label:"Health",component:Ok},{id:"resources",label:"Resources",component:Nk},{id:"logs",label:"Logs",component:Pk},{id:"config",label:"Config",component:Wk},{id:"discord",label:"Discord",component:Jk},{id:"host-access",label:"Host Access",component:Qk},{id:"api-tokens",label:"API Tokens",component:Xk},{id:"llm",label:"LLM Config",component:ow},{id:"internals",label:"Internals",component:dw},{id:"update",label:"Update",component:uw}],fw={components:{TabbedPage:Mr},setup(){return{tabs:Im}},template:''},wl=(e,t,s,n)=>n.map(({id:a,label:i})=>({group:e,label:i,icon:t,to:{path:s,query:{tab:a}}})),pw=[{group:"Workspace",label:"Dashboard",icon:"dashboard",to:{path:"/dashboard"}},{group:"Workspace",label:"Chat",icon:"chat",to:{path:"/chat"}},...wl("Operations","operations","/operations",_m),...wl("History","history","/history",km),...wl("Capabilities","capabilities","/capabilities",wm),{group:"Manage",label:"Personality",icon:"personality",to:{path:"/personality"}},...wl("System","system","/system",Im)],us=Hn({open:!1,query:"",selected:0});function Uu(){us.query="",us.selected=0,us.open=!0}function ro(){us.open=!1}function hw(e,t){const s=e.label.toLowerCase(),n=`${e.group} ${e.label}`.toLowerCase();return t?s.startsWith(t)?100:n.startsWith(t)?80:s.includes(t)?60:n.includes(t)?40:0:1}const mw={setup(){const e=pm(),t=h(null),s=J(()=>{const i=us.query.trim().toLowerCase();return pw.map(l=>({...l,_score:hw(l,i)})).filter(l=>l._score>0).sort((l,r)=>r._score-l._score)});ns(()=>us.open,async i=>{var l;i&&(await Rt(),(l=t.value)==null||l.focus())}),ns(()=>us.query,()=>{us.selected=0});function n(i){ro(),e.push(i.to)}function a(i){if(i.key==="Escape"){i.preventDefault(),ro();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&&n(l)}}return{state:us,results:s,inputEl:t,go:n,onKeydown:a,closePalette:ro}},template:`
@@ -5439,7 +5527,7 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `},zo={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",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(zo));const gw={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()=>Ua("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"},[Ua("path",{d:zo[e.name]||zo.info})])}},vw=["a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])'].join(",");function Hu(e){return[...e.querySelectorAll(vw)].filter(t=>!t.hasAttribute("hidden")&&t.getAttribute("aria-hidden")!=="true")}const bw={mounted(e){const t=document.activeElement,s=n=>{if(n.key!=="Tab")return;const a=Hu(e);if(!a.length){n.preventDefault(),e.focus();return}const i=a[0],l=a[a.length-1];n.shiftKey&&document.activeElement===i?(n.preventDefault(),l.focus()):!n.shiftKey&&document.activeElement===l&&(n.preventDefault(),i.focus())};e.__odinModalFocus={previous:t,onKeydown:s},e.addEventListener("keydown",s),requestAnimationFrame(()=>{(e.querySelector("[autofocus]")||Hu(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)}},yw={template:` + `},jo={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",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(jo));const gw={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()=>ja("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"},[ja("path",{d:jo[e.name]||jo.info})])}},vw=["a[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])'].join(",");function Hu(e){return[...e.querySelectorAll(vw)].filter(t=>!t.hasAttribute("hidden")&&t.getAttribute("aria-hidden")!=="true")}const bw={mounted(e){const t=document.activeElement,s=n=>{if(n.key!=="Tab")return;const a=Hu(e);if(!a.length){n.preventDefault(),e.focus();return}const i=a[0],l=a[a.length-1];n.shiftKey&&document.activeElement===i?(n.preventDefault(),l.focus()):!n.shiftKey&&document.activeElement===l&&(n.preventDefault(),i.focus())};e.__odinModalFocus={previous:t,onKeydown:s},e.addEventListener("keydown",s),requestAnimationFrame(()=>{(e.querySelector("[autofocus]")||Hu(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)}},yw={template:`
@@ -5627,14 +5715,14 @@ var Gm=Object.defineProperty;var Km=(e,t,s)=>t in e?Gm(e,t,{enumerable:!0,config
- `,setup(){const e=h({}),t=h(!0),s=h(null),n=h([]),a=h(!1),i=h([]),l=h(!1),r=h([]),o=h(0),c=h(null),d=h({reload:!1,clearSessions:!1,stopLoops:!1});let u=0;const f=J(()=>{const I=e.value.uptime_seconds||0,j=Math.floor(I/86400),Y=Math.floor(I%86400/3600),H=Math.floor(I%3600/60),N=[];return j>0&&N.push(`${j}d`),Y>0&&N.push(`${Y}h`),(N.length===0||j===0&&Y===0)&&N.push(`${H}m`),N.join(" ")}),p=J(()=>{const I=e.value.uptime_seconds||0;return 125.66*(1-Math.min(I/86400,1))}),b=J(()=>{const I=e.value;return[{label:"Guilds",value:I.guild_count??0,icon:"home",iconColor:"text-blue-400"},{label:"Sessions",value:I.session_count??0,icon:"message",iconColor:"text-yellow-400"},{label:"Tools",value:I.tool_count??0,icon:"wrench",iconColor:"text-purple-400",sub:`${I.skill_count??0} skills`,subColor:"text-gray-500"},{label:"Loops",value:I.loop_count??0,icon:"rotate",iconColor:"text-green-400",color:I.loop_count>0?"text-green-400":"",highlight:I.loop_count>0},{label:"Agents",value:I.agent_running??0,icon:"bot",iconColor:"text-cyan-400",sub:I.agent_count>0?`${I.agent_count} total`:"",subColor:"text-gray-500",highlight:(I.agent_running??0)>0},{label:"Processes",value:I.process_running??0,icon:"sliders",iconColor:"text-orange-400",sub:I.process_count>0?`${I.process_count} total`:"",subColor:"text-gray-500",highlight:(I.process_running??0)>0},{label:"Schedules",value:I.schedule_count??0,icon:"clock",iconColor:"text-amber-400",sub:(I.schedule_failing>0?`${I.schedule_failing} failing`:"")+(I.schedule_failing>0&&I.schedule_paused>0?", ":"")+(I.schedule_paused>0?`${I.schedule_paused} paused`:"")||void 0,subColor:I.schedule_failing>0?"text-red-400":"text-yellow-400",color:I.schedule_failing>0?"text-red-400":"",highlight:I.schedule_failing>0},{label:"Users",value:I.user_count??0,icon:"users",iconColor:"text-indigo-400"},...c.value!==null?[{label:"Knowledge",value:c.value,icon:"book",iconColor:"text-teal-400",sub:"chunks",subColor:"text-gray-500"}]:[]]}),y=J(()=>{const I=e.value,j=[];return j.push({label:"Bot",status:I.status==="online"?"ok":"warn",detail:I.status==="online"?"Online":"Starting"}),(I.schedule_failing||0)>0?j.push({label:"Schedules",status:"error",detail:`${I.schedule_failing} failing`}):(I.schedule_count||0)>0&&j.push({label:"Schedules",status:"ok",detail:`${I.schedule_count} configured`}),(I.loop_count||0)>0&&j.push({label:"Loops",status:"ok",detail:`${I.loop_count} active`}),(I.agent_running||0)>0&&j.push({label:"Agents",status:"ok",detail:`${I.agent_running} running`}),(I.process_running||0)>0&&j.push({label:"Processes",status:"ok",detail:`${I.process_running} running`}),j});async function A(){try{e.value=await K.get("/api/status"),s.value=null}catch(I){s.value=I.message}finally{t.value=!1}}async function O(){a.value=!0;try{n.value=await K.get("/api/audit?limit=10"),o.value=0}catch{}a.value=!1}async function x(){l.value=!0;try{i.value=await K.get("/api/audit?error_only=1&limit=5")}catch{}l.value=!1}async function m(){try{const I=await K.get("/api/knowledge");c.value=(Array.isArray(I)?I:[]).reduce((j,Y)=>j+(Y.chunks||0),0)}catch{c.value=null}}async function _(){try{const I=await K.get("/api/agents");r.value=I.filter(j=>j.status==="running")}catch{}}async function S(){d.value={...d.value,reload:!0};try{await K.post("/api/reload"),Te.success("Config reloaded")}catch(I){Te.error(I.message)}d.value={...d.value,reload:!1}}async function g(){if(!await gs({title:"Clear all sessions",message:"Clear all conversation sessions? This cannot be undone.",confirmLabel:"Clear All",danger:!0}))return;d.value={...d.value,clearSessions:!0};const j=e.value.session_count;e.value={...e.value,session_count:0};try{const Y=await K.post("/api/sessions/clear-all");Te.success(`Cleared ${Y.count} session${Y.count!==1?"s":""}`),await A()}catch(Y){e.value={...e.value,session_count:j},Te.error(Y.message)}d.value={...d.value,clearSessions:!1}}async function w(){if(!await gs({title:"Stop all loops",message:"Stop all running loops?",confirmLabel:"Stop Loops",danger:!0}))return;d.value={...d.value,stopLoops:!0};const j=e.value.loop_count;e.value={...e.value,loop_count:0};try{const Y=await K.post("/api/loops/stop-all");Te.success(Y.result),await A()}catch(Y){e.value={...e.value,loop_count:j},Te.error(Y.message)}d.value={...d.value,stopLoops:!1}}function T(){t.value=!0,s.value=null,A(),O(),x(),_()}let C=null,M=null,B=null;function $(I){if(I.payload&&I.payload.tool_name){const j={...I.payload,_isNew:!0,_key:++u};n.value.unshift(j),n.value.length>10&&n.value.pop(),o.value++,j.error&&(i.value.unshift(j),i.value.length>5&&i.value.pop()),setTimeout(()=>{j._isNew=!1},1500),clearTimeout(B),B=setTimeout(()=>{o.value=0},1e4)}}return We(async()=>{await Promise.all([A(),O(),x(),_(),m()]),C=setInterval(A,15e3),M=setInterval(_,1e4),Ke.subscribe("events",$)}),xt(()=>{C&&clearInterval(C),M&&clearInterval(M),clearTimeout(B),Ke.unsubscribe("events",$)}),{status:e,loading:t,error:s,uptime:f,uptimeRingOffset:p,stats:b,healthIndicators:y,activity:n,activityLoading:a,newEventCount:o,errors:i,errorsLoading:l,agents:r,actionLoading:d,fetchActivity:O,fetchStatus:A,formatTime:hm,formatDuration:Wa,retry:T,reloadConfig:S,clearSessions:g,stopAllLoops:w}}};/*! @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 Vu(e,t){(t==null||t>e.length)&&(t=e.length);for(var s=0,n=Array(t);s2?n-2:0),i=2;i1?s-1:0),a=1;a"u"?null:Tt(BigInt.prototype.toString),Wu=typeof Symbol>"u"?null:Tt(Symbol.prototype.toString),ht=Tt(Object.prototype.hasOwnProperty),ri=Tt(Object.prototype.toString),Lt=Tt(RegExp.prototype.test),qn=Lw(TypeError);function Tt(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var s=arguments.length,n=new Array(s>1?s-1:0),a=1;a2&&arguments[2]!==void 0?arguments[2]:hi;if(ju&&ju(e,null),!Jt(t))return e;let n=t.length;for(;n--;){let a=t[n];if(typeof a=="string"){const i=s(a);i!==a&&(Tw(t)||(t[n]=i),a=i)}e[a]=!0}return e}function Dw(e){for(let t=0;t/g),Hw=Ts(/\${[\w\W]*/g),Vw=Ts(/^data-[\-\w.\u00B7-\uFFFF]+$/),jw=Ts(/^aria-[\-\w]+$/),Xu=Ts(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),zw=Ts(/^(?:\w+script|data):/i),qw=Ts(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Gw=Ts(/^html$/i),Kw=Ts(/^[a-z][.\w]*(-[.\w]+)+$/i),Vs={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,progressingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},Ww=function(){return typeof window>"u"?null:window},Zw=function(t,s){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let n=null;const a="data-tt-policy-suffix";s&&s.hasAttribute(a)&&(n=s.getAttribute(a));const i="dompurify"+(n?"#"+n:"");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}},ef=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function Lm(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:Ww();const t=pe=>Lm(pe);if(t.version="3.4.9",t.removed=[],!e||!e.document||e.document.nodeType!==Vs.document||!e.Element)return t.isSupported=!1,t;let s=e.document;const n=s,a=n.currentScript;e.DocumentFragment;const i=e.HTMLTemplateElement,l=e.Node,r=e.Element,o=e.NodeFilter,c=e.NamedNodeMap;c===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;const d=e.DOMParser,u=e.trustedTypes,f=r.prototype,p=zs(f,"cloneNode"),b=zs(f,"remove"),y=zs(f,"nextSibling"),A=zs(f,"childNodes"),O=zs(f,"parentNode"),x=zs(f,"shadowRoot"),m=zs(f,"attributes"),_=l&&l.prototype?zs(l.prototype,"nodeType"):null,S=l&&l.prototype?zs(l.prototype,"nodeName"):null;if(typeof i=="function"){const pe=s.createElement("template");pe.content&&pe.content.ownerDocument&&(s=pe.content.ownerDocument)}let g,w="",T,C=!1,M=0;const B=function(){if(M>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.')},$=function(R){B(),M++;try{return g.createHTML(R)}finally{M--}},I=function(R){B(),M++;try{return g.createScriptURL(R)}finally{M--}},j=function(){return C||(T=Zw(u,a),C=!0),T},Y=s,H=Y.implementation,N=Y.createNodeIterator,L=Y.createDocumentFragment,Z=Y.getElementsByTagName,xe=n.importNode;let _e=ef();t.isSupported=typeof Om=="function"&&typeof O=="function"&&H&&H.createHTMLDocument!==void 0;const ae=Uw,fe=Bw,P=Hw,se=Vw,ke=jw,V=zw,ce=qw,de=Kw;let ve=Xu,me=null;const He=$e({},[...Zu,...co,...uo,...fo,...Ju]);let k=null;const E=$e({},[...Yu,...po,...Qu,...wl]);let U=Object.seal(Sa(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}})),X=null,q=null;const Q=Object.seal(Sa(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let ie=!0,re=!0,le=!1,te=!0,be=!1,ue=!0,he=!1,we=!1,Ee=!1,Le=!1,Oe=!1,Fe=!1,Ve=!0,lt=!1;const G="user-content-";let ye=!0,Ce=!1,Re={},Be=null;const ze=$e({},["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 pt=null;const ns=$e({},["audio","video","img","source","image","track"]);let As=null;const Qs=$e({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),$s="http://www.w3.org/1998/Math/MathML",Rs="http://www.w3.org/2000/svg",Nt="http://www.w3.org/1999/xhtml";let us=Nt,Us=!1,Xs=null;const Hn=$e({},[$s,Rs,Nt],oo);let en=$e({},["mi","mo","mn","ms","mtext"]),Kt=$e({},["annotation-xml"]);const ee=$e({},["title","style","font","a","script"]);let Se=null;const De=["application/xhtml+xml","text/html"],Bs="text/html";let rt=null,Is=null;const z=s.createElement("form"),oe=function(R){return R instanceof RegExp||R instanceof Function},Ae=function(){let R=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(Is&&Is===R)return;(!R||typeof R!="object")&&(R={}),R=Ht(R),Se=De.indexOf(R.PARSER_MEDIA_TYPE)===-1?Bs:R.PARSER_MEDIA_TYPE,rt=Se==="application/xhtml+xml"?oo:hi,me=ht(R,"ALLOWED_TAGS")&&Jt(R.ALLOWED_TAGS)?$e({},R.ALLOWED_TAGS,rt):He,k=ht(R,"ALLOWED_ATTR")&&Jt(R.ALLOWED_ATTR)?$e({},R.ALLOWED_ATTR,rt):E,Xs=ht(R,"ALLOWED_NAMESPACES")&&Jt(R.ALLOWED_NAMESPACES)?$e({},R.ALLOWED_NAMESPACES,oo):Hn,As=ht(R,"ADD_URI_SAFE_ATTR")&&Jt(R.ADD_URI_SAFE_ATTR)?$e(Ht(Qs),R.ADD_URI_SAFE_ATTR,rt):Qs,pt=ht(R,"ADD_DATA_URI_TAGS")&&Jt(R.ADD_DATA_URI_TAGS)?$e(Ht(ns),R.ADD_DATA_URI_TAGS,rt):ns,Be=ht(R,"FORBID_CONTENTS")&&Jt(R.FORBID_CONTENTS)?$e({},R.FORBID_CONTENTS,rt):ze,X=ht(R,"FORBID_TAGS")&&Jt(R.FORBID_TAGS)?$e({},R.FORBID_TAGS,rt):Ht({}),q=ht(R,"FORBID_ATTR")&&Jt(R.FORBID_ATTR)?$e({},R.FORBID_ATTR,rt):Ht({}),Re=ht(R,"USE_PROFILES")?R.USE_PROFILES&&typeof R.USE_PROFILES=="object"?Ht(R.USE_PROFILES):R.USE_PROFILES:!1,ie=R.ALLOW_ARIA_ATTR!==!1,re=R.ALLOW_DATA_ATTR!==!1,le=R.ALLOW_UNKNOWN_PROTOCOLS||!1,te=R.ALLOW_SELF_CLOSE_IN_ATTR!==!1,be=R.SAFE_FOR_TEMPLATES||!1,ue=R.SAFE_FOR_XML!==!1,he=R.WHOLE_DOCUMENT||!1,Le=R.RETURN_DOM||!1,Oe=R.RETURN_DOM_FRAGMENT||!1,Fe=R.RETURN_TRUSTED_TYPE||!1,Ee=R.FORCE_BODY||!1,Ve=R.SANITIZE_DOM!==!1,lt=R.SANITIZE_NAMED_PROPS||!1,ye=R.KEEP_CONTENT!==!1,Ce=R.IN_PLACE||!1,ve=Pw(R.ALLOWED_URI_REGEXP)?R.ALLOWED_URI_REGEXP:Xu,us=typeof R.NAMESPACE=="string"?R.NAMESPACE:Nt,en=ht(R,"MATHML_TEXT_INTEGRATION_POINTS")&&R.MATHML_TEXT_INTEGRATION_POINTS&&typeof R.MATHML_TEXT_INTEGRATION_POINTS=="object"?Ht(R.MATHML_TEXT_INTEGRATION_POINTS):$e({},["mi","mo","mn","ms","mtext"]),Kt=ht(R,"HTML_INTEGRATION_POINTS")&&R.HTML_INTEGRATION_POINTS&&typeof R.HTML_INTEGRATION_POINTS=="object"?Ht(R.HTML_INTEGRATION_POINTS):$e({},["annotation-xml"]);const W=ht(R,"CUSTOM_ELEMENT_HANDLING")&&R.CUSTOM_ELEMENT_HANDLING&&typeof R.CUSTOM_ELEMENT_HANDLING=="object"?Ht(R.CUSTOM_ELEMENT_HANDLING):Sa(null);if(U=Sa(null),ht(W,"tagNameCheck")&&oe(W.tagNameCheck)&&(U.tagNameCheck=W.tagNameCheck),ht(W,"attributeNameCheck")&&oe(W.attributeNameCheck)&&(U.attributeNameCheck=W.attributeNameCheck),ht(W,"allowCustomizedBuiltInElements")&&typeof W.allowCustomizedBuiltInElements=="boolean"&&(U.allowCustomizedBuiltInElements=W.allowCustomizedBuiltInElements),be&&(re=!1),Oe&&(Le=!0),Re&&(me=$e({},Ju),k=Sa(null),Re.html===!0&&($e(me,Zu),$e(k,Yu)),Re.svg===!0&&($e(me,co),$e(k,po),$e(k,wl)),Re.svgFilters===!0&&($e(me,uo),$e(k,po),$e(k,wl)),Re.mathMl===!0&&($e(me,fo),$e(k,Qu),$e(k,wl))),Q.tagCheck=null,Q.attributeCheck=null,ht(R,"ADD_TAGS")&&(typeof R.ADD_TAGS=="function"?Q.tagCheck=R.ADD_TAGS:Jt(R.ADD_TAGS)&&(me===He&&(me=Ht(me)),$e(me,R.ADD_TAGS,rt))),ht(R,"ADD_ATTR")&&(typeof R.ADD_ATTR=="function"?Q.attributeCheck=R.ADD_ATTR:Jt(R.ADD_ATTR)&&(k===E&&(k=Ht(k)),$e(k,R.ADD_ATTR,rt))),ht(R,"ADD_URI_SAFE_ATTR")&&Jt(R.ADD_URI_SAFE_ATTR)&&$e(As,R.ADD_URI_SAFE_ATTR,rt),ht(R,"FORBID_CONTENTS")&&Jt(R.FORBID_CONTENTS)&&(Be===ze&&(Be=Ht(Be)),$e(Be,R.FORBID_CONTENTS,rt)),ht(R,"ADD_FORBID_CONTENTS")&&Jt(R.ADD_FORBID_CONTENTS)&&(Be===ze&&(Be=Ht(Be)),$e(Be,R.ADD_FORBID_CONTENTS,rt)),ye&&(me["#text"]=!0),he&&$e(me,["html","head","body"]),me.table&&($e(me,["tbody"]),delete X.tbody),R.TRUSTED_TYPES_POLICY){if(typeof R.TRUSTED_TYPES_POLICY.createHTML!="function")throw qn('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof R.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw qn('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const v=g;g=R.TRUSTED_TYPES_POLICY;try{w=$("")}catch(D){throw g=v,D}}else R.TRUSTED_TYPES_POLICY===null?(g=void 0,w=""):(g===void 0&&(g=j()),g&&typeof w=="string"&&(w=$("")));(_e.uponSanitizeElement.length>0||_e.uponSanitizeAttribute.length>0)&&me===He&&(me=Ht(me)),_e.uponSanitizeAttribute.length>0&&k===E&&(k=Ht(k)),ss&&ss(R),Is=R},Je=$e({},[...co,...uo,...Fw]),ct=$e({},[...fo,...$w]),$t=function(R){let W=O(R);(!W||!W.tagName)&&(W={namespaceURI:us,tagName:"template"});const v=hi(R.tagName),D=hi(W.tagName);return Xs[R.namespaceURI]?R.namespaceURI===Rs?W.namespaceURI===Nt?v==="svg":W.namespaceURI===$s?v==="svg"&&(D==="annotation-xml"||en[D]):!!Je[v]:R.namespaceURI===$s?W.namespaceURI===Nt?v==="math":W.namespaceURI===Rs?v==="math"&&Kt[D]:!!ct[v]:R.namespaceURI===Nt?W.namespaceURI===Rs&&!Kt[D]||W.namespaceURI===$s&&!en[D]?!1:!ct[v]&&(ee[v]||!Je[v]):!!(Se==="application/xhtml+xml"&&Xs[R.namespaceURI]):!1},Wt=function(R){ya(t.removed,{element:R});try{O(R).removeChild(R)}catch{if(b(R),!O(R))throw qn("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},tl=function(R){const W=A?A(R):R.childNodes;if(W){const D=[];ln(W,F=>{ya(D,F)}),ln(D,F=>{try{b(F)}catch{}})}const v=m?m(R):null;if(v)for(let D=v.length-1;D>=0;--D){const F=v[D],ne=F&&F.name;if(typeof ne=="string")try{R.removeAttribute(ne)}catch{}}},Hs=function(R,W){try{ya(t.removed,{attribute:W.getAttributeNode(R),from:W})}catch{ya(t.removed,{attribute:null,from:W})}if(W.removeAttribute(R),R==="is")if(Le||Oe)try{Wt(W)}catch{}else try{W.setAttribute(R,"")}catch{}},sl=function(R){const W=m?m(R):R.attributes;if(W)for(let v=W.length-1;v>=0;--v){const D=W[v],F=D&&D.name;if(!(typeof F!="string"||k[rt(F)]))try{R.removeAttribute(F)}catch{}}},$r=function(R){const W=[R];for(;W.length>0;){const v=W.pop();(_?_(v):v.nodeType)===Vs.element&&sl(v);const F=A?A(v):v.childNodes;if(F)for(let ne=F.length-1;ne>=0;--ne)W.push(F[ne])}},nl=function(R){let W=null,v=null;if(Ee)R=""+R;else{const ne=qu(R,/^[\r\n\t ]+/);v=ne&&ne[0]}Se==="application/xhtml+xml"&&us===Nt&&(R=''+R+"");const D=g?$(R):R;if(us===Nt)try{W=new d().parseFromString(D,Se)}catch{}if(!W||!W.documentElement){W=H.createDocument(us,"template",null);try{W.documentElement.innerHTML=Us?w:D}catch{}}const F=W.body||W.documentElement;return R&&v&&F.insertBefore(s.createTextNode(v),F.childNodes[0]||null),us===Nt?Z.call(W,he?"html":"body")[0]:he?W.documentElement:F},al=function(R){return N.call(R.ownerDocument||R,R,o.SHOW_ELEMENT|o.SHOW_COMMENT|o.SHOW_TEXT|o.SHOW_PROCESSING_INSTRUCTION|o.SHOW_CDATA_SECTION,null)},Cn=function(R){var W,v;R.normalize();const D=N.call(R.ownerDocument||R,R,o.SHOW_TEXT|o.SHOW_COMMENT|o.SHOW_CDATA_SECTION|o.SHOW_PROCESSING_INSTRUCTION,null);let F=D.nextNode();for(;F;){let Ne=F.data;ln([ae,fe,P],Ue=>{Ne=xa(Ne,Ue," ")}),F.data=Ne,F=D.nextNode()}const ne=(W=(v=R.querySelectorAll)===null||v===void 0?void 0:v.call(R,"template"))!==null&&W!==void 0?W:[];ln(Array.from(ne),Ne=>{Os(Ne.content)&&Cn(Ne.content)})},pa=function(R){const W=S?S(R):null;return typeof W!="string"||rt(W)!=="form"?!1:typeof R.nodeName!="string"||typeof R.textContent!="string"||typeof R.removeChild!="function"||R.attributes!==m(R)||typeof R.removeAttribute!="function"||typeof R.setAttribute!="function"||typeof R.namespaceURI!="string"||typeof R.insertBefore!="function"||typeof R.hasChildNodes!="function"||R.nodeType!==_(R)||R.childNodes!==A(R)},Os=function(R){if(!_||typeof R!="object"||R===null)return!1;try{return _(R)===Vs.documentFragment}catch{return!1}},Vn=function(R){if(!_||typeof R!="object"||R===null)return!1;try{return typeof _(R)=="number"}catch{return!1}};function Ns(pe,R,W){ln(pe,v=>{v.call(t,R,W,Is)})}const tn=function(R){let W=null;if(Ns(_e.beforeSanitizeElements,R,null),pa(R))return Wt(R),!0;const v=rt(S?S(R):R.nodeName);if(Ns(_e.uponSanitizeElement,R,{tagName:v,allowedTags:me}),ue&&R.hasChildNodes()&&!Vn(R.firstElementChild)&&Lt(/<[/\w!]/g,R.innerHTML)&&Lt(/<[/\w!]/g,R.textContent)||ue&&R.namespaceURI===Nt&&v==="style"&&Vn(R.firstElementChild)||R.nodeType===Vs.progressingInstruction||ue&&R.nodeType===Vs.comment&&Lt(/<[/\w]/g,R.data))return Wt(R),!0;if(X[v]||!(Q.tagCheck instanceof Function&&Q.tagCheck(v))&&!me[v]){if(!X[v]&&Qa(v)&&(U.tagNameCheck instanceof RegExp&&Lt(U.tagNameCheck,v)||U.tagNameCheck instanceof Function&&U.tagNameCheck(v)))return!1;if(ye&&!Be[v]){const F=O(R),ne=A(R);if(ne&&F){const Ne=ne.length;for(let Ue=Ne-1;Ue>=0;--Ue){const Xe=Ce?ne[Ue]:p(ne[Ue],!0);F.insertBefore(Xe,y(R))}}}return Wt(R),!0}return(_?_(R):R.nodeType)===Vs.element&&!$t(R)||(v==="noscript"||v==="noembed"||v==="noframes")&&Lt(/<\/no(script|embed|frames)/i,R.innerHTML)?(Wt(R),!0):(be&&R.nodeType===Vs.text&&(W=R.textContent,ln([ae,fe,P],F=>{W=xa(W,F," ")}),R.textContent!==W&&(ya(t.removed,{element:R.cloneNode()}),R.textContent=W)),Ns(_e.afterSanitizeElements,R,null),!1)},il=function(R,W,v){if(q[W]||Ve&&(W==="id"||W==="name")&&(v in s||v in z))return!1;const D=k[W]||Q.attributeCheck instanceof Function&&Q.attributeCheck(W,R);if(!(re&&!q[W]&&Lt(se,W))){if(!(ie&&Lt(ke,W))){if(!D||q[W]){if(!(Qa(R)&&(U.tagNameCheck instanceof RegExp&&Lt(U.tagNameCheck,R)||U.tagNameCheck instanceof Function&&U.tagNameCheck(R))&&(U.attributeNameCheck instanceof RegExp&&Lt(U.attributeNameCheck,W)||U.attributeNameCheck instanceof Function&&U.attributeNameCheck(W,R))||W==="is"&&U.allowCustomizedBuiltInElements&&(U.tagNameCheck instanceof RegExp&&Lt(U.tagNameCheck,v)||U.tagNameCheck instanceof Function&&U.tagNameCheck(v))))return!1}else if(!As[W]){if(!Lt(ve,xa(v,ce,""))){if(!((W==="src"||W==="xlink:href"||W==="href")&&R!=="script"&&Gu(v,"data:")===0&&pt[R])){if(!(le&&!Lt(V,xa(v,ce,"")))){if(v)return!1}}}}}}return!0},Ur=$e({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),Qa=function(R){return!Ur[hi(R)]&&Lt(de,R)},ll=function(R){Ns(_e.beforeSanitizeAttributes,R,null);const W=R.attributes;if(!W||pa(R))return;const v={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:k,forceKeepAttr:void 0};let D=W.length;for(;D--;){const F=W[D],ne=F.name,Ne=F.namespaceURI,Ue=F.value,Xe=rt(ne),Ut=Ue;let vt=ne==="value"?Ut:Iw(Ut);if(v.attrName=Xe,v.attrValue=vt,v.keepAttr=!0,v.forceKeepAttr=void 0,Ns(_e.uponSanitizeAttribute,R,v),vt=v.attrValue,lt&&(Xe==="id"||Xe==="name")&&Gu(vt,G)!==0&&(Hs(ne,R),vt=G+vt),ue&&Lt(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,vt)){Hs(ne,R);continue}if(Xe==="attributename"&&qu(vt,"href")){Hs(ne,R);continue}if(v.forceKeepAttr)continue;if(!v.keepAttr){Hs(ne,R);continue}if(!te&&Lt(/\/>/i,vt)){Hs(ne,R);continue}be&&ln([ae,fe,P],id=>{vt=xa(vt,id," ")});const ei=rt(R.nodeName);if(!il(ei,Xe,vt)){Hs(ne,R);continue}if(g&&typeof u=="object"&&typeof u.getAttributeType=="function"&&!Ne)switch(u.getAttributeType(ei,Xe)){case"TrustedHTML":{vt=$(vt);break}case"TrustedScriptURL":{vt=I(vt);break}}if(vt!==Ut)try{Ne?R.setAttributeNS(Ne,ne,vt):R.setAttribute(ne,vt),pa(R)?Wt(R):zu(t.removed)}catch{Hs(ne,R)}}Ns(_e.afterSanitizeAttributes,R,null)},ha=function(R){let W=null;const v=al(R);for(Ns(_e.beforeSanitizeShadowDOM,R,null);W=v.nextNode();)if(Ns(_e.uponSanitizeShadowNode,W,null),tn(W),ll(W),Os(W.content)&&ha(W.content),(_?_(W):W.nodeType)===Vs.element){const F=x?x(W):W.shadowRoot;Os(F)&&(Xa(F),ha(F))}Ns(_e.afterSanitizeShadowDOM,R,null)},Xa=function(R){const W=[{node:R,shadow:null}];for(;W.length>0;){const v=W.pop();if(v.shadow){ha(v.shadow);continue}const D=v.node,ne=(_?_(D):D.nodeType)===Vs.element,Ne=A?A(D):D.childNodes;if(Ne)for(let Ue=Ne.length-1;Ue>=0;--Ue)W.push({node:Ne[Ue],shadow:null});if(ne){const Ue=S?S(D):null;if(typeof Ue=="string"&&rt(Ue)==="template"){const Xe=D.content;Os(Xe)&&W.push({node:Xe,shadow:null})}}if(ne){const Ue=x?x(D):D.shadowRoot;Os(Ue)&&W.push({node:null,shadow:Ue},{node:Ue,shadow:null})}}};return t.sanitize=function(pe){let R=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},W=null,v=null,D=null,F=null;if(Us=!pe,Us&&(pe=""),typeof pe!="string"&&!Vn(pe)&&(pe=Mw(pe),typeof pe!="string"))throw qn("dirty is not a string, aborting");if(!t.isSupported)return pe;we||Ae(R),t.removed=[];const ne=Ce&&typeof pe!="string"&&Vn(pe);if(ne){const Xe=S?S(pe):pe.nodeName;if(typeof Xe=="string"){const Ut=rt(Xe);if(!me[Ut]||X[Ut])throw qn("root node is forbidden and cannot be sanitized in-place")}if(pa(pe))throw qn("root node is clobbered and cannot be sanitized in-place");try{Xa(pe)}catch(Ut){throw tl(pe),Ut}}else if(Vn(pe))W=nl(""),v=W.ownerDocument.importNode(pe,!0),v.nodeType===Vs.element&&v.nodeName==="BODY"||v.nodeName==="HTML"?W=v:W.appendChild(v),Xa(v);else{if(!Le&&!be&&!he&&pe.indexOf("<")===-1)return g&&Fe?$(pe):pe;if(W=nl(pe),!W)return Le?null:Fe?w:""}W&&Ee&&Wt(W.firstChild);const Ne=al(ne?pe:W);try{for(;D=Ne.nextNode();)tn(D),ll(D),Os(D.content)&&ha(D.content)}catch(Xe){throw ne&&tl(pe),Xe}if(ne)return ln(t.removed,Xe=>{Xe.element&&$r(Xe.element)}),be&&Cn(pe),pe;if(Le){if(be&&Cn(W),Oe)for(F=L.call(W.ownerDocument);W.firstChild;)F.appendChild(W.firstChild);else F=W;return(k.shadowroot||k.shadowrootmode)&&(F=xe.call(n,F,!0)),F}let Ue=he?W.outerHTML:W.innerHTML;return he&&me["!doctype"]&&W.ownerDocument&&W.ownerDocument.doctype&&W.ownerDocument.doctype.name&&Lt(Gw,W.ownerDocument.doctype.name)&&(Ue=" -`+Ue),be&&ln([ae,fe,P],Xe=>{Ue=xa(Ue,Xe," ")}),g&&Fe?$(Ue):Ue},t.setConfig=function(){let pe=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};Ae(pe),we=!0},t.clearConfig=function(){Is=null,we=!1,g=T,w=""},t.isValidAttribute=function(pe,R,W){Is||Ae({});const v=rt(pe),D=rt(R);return il(v,D,W)},t.addHook=function(pe,R){typeof R=="function"&&ya(_e[pe],R)},t.removeHook=function(pe,R){if(R!==void 0){const W=Aw(_e[pe],R);return W===-1?void 0:Rw(_e[pe],W,1)[0]}return zu(_e[pe])},t.removeHooks=function(pe){_e[pe]=[]},t.removeAllHooks=function(){_e=ef()},t}var tf=Lm();function Jc(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var fa=Jc();function Dm(e){fa=e}var Si={exec:()=>null};function nt(e,t=""){let s=typeof e=="string"?e:e.source;const n={replace:(a,i)=>{let l=typeof i=="string"?i:i.source;return l=l.replace(Xt.caret,"$1"),s=s.replace(a,l),n},getRegex:()=>new RegExp(s,t)};return n}var Xt={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")},Jw=/^(?:[ \t]*(?:\n|$))+/,Yw=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Qw=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,el=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Xw=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,Yc=/(?:[*+-]|\d{1,9}[.)])/,Mm=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,Pm=nt(Mm).replace(/bull/g,Yc).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(),eS=nt(Mm).replace(/bull/g,Yc).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(),Qc=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,tS=/^[^\n]+/,Xc=/(?!\s*\])(?:\\.|[^\[\]\\])+/,sS=nt(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",Xc).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),nS=nt(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,Yc).getRegex(),Pr="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",ed=/|$))/,aS=nt("^ {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",ed).replace("tag",Pr).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Fm=nt(Qc).replace("hr",el).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",Pr).getRegex(),iS=nt(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",Fm).getRegex(),td={blockquote:iS,code:Yw,def:sS,fences:Qw,heading:Xw,hr:el,html:aS,lheading:Pm,list:nS,newline:Jw,paragraph:Fm,table:Si,text:tS},sf=nt("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",el).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",Pr).getRegex(),lS={...td,lheading:eS,table:sf,paragraph:nt(Qc).replace("hr",el).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",sf).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",Pr).getRegex()},rS={...td,html:nt(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",ed).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:Si,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:nt(Qc).replace("hr",el).replace("heading",` *#{1,6} *[^ -]`).replace("lheading",Pm).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},oS=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,cS=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,$m=/^( {2,}|\\)\n(?!\s*$)/,dS=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,Hm=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,mS=nt(Hm,"u").replace(/punct/g,Fr).getRegex(),gS=nt(Hm,"u").replace(/punct/g,Bm).getRegex(),Vm="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",vS=nt(Vm,"gu").replace(/notPunctSpace/g,Um).replace(/punctSpace/g,sd).replace(/punct/g,Fr).getRegex(),bS=nt(Vm,"gu").replace(/notPunctSpace/g,pS).replace(/punctSpace/g,fS).replace(/punct/g,Bm).getRegex(),yS=nt("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,Um).replace(/punctSpace/g,sd).replace(/punct/g,Fr).getRegex(),xS=nt(/\\(punct)/,"gu").replace(/punct/g,Fr).getRegex(),_S=nt(/^<(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(),kS=nt(ed).replace("(?:-->|$)","-->").getRegex(),wS=nt("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",kS).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),ir=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,SS=nt(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",ir).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),jm=nt(/^!?\[(label)\]\[(ref)\]/).replace("label",ir).replace("ref",Xc).getRegex(),zm=nt(/^!?\[(ref)\](?:\[\])?/).replace("ref",Xc).getRegex(),TS=nt("reflink|nolink(?!\\()","g").replace("reflink",jm).replace("nolink",zm).getRegex(),nd={_backpedal:Si,anyPunctuation:xS,autolink:_S,blockSkip:hS,br:$m,code:cS,del:Si,emStrongLDelim:mS,emStrongRDelimAst:vS,emStrongRDelimUnd:yS,escape:oS,link:SS,nolink:zm,punctuation:uS,reflink:jm,reflinkSearch:TS,tag:wS,text:dS,url:Si},CS={...nd,link:nt(/^!?\[(label)\]\((.*?)\)/).replace("label",ir).getRegex(),reflink:nt(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",ir).getRegex()},Ko={...nd,emStrongRDelimAst:bS,emStrongLDelim:gS,url:nt(/^((?: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]*?(?:(?=[\\":">",'"':""","'":"'"},nf=e=>AS[e];function qs(e,t){if(t){if(Xt.escapeTest.test(e))return e.replace(Xt.escapeReplace,nf)}else if(Xt.escapeTestNoEncode.test(e))return e.replace(Xt.escapeReplaceNoEncode,nf);return e}function af(e){try{e=encodeURI(e).replace(Xt.percentDecode,"%")}catch{return null}return e}function lf(e,t){var i;const s=e.replace(Xt.findPipe,(l,r,o)=>{let c=!1,d=r;for(;--d>=0&&o[d]==="\\";)c=!c;return c?"|":" |"}),n=s.split(Xt.splitPipe);let a=0;if(n[0].trim()||n.shift(),n.length>0&&!((i=n.at(-1))!=null&&i.trim())&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0?-2:-1}function rf(e,t,s,n,a){const i=t.href,l=t.title||null,r=e[1].replace(a.other.outputLinkReplace,"$1");n.state.inLink=!0;const o={type:e[0].charAt(0)==="!"?"image":"link",raw:s,href:i,title:l,text:r,tokens:n.inlineTokens(r)};return n.state.inLink=!1,o}function IS(e,t,s){const n=e.match(s.other.indentCodeCompensation);if(n===null)return t;const a=n[1];return t.split(` + `,setup(){const e=h({}),t=h(!0),s=h(null),n=h([]),a=h(!1),i=h([]),l=h(!1),r=h([]),o=h(0),c=h(null),d=h({reload:!1,clearSessions:!1,stopLoops:!1});let u=0;const f=J(()=>{const R=e.value.uptime_seconds||0,j=Math.floor(R/86400),Q=Math.floor(R%86400/3600),U=Math.floor(R%3600/60),O=[];return j>0&&O.push(`${j}d`),Q>0&&O.push(`${Q}h`),(O.length===0||j===0&&Q===0)&&O.push(`${U}m`),O.join(" ")}),p=J(()=>{const R=e.value.uptime_seconds||0;return 125.66*(1-Math.min(R/86400,1))}),b=J(()=>{const R=e.value;return[{label:"Guilds",value:R.guild_count??0,icon:"home",iconColor:"text-blue-400"},{label:"Sessions",value:R.session_count??0,icon:"message",iconColor:"text-yellow-400"},{label:"Tools",value:R.tool_count??0,icon:"wrench",iconColor:"text-purple-400",sub:`${R.skill_count??0} skills`,subColor:"text-gray-500"},{label:"Loops",value:R.loop_count??0,icon:"rotate",iconColor:"text-green-400",color:R.loop_count>0?"text-green-400":"",highlight:R.loop_count>0},{label:"Agents",value:R.agent_running??0,icon:"bot",iconColor:"text-cyan-400",sub:R.agent_count>0?`${R.agent_count} total`:"",subColor:"text-gray-500",highlight:(R.agent_running??0)>0},{label:"Processes",value:R.process_running??0,icon:"sliders",iconColor:"text-orange-400",sub:R.process_count>0?`${R.process_count} total`:"",subColor:"text-gray-500",highlight:(R.process_running??0)>0},{label:"Schedules",value:R.schedule_count??0,icon:"clock",iconColor:"text-amber-400",sub:(R.schedule_failing>0?`${R.schedule_failing} failing`:"")+(R.schedule_failing>0&&R.schedule_paused>0?", ":"")+(R.schedule_paused>0?`${R.schedule_paused} paused`:"")||void 0,subColor:R.schedule_failing>0?"text-red-400":"text-yellow-400",color:R.schedule_failing>0?"text-red-400":"",highlight:R.schedule_failing>0},{label:"Users",value:R.user_count??0,icon:"users",iconColor:"text-indigo-400"},...c.value!==null?[{label:"Knowledge",value:c.value,icon:"book",iconColor:"text-teal-400",sub:"chunks",subColor:"text-gray-500"}]:[]]}),y=J(()=>{const R=e.value,j=[];return j.push({label:"Bot",status:R.status==="online"?"ok":"warn",detail:R.status==="online"?"Online":"Starting"}),(R.schedule_failing||0)>0?j.push({label:"Schedules",status:"error",detail:`${R.schedule_failing} failing`}):(R.schedule_count||0)>0&&j.push({label:"Schedules",status:"ok",detail:`${R.schedule_count} configured`}),(R.loop_count||0)>0&&j.push({label:"Loops",status:"ok",detail:`${R.loop_count} active`}),(R.agent_running||0)>0&&j.push({label:"Agents",status:"ok",detail:`${R.agent_running} running`}),(R.process_running||0)>0&&j.push({label:"Processes",status:"ok",detail:`${R.process_running} running`}),j});async function E(){try{e.value=await G.get("/api/status"),s.value=null}catch(R){s.value=R.message}finally{t.value=!1}}async function I(){a.value=!0;try{n.value=await G.get("/api/audit?limit=10"),o.value=0}catch{}a.value=!1}async function x(){l.value=!0;try{i.value=await G.get("/api/audit?error_only=1&limit=5")}catch{}l.value=!1}async function m(){try{const R=await G.get("/api/knowledge");c.value=(Array.isArray(R)?R:[]).reduce((j,Q)=>j+(Q.chunks||0),0)}catch{c.value=null}}async function _(){try{const R=await G.get("/api/agents");r.value=R.filter(j=>j.status==="running")}catch{}}async function S(){d.value={...d.value,reload:!0};try{await G.post("/api/reload"),Ae.success("Config reloaded")}catch(R){Ae.error(R.message)}d.value={...d.value,reload:!1}}async function g(){if(!await _s({title:"Clear all sessions",message:"Clear all conversation sessions? This cannot be undone.",confirmLabel:"Clear All",danger:!0}))return;d.value={...d.value,clearSessions:!0};const j=e.value.session_count;e.value={...e.value,session_count:0};try{const Q=await G.post("/api/sessions/clear-all");Ae.success(`Cleared ${Q.count} session${Q.count!==1?"s":""}`),await E()}catch(Q){e.value={...e.value,session_count:j},Ae.error(Q.message)}d.value={...d.value,clearSessions:!1}}async function w(){if(!await _s({title:"Stop all loops",message:"Stop all running loops?",confirmLabel:"Stop Loops",danger:!0}))return;d.value={...d.value,stopLoops:!0};const j=e.value.loop_count;e.value={...e.value,loop_count:0};try{const Q=await G.post("/api/loops/stop-all");Ae.success(Q.result),await E()}catch(Q){e.value={...e.value,loop_count:j},Ae.error(Q.message)}d.value={...d.value,stopLoops:!1}}function T(){t.value=!0,s.value=null,E(),I(),x(),_()}let C=null,M=null,H=null;function P(R){if(R.payload&&R.payload.tool_name){const j={...R.payload,_isNew:!0,_key:++u};n.value.unshift(j),n.value.length>10&&n.value.pop(),o.value++,j.error&&(i.value.unshift(j),i.value.length>5&&i.value.pop()),setTimeout(()=>{j._isNew=!1},1500),clearTimeout(H),H=setTimeout(()=>{o.value=0},1e4)}}return We(async()=>{await Promise.all([E(),I(),x(),_(),m()]),C=setInterval(E,15e3),M=setInterval(_,1e4),Ke.subscribe("events",P)}),xt(()=>{C&&clearInterval(C),M&&clearInterval(M),clearTimeout(H),Ke.unsubscribe("events",P)}),{status:e,loading:t,error:s,uptime:f,uptimeRingOffset:p,stats:b,healthIndicators:y,activity:n,activityLoading:a,newEventCount:o,errors:i,errorsLoading:l,agents:r,actionLoading:d,fetchActivity:I,fetchStatus:E,formatTime:hm,formatDuration:Xa,retry:T,reloadConfig:S,clearSessions:g,stopAllLoops:w}}};/*! @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 zu(e,t){(t==null||t>e.length)&&(t=e.length);for(var s=0,n=Array(t);s2?n-2:0),i=2;i1?s-1:0),a=1;a"u"?null:Tt(BigInt.prototype.toString),Wu=typeof Symbol>"u"?null:Tt(Symbol.prototype.toString),ht=Tt(Object.prototype.hasOwnProperty),pi=Tt(Object.prototype.toString),Ft=Tt(RegExp.prototype.test),Kn=Lw(TypeError);function Tt(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var s=arguments.length,n=new Array(s>1?s-1:0),a=1;a2&&arguments[2]!==void 0?arguments[2]:xi;if(Vu&&Vu(e,null),!Xt(t))return e;let n=t.length;for(;n--;){let a=t[n];if(typeof a=="string"){const i=s(a);i!==a&&(Tw(t)||(t[n]=i),a=i)}e[a]=!0}return e}function Dw(e){for(let t=0;t/g),Hw=Ls(/\${[\w\W]*/g),zw=Ls(/^data-[\-\w.\u00B7-\uFFFF]+$/),Vw=Ls(/^aria-[\-\w]+$/),Xu=Ls(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),jw=Ls(/^(?:\w+script|data):/i),qw=Ls(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Gw=Ls(/^html$/i),Kw=Ls(/^[a-z][.\w]*(-[.\w]+)+$/i),Ks={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,progressingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},Ww=function(){return typeof window>"u"?null:window},Zw=function(t,s){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let n=null;const a="data-tt-policy-suffix";s&&s.hasAttribute(a)&&(n=s.getAttribute(a));const i="dompurify"+(n?"#"+n:"");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}},ef=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function Lm(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:Ww();const t=me=>Lm(me);if(t.version="3.4.9",t.removed=[],!e||!e.document||e.document.nodeType!==Ks.document||!e.Element)return t.isSupported=!1,t;let s=e.document;const n=s,a=n.currentScript;e.DocumentFragment;const i=e.HTMLTemplateElement,l=e.Node,r=e.Element,o=e.NodeFilter,c=e.NamedNodeMap;c===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;const d=e.DOMParser,u=e.trustedTypes,f=r.prototype,p=Zs(f,"cloneNode"),b=Zs(f,"remove"),y=Zs(f,"nextSibling"),E=Zs(f,"childNodes"),I=Zs(f,"parentNode"),x=Zs(f,"shadowRoot"),m=Zs(f,"attributes"),_=l&&l.prototype?Zs(l.prototype,"nodeType"):null,S=l&&l.prototype?Zs(l.prototype,"nodeName"):null;if(typeof i=="function"){const me=s.createElement("template");me.content&&me.content.ownerDocument&&(s=me.content.ownerDocument)}let g,w="",T,C=!1,M=0;const H=function(){if(M>0)throw Kn('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.')},P=function(A){H(),M++;try{return g.createHTML(A)}finally{M--}},R=function(A){H(),M++;try{return g.createScriptURL(A)}finally{M--}},j=function(){return C||(T=Zw(u,a),C=!0),T},Q=s,U=Q.implementation,O=Q.createNodeIterator,N=Q.createDocumentFragment,Y=Q.getElementsByTagName,we=n.importNode;let ke=ef();t.isSupported=typeof Om=="function"&&typeof I=="function"&&U&&U.createHTMLDocument!==void 0;const ie=Bw,he=Uw,F=Hw,se=zw,Se=Vw,V=jw,de=qw,ce=Kw;let ye=Xu,ge=null;const He=$e({},[...Zu,...co,...uo,...fo,...Ju]);let k=null;const L=$e({},[...Yu,...po,...Qu,...Sl]);let $=Object.seal(Ra(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}})),ee=null,Z=null;const X=Object.seal(Ra(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let ue=!0,oe=!0,le=!1,te=!0,ne=!1,fe=!0,ve=!1,Te=!1,Oe=!1,Le=!1,De=!1,Be=!1,qe=!0,ct=!1;const K="user-content-";let xe=!0,Ce=!1,Re={},Ve=null;const Pe=$e({},["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 pt=null;const ls=$e({},["audio","video","img","source","image","track"]);let Ps=null;const nn=$e({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ss="http://www.w3.org/1998/Math/MathML",Fs="http://www.w3.org/2000/svg",Mt="http://www.w3.org/1999/xhtml";let Yt=Mt,$s=!1,Bs=null;const An=$e({},[Ss,Fs,Mt],oo);let Us=$e({},["mi","mo","mn","ms","mtext"]),zt=$e({},["annotation-xml"]);const Vn=$e({},["title","style","font","a","script"]);let Ct=null;const rs=["application/xhtml+xml","text/html"],os="text/html";let Ye=null,gs=null;const q=s.createElement("form"),re=function(A){return A instanceof RegExp||A instanceof Function},Ee=function(){let A=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(gs&&gs===A)return;(!A||typeof A!="object")&&(A={}),A=qt(A),Ct=rs.indexOf(A.PARSER_MEDIA_TYPE)===-1?os:A.PARSER_MEDIA_TYPE,Ye=Ct==="application/xhtml+xml"?oo:xi,ge=ht(A,"ALLOWED_TAGS")&&Xt(A.ALLOWED_TAGS)?$e({},A.ALLOWED_TAGS,Ye):He,k=ht(A,"ALLOWED_ATTR")&&Xt(A.ALLOWED_ATTR)?$e({},A.ALLOWED_ATTR,Ye):L,Bs=ht(A,"ALLOWED_NAMESPACES")&&Xt(A.ALLOWED_NAMESPACES)?$e({},A.ALLOWED_NAMESPACES,oo):An,Ps=ht(A,"ADD_URI_SAFE_ATTR")&&Xt(A.ADD_URI_SAFE_ATTR)?$e(qt(nn),A.ADD_URI_SAFE_ATTR,Ye):nn,pt=ht(A,"ADD_DATA_URI_TAGS")&&Xt(A.ADD_DATA_URI_TAGS)?$e(qt(ls),A.ADD_DATA_URI_TAGS,Ye):ls,Ve=ht(A,"FORBID_CONTENTS")&&Xt(A.FORBID_CONTENTS)?$e({},A.FORBID_CONTENTS,Ye):Pe,ee=ht(A,"FORBID_TAGS")&&Xt(A.FORBID_TAGS)?$e({},A.FORBID_TAGS,Ye):qt({}),Z=ht(A,"FORBID_ATTR")&&Xt(A.FORBID_ATTR)?$e({},A.FORBID_ATTR,Ye):qt({}),Re=ht(A,"USE_PROFILES")?A.USE_PROFILES&&typeof A.USE_PROFILES=="object"?qt(A.USE_PROFILES):A.USE_PROFILES:!1,ue=A.ALLOW_ARIA_ATTR!==!1,oe=A.ALLOW_DATA_ATTR!==!1,le=A.ALLOW_UNKNOWN_PROTOCOLS||!1,te=A.ALLOW_SELF_CLOSE_IN_ATTR!==!1,ne=A.SAFE_FOR_TEMPLATES||!1,fe=A.SAFE_FOR_XML!==!1,ve=A.WHOLE_DOCUMENT||!1,Le=A.RETURN_DOM||!1,De=A.RETURN_DOM_FRAGMENT||!1,Be=A.RETURN_TRUSTED_TYPE||!1,Oe=A.FORCE_BODY||!1,qe=A.SANITIZE_DOM!==!1,ct=A.SANITIZE_NAMED_PROPS||!1,xe=A.KEEP_CONTENT!==!1,Ce=A.IN_PLACE||!1,ye=Pw(A.ALLOWED_URI_REGEXP)?A.ALLOWED_URI_REGEXP:Xu,Yt=typeof A.NAMESPACE=="string"?A.NAMESPACE:Mt,Us=ht(A,"MATHML_TEXT_INTEGRATION_POINTS")&&A.MATHML_TEXT_INTEGRATION_POINTS&&typeof A.MATHML_TEXT_INTEGRATION_POINTS=="object"?qt(A.MATHML_TEXT_INTEGRATION_POINTS):$e({},["mi","mo","mn","ms","mtext"]),zt=ht(A,"HTML_INTEGRATION_POINTS")&&A.HTML_INTEGRATION_POINTS&&typeof A.HTML_INTEGRATION_POINTS=="object"?qt(A.HTML_INTEGRATION_POINTS):$e({},["annotation-xml"]);const W=ht(A,"CUSTOM_ELEMENT_HANDLING")&&A.CUSTOM_ELEMENT_HANDLING&&typeof A.CUSTOM_ELEMENT_HANDLING=="object"?qt(A.CUSTOM_ELEMENT_HANDLING):Ra(null);if($=Ra(null),ht(W,"tagNameCheck")&&re(W.tagNameCheck)&&($.tagNameCheck=W.tagNameCheck),ht(W,"attributeNameCheck")&&re(W.attributeNameCheck)&&($.attributeNameCheck=W.attributeNameCheck),ht(W,"allowCustomizedBuiltInElements")&&typeof W.allowCustomizedBuiltInElements=="boolean"&&($.allowCustomizedBuiltInElements=W.allowCustomizedBuiltInElements),ne&&(oe=!1),De&&(Le=!0),Re&&(ge=$e({},Ju),k=Ra(null),Re.html===!0&&($e(ge,Zu),$e(k,Yu)),Re.svg===!0&&($e(ge,co),$e(k,po),$e(k,Sl)),Re.svgFilters===!0&&($e(ge,uo),$e(k,po),$e(k,Sl)),Re.mathMl===!0&&($e(ge,fo),$e(k,Qu),$e(k,Sl))),X.tagCheck=null,X.attributeCheck=null,ht(A,"ADD_TAGS")&&(typeof A.ADD_TAGS=="function"?X.tagCheck=A.ADD_TAGS:Xt(A.ADD_TAGS)&&(ge===He&&(ge=qt(ge)),$e(ge,A.ADD_TAGS,Ye))),ht(A,"ADD_ATTR")&&(typeof A.ADD_ATTR=="function"?X.attributeCheck=A.ADD_ATTR:Xt(A.ADD_ATTR)&&(k===L&&(k=qt(k)),$e(k,A.ADD_ATTR,Ye))),ht(A,"ADD_URI_SAFE_ATTR")&&Xt(A.ADD_URI_SAFE_ATTR)&&$e(Ps,A.ADD_URI_SAFE_ATTR,Ye),ht(A,"FORBID_CONTENTS")&&Xt(A.FORBID_CONTENTS)&&(Ve===Pe&&(Ve=qt(Ve)),$e(Ve,A.FORBID_CONTENTS,Ye)),ht(A,"ADD_FORBID_CONTENTS")&&Xt(A.ADD_FORBID_CONTENTS)&&(Ve===Pe&&(Ve=qt(Ve)),$e(Ve,A.ADD_FORBID_CONTENTS,Ye)),xe&&(ge["#text"]=!0),ve&&$e(ge,["html","head","body"]),ge.table&&($e(ge,["tbody"]),delete ee.tbody),A.TRUSTED_TYPES_POLICY){if(typeof A.TRUSTED_TYPES_POLICY.createHTML!="function")throw Kn('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof A.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Kn('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const v=g;g=A.TRUSTED_TYPES_POLICY;try{w=P("")}catch(D){throw g=v,D}}else A.TRUSTED_TYPES_POLICY===null?(g=void 0,w=""):(g===void 0&&(g=j()),g&&typeof w=="string"&&(w=P("")));(ke.uponSanitizeElement.length>0||ke.uponSanitizeAttribute.length>0)&&ge===He&&(ge=qt(ge)),ke.uponSanitizeAttribute.length>0&&k===L&&(k=qt(k)),is&&is(A),gs=A},Ze=$e({},[...co,...uo,...Fw]),lt=$e({},[...fo,...$w]),Ot=function(A){let W=I(A);(!W||!W.tagName)&&(W={namespaceURI:Yt,tagName:"template"});const v=xi(A.tagName),D=xi(W.tagName);return Bs[A.namespaceURI]?A.namespaceURI===Fs?W.namespaceURI===Mt?v==="svg":W.namespaceURI===Ss?v==="svg"&&(D==="annotation-xml"||Us[D]):!!Ze[v]:A.namespaceURI===Ss?W.namespaceURI===Mt?v==="math":W.namespaceURI===Fs?v==="math"&&zt[D]:!!lt[v]:A.namespaceURI===Mt?W.namespaceURI===Fs&&!zt[D]||W.namespaceURI===Ss&&!Us[D]?!1:!lt[v]&&(Vn[v]||!Ze[v]):!!(Ct==="application/xhtml+xml"&&Bs[A.namespaceURI]):!1},Pt=function(A){Sa(t.removed,{element:A});try{I(A).removeChild(A)}catch{if(b(A),!I(A))throw Kn("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},ma=function(A){const W=E?E(A):A.childNodes;if(W){const D=[];on(W,B=>{Sa(D,B)}),on(D,B=>{try{b(B)}catch{}})}const v=m?m(A):null;if(v)for(let D=v.length-1;D>=0;--D){const B=v[D],ae=B&&B.name;if(typeof ae=="string")try{A.removeAttribute(ae)}catch{}}},Ts=function(A,W){try{Sa(t.removed,{attribute:W.getAttributeNode(A),from:W})}catch{Sa(t.removed,{attribute:null,from:W})}if(W.removeAttribute(A),A==="is")if(Le||De)try{Pt(W)}catch{}else try{W.setAttribute(A,"")}catch{}},ga=function(A){const W=m?m(A):A.attributes;if(W)for(let v=W.length-1;v>=0;--v){const D=W[v],B=D&&D.name;if(!(typeof B!="string"||k[Ye(B)]))try{A.removeAttribute(B)}catch{}}},ni=function(A){const W=[A];for(;W.length>0;){const v=W.pop();(_?_(v):v.nodeType)===Ks.element&&ga(v);const B=E?E(v):v.childNodes;if(B)for(let ae=B.length-1;ae>=0;--ae)W.push(B[ae])}},va=function(A){let W=null,v=null;if(Oe)A=""+A;else{const ae=qu(A,/^[\r\n\t ]+/);v=ae&&ae[0]}Ct==="application/xhtml+xml"&&Yt===Mt&&(A=''+A+"");const D=g?P(A):A;if(Yt===Mt)try{W=new d().parseFromString(D,Ct)}catch{}if(!W||!W.documentElement){W=U.createDocument(Yt,"template",null);try{W.documentElement.innerHTML=$s?w:D}catch{}}const B=W.body||W.documentElement;return A&&v&&B.insertBefore(s.createTextNode(v),B.childNodes[0]||null),Yt===Mt?Y.call(W,ve?"html":"body")[0]:ve?W.documentElement:B},ba=function(A){return O.call(A.ownerDocument||A,A,o.SHOW_ELEMENT|o.SHOW_COMMENT|o.SHOW_TEXT|o.SHOW_PROCESSING_INSTRUCTION|o.SHOW_CDATA_SECTION,null)},Gs=function(A){var W,v;A.normalize();const D=O.call(A.ownerDocument||A,A,o.SHOW_TEXT|o.SHOW_COMMENT|o.SHOW_CDATA_SECTION|o.SHOW_PROCESSING_INSTRUCTION,null);let B=D.nextNode();for(;B;){let Ne=B.data;on([ie,he,F],Ue=>{Ne=Ta(Ne,Ue," ")}),B.data=Ne,B=D.nextNode()}const ae=(W=(v=A.querySelectorAll)===null||v===void 0?void 0:v.call(A,"template"))!==null&&W!==void 0?W:[];on(Array.from(ae),Ne=>{pe(Ne.content)&&Gs(Ne.content)})},z=function(A){const W=S?S(A):null;return typeof W!="string"||Ye(W)!=="form"?!1:typeof A.nodeName!="string"||typeof A.textContent!="string"||typeof A.removeChild!="function"||A.attributes!==m(A)||typeof A.removeAttribute!="function"||typeof A.setAttribute!="function"||typeof A.namespaceURI!="string"||typeof A.insertBefore!="function"||typeof A.hasChildNodes!="function"||A.nodeType!==_(A)||A.childNodes!==E(A)},pe=function(A){if(!_||typeof A!="object"||A===null)return!1;try{return _(A)===Ks.documentFragment}catch{return!1}},_e=function(A){if(!_||typeof A!="object"||A===null)return!1;try{return typeof _(A)=="number"}catch{return!1}};function Nt(me,A,W){on(me,v=>{v.call(t,A,W,gs)})}const Cs=function(A){let W=null;if(Nt(ke.beforeSanitizeElements,A,null),z(A))return Pt(A),!0;const v=Ye(S?S(A):A.nodeName);if(Nt(ke.uponSanitizeElement,A,{tagName:v,allowedTags:ge}),fe&&A.hasChildNodes()&&!_e(A.firstElementChild)&&Ft(/<[/\w!]/g,A.innerHTML)&&Ft(/<[/\w!]/g,A.textContent)||fe&&A.namespaceURI===Mt&&v==="style"&&_e(A.firstElementChild)||A.nodeType===Ks.progressingInstruction||fe&&A.nodeType===Ks.comment&&Ft(/<[/\w]/g,A.data))return Pt(A),!0;if(ee[v]||!(X.tagCheck instanceof Function&&X.tagCheck(v))&&!ge[v]){if(!ee[v]&&ai(v)&&($.tagNameCheck instanceof RegExp&&Ft($.tagNameCheck,v)||$.tagNameCheck instanceof Function&&$.tagNameCheck(v)))return!1;if(xe&&!Ve[v]){const B=I(A),ae=E(A);if(ae&&B){const Ne=ae.length;for(let Ue=Ne-1;Ue>=0;--Ue){const et=Ce?ae[Ue]:p(ae[Ue],!0);B.insertBefore(et,y(A))}}}return Pt(A),!0}return(_?_(A):A.nodeType)===Ks.element&&!Ot(A)||(v==="noscript"||v==="noembed"||v==="noframes")&&Ft(/<\/no(script|embed|frames)/i,A.innerHTML)?(Pt(A),!0):(ne&&A.nodeType===Ks.text&&(W=A.textContent,on([ie,he,F],B=>{W=Ta(W,B," ")}),A.textContent!==W&&(Sa(t.removed,{element:A.cloneNode()}),A.textContent=W)),Nt(ke.afterSanitizeElements,A,null),!1)},jn=function(A,W,v){if(Z[W]||qe&&(W==="id"||W==="name")&&(v in s||v in q))return!1;const D=k[W]||X.attributeCheck instanceof Function&&X.attributeCheck(W,A);if(!(oe&&!Z[W]&&Ft(se,W))){if(!(ue&&Ft(Se,W))){if(!D||Z[W]){if(!(ai(A)&&($.tagNameCheck instanceof RegExp&&Ft($.tagNameCheck,A)||$.tagNameCheck instanceof Function&&$.tagNameCheck(A))&&($.attributeNameCheck instanceof RegExp&&Ft($.attributeNameCheck,W)||$.attributeNameCheck instanceof Function&&$.attributeNameCheck(W,A))||W==="is"&&$.allowCustomizedBuiltInElements&&($.tagNameCheck instanceof RegExp&&Ft($.tagNameCheck,v)||$.tagNameCheck instanceof Function&&$.tagNameCheck(v))))return!1}else if(!Ps[W]){if(!Ft(ye,Ta(v,de,""))){if(!((W==="src"||W==="xlink:href"||W==="href")&&A!=="script"&&Gu(v,"data:")===0&&pt[A])){if(!(le&&!Ft(V,Ta(v,de,"")))){if(v)return!1}}}}}}return!0},Br=$e({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),ai=function(A){return!Br[xi(A)]&&Ft(ce,A)},rl=function(A){Nt(ke.beforeSanitizeAttributes,A,null);const W=A.attributes;if(!W||z(A))return;const v={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:k,forceKeepAttr:void 0};let D=W.length;for(;D--;){const B=W[D],ae=B.name,Ne=B.namespaceURI,Ue=B.value,et=Ye(ae),Vt=Ue;let vt=ae==="value"?Vt:Iw(Vt);if(v.attrName=et,v.attrValue=vt,v.keepAttr=!0,v.forceKeepAttr=void 0,Nt(ke.uponSanitizeAttribute,A,v),vt=v.attrValue,ct&&(et==="id"||et==="name")&&Gu(vt,K)!==0&&(Ts(ae,A),vt=K+vt),fe&&Ft(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,vt)){Ts(ae,A);continue}if(et==="attributename"&&qu(vt,"href")){Ts(ae,A);continue}if(v.forceKeepAttr)continue;if(!v.keepAttr){Ts(ae,A);continue}if(!te&&Ft(/\/>/i,vt)){Ts(ae,A);continue}ne&&on([ie,he,F],id=>{vt=Ta(vt,id," ")});const li=Ye(A.nodeName);if(!jn(li,et,vt)){Ts(ae,A);continue}if(g&&typeof u=="object"&&typeof u.getAttributeType=="function"&&!Ne)switch(u.getAttributeType(li,et)){case"TrustedHTML":{vt=P(vt);break}case"TrustedScriptURL":{vt=R(vt);break}}if(vt!==Vt)try{Ne?A.setAttributeNS(Ne,ae,vt):A.setAttribute(ae,vt),z(A)?Pt(A):ju(t.removed)}catch{Ts(ae,A)}}Nt(ke.afterSanitizeAttributes,A,null)},ya=function(A){let W=null;const v=ba(A);for(Nt(ke.beforeSanitizeShadowDOM,A,null);W=v.nextNode();)if(Nt(ke.uponSanitizeShadowNode,W,null),Cs(W),rl(W),pe(W.content)&&ya(W.content),(_?_(W):W.nodeType)===Ks.element){const B=x?x(W):W.shadowRoot;pe(B)&&(ii(B),ya(B))}Nt(ke.afterSanitizeShadowDOM,A,null)},ii=function(A){const W=[{node:A,shadow:null}];for(;W.length>0;){const v=W.pop();if(v.shadow){ya(v.shadow);continue}const D=v.node,ae=(_?_(D):D.nodeType)===Ks.element,Ne=E?E(D):D.childNodes;if(Ne)for(let Ue=Ne.length-1;Ue>=0;--Ue)W.push({node:Ne[Ue],shadow:null});if(ae){const Ue=S?S(D):null;if(typeof Ue=="string"&&Ye(Ue)==="template"){const et=D.content;pe(et)&&W.push({node:et,shadow:null})}}if(ae){const Ue=x?x(D):D.shadowRoot;pe(Ue)&&W.push({node:null,shadow:Ue},{node:Ue,shadow:null})}}};return t.sanitize=function(me){let A=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},W=null,v=null,D=null,B=null;if($s=!me,$s&&(me=""),typeof me!="string"&&!_e(me)&&(me=Mw(me),typeof me!="string"))throw Kn("dirty is not a string, aborting");if(!t.isSupported)return me;Te||Ee(A),t.removed=[];const ae=Ce&&typeof me!="string"&&_e(me);if(ae){const et=S?S(me):me.nodeName;if(typeof et=="string"){const Vt=Ye(et);if(!ge[Vt]||ee[Vt])throw Kn("root node is forbidden and cannot be sanitized in-place")}if(z(me))throw Kn("root node is clobbered and cannot be sanitized in-place");try{ii(me)}catch(Vt){throw ma(me),Vt}}else if(_e(me))W=va(""),v=W.ownerDocument.importNode(me,!0),v.nodeType===Ks.element&&v.nodeName==="BODY"||v.nodeName==="HTML"?W=v:W.appendChild(v),ii(v);else{if(!Le&&!ne&&!ve&&me.indexOf("<")===-1)return g&&Be?P(me):me;if(W=va(me),!W)return Le?null:Be?w:""}W&&Oe&&Pt(W.firstChild);const Ne=ba(ae?me:W);try{for(;D=Ne.nextNode();)Cs(D),rl(D),pe(D.content)&&ya(D.content)}catch(et){throw ae&&ma(me),et}if(ae)return on(t.removed,et=>{et.element&&ni(et.element)}),ne&&Gs(me),me;if(Le){if(ne&&Gs(W),De)for(B=N.call(W.ownerDocument);W.firstChild;)B.appendChild(W.firstChild);else B=W;return(k.shadowroot||k.shadowrootmode)&&(B=we.call(n,B,!0)),B}let Ue=ve?W.outerHTML:W.innerHTML;return ve&&ge["!doctype"]&&W.ownerDocument&&W.ownerDocument.doctype&&W.ownerDocument.doctype.name&&Ft(Gw,W.ownerDocument.doctype.name)&&(Ue=" +`+Ue),ne&&on([ie,he,F],et=>{Ue=Ta(Ue,et," ")}),g&&Be?P(Ue):Ue},t.setConfig=function(){let me=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};Ee(me),Te=!0},t.clearConfig=function(){gs=null,Te=!1,g=T,w=""},t.isValidAttribute=function(me,A,W){gs||Ee({});const v=Ye(me),D=Ye(A);return jn(v,D,W)},t.addHook=function(me,A){typeof A=="function"&&Sa(ke[me],A)},t.removeHook=function(me,A){if(A!==void 0){const W=Aw(ke[me],A);return W===-1?void 0:Rw(ke[me],W,1)[0]}return ju(ke[me])},t.removeHooks=function(me){ke[me]=[]},t.removeAllHooks=function(){ke=ef()},t}var tf=Lm();function Jc(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var ha=Jc();function Dm(e){ha=e}var Ii={exec:()=>null};function at(e,t=""){let s=typeof e=="string"?e:e.source;const n={replace:(a,i)=>{let l=typeof i=="string"?i:i.source;return l=l.replace(ss.caret,"$1"),s=s.replace(a,l),n},getRegex:()=>new RegExp(s,t)};return n}var ss={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")},Jw=/^(?:[ \t]*(?:\n|$))+/,Yw=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Qw=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,ll=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Xw=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,Yc=/(?:[*+-]|\d{1,9}[.)])/,Mm=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,Pm=at(Mm).replace(/bull/g,Yc).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(),eS=at(Mm).replace(/bull/g,Yc).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(),Qc=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,tS=/^[^\n]+/,Xc=/(?!\s*\])(?:\\.|[^\[\]\\])+/,sS=at(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",Xc).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),nS=at(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,Yc).getRegex(),Fr="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",ed=/|$))/,aS=at("^ {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",ed).replace("tag",Fr).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Fm=at(Qc).replace("hr",ll).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",Fr).getRegex(),iS=at(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",Fm).getRegex(),td={blockquote:iS,code:Yw,def:sS,fences:Qw,heading:Xw,hr:ll,html:aS,lheading:Pm,list:nS,newline:Jw,paragraph:Fm,table:Ii,text:tS},sf=at("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",ll).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",Fr).getRegex(),lS={...td,lheading:eS,table:sf,paragraph:at(Qc).replace("hr",ll).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",sf).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",Fr).getRegex()},rS={...td,html:at(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",ed).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:Ii,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:at(Qc).replace("hr",ll).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",Pm).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},oS=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,cS=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,$m=/^( {2,}|\\)\n(?!\s*$)/,dS=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,Hm=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,mS=at(Hm,"u").replace(/punct/g,$r).getRegex(),gS=at(Hm,"u").replace(/punct/g,Um).getRegex(),zm="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",vS=at(zm,"gu").replace(/notPunctSpace/g,Bm).replace(/punctSpace/g,sd).replace(/punct/g,$r).getRegex(),bS=at(zm,"gu").replace(/notPunctSpace/g,pS).replace(/punctSpace/g,fS).replace(/punct/g,Um).getRegex(),yS=at("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,Bm).replace(/punctSpace/g,sd).replace(/punct/g,$r).getRegex(),xS=at(/\\(punct)/,"gu").replace(/punct/g,$r).getRegex(),_S=at(/^<(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(),kS=at(ed).replace("(?:-->|$)","-->").getRegex(),wS=at("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",kS).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),lr=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,SS=at(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",lr).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Vm=at(/^!?\[(label)\]\[(ref)\]/).replace("label",lr).replace("ref",Xc).getRegex(),jm=at(/^!?\[(ref)\](?:\[\])?/).replace("ref",Xc).getRegex(),TS=at("reflink|nolink(?!\\()","g").replace("reflink",Vm).replace("nolink",jm).getRegex(),nd={_backpedal:Ii,anyPunctuation:xS,autolink:_S,blockSkip:hS,br:$m,code:cS,del:Ii,emStrongLDelim:mS,emStrongRDelimAst:vS,emStrongRDelimUnd:yS,escape:oS,link:SS,nolink:jm,punctuation:uS,reflink:Vm,reflinkSearch:TS,tag:wS,text:dS,url:Ii},CS={...nd,link:at(/^!?\[(label)\]\((.*?)\)/).replace("label",lr).getRegex(),reflink:at(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",lr).getRegex()},Ko={...nd,emStrongRDelimAst:bS,emStrongLDelim:gS,url:at(/^((?: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]*?(?:(?=[\\":">",'"':""","'":"'"},nf=e=>AS[e];function Js(e,t){if(t){if(ss.escapeTest.test(e))return e.replace(ss.escapeReplace,nf)}else if(ss.escapeTestNoEncode.test(e))return e.replace(ss.escapeReplaceNoEncode,nf);return e}function af(e){try{e=encodeURI(e).replace(ss.percentDecode,"%")}catch{return null}return e}function lf(e,t){var i;const s=e.replace(ss.findPipe,(l,r,o)=>{let c=!1,d=r;for(;--d>=0&&o[d]==="\\";)c=!c;return c?"|":" |"}),n=s.split(ss.splitPipe);let a=0;if(n[0].trim()||n.shift(),n.length>0&&!((i=n.at(-1))!=null&&i.trim())&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0?-2:-1}function rf(e,t,s,n,a){const i=t.href,l=t.title||null,r=e[1].replace(a.other.outputLinkReplace,"$1");n.state.inLink=!0;const o={type:e[0].charAt(0)==="!"?"image":"link",raw:s,href:i,title:l,text:r,tokens:n.inlineTokens(r)};return n.state.inLink=!1,o}function IS(e,t,s){const n=e.match(s.other.indentCodeCompensation);if(n===null)return t;const a=n[1];return t.split(` `).map(i=>{const l=i.match(s.other.beginningSpace);if(l===null)return i;const[r]=l;return r.length>=a.length?i.slice(a.length):i}).join(` -`)}var lr=class{constructor(e){it(this,"options");it(this,"rules");it(this,"lexer");this.options=e||fa}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:ci(s,` -`)}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const s=t[0],n=IS(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:n}}}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 n=ci(s,"#");(this.options.pedantic||!n||this.rules.other.endingSpaceChar.test(n))&&(s=n.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:ci(t[0],` -`)}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let s=ci(t[0],` +`)}var rr=class{constructor(e){rt(this,"options");rt(this,"rules");rt(this,"lexer");this.options=e||ha}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:mi(s,` +`)}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const s=t[0],n=IS(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:n}}}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 n=mi(s,"#");(this.options.pedantic||!n||this.rules.other.endingSpaceChar.test(n))&&(s=n.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:mi(t[0],` +`)}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let s=mi(t[0],` `).split(` `),n="",a="";const i=[];for(;s.length>0;){let l=!1;const r=[];let o;for(o=0;o1,a={type:"list",raw:"",ordered:n,start:n?+s.slice(0,-1):"",loose:!1,items:[]};s=n?`\\d{1,9}\\${s.slice(-1)}`:`\\${s}`,this.options.pedantic&&(s=n?s:"[*+-]");const i=this.rules.other.listItemRegex(s);let l=!1;for(;e;){let o=!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,O=>" ".repeat(3*O.length)),f=e.split(` +`,1)[0].replace(this.rules.other.listReplaceTabs,I=>" ".repeat(3*I.length)),f=e.split(` `,1)[0],p=!u.trim(),b=0;if(this.options.pedantic?(b=2,d=u.trimStart()):p?b=t[1].length+1:(b=t[2].search(this.rules.other.nonSpaceChar),b=b>4?1:b,d=u.slice(b),b+=t[1].length),p&&this.rules.other.blankLine.test(f)&&(c+=f+` -`,e=e.substring(f.length+1),o=!0),!o){const O=this.rules.other.nextBulletRegex(b),x=this.rules.other.hrRegex(b),m=this.rules.other.fencesBeginRegex(b),_=this.rules.other.headingBeginRegex(b),S=this.rules.other.htmlBeginRegex(b);for(;e;){const g=e.split(` -`,1)[0];let w;if(f=g,this.options.pedantic?(f=f.replace(this.rules.other.listReplaceNesting," "),w=f):w=f.replace(this.rules.other.tabCharGlobal," "),m.test(f)||_.test(f)||S.test(f)||O.test(f)||x.test(f))break;if(w.search(this.rules.other.nonSpaceChar)>=b||!f.trim())d+=` +`,e=e.substring(f.length+1),o=!0),!o){const I=this.rules.other.nextBulletRegex(b),x=this.rules.other.hrRegex(b),m=this.rules.other.fencesBeginRegex(b),_=this.rules.other.headingBeginRegex(b),S=this.rules.other.htmlBeginRegex(b);for(;e;){const g=e.split(` +`,1)[0];let w;if(f=g,this.options.pedantic?(f=f.replace(this.rules.other.listReplaceNesting," "),w=f):w=f.replace(this.rules.other.tabCharGlobal," "),m.test(f)||_.test(f)||S.test(f)||I.test(f)||x.test(f))break;if(w.search(this.rules.other.nonSpaceChar)>=b||!f.trim())d+=` `+w.slice(b);else{if(p||u.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||m.test(u)||_.test(u)||x.test(u))break;d+=` `+f}!p&&!f.trim()&&(p=!0),c+=g+` -`,e=e.substring(g.length+1),u=w.slice(b)}}a.loose||(l?a.loose=!0:this.rules.other.doubleBlankLine.test(c)&&(l=!0));let y=null,A;this.options.gfm&&(y=this.rules.other.listIsTask.exec(d),y&&(A=y[0]!=="[ ] ",d=d.replace(this.rules.other.listReplaceTask,""))),a.items.push({type:"list_item",raw:c,task:!!y,checked:A,loose:!1,text:d,tokens:[]}),a.raw+=c}const r=a.items.at(-1);if(r)r.raw=r.raw.trimEnd(),r.text=r.text.trimEnd();else return;a.raw=a.raw.trimEnd();for(let o=0;ou.type==="space"),d=c.length>0&&c.some(u=>this.rules.other.anyLine.test(u.raw));a.loose=d}if(a.loose)for(let o=0;ou.type==="space"),d=c.length>0&&c.some(u=>this.rules.other.anyLine.test(u.raw));a.loose=d}if(a.loose)for(let o=0;o({text:o,tokens:this.lexer.inline(o),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=ci(s.slice(0,-1),"\\");if((s.length-i.length)%2===0)return}else{const i=RS(t[2],"()");if(i===-2)return;if(i>-1){const r=(t[0].indexOf("!")===0?5:4)+t[1].length+i;t[2]=t[2].substring(0,i),t[0]=t[0].substring(0,r).trim(),t[3]=""}}let n=t[2],a="";if(this.options.pedantic){const i=this.rules.other.pedanticHrefTitle.exec(n);i&&(n=i[1],a=i[3])}else a=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(s)?n=n.slice(1):n=n.slice(1,-1)),rf(t,{href:n&&n.replace(this.rules.inline.anyPunctuation,"$1"),title:a&&a.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 n=(s[2]||s[1]).replace(this.rules.other.multipleSpaceGlobal," "),a=t[n.toLowerCase()];if(!a){const i=s[0].charAt(0);return{type:"text",raw:i,text:i}}return rf(s,a,s[0],this.lexer,this.rules)}}emStrong(e,t,s=""){let n=this.rules.inline.emStrongLDelim.exec(e);if(!n||n[3]&&s.match(this.rules.other.unicodeAlphaNumeric))return;if(!(n[1]||n[2]||"")||!s||this.rules.inline.punctuation.exec(s)){const i=[...n[0]].length-1;let l,r,o=i,c=0;const d=n[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(d.lastIndex=0,t=t.slice(-1*e.length+i);(n=d.exec(t))!=null;){if(l=n[1]||n[2]||n[3]||n[4]||n[5]||n[6],!l)continue;if(r=[...l].length,n[3]||n[4]){o+=r;continue}else if((n[5]||n[6])&&i%3&&!((i+r)%3)){c+=r;continue}if(o-=r,o>0)continue;r=Math.min(r,r+o+c);const u=[...n[0]][0].length,f=e.slice(0,i+n.index+u+r);if(Math.min(i,r)%2){const b=f.slice(1,-1);return{type:"em",raw:f,text:b,tokens:this.lexer.inlineTokens(b)}}const p=f.slice(2,-2);return{type:"strong",raw:f,text:p,tokens:this.lexer.inlineTokens(p)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let s=t[2].replace(this.rules.other.newLineCharGlobal," ");const n=this.rules.other.nonSpaceChar.test(s),a=this.rules.other.startingSpaceChar.test(s)&&this.rules.other.endingSpaceChar.test(s);return n&&a&&(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,n;return t[2]==="@"?(s=t[1],n="mailto:"+s):(s=t[1],n=s),{type:"link",raw:t[0],text:s,href:n,tokens:[{type:"text",raw:s,text:s}]}}}url(e){var s;let t;if(t=this.rules.inline.url.exec(e)){let n,a;if(t[2]==="@")n=t[0],a="mailto:"+n;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]);n=t[0],t[1]==="www."?a="http://"+t[0]:a=t[0]}return{type:"link",raw:t[0],text:n,href:a,tokens:[{type:"text",raw:n,text:n}]}}}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}}}},hn=class Wo{constructor(t){it(this,"tokens");it(this,"options");it(this,"state");it(this,"tokenizer");it(this,"inlineQueue");this.tokens=[],this.tokens.links=Object.create(null),this.options=t||fa,this.options.tokenizer=this.options.tokenizer||new lr,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:Xt,block:Sl.normal,inline:oi.normal};this.options.pedantic?(s.block=Sl.pedantic,s.inline=oi.pedantic):this.options.gfm&&(s.block=Sl.gfm,this.options.breaks?s.inline=oi.breaks:s.inline=oi.gfm),this.tokenizer.rules=s}static get rules(){return{block:Sl,inline:oi}}static lex(t,s){return new Wo(s).lex(t)}static lexInline(t,s){return new Wo(s).inlineTokens(t)}lex(t){t=t.replace(Xt.carriageReturn,` -`),this.blockTokens(t,this.tokens);for(let s=0;s(r=c.call({lexer:this},t,s))?(t=t.substring(r.raw.length),s.push(r),!0):!1))continue;if(r=this.tokenizer.space(t)){t=t.substring(r.raw.length);const c=s.at(-1);r.raw.length===1&&c!==void 0?c.raw+=` +`?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=mi(s.slice(0,-1),"\\");if((s.length-i.length)%2===0)return}else{const i=RS(t[2],"()");if(i===-2)return;if(i>-1){const r=(t[0].indexOf("!")===0?5:4)+t[1].length+i;t[2]=t[2].substring(0,i),t[0]=t[0].substring(0,r).trim(),t[3]=""}}let n=t[2],a="";if(this.options.pedantic){const i=this.rules.other.pedanticHrefTitle.exec(n);i&&(n=i[1],a=i[3])}else a=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(s)?n=n.slice(1):n=n.slice(1,-1)),rf(t,{href:n&&n.replace(this.rules.inline.anyPunctuation,"$1"),title:a&&a.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 n=(s[2]||s[1]).replace(this.rules.other.multipleSpaceGlobal," "),a=t[n.toLowerCase()];if(!a){const i=s[0].charAt(0);return{type:"text",raw:i,text:i}}return rf(s,a,s[0],this.lexer,this.rules)}}emStrong(e,t,s=""){let n=this.rules.inline.emStrongLDelim.exec(e);if(!n||n[3]&&s.match(this.rules.other.unicodeAlphaNumeric))return;if(!(n[1]||n[2]||"")||!s||this.rules.inline.punctuation.exec(s)){const i=[...n[0]].length-1;let l,r,o=i,c=0;const d=n[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(d.lastIndex=0,t=t.slice(-1*e.length+i);(n=d.exec(t))!=null;){if(l=n[1]||n[2]||n[3]||n[4]||n[5]||n[6],!l)continue;if(r=[...l].length,n[3]||n[4]){o+=r;continue}else if((n[5]||n[6])&&i%3&&!((i+r)%3)){c+=r;continue}if(o-=r,o>0)continue;r=Math.min(r,r+o+c);const u=[...n[0]][0].length,f=e.slice(0,i+n.index+u+r);if(Math.min(i,r)%2){const b=f.slice(1,-1);return{type:"em",raw:f,text:b,tokens:this.lexer.inlineTokens(b)}}const p=f.slice(2,-2);return{type:"strong",raw:f,text:p,tokens:this.lexer.inlineTokens(p)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let s=t[2].replace(this.rules.other.newLineCharGlobal," ");const n=this.rules.other.nonSpaceChar.test(s),a=this.rules.other.startingSpaceChar.test(s)&&this.rules.other.endingSpaceChar.test(s);return n&&a&&(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,n;return t[2]==="@"?(s=t[1],n="mailto:"+s):(s=t[1],n=s),{type:"link",raw:t[0],text:s,href:n,tokens:[{type:"text",raw:s,text:s}]}}}url(e){var s;let t;if(t=this.rules.inline.url.exec(e)){let n,a;if(t[2]==="@")n=t[0],a="mailto:"+n;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]);n=t[0],t[1]==="www."?a="http://"+t[0]:a=t[0]}return{type:"link",raw:t[0],text:n,href:a,tokens:[{type:"text",raw:n,text:n}]}}}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}}}},gn=class Wo{constructor(t){rt(this,"tokens");rt(this,"options");rt(this,"state");rt(this,"tokenizer");rt(this,"inlineQueue");this.tokens=[],this.tokens.links=Object.create(null),this.options=t||ha,this.options.tokenizer=this.options.tokenizer||new rr,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:ss,block:Tl.normal,inline:hi.normal};this.options.pedantic?(s.block=Tl.pedantic,s.inline=hi.pedantic):this.options.gfm&&(s.block=Tl.gfm,this.options.breaks?s.inline=hi.breaks:s.inline=hi.gfm),this.tokenizer.rules=s}static get rules(){return{block:Tl,inline:hi}}static lex(t,s){return new Wo(s).lex(t)}static lexInline(t,s){return new Wo(s).inlineTokens(t)}lex(t){t=t.replace(ss.carriageReturn,` +`),this.blockTokens(t,this.tokens);for(let s=0;s(r=c.call({lexer:this},t,s))?(t=t.substring(r.raw.length),s.push(r),!0):!1))continue;if(r=this.tokenizer.space(t)){t=t.substring(r.raw.length);const c=s.at(-1);r.raw.length===1&&c!==void 0?c.raw+=` `:s.push(r);continue}if(r=this.tokenizer.code(t)){t=t.substring(r.raw.length);const c=s.at(-1);(c==null?void 0:c.type)==="paragraph"||(c==null?void 0:c.type)==="text"?(c.raw+=` `+r.raw,c.text+=` `+r.text,this.inlineQueue.at(-1).src=c.text):s.push(r);continue}if(r=this.tokenizer.fences(t)){t=t.substring(r.raw.length),s.push(r);continue}if(r=this.tokenizer.heading(t)){t=t.substring(r.raw.length),s.push(r);continue}if(r=this.tokenizer.hr(t)){t=t.substring(r.raw.length),s.push(r);continue}if(r=this.tokenizer.blockquote(t)){t=t.substring(r.raw.length),s.push(r);continue}if(r=this.tokenizer.list(t)){t=t.substring(r.raw.length),s.push(r);continue}if(r=this.tokenizer.html(t)){t=t.substring(r.raw.length),s.push(r);continue}if(r=this.tokenizer.def(t)){t=t.substring(r.raw.length);const c=s.at(-1);(c==null?void 0:c.type)==="paragraph"||(c==null?void 0:c.type)==="text"?(c.raw+=` @@ -5664,16 +5752,16 @@ ${d}`:d;const u=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.block `+r.raw,c.text+=` `+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=c.text):s.push(r),n=o.length!==t.length,t=t.substring(r.raw.length);continue}if(r=this.tokenizer.text(t)){t=t.substring(r.raw.length);const c=s.at(-1);(c==null?void 0:c.type)==="text"?(c.raw+=` `+r.raw,c.text+=` -`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=c.text):s.push(r);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 r,o,c;let n=t,a=null;if(this.tokens.links){const d=Object.keys(this.tokens.links);if(d.length>0)for(;(a=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)d.includes(a[0].slice(a[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,a.index)+"["+"a".repeat(a[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(a=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,a.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(a=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,a.index)+"["+"a".repeat(a[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,l="";for(;t;){i||(l=""),i=!1;let d;if((o=(r=this.options.extensions)==null?void 0:r.inline)!=null&&o.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,n,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 p=t.slice(1);let b;this.options.extensions.startInline.forEach(y=>{b=y.call({lexer:this},p),typeof b=="number"&&b>=0&&(f=Math.min(f,b))}),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}},rr=class{constructor(e){it(this,"options");it(this,"parser");this.options=e||fa}space(e){return""}code({text:e,lang:t,escaped:s}){var i;const n=(i=(t||"").match(Xt.notSpaceStart))==null?void 0:i[0],a=e.replace(Xt.endingNewline,"")+` -`;return n?'
'+(s?a:qs(a,!0))+`
-`:"
"+(s?a:qs(a,!0))+`
+`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=c.text):s.push(r);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 r,o,c;let n=t,a=null;if(this.tokens.links){const d=Object.keys(this.tokens.links);if(d.length>0)for(;(a=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)d.includes(a[0].slice(a[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,a.index)+"["+"a".repeat(a[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(a=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,a.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(a=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,a.index)+"["+"a".repeat(a[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,l="";for(;t;){i||(l=""),i=!1;let d;if((o=(r=this.options.extensions)==null?void 0:r.inline)!=null&&o.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,n,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 p=t.slice(1);let b;this.options.extensions.startInline.forEach(y=>{b=y.call({lexer:this},p),typeof b=="number"&&b>=0&&(f=Math.min(f,b))}),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){rt(this,"options");rt(this,"parser");this.options=e||ha}space(e){return""}code({text:e,lang:t,escaped:s}){var i;const n=(i=(t||"").match(ss.notSpaceStart))==null?void 0:i[0],a=e.replace(ss.endingNewline,"")+` +`;return n?'
'+(s?a:Js(a,!0))+`
+`:"
"+(s?a:Js(a,!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 n="";for(let l=0;l `+n+" -`}listitem(e){var s;let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?((s=e.tokens[0])==null?void 0:s.type)==="paragraph"?(e.tokens[0].text=n+" "+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=n+" "+qs(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • +`}listitem(e){var s;let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?((s=e.tokens[0])==null?void 0:s.type)==="paragraph"?(e.tokens[0].text=n+" "+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=n+" "+Js(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}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 a=0;a${n}`),` @@ -5682,9 +5770,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`${qs(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:s}){const n=this.parser.parseInline(s),a=af(e);if(a===null)return n;e=a;let i='",i}image({href:e,title:t,text:s,tokens:n}){n&&(s=this.parser.parseInline(n,this.parser.textRenderer));const a=af(e);if(a===null)return qs(s);e=a;let i=`${s}{const o=l[r].flat(1/0);s=s.concat(this.walkTokens(o,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 n={...s};if(n.async=this.defaults.async||n.async||!1,s.extensions&&(s.extensions.forEach(a=>{if(!a.name)throw new Error("extension name required");if("renderer"in a){const i=t.renderers[a.name];i?t.renderers[a.name]=function(...l){let r=a.renderer.apply(this,l);return r===!1&&(r=i.apply(this,l)),r}:t.renderers[a.name]=a.renderer}if("tokenizer"in a){if(!a.level||a.level!=="block"&&a.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");const i=t[a.level];i?i.unshift(a.tokenizer):t[a.level]=[a.tokenizer],a.start&&(a.level==="block"?t.startBlock?t.startBlock.push(a.start):t.startBlock=[a.start]:a.level==="inline"&&(t.startInline?t.startInline.push(a.start):t.startInline=[a.start]))}"childTokens"in a&&a.childTokens&&(t.childTokens[a.name]=a.childTokens)}),n.extensions=t),s.renderer){const a=this.defaults.renderer||new rr(this.defaults);for(const i in s.renderer){if(!(i in a))throw new Error(`renderer '${i}' does not exist`);if(["options","parser"].includes(i))continue;const l=i,r=s.renderer[l],o=a[l];a[l]=(...c)=>{let d=r.apply(a,c);return d===!1&&(d=o.apply(a,c)),d||""}}n.renderer=a}if(s.tokenizer){const a=this.defaults.tokenizer||new lr(this.defaults);for(const i in s.tokenizer){if(!(i in a))throw new Error(`tokenizer '${i}' does not exist`);if(["options","rules","lexer"].includes(i))continue;const l=i,r=s.tokenizer[l],o=a[l];a[l]=(...c)=>{let d=r.apply(a,c);return d===!1&&(d=o.apply(a,c)),d}}n.tokenizer=a}if(s.hooks){const a=this.defaults.hooks||new Ol;for(const i in s.hooks){if(!(i in a))throw new Error(`hook '${i}' does not exist`);if(["options","block"].includes(i))continue;const l=i,r=s.hooks[l],o=a[l];Ol.passThroughHooks.has(i)?a[l]=c=>{if(this.defaults.async)return Promise.resolve(r.call(a,c)).then(u=>o.call(a,u));const d=r.call(a,c);return o.call(a,d)}:a[l]=(...c)=>{let d=r.apply(a,c);return d===!1&&(d=o.apply(a,c)),d}}n.hooks=a}if(s.walkTokens){const a=this.defaults.walkTokens,i=s.walkTokens;n.walkTokens=function(l){let r=[];return r.push(i.call(this,l)),a&&(r=r.concat(a.call(this,l))),r}}this.defaults={...this.defaults,...n}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return hn.lex(e,t??this.defaults)}parser(e,t){return mn.parse(e,t??this.defaults)}parseMarkdown(e){return(s,n)=>{const a={...n},i={...this.defaults,...a},l=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&a.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 r=i.hooks?i.hooks.provideLexer():e?hn.lex:hn.lexInline,o=i.hooks?i.hooks.provideParser():e?mn.parse:mn.parseInline;if(i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(s):s).then(c=>r(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=>o(c,i)).then(c=>i.hooks?i.hooks.postprocess(c):c).catch(l);try{i.hooks&&(s=i.hooks.preprocess(s));let c=r(s,i);i.hooks&&(c=i.hooks.processAllTokens(c)),i.walkTokens&&this.walkTokens(c,i.walkTokens);let d=o(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 n="

    An error occurred:

    "+qs(s.message+"",!0)+"
    ";return t?Promise.resolve(n):n}if(t)return Promise.reject(s);throw s}}},la=new OS;function tt(e,t){return la.parse(e,t)}tt.options=tt.setOptions=function(e){return la.setOptions(e),tt.defaults=la.defaults,Dm(tt.defaults),tt};tt.getDefaults=Jc;tt.defaults=fa;tt.use=function(...e){return la.use(...e),tt.defaults=la.defaults,Dm(tt.defaults),tt};tt.walkTokens=function(e,t){return la.walkTokens(e,t)};tt.parseInline=la.parseInline;tt.Parser=mn;tt.parser=mn.parse;tt.Renderer=rr;tt.TextRenderer=ad;tt.Lexer=hn;tt.lexer=hn.lex;tt.Tokenizer=lr;tt.Hooks=Ol;tt.parse=tt;tt.options;tt.setOptions;tt.use;tt.walkTokens;tt.parseInline;mn.parse;hn.lex;const NS={breaks:!0,gfm:!0};function of(e){if(!e)return"";try{if(typeof tt<"u"&&tt.parse){const t=tt.parse(e,NS);return typeof tf<"u"?tf.sanitize(t):t}}catch{}return e.replace(/&/g,"&").replace(//g,">").replace(/\n/g,"
    ")}function LS(e){const t=new Date(e),s=t.getHours().toString().padStart(2,"0"),n=t.getMinutes().toString().padStart(2,"0");return`${s}:${n}`}const DS={run_command:"terminal",ssh_command:"terminal",run_script:"terminal",read_file:"file",write_file:"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 MS(e){return DS[e]||"wrench"}const PS=/https?:\/\/\S+\.(?:png|jpg|jpeg|gif|webp|svg)(?:\?\S*)?/gi;function cf(e){if(!e)return[];const t=e.match(PS);return t?[...new Set(t)]:[]}const FS={template:` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${Js(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:s}){const n=this.parser.parseInline(s),a=af(e);if(a===null)return n;e=a;let i='
    ",i}image({href:e,title:t,text:s,tokens:n}){n&&(s=this.parser.parseInline(n,this.parser.textRenderer));const a=af(e);if(a===null)return Js(s);e=a;let i=`${s}{const o=l[r].flat(1/0);s=s.concat(this.walkTokens(o,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 n={...s};if(n.async=this.defaults.async||n.async||!1,s.extensions&&(s.extensions.forEach(a=>{if(!a.name)throw new Error("extension name required");if("renderer"in a){const i=t.renderers[a.name];i?t.renderers[a.name]=function(...l){let r=a.renderer.apply(this,l);return r===!1&&(r=i.apply(this,l)),r}:t.renderers[a.name]=a.renderer}if("tokenizer"in a){if(!a.level||a.level!=="block"&&a.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");const i=t[a.level];i?i.unshift(a.tokenizer):t[a.level]=[a.tokenizer],a.start&&(a.level==="block"?t.startBlock?t.startBlock.push(a.start):t.startBlock=[a.start]:a.level==="inline"&&(t.startInline?t.startInline.push(a.start):t.startInline=[a.start]))}"childTokens"in a&&a.childTokens&&(t.childTokens[a.name]=a.childTokens)}),n.extensions=t),s.renderer){const a=this.defaults.renderer||new or(this.defaults);for(const i in s.renderer){if(!(i in a))throw new Error(`renderer '${i}' does not exist`);if(["options","parser"].includes(i))continue;const l=i,r=s.renderer[l],o=a[l];a[l]=(...c)=>{let d=r.apply(a,c);return d===!1&&(d=o.apply(a,c)),d||""}}n.renderer=a}if(s.tokenizer){const a=this.defaults.tokenizer||new rr(this.defaults);for(const i in s.tokenizer){if(!(i in a))throw new Error(`tokenizer '${i}' does not exist`);if(["options","rules","lexer"].includes(i))continue;const l=i,r=s.tokenizer[l],o=a[l];a[l]=(...c)=>{let d=r.apply(a,c);return d===!1&&(d=o.apply(a,c)),d}}n.tokenizer=a}if(s.hooks){const a=this.defaults.hooks||new Nl;for(const i in s.hooks){if(!(i in a))throw new Error(`hook '${i}' does not exist`);if(["options","block"].includes(i))continue;const l=i,r=s.hooks[l],o=a[l];Nl.passThroughHooks.has(i)?a[l]=c=>{if(this.defaults.async)return Promise.resolve(r.call(a,c)).then(u=>o.call(a,u));const d=r.call(a,c);return o.call(a,d)}:a[l]=(...c)=>{let d=r.apply(a,c);return d===!1&&(d=o.apply(a,c)),d}}n.hooks=a}if(s.walkTokens){const a=this.defaults.walkTokens,i=s.walkTokens;n.walkTokens=function(l){let r=[];return r.push(i.call(this,l)),a&&(r=r.concat(a.call(this,l))),r}}this.defaults={...this.defaults,...n}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return gn.lex(e,t??this.defaults)}parser(e,t){return vn.parse(e,t??this.defaults)}parseMarkdown(e){return(s,n)=>{const a={...n},i={...this.defaults,...a},l=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&a.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 r=i.hooks?i.hooks.provideLexer():e?gn.lex:gn.lexInline,o=i.hooks?i.hooks.provideParser():e?vn.parse:vn.parseInline;if(i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(s):s).then(c=>r(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=>o(c,i)).then(c=>i.hooks?i.hooks.postprocess(c):c).catch(l);try{i.hooks&&(s=i.hooks.preprocess(s));let c=r(s,i);i.hooks&&(c=i.hooks.processAllTokens(c)),i.walkTokens&&this.walkTokens(c,i.walkTokens);let d=o(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 n="

    An error occurred:

    "+Js(s.message+"",!0)+"
    ";return t?Promise.resolve(n):n}if(t)return Promise.reject(s);throw s}}},oa=new OS;function st(e,t){return oa.parse(e,t)}st.options=st.setOptions=function(e){return oa.setOptions(e),st.defaults=oa.defaults,Dm(st.defaults),st};st.getDefaults=Jc;st.defaults=ha;st.use=function(...e){return oa.use(...e),st.defaults=oa.defaults,Dm(st.defaults),st};st.walkTokens=function(e,t){return oa.walkTokens(e,t)};st.parseInline=oa.parseInline;st.Parser=vn;st.parser=vn.parse;st.Renderer=or;st.TextRenderer=ad;st.Lexer=gn;st.lexer=gn.lex;st.Tokenizer=rr;st.Hooks=Nl;st.parse=st;st.options;st.setOptions;st.use;st.walkTokens;st.parseInline;vn.parse;gn.lex;const NS={breaks:!0,gfm:!0};function of(e){if(!e)return"";try{if(typeof st<"u"&&st.parse){const t=st.parse(e,NS);return typeof tf<"u"?tf.sanitize(t):t}}catch{}return e.replace(/&/g,"&").replace(//g,">").replace(/\n/g,"
    ")}function LS(e){const t=new Date(e),s=t.getHours().toString().padStart(2,"0"),n=t.getMinutes().toString().padStart(2,"0");return`${s}:${n}`}const DS={run_command:"terminal",ssh_command:"terminal",run_script:"terminal",read_file:"file",write_file:"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 MS(e){return DS[e]||"wrench"}const PS=/https?:\/\/\S+\.(?:png|jpg|jpeg|gif|webp|svg)(?:\?\S*)?/gi;function cf(e){if(!e)return[];const t=e.match(PS);return t?[...new Set(t)]:[]}const FS={template:`
    @@ -5847,7 +5935,7 @@ Please report this to https://github.com/markedjs/marked.`,e){const n="

    An err

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

    Personality

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

    An err

    - `},_t=(e,t)=>s=>({path:e,query:{...s.query,tab:t}}),qm=[{path:"/",redirect:"/dashboard"},{path:"/dashboard",component:yw,meta:{label:"Dashboard",icon:"dashboard",section:"Workspace",description:"System posture and recent activity"}},{path:"/chat",component:FS,meta:{label:"Chat",icon:"chat",section:"Workspace",description:"Direct operator conversation"}},{path:"/operations",component:uk,meta:{label:"Operations",icon:"operations",section:"Operate",description:"Execution, agents, loops, processes, and schedules"}},{path:"/history",component:bk,meta:{label:"History",icon:"history",section:"Observe",description:"Audit trail, sessions, traces, and usage"}},{path:"/capabilities",component:Ek,meta:{label:"Capabilities",icon:"capabilities",section:"Manage",description:"Tools, skills, knowledge, and memory"}},{path:"/personality",component:$S,meta:{label:"Personality",icon:"personality",section:"Manage",description:"Behavior and response profile"}},{path:"/system",component:fw,meta:{label:"System",icon:"system",section:"Manage",description:"Health, configuration, access, and updates"}},{path:"/execution",redirect:_t("/operations","live")},{path:"/agents",redirect:_t("/operations","agents")},{path:"/loops",redirect:_t("/operations","loops")},{path:"/processes",redirect:_t("/operations","processes")},{path:"/schedules",redirect:_t("/operations","schedules")},{path:"/audit",redirect:_t("/history","audit")},{path:"/sessions",redirect:_t("/history","sessions")},{path:"/traces",redirect:_t("/history","traces")},{path:"/usage",redirect:_t("/history","usage")},{path:"/tools",redirect:_t("/capabilities","tools")},{path:"/skills",redirect:_t("/capabilities","skills")},{path:"/knowledge",redirect:_t("/capabilities","knowledge")},{path:"/memory",redirect:_t("/capabilities","memory")},{path:"/learned",redirect:_t("/capabilities","learned")},{path:"/health",redirect:_t("/system","health")},{path:"/resources",redirect:_t("/system","resources")},{path:"/logs",redirect:_t("/system","logs")},{path:"/config",redirect:_t("/system","config")},{path:"/host-access",redirect:_t("/system","host-access")},{path:"/internals",redirect:_t("/system","internals")}],Ti=J_({history:A_(),routes:qm});Ti.afterEach(e=>{var s;const t=(s=e.meta)==null?void 0:s.label;document.title=t?`Odin — ${t}`:"Odin — Management"});const US={template:` + `},_t=(e,t)=>s=>({path:e,query:{...s.query,tab:t}}),qm=[{path:"/",redirect:"/dashboard"},{path:"/dashboard",component:yw,meta:{label:"Dashboard",icon:"dashboard",section:"Workspace",description:"System posture and recent activity"}},{path:"/chat",component:FS,meta:{label:"Chat",icon:"chat",section:"Workspace",description:"Direct operator conversation"}},{path:"/operations",component:uk,meta:{label:"Operations",icon:"operations",section:"Operate",description:"Execution, agents, loops, processes, and schedules"}},{path:"/history",component:bk,meta:{label:"History",icon:"history",section:"Observe",description:"Audit trail, sessions, traces, and usage"}},{path:"/capabilities",component:Ek,meta:{label:"Capabilities",icon:"capabilities",section:"Manage",description:"Tools, skills, knowledge, and memory"}},{path:"/personality",component:$S,meta:{label:"Personality",icon:"personality",section:"Manage",description:"Behavior and response profile"}},{path:"/system",component:fw,meta:{label:"System",icon:"system",section:"Manage",description:"Health, configuration, access, and updates"}},{path:"/execution",redirect:_t("/operations","live")},{path:"/agents",redirect:_t("/operations","agents")},{path:"/loops",redirect:_t("/operations","loops")},{path:"/processes",redirect:_t("/operations","processes")},{path:"/schedules",redirect:_t("/operations","schedules")},{path:"/audit",redirect:_t("/history","audit")},{path:"/sessions",redirect:_t("/history","sessions")},{path:"/traces",redirect:_t("/history","traces")},{path:"/usage",redirect:_t("/history","usage")},{path:"/tools",redirect:_t("/capabilities","tools")},{path:"/skills",redirect:_t("/capabilities","skills")},{path:"/knowledge",redirect:_t("/capabilities","knowledge")},{path:"/memory",redirect:_t("/capabilities","memory")},{path:"/learned",redirect:_t("/capabilities","learned")},{path:"/health",redirect:_t("/system","health")},{path:"/resources",redirect:_t("/system","resources")},{path:"/logs",redirect:_t("/system","logs")},{path:"/config",redirect:_t("/system","config")},{path:"/host-access",redirect:_t("/system","host-access")},{path:"/internals",redirect:_t("/system","internals")}],Oi=J_({history:A_(),routes:qm});Oi.afterEach(e=>{var s;const t=(s=e.meta)==null?void 0:s.label;document.title=t?`Odin — ${t}`:"Odin — Management"});const BS={template:`
    @@ -5977,7 +6065,7 @@ Please report this to https://github.com/markedjs/marked.`,e){const n="

    An err

    -
    `,props:["onLogin","sessionExpired"],setup(e){const t=h(""),s=h(null),n=h(!1),a=h(!1);async function i(){n.value=!0,s.value=null;try{K.setPersist(a.value),await K.login(t.value),e.onLogin()}catch(l){s.value=l.message||"Login failed"}finally{n.value=!1}}return{token:t,error:s,busy:n,persist:a,login:i}}},BS={template:` + `,props:["onLogin","sessionExpired"],setup(e){const t=h(""),s=h(null),n=h(!1),a=h(!1);async function i(){n.value=!0,s.value=null;try{G.setPersist(a.value),await G.login(t.value),e.onLogin()}catch(l){s.value=l.message||"Login failed"}finally{n.value=!1}}return{token:t,error:s,busy:n,persist:a,login:i}}},US={template:`
    Loading application... @@ -6075,4 +6163,4 @@ Please report this to https://github.com/markedjs/marked.`,e){const n="

    An err

    - `,setup(){const e=h("checking"),t=h(!1),s=h(!1),n=h(!1),a=h(null),i=h(null),l=h(!1);let r=null,o=null;const c=h(!1),d=h("disconnected"),u=h(-1),f=h(null);let p=null;const b=h("starting"),y=h(""),A=qm.filter(N=>N.meta),O=J(()=>["Workspace","Operate","Observe","Manage"].map(N=>({name:N,routes:A.filter(L=>L.meta.section===N)})).filter(N=>N.routes.length)),x=J(()=>{var N;return((N=Ti.currentRoute.value.meta)==null?void 0:N.label)||"Odin"}),m=J(()=>{var N;return((N=Ti.currentRoute.value.meta)==null?void 0:N.section)||"Management"}),_=J(()=>{var N;return((N=Ti.currentRoute.value.meta)==null?void 0:N.description)||"Management console"});K.onSessionExpired=()=>{t.value=!0,Ke.disconnect(),K.setToken(""),e.value="login"};function S(N){var L;if((N.ctrlKey||N.metaKey)&&N.key.toLowerCase()==="k"){e.value==="ready"&&(N.preventDefault(),Bu());return}if(n.value&&N.key==="Tab"){const Z=[...((L=a.value)==null?void 0:L.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'))||[]];if(Z.length){const xe=Z[0],_e=Z[Z.length-1];if(N.shiftKey&&(document.activeElement===xe||!a.value.contains(document.activeElement))){N.preventDefault(),_e.focus();return}if(!N.shiftKey&&(document.activeElement===_e||!a.value.contains(document.activeElement))){N.preventDefault(),xe.focus();return}}}if(N.key==="Escape"&&n.value){n.value=!1,N.preventDefault();return}if(N.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(N.target.tagName)){N.preventDefault();const Z=document.querySelector('.hm-main input[type="text"], .hm-main .hm-input:not(textarea):not(select)');Z&&Z.focus()}}function g(){l.value=!!(r!=null&&r.matches),l.value||(n.value=!1)}We(async()=>{document.addEventListener("keydown",S),r=window.matchMedia("(max-width: 900px)"),g(),r.addEventListener("change",g);const N=await K.check();N.ok?(e.value="ready",Y()):N.needsAuth?e.value="login":(e.value="ready",Y())});function w(){t.value=!1,e.value="ready",Y()}async function T(){await K.logout(),Ke.disconnect(),e.value="login"}function C(){s.value=!s.value}function M(){n.value=!n.value}es(n,async N=>{var L,Z;if(N)o=document.activeElement,await At(),(Z=(L=a.value)==null?void 0:L.querySelector(".nav-item"))==null||Z.focus();else if(o!=null&&o.isConnected){const xe=o;o=null,requestAnimationFrame(()=>xe.focus())}});const B=J(()=>{switch(d.value){case"connected":return"Live";case"connecting":return"Connecting…";case"reconnecting":return"Reconnecting…";default:return"Disconnected"}});function $(N,L="info",Z=3e3){f.value={text:N,level:L},clearTimeout(p),p=setTimeout(()=>{f.value=null},Z)}let I=null,j=!1;function Y(){Ke.onStatusChange=N=>{c.value=N},Ke.onLatency=N=>{u.value=N},Ke.onStateChange=(N,L)=>{d.value=N,N==="connected"?(j&&$("Connection restored","success"),j=!0):N==="reconnecting"&&L.attempt===1&&$("Connection lost — reconnecting…","warn")},Ke.connect(),H(),I&&clearInterval(I),I=setInterval(H,15e3)}async function H(){try{const N=await K.get("/api/status");b.value=N.status==="online"?"online":"starting";const L=N.uptime_seconds||0,Z=Math.floor(L/3600),xe=Math.floor(L%3600/60);y.value=`${Z}h ${xe}m uptime`}catch{b.value="offline",y.value=""}}return xt(()=>{I&&clearInterval(I),Ke.disconnect(),document.removeEventListener("keydown",S),r==null||r.removeEventListener("change",g)}),{authState:e,sessionExpired:t,sidebarCollapsed:s,mobileOpen:n,wsConnected:c,wsState:d,wsLatency:u,wsLabel:B,wsToast:f,botStatus:b,botUptime:y,navRoutes:A,navGroups:O,currentPage:x,currentSection:m,currentDescription:_,sidebarEl:a,mobileMenuButton:i,isMobileViewport:l,onLogin:w,logout:T,toggleSidebar:C,toggleMobileNavigation:M,openPalette:Bu}}},Bn=Zl(BS);Bn.component("odin-icon",gw);Bn.component("login-screen",US);Bn.component("toast-container",V0);Bn.component("confirm-host",j0);Bn.component("command-palette",mw);Bn.directive("modal-focus",bw);Bn.use(Ti);Bn.mount("#app"); + `,setup(){const e=h("checking"),t=h(!1),s=h(!1),n=h(!1),a=h(null),i=h(null),l=h(!1);let r=null,o=null;const c=h(!1),d=h("disconnected"),u=h(-1),f=h(null);let p=null;const b=h("starting"),y=h(""),E=qm.filter(O=>O.meta),I=J(()=>["Workspace","Operate","Observe","Manage"].map(O=>({name:O,routes:E.filter(N=>N.meta.section===O)})).filter(O=>O.routes.length)),x=J(()=>{var O;return((O=Oi.currentRoute.value.meta)==null?void 0:O.label)||"Odin"}),m=J(()=>{var O;return((O=Oi.currentRoute.value.meta)==null?void 0:O.section)||"Management"}),_=J(()=>{var O;return((O=Oi.currentRoute.value.meta)==null?void 0:O.description)||"Management console"});G.onSessionExpired=()=>{t.value=!0,Ke.disconnect(),G.setToken(""),e.value="login"};function S(O){var N;if((O.ctrlKey||O.metaKey)&&O.key.toLowerCase()==="k"){e.value==="ready"&&(O.preventDefault(),Uu());return}if(n.value&&O.key==="Tab"){const Y=[...((N=a.value)==null?void 0:N.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'))||[]];if(Y.length){const we=Y[0],ke=Y[Y.length-1];if(O.shiftKey&&(document.activeElement===we||!a.value.contains(document.activeElement))){O.preventDefault(),ke.focus();return}if(!O.shiftKey&&(document.activeElement===ke||!a.value.contains(document.activeElement))){O.preventDefault(),we.focus();return}}}if(O.key==="Escape"&&n.value){n.value=!1,O.preventDefault();return}if(O.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(O.target.tagName)){O.preventDefault();const Y=document.querySelector('.hm-main input[type="text"], .hm-main .hm-input:not(textarea):not(select)');Y&&Y.focus()}}function g(){l.value=!!(r!=null&&r.matches),l.value||(n.value=!1)}We(async()=>{document.addEventListener("keydown",S),r=window.matchMedia("(max-width: 900px)"),g(),r.addEventListener("change",g);const O=await G.check();O.ok?(e.value="ready",Q()):O.needsAuth?e.value="login":(e.value="ready",Q())});function w(){t.value=!1,e.value="ready",Q()}async function T(){await G.logout(),Ke.disconnect(),e.value="login"}function C(){s.value=!s.value}function M(){n.value=!n.value}ns(n,async O=>{var N,Y;if(O)o=document.activeElement,await Rt(),(Y=(N=a.value)==null?void 0:N.querySelector(".nav-item"))==null||Y.focus();else if(o!=null&&o.isConnected){const we=o;o=null,requestAnimationFrame(()=>we.focus())}});const H=J(()=>{switch(d.value){case"connected":return"Live";case"connecting":return"Connecting…";case"reconnecting":return"Reconnecting…";default:return"Disconnected"}});function P(O,N="info",Y=3e3){f.value={text:O,level:N},clearTimeout(p),p=setTimeout(()=>{f.value=null},Y)}let R=null,j=!1;function Q(){Ke.onStatusChange=O=>{c.value=O},Ke.onLatency=O=>{u.value=O},Ke.onStateChange=(O,N)=>{d.value=O,O==="connected"?(j&&P("Connection restored","success"),j=!0):O==="reconnecting"&&N.attempt===1&&P("Connection lost — reconnecting…","warn")},Ke.connect(),U(),R&&clearInterval(R),R=setInterval(U,15e3)}async function U(){try{const O=await G.get("/api/status");b.value=O.status==="online"?"online":"starting";const N=O.uptime_seconds||0,Y=Math.floor(N/3600),we=Math.floor(N%3600/60);y.value=`${Y}h ${we}m uptime`}catch{b.value="offline",y.value=""}}return xt(()=>{R&&clearInterval(R),Ke.disconnect(),document.removeEventListener("keydown",S),r==null||r.removeEventListener("change",g)}),{authState:e,sessionExpired:t,sidebarCollapsed:s,mobileOpen:n,wsConnected:c,wsState:d,wsLatency:u,wsLabel:H,wsToast:f,botStatus:b,botUptime:y,navRoutes:E,navGroups:I,currentPage:x,currentSection:m,currentDescription:_,sidebarEl:a,mobileMenuButton:i,isMobileViewport:l,onLogin:w,logout:T,toggleSidebar:C,toggleMobileNavigation:M,openPalette:Uu}}},zn=Jl(US);zn.component("odin-icon",gw);zn.component("login-screen",BS);zn.component("toast-container",z0);zn.component("confirm-host",V0);zn.component("command-palette",mw);zn.directive("modal-focus",bw);zn.use(Oi);zn.mount("#app"); diff --git a/ui/dist/assets/index-D0Js0srM.css b/ui/dist/assets/index-Dd0z3pui.css similarity index 83% rename from ui/dist/assets/index-D0Js0srM.css rename to ui/dist/assets/index-Dd0z3pui.css index ac453da7..816407f9 100644 --- a/ui/dist/assets/index-D0Js0srM.css +++ b/ui/dist/assets/index-Dd0z3pui.css @@ -1 +1 @@ -@font-face{font-family:Fira Code;font-style:normal;font-weight:400;font-display:swap;src:url(/ui/assets/fira-code-400-CHoedHDv.woff2) format("woff2")}@font-face{font-family:Fira Code;font-style:normal;font-weight:500;font-display:swap;src:url(/ui/assets/fira-code-400-CHoedHDv.woff2) format("woff2")}@font-face{font-family:Fira Code;font-style:normal;font-weight:600;font-display:swap;src:url(/ui/assets/fira-code-400-CHoedHDv.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:400;font-display:swap;src:url(/ui/assets/inter-400-Dx4kXJAl.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:500;font-display:swap;src:url(/ui/assets/inter-400-Dx4kXJAl.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:600;font-display:swap;src:url(/ui/assets/inter-400-Dx4kXJAl.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:700;font-display:swap;src:url(/ui/assets/inter-400-Dx4kXJAl.woff2) format("woff2")}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.m-2{margin:.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.max-h-24{max-height:6rem}.max-h-32{max-height:8rem}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-64{max-height:16rem}.max-h-96{max-height:24rem}.min-h-screen{min-height:100vh}.w-2{width:.5rem}.w-32{width:8rem}.w-5{width:1.25rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[100px\]{min-width:100px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.self-end{align-self:flex-end}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-amber-900{--tw-border-opacity: 1;border-color:rgb(120 53 15 / var(--tw-border-opacity, 1))}.border-blue-500\/30{border-color:#3b82f64d}.border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-700\/50{border-color:#37415180}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.border-gray-800\/50{border-color:#1f293780}.border-green-500\/30{border-color:#22c55e4d}.border-green-500\/40{border-color:#22c55e66}.border-green-800{--tw-border-opacity: 1;border-color:rgb(22 101 52 / var(--tw-border-opacity, 1))}.border-indigo-900\/30{border-color:#312e814d}.border-red-500\/30{border-color:#ef44444d}.border-red-500\/40{border-color:#ef444466}.border-red-700{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity, 1))}.border-red-800{--tw-border-opacity: 1;border-color:rgb(153 27 27 / var(--tw-border-opacity, 1))}.border-red-900{--tw-border-opacity: 1;border-color:rgb(127 29 29 / var(--tw-border-opacity, 1))}.border-red-900\/50{border-color:#7f1d1d80}.border-slate-700{--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.border-yellow-800{--tw-border-opacity: 1;border-color:rgb(133 77 14 / var(--tw-border-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-400{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity, 1))}.bg-blue-500\/60{background-color:#3b82f699}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-blue-900\/40{background-color:#1e3a8a66}.bg-blue-900\/50{background-color:#1e3a8a80}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/30{background-color:#1118274d}.bg-gray-900\/50{background-color:#11182780}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-green-400{--tw-bg-opacity: 1;background-color:rgb(74 222 128 / var(--tw-bg-opacity, 1))}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-green-900\/30{background-color:#14532d4d}.bg-green-950\/30{background-color:#052e164d}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity, 1))}.bg-indigo-900{--tw-bg-opacity: 1;background-color:rgb(49 46 129 / var(--tw-bg-opacity, 1))}.bg-indigo-950\/30{background-color:#1e1b4b4d}.bg-red-900\/20{background-color:#7f1d1d33}.bg-red-900\/30{background-color:#7f1d1d4d}.bg-red-900\/50{background-color:#7f1d1d80}.bg-red-950\/30{background-color:#450a0a4d}.bg-yellow-900\/20{background-color:#713f1233}.\!p-0{padding:0!important}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pl-2{padding-left:.5rem}.pr-1{padding-right:.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-indigo-300{--tw-text-opacity: 1;color:rgb(165 180 252 / var(--tw-text-opacity, 1))}.text-indigo-400{--tw-text-opacity: 1;color:rgb(129 140 248 / var(--tw-text-opacity, 1))}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-teal-400{--tw-text-opacity: 1;color:rgb(45 212 191 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.last\:mb-0:last-child{margin-bottom:0}.last\:border-0:last-child{border-width:0px}.hover\:border-gray-600:hover{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:text-blue-300:hover{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.hover\:text-green-300:hover{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.hover\:text-indigo-300:hover{--tw-text-opacity: 1;color:rgb(165 180 252 / var(--tw-text-opacity, 1))}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-yellow-300:hover{--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity, 1))}@media(min-width:360px){.min-\[360px\]\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}html{-webkit-text-size-adjust:100%}*{-webkit-tap-highlight-color:rgba(217,119,6,.15)}:root{--hm-font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--hm-font-mono: "Fira Code", "Cascadia Code", "JetBrains Mono", ui-monospace, monospace;--hm-text-xs: .6875rem;--hm-text-sm: .75rem;--hm-text-base: .8125rem;--hm-text-md: .875rem;--hm-text-lg: 1rem;--hm-text-xl: 1.25rem;--hm-text-2xl: 1.5rem;--hm-leading-tight: 1.25;--hm-leading-normal: 1.5;--hm-leading-relaxed: 1.65;--hm-space-1: .25rem;--hm-space-2: .375rem;--hm-space-3: .5rem;--hm-space-4: .75rem;--hm-space-5: 1rem;--hm-space-6: 1.5rem;--hm-space-8: 2rem;--hm-radius-full: 9999px;--hm-shadow-glow: 0 0 20px rgba(197, 139, 50, .15), 0 0 4px rgba(197, 139, 50, .1);--hm-shadow-glow-sm: 0 0 8px rgba(197, 139, 50, .1);--hm-transition-fast: .1s ease;--hm-transition-base: .15s ease;--hm-transition-slow: .25s ease;--hm-transition-spring: .3s cubic-bezier(.34, 1.56, .64, 1)}body{font-family:var(--hm-font-sans);font-size:var(--hm-text-md);line-height:var(--hm-leading-normal);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;letter-spacing:-.011em}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--hm-bg)}::-webkit-scrollbar-thumb{background:#374151;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#4b5563}*{scrollbar-width:thin;scrollbar-color:#374151 var(--hm-bg)}.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0}.status-dot.online{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success)}.status-dot.offline{background:var(--hm-danger);box-shadow:0 0 6px var(--hm-danger)}.status-dot.starting{background:var(--hm-warning);box-shadow:0 0 6px var(--hm-warning)}.hm-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-5);box-shadow:var(--hm-shadow-sm);transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.hm-table{width:100%;border-collapse:collapse;font-size:var(--hm-text-md)}.hm-table th{text-align:left;padding:var(--hm-space-3) var(--hm-space-4);color:var(--hm-text-muted);font-weight:500;font-size:var(--hm-text-sm);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--hm-border);white-space:nowrap}.hm-table td{padding:var(--hm-space-3) var(--hm-space-4);border-bottom:1px solid rgba(31,41,55,.5)}.hm-table tr:hover td{background:#1f29374d}.btn{display:inline-flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-2) var(--hm-space-4);border-radius:var(--hm-radius-md);font-size:var(--hm-text-base);font-weight:500;font-family:var(--hm-font-sans);cursor:pointer;border:none;transition:background var(--hm-transition-base),box-shadow var(--hm-transition-base),transform var(--hm-transition-fast);letter-spacing:-.006em}.btn:active:not(:disabled){transform:scale(.97)}.btn-primary{background:var(--hm-accent);color:#fff}.btn-primary:hover{background:var(--hm-accent-hover);box-shadow:var(--hm-shadow-glow-sm)}.btn-danger{background:#ef444426;color:#f87171}.btn-danger:hover{background:#ef444440}.btn-ghost{background:transparent;color:var(--hm-text-muted)}.btn-ghost:hover{background:var(--hm-surface-hover);color:var(--hm-text)}.btn:disabled{opacity:.5;cursor:not-allowed}.hm-input{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4);color:var(--hm-text);font-size:var(--hm-text-md);font-family:var(--hm-font-sans);width:100%;outline:none;transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.hm-input:focus{border-color:var(--hm-accent);box-shadow:0 0 0 3px var(--hm-accent-glow)}textarea.hm-input{font-family:var(--hm-font-mono);resize:vertical;min-height:120px}.toggle-switch{position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;top:0;right:0;bottom:0;left:0;background:#374151;border-radius:10px;cursor:pointer;transition:background .2s}.toggle-slider:before{content:"";position:absolute;width:14px;height:14px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:transform .2s}.toggle-switch input:checked+.toggle-slider{background:var(--hm-accent);box-shadow:var(--hm-shadow-glow-sm)}.toggle-switch input:checked+.toggle-slider:before{transform:translate(16px)}.toggle-switch input:disabled+.toggle-slider{opacity:.4;cursor:not-allowed}.field-changed td:first-child{border-left:2px solid var(--hm-accent)}.config-tag{display:inline-flex;align-items:center;gap:var(--hm-space-1);padding:.125rem .5rem;background:var(--hm-gold-dim);border:1px solid rgba(217,119,6,.2);border-radius:var(--hm-radius-sm);font-size:var(--hm-text-sm);font-family:var(--hm-font-mono);color:var(--hm-text)}.config-tag button{background:none;border:none;color:var(--hm-text-muted);cursor:pointer;padding:0;font-size:var(--hm-text-md);line-height:1}.config-tag button:hover{color:var(--hm-danger)}.hm-select{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-2) var(--hm-space-3);color:var(--hm-text);font-size:var(--hm-text-base);font-family:var(--hm-font-sans);outline:none;cursor:pointer;transition:border-color var(--hm-transition-base)}.hm-select:focus{border-color:var(--hm-accent)}.badge{display:inline-block;padding:.125rem .5rem;border-radius:var(--hm-radius-full);font-size:var(--hm-text-sm);font-weight:500;letter-spacing:.01em}.badge-success{background:#22c55e26;color:#4ade80}.badge-warning{background:#eab30826;color:#facc15}.badge-danger{background:#ef444426;color:#f87171}.badge-info{background:var(--hm-accent-dim);color:var(--hm-gold)}.spinner{width:20px;height:20px;border:2px solid var(--hm-border);border-top-color:var(--hm-accent);border-radius:50%;animation:spin .6s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#0009;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:50}.modal-content{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-xl);padding:var(--hm-space-6);max-width:600px;width:90%;max-height:80vh;overflow-y:auto;box-shadow:var(--hm-shadow-lg)}.skeleton{background:linear-gradient(90deg,var(--hm-surface) 25%,var(--hm-surface-hover) 50%,var(--hm-surface) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:var(--hm-radius-sm)}.skeleton-text{height:.875rem;margin-bottom:.5rem}.skeleton-stat{height:2rem;width:3rem;margin:0 auto .25rem}.skeleton-row{height:2.25rem;margin-bottom:.375rem}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.flash-new{animation:flash-highlight 1.5s ease-out}@keyframes flash-highlight{0%{background-color:#d9770640}to{background-color:transparent}}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.error-state{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-4);padding:var(--hm-space-6);text-align:center}.error-state .error-icon{font-size:1.5rem;opacity:.6}.chat-container{display:flex;flex-direction:column;height:calc(100vh - var(--hm-topbar-h));overflow:hidden}.page-viewport .chat-container.page-fade-in{max-width:none;margin-inline:0;--chat-work-lane: 1400px;--chat-reading-lane: 1080px}.chat-messages{flex:1;overflow-y:auto;padding:var(--hm-space-5) var(--hm-space-5) var(--hm-space-3);scroll-behavior:smooth}.chat-messages>.chat-message,.chat-messages>.chat-date-sep{max-width:var(--chat-reading-lane);margin-inline:auto}.chat-messages>.chat-message:has(pre) .chat-bubble-wrap,.chat-messages>.chat-message:has(table) .chat-bubble-wrap{max-width:100%;width:100%}.chat-empty{display:flex;align-items:center;justify-content:center;height:100%}.chat-welcome{text-align:center;max-width:28rem}.chat-welcome-icon{color:var(--hm-accent);margin-bottom:var(--hm-space-4);opacity:.8}.chat-welcome-title{font-size:1.25rem;font-weight:700;color:var(--hm-text);margin-bottom:var(--hm-space-2);letter-spacing:.01em}.chat-welcome-subtitle{font-size:var(--hm-text-sm);color:var(--hm-text-muted);margin-bottom:var(--hm-space-5)}.chat-suggestions{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--hm-space-2)}.chat-suggestion{background:var(--hm-surface-elevated);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-2) var(--hm-space-4);color:var(--hm-text-muted);font-size:var(--hm-text-sm);cursor:pointer;transition:border-color var(--hm-transition-base),color var(--hm-transition-base)}.chat-suggestion:hover{border-color:var(--hm-accent);color:var(--hm-accent-hover)}.chat-date-sep{display:flex;align-items:center;gap:var(--hm-space-4);margin:var(--hm-space-4) 0}.chat-date-sep:before,.chat-date-sep:after{content:"";flex:1;height:1px;background:var(--hm-border)}.chat-date-sep span{font-size:var(--hm-text-xs);color:var(--hm-text-muted);white-space:nowrap}.chat-message{display:flex;gap:var(--hm-space-3);margin-bottom:var(--hm-space-4);align-items:flex-start}.chat-message.chat-user{flex-direction:row-reverse}.chat-message.chat-bot{flex-direction:row}.chat-avatar{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}.chat-avatar-bot{background:linear-gradient(135deg,#d9770633,#d977061a);border:1px solid rgba(217,119,6,.3);color:var(--hm-accent)}.chat-avatar-user{background:var(--hm-surface-hover);border:1px solid var(--hm-border);color:var(--hm-text-muted)}.chat-avatar-eye,.chat-avatar-user{display:flex;align-items:center;justify-content:center}.chat-avatar-pulse{animation:avatar-pulse 2s ease-in-out infinite}@keyframes avatar-pulse{0%,to{opacity:1}50%{opacity:.5}}.chat-bubble-wrap{max-width:75%;min-width:0}.chat-message.chat-user .chat-bubble-wrap{align-items:flex-end;display:flex;flex-direction:column}.chat-bubble{border-radius:var(--hm-radius-xl);padding:.625rem .875rem;font-size:var(--hm-text-md);line-height:var(--hm-leading-normal);word-break:break-word}.chat-bubble-user{background:linear-gradient(135deg,var(--hm-accent),#b45309);color:#fff;border-bottom-right-radius:var(--hm-radius-sm);box-shadow:var(--hm-shadow-sm)}.chat-bubble-bot{background:var(--hm-surface-elevated);border:1px solid var(--hm-border);border-bottom-left-radius:var(--hm-radius-sm)}.chat-bubble-typing{display:flex;align-items:center;gap:var(--hm-space-3)}.chat-bubble-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:var(--hm-space-1)}.chat-bubble-label{font-size:var(--hm-text-xs);color:var(--hm-accent-hover);font-weight:600;letter-spacing:.02em}.chat-error-indicator{font-size:.625rem;color:#f87171;background:#ef444426;border:1px solid rgba(239,68,68,.3);border-radius:var(--hm-radius-sm);padding:0 .375rem;line-height:1.4;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.chat-bubble-text{white-space:pre-wrap}.chat-timestamp{font-size:.625rem;color:var(--hm-text-muted);margin-top:2px;opacity:0;transition:opacity var(--hm-transition-base);padding:0 .25rem}.chat-message:hover .chat-timestamp{opacity:1}.chat-markdown{white-space:normal}.chat-markdown p{margin:0 0 .5rem}.chat-markdown p:last-child{margin-bottom:0}.chat-markdown code{background:#0000004d;padding:.125rem .375rem;border-radius:var(--hm-radius-sm);font-size:var(--hm-text-base);font-family:var(--hm-font-mono)}.chat-markdown pre{background:#0000004d;border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4);margin:var(--hm-space-2) 0;overflow-x:auto;position:relative}.chat-markdown pre code{background:none;padding:0}.chat-markdown ul,.chat-markdown ol{margin:.25rem 0;padding-left:1.25rem}.chat-markdown blockquote{border-left:3px solid var(--hm-accent);padding-left:var(--hm-space-4);margin:var(--hm-space-2) 0;color:var(--hm-text-muted)}.chat-markdown a{color:var(--hm-accent-hover);text-decoration:underline}.chat-markdown table{border-collapse:collapse;margin:var(--hm-space-2) 0;font-size:var(--hm-text-sm);display:block;width:-moz-max-content;width:max-content;max-width:100%;overflow-x:auto}.chat-markdown th,.chat-markdown td{border:1px solid var(--hm-border);padding:var(--hm-space-2) var(--hm-space-3);text-align:left;white-space:nowrap;word-break:normal}.chat-markdown th{background:#0003;font-weight:600}.chat-code-copy{position:absolute;top:var(--hm-space-2);right:var(--hm-space-2);background:var(--hm-surface-hover);border:1px solid var(--hm-border);border-radius:var(--hm-radius-sm);color:var(--hm-text-muted);font-size:.625rem;padding:2px 8px;cursor:pointer;opacity:0;transition:opacity var(--hm-transition-base),color var(--hm-transition-base);font-family:var(--hm-font-sans);text-transform:uppercase;letter-spacing:.04em}.chat-code-copy:hover{color:var(--hm-accent-hover)}.chat-tool-cards{margin-bottom:var(--hm-space-2)}.chat-tools-toggle{background:none;border:none;color:var(--hm-text-muted);font-size:var(--hm-text-sm);cursor:pointer;display:flex;align-items:center;gap:var(--hm-space-1);padding:.125rem 0}.chat-tools-toggle:hover{color:var(--hm-text)}.chat-tools-toggle-icon{font-size:.5rem}.chat-tools-toggle-count{background:#d9770626;color:var(--hm-accent-hover);border-radius:var(--hm-radius-sm);padding:0 .375rem;font-weight:600;font-size:var(--hm-text-xs)}.chat-tool-list{display:flex;flex-wrap:wrap;gap:var(--hm-space-1);margin-top:var(--hm-space-2)}.chat-tool-card{display:flex;align-items:center;gap:var(--hm-space-2);background:#0003;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-1) var(--hm-space-3);font-size:var(--hm-text-xs)}.chat-tool-icon{font-size:var(--hm-text-sm)}.chat-tool-name{font-family:var(--hm-font-mono);color:var(--hm-text-muted)}.chat-images{display:flex;flex-wrap:wrap;gap:var(--hm-space-2);margin-top:var(--hm-space-2)}.chat-image-thumb{border-radius:var(--hm-radius-md);overflow:hidden;border:1px solid var(--hm-border);max-width:240px;cursor:pointer;transition:border-color var(--hm-transition-base)}.chat-image-thumb:hover{border-color:var(--hm-accent)}.chat-image-thumb img{display:block;width:100%;height:auto;max-height:200px;-o-object-fit:cover;object-fit:cover}.chat-typing{display:flex;gap:var(--hm-space-1);padding:var(--hm-space-1) 0}.chat-typing span{width:6px;height:6px;background:var(--hm-accent);border-radius:50%;animation:typing-dot 1.4s infinite ease-in-out both}.chat-typing span:nth-child(1){animation-delay:0s}.chat-typing span:nth-child(2){animation-delay:.2s}.chat-typing span:nth-child(3){animation-delay:.4s}@keyframes typing-dot{0%,80%,to{transform:scale(.6);opacity:.4}40%{transform:scale(1);opacity:1}}.chat-typing-text{font-size:var(--hm-text-xs);color:var(--hm-text-muted);margin-left:var(--hm-space-2);font-style:italic}.chat-input-area{border-top:1px solid var(--hm-border);background:var(--hm-surface);padding:var(--hm-space-4) var(--hm-space-5);min-height:var(--hm-rail-h);display:flex;flex-direction:column;justify-content:center}.chat-input-row,.chat-input-hint{width:100%;max-width:var(--chat-work-lane);margin-inline:auto}.chat-input-row{display:flex;gap:var(--hm-space-3);align-items:flex-end}.chat-input{flex:1;background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-3) var(--hm-space-4);color:var(--hm-text);font-size:var(--hm-text-md);resize:none;outline:none;line-height:var(--hm-leading-normal);max-height:120px;font-family:inherit;transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.chat-input:focus{border-color:var(--hm-accent);box-shadow:0 0 0 3px var(--hm-accent-glow)}.chat-input:disabled{opacity:.5}.chat-send-btn{height:36px;min-width:44px;flex-shrink:0;display:flex;align-items:center;justify-content:center}.chat-send-icon{pointer-events:none}.chat-input-hint{display:flex;justify-content:space-between;align-items:center;padding-top:var(--hm-space-1)}.chat-connection-status{display:flex;align-items:center;gap:var(--hm-space-1);font-size:var(--hm-text-xs)}.chat-status-dot{width:6px;height:6px;border-radius:50%}.chat-ws-on .chat-status-dot{background:#4ade80}.chat-ws-off .chat-status-dot{background:#f87171}.chat-ws-on{color:var(--hm-text-muted)}.chat-ws-off{color:#f87171}.session-card{transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.session-card:hover{border-color:var(--hm-accent);box-shadow:var(--hm-shadow-glow-sm)}.session-selected{border-color:var(--hm-accent)!important;background:#d977060d}.session-checkbox{width:14px;height:14px;accent-color:var(--hm-accent);cursor:pointer;flex-shrink:0}.session-preview-role{font-weight:600;font-family:var(--hm-font-mono);flex-shrink:0;width:36px}.session-msg-content{font-size:var(--hm-text-base)}.sess-source-icon{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:var(--hm-text-sm)}.sess-source-discord{background:#5865f226;border:1px solid rgba(88,101,242,.3)}.sess-source-web{background:var(--hm-accent-dim);border:1px solid rgba(217,119,6,.3)}.sess-expand-icon{font-size:.5rem;color:var(--hm-text-dim);transition:transform var(--hm-transition-base);display:inline-block;margin-right:var(--hm-space-2)}.sess-summary-banner{padding:var(--hm-space-3) var(--hm-space-4);background:linear-gradient(135deg,#eab30814,#eab30808);border:1px solid rgba(234,179,8,.2);border-radius:var(--hm-radius-md)}.sess-summary-label{font-size:var(--hm-text-xs);font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--hm-warning)}.sess-view-btn{background:#ffffff0d;border:1px solid transparent;border-radius:var(--hm-radius-md);padding:var(--hm-space-1) var(--hm-space-4);font-size:var(--hm-text-xs);color:var(--hm-text-muted);cursor:pointer;transition:all var(--hm-transition-base);font-family:var(--hm-font-sans)}.sess-view-btn:hover{background:#ffffff1a}.sess-view-active{background:var(--hm-accent-dim)!important;color:var(--hm-gold);border-color:#d9770666}.sess-thread-container{max-height:32rem;overflow-y:auto;scrollbar-gutter:stable;display:flex;flex-direction:column;gap:var(--hm-space-3)}.sess-thread{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);overflow:hidden;transition:border-color var(--hm-transition-base)}.sess-thread:hover{border-color:#ffffff1a}.sess-thread-header{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-3) var(--hm-space-4);cursor:pointer;transition:background var(--hm-transition-base)}.sess-thread-header:hover{background:#ffffff08}.sess-thread-num{width:22px;height:22px;border-radius:50%;background:var(--hm-accent-dim);color:var(--hm-accent-hover);display:flex;align-items:center;justify-content:center;font-size:var(--hm-text-xs);font-weight:700;flex-shrink:0}.sess-thread-arrow{font-size:.5rem;color:var(--hm-text-dim);transition:transform var(--hm-transition-base);display:inline-block}.sess-thread-arrow-open{transform:rotate(90deg)}.sess-thread-summary{flex:1;min-width:0;font-size:var(--hm-text-sm);color:var(--hm-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sess-thread-count{flex-shrink:0}.sess-thread-messages{border-top:1px solid var(--hm-border);padding:var(--hm-space-3) var(--hm-space-4);display:flex;flex-direction:column;gap:var(--hm-space-3)}.sess-thread-msg{padding:8px 12px;border-radius:6px;margin-bottom:8px}.sess-msg-user{background:#06b6d414;border-left:3px solid rgba(6,182,212,.5)}.sess-msg-assistant{background:#6366f114;border-left:3px solid rgba(99,102,241,.5)}.sess-msg-system{background:#6b728014;border-left:3px solid rgba(107,114,128,.4)}.sess-msg-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:var(--hm-space-2)}.sess-role-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.sess-dot-user{background:#06b6d4;box-shadow:0 0 4px #06b6d480}.sess-dot-assistant{background:#6366f1;box-shadow:0 0 4px #6366f180}.sess-dot-system{background:#6b7280}.sess-role-label{font-size:var(--hm-text-xs);font-weight:600}.sess-msg-content{font-size:13px;color:var(--hm-text);line-height:1.5;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word;max-height:200px;overflow-y:auto}.sess-filter-bar,.logs-filter-bar{border-bottom:1px solid var(--hm-border);padding-bottom:var(--hm-space-3)}.sess-preset-chip{display:inline-flex;align-items:center;gap:var(--hm-space-1);padding:var(--hm-space-1) var(--hm-space-4);border-radius:var(--hm-radius-full);font-size:var(--hm-text-xs);font-weight:500;cursor:pointer;border:1px solid transparent;transition:all var(--hm-transition-base);background:#ffffff0d;color:var(--hm-text-muted);font-family:var(--hm-font-sans)}.sess-preset-chip:hover{background:#ffffff1a;color:var(--hm-text)}.sess-preset-active{background:var(--hm-accent-dim)!important;color:var(--hm-gold)!important;border-color:#d9770666}.sess-preset-icon{font-size:var(--hm-text-sm)}.sess-preset-custom{border-style:dashed;border-color:#ffffff1a}.sess-preset-remove{margin-left:var(--hm-space-1);font-size:var(--hm-text-md);color:var(--hm-text-dim);cursor:pointer;line-height:1}.sess-preset-remove:hover{color:var(--hm-danger)}.log-line{border-left:2px solid transparent;padding-left:var(--hm-space-3)}.log-line-error{color:#f87171;border-left-color:#ef4444;background:#ef44440d}.log-line-warning{color:#fbbf24;border-left-color:#eab308;background:#eab3080a}.log-ts{transition:color var(--hm-transition-base)}.log-chip{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:var(--hm-radius-full);font-size:var(--hm-text-xs);font-weight:600;cursor:pointer;border:1px solid transparent;transition:all var(--hm-transition-base);font-family:var(--hm-font-mono);background:#ffffff0d;color:var(--hm-text-muted)}.log-chip:hover{background:#ffffff1a}.log-chip-info.log-chip-active{background:#d9770633;color:var(--hm-gold);border-color:#d9770666}.log-chip-warning.log-chip-active{background:#eab30833;color:#facc15;border-color:#eab30866}.log-chip-error.log-chip-active{background:#ef444433;color:#f87171;border-color:#ef444466}.log-chip-clear{background:#ffffff0d;color:var(--hm-text-muted)}.log-chip-clear:hover{background:#ffffff1a;color:var(--hm-text)}.log-jump-btn{position:absolute;bottom:1rem;left:50%;transform:translate(-50%);background:var(--hm-accent);color:#fff;border:none;border-radius:var(--hm-radius-full);padding:var(--hm-space-2) var(--hm-space-5);font-size:var(--hm-text-sm);font-weight:500;cursor:pointer;z-index:10;box-shadow:0 2px 8px #0006;transition:background var(--hm-transition-base),transform var(--hm-transition-base);animation:jump-btn-in .2s ease-out}.log-jump-btn:hover{background:var(--hm-accent-hover)}@keyframes jump-btn-in{0%{opacity:0;transform:translate(-50%) translateY(.5rem)}to{opacity:1;transform:translate(-50%) translateY(0)}}.logs-tool-badge{display:inline-block;background:#d977061a;border:1px solid rgba(217,119,6,.2);border-radius:var(--hm-radius-sm);padding:0 var(--hm-space-2);font-size:var(--hm-text-xs);color:var(--hm-accent-hover);margin-right:var(--hm-space-2);font-family:var(--hm-font-mono)}.logs-timeline{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4)}.logs-timeline-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--hm-space-2)}.logs-timeline-chart{display:flex;gap:2px;align-items:flex-end;height:48px}.logs-timeline-bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;cursor:pointer;min-width:0}.logs-timeline-bar{width:100%;height:40px;display:flex;flex-direction:column-reverse;border-radius:2px 2px 0 0;overflow:hidden;background:#ffffff05;transition:background var(--hm-transition-base)}.logs-timeline-bar-wrap:hover .logs-timeline-bar{background:#ffffff0f}.logs-timeline-segment{width:100%;min-height:1px;transition:height var(--hm-transition-slow)}.logs-tl-error{background:#ef4444b3}.logs-tl-warning{background:#eab30899}.logs-tl-info{background:#3b82f666}.logs-timeline-label{font-size:.5rem;color:var(--hm-text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.stat-card{position:relative;overflow:hidden}.stat-card:before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--hm-accent),transparent);opacity:0;transition:opacity var(--hm-transition-base)}.stat-card:hover:before{opacity:1}.stat-icon{font-size:var(--hm-text-md);margin-bottom:var(--hm-space-1);opacity:.7}.dash-hero{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-5);position:relative}.dash-hero-left{display:flex;align-items:center;gap:var(--hm-space-4);flex:1;min-width:0}.dash-hero-ring{position:relative;width:48px;height:48px;flex-shrink:0}.dash-ring-svg{width:48px;height:48px;transform:rotate(-90deg)}.ring-online{color:var(--hm-success)}.ring-starting{color:var(--hm-warning)}.dash-ring-progress{transition:stroke-dashoffset 1s ease}.dash-hero-icon{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;font-size:1rem;color:var(--hm-text-muted)}.dash-hero-name{font-size:var(--hm-text-lg);font-weight:600;color:var(--hm-text);letter-spacing:-.02em}.dash-hero-sub{display:flex;align-items:center;gap:var(--hm-space-2);font-size:var(--hm-text-sm);color:var(--hm-text-muted)}.dash-hero-sep{opacity:.4}.dash-hero-actions{display:flex;gap:var(--hm-space-2);flex-shrink:0}.dash-hero-skeleton{display:flex;align-items:center;gap:var(--hm-space-4)}.dash-stat{display:flex;flex-direction:column;gap:var(--hm-space-1);padding:var(--hm-space-4) var(--hm-space-5)}.dash-stat-header{display:flex;align-items:center;gap:var(--hm-space-2)}.dash-stat-icon{font-size:var(--hm-text-md);opacity:.7}.dash-stat-label{font-size:var(--hm-text-xs);color:var(--hm-text-dim);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.dash-stat-value{font-size:var(--hm-text-2xl);font-weight:700;letter-spacing:-.02em;line-height:1}.dash-stat-sub{font-size:var(--hm-text-xs)}.dash-stat-highlight{border-color:#d9770640;background:linear-gradient(135deg,var(--hm-surface),rgba(217,119,6,.03))}.dash-health-bar{padding:var(--hm-space-4) var(--hm-space-5)}.dash-health-items{display:flex;flex-wrap:wrap;gap:var(--hm-space-4)}.dash-health-item{display:flex;align-items:center;gap:var(--hm-space-2);font-size:var(--hm-text-sm)}.dash-health-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.dash-health-ok{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success)}.dash-health-warn{background:var(--hm-warning);box-shadow:0 0 6px var(--hm-warning)}.dash-health-error{background:var(--hm-danger);box-shadow:0 0 6px var(--hm-danger)}.dash-health-label{color:var(--hm-text);font-weight:500}.dash-health-detail{color:var(--hm-text-muted)}.dash-panel{padding:var(--hm-space-4) var(--hm-space-5);min-height:120px}.dash-panel-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--hm-space-3)}.dash-panel-title{font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-text-muted);text-transform:uppercase;letter-spacing:.04em}.dash-empty{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-5) 0;color:var(--hm-text-dim);font-size:var(--hm-text-sm)}.dash-empty-icon{font-size:1.25rem;opacity:.3}.dash-agent-list{display:flex;flex-direction:column;gap:var(--hm-space-3)}.dash-agent-item{padding:var(--hm-space-3);background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.dash-agent-top{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:var(--hm-space-1)}.dash-agent-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.dash-agent-running{background:var(--hm-success);box-shadow:0 0 4px var(--hm-success);animation:loop-pulse 2s ease-in-out infinite}.dash-agent-completed{background:var(--hm-info)}.dash-agent-failed{background:var(--hm-danger)}.dash-agent-timeout{background:var(--hm-warning)}.dash-agent-killed{background:var(--hm-text-dim)}.dash-agent-label{font-weight:600;font-size:var(--hm-text-sm);color:var(--hm-text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dash-agent-iters{font-size:var(--hm-text-xs);color:var(--hm-text-dim);font-family:var(--hm-font-mono);flex-shrink:0}.dash-agent-goal{font-size:var(--hm-text-xs);color:var(--hm-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dash-agent-meta{display:flex;gap:var(--hm-space-3);font-size:var(--hm-text-xs);color:var(--hm-text-dim);margin-top:var(--hm-space-1)}.dash-agent-tools{font-family:var(--hm-font-mono)}.dash-activity-list{display:flex;flex-direction:column}.dash-activity-item{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-2) 0;border-bottom:1px solid rgba(31,41,55,.5);font-size:var(--hm-text-xs)}.dash-activity-item:last-child{border-bottom:none}.dash-activity-dot{width:5px;height:5px;border-radius:50%;flex-shrink:0}.dot-ok{background:var(--hm-success)}.dot-error{background:var(--hm-danger)}.dash-activity-tool{font-family:var(--hm-font-mono);color:var(--hm-text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dash-activity-time{color:var(--hm-text-dim);white-space:nowrap;flex-shrink:0}.dash-guild-item{display:flex;align-items:center;gap:var(--hm-space-2);font-size:var(--hm-text-sm)}.dash-guild-count{margin-left:auto;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.dash-error-list{display:flex;flex-direction:column;gap:var(--hm-space-2)}.dash-error-item{padding:var(--hm-space-2) 0;border-bottom:1px solid rgba(31,41,55,.5);font-size:var(--hm-text-xs)}.dash-error-item:last-child{border-bottom:none}.dash-error-top{display:flex;align-items:center;gap:var(--hm-space-2)}.dash-error-tool{font-family:var(--hm-font-mono);color:#f87171;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dash-error-time{color:var(--hm-text-dim);white-space:nowrap;flex-shrink:0}.dash-error-msg{color:var(--hm-text-dim);padding-left:1.25rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:90%}.tool-expand-icon{font-size:.625rem;display:inline-block;transition:transform var(--hm-transition-base)}.tool-detail-row td{padding:0!important}.tool-detail-cell{padding:var(--hm-space-4) var(--hm-space-5) var(--hm-space-4) 2rem!important;background:#11182780;border-left:2px solid var(--hm-accent)}.tl-stat-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-4) var(--hm-space-5);text-align:center;position:relative;overflow:hidden}.tl-stat-card:hover{border-color:var(--hm-accent-dim)}.tl-stat-value{font-size:var(--hm-text-2xl);font-weight:700;color:var(--hm-text);line-height:1.2}.tl-stat-label{font-size:var(--hm-text-xs);color:var(--hm-text-dim);margin-top:var(--hm-space-1)}.tl-stat-spark{display:flex;justify-content:center;margin-top:var(--hm-space-2)}.tl-view-toggle{display:inline-flex;background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);overflow:hidden}.tl-view-btn{padding:var(--hm-space-1) var(--hm-space-3);background:none;border:none;color:var(--hm-text-dim);cursor:pointer;font-size:var(--hm-text-sm);transition:all var(--hm-transition-base)}.tl-view-btn:hover{color:var(--hm-text);background:var(--hm-surface-hover)}.tl-view-active{color:var(--hm-accent)!important;background:var(--hm-accent-dim)!important}.tl-search{flex:1;min-width:200px}.tl-category-chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-1)}.tl-category-chip{padding:var(--hm-space-1) var(--hm-space-3);border-radius:var(--hm-radius-full);border:1px solid var(--hm-border);background:var(--hm-surface);color:var(--hm-text-muted);font-size:var(--hm-text-xs);cursor:pointer;transition:all var(--hm-transition-base);white-space:nowrap}.tl-category-chip:hover{border-color:var(--hm-accent-dim);color:var(--hm-text)}.tl-category-active{background:var(--hm-accent-dim)!important;border-color:var(--hm-accent)!important;color:var(--hm-accent)!important}.tl-group-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:var(--hm-space-3);padding-bottom:var(--hm-space-2);border-bottom:1px solid var(--hm-border-subtle)}.tl-group-icon{font-size:var(--hm-text-md)}.tl-group-label{font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-text)}.tl-tool-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--hm-space-3)}.tl-tool-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-4);cursor:pointer;transition:all var(--hm-transition-base)}.tl-tool-card:hover{border-color:#d977064d;box-shadow:var(--hm-shadow-glow-sm)}.tl-tool-card-active{border-left:2px solid var(--hm-accent)}.tl-tool-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--hm-space-2)}.tl-tool-name{font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-text)}.tl-tool-desc{font-size:var(--hm-text-xs);color:var(--hm-text-muted);line-height:var(--hm-leading-normal);margin-bottom:var(--hm-space-3)}.tl-tool-footer{display:flex;align-items:center;justify-content:space-between}.tl-tool-usage{display:flex;align-items:baseline;gap:var(--hm-space-1)}.tl-tool-usage-count{font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-accent)}.tl-tool-usage-zero{font-size:var(--hm-text-sm);color:var(--hm-text-dim)}.tl-tool-usage-label{font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.tl-tool-spark{display:flex;align-items:center}.tl-sparkline{display:block}.tl-tool-detail{margin-top:var(--hm-space-3);padding-top:var(--hm-space-3);border-top:1px solid var(--hm-border-subtle)}.tl-tool-detail-desc{font-size:var(--hm-text-xs);color:var(--hm-text);line-height:var(--hm-leading-relaxed);white-space:pre-wrap;margin-bottom:var(--hm-space-3)}.tl-tool-params{margin-top:var(--hm-space-2)}.tl-tool-params-title{font-size:var(--hm-text-xs);font-weight:600;color:var(--hm-text-muted);margin-bottom:var(--hm-space-2)}.tl-tool-param{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-1) 0;font-size:var(--hm-text-xs)}.tl-tool-param-name{font-family:var(--hm-font-mono);color:var(--hm-text);font-weight:500}.tl-tool-param-type{color:var(--hm-info);font-family:var(--hm-font-mono)}.tl-tool-param-req{color:var(--hm-warning);font-size:.625rem;font-weight:600;text-transform:uppercase}.sk-stat-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-4) var(--hm-space-5);text-align:center}.sk-stat-card:hover{border-color:var(--hm-accent-dim)}.sk-stat-value{font-size:var(--hm-text-2xl);font-weight:700;color:var(--hm-text);line-height:1.2}.sk-stat-label{font-size:var(--hm-text-xs);color:var(--hm-text-dim);margin-top:var(--hm-space-1)}.sk-search{width:100%;max-width:400px}.sk-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,360px),1fr));gap:var(--hm-space-4)}.sk-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);overflow:hidden;transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.sk-card:hover{border-color:#d977064d;box-shadow:var(--hm-shadow-glow-sm)}.sk-card-tested{border-left:2px solid var(--hm-success)}.sk-card-header{display:flex;align-items:center;justify-content:space-between;padding:var(--hm-space-4) var(--hm-space-5);border-bottom:1px solid var(--hm-border-subtle);flex-wrap:wrap;gap:var(--hm-space-2)}.sk-card-title-row{display:flex;align-items:center;gap:var(--hm-space-2)}.sk-card-icon{font-size:var(--hm-text-md);opacity:.7}.sk-card-name{font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-text)}.sk-card-runs{font-size:var(--hm-text-xs);color:var(--hm-text-dim);font-family:var(--hm-font-mono)}.sk-card-actions{display:flex;gap:var(--hm-space-1)}.sk-action-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:var(--hm-radius-md);border:1px solid var(--hm-border);background:var(--hm-surface);color:var(--hm-text-muted);cursor:pointer;font-size:var(--hm-text-xs);transition:all var(--hm-transition-base)}.sk-action-btn:hover{background:var(--hm-surface-hover);color:var(--hm-text)}.sk-action-test:hover{color:var(--hm-success);border-color:#22c55e4d}.sk-action-code:hover{color:var(--hm-info);border-color:#3b82f64d}.sk-action-edit:hover{color:var(--hm-accent);border-color:var(--hm-accent-dim)}.sk-action-delete:hover{color:var(--hm-danger);border-color:#ef44444d}.sk-card-body{padding:var(--hm-space-4) var(--hm-space-5)}.sk-card-desc{font-size:var(--hm-text-sm);color:var(--hm-text-muted);line-height:var(--hm-leading-normal);margin-bottom:var(--hm-space-2)}.sk-card-meta{display:flex;align-items:center;gap:var(--hm-space-4);font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.sk-card-date,.sk-card-lines{white-space:nowrap}.sk-test-result{margin:0 var(--hm-space-5) var(--hm-space-4);padding:var(--hm-space-3) var(--hm-space-4);border-radius:var(--hm-radius-md);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs)}.sk-test-pass{background:#22c55e14;border:1px solid rgba(34,197,94,.2)}.sk-test-fail{background:#ef444414;border:1px solid rgba(239,68,68,.2)}.sk-test-label{font-weight:600;font-family:var(--hm-font-sans);font-size:var(--hm-text-xs);margin-bottom:var(--hm-space-1)}.sk-test-pass .sk-test-label{color:var(--hm-success)}.sk-test-fail .sk-test-label{color:var(--hm-danger)}.sk-test-output{white-space:pre-wrap;color:var(--hm-text-muted);max-height:120px;overflow-y:auto}.sk-code-container{border-top:1px solid var(--hm-border-subtle)}.sk-code-header{display:flex;align-items:center;justify-content:space-between;padding:var(--hm-space-2) var(--hm-space-5);background:#03071280;border-bottom:1px solid var(--hm-border-subtle)}.sk-code-filename{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.sk-code-copy{background:none;border:none;color:var(--hm-text-dim);cursor:pointer;font-size:var(--hm-text-sm);padding:var(--hm-space-1);border-radius:var(--hm-radius-sm);transition:color var(--hm-transition-base)}.sk-code-copy:hover{color:var(--hm-text)}.sk-code-wrap{display:flex;max-height:400px;overflow:auto}.sk-line-numbers{padding:var(--hm-space-4) var(--hm-space-3);text-align:right;color:var(--hm-text-dim);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);line-height:var(--hm-leading-relaxed);background:#03071280;border-right:1px solid var(--hm-border-subtle);-webkit-user-select:none;-moz-user-select:none;user-select:none;min-width:2.5rem;margin:0;flex-shrink:0}.sk-code-block{flex:1;padding:var(--hm-space-4) var(--hm-space-5);margin:0;background:var(--hm-bg);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);line-height:var(--hm-leading-relaxed);color:var(--hm-text);overflow-x:auto}.sk-code-block code{background:none;padding:0;font-size:inherit}.sk-kw{color:#c084fc;font-weight:600}.sk-str{color:#86efac}.sk-cmt{color:var(--hm-text-dim);font-style:italic}.sk-dec{color:var(--hm-gold)}.sk-num{color:#67e8f9}.sk-builtin{color:#93c5fd}.sk-editor-panel{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-5) var(--hm-space-6);margin-top:var(--hm-space-4)}.sk-editor-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--hm-space-4)}.sk-editor-title{font-size:var(--hm-text-md);font-weight:600;color:var(--hm-text)}.sk-field-label{display:block;font-size:var(--hm-text-xs);color:var(--hm-text-muted);margin-bottom:var(--hm-space-1)}.sk-field-hint{font-size:var(--hm-text-xs);color:var(--hm-text-dim);margin-top:var(--hm-space-1)}.sk-editor-wrap{display:flex;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);overflow:hidden;background:var(--hm-bg)}.sk-editor-gutter{padding:var(--hm-space-4) var(--hm-space-3);text-align:right;color:var(--hm-text-dim);font-family:var(--hm-font-mono);font-size:var(--hm-text-base);line-height:var(--hm-leading-relaxed);background:#03071280;border-right:1px solid var(--hm-border-subtle);-webkit-user-select:none;-moz-user-select:none;user-select:none;min-width:2.5rem;white-space:pre;overflow:hidden;flex-shrink:0}.sk-editor-textarea{flex:1;padding:var(--hm-space-4) var(--hm-space-5);background:transparent;border:none;color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-text-base);line-height:var(--hm-leading-relaxed);resize:vertical;-moz-tab-size:4;-o-tab-size:4;tab-size:4;outline:none;min-height:300px;width:100%}.sk-editor-textarea::-moz-placeholder{color:var(--hm-text-dim)}.sk-editor-textarea::placeholder{color:var(--hm-text-dim)}.sk-editor-status{display:flex;gap:var(--hm-space-4);margin-top:var(--hm-space-2);font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.sk-editor-line-count,.sk-editor-char-count{font-family:var(--hm-font-mono)}.sk-validation-box{padding:var(--hm-space-3) var(--hm-space-4);border-radius:var(--hm-radius-md);font-size:var(--hm-text-xs);margin-bottom:var(--hm-space-3)}.sk-validation-ok{background:#22c55e14;border:1px solid rgba(34,197,94,.2);color:var(--hm-success)}.sk-validation-err{background:#eab30814;border:1px solid rgba(234,179,8,.2);color:var(--hm-warning)}.skill-code-block{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-4) var(--hm-space-5);font-size:var(--hm-text-base);font-family:var(--hm-font-mono);overflow-x:auto;line-height:var(--hm-leading-relaxed);max-height:400px;overflow-y:auto;margin:0;color:var(--hm-text)}.skill-card{transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.skill-card:hover{border-color:#d977064d;box-shadow:var(--hm-shadow-glow-sm)}.skill-editor{font-family:var(--hm-font-mono);font-size:var(--hm-text-base);line-height:var(--hm-leading-relaxed);-moz-tab-size:4;-o-tab-size:4;tab-size:4;min-height:300px}.cfg-group{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);overflow:hidden;box-shadow:var(--hm-shadow-sm)}.cfg-group-header{display:flex;align-items:center;gap:var(--hm-space-3);padding:var(--hm-space-4) var(--hm-space-5);background:linear-gradient(135deg,var(--hm-surface),var(--hm-surface-elevated));border-bottom:1px solid var(--hm-border);transition:background var(--hm-transition-base)}.cfg-group-header:hover{background:var(--hm-surface-hover)}.cfg-group-icon{font-size:var(--hm-text-lg);opacity:.8}.cfg-group-label{font-weight:600;font-size:var(--hm-text-md);color:var(--hm-text);flex:1}.cfg-group-arrow{font-size:var(--hm-text-xs);color:var(--hm-text-dim);font-family:var(--hm-font-mono)}.cfg-group-body{padding:var(--hm-space-3);display:flex;flex-direction:column;gap:var(--hm-space-3)}.cfg-section{background:var(--hm-bg);border:1px solid rgba(31,41,55,.5);border-radius:var(--hm-radius-md);overflow:hidden}.cfg-section-header{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-3) var(--hm-space-4);transition:background var(--hm-transition-base)}.cfg-section-header:hover{background:#1f29374d}.cfg-section-name{font-size:var(--hm-text-sm);font-weight:500;color:var(--hm-text)}.cfg-section-body{padding:0 var(--hm-space-3) var(--hm-space-3)}.cfg-field-error{font-size:var(--hm-text-xs);color:#f87171;margin-top:2px;font-family:var(--hm-font-sans);font-weight:400}.cfg-input-error{border-color:#ef444480!important;box-shadow:0 0 0 2px #ef444426!important}.cfg-change-count{font-size:var(--hm-text-xs);color:var(--hm-accent-hover);background:var(--hm-accent-dim);padding:2px 8px;border-radius:var(--hm-radius-full);font-weight:600}.cfg-diff-list{max-height:50vh;overflow-y:auto;display:flex;flex-direction:column;gap:var(--hm-space-3)}.cfg-diff-entry{border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);overflow:hidden}.cfg-diff-path{padding:var(--hm-space-2) var(--hm-space-3);background:var(--hm-surface-hover);border-bottom:1px solid var(--hm-border);color:var(--hm-accent-hover)}.cfg-diff-values{font-size:var(--hm-text-sm)}.cfg-diff-old{padding:var(--hm-space-2) var(--hm-space-3);background:#ef444414;border-bottom:1px solid rgba(31,41,55,.3);color:#f87171;white-space:pre-wrap;word-break:break-word}.cfg-diff-new{padding:var(--hm-space-2) var(--hm-space-3);background:#22c55e14;color:#4ade80;white-space:pre-wrap;word-break:break-word}.cfg-diff-label{display:inline-block;width:1.25rem;font-weight:700;font-family:var(--hm-font-mono)}.knowledge-highlight{background:#d977064d;color:#fde68a;padding:0 2px;border-radius:2px}.knowledge-preview{border-left:2px solid var(--hm-border);padding-left:var(--hm-space-4);max-height:4rem;overflow:hidden;font-style:italic}.kb-stats-bar{display:flex;gap:var(--hm-space-4);margin-bottom:var(--hm-space-4);flex-wrap:wrap}.kb-stat{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-3) var(--hm-space-4);text-align:center;min-width:80px}.kb-stat-value{display:block;font-size:1.25rem;font-weight:700;color:var(--hm-gold)}.kb-stat-label{display:block;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.kb-score-badge{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);color:var(--hm-text-dim);background:var(--hm-gold-dim);padding:1px 6px;border-radius:3px}.kb-search-result{border-left:3px solid var(--hm-accent-dim)}.kb-ingest-form{border-left:3px solid var(--hm-accent)}.kb-tree,.kb-tree-list{display:flex;flex-direction:column;gap:var(--hm-space-2)}.kb-tree-node{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);overflow:hidden}.kb-tree-header{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-3) var(--hm-space-4);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background .15s}.kb-tree-header:hover{background:var(--hm-surface-hover)}.kb-tree-arrow{font-size:10px;color:var(--hm-text-dim);transition:transform .2s;flex-shrink:0}.kb-tree-icon{font-size:1rem;flex-shrink:0}.kb-tree-name{font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600}.kb-tree-actions{margin-left:auto;display:flex;gap:var(--hm-space-1);flex-shrink:0}.kb-tree-meta{padding:0 var(--hm-space-4) var(--hm-space-2) 2.25rem;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.kb-tree-preview{padding:0 var(--hm-space-4) var(--hm-space-3) 2.25rem;font-size:var(--hm-text-sm);color:var(--hm-text-muted);font-style:italic;max-height:3rem;overflow:hidden;white-space:pre-wrap;word-break:break-word}.kb-chunk-browser{padding:0 var(--hm-space-4) var(--hm-space-4) var(--hm-space-4)}.kb-chunk-loading{display:flex;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-dim);font-size:var(--hm-text-sm);padding:var(--hm-space-2) 0}.kb-chunk-list{display:flex;flex-direction:column;gap:var(--hm-space-1)}.kb-chunk-header{display:flex;justify-content:space-between;padding:var(--hm-space-2) 0;border-bottom:1px solid var(--hm-border)}.kb-chunk-empty{padding:var(--hm-space-2) 0}.kb-chunk-item{padding:var(--hm-space-2) var(--hm-space-3);border-radius:var(--hm-radius-md);border:1px solid var(--hm-border-subtle);cursor:pointer;transition:all .15s}.kb-chunk-item:hover{background:var(--hm-surface-hover);border-color:var(--hm-border)}.kb-chunk-selected{background:var(--hm-gold-dim)!important;border-color:#d977064d!important}.kb-chunk-item-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:2px}.kb-chunk-index{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);font-weight:600;color:var(--hm-accent);min-width:2rem}.kb-chunk-chars{font-size:var(--hm-text-xs);color:var(--hm-text-dim);min-width:4.5rem}.kb-chunk-bar{flex:1;height:4px;background:var(--hm-border);border-radius:2px;overflow:hidden}.kb-chunk-bar-fill{height:100%;background:var(--hm-accent);border-radius:2px;transition:width .3s}.kb-chunk-preview{font-size:var(--hm-text-xs);color:var(--hm-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kb-chunk-content{font-size:var(--hm-text-sm);color:var(--hm-text);white-space:pre-wrap;word-break:break-word;max-height:20rem;overflow-y:auto;margin-top:var(--hm-space-2);padding:var(--hm-space-2);background:var(--hm-bg);border-radius:var(--hm-radius-md);border:1px solid var(--hm-border)}.memory-scope-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:var(--hm-text-sm);font-weight:600;font-family:var(--hm-font-mono)}.memory-scope-global{background:#d9770633;color:var(--hm-gold);border:1px solid rgba(217,119,6,.3)}.memory-scope-user{background:#eab30826;color:#fde047;border:1px solid rgba(234,179,8,.25)}.memory-checkbox{width:16px;height:16px;accent-color:var(--hm-accent);cursor:pointer}.mem-stats-bar{display:flex;gap:var(--hm-space-4);margin-bottom:var(--hm-space-4);flex-wrap:wrap}.mem-stat{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-3) var(--hm-space-4);text-align:center;min-width:80px}.mem-stat-value{display:block;font-size:1.25rem;font-weight:700;color:var(--hm-gold)}.mem-stat-label{display:block;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.mem-stat-action{display:flex;align-items:center;justify-content:center}.mem-add-form{border-left:3px solid var(--hm-accent)}.mem-tree{display:flex;flex-direction:column;gap:var(--hm-space-2)}.mem-tree-node{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);overflow:hidden}.mem-tree-header{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-3) var(--hm-space-4);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background .15s}.mem-tree-header:hover{background:var(--hm-surface-hover)}.mem-tree-arrow{font-size:10px;color:var(--hm-text-dim);transition:transform .2s;flex-shrink:0}.mem-tree-entries{padding:0 var(--hm-space-4) var(--hm-space-3)}.mem-tree-loading{display:flex;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-dim);font-size:var(--hm-text-sm);padding:var(--hm-space-2) 0}.mem-tree-empty{padding:var(--hm-space-2) 0}.mem-tree-entry{padding:var(--hm-space-2) var(--hm-space-3);border-radius:var(--hm-radius-md);border:1px solid var(--hm-border-subtle);margin-bottom:var(--hm-space-1);transition:all .15s}.mem-tree-entry:hover{background:var(--hm-surface-hover)}.mem-tree-entry-selected{background:var(--hm-gold-dim);border-color:#d977064d}.mem-tree-entry-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:2px}.mem-tree-key{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);font-weight:500;color:var(--hm-text-muted)}.mem-tree-entry-actions{margin-left:auto;display:flex;gap:var(--hm-space-1);flex-shrink:0}.mem-tree-value{font-size:var(--hm-text-sm);color:var(--hm-text);white-space:pre-wrap;word-break:break-word;max-height:6rem;overflow:hidden;padding-left:1.5rem}.mem-tree-edit{padding-left:1.5rem;margin-top:var(--hm-space-1)}.ag-checkbox{width:14px;height:14px;accent-color:var(--hm-accent);cursor:pointer}.ag-stats-bar{display:flex;gap:var(--hm-space-4);margin-bottom:var(--hm-space-4);flex-wrap:wrap}.ag-stat{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-3) var(--hm-space-4);text-align:center;min-width:80px}.ag-stat-value{display:block;font-size:1.25rem;font-weight:700;color:var(--hm-gold)}.ag-stat-label{display:block;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.ag-stat-running{color:var(--hm-success)!important}.ag-stat-completed{color:var(--hm-info)!important}.ag-stat-failed{color:var(--hm-danger)!important}.ag-filter-bar{display:flex;gap:var(--hm-space-2);margin-bottom:var(--hm-space-4);flex-wrap:wrap}.ag-filter-btn{padding:var(--hm-space-1) var(--hm-space-3);border-radius:var(--hm-radius-md);font-size:var(--hm-text-xs);color:var(--hm-text-muted);background:var(--hm-surface);border:1px solid var(--hm-border);cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:var(--hm-space-1)}.ag-filter-btn:hover{background:var(--hm-surface-hover);color:var(--hm-text)}.ag-filter-active{background:var(--hm-accent-dim)!important;border-color:var(--hm-accent)!important;color:var(--hm-gold)!important}.ag-filter-count{font-family:var(--hm-font-mono);font-weight:600;font-size:.65rem;background:var(--hm-border);padding:0 4px;border-radius:3px}.ag-filter-active .ag-filter-count{background:#d977064d}.ag-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,340px),1fr));gap:var(--hm-space-4)}.ag-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-4);display:flex;flex-direction:column;gap:var(--hm-space-2);transition:border-color .2s,box-shadow .2s}.ag-card:hover{border-color:var(--hm-border)}.ag-card-running{border-left:3px solid var(--hm-success)}.ag-card-completed{border-left:3px solid var(--hm-info)}.ag-card-failed{border-left:3px solid var(--hm-danger)}.ag-card-timeout{border-left:3px solid var(--hm-warning)}.ag-card-killed{border-left:3px solid var(--hm-text-dim)}.ag-card-header{display:flex;align-items:center;justify-content:space-between}.ag-card-title-row{display:flex;align-items:center;gap:var(--hm-space-2);min-width:0;flex-wrap:nowrap}.ag-card-label{font-weight:600;font-size:var(--hm-text-sm);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ag-card-id{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);color:var(--hm-text-dim);flex:none}.ag-status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.ag-dot-running{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success);animation:loop-pulse 2s ease-in-out infinite}.ag-dot-completed{background:var(--hm-info)}.ag-dot-failed{background:var(--hm-danger)}.ag-dot-timeout{background:var(--hm-warning)}.ag-dot-killed{background:var(--hm-text-dim)}.ag-status-badge{font-size:var(--hm-text-xs);font-weight:600;padding:1px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.05em}.ag-badge-running{background:#22c55e26;color:var(--hm-success)}.ag-badge-completed{background:#3b82f626;color:var(--hm-info)}.ag-badge-failed{background:#ef444426;color:var(--hm-danger)}.ag-badge-timeout{background:#eab30826;color:var(--hm-warning)}.ag-badge-killed{background:#9ca3af26;color:var(--hm-text-dim)}.ag-card-goal{font-size:var(--hm-text-sm);color:var(--hm-text-muted);line-height:1.4;height:3.5rem;overflow:hidden;position:relative}.ag-card-goal:after{content:"";position:absolute;inset:auto 0 0 0;height:1.5rem;background:linear-gradient(transparent,var(--hm-surface));pointer-events:none}.ag-card-clickable:hover .ag-card-goal:after{background:linear-gradient(transparent,var(--hm-surface-hover))}.ag-progress-bar{height:3px;background:var(--hm-border);border-radius:2px;overflow:hidden}.ag-progress-fill{height:100%;background:var(--hm-success);border-radius:2px;transition:width .5s;animation:ag-progress-glow 2s ease-in-out infinite}@keyframes ag-progress-glow{0%,to{opacity:1}50%{opacity:.6}}.ag-card-stats{display:flex;gap:var(--hm-space-3)}.ag-card-stat{flex:1;text-align:center}.ag-card-stat-label{display:block;font-size:.6rem;color:var(--hm-text-dim);text-transform:uppercase;letter-spacing:.05em}.ag-card-stat-value{display:block;font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600}.ag-card-tools{display:flex;flex-wrap:wrap;gap:4px}.ag-tool-chip{font-family:var(--hm-font-mono);font-size:.6rem;padding:1px 6px;background:var(--hm-gold-dim);color:var(--hm-text-muted);border-radius:3px;border:1px solid var(--hm-border-subtle)}.ag-card-policy{display:flex;flex-wrap:nowrap;gap:4px;min-width:0}.ag-policy-chip{font-family:var(--hm-font-mono);font-size:.62rem;padding:1px 7px;color:var(--hm-text-muted);background:var(--hm-surface-elevated);border:1px solid var(--hm-border);border-radius:3px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ag-policy-chip:not(.ag-policy-effort){flex:0 1 auto}.ag-policy-effort{flex:0 0 auto;color:var(--hm-accent);border-color:#c58b3247}.ag-card-body{display:flex;flex-direction:column;gap:var(--hm-space-2);text-align:left;border-radius:var(--hm-radius-md)}.ag-card-clickable{cursor:pointer;transition:background .12s ease,box-shadow .12s ease}.ag-card-clickable:hover{background:var(--hm-surface-hover)}.ag-card-clickable:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}.modal-content.ag-detail-modal{width:min(1000px,94vw);max-width:min(1000px,94vw);max-height:88vh;display:flex;flex-direction:column;gap:var(--hm-space-3);overflow-y:auto}.ag-detail-header{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-3)}.ag-detail-title-row{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-2);min-width:0}.ag-detail-title{margin:0;font-size:1rem;font-weight:650;letter-spacing:-.015em;color:#f2f3f6}.ag-detail-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:var(--hm-space-2) var(--hm-space-3)}.ag-detail-meta-item{display:flex;flex-direction:column;gap:1px;min-width:0}.ag-detail-meta-label{font-size:.6rem;color:var(--hm-text-dim);text-transform:uppercase;letter-spacing:.05em}.ag-detail-meta-value{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);color:var(--hm-text);word-break:break-word}.ag-detail-source{margin:0;font-size:.66rem;color:var(--hm-text-dim);font-style:italic}.ag-detail-section{display:flex;flex-direction:column;gap:4px}.ag-detail-section-head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-2)}.ag-detail-text{margin:0;padding:var(--hm-space-2);max-height:34vh;overflow:auto;font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--hm-text-muted);background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.ag-detail-pending{margin:0;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}@media(max-width:640px){.modal-content.ag-detail-modal{width:96vw;max-width:96vw;max-height:92vh}.ag-detail-text{max-height:40vh}}.ag-card-meta{display:flex;justify-content:space-between;gap:var(--hm-space-2)}.ag-card-result,.ag-card-error{padding:var(--hm-space-2);border-radius:var(--hm-radius-md);background:var(--hm-bg);border:1px solid var(--hm-border)}.ag-result-label{font-size:.6rem;color:var(--hm-text-dim);text-transform:uppercase;margin-bottom:2px}.ag-result-text{font-size:var(--hm-text-xs);white-space:pre-wrap;word-break:break-word;max-height:6rem;overflow:hidden;color:var(--hm-text-muted)}.ag-card-error{border-color:#ef444433}.ag-card-actions{display:flex;justify-content:flex-end}.loop-status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0}.loop-status-running{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success);animation:loop-pulse 2s ease-in-out infinite}.loop-status-error{background:var(--hm-danger);box-shadow:0 0 6px var(--hm-danger)}.loop-status-stopped{background:var(--hm-text-dim)}@keyframes loop-pulse{0%,to{opacity:1;box-shadow:0 0 6px var(--hm-success)}50%{opacity:.5;box-shadow:0 0 2px var(--hm-success)}}.loop-history{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4);max-height:200px;overflow-y:auto}.log-filter-field{flex:1 1 0%;min-width:0}.log-filter-row{display:flex;align-items:center;gap:.375rem;min-width:0}.log-filter-input{min-width:120px}@media(max-width:640px){.log-filter-field{flex:1 0 100%}.log-filter-input{min-width:0}}.loop-history-entry{font-size:var(--hm-text-sm);font-family:var(--hm-font-mono);color:var(--hm-text-muted);padding:var(--hm-space-1) 0;border-bottom:1px solid rgba(31,41,55,.3);white-space:pre-wrap;word-break:break-word}.loop-history-entry:last-child{border-bottom:none}.loop-card{display:flex;flex-direction:column;gap:var(--hm-space-3)}.loop-card-main{min-width:0;cursor:pointer;border-radius:var(--hm-radius-md);transition:background .12s ease,box-shadow .12s ease}.loop-card-main:hover{background:var(--hm-surface-hover)}.loop-card-goal{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;margin-bottom:var(--hm-space-2);color:var(--hm-text);font-size:var(--hm-text-sm);line-height:1.45}.loop-card-main:focus-visible{outline:2px solid var(--hm-accent);outline-offset:3px}.loop-card-actions{display:flex;justify-content:flex-end;gap:var(--hm-space-2)}.loop-card-preview{display:flex;flex-direction:column;gap:3px;margin-top:var(--hm-space-3);padding:var(--hm-space-2);border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);background:var(--hm-bg);font-size:var(--hm-text-xs);color:var(--hm-text-dim);max-height:4.5rem;overflow:hidden;white-space:pre-wrap;word-break:break-word}.loop-detail-modal{gap:var(--hm-space-4)}.loop-detail-goal{max-height:22vh}.loop-detail-condition{max-height:14vh}.loop-detail-history-head{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--hm-space-3);border-top:1px solid var(--hm-border);padding-top:var(--hm-space-3)}.loop-detail-history-head h3{margin:0 0 2px;font-size:var(--hm-text-sm)}.loop-detail-notice{padding:var(--hm-space-2);color:var(--hm-text-muted);font-size:var(--hm-text-xs);background:#c58b3214;border:1px solid rgba(197,139,50,.2);border-radius:var(--hm-radius-md)}.loop-detail-iterations{display:flex;flex-direction:column;gap:var(--hm-space-3)}.loop-detail-iteration{display:flex;flex-direction:column;gap:var(--hm-space-2);padding:var(--hm-space-3);border:1px solid var(--hm-border);background:var(--hm-surface-elevated);border-radius:var(--hm-radius-lg)}.loop-detail-iteration-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-2)}.loop-detail-iteration-head>div{display:flex;flex-direction:column;min-width:0}.loop-detail-turn-meta{display:flex;flex-wrap:wrap;gap:5px var(--hm-space-3);font-family:var(--hm-font-mono);font-size:.65rem;color:var(--hm-text-dim)}.loop-detail-response{max-height:30vh}.loop-detail-tools{display:flex;flex-wrap:wrap;gap:4px}.loop-context-details{padding:var(--hm-space-3);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);background:var(--hm-bg)}.loop-context-details summary{cursor:pointer;color:var(--hm-text-muted);font-size:var(--hm-text-xs);font-weight:600}.loop-context-details[open] summary{margin-bottom:var(--hm-space-2)}.loop-context-list{display:flex;flex-direction:column;gap:var(--hm-space-2);margin-top:var(--hm-space-2)}.loop-context-entry{max-height:18vh}@media(max-width:640px){.loop-card-actions{justify-content:stretch}.loop-card-actions .btn{flex:1}.loop-detail-history-head{align-items:flex-start}.loop-detail-turn-meta{display:grid;grid-template-columns:1fr 1fr}}.process-output-preview{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4);font-size:var(--hm-text-sm);font-family:var(--hm-font-mono);color:var(--hm-text-muted);max-height:120px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;margin:0}.hm-input:focus-visible,.hm-select:focus-visible,.chat-input:focus-visible,.toggle-switch input:focus-visible+.toggle-slider,.session-checkbox:focus-visible,.memory-checkbox:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}.page-fade-in{animation:page-fade .2s ease-out}@keyframes page-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.empty-state{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-3);padding:var(--hm-space-8) var(--hm-space-5);text-align:center}.empty-state-icon{font-size:1.75rem;opacity:.4}.empty-state-text{color:var(--hm-text-muted);font-size:var(--hm-text-md)}.empty-state-hint{color:var(--hm-text-dim);font-size:var(--hm-text-sm)}.cron-preset-btn{display:inline-block;padding:.125rem .5rem;background:var(--hm-gold-dim);border:1px solid rgba(217,119,6,.2);border-radius:var(--hm-radius-sm);font-size:var(--hm-text-xs);color:var(--hm-text-muted);cursor:pointer;transition:all var(--hm-transition-base)}.cron-preset-btn:hover{background:var(--hm-accent-dim);color:var(--hm-text);border-color:#d9770666}.hm-card-accent{border-top:2px solid var(--hm-accent)}.hm-divider{height:1px;background:linear-gradient(90deg,transparent,var(--hm-accent-dim),transparent);border:none;margin:var(--hm-space-5) 0}.hm-mono{font-family:var(--hm-font-mono)}.hm-section-title{font-size:var(--hm-text-sm);font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--hm-text-dim)}.ws-indicator{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;transition:background .3s,box-shadow .3s}.ws-connected{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success)}.ws-disconnected{background:var(--hm-danger);box-shadow:0 0 6px var(--hm-danger)}.ws-connecting,.ws-reconnecting{background:var(--hm-warning);box-shadow:0 0 6px var(--hm-warning);animation:ws-pulse 1.2s ease-in-out infinite}@keyframes ws-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.75)}}.ws-toast{position:fixed;bottom:var(--hm-space-5);left:50%;transform:translate(-50%);z-index:9999;padding:var(--hm-space-3) var(--hm-space-5);border-radius:var(--hm-radius-lg);font-size:var(--hm-text-sm);font-weight:500;box-shadow:var(--hm-shadow-lg);pointer-events:none}.ws-toast-success{background:#22c55e26;border:1px solid rgba(34,197,94,.3);color:#4ade80}.ws-toast-warn{background:#eab30826;border:1px solid rgba(234,179,8,.3);color:#fbbf24}.ws-toast-info{background:#3b82f626;border:1px solid rgba(59,130,246,.3);color:#60a5fa}.ws-toast-enter-active{animation:ws-toast-in .3s ease-out}.ws-toast-leave-active{animation:ws-toast-out .25s ease-in forwards}@keyframes ws-toast-in{0%{opacity:0;transform:translate(-50%) translateY(12px)}to{opacity:1;transform:translate(-50%) translateY(0)}}@keyframes ws-toast-out{0%{opacity:1;transform:translate(-50%) translateY(0)}to{opacity:0;transform:translate(-50%) translateY(12px)}}.toast-stack{position:fixed;bottom:var(--hm-space-5);right:var(--hm-space-5);z-index:9999;display:flex;flex-direction:column;align-items:flex-end;gap:var(--hm-space-2);pointer-events:none}.toast-item{display:flex;align-items:center;gap:var(--hm-space-2);max-width:380px;padding:var(--hm-space-3) var(--hm-space-4);border-radius:var(--hm-radius-lg);font-size:var(--hm-text-sm);font-weight:500;box-shadow:var(--hm-shadow-lg);cursor:pointer;pointer-events:auto;background:var(--hm-surface);border:1px solid var(--hm-border);color:var(--hm-text)}.toast-item .toast-icon{flex-shrink:0}.toast-item .toast-text{overflow-wrap:anywhere}.toast-success{background:#22c55e1f;border-color:#22c55e4d;color:#4ade80}.toast-error{background:#ef44441f;border-color:#ef444459;color:#f87171}.toast-info{background:#3b82f61f;border-color:#3b82f64d;color:#60a5fa}.toast-enter-active{transition:all .25s ease-out}.toast-leave-active{transition:all .2s ease-in}.toast-enter-from,.toast-leave-to{opacity:0;transform:translateY(10px)}.modal-enter-active{transition:opacity .15s ease-out}.modal-leave-active{transition:opacity .12s ease-in}.modal-enter-from,.modal-leave-to{opacity:0}.confirm-dialog{max-width:420px}.palette-overlay{align-items:flex-start;padding-top:14vh}.palette{width:90%;max-width:520px;background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-xl);box-shadow:var(--hm-shadow-lg);overflow:hidden}.palette-input{width:100%;background:transparent;border:none;border-bottom:1px solid var(--hm-border);padding:var(--hm-space-4) var(--hm-space-5);font-size:var(--hm-text-base);color:var(--hm-text);outline:none}.palette-input::-moz-placeholder{color:var(--hm-text-muted)}.palette-input::placeholder{color:var(--hm-text-muted)}.palette-results{max-height:320px;overflow-y:auto;padding:var(--hm-space-2)}.palette-item{display:flex;align-items:center;gap:var(--hm-space-2);width:100%;text-align:left;padding:var(--hm-space-2) var(--hm-space-3);border-radius:var(--hm-radius-md);font-size:var(--hm-text-sm);color:var(--hm-text);background:transparent;border:none;cursor:pointer}.palette-item.selected{background:var(--hm-surface-hover)}.palette-item .palette-icon{flex-shrink:0}.palette-item .palette-group{color:var(--hm-text-muted)}.palette-empty{padding:var(--hm-space-4);text-align:center;color:var(--hm-text-muted);font-size:var(--hm-text-sm)}.palette-footer{display:flex;gap:var(--hm-space-3);padding:var(--hm-space-2) var(--hm-space-4);border-top:1px solid var(--hm-border);font-size:var(--hm-text-xs);color:var(--hm-text-muted)}.palette-footer kbd{padding:0 .3em;background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-sm)}.item-enter{animation:item-slide-in .25s ease-out}@keyframes item-slide-in{0%{opacity:0;transform:translate(-8px)}to{opacity:1;transform:translate(0)}}.dash-stat-value{transition:color var(--hm-transition-slow)}.badge{transition:transform var(--hm-transition-fast),background var(--hm-transition-base)}.badge-pop{animation:badge-pop .3s var(--hm-transition-spring)}@keyframes badge-pop{0%{transform:scale(1)}50%{transform:scale(1.3)}to{transform:scale(1)}}.action-pending{opacity:.6;pointer-events:none}.action-success{animation:action-flash-ok .5s ease-out}.action-error{animation:action-flash-err .5s ease-out}@keyframes action-flash-ok{0%{box-shadow:0 0 #22c55e66}to{box-shadow:0 0 0 0 transparent}}@keyframes action-flash-err{0%{box-shadow:0 0 #ef444466}to{box-shadow:0 0 0 0 transparent}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.skip-nav{position:absolute;top:-100%;left:var(--hm-space-5);z-index:1000;padding:var(--hm-space-3) var(--hm-space-5);background:var(--hm-accent);color:#fff;border-radius:var(--hm-radius-md);font-size:var(--hm-text-md);font-weight:600;text-decoration:none;transition:top var(--hm-transition-base)}.skip-nav:focus{top:var(--hm-space-3)}.ag-filter-btn:focus-visible,.kb-tree-header:focus-visible,.kb-chunk-item:focus-visible,.cron-preset-btn:focus-visible,.dash-health-item:focus-visible,[role=tab]:focus-visible,[role=button]:focus-visible,a:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}.spinner{animation:none;border-top-color:var(--hm-accent)}.page-fade-in{animation:none}.hm-sidebar,.dash-ring-progress{transition:none}.ws-connecting,.ws-reconnecting,.ws-toast{animation:none}}@media(forced-colors:active){.status-dot,.ag-status-dot,.dash-health-dot,.loop-status-dot{forced-color-adjust:none}.btn-primary{border:1px solid ButtonText}.hm-card{border:1px solid CanvasText}.nav-item.active{border-left:3px solid Highlight}}.hm-main .p-6{max-width:1600px}@media(min-width:1200px){:root{--hm-text-md: .9375rem}}@media(max-width:768px){.hm-main .p-6{padding:var(--hm-space-5)}.hm-table{font-size:var(--hm-text-sm)}.hm-table th,.hm-table td{padding:var(--hm-space-2) var(--hm-space-3)}.mobile-hide{display:none!important}.chat-bubble{max-width:90%}.btn{min-height:36px}.hm-input,.hm-select{min-height:36px;font-size:1rem}.toggle-switch{width:44px;height:24px}.toggle-slider:before{width:18px;height:18px}.toggle-switch input:checked+.toggle-slider:before{transform:translate(20px)}.modal-content{padding:var(--hm-space-5);width:95%}.config-key-col{width:auto!important;min-width:80px}.log-chip{padding:var(--hm-space-1) .625rem;font-size:var(--hm-text-sm)}.session-checkbox,.memory-checkbox{width:18px;height:18px}.cron-preset-btn{padding:var(--hm-space-1) .625rem;font-size:var(--hm-text-sm)}}@media(max-width:640px){.dash-hero-left{flex-basis:100%}}@media(max-width:480px){.chat-bubble-wrap{max-width:90%}.chat-avatar{width:24px;height:24px}.chat-input-row{flex-direction:column;align-items:stretch}.chat-input{width:100%}.chat-send-btn{width:100%;min-width:unset}.chat-suggestions{flex-direction:column}.chat-suggestion{width:100%}.stat-grid-mobile{grid-template-columns:repeat(2,1fr)!important}h1{font-size:1.125rem!important}.hm-main .p-6{padding:var(--hm-space-4)}}.fts-result-user{background:#11182780;border-color:#1f2937}.fts-result-assistant{background:#312e814d;border-color:#312e814d}.fts-result-summary{background:#451a0333;border-color:#78350f4d}.fts-result-fts{background:#022c2233;border-color:#064e3b4d}.fts-result-channel{background:#3b076433;border-color:#581c874d}.fts-result-default{background:#1118274d;border-color:#1f293780}.fts-highlight{background:#d977064d;color:var(--hm-gold);border-radius:2px;padding:0 2px}.health-card{padding:1rem;border-left:3px solid transparent;transition:border-color .2s}.health-card-ok{border-left-color:#22c55e}.health-card-degraded{border-left-color:#eab308}.health-card-down{border-left-color:#ef4444}.health-card-unconfigured{border-left-color:var(--hm-border)}.health-card-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.375rem}.health-card-icon{font-size:.875rem;flex-shrink:0}.health-card-name{font-size:.8125rem;font-weight:600;flex:1}.health-card-detail{font-size:.75rem;color:var(--hm-text-muted);line-height:1.4}.health-card-meta{margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--hm-border-subtle)}.health-meta-row{display:flex;align-items:center;gap:.5rem;padding:1px 0}.health-host-item{display:flex;align-items:center;gap:.375rem;padding:2px 0}.health-host-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.health-host-dot.dot-connected{background:#22c55e}.health-host-dot.dot-idle{background:#6b7280}.health-host-dot.dot-unknown{background:#374151}.res-bar-bg{height:8px;background:#1e293b;border-radius:4px;overflow:hidden}.res-bar-fill{height:100%;border-radius:4px;transition:width .4s ease;min-width:2px}.res-bar-blue{background:#3b82f6}.res-bar-purple{background:#a855f7}.res-bar-emerald{background:#10b981}.res-bar-amber{background:#f59e0b}.discord-global-card{display:grid;gap:var(--hm-space-4)}.discord-global-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-4)}.discord-global-heading p{margin-top:.2rem;color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.discord-global-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-3)}.discord-global-toggle,.discord-global-list{padding:var(--hm-space-4);border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);background:#04070b40}.discord-global-toggle{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.discord-global-list{display:grid;gap:var(--hm-space-2)}.discord-global-list strong{color:var(--hm-text);font-size:var(--hm-text-xs)}.discord-global-list>p{color:var(--hm-text-dim);font-size:.64rem;line-height:1.45}.discord-global-footer{padding-top:var(--hm-space-3);display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);border-top:1px solid var(--hm-border-subtle)}.discord-global-footer span{color:var(--hm-text-dim);font-size:var(--hm-text-xs)}@media(max-width:760px){.discord-global-heading,.discord-global-footer{align-items:stretch;flex-direction:column}.discord-global-grid{grid-template-columns:minmax(0,1fr)}.discord-global-footer .btn{width:100%}}.llm-advanced{margin-top:var(--hm-space-4);overflow:hidden;background:#04070b45;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.llm-advanced>summary{min-height:50px;padding:var(--hm-space-3) var(--hm-space-4);display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);color:var(--hm-text);cursor:pointer;list-style:none}.llm-advanced>summary::-webkit-details-marker{display:none}.llm-advanced>summary:after{content:"+";flex:none;color:var(--hm-accent);font-size:1rem}.llm-advanced[open]>summary:after{content:"−"}.llm-advanced>summary span{font-size:var(--hm-text-sm);font-weight:650}.llm-advanced>summary small{color:var(--hm-text-dim);font-size:var(--hm-text-xs);text-align:right}.llm-advanced-body{padding:var(--hm-space-4);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-4);border-top:1px solid var(--hm-border-subtle)}.llm-advanced-group{padding:var(--hm-space-4);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-3);background:#111720a6;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.llm-advanced-group>header{grid-column:1 / -1;display:grid;gap:.1rem}.llm-advanced-group>header strong{color:var(--hm-text);font-size:var(--hm-text-xs)}.llm-advanced-group>header span{color:var(--hm-text-dim);font-size:.62rem}.llm-advanced-group>label{min-width:0;display:grid;align-content:start;gap:.3rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.llm-advanced-group>label small{color:var(--hm-text-dim)}.llm-advanced-group.single{grid-template-columns:minmax(0,360px)}.llm-advanced-toggle{grid-template-columns:minmax(0,1fr) auto;align-items:center}.llm-advanced-footer{grid-column:1 / -1;display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4)}.llm-advanced-footer p{max-width:680px;color:var(--hm-text-dim);font-size:var(--hm-text-xs);line-height:1.5}.llm-advanced-state{grid-column:1 / -1;margin:0;color:var(--hm-text-dim);font-size:var(--hm-text-xs);line-height:1.45}.llm-advanced-state.pending{color:var(--hm-warning-text)}.llm-advanced.compact .llm-advanced-body{grid-template-columns:minmax(0,1fr) auto;align-items:end}.llm-advanced.compact .llm-advanced-footer{grid-column:auto}.llm-field-note{display:block;margin-top:.3rem;color:#9a8255;line-height:1.4}@media(max-width:760px){.llm-advanced>summary{align-items:flex-start}.llm-advanced>summary small{text-align:left}.llm-advanced-body,.llm-advanced.compact .llm-advanced-body,.llm-advanced-group{grid-template-columns:minmax(0,1fr)}.llm-advanced-footer,.llm-advanced.compact .llm-advanced-footer{grid-column:1;align-items:stretch;flex-direction:column}.llm-advanced-footer .btn{width:100%}}.config-center-page{--cfgc-rail-w: 218px;--cfgc-panel-border: rgba(49, 57, 71, .86);height:calc(100vh - var(--hm-topbar-h) - var(--hm-section-tabs-h));min-height:0!important;padding-bottom:0;display:flex;flex-direction:column;overflow:hidden}.cfgc-page-header{position:relative;z-index:16;flex:none;display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-6);margin-bottom:var(--hm-space-5)}.cfgc-eyebrow{margin-bottom:.28rem;color:var(--hm-accent);font-size:.625rem;font-weight:700;letter-spacing:.115em;line-height:1.2;text-transform:uppercase}.cfgc-page-summary{margin-top:.35rem;color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.cfgc-header-actions{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-2);flex-wrap:wrap}.cfgc-header-actions .btn{display:inline-flex;align-items:center;gap:var(--hm-space-2)}.cfgc-loading{display:grid;gap:var(--hm-space-4)}.cfgc-loading-grid{display:grid;grid-template-columns:var(--cfgc-rail-w) minmax(0,1fr);gap:var(--hm-space-5)}.cfgc-loading-grid .skeleton:last-child{grid-column:2}.cfgc-health{flex:none;margin-bottom:var(--hm-space-5);padding:var(--hm-space-5);background:radial-gradient(circle at 100% 0,rgba(197,139,50,.09),transparent 30%),linear-gradient(150deg,#131821fa,#0c1017fa);border:1px solid var(--cfgc-panel-border);border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-sm)}.cfgc-health-heading{display:flex;align-items:center;gap:var(--hm-space-4);margin-bottom:var(--hm-space-4)}.cfgc-health-heading>div:first-child{flex:1;min-width:0}.cfgc-health-heading h2,.cfgc-category-panel-heading h2,.cfgc-empty h2,.cfgc-review-header h2{color:var(--hm-text);font-size:var(--hm-text-lg);font-weight:650;letter-spacing:-.015em}.cfgc-health-ok,.cfgc-unsaved-pill{display:inline-flex;align-items:center;gap:var(--hm-space-2);padding:.3rem .55rem;border-radius:var(--hm-radius-full);font-size:var(--hm-text-xs);font-weight:650;white-space:nowrap}.cfgc-health-ok{color:#83d6a6;background:#22c55e14;border:1px solid rgba(34,197,94,.19)}.cfgc-unsaved-pill{color:#ecc16f;background:#d977061a;border:1px solid rgba(217,119,6,.24)}.cfgc-health-filters{display:grid;grid-template-columns:repeat(7,minmax(112px,1fr));gap:var(--hm-space-2);overflow-x:auto;scrollbar-width:thin}.cfgc-health-filter{min-width:0;min-height:56px;padding:.55rem .6rem;display:flex;align-items:center;gap:var(--hm-space-3);color:var(--hm-text-muted);background:#06090d61;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);text-align:left;cursor:pointer;transition:color var(--hm-transition-base),border-color var(--hm-transition-base),background var(--hm-transition-base)}.cfgc-health-filter:hover{color:var(--hm-text);border-color:var(--hm-border);background:#ffffff06}.cfgc-health-filter.active{color:var(--hm-text);border-color:#c58b326b;background:#c58b3213;box-shadow:inset 0 0 0 1px #c58b320d}.cfgc-health-icon{width:28px;height:28px;display:inline-grid;place-items:center;flex:none;color:#a5afbd;background:#ffffff0a;border-radius:var(--hm-radius-md)}.cfgc-health-icon.state-applied{color:#69c78e;background:#22c55e14}.cfgc-health-icon.state-pending_restart{color:#e1aa55;background:#d977061a}.cfgc-health-icon.state-dormant{color:#a792d8;background:#8b5cf617}.cfgc-health-icon.state-invalid{color:#e17878;background:#ef444417}.cfgc-health-icon.state-drift{color:#e1bd65;background:#eab30817}.cfgc-health-icon.state-unknown{color:#9aa6b6;background:#94a3b817}.cfgc-health-copy{min-width:0;display:grid;gap:.1rem}.cfgc-health-copy>span{font-size:var(--hm-text-xs);font-weight:650;line-height:1.25;overflow-wrap:anywhere;white-space:normal}.cfgc-health-copy small{color:var(--hm-text-dim);font-size:.625rem;white-space:nowrap}.cfgc-health-alert{display:flex;align-items:flex-start;gap:var(--hm-space-3);margin-top:var(--hm-space-3);padding:var(--hm-space-3) var(--hm-space-4);border-radius:var(--hm-radius-md);font-size:var(--hm-text-xs)}.cfgc-health-alert>.odin-icon{flex:none;margin-top:.1rem}.cfgc-health-alert>div{display:grid;gap:.1rem}.cfgc-health-alert>div span{color:var(--hm-text-muted)}.cfgc-health-alert.danger{color:#eb8a8a;background:#ef444413;border:1px solid rgba(239,68,68,.19)}.cfgc-health-alert.warning{color:#e3bb69;background:#eab30811;border:1px solid rgba(234,179,8,.17)}.cfgc-workspace{min-height:0;flex:1;display:grid;grid-template-columns:var(--cfgc-rail-w) minmax(0,1fr);align-items:stretch;gap:var(--hm-space-5);overflow:hidden}.cfgc-category-rail{position:relative;top:auto;height:100%;max-height:none;padding:var(--hm-space-3);overflow:hidden auto;background:#0c1017e6;border:1px solid var(--cfgc-panel-border);border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-sm)}.cfgc-rail-label{padding:var(--hm-space-2) var(--hm-space-3) var(--hm-space-3);color:var(--hm-text-dim);font-size:.625rem;font-weight:700;letter-spacing:.105em;text-transform:uppercase}.cfgc-category-scroll{display:grid;gap:.2rem}.cfgc-category{width:100%;min-width:0;padding:.55rem var(--hm-space-3);display:grid;grid-template-columns:26px minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-muted);background:transparent;border:1px solid transparent;border-radius:var(--hm-radius-md);text-align:left;cursor:pointer}.cfgc-category:hover{color:var(--hm-text);background:#ffffff07}.cfgc-category.active{color:#edd19a;background:#c58b3218;border-color:#c58b323d}.cfgc-category-icon{width:26px;height:26px;display:inline-grid;place-items:center;color:currentColor;background:#ffffff09;border-radius:var(--hm-radius-md)}.cfgc-category-copy{min-width:0;display:grid;gap:.05rem}.cfgc-category-copy>span{overflow:hidden;font-size:var(--hm-text-sm);font-weight:600;text-overflow:ellipsis;white-space:nowrap}.cfgc-category-copy small{color:var(--hm-text-dim);font-size:.6rem}.cfgc-category-counts{display:flex;align-items:center;justify-content:flex-end;gap:.18rem;flex-wrap:wrap;max-width:50px}.cfgc-category-counts>span,.cfgc-rail-key b{display:inline-grid;min-width:18px;height:18px;place-items:center;padding:0 .2rem;border-radius:4px;font-size:.55rem;font-weight:750}.cfgc-category-counts .modified,.cfgc-rail-key .modified{color:#edc171;background:#d977061f}.cfgc-category-counts .restart,.cfgc-rail-key .restart{color:#dfaa5d;background:#b4681b21}.cfgc-category-counts .invalid,.cfgc-rail-key .invalid{color:#e38282;background:#ef44441c}.cfgc-category-counts .dormant,.cfgc-rail-key .dormant{color:#b09adf;background:#8b5cf61f}.cfgc-rail-key{margin-top:var(--hm-space-3);padding:var(--hm-space-3) var(--hm-space-2) var(--hm-space-1);display:grid;grid-template-columns:1fr 1fr;gap:.32rem;color:var(--hm-text-dim);border-top:1px solid var(--hm-border-subtle);font-size:.56rem}.cfgc-rail-key span{display:flex;align-items:center;gap:.25rem}.cfgc-rail-key b{font-style:normal}.cfgc-main{min-width:0;min-height:0;height:100%;overflow-y:auto;padding-bottom:5rem}.cfgc-toolbar{position:sticky;top:0;z-index:12;margin:0 0 var(--hm-space-4);padding:var(--hm-space-3);display:flex;align-items:center;gap:var(--hm-space-3);background:#090c11f0;border:1px solid var(--cfgc-panel-border);border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-sm);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}.cfgc-search{min-width:0;height:38px;display:flex;align-items:center;flex:1;gap:var(--hm-space-3);padding:0 .45rem 0 .75rem;color:var(--hm-text-dim);background:#04070b8c;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.cfgc-search:focus-within{color:var(--hm-accent);border-color:#c58b3275;box-shadow:0 0 0 3px #c58b3214}.cfgc-search input{min-width:0;height:100%;flex:1;color:var(--hm-text);background:transparent;border:0;outline:0;font-size:var(--hm-text-sm)}.cfgc-search input::-moz-placeholder{color:#5f6977}.cfgc-search input::placeholder{color:#5f6977}.cfgc-search .icon-btn{width:28px;height:28px}.cfgc-category-panel{display:grid;gap:var(--hm-space-3)}.cfgc-category-panel+.cfgc-category-panel{margin-top:var(--hm-space-6)}.cfgc-category-panel-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--hm-space-4);padding:0 var(--hm-space-1)}.cfgc-category-panel-heading>span{color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.cfgc-empty{min-height:280px;display:grid;place-items:center;align-content:center;gap:var(--hm-space-3);color:var(--hm-text-dim);text-align:center}.cfgc-empty p{max-width:520px;color:var(--hm-text-muted);font-size:var(--hm-text-sm)}.cfgc-empty code{color:#c5a569}.cfgc-section{overflow:clip;background:linear-gradient(150deg,#121720f5,#0d1118fa);border:1px solid var(--cfgc-panel-border);border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-sm);transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.cfgc-section:hover{border-color:#394252}.cfgc-section.modified{border-color:#c58b3266;box-shadow:inset 3px 0 #c58b328c,var(--hm-shadow-sm)}.cfgc-section.editing{border-color:#c58b3294;box-shadow:inset 3px 0 var(--hm-accent),var(--hm-shadow-glow-sm)}.cfgc-section-header{width:100%;min-width:0;min-height:66px;padding:var(--hm-space-4) var(--hm-space-5);display:grid;grid-template-columns:20px minmax(150px,.65fr) minmax(220px,1.35fr) auto;align-items:center;gap:var(--hm-space-3);color:inherit;background:transparent;border:0;text-align:left;cursor:pointer}.cfgc-section-header:hover{background:#ffffff05}.cfgc-section-chevron{color:var(--hm-text-dim)}.cfgc-section-title{min-width:0;display:grid;gap:.12rem}.cfgc-section-title>span{overflow:hidden;color:var(--hm-text);font-size:var(--hm-text-sm);font-weight:650;text-overflow:ellipsis;white-space:nowrap}.cfgc-section-title small{overflow:hidden;color:#67717f;font-family:var(--hm-font-mono);font-size:.6rem;text-overflow:ellipsis;white-space:nowrap}.cfgc-section-summary{min-width:0;overflow:hidden;color:var(--hm-text-muted);font-size:var(--hm-text-xs);line-height:1.45;text-overflow:ellipsis;white-space:nowrap}.cfgc-section-badges{min-width:0;display:flex;align-items:center;justify-content:flex-end;gap:.3rem;flex-wrap:wrap}.cfgc-section-badges .badge{padding:.15rem .4rem;font-size:.56rem}.cfgc-badge-restart{color:#dba454;background:#d9770617;border:1px solid rgba(217,119,6,.22)}.cfgc-badge-dormant{color:#ac98d7;background:#8b5cf617;border:1px solid rgba(139,92,246,.2)}.cfgc-field-count{min-width:27px;height:22px;display:inline-grid;place-items:center;padding:0 .35rem;color:var(--hm-text-dim);background:#ffffff09;border-radius:var(--hm-radius-full);font-family:var(--hm-font-mono);font-size:.6rem}.cfgc-section-body{border-top:1px solid var(--hm-border-subtle)}.cfgc-section-actions{min-height:58px;padding:var(--hm-space-3) var(--hm-space-5);display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);background:#04070b47;border-bottom:1px solid var(--hm-border-subtle)}.cfgc-section-actions>div:first-child{min-width:0;display:grid;gap:.1rem}.cfgc-section-actions strong{color:var(--hm-text);font-size:var(--hm-text-xs);font-weight:650}.cfgc-section-actions span{overflow:hidden;color:var(--hm-text-dim);font-size:.625rem;text-overflow:ellipsis;white-space:nowrap}.cfgc-section-actions .btn{display:inline-flex;align-items:center;gap:var(--hm-space-2)}.cfgc-search-hits{padding:var(--hm-space-3) var(--hm-space-5);display:flex;align-items:center;gap:var(--hm-space-2);flex-wrap:wrap;color:var(--hm-text-dim);background:#c58b320a;border-bottom:1px solid rgba(197,139,50,.12);font-size:.625rem}.cfgc-search-hits button{padding:.22rem .4rem;color:#dbbd84;background:#c58b3214;border:1px solid rgba(197,139,50,.18);border-radius:5px;cursor:pointer}.cfgc-search-hits code{margin-left:.2rem;color:#7b8491}.cfgc-owner-card{margin:var(--hm-space-5);padding:var(--hm-space-5);display:grid;grid-template-columns:36px minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-4);background:#c58b320e;border:1px solid rgba(197,139,50,.18);border-radius:var(--hm-radius-md)}.cfgc-owner-card.compact{margin-bottom:0}.cfgc-owner-icon{width:36px;height:36px;display:inline-grid;place-items:center;color:#ddba78;background:#c58b321a;border-radius:var(--hm-radius-md)}.cfgc-owner-card strong{color:var(--hm-text);font-size:var(--hm-text-sm)}.cfgc-owner-card p{margin-top:.18rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs);line-height:1.5}.cfgc-owner-card .btn{display:inline-flex;align-items:center;gap:var(--hm-space-2)}.cfgc-fields{padding:0 var(--hm-space-5)}.cfgc-field{min-width:0;padding:var(--hm-space-5) 0;display:grid;grid-template-columns:minmax(210px,.95fr) minmax(240px,1.05fr);gap:var(--hm-space-6);scroll-margin-top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h) + 70px);border-bottom:1px solid var(--hm-border-subtle)}.cfgc-field:last-child{border-bottom:0}.cfgc-field.changed{margin-inline:calc(var(--hm-space-3) * -1);padding-inline:var(--hm-space-3);background:#c58b3209;border-radius:var(--hm-radius-md)}.cfgc-field.invalid{background:#ef444409}.cfgc-field-copy{min-width:0;padding-inline:var(--hm-space-4)}.cfgc-field-copy>label{display:block;color:var(--hm-text);font-size:var(--hm-text-sm);font-weight:630}.cfgc-field-copy>code{display:block;margin-top:.2rem;overflow-wrap:anywhere;color:#687382;font-family:var(--hm-font-mono);font-size:.6rem}.cfgc-field-copy>p{max-width:620px;margin-top:.42rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs);line-height:1.52}.cfgc-field-meta{margin-top:var(--hm-space-3);display:flex;align-items:center;gap:var(--hm-space-2);flex-wrap:wrap;color:var(--hm-text-dim);font-size:.6rem}.cfgc-apply-pill{display:inline-flex;align-items:center;min-height:20px;padding:.14rem .42rem;color:#9ea8b5;background:#ffffff09;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-full);font-size:.58rem;font-weight:650;line-height:1.2}.cfgc-apply-pill.apply-live-read,.cfgc-apply-pill.apply-live-apply{color:#78cb98;background:#22c55e12;border-color:#22c55e2b}.cfgc-apply-pill.apply-live-for-new-work{color:#78b9d6;background:#0ea5e912;border-color:#0ea5e92e}.cfgc-apply-pill.apply-restart{color:#dfa858;background:#d9770614;border-color:#d9770630}.cfgc-apply-pill.apply-activation-required,.cfgc-apply-pill.apply-dormant{color:#af99df;background:#8b5cf614;border-color:#8b5cf62e}.cfgc-apply-pill.apply-legacy-control{color:#d8bd70;background:#eab30812;border-color:#eab3082e}.cfgc-sensitive{display:inline-flex;align-items:center;gap:.22rem;color:#b6a06f}.cfgc-apply-details{margin-top:var(--hm-space-3);display:grid;gap:var(--hm-space-2)}.cfgc-section-apply-details{margin:0 var(--hm-space-5) var(--hm-space-4);display:grid;gap:var(--hm-space-2)}.cfgc-apply-detail{padding:.55rem .65rem;background:#04070b42;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.cfgc-apply-detail-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-2)}.cfgc-apply-detail-heading strong{color:#aeb8c5;font-size:.64rem;font-weight:650}.cfgc-apply-detail>p{margin-top:.3rem;color:var(--hm-text-dim);font-size:.62rem;line-height:1.45}.cfgc-apply-detail>code{display:block;margin-top:.3rem;overflow-wrap:anywhere;color:#c8ad75;font-family:var(--hm-font-mono);font-size:.6rem}.cfgc-apply-detail.detail-restart{border-color:#d9770629}.cfgc-apply-detail.detail-activation{border-color:#8b5cf62b}.cfgc-field-control{min-width:0;display:grid;align-content:start}.cfgc-field-control .hm-input,.cfgc-field-control .hm-select{width:100%}.cfgc-field-control .hm-input[type=number]{max-width:250px}.cfgc-value{display:block;min-width:0;padding:.52rem .65rem;overflow:hidden;color:#c2c9d2;background:#04070b5e;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);text-overflow:ellipsis;white-space:nowrap}.cfgc-value-block{max-height:160px;margin:0;padding:var(--hm-space-3);overflow:auto;color:#aeb7c3;background:#04070b5e;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);font-family:var(--hm-font-mono);font-size:.65rem;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere}.cfgc-boolean-control{min-height:38px;padding:.42rem .6rem;display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);color:var(--hm-text-muted);background:#04070b57;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);font-size:var(--hm-text-xs)}.cfgc-json-input{resize:vertical;line-height:1.5}.cfgc-expert-note{margin-top:.35rem;color:var(--hm-text-dim);font-size:.6rem}.cfgc-field-error{margin-top:.35rem;color:#e27c7c;font-size:var(--hm-text-xs)}.cfgc-write-only{padding:var(--hm-space-3);display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.15rem var(--hm-space-4);background:#c58b320b;border:1px solid rgba(197,139,50,.15);border-radius:var(--hm-radius-md)}.cfgc-write-only>span{display:inline-flex;align-items:center;gap:var(--hm-space-2);color:#cdb57e;font-size:var(--hm-text-xs);font-weight:620}.cfgc-write-only>small{grid-column:1;color:var(--hm-text-dim);font-size:.6rem}.cfgc-write-only>button{grid-column:2;grid-row:1 / span 2}.cfgc-mobile-action-bar{display:none}.cfgc-review-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:80;display:flex;justify-content:flex-end;background:#000000a8;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.cfgc-review-tray{width:min(680px,94vw);height:100%;display:grid;grid-template-rows:auto minmax(0,1fr) auto;color:var(--hm-text);background:#0c1017;border-left:1px solid #303846;box-shadow:-24px 0 70px #0000006b;outline:0;animation:cfgc-tray-in .18s ease-out}@keyframes cfgc-tray-in{0%{transform:translate(20px);opacity:.55}to{transform:translate(0);opacity:1}}.cfgc-review-header{padding:var(--hm-space-6);display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-4);border-bottom:1px solid var(--hm-border)}.cfgc-review-header p{margin-top:.32rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.cfgc-review-body{min-height:0;padding:var(--hm-space-5) var(--hm-space-6);overflow-y:auto}.cfgc-review-group{overflow:hidden;background:#121720bd;border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg)}.cfgc-review-group+.cfgc-review-group{margin-top:var(--hm-space-4)}.cfgc-review-group>header{padding:var(--hm-space-3) var(--hm-space-4);display:flex;align-items:center;justify-content:space-between;background:#05080c5c;border-bottom:1px solid var(--hm-border-subtle)}.cfgc-review-group>header>span:last-child{min-width:23px;height:23px;display:inline-grid;place-items:center;color:var(--hm-text-dim);background:#ffffff0a;border-radius:var(--hm-radius-full);font-family:var(--hm-font-mono);font-size:.6rem}.cfgc-review-entry{padding:var(--hm-space-4);display:grid;grid-template-columns:minmax(160px,.7fr) minmax(200px,1.3fr);align-items:center;gap:var(--hm-space-4);border-bottom:1px solid var(--hm-border-subtle)}.cfgc-review-entry:last-child{border-bottom:0}.cfgc-review-entry>div:first-child{min-width:0;display:grid;gap:.17rem}.cfgc-review-entry strong{overflow:hidden;font-size:var(--hm-text-xs);font-weight:620;text-overflow:ellipsis;white-space:nowrap}.cfgc-review-entry code{overflow-wrap:anywhere;color:var(--hm-text-dim);font-size:.58rem}.cfgc-review-values{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:var(--hm-space-2)}.cfgc-review-values span{min-width:0;padding:.38rem .48rem;overflow:hidden;color:#9da7b4;background:#04070b66;border:1px solid var(--hm-border-subtle);border-radius:5px;font-family:var(--hm-font-mono);font-size:.6rem;text-overflow:ellipsis;white-space:nowrap}.cfgc-review-values span:last-child{color:#d5bd89;border-color:#c58b322b}.cfgc-review-values .odin-icon{color:var(--hm-text-dim)}.cfgc-review-footer{padding:var(--hm-space-5) var(--hm-space-6);display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:var(--hm-space-3);background:#080b10f5;border-top:1px solid var(--hm-border)}.cfgc-review-footer>div{min-width:0;display:grid;gap:.1rem}.cfgc-review-footer strong{color:var(--hm-text);font-size:var(--hm-text-xs);font-weight:620}.cfgc-review-footer span{color:var(--hm-text-dim);font-size:.6rem}.config-center-page{max-width:1600px;margin-inline:auto}.cfgc-health-filters{grid-template-columns:repeat(auto-fit,minmax(128px,1fr))}.cfgc-restart-banner{flex:none;margin-bottom:var(--hm-space-5);padding:var(--hm-space-4) var(--hm-space-5);display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-4);color:#e6ba6c;background:#b4681b17;border:1px solid rgba(217,119,6,.28);border-radius:var(--hm-radius-lg)}.cfgc-restart-banner>div:nth-child(2){min-width:0;display:grid;gap:.15rem}.cfgc-restart-banner strong{color:#f0cf92;font-size:var(--hm-text-sm)}.cfgc-restart-banner span{color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.cfgc-restart-actions{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-2);flex-wrap:wrap}.cfgc-restart-dialog{width:min(620px,calc(100vw - 2rem));margin:auto;padding:var(--hm-space-6);color:var(--hm-text);background:#0f141c;border:1px solid #3a4352;border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-lg)}.cfgc-restart-dialog h2{font-size:var(--hm-text-xl);font-weight:650}.cfgc-restart-dialog>p{margin-top:var(--hm-space-3);color:var(--hm-text-muted);font-size:var(--hm-text-sm);line-height:1.55}.cfgc-restart-dialog-actions{margin-top:var(--hm-space-5);display:flex;justify-content:flex-end;gap:var(--hm-space-2);flex-wrap:wrap}.cfgc-field-groups{display:grid;gap:var(--hm-space-4);padding:0 var(--hm-space-5) var(--hm-space-5)}.cfgc-field-group{overflow:clip;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.cfgc-field-group:not(.nested){border-color:transparent}.cfgc-field-group-header{padding:var(--hm-space-3) var(--hm-space-4);display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-4);background:#04070b59;border-bottom:1px solid var(--hm-border-subtle)}.cfgc-field-group-header>div{min-width:0;display:grid;gap:.18rem}.cfgc-field-group-header strong{color:var(--hm-text);font-size:var(--hm-text-sm)}.cfgc-field-group-header code{color:var(--hm-text-dim);font-size:.58rem}.cfgc-field-group-header p{color:var(--hm-text-muted);font-size:var(--hm-text-xs);line-height:1.45}.cfgc-field-group-header>span{flex:none;color:var(--hm-text-dim);font-size:.6rem}.cfgc-field-group .cfgc-fields{padding:0}.cfgc-field{grid-template-columns:minmax(180px,4fr) minmax(190px,5fr);align-items:start}.cfgc-field-runtime-note{grid-column:1 / -1;min-width:0;margin-top:calc(var(--hm-space-2) * -1);padding:var(--hm-space-3);display:grid;gap:.25rem;background:#8b5cf60b;border:1px solid rgba(139,92,246,.16);border-radius:var(--hm-radius-md)}.cfgc-field-runtime-note strong{color:#b9a6df;font-size:.6rem;text-transform:uppercase;letter-spacing:.055em}.cfgc-field-runtime-note p{color:var(--hm-text-muted);font-size:.66rem;line-height:1.45}.cfgc-field-runtime-note .btn{margin-top:var(--hm-space-2);justify-self:start}.cfgc-runtime-summary-list{margin-top:var(--hm-space-3);display:grid;gap:var(--hm-space-2)}.cfgc-runtime-summary{padding:var(--hm-space-3);background:#04070b45;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.cfgc-runtime-summary strong{color:#b8c0cb;font-size:.62rem}.cfgc-runtime-summary p{margin-top:.25rem;color:var(--hm-text-muted);font-size:.66rem;line-height:1.45}.cfgc-group-apply-details{padding:0 var(--hm-space-4) var(--hm-space-4)}.cfgc-group-apply-details summary{color:var(--hm-text-dim);cursor:pointer;font-size:var(--hm-text-xs)}.cfgc-apply-detail-list{margin-top:var(--hm-space-3);display:grid;gap:var(--hm-space-2)}.discord-user-combobox{position:relative;width:100%}.discord-user-combobox>.hm-input{width:100%}.discord-user-combobox-options{position:absolute;z-index:60;top:calc(100% + .25rem);left:0;right:0;max-height:15rem;overflow-y:auto;background:#0d121a;border:1px solid #3b4555;border-radius:var(--hm-radius-md);box-shadow:var(--hm-shadow-lg)}.discord-user-combobox-option{width:100%;min-height:38px;padding:.45rem .65rem;display:flex;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-muted);background:transparent;border:0;cursor:pointer;font-size:var(--hm-text-xs);text-align:left}.discord-user-combobox-option:hover,.discord-user-combobox-option.active{color:var(--hm-text);background:#c58b3217}.discord-user-combobox-option img,.discord-user-combobox-avatar{width:22px;height:22px;flex:none;border-radius:var(--hm-radius-full)}.discord-user-combobox-avatar{display:inline-grid;place-items:center;color:var(--hm-text-dim);background:#293140;font-size:.6rem}.discord-user-combobox-name{min-width:0;overflow:hidden;color:inherit;text-overflow:ellipsis;white-space:nowrap}.discord-user-combobox-username{min-width:0;overflow:hidden;color:var(--hm-text-dim);text-overflow:ellipsis;white-space:nowrap}.discord-user-combobox-bot{margin-left:auto;padding:.08rem .25rem;color:#c4b5fd;background:#6366f133;border-radius:3px;font-size:.52rem}.discord-global-user-picker{display:block}.discord-global-list-full{grid-column:1 / -1}.cfgc-chip-editor{display:grid;gap:var(--hm-space-3)}.cfgc-chip-list{min-height:32px;display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}.cfgc-chip{max-width:100%;padding:.25rem .35rem .25rem .55rem;display:inline-flex;align-items:center;gap:.35rem;overflow-wrap:anywhere;color:#d8c28e;background:#c58b3214;border:1px solid rgba(197,139,50,.2);border-radius:var(--hm-radius-full);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs)}.cfgc-chip button{width:20px;height:20px;color:#a9956e;background:transparent;border:0;border-radius:50%;cursor:pointer}.cfgc-chip button:hover{color:#fff;background:#ffffff14}.cfgc-chip-empty{color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.cfgc-chip-add{display:flex;align-items:center;gap:var(--hm-space-2);flex-wrap:wrap;color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.cfgc-chip-add .hm-input{width:min(170px,100%)}.cfgc-structured-summary{padding:var(--hm-space-3);display:grid;gap:.15rem;background:#04070b52;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.cfgc-structured-summary span{color:var(--hm-text);font-size:var(--hm-text-xs)}.cfgc-structured-summary small{color:var(--hm-text-dim);font-size:.62rem;line-height:1.4}@media(max-width:1180px){.config-center-page{--cfgc-rail-w: 190px}.cfgc-health-filters{grid-template-columns:repeat(7,minmax(106px,1fr))}.cfgc-category-copy small{display:none}.cfgc-section-header{grid-template-columns:20px minmax(0,1fr) auto}.cfgc-section-summary{grid-column:2;white-space:normal}.cfgc-section-badges{grid-column:3;grid-row:1 / span 2}.cfgc-field{grid-template-columns:minmax(180px,5fr) minmax(220px,7fr)}}@media(max-width:900px){.config-center-page{--cfgc-rail-w: 100%;height:auto;min-height:100%!important;overflow:visible}.cfgc-workspace{min-height:auto;flex:none;grid-template-columns:minmax(0,1fr);align-items:start;gap:var(--hm-space-3);overflow:visible}.cfgc-main{height:auto;overflow:visible;padding-bottom:5rem}.cfgc-category-rail{position:sticky;top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h));height:auto;z-index:13;max-height:none;padding:var(--hm-space-2);overflow:visible}.cfgc-rail-label,.cfgc-rail-key{display:none}.cfgc-category-scroll{display:flex;gap:var(--hm-space-2);overflow-x:auto;scroll-snap-type:x proximity}.cfgc-category{width:auto;min-width:150px;grid-template-columns:24px minmax(0,1fr) auto;flex:0 0 auto;scroll-snap-align:start}.cfgc-category-copy small{display:block}.cfgc-toolbar{top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h) + 59px)}.cfgc-section-header{grid-template-columns:20px minmax(120px,.8fr) minmax(150px,1.2fr) auto}.cfgc-field{grid-template-columns:minmax(180px,.85fr) minmax(210px,1.15fr);gap:var(--hm-space-4)}}@media(max-width:760px){.section-tabs-wrap{position:sticky;overflow:hidden}.section-tabs-wrap:after{content:"";position:absolute;top:0;right:0;bottom:0;width:34px;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(9,12,17,.98));border-right:2px solid rgba(197,139,50,.28)}.config-center-page{padding-bottom:6.75rem!important}.cfgc-page-header{align-items:stretch}.cfgc-header-actions{flex:none}.cfgc-header-actions .cfgc-desktop-history,.cfgc-header-actions .btn-primary{display:none}.cfgc-health{margin-inline:-.15rem;padding:var(--hm-space-4)}.cfgc-health-heading{align-items:flex-start;flex-wrap:wrap}.cfgc-health-filters{grid-template-columns:repeat(7,122px);margin-inline:calc(var(--hm-space-4) * -1);padding-inline:var(--hm-space-4);padding-bottom:.2rem;scroll-snap-type:x proximity}.cfgc-health-filter{scroll-snap-align:start}.cfgc-category-rail{top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h));margin-inline:-.1rem}.cfgc-category-scroll{padding-right:1.5rem}.cfgc-category-scroll:after{content:"";flex:0 0 .1rem}.cfgc-toolbar{top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h) + 58px);padding:var(--hm-space-2)}.cfgc-toolbar>.btn{display:none}.cfgc-search input{font-size:1rem}.cfgc-category-panel-heading{align-items:center}.cfgc-section-header{min-height:72px;padding:var(--hm-space-4);grid-template-columns:18px minmax(0,1fr) auto;gap:var(--hm-space-2)}.cfgc-section-summary,.cfgc-section-badges .badge{display:none}.cfgc-section-actions{padding:var(--hm-space-3) var(--hm-space-4);align-items:stretch;flex-direction:column}.cfgc-section-actions>div:last-child{justify-content:flex-end}.cfgc-search-hits{padding-inline:var(--hm-space-4)}.cfgc-owner-card{margin:var(--hm-space-4);grid-template-columns:34px minmax(0,1fr);padding:var(--hm-space-4)}.cfgc-owner-card .btn{grid-column:1 / -1;width:100%;justify-content:center}.cfgc-field-groups{padding-inline:var(--hm-space-4)}.cfgc-fields{padding:0}.cfgc-field{grid-template-columns:minmax(0,1fr);gap:var(--hm-space-3);padding-block:var(--hm-space-4)}.cfgc-field-copy>p{line-height:1.45}.cfgc-write-only{grid-template-columns:minmax(0,1fr)}.cfgc-write-only>small,.cfgc-write-only>button{grid-column:1;grid-row:auto}.cfgc-write-only>button{width:100%;margin-top:var(--hm-space-2)}.cfgc-restart-banner{grid-template-columns:auto minmax(0,1fr);padding:var(--hm-space-4)}.cfgc-restart-actions{grid-column:1 / -1;justify-content:stretch}.cfgc-restart-actions .btn{flex:1}.cfgc-restart-dialog-actions{display:grid}.cfgc-mobile-action-bar{position:fixed;z-index:55;left:0;right:0;bottom:0;min-height:68px;padding:var(--hm-space-3) max(var(--hm-space-4),env(safe-area-inset-right)) calc(var(--hm-space-3) + env(safe-area-inset-bottom)) max(var(--hm-space-4),env(safe-area-inset-left));display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-2);background:#090c11fa;border-top:1px solid #394252;box-shadow:0 -12px 32px #0006;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.cfgc-mobile-action-bar .btn{min-width:0;padding-inline:.55rem}.cfgc-mobile-overflow{position:relative}.cfgc-mobile-overflow-menu{position:absolute;right:0;bottom:calc(100% + var(--hm-space-2));width:170px;padding:var(--hm-space-2);display:grid;gap:.15rem;background:#111720;border:1px solid #3a4352;border-radius:var(--hm-radius-md);box-shadow:var(--hm-shadow-lg)}.cfgc-mobile-overflow-menu button{min-height:38px;padding:0 var(--hm-space-3);display:flex;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-muted);background:transparent;border:0;border-radius:5px;font-size:var(--hm-text-sm);text-align:left}.cfgc-mobile-overflow-menu button:hover:not(:disabled){color:var(--hm-text);background:#ffffff0a}.cfgc-mobile-overflow-menu button:disabled{opacity:.38}.cfgc-review-tray{width:100%;max-width:none}.cfgc-review-header{padding:var(--hm-space-5) var(--hm-space-4)}.cfgc-review-body{padding:var(--hm-space-4)}.cfgc-review-entry{grid-template-columns:minmax(0,1fr);gap:var(--hm-space-3)}.cfgc-review-footer{padding:var(--hm-space-4);grid-template-columns:1fr 1fr}.cfgc-review-footer>div{grid-column:1 / -1}}@media(max-width:420px){.cfgc-page-header{gap:var(--hm-space-3)}.cfgc-header-actions .btn{padding-inline:.65rem}.cfgc-page-summary{max-width:220px}.cfgc-mobile-action-bar{grid-template-columns:minmax(70px,1fr) minmax(90px,1fr) 36px}.cfgc-section-title>span{white-space:normal}.cfgc-review-values{grid-template-columns:minmax(0,1fr)}.cfgc-review-values .odin-icon{margin:-.1rem auto;transform:rotate(90deg)}}:root{--hm-topbar-h: 76px;--hm-rail-h: 107px;--hm-section-tabs-inner-h: 48px;--hm-section-tabs-border: 1px;--hm-section-tabs-h: calc(var(--hm-section-tabs-inner-h) + var(--hm-section-tabs-border));--hm-bg: #07090d;--hm-bg-raised: #0b0e14;--hm-surface: #10141c;--hm-surface-hover: #171d28;--hm-surface-elevated: #151a24;--hm-border: #252c39;--hm-border-subtle: #1a202b;--hm-accent: #c58b32;--hm-accent-hover: #dda84f;--hm-accent-dim: rgba(197, 139, 50, .13);--hm-accent-glow: rgba(197, 139, 50, .16);--hm-gold: #e0b766;--hm-gold-dim: rgba(197, 139, 50, .08);--hm-text: #edf0f5;--hm-text-muted: #9aa4b3;--hm-text-dim: #667182;--hm-success: #3db983;--hm-warning: #d8a342;--hm-danger: #e06464;--hm-info: #668fd7;--hm-radius-sm: 5px;--hm-radius-md: 7px;--hm-radius-lg: 10px;--hm-radius-xl: 14px;--hm-shadow-sm: 0 1px 2px rgba(0,0,0,.24), 0 0 0 1px rgba(255,255,255,.015);--hm-shadow-md: 0 10px 30px rgba(0,0,0,.28);--hm-shadow-lg: 0 24px 64px rgba(0,0,0,.48);--shell-sidebar: 252px;--shell-sidebar-collapsed: 72px}html{background:var(--hm-bg)}body{color:var(--hm-text);background:radial-gradient(circle at 72% -20%,rgba(197,139,50,.055),transparent 34rem),linear-gradient(180deg,#080a0f 0%,var(--hm-bg) 35%)}button,input,select,textarea{font:inherit}::-moz-selection{background:#c58b3252;color:#fff}::selection{background:#c58b3252;color:#fff}.odin-icon{display:block;flex:none}.app-loading{min-height:100vh;display:grid;place-items:center}.brand-loader{color:var(--hm-accent-hover);animation:brand-pulse 1.4s ease-in-out infinite}@keyframes brand-pulse{50%{opacity:.45;transform:scale(.94)}}.app-shell{display:flex;min-height:100vh}.hm-sidebar{position:relative;width:var(--shell-sidebar);min-width:var(--shell-sidebar);height:100vh;min-height:0;background:#0d1017f5;border-right:1px solid var(--hm-border-subtle);box-shadow:10px 0 35px #0000001f;transition:width .22s ease,min-width .22s ease,transform .22s ease;display:flex;flex-direction:column;z-index:40}.hm-sidebar:before{content:"";position:absolute;inset:0 auto 0 0;width:2px;background:linear-gradient(180deg,transparent 3%,var(--hm-accent) 24%,rgba(197,139,50,.15) 68%,transparent);opacity:.65;pointer-events:none}.hm-sidebar.collapsed{width:var(--shell-sidebar-collapsed);min-width:var(--shell-sidebar-collapsed)}.sidebar-brand{min-height:76px;padding:0 16px 0 18px;display:flex;align-items:center;gap:11px;border-bottom:1px solid var(--hm-border-subtle)}.brand-mark{width:34px;height:34px;display:grid;place-items:center;color:var(--hm-accent-hover);background:linear-gradient(145deg,#c58b3229,#c58b3209);border:1px solid rgba(197,139,50,.3);border-radius:9px;flex:none}.sidebar-brand-copy{display:flex;flex-direction:column;min-width:0;line-height:1}.brand-wordmark{font-size:.83rem;letter-spacing:.23em;font-weight:700;color:#f2dfb8}.brand-caption{margin-top:7px;font-size:.61rem;letter-spacing:.1em;text-transform:uppercase;color:var(--hm-text-dim)}.sidebar-toggle-btn{margin-left:auto}.hm-sidebar.collapsed .sidebar-brand{padding-inline:18px;justify-content:center}.hm-sidebar.collapsed .sidebar-brand-copy,.hm-sidebar.collapsed .nav-label,.hm-sidebar.collapsed .nav-section-label,.hm-sidebar.collapsed .connection-copy,.hm-sidebar.collapsed .shortcut-hint span,.hm-sidebar.collapsed .shortcut-hint kbd,.hm-sidebar.collapsed .brand-mark{display:none}.hm-sidebar.collapsed .sidebar-toggle-btn{margin-left:0}.sidebar-nav{flex:1;min-height:0;padding:13px 10px;overflow:auto}.nav-group+.nav-group{margin-top:18px}.nav-section-label{padding:0 10px 6px;color:#555f6f;font-size:.625rem;line-height:1;text-transform:uppercase;letter-spacing:.14em;font-weight:700}.nav-item{position:relative;min-height:40px;display:flex;align-items:center;white-space:nowrap;overflow:hidden;margin:2px 0;padding:0 10px;gap:11px;color:#8d97a7;border-radius:7px;font-size:.79rem;font-weight:500;text-decoration:none;cursor:pointer;transition:background var(--hm-transition-base),color var(--hm-transition-base)}.nav-item:hover{background:#ffffff09;color:#d9dee7}.nav-item.active{background:linear-gradient(90deg,#c58b3224,#c58b320b);color:#ebc982;box-shadow:inset 2px 0 0 var(--hm-accent)}.nav-item.active:after{content:"";position:absolute;right:10px;width:4px;height:4px;background:var(--hm-accent-hover);border-radius:50%;box-shadow:0 0 8px var(--hm-accent)}.nav-icon{width:20px;display:grid;flex:none;place-items:center;color:#717d8f}.nav-item:hover .nav-icon{color:#aeb7c4}.nav-item.active .nav-icon{color:var(--hm-accent-hover)}.hm-sidebar.collapsed .sidebar-nav{padding-inline:12px}.hm-sidebar.collapsed .nav-item{padding:0;justify-content:center}.hm-sidebar.collapsed .nav-item.active:after{right:5px}.sidebar-footer{min-height:var(--hm-rail-h);padding:12px;border-top:1px solid var(--hm-border-subtle);display:grid;gap:8px;align-content:center}.connection-card{min-height:42px;padding:8px 10px;display:flex;align-items:center;gap:9px;border:1px solid var(--hm-border-subtle);background:#00000024;border-radius:8px}.connection-copy{min-width:0;display:flex;flex:1;justify-content:space-between;align-items:baseline;gap:8px}.connection-label{font-size:.7rem;color:var(--hm-text-muted)}.connection-latency{font-family:var(--hm-font-mono);font-size:.58rem;color:var(--hm-text-dim)}.shortcut-hint{width:100%;min-height:32px;display:flex;align-items:center;gap:7px;padding:0 9px;border:0;border-radius:6px;background:transparent;color:var(--hm-text-dim);font-size:.67rem;cursor:pointer}.shortcut-hint:hover{color:var(--hm-text-muted);background:#ffffff08}.shortcut-hint kbd{margin-left:auto}.hm-sidebar.collapsed .connection-card,.hm-sidebar.collapsed .shortcut-hint{justify-content:center;padding-inline:0}.hm-main{flex:1;min-width:0;height:100vh;max-height:none;overflow-y:auto;background:transparent}.hm-topbar{position:sticky;top:0;display:flex;align-items:center;z-index:25;height:var(--hm-topbar-h);padding:0 28px;gap:22px;background:#07090de0;border-bottom:1px solid var(--hm-border-subtle);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.topbar-context{display:flex;flex-direction:column;min-width:140px}.topbar-kicker{margin-bottom:2px;color:var(--hm-accent);font-size:.57rem;line-height:1;text-transform:uppercase;letter-spacing:.16em;font-weight:700}.topbar-title-row{display:flex;align-items:center;gap:10px}.topbar-title-row h1{margin:0;color:#f3f4f7;font-size:1rem;line-height:1.25;font-weight:650;letter-spacing:-.018em}.topbar-description{margin:0;padding-left:22px;border-left:1px solid var(--hm-border);color:var(--hm-text-dim);font-size:.72rem}.topbar-actions{margin-left:auto;display:flex;align-items:center;gap:8px}.uptime-label{margin-right:5px;color:var(--hm-text-dim);font-family:var(--hm-font-mono);font-size:.62rem}.status-pill{display:inline-flex;align-items:center;gap:5px;color:var(--hm-text-muted);font-size:.58rem;text-transform:capitalize;font-weight:600}.status-pill .status-dot{width:6px;height:6px;box-shadow:none}.command-trigger{min-height:34px;padding:0 8px 0 10px;display:flex;align-items:center;gap:7px;color:var(--hm-text-muted);background:#ffffff06;border:1px solid var(--hm-border);border-radius:7px;font-size:.68rem;cursor:pointer}.command-trigger:hover{color:var(--hm-text);border-color:#3a4352;background:#ffffff0b}kbd{padding:2px 5px;color:#818b99;background:#090c11;border:1px solid #2b3340;border-radius:4px;font-family:var(--hm-font-sans);font-size:.57rem;box-shadow:inset 0 -1px #ffffff09}.page-viewport{min-height:calc(100vh - var(--hm-topbar-h))}.icon-btn.mobile-menu-btn,.mobile-scrim{display:none}.icon-btn{width:34px;height:34px;padding:0;display:inline-grid;place-items:center;flex:none;color:var(--hm-text-muted);background:transparent;border:1px solid transparent;border-radius:7px;cursor:pointer;transition:color .15s ease,border-color .15s ease,background .15s ease}.icon-btn:hover{color:var(--hm-text);background:#ffffff0b;border-color:var(--hm-border)}.icon-btn-danger{color:#d67a7a}.icon-btn-danger:hover{color:#f08a8a;background:#e064641a;border-color:#e064643d}.section-shell{min-height:calc(100vh - var(--hm-topbar-h))}.section-tabs-wrap{position:sticky;top:var(--hm-topbar-h);z-index:20;padding:0 24px;background:#090c11eb;border-bottom:var(--hm-section-tabs-border) solid var(--hm-border-subtle);-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.section-tabs{display:flex;align-items:stretch;gap:2px;min-height:var(--hm-section-tabs-inner-h);overflow-x:auto}.section-tab{position:relative;padding:0 13px;white-space:nowrap;color:#7f8998;background:transparent;border:0;font-size:.72rem;font-weight:550;cursor:pointer}.section-tab:hover{color:#c8ced7}.section-tab.active{color:#e9ca8e}.section-tab.active:after{content:"";position:absolute;left:13px;right:13px;bottom:0;height:2px;background:var(--hm-accent);border-radius:2px 2px 0 0;box-shadow:0 -3px 12px #c58b3233}.section-panel>*{min-height:100%}.hm-card{background:linear-gradient(150deg,#121720f5,#0e1219fa);border-color:var(--hm-border);box-shadow:var(--hm-shadow-sm)}.hm-card:hover{border-color:#303847}.hm-table th{background:#07090d4d;color:#7e8898;font-size:.64rem;letter-spacing:.075em;font-weight:650}.hm-table td{border-bottom-color:var(--hm-border-subtle)}.hm-table tr:hover td{background:#ffffff05}.btn{min-height:34px;padding:7px 12px;border:1px solid transparent;font-weight:600}.btn-primary{color:#171006;background:linear-gradient(180deg,#d4a14e,#b77b29);border-color:#dcaa57;box-shadow:inset 0 1px #ffffff21,0 5px 14px #83521324}.btn-primary:hover{background:linear-gradient(180deg,#e2b35f,#c88a31);box-shadow:0 7px 18px #83521333}.btn-ghost{border-color:var(--hm-border);background:#ffffff04}.btn-ghost:hover{border-color:#394251;background:#ffffff0a}.hm-input{min-height:38px;background:#090c11;border-color:#2a3240}.hm-input:hover{border-color:#36404f}.hm-input:focus{border-color:var(--hm-accent);box-shadow:0 0 0 3px #c58b321a}.login-shell{min-height:100vh;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at 50% 20%,rgba(197,139,50,.09),transparent 30rem)}.login-panel{width:100%;max-width:380px;padding:34px;background:linear-gradient(150deg,#131821f7,#0b0e14fc);border:1px solid var(--hm-border);border-radius:14px;box-shadow:var(--hm-shadow-lg)}.login-brand{width:48px;height:48px;margin-bottom:26px;display:grid;place-items:center;color:var(--hm-accent-hover);background:var(--hm-accent-dim);border:1px solid rgba(197,139,50,.3);border-radius:11px}.login-eyebrow{margin:0 0 7px;color:var(--hm-accent);font-size:.63rem;font-weight:700;letter-spacing:.15em;text-transform:uppercase}.login-title{margin:0;color:#f2f3f6;font-size:1.65rem;line-height:1.2;font-weight:650;letter-spacing:-.035em}.login-subtitle{margin:7px 0 25px;color:var(--hm-text-muted);font-size:.8rem}.modal-overlay{background:#030508c2;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}.modal-content,.palette{background:linear-gradient(155deg,#171c25,#0f131a);border-color:#303847;box-shadow:var(--hm-shadow-lg)}.confirm-heading{display:flex;gap:13px;margin-bottom:22px}.confirm-heading h3{margin:2px 0 6px;font-size:.92rem;font-weight:650}.confirm-heading p{margin:0;color:var(--hm-text-muted);font-size:.76rem;line-height:1.55}.confirm-icon{width:38px;height:38px;display:grid;place-items:center;flex:none;color:var(--hm-info);background:#668fd71a;border:1px solid rgba(102,143,215,.22);border-radius:9px}.confirm-icon.danger{color:var(--hm-danger);background:#e064641a;border-color:#e0646438}.toast-item{min-width:260px;padding:11px 13px;background:#141923;border-color:#2b3442;border-left-width:3px;color:var(--hm-text)}.toast-success{border-left-color:var(--hm-success)}.toast-error{border-left-color:var(--hm-danger)}.toast-info{border-left-color:var(--hm-info)}.toast-success,.toast-error,.toast-info{background:#141923;color:var(--hm-text)}.toast-success .toast-icon{color:var(--hm-success)}.toast-error .toast-icon{color:var(--hm-danger)}.toast-info .toast-icon{color:var(--hm-info)}.palette-overlay{padding-top:12vh}.palette{max-width:570px}.palette-search{display:flex;align-items:center;gap:10px;padding-left:17px;color:var(--hm-text-dim);border-bottom:1px solid var(--hm-border)}.palette-input{border:0;padding:17px 17px 17px 0;font-size:.82rem}.palette-results{max-height:390px;padding:8px}.palette-item{min-height:48px;gap:11px;padding:6px 10px}.palette-item.selected{background:linear-gradient(90deg,#c58b321f,#c58b3209)}.palette-icon{width:30px;height:30px;display:grid;place-items:center;color:#8994a4;background:#ffffff09;border:1px solid var(--hm-border-subtle);border-radius:7px}.palette-item.selected .palette-icon{color:var(--hm-accent-hover);border-color:#c58b3240}.palette-copy{display:flex;flex-direction:column;gap:2px}.palette-label{color:#dfe3e9;font-size:.76rem;font-weight:550}.palette-group{color:var(--hm-text-dim);font-size:.61rem}.palette-arrow{margin-left:auto;color:#4e5867}.palette-footer{justify-content:flex-end;gap:16px;padding:10px 14px}.palette-footer span{display:flex;align-items:center;gap:5px}.error-icon,.empty-state-icon{display:grid;place-items:center;color:var(--hm-text-dim)}.empty-state{min-height:220px;border-style:dashed}.empty-state-icon{width:44px;height:44px;font-size:initial;background:#ffffff06;border:1px solid var(--hm-border-subtle);border-radius:10px}@media(max-width:900px){.hm-sidebar{position:fixed;left:0;top:0;transform:translate(-102%);width:min(280px,86vw);min-width:min(280px,86vw);box-shadow:20px 0 60px #00000080}.hm-sidebar.mobile-open{transform:translate(0)}.hm-sidebar.collapsed{width:min(280px,86vw);min-width:min(280px,86vw)}.hm-sidebar.collapsed .sidebar-brand-copy,.hm-sidebar.collapsed .nav-label,.hm-sidebar.collapsed .nav-section-label,.hm-sidebar.collapsed .connection-copy,.hm-sidebar.collapsed .shortcut-hint span,.hm-sidebar.collapsed .shortcut-hint kbd{display:flex}.hm-sidebar.collapsed .sidebar-brand{justify-content:flex-start}.hm-sidebar.collapsed .brand-mark{display:grid}.hm-sidebar.collapsed .nav-item{padding:0 10px;justify-content:flex-start}.mobile-scrim{display:block;position:fixed;top:0;right:0;bottom:0;left:0;z-index:35;background:#0000009e;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.icon-btn.mobile-menu-btn{display:inline-grid}.hm-sidebar .sidebar-toggle-btn{display:none}.hm-topbar{padding:0 16px;gap:12px}.topbar-description,.uptime-label,.command-trigger span{display:none}.command-trigger{width:34px;padding:0;justify-content:center}.command-trigger kbd{display:none}}@media(max-width:640px){:root{--hm-topbar-h: 64px}.section-tabs-wrap{padding:0 12px}.topbar-kicker{display:none}.topbar-title-row h1{font-size:.9rem}.status-pill{display:none}.section-tab{padding-inline:10px}.section-panel .p-6{padding:1rem}.login-panel{padding:26px}.toast-stack{left:12px;right:12px;bottom:12px}.toast-item{width:100%;max-width:none}}@media(prefers-reduced-motion:reduce){.brand-loader{animation:none}}.page-viewport .page-fade-in{width:100%;max-width:1600px;margin-inline:0}.page-viewport .p-6{padding:clamp(1rem,2vw,1.75rem)}.page-viewport h1.text-xl{color:#f2f3f6;font-size:1.12rem;line-height:1.3;letter-spacing:-.025em;font-weight:650}.page-viewport h2,.page-viewport h3{letter-spacing:-.012em}.page-lede{margin-top:4px;max-width:64ch;color:var(--hm-text-dim);font-size:.72rem;line-height:1.55}.form-panel{position:relative;overflow:hidden;border-color:#c58b3238;background:linear-gradient(145deg,#c58b320b,#0d1118fa 35%)}.form-panel:before{content:"";position:absolute;inset:0 auto 0 0;width:2px;background:linear-gradient(180deg,var(--hm-accent),transparent 75%)}.provider-choice-list{display:grid;gap:8px}.provider-choice{padding:11px 12px;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);background:#0000001f}.provider-choice:has(input:checked){border-color:#c58b324d;background:var(--hm-accent-dim)}.provider-choice-label{display:flex;align-items:center;gap:9px;cursor:pointer}.provider-control{accent-color:var(--hm-accent)}.hm-card{padding:1rem;border-radius:var(--hm-radius-lg)}.hm-card>h2:first-child,.hm-card>h3:first-child{color:#e5e8ee}.table-responsive{border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-lg);background:#06080c3d}.hm-table{width:100%;border-collapse:collapse}.hm-table th{height:38px;padding:0 12px;white-space:nowrap}.hm-table td{padding:10px 12px;vertical-align:middle}.hm-table tbody tr:last-child td{border-bottom:0}.hm-select,select.hm-input{min-height:38px;background-color:#090c11;border-color:#2a3240}textarea.hm-input{min-height:90px;line-height:1.55}label{accent-color:var(--hm-accent)}input[type=checkbox]{width:15px;height:15px;accent-color:var(--hm-accent);border-radius:4px;cursor:pointer}input[type=checkbox]:focus-visible{outline:2px solid var(--hm-accent);outline-offset:3px}input[type=checkbox]:disabled{cursor:not-allowed;opacity:.45}.btn-danger{color:#f6b2b2;background:#e0646414;border-color:#e0646452}.btn-danger:hover{color:#ffd0d0;background:#e0646429;border-color:#e0646480}.btn:focus-visible,.icon-btn:focus-visible,.nav-item:focus-visible,.section-tab:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}.modal-content{width:min(92vw,520px);padding:22px;border-radius:var(--hm-radius-xl)}.modal-content h2,.modal-content h3{color:#f0f2f5}.modal-content .btn{min-width:76px}.error-state{min-height:180px;border-color:#e0646442!important;background:linear-gradient(150deg,#34141852,#0e1219fa)}.error-icon{width:42px;height:42px;border-radius:10px;background:#e0646417;border:1px solid rgba(224,100,100,.2)}.sess-preset-icon,.tl-group-icon,.cfg-group-icon,.chat-tool-icon{display:inline-grid;place-items:center;flex:none}.sk-action-btn{display:inline-grid;place-items:center}.sk-card-icon{display:inline-grid;color:var(--hm-accent-hover)}.health-card-icon{display:inline-grid}.provider-status{display:inline-flex;align-items:center;gap:6px}.provider-status .status-dot{width:7px;height:7px;box-shadow:none}.row-expander{width:28px;height:28px;display:inline-grid;place-items:center;border:0;border-radius:6px;color:var(--hm-text-dim);background:transparent;cursor:pointer}.row-expander:hover{color:var(--hm-text);background:#ffffff0b}.row-expander:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}.tool-expand-icon .odin-icon,.chat-tools-toggle-icon .odin-icon,.cfg-group-arrow .odin-icon,.sess-expand-icon .odin-icon,.mem-tree-arrow .odin-icon,.kb-tree-arrow .odin-icon{transition:transform var(--hm-transition-base)}.chat-markdown .chat-code-copy{opacity:0}.chat-markdown pre:hover .chat-code-copy,.chat-markdown .chat-code-copy:focus-visible{opacity:1}.fts-highlight,.knowledge-highlight{background:#e0b7663d;color:#fff0c8;border-radius:2px;padding-inline:1px}@media(max-width:640px){.page-viewport .p-6,.hm-card{padding:.85rem}.hm-table th,.hm-table td{padding-inline:9px}.modal-content{padding:18px}} +@font-face{font-family:Fira Code;font-style:normal;font-weight:400;font-display:swap;src:url(/ui/assets/fira-code-400-CHoedHDv.woff2) format("woff2")}@font-face{font-family:Fira Code;font-style:normal;font-weight:500;font-display:swap;src:url(/ui/assets/fira-code-400-CHoedHDv.woff2) format("woff2")}@font-face{font-family:Fira Code;font-style:normal;font-weight:600;font-display:swap;src:url(/ui/assets/fira-code-400-CHoedHDv.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:400;font-display:swap;src:url(/ui/assets/inter-400-Dx4kXJAl.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:500;font-display:swap;src:url(/ui/assets/inter-400-Dx4kXJAl.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:600;font-display:swap;src:url(/ui/assets/inter-400-Dx4kXJAl.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:700;font-display:swap;src:url(/ui/assets/inter-400-Dx4kXJAl.woff2) format("woff2")}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.m-2{margin:.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.max-h-24{max-height:6rem}.max-h-32{max-height:8rem}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-64{max-height:16rem}.max-h-96{max-height:24rem}.min-h-screen{min-height:100vh}.w-2{width:.5rem}.w-32{width:8rem}.w-5{width:1.25rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[100px\]{min-width:100px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.self-end{align-self:flex-end}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-amber-900{--tw-border-opacity: 1;border-color:rgb(120 53 15 / var(--tw-border-opacity, 1))}.border-blue-500\/30{border-color:#3b82f64d}.border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-700\/50{border-color:#37415180}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.border-gray-800\/50{border-color:#1f293780}.border-green-500\/30{border-color:#22c55e4d}.border-green-500\/40{border-color:#22c55e66}.border-green-800{--tw-border-opacity: 1;border-color:rgb(22 101 52 / var(--tw-border-opacity, 1))}.border-indigo-900\/30{border-color:#312e814d}.border-red-500\/30{border-color:#ef44444d}.border-red-500\/40{border-color:#ef444466}.border-red-700{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity, 1))}.border-red-800{--tw-border-opacity: 1;border-color:rgb(153 27 27 / var(--tw-border-opacity, 1))}.border-red-900{--tw-border-opacity: 1;border-color:rgb(127 29 29 / var(--tw-border-opacity, 1))}.border-red-900\/50{border-color:#7f1d1d80}.border-slate-700{--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.border-yellow-800{--tw-border-opacity: 1;border-color:rgb(133 77 14 / var(--tw-border-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-400{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity, 1))}.bg-blue-500\/60{background-color:#3b82f699}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-blue-900\/40{background-color:#1e3a8a66}.bg-blue-900\/50{background-color:#1e3a8a80}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/30{background-color:#1118274d}.bg-gray-900\/50{background-color:#11182780}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-green-400{--tw-bg-opacity: 1;background-color:rgb(74 222 128 / var(--tw-bg-opacity, 1))}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-green-900\/30{background-color:#14532d4d}.bg-green-950\/30{background-color:#052e164d}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity, 1))}.bg-indigo-900{--tw-bg-opacity: 1;background-color:rgb(49 46 129 / var(--tw-bg-opacity, 1))}.bg-indigo-950\/30{background-color:#1e1b4b4d}.bg-red-900\/20{background-color:#7f1d1d33}.bg-red-900\/30{background-color:#7f1d1d4d}.bg-red-900\/50{background-color:#7f1d1d80}.bg-red-950\/30{background-color:#450a0a4d}.bg-yellow-900\/20{background-color:#713f1233}.\!p-0{padding:0!important}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pl-2{padding-left:.5rem}.pr-1{padding-right:.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-indigo-300{--tw-text-opacity: 1;color:rgb(165 180 252 / var(--tw-text-opacity, 1))}.text-indigo-400{--tw-text-opacity: 1;color:rgb(129 140 248 / var(--tw-text-opacity, 1))}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-teal-400{--tw-text-opacity: 1;color:rgb(45 212 191 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.last\:mb-0:last-child{margin-bottom:0}.last\:border-0:last-child{border-width:0px}.hover\:border-gray-600:hover{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:text-blue-300:hover{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.hover\:text-green-300:hover{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.hover\:text-indigo-300:hover{--tw-text-opacity: 1;color:rgb(165 180 252 / var(--tw-text-opacity, 1))}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-yellow-300:hover{--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity, 1))}@media(min-width:360px){.min-\[360px\]\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}html{-webkit-text-size-adjust:100%}*{-webkit-tap-highlight-color:rgba(217,119,6,.15)}:root{--hm-font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--hm-font-mono: "Fira Code", "Cascadia Code", "JetBrains Mono", ui-monospace, monospace;--hm-text-xs: .6875rem;--hm-text-sm: .75rem;--hm-text-base: .8125rem;--hm-text-md: .875rem;--hm-text-lg: 1rem;--hm-text-xl: 1.25rem;--hm-text-2xl: 1.5rem;--hm-leading-tight: 1.25;--hm-leading-normal: 1.5;--hm-leading-relaxed: 1.65;--hm-space-1: .25rem;--hm-space-2: .375rem;--hm-space-3: .5rem;--hm-space-4: .75rem;--hm-space-5: 1rem;--hm-space-6: 1.5rem;--hm-space-8: 2rem;--hm-radius-full: 9999px;--hm-shadow-glow: 0 0 20px rgba(197, 139, 50, .15), 0 0 4px rgba(197, 139, 50, .1);--hm-shadow-glow-sm: 0 0 8px rgba(197, 139, 50, .1);--hm-transition-fast: .1s ease;--hm-transition-base: .15s ease;--hm-transition-slow: .25s ease;--hm-transition-spring: .3s cubic-bezier(.34, 1.56, .64, 1)}body{font-family:var(--hm-font-sans);font-size:var(--hm-text-md);line-height:var(--hm-leading-normal);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;letter-spacing:-.011em}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--hm-bg)}::-webkit-scrollbar-thumb{background:#374151;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#4b5563}*{scrollbar-width:thin;scrollbar-color:#374151 var(--hm-bg)}.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0}.status-dot.online{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success)}.status-dot.offline{background:var(--hm-danger);box-shadow:0 0 6px var(--hm-danger)}.status-dot.starting{background:var(--hm-warning);box-shadow:0 0 6px var(--hm-warning)}.hm-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-5);box-shadow:var(--hm-shadow-sm);transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.hm-table{width:100%;border-collapse:collapse;font-size:var(--hm-text-md)}.hm-table th{text-align:left;padding:var(--hm-space-3) var(--hm-space-4);color:var(--hm-text-muted);font-weight:500;font-size:var(--hm-text-sm);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--hm-border);white-space:nowrap}.hm-table td{padding:var(--hm-space-3) var(--hm-space-4);border-bottom:1px solid rgba(31,41,55,.5)}.hm-table tr:hover td{background:#1f29374d}.btn{display:inline-flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-2) var(--hm-space-4);border-radius:var(--hm-radius-md);font-size:var(--hm-text-base);font-weight:500;font-family:var(--hm-font-sans);cursor:pointer;border:none;transition:background var(--hm-transition-base),box-shadow var(--hm-transition-base),transform var(--hm-transition-fast);letter-spacing:-.006em}.btn:active:not(:disabled){transform:scale(.97)}.btn-primary{background:var(--hm-accent);color:#fff}.btn-primary:hover{background:var(--hm-accent-hover);box-shadow:var(--hm-shadow-glow-sm)}.btn-danger{background:#ef444426;color:#f87171}.btn-danger:hover{background:#ef444440}.btn-ghost{background:transparent;color:var(--hm-text-muted)}.btn-ghost:hover{background:var(--hm-surface-hover);color:var(--hm-text)}.btn:disabled{opacity:.5;cursor:not-allowed}.hm-input{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4);color:var(--hm-text);font-size:var(--hm-text-md);font-family:var(--hm-font-sans);width:100%;outline:none;transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.hm-input:focus{border-color:var(--hm-accent);box-shadow:0 0 0 3px var(--hm-accent-glow)}textarea.hm-input{font-family:var(--hm-font-mono);resize:vertical;min-height:120px}.toggle-switch{position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;top:0;right:0;bottom:0;left:0;background:#374151;border-radius:10px;cursor:pointer;transition:background .2s}.toggle-slider:before{content:"";position:absolute;width:14px;height:14px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:transform .2s}.toggle-switch input:checked+.toggle-slider{background:var(--hm-accent);box-shadow:var(--hm-shadow-glow-sm)}.toggle-switch input:checked+.toggle-slider:before{transform:translate(16px)}.toggle-switch input:disabled+.toggle-slider{opacity:.4;cursor:not-allowed}.field-changed td:first-child{border-left:2px solid var(--hm-accent)}.config-tag{display:inline-flex;align-items:center;gap:var(--hm-space-1);padding:.125rem .5rem;background:var(--hm-gold-dim);border:1px solid rgba(217,119,6,.2);border-radius:var(--hm-radius-sm);font-size:var(--hm-text-sm);font-family:var(--hm-font-mono);color:var(--hm-text)}.config-tag button{background:none;border:none;color:var(--hm-text-muted);cursor:pointer;padding:0;font-size:var(--hm-text-md);line-height:1}.config-tag button:hover{color:var(--hm-danger)}.hm-select{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-2) var(--hm-space-3);color:var(--hm-text);font-size:var(--hm-text-base);font-family:var(--hm-font-sans);outline:none;cursor:pointer;transition:border-color var(--hm-transition-base)}.hm-select:focus{border-color:var(--hm-accent)}.badge{display:inline-block;padding:.125rem .5rem;border-radius:var(--hm-radius-full);font-size:var(--hm-text-sm);font-weight:500;letter-spacing:.01em}.badge-success{background:#22c55e26;color:#4ade80}.badge-warning{background:#eab30826;color:#facc15}.badge-danger{background:#ef444426;color:#f87171}.badge-info{background:var(--hm-accent-dim);color:var(--hm-gold)}.spinner{width:20px;height:20px;border:2px solid var(--hm-border);border-top-color:var(--hm-accent);border-radius:50%;animation:spin .6s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#0009;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:50}.modal-content{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-xl);padding:var(--hm-space-6);max-width:600px;width:90%;max-height:80vh;overflow-y:auto;box-shadow:var(--hm-shadow-lg)}.skeleton{background:linear-gradient(90deg,var(--hm-surface) 25%,var(--hm-surface-hover) 50%,var(--hm-surface) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:var(--hm-radius-sm)}.skeleton-text{height:.875rem;margin-bottom:.5rem}.skeleton-stat{height:2rem;width:3rem;margin:0 auto .25rem}.skeleton-row{height:2.25rem;margin-bottom:.375rem}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.flash-new{animation:flash-highlight 1.5s ease-out}@keyframes flash-highlight{0%{background-color:#d9770640}to{background-color:transparent}}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.error-state{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-4);padding:var(--hm-space-6);text-align:center}.error-state .error-icon{font-size:1.5rem;opacity:.6}.chat-container{display:flex;flex-direction:column;height:calc(100vh - var(--hm-topbar-h));overflow:hidden}.page-viewport .chat-container.page-fade-in{max-width:none;margin-inline:0;--chat-work-lane: 1400px;--chat-reading-lane: 1080px}.chat-messages{flex:1;overflow-y:auto;padding:var(--hm-space-5) var(--hm-space-5) var(--hm-space-3);scroll-behavior:smooth}.chat-messages>.chat-message,.chat-messages>.chat-date-sep{max-width:var(--chat-reading-lane);margin-inline:auto}.chat-messages>.chat-message:has(pre) .chat-bubble-wrap,.chat-messages>.chat-message:has(table) .chat-bubble-wrap{max-width:100%;width:100%}.chat-empty{display:flex;align-items:center;justify-content:center;height:100%}.chat-welcome{text-align:center;max-width:28rem}.chat-welcome-icon{color:var(--hm-accent);margin-bottom:var(--hm-space-4);opacity:.8}.chat-welcome-title{font-size:1.25rem;font-weight:700;color:var(--hm-text);margin-bottom:var(--hm-space-2);letter-spacing:.01em}.chat-welcome-subtitle{font-size:var(--hm-text-sm);color:var(--hm-text-muted);margin-bottom:var(--hm-space-5)}.chat-suggestions{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--hm-space-2)}.chat-suggestion{background:var(--hm-surface-elevated);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-2) var(--hm-space-4);color:var(--hm-text-muted);font-size:var(--hm-text-sm);cursor:pointer;transition:border-color var(--hm-transition-base),color var(--hm-transition-base)}.chat-suggestion:hover{border-color:var(--hm-accent);color:var(--hm-accent-hover)}.chat-date-sep{display:flex;align-items:center;gap:var(--hm-space-4);margin:var(--hm-space-4) 0}.chat-date-sep:before,.chat-date-sep:after{content:"";flex:1;height:1px;background:var(--hm-border)}.chat-date-sep span{font-size:var(--hm-text-xs);color:var(--hm-text-muted);white-space:nowrap}.chat-message{display:flex;gap:var(--hm-space-3);margin-bottom:var(--hm-space-4);align-items:flex-start}.chat-message.chat-user{flex-direction:row-reverse}.chat-message.chat-bot{flex-direction:row}.chat-avatar{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}.chat-avatar-bot{background:linear-gradient(135deg,#d9770633,#d977061a);border:1px solid rgba(217,119,6,.3);color:var(--hm-accent)}.chat-avatar-user{background:var(--hm-surface-hover);border:1px solid var(--hm-border);color:var(--hm-text-muted)}.chat-avatar-eye,.chat-avatar-user{display:flex;align-items:center;justify-content:center}.chat-avatar-pulse{animation:avatar-pulse 2s ease-in-out infinite}@keyframes avatar-pulse{0%,to{opacity:1}50%{opacity:.5}}.chat-bubble-wrap{max-width:75%;min-width:0}.chat-message.chat-user .chat-bubble-wrap{align-items:flex-end;display:flex;flex-direction:column}.chat-bubble{border-radius:var(--hm-radius-xl);padding:.625rem .875rem;font-size:var(--hm-text-md);line-height:var(--hm-leading-normal);word-break:break-word}.chat-bubble-user{background:linear-gradient(135deg,var(--hm-accent),#b45309);color:#fff;border-bottom-right-radius:var(--hm-radius-sm);box-shadow:var(--hm-shadow-sm)}.chat-bubble-bot{background:var(--hm-surface-elevated);border:1px solid var(--hm-border);border-bottom-left-radius:var(--hm-radius-sm)}.chat-bubble-typing{display:flex;align-items:center;gap:var(--hm-space-3)}.chat-bubble-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:var(--hm-space-1)}.chat-bubble-label{font-size:var(--hm-text-xs);color:var(--hm-accent-hover);font-weight:600;letter-spacing:.02em}.chat-error-indicator{font-size:.625rem;color:#f87171;background:#ef444426;border:1px solid rgba(239,68,68,.3);border-radius:var(--hm-radius-sm);padding:0 .375rem;line-height:1.4;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.chat-bubble-text{white-space:pre-wrap}.chat-timestamp{font-size:.625rem;color:var(--hm-text-muted);margin-top:2px;opacity:0;transition:opacity var(--hm-transition-base);padding:0 .25rem}.chat-message:hover .chat-timestamp{opacity:1}.chat-markdown{white-space:normal}.chat-markdown p{margin:0 0 .5rem}.chat-markdown p:last-child{margin-bottom:0}.chat-markdown code{background:#0000004d;padding:.125rem .375rem;border-radius:var(--hm-radius-sm);font-size:var(--hm-text-base);font-family:var(--hm-font-mono)}.chat-markdown pre{background:#0000004d;border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4);margin:var(--hm-space-2) 0;overflow-x:auto;position:relative}.chat-markdown pre code{background:none;padding:0}.chat-markdown ul,.chat-markdown ol{margin:.25rem 0;padding-left:1.25rem}.chat-markdown blockquote{border-left:3px solid var(--hm-accent);padding-left:var(--hm-space-4);margin:var(--hm-space-2) 0;color:var(--hm-text-muted)}.chat-markdown a{color:var(--hm-accent-hover);text-decoration:underline}.chat-markdown table{border-collapse:collapse;margin:var(--hm-space-2) 0;font-size:var(--hm-text-sm);display:block;width:-moz-max-content;width:max-content;max-width:100%;overflow-x:auto}.chat-markdown th,.chat-markdown td{border:1px solid var(--hm-border);padding:var(--hm-space-2) var(--hm-space-3);text-align:left;white-space:nowrap;word-break:normal}.chat-markdown th{background:#0003;font-weight:600}.chat-code-copy{position:absolute;top:var(--hm-space-2);right:var(--hm-space-2);background:var(--hm-surface-hover);border:1px solid var(--hm-border);border-radius:var(--hm-radius-sm);color:var(--hm-text-muted);font-size:.625rem;padding:2px 8px;cursor:pointer;opacity:0;transition:opacity var(--hm-transition-base),color var(--hm-transition-base);font-family:var(--hm-font-sans);text-transform:uppercase;letter-spacing:.04em}.chat-code-copy:hover{color:var(--hm-accent-hover)}.chat-tool-cards{margin-bottom:var(--hm-space-2)}.chat-tools-toggle{background:none;border:none;color:var(--hm-text-muted);font-size:var(--hm-text-sm);cursor:pointer;display:flex;align-items:center;gap:var(--hm-space-1);padding:.125rem 0}.chat-tools-toggle:hover{color:var(--hm-text)}.chat-tools-toggle-icon{font-size:.5rem}.chat-tools-toggle-count{background:#d9770626;color:var(--hm-accent-hover);border-radius:var(--hm-radius-sm);padding:0 .375rem;font-weight:600;font-size:var(--hm-text-xs)}.chat-tool-list{display:flex;flex-wrap:wrap;gap:var(--hm-space-1);margin-top:var(--hm-space-2)}.chat-tool-card{display:flex;align-items:center;gap:var(--hm-space-2);background:#0003;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-1) var(--hm-space-3);font-size:var(--hm-text-xs)}.chat-tool-icon{font-size:var(--hm-text-sm)}.chat-tool-name{font-family:var(--hm-font-mono);color:var(--hm-text-muted)}.chat-images{display:flex;flex-wrap:wrap;gap:var(--hm-space-2);margin-top:var(--hm-space-2)}.chat-image-thumb{border-radius:var(--hm-radius-md);overflow:hidden;border:1px solid var(--hm-border);max-width:240px;cursor:pointer;transition:border-color var(--hm-transition-base)}.chat-image-thumb:hover{border-color:var(--hm-accent)}.chat-image-thumb img{display:block;width:100%;height:auto;max-height:200px;-o-object-fit:cover;object-fit:cover}.chat-typing{display:flex;gap:var(--hm-space-1);padding:var(--hm-space-1) 0}.chat-typing span{width:6px;height:6px;background:var(--hm-accent);border-radius:50%;animation:typing-dot 1.4s infinite ease-in-out both}.chat-typing span:nth-child(1){animation-delay:0s}.chat-typing span:nth-child(2){animation-delay:.2s}.chat-typing span:nth-child(3){animation-delay:.4s}@keyframes typing-dot{0%,80%,to{transform:scale(.6);opacity:.4}40%{transform:scale(1);opacity:1}}.chat-typing-text{font-size:var(--hm-text-xs);color:var(--hm-text-muted);margin-left:var(--hm-space-2);font-style:italic}.chat-input-area{border-top:1px solid var(--hm-border);background:var(--hm-surface);padding:var(--hm-space-4) var(--hm-space-5);min-height:var(--hm-rail-h);display:flex;flex-direction:column;justify-content:center}.chat-input-row,.chat-input-hint{width:100%;max-width:var(--chat-work-lane);margin-inline:auto}.chat-input-row{display:flex;gap:var(--hm-space-3);align-items:flex-end}.chat-input{flex:1;background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-3) var(--hm-space-4);color:var(--hm-text);font-size:var(--hm-text-md);resize:none;outline:none;line-height:var(--hm-leading-normal);max-height:120px;font-family:inherit;transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.chat-input:focus{border-color:var(--hm-accent);box-shadow:0 0 0 3px var(--hm-accent-glow)}.chat-input:disabled{opacity:.5}.chat-send-btn{height:36px;min-width:44px;flex-shrink:0;display:flex;align-items:center;justify-content:center}.chat-send-icon{pointer-events:none}.chat-input-hint{display:flex;justify-content:space-between;align-items:center;padding-top:var(--hm-space-1)}.chat-connection-status{display:flex;align-items:center;gap:var(--hm-space-1);font-size:var(--hm-text-xs)}.chat-status-dot{width:6px;height:6px;border-radius:50%}.chat-ws-on .chat-status-dot{background:#4ade80}.chat-ws-off .chat-status-dot{background:#f87171}.chat-ws-on{color:var(--hm-text-muted)}.chat-ws-off{color:#f87171}.session-card{transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.session-card:hover{border-color:var(--hm-accent);box-shadow:var(--hm-shadow-glow-sm)}.session-selected{border-color:var(--hm-accent)!important;background:#d977060d}.session-checkbox{width:14px;height:14px;accent-color:var(--hm-accent);cursor:pointer;flex-shrink:0}.session-preview-role{font-weight:600;font-family:var(--hm-font-mono);flex-shrink:0;width:36px}.session-msg-content{font-size:var(--hm-text-base)}.sess-source-icon{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:var(--hm-text-sm)}.sess-source-discord{background:#5865f226;border:1px solid rgba(88,101,242,.3)}.sess-source-web{background:var(--hm-accent-dim);border:1px solid rgba(217,119,6,.3)}.sess-expand-icon{font-size:.5rem;color:var(--hm-text-dim);transition:transform var(--hm-transition-base);display:inline-block;margin-right:var(--hm-space-2)}.sess-summary-banner{padding:var(--hm-space-3) var(--hm-space-4);background:linear-gradient(135deg,#eab30814,#eab30808);border:1px solid rgba(234,179,8,.2);border-radius:var(--hm-radius-md)}.sess-summary-label{font-size:var(--hm-text-xs);font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--hm-warning)}.sess-view-btn{background:#ffffff0d;border:1px solid transparent;border-radius:var(--hm-radius-md);padding:var(--hm-space-1) var(--hm-space-4);font-size:var(--hm-text-xs);color:var(--hm-text-muted);cursor:pointer;transition:all var(--hm-transition-base);font-family:var(--hm-font-sans)}.sess-view-btn:hover{background:#ffffff1a}.sess-view-active{background:var(--hm-accent-dim)!important;color:var(--hm-gold);border-color:#d9770666}.sess-thread-container{max-height:32rem;overflow-y:auto;scrollbar-gutter:stable;display:flex;flex-direction:column;gap:var(--hm-space-3)}.sess-thread{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);overflow:hidden;transition:border-color var(--hm-transition-base)}.sess-thread:hover{border-color:#ffffff1a}.sess-thread-header{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-3) var(--hm-space-4);cursor:pointer;transition:background var(--hm-transition-base)}.sess-thread-header:hover{background:#ffffff08}.sess-thread-num{width:22px;height:22px;border-radius:50%;background:var(--hm-accent-dim);color:var(--hm-accent-hover);display:flex;align-items:center;justify-content:center;font-size:var(--hm-text-xs);font-weight:700;flex-shrink:0}.sess-thread-arrow{font-size:.5rem;color:var(--hm-text-dim);transition:transform var(--hm-transition-base);display:inline-block}.sess-thread-arrow-open{transform:rotate(90deg)}.sess-thread-summary{flex:1;min-width:0;font-size:var(--hm-text-sm);color:var(--hm-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sess-thread-count{flex-shrink:0}.sess-thread-messages{border-top:1px solid var(--hm-border);padding:var(--hm-space-3) var(--hm-space-4);display:flex;flex-direction:column;gap:var(--hm-space-3)}.sess-thread-msg{padding:8px 12px;border-radius:6px;margin-bottom:8px}.sess-msg-user{background:#06b6d414;border-left:3px solid rgba(6,182,212,.5)}.sess-msg-assistant{background:#6366f114;border-left:3px solid rgba(99,102,241,.5)}.sess-msg-system{background:#6b728014;border-left:3px solid rgba(107,114,128,.4)}.sess-msg-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:var(--hm-space-2)}.sess-role-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.sess-dot-user{background:#06b6d4;box-shadow:0 0 4px #06b6d480}.sess-dot-assistant{background:#6366f1;box-shadow:0 0 4px #6366f180}.sess-dot-system{background:#6b7280}.sess-role-label{font-size:var(--hm-text-xs);font-weight:600}.sess-msg-content{font-size:13px;color:var(--hm-text);line-height:1.5;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word;max-height:200px;overflow-y:auto}.sess-filter-bar,.logs-filter-bar{border-bottom:1px solid var(--hm-border);padding-bottom:var(--hm-space-3)}.sess-preset-chip{display:inline-flex;align-items:center;gap:var(--hm-space-1);padding:var(--hm-space-1) var(--hm-space-4);border-radius:var(--hm-radius-full);font-size:var(--hm-text-xs);font-weight:500;cursor:pointer;border:1px solid transparent;transition:all var(--hm-transition-base);background:#ffffff0d;color:var(--hm-text-muted);font-family:var(--hm-font-sans)}.sess-preset-chip:hover{background:#ffffff1a;color:var(--hm-text)}.sess-preset-active{background:var(--hm-accent-dim)!important;color:var(--hm-gold)!important;border-color:#d9770666}.sess-preset-icon{font-size:var(--hm-text-sm)}.sess-preset-custom{border-style:dashed;border-color:#ffffff1a}.sess-preset-remove{margin-left:var(--hm-space-1);font-size:var(--hm-text-md);color:var(--hm-text-dim);cursor:pointer;line-height:1}.sess-preset-remove:hover{color:var(--hm-danger)}.log-line{border-left:2px solid transparent;padding-left:var(--hm-space-3)}.log-line-error{color:#f87171;border-left-color:#ef4444;background:#ef44440d}.log-line-warning{color:#fbbf24;border-left-color:#eab308;background:#eab3080a}.log-ts{transition:color var(--hm-transition-base)}.log-chip{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:var(--hm-radius-full);font-size:var(--hm-text-xs);font-weight:600;cursor:pointer;border:1px solid transparent;transition:all var(--hm-transition-base);font-family:var(--hm-font-mono);background:#ffffff0d;color:var(--hm-text-muted)}.log-chip:hover{background:#ffffff1a}.log-chip-info.log-chip-active{background:#d9770633;color:var(--hm-gold);border-color:#d9770666}.log-chip-warning.log-chip-active{background:#eab30833;color:#facc15;border-color:#eab30866}.log-chip-error.log-chip-active{background:#ef444433;color:#f87171;border-color:#ef444466}.log-chip-clear{background:#ffffff0d;color:var(--hm-text-muted)}.log-chip-clear:hover{background:#ffffff1a;color:var(--hm-text)}.log-jump-btn{position:absolute;bottom:1rem;left:50%;transform:translate(-50%);background:var(--hm-accent);color:#fff;border:none;border-radius:var(--hm-radius-full);padding:var(--hm-space-2) var(--hm-space-5);font-size:var(--hm-text-sm);font-weight:500;cursor:pointer;z-index:10;box-shadow:0 2px 8px #0006;transition:background var(--hm-transition-base),transform var(--hm-transition-base);animation:jump-btn-in .2s ease-out}.log-jump-btn:hover{background:var(--hm-accent-hover)}@keyframes jump-btn-in{0%{opacity:0;transform:translate(-50%) translateY(.5rem)}to{opacity:1;transform:translate(-50%) translateY(0)}}.logs-tool-badge{display:inline-block;background:#d977061a;border:1px solid rgba(217,119,6,.2);border-radius:var(--hm-radius-sm);padding:0 var(--hm-space-2);font-size:var(--hm-text-xs);color:var(--hm-accent-hover);margin-right:var(--hm-space-2);font-family:var(--hm-font-mono)}.logs-timeline{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4)}.logs-timeline-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--hm-space-2)}.logs-timeline-chart{display:flex;gap:2px;align-items:flex-end;height:48px}.logs-timeline-bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;cursor:pointer;min-width:0}.logs-timeline-bar{width:100%;height:40px;display:flex;flex-direction:column-reverse;border-radius:2px 2px 0 0;overflow:hidden;background:#ffffff05;transition:background var(--hm-transition-base)}.logs-timeline-bar-wrap:hover .logs-timeline-bar{background:#ffffff0f}.logs-timeline-segment{width:100%;min-height:1px;transition:height var(--hm-transition-slow)}.logs-tl-error{background:#ef4444b3}.logs-tl-warning{background:#eab30899}.logs-tl-info{background:#3b82f666}.logs-timeline-label{font-size:.5rem;color:var(--hm-text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.stat-card{position:relative;overflow:hidden}.stat-card:before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--hm-accent),transparent);opacity:0;transition:opacity var(--hm-transition-base)}.stat-card:hover:before{opacity:1}.stat-icon{font-size:var(--hm-text-md);margin-bottom:var(--hm-space-1);opacity:.7}.dash-hero{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-5);position:relative}.dash-hero-left{display:flex;align-items:center;gap:var(--hm-space-4);flex:1;min-width:0}.dash-hero-ring{position:relative;width:48px;height:48px;flex-shrink:0}.dash-ring-svg{width:48px;height:48px;transform:rotate(-90deg)}.ring-online{color:var(--hm-success)}.ring-starting{color:var(--hm-warning)}.dash-ring-progress{transition:stroke-dashoffset 1s ease}.dash-hero-icon{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;font-size:1rem;color:var(--hm-text-muted)}.dash-hero-name{font-size:var(--hm-text-lg);font-weight:600;color:var(--hm-text);letter-spacing:-.02em}.dash-hero-sub{display:flex;align-items:center;gap:var(--hm-space-2);font-size:var(--hm-text-sm);color:var(--hm-text-muted)}.dash-hero-sep{opacity:.4}.dash-hero-actions{display:flex;gap:var(--hm-space-2);flex-shrink:0}.dash-hero-skeleton{display:flex;align-items:center;gap:var(--hm-space-4)}.dash-stat{display:flex;flex-direction:column;gap:var(--hm-space-1);padding:var(--hm-space-4) var(--hm-space-5)}.dash-stat-header{display:flex;align-items:center;gap:var(--hm-space-2)}.dash-stat-icon{font-size:var(--hm-text-md);opacity:.7}.dash-stat-label{font-size:var(--hm-text-xs);color:var(--hm-text-dim);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.dash-stat-value{font-size:var(--hm-text-2xl);font-weight:700;letter-spacing:-.02em;line-height:1}.dash-stat-sub{font-size:var(--hm-text-xs)}.dash-stat-highlight{border-color:#d9770640;background:linear-gradient(135deg,var(--hm-surface),rgba(217,119,6,.03))}.dash-health-bar{padding:var(--hm-space-4) var(--hm-space-5)}.dash-health-items{display:flex;flex-wrap:wrap;gap:var(--hm-space-4)}.dash-health-item{display:flex;align-items:center;gap:var(--hm-space-2);font-size:var(--hm-text-sm)}.dash-health-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.dash-health-ok{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success)}.dash-health-warn{background:var(--hm-warning);box-shadow:0 0 6px var(--hm-warning)}.dash-health-error{background:var(--hm-danger);box-shadow:0 0 6px var(--hm-danger)}.dash-health-label{color:var(--hm-text);font-weight:500}.dash-health-detail{color:var(--hm-text-muted)}.dash-panel{padding:var(--hm-space-4) var(--hm-space-5);min-height:120px}.dash-panel-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--hm-space-3)}.dash-panel-title{font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-text-muted);text-transform:uppercase;letter-spacing:.04em}.dash-empty{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-5) 0;color:var(--hm-text-dim);font-size:var(--hm-text-sm)}.dash-empty-icon{font-size:1.25rem;opacity:.3}.dash-agent-list{display:flex;flex-direction:column;gap:var(--hm-space-3)}.dash-agent-item{padding:var(--hm-space-3);background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.dash-agent-top{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:var(--hm-space-1)}.dash-agent-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.dash-agent-running{background:var(--hm-success);box-shadow:0 0 4px var(--hm-success);animation:loop-pulse 2s ease-in-out infinite}.dash-agent-completed{background:var(--hm-info)}.dash-agent-failed{background:var(--hm-danger)}.dash-agent-timeout{background:var(--hm-warning)}.dash-agent-killed{background:var(--hm-text-dim)}.dash-agent-label{font-weight:600;font-size:var(--hm-text-sm);color:var(--hm-text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dash-agent-iters{font-size:var(--hm-text-xs);color:var(--hm-text-dim);font-family:var(--hm-font-mono);flex-shrink:0}.dash-agent-goal{font-size:var(--hm-text-xs);color:var(--hm-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dash-agent-meta{display:flex;gap:var(--hm-space-3);font-size:var(--hm-text-xs);color:var(--hm-text-dim);margin-top:var(--hm-space-1)}.dash-agent-tools{font-family:var(--hm-font-mono)}.dash-activity-list{display:flex;flex-direction:column}.dash-activity-item{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-2) 0;border-bottom:1px solid rgba(31,41,55,.5);font-size:var(--hm-text-xs)}.dash-activity-item:last-child{border-bottom:none}.dash-activity-dot{width:5px;height:5px;border-radius:50%;flex-shrink:0}.dot-ok{background:var(--hm-success)}.dot-error{background:var(--hm-danger)}.dash-activity-tool{font-family:var(--hm-font-mono);color:var(--hm-text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dash-activity-time{color:var(--hm-text-dim);white-space:nowrap;flex-shrink:0}.dash-guild-item{display:flex;align-items:center;gap:var(--hm-space-2);font-size:var(--hm-text-sm)}.dash-guild-count{margin-left:auto;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.dash-error-list{display:flex;flex-direction:column;gap:var(--hm-space-2)}.dash-error-item{padding:var(--hm-space-2) 0;border-bottom:1px solid rgba(31,41,55,.5);font-size:var(--hm-text-xs)}.dash-error-item:last-child{border-bottom:none}.dash-error-top{display:flex;align-items:center;gap:var(--hm-space-2)}.dash-error-tool{font-family:var(--hm-font-mono);color:#f87171;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dash-error-time{color:var(--hm-text-dim);white-space:nowrap;flex-shrink:0}.dash-error-msg{color:var(--hm-text-dim);padding-left:1.25rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:90%}.tool-expand-icon{font-size:.625rem;display:inline-block;transition:transform var(--hm-transition-base)}.tool-detail-row td{padding:0!important}.tool-detail-cell{padding:var(--hm-space-4) var(--hm-space-5) var(--hm-space-4) 2rem!important;background:#11182780;border-left:2px solid var(--hm-accent)}.tl-stat-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-4) var(--hm-space-5);text-align:center;position:relative;overflow:hidden}.tl-stat-card:hover{border-color:var(--hm-accent-dim)}.tl-stat-value{font-size:var(--hm-text-2xl);font-weight:700;color:var(--hm-text);line-height:1.2}.tl-stat-label{font-size:var(--hm-text-xs);color:var(--hm-text-dim);margin-top:var(--hm-space-1)}.tl-stat-spark{display:flex;justify-content:center;margin-top:var(--hm-space-2)}.tl-view-toggle{display:inline-flex;background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);overflow:hidden}.tl-view-btn{padding:var(--hm-space-1) var(--hm-space-3);background:none;border:none;color:var(--hm-text-dim);cursor:pointer;font-size:var(--hm-text-sm);transition:all var(--hm-transition-base)}.tl-view-btn:hover{color:var(--hm-text);background:var(--hm-surface-hover)}.tl-view-active{color:var(--hm-accent)!important;background:var(--hm-accent-dim)!important}.tl-search{flex:1;min-width:200px}.tl-category-chips{display:flex;flex-wrap:wrap;gap:var(--hm-space-1)}.tl-category-chip{padding:var(--hm-space-1) var(--hm-space-3);border-radius:var(--hm-radius-full);border:1px solid var(--hm-border);background:var(--hm-surface);color:var(--hm-text-muted);font-size:var(--hm-text-xs);cursor:pointer;transition:all var(--hm-transition-base);white-space:nowrap}.tl-category-chip:hover{border-color:var(--hm-accent-dim);color:var(--hm-text)}.tl-category-active{background:var(--hm-accent-dim)!important;border-color:var(--hm-accent)!important;color:var(--hm-accent)!important}.tl-group-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:var(--hm-space-3);padding-bottom:var(--hm-space-2);border-bottom:1px solid var(--hm-border-subtle)}.tl-group-icon{font-size:var(--hm-text-md)}.tl-group-label{font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-text)}.tl-tool-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--hm-space-3)}.tl-tool-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-4);cursor:pointer;transition:all var(--hm-transition-base)}.tl-tool-card:hover{border-color:#d977064d;box-shadow:var(--hm-shadow-glow-sm)}.tl-tool-card-active{border-left:2px solid var(--hm-accent)}.tl-tool-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--hm-space-2)}.tl-tool-name{font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-text)}.tl-tool-desc{font-size:var(--hm-text-xs);color:var(--hm-text-muted);line-height:var(--hm-leading-normal);margin-bottom:var(--hm-space-3)}.tl-tool-footer{display:flex;align-items:center;justify-content:space-between}.tl-tool-usage{display:flex;align-items:baseline;gap:var(--hm-space-1)}.tl-tool-usage-count{font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-accent)}.tl-tool-usage-zero{font-size:var(--hm-text-sm);color:var(--hm-text-dim)}.tl-tool-usage-label{font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.tl-tool-spark{display:flex;align-items:center}.tl-sparkline{display:block}.tl-tool-detail{margin-top:var(--hm-space-3);padding-top:var(--hm-space-3);border-top:1px solid var(--hm-border-subtle)}.tl-tool-detail-desc{font-size:var(--hm-text-xs);color:var(--hm-text);line-height:var(--hm-leading-relaxed);white-space:pre-wrap;margin-bottom:var(--hm-space-3)}.tl-tool-params{margin-top:var(--hm-space-2)}.tl-tool-params-title{font-size:var(--hm-text-xs);font-weight:600;color:var(--hm-text-muted);margin-bottom:var(--hm-space-2)}.tl-tool-param{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-1) 0;font-size:var(--hm-text-xs)}.tl-tool-param-name{font-family:var(--hm-font-mono);color:var(--hm-text);font-weight:500}.tl-tool-param-type{color:var(--hm-info);font-family:var(--hm-font-mono)}.tl-tool-param-req{color:var(--hm-warning);font-size:.625rem;font-weight:600;text-transform:uppercase}.sk-stat-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-4) var(--hm-space-5);text-align:center}.sk-stat-card:hover{border-color:var(--hm-accent-dim)}.sk-stat-value{font-size:var(--hm-text-2xl);font-weight:700;color:var(--hm-text);line-height:1.2}.sk-stat-label{font-size:var(--hm-text-xs);color:var(--hm-text-dim);margin-top:var(--hm-space-1)}.sk-search{width:100%;max-width:400px}.sk-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,360px),1fr));gap:var(--hm-space-4)}.sk-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);overflow:hidden;transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.sk-card:hover{border-color:#d977064d;box-shadow:var(--hm-shadow-glow-sm)}.sk-card-tested{border-left:2px solid var(--hm-success)}.sk-card-header{display:flex;align-items:center;justify-content:space-between;padding:var(--hm-space-4) var(--hm-space-5);border-bottom:1px solid var(--hm-border-subtle);flex-wrap:wrap;gap:var(--hm-space-2)}.sk-card-title-row{display:flex;align-items:center;gap:var(--hm-space-2)}.sk-card-icon{font-size:var(--hm-text-md);opacity:.7}.sk-card-name{font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600;color:var(--hm-text)}.sk-card-runs{font-size:var(--hm-text-xs);color:var(--hm-text-dim);font-family:var(--hm-font-mono)}.sk-card-actions{display:flex;gap:var(--hm-space-1)}.sk-action-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:var(--hm-radius-md);border:1px solid var(--hm-border);background:var(--hm-surface);color:var(--hm-text-muted);cursor:pointer;font-size:var(--hm-text-xs);transition:all var(--hm-transition-base)}.sk-action-btn:hover{background:var(--hm-surface-hover);color:var(--hm-text)}.sk-action-test:hover{color:var(--hm-success);border-color:#22c55e4d}.sk-action-code:hover{color:var(--hm-info);border-color:#3b82f64d}.sk-action-edit:hover{color:var(--hm-accent);border-color:var(--hm-accent-dim)}.sk-action-delete:hover{color:var(--hm-danger);border-color:#ef44444d}.sk-card-body{padding:var(--hm-space-4) var(--hm-space-5)}.sk-card-desc{font-size:var(--hm-text-sm);color:var(--hm-text-muted);line-height:var(--hm-leading-normal);margin-bottom:var(--hm-space-2)}.sk-card-meta{display:flex;align-items:center;gap:var(--hm-space-4);font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.sk-card-date,.sk-card-lines{white-space:nowrap}.sk-test-result{margin:0 var(--hm-space-5) var(--hm-space-4);padding:var(--hm-space-3) var(--hm-space-4);border-radius:var(--hm-radius-md);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs)}.sk-test-pass{background:#22c55e14;border:1px solid rgba(34,197,94,.2)}.sk-test-fail{background:#ef444414;border:1px solid rgba(239,68,68,.2)}.sk-test-label{font-weight:600;font-family:var(--hm-font-sans);font-size:var(--hm-text-xs);margin-bottom:var(--hm-space-1)}.sk-test-pass .sk-test-label{color:var(--hm-success)}.sk-test-fail .sk-test-label{color:var(--hm-danger)}.sk-test-output{white-space:pre-wrap;color:var(--hm-text-muted);max-height:120px;overflow-y:auto}.sk-code-container{border-top:1px solid var(--hm-border-subtle)}.sk-code-header{display:flex;align-items:center;justify-content:space-between;padding:var(--hm-space-2) var(--hm-space-5);background:#03071280;border-bottom:1px solid var(--hm-border-subtle)}.sk-code-filename{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.sk-code-copy{background:none;border:none;color:var(--hm-text-dim);cursor:pointer;font-size:var(--hm-text-sm);padding:var(--hm-space-1);border-radius:var(--hm-radius-sm);transition:color var(--hm-transition-base)}.sk-code-copy:hover{color:var(--hm-text)}.sk-code-wrap{display:flex;max-height:400px;overflow:auto}.sk-line-numbers{padding:var(--hm-space-4) var(--hm-space-3);text-align:right;color:var(--hm-text-dim);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);line-height:var(--hm-leading-relaxed);background:#03071280;border-right:1px solid var(--hm-border-subtle);-webkit-user-select:none;-moz-user-select:none;user-select:none;min-width:2.5rem;margin:0;flex-shrink:0}.sk-code-block{flex:1;padding:var(--hm-space-4) var(--hm-space-5);margin:0;background:var(--hm-bg);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);line-height:var(--hm-leading-relaxed);color:var(--hm-text);overflow-x:auto}.sk-code-block code{background:none;padding:0;font-size:inherit}.sk-kw{color:#c084fc;font-weight:600}.sk-str{color:#86efac}.sk-cmt{color:var(--hm-text-dim);font-style:italic}.sk-dec{color:var(--hm-gold)}.sk-num{color:#67e8f9}.sk-builtin{color:#93c5fd}.sk-editor-panel{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-5) var(--hm-space-6);margin-top:var(--hm-space-4)}.sk-editor-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--hm-space-4)}.sk-editor-title{font-size:var(--hm-text-md);font-weight:600;color:var(--hm-text)}.sk-field-label{display:block;font-size:var(--hm-text-xs);color:var(--hm-text-muted);margin-bottom:var(--hm-space-1)}.sk-field-hint{font-size:var(--hm-text-xs);color:var(--hm-text-dim);margin-top:var(--hm-space-1)}.sk-editor-wrap{display:flex;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);overflow:hidden;background:var(--hm-bg)}.sk-editor-gutter{padding:var(--hm-space-4) var(--hm-space-3);text-align:right;color:var(--hm-text-dim);font-family:var(--hm-font-mono);font-size:var(--hm-text-base);line-height:var(--hm-leading-relaxed);background:#03071280;border-right:1px solid var(--hm-border-subtle);-webkit-user-select:none;-moz-user-select:none;user-select:none;min-width:2.5rem;white-space:pre;overflow:hidden;flex-shrink:0}.sk-editor-textarea{flex:1;padding:var(--hm-space-4) var(--hm-space-5);background:transparent;border:none;color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-text-base);line-height:var(--hm-leading-relaxed);resize:vertical;-moz-tab-size:4;-o-tab-size:4;tab-size:4;outline:none;min-height:300px;width:100%}.sk-editor-textarea::-moz-placeholder{color:var(--hm-text-dim)}.sk-editor-textarea::placeholder{color:var(--hm-text-dim)}.sk-editor-status{display:flex;gap:var(--hm-space-4);margin-top:var(--hm-space-2);font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.sk-editor-line-count,.sk-editor-char-count{font-family:var(--hm-font-mono)}.sk-validation-box{padding:var(--hm-space-3) var(--hm-space-4);border-radius:var(--hm-radius-md);font-size:var(--hm-text-xs);margin-bottom:var(--hm-space-3)}.sk-validation-ok{background:#22c55e14;border:1px solid rgba(34,197,94,.2);color:var(--hm-success)}.sk-validation-err{background:#eab30814;border:1px solid rgba(234,179,8,.2);color:var(--hm-warning)}.skill-code-block{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-4) var(--hm-space-5);font-size:var(--hm-text-base);font-family:var(--hm-font-mono);overflow-x:auto;line-height:var(--hm-leading-relaxed);max-height:400px;overflow-y:auto;margin:0;color:var(--hm-text)}.skill-card{transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.skill-card:hover{border-color:#d977064d;box-shadow:var(--hm-shadow-glow-sm)}.skill-editor{font-family:var(--hm-font-mono);font-size:var(--hm-text-base);line-height:var(--hm-leading-relaxed);-moz-tab-size:4;-o-tab-size:4;tab-size:4;min-height:300px}.cfg-group{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);overflow:hidden;box-shadow:var(--hm-shadow-sm)}.cfg-group-header{display:flex;align-items:center;gap:var(--hm-space-3);padding:var(--hm-space-4) var(--hm-space-5);background:linear-gradient(135deg,var(--hm-surface),var(--hm-surface-elevated));border-bottom:1px solid var(--hm-border);transition:background var(--hm-transition-base)}.cfg-group-header:hover{background:var(--hm-surface-hover)}.cfg-group-icon{font-size:var(--hm-text-lg);opacity:.8}.cfg-group-label{font-weight:600;font-size:var(--hm-text-md);color:var(--hm-text);flex:1}.cfg-group-arrow{font-size:var(--hm-text-xs);color:var(--hm-text-dim);font-family:var(--hm-font-mono)}.cfg-group-body{padding:var(--hm-space-3);display:flex;flex-direction:column;gap:var(--hm-space-3)}.cfg-section{background:var(--hm-bg);border:1px solid rgba(31,41,55,.5);border-radius:var(--hm-radius-md);overflow:hidden}.cfg-section-header{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-3) var(--hm-space-4);transition:background var(--hm-transition-base)}.cfg-section-header:hover{background:#1f29374d}.cfg-section-name{font-size:var(--hm-text-sm);font-weight:500;color:var(--hm-text)}.cfg-section-body{padding:0 var(--hm-space-3) var(--hm-space-3)}.cfg-field-error{font-size:var(--hm-text-xs);color:#f87171;margin-top:2px;font-family:var(--hm-font-sans);font-weight:400}.cfg-input-error{border-color:#ef444480!important;box-shadow:0 0 0 2px #ef444426!important}.cfg-change-count{font-size:var(--hm-text-xs);color:var(--hm-accent-hover);background:var(--hm-accent-dim);padding:2px 8px;border-radius:var(--hm-radius-full);font-weight:600}.cfg-diff-list{max-height:50vh;overflow-y:auto;display:flex;flex-direction:column;gap:var(--hm-space-3)}.cfg-diff-entry{border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);overflow:hidden}.cfg-diff-path{padding:var(--hm-space-2) var(--hm-space-3);background:var(--hm-surface-hover);border-bottom:1px solid var(--hm-border);color:var(--hm-accent-hover)}.cfg-diff-values{font-size:var(--hm-text-sm)}.cfg-diff-old{padding:var(--hm-space-2) var(--hm-space-3);background:#ef444414;border-bottom:1px solid rgba(31,41,55,.3);color:#f87171;white-space:pre-wrap;word-break:break-word}.cfg-diff-new{padding:var(--hm-space-2) var(--hm-space-3);background:#22c55e14;color:#4ade80;white-space:pre-wrap;word-break:break-word}.cfg-diff-label{display:inline-block;width:1.25rem;font-weight:700;font-family:var(--hm-font-mono)}.knowledge-highlight{background:#d977064d;color:#fde68a;padding:0 2px;border-radius:2px}.knowledge-preview{border-left:2px solid var(--hm-border);padding-left:var(--hm-space-4);max-height:4rem;overflow:hidden;font-style:italic}.kb-stats-bar{display:flex;gap:var(--hm-space-4);margin-bottom:var(--hm-space-4);flex-wrap:wrap}.kb-stat{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-3) var(--hm-space-4);text-align:center;min-width:80px}.kb-stat-value{display:block;font-size:1.25rem;font-weight:700;color:var(--hm-gold)}.kb-stat-label{display:block;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.kb-score-badge{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);color:var(--hm-text-dim);background:var(--hm-gold-dim);padding:1px 6px;border-radius:3px}.kb-search-result{border-left:3px solid var(--hm-accent-dim)}.kb-ingest-form{border-left:3px solid var(--hm-accent)}.kb-tree,.kb-tree-list{display:flex;flex-direction:column;gap:var(--hm-space-2)}.kb-tree-node{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);overflow:hidden}.kb-tree-header{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-3) var(--hm-space-4);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background .15s}.kb-tree-header:hover{background:var(--hm-surface-hover)}.kb-tree-arrow{font-size:10px;color:var(--hm-text-dim);transition:transform .2s;flex-shrink:0}.kb-tree-icon{font-size:1rem;flex-shrink:0}.kb-tree-name{font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600}.kb-tree-actions{margin-left:auto;display:flex;gap:var(--hm-space-1);flex-shrink:0}.kb-tree-meta{padding:0 var(--hm-space-4) var(--hm-space-2) 2.25rem;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.kb-tree-preview{padding:0 var(--hm-space-4) var(--hm-space-3) 2.25rem;font-size:var(--hm-text-sm);color:var(--hm-text-muted);font-style:italic;max-height:3rem;overflow:hidden;white-space:pre-wrap;word-break:break-word}.kb-chunk-browser{padding:0 var(--hm-space-4) var(--hm-space-4) var(--hm-space-4)}.kb-chunk-loading{display:flex;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-dim);font-size:var(--hm-text-sm);padding:var(--hm-space-2) 0}.kb-chunk-list{display:flex;flex-direction:column;gap:var(--hm-space-1)}.kb-chunk-header{display:flex;justify-content:space-between;padding:var(--hm-space-2) 0;border-bottom:1px solid var(--hm-border)}.kb-chunk-empty{padding:var(--hm-space-2) 0}.kb-chunk-item{padding:var(--hm-space-2) var(--hm-space-3);border-radius:var(--hm-radius-md);border:1px solid var(--hm-border-subtle);cursor:pointer;transition:all .15s}.kb-chunk-item:hover{background:var(--hm-surface-hover);border-color:var(--hm-border)}.kb-chunk-selected{background:var(--hm-gold-dim)!important;border-color:#d977064d!important}.kb-chunk-item-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:2px}.kb-chunk-index{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);font-weight:600;color:var(--hm-accent);min-width:2rem}.kb-chunk-chars{font-size:var(--hm-text-xs);color:var(--hm-text-dim);min-width:4.5rem}.kb-chunk-bar{flex:1;height:4px;background:var(--hm-border);border-radius:2px;overflow:hidden}.kb-chunk-bar-fill{height:100%;background:var(--hm-accent);border-radius:2px;transition:width .3s}.kb-chunk-preview{font-size:var(--hm-text-xs);color:var(--hm-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.kb-chunk-content{font-size:var(--hm-text-sm);color:var(--hm-text);white-space:pre-wrap;word-break:break-word;max-height:20rem;overflow-y:auto;margin-top:var(--hm-space-2);padding:var(--hm-space-2);background:var(--hm-bg);border-radius:var(--hm-radius-md);border:1px solid var(--hm-border)}.memory-scope-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:var(--hm-text-sm);font-weight:600;font-family:var(--hm-font-mono)}.memory-scope-global{background:#d9770633;color:var(--hm-gold);border:1px solid rgba(217,119,6,.3)}.memory-scope-user{background:#eab30826;color:#fde047;border:1px solid rgba(234,179,8,.25)}.memory-checkbox{width:16px;height:16px;accent-color:var(--hm-accent);cursor:pointer}.mem-stats-bar{display:flex;gap:var(--hm-space-4);margin-bottom:var(--hm-space-4);flex-wrap:wrap}.mem-stat{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-3) var(--hm-space-4);text-align:center;min-width:80px}.mem-stat-value{display:block;font-size:1.25rem;font-weight:700;color:var(--hm-gold)}.mem-stat-label{display:block;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.mem-stat-action{display:flex;align-items:center;justify-content:center}.mem-add-form{border-left:3px solid var(--hm-accent)}.mem-tree{display:flex;flex-direction:column;gap:var(--hm-space-2)}.mem-tree-node{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);overflow:hidden}.mem-tree-header{display:flex;align-items:center;gap:var(--hm-space-2);padding:var(--hm-space-3) var(--hm-space-4);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background .15s}.mem-tree-header:hover{background:var(--hm-surface-hover)}.mem-tree-arrow{font-size:10px;color:var(--hm-text-dim);transition:transform .2s;flex-shrink:0}.mem-tree-entries{padding:0 var(--hm-space-4) var(--hm-space-3)}.mem-tree-loading{display:flex;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-dim);font-size:var(--hm-text-sm);padding:var(--hm-space-2) 0}.mem-tree-empty{padding:var(--hm-space-2) 0}.mem-tree-entry{padding:var(--hm-space-2) var(--hm-space-3);border-radius:var(--hm-radius-md);border:1px solid var(--hm-border-subtle);margin-bottom:var(--hm-space-1);transition:all .15s}.mem-tree-entry:hover{background:var(--hm-surface-hover)}.mem-tree-entry-selected{background:var(--hm-gold-dim);border-color:#d977064d}.mem-tree-entry-header{display:flex;align-items:center;gap:var(--hm-space-2);margin-bottom:2px}.mem-tree-key{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);font-weight:500;color:var(--hm-text-muted)}.mem-tree-entry-actions{margin-left:auto;display:flex;gap:var(--hm-space-1);flex-shrink:0}.mem-tree-value{font-size:var(--hm-text-sm);color:var(--hm-text);white-space:pre-wrap;word-break:break-word;max-height:6rem;overflow:hidden;padding-left:1.5rem}.mem-tree-edit{padding-left:1.5rem;margin-top:var(--hm-space-1)}.ag-checkbox{width:14px;height:14px;accent-color:var(--hm-accent);cursor:pointer}.ag-stats-bar{display:flex;gap:var(--hm-space-4);margin-bottom:var(--hm-space-4);flex-wrap:wrap}.ag-stat{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-3) var(--hm-space-4);text-align:center;min-width:80px}.ag-stat-value{display:block;font-size:1.25rem;font-weight:700;color:var(--hm-gold)}.ag-stat-label{display:block;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}.ag-stat-running{color:var(--hm-success)!important}.ag-stat-completed{color:var(--hm-info)!important}.ag-stat-failed{color:var(--hm-danger)!important}.ag-filter-bar{display:flex;gap:var(--hm-space-2);margin-bottom:var(--hm-space-4);flex-wrap:wrap}.ag-filter-btn{padding:var(--hm-space-1) var(--hm-space-3);border-radius:var(--hm-radius-md);font-size:var(--hm-text-xs);color:var(--hm-text-muted);background:var(--hm-surface);border:1px solid var(--hm-border);cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:var(--hm-space-1)}.ag-filter-btn:hover{background:var(--hm-surface-hover);color:var(--hm-text)}.ag-filter-active{background:var(--hm-accent-dim)!important;border-color:var(--hm-accent)!important;color:var(--hm-gold)!important}.ag-filter-count{font-family:var(--hm-font-mono);font-weight:600;font-size:.65rem;background:var(--hm-border);padding:0 4px;border-radius:3px}.ag-filter-active .ag-filter-count{background:#d977064d}.ag-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,340px),1fr));gap:var(--hm-space-4)}.ag-card{background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);padding:var(--hm-space-4);display:flex;flex-direction:column;gap:var(--hm-space-2);transition:border-color .2s,box-shadow .2s}.ag-card:hover{border-color:var(--hm-border)}.ag-card-running{border-left:3px solid var(--hm-success)}.ag-card-completed{border-left:3px solid var(--hm-info)}.ag-card-failed{border-left:3px solid var(--hm-danger)}.ag-card-timeout{border-left:3px solid var(--hm-warning)}.ag-card-killed{border-left:3px solid var(--hm-text-dim)}.ag-card-header{display:flex;align-items:center;justify-content:space-between}.ag-card-title-row{display:flex;align-items:center;gap:var(--hm-space-2);min-width:0;flex-wrap:nowrap}.ag-card-label{font-weight:600;font-size:var(--hm-text-sm);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ag-card-id{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);color:var(--hm-text-dim);flex:none}.ag-status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.ag-dot-running{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success);animation:loop-pulse 2s ease-in-out infinite}.ag-dot-completed{background:var(--hm-info)}.ag-dot-failed{background:var(--hm-danger)}.ag-dot-timeout{background:var(--hm-warning)}.ag-dot-killed{background:var(--hm-text-dim)}.ag-status-badge{font-size:var(--hm-text-xs);font-weight:600;padding:1px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.05em}.ag-badge-running{background:#22c55e26;color:var(--hm-success)}.ag-badge-completed{background:#3b82f626;color:var(--hm-info)}.ag-badge-failed{background:#ef444426;color:var(--hm-danger)}.ag-badge-timeout{background:#eab30826;color:var(--hm-warning)}.ag-badge-killed{background:#9ca3af26;color:var(--hm-text-dim)}.ag-card-goal{font-size:var(--hm-text-sm);color:var(--hm-text-muted);line-height:1.4;height:3.5rem;overflow:hidden;position:relative}.ag-card-goal:after{content:"";position:absolute;inset:auto 0 0 0;height:1.5rem;background:linear-gradient(transparent,var(--hm-surface));pointer-events:none}.ag-card-clickable:hover .ag-card-goal:after{background:linear-gradient(transparent,var(--hm-surface-hover))}.ag-progress-bar{height:3px;background:var(--hm-border);border-radius:2px;overflow:hidden}.ag-progress-fill{height:100%;background:var(--hm-success);border-radius:2px;transition:width .5s;animation:ag-progress-glow 2s ease-in-out infinite}@keyframes ag-progress-glow{0%,to{opacity:1}50%{opacity:.6}}.ag-card-stats{display:flex;gap:var(--hm-space-3)}.ag-card-stat{flex:1;text-align:center}.ag-card-stat-label{display:block;font-size:.6rem;color:var(--hm-text-dim);text-transform:uppercase;letter-spacing:.05em}.ag-card-stat-value{display:block;font-family:var(--hm-font-mono);font-size:var(--hm-text-sm);font-weight:600}.ag-card-tools{display:flex;flex-wrap:wrap;gap:4px}.ag-tool-chip{font-family:var(--hm-font-mono);font-size:.6rem;padding:1px 6px;background:var(--hm-gold-dim);color:var(--hm-text-muted);border-radius:3px;border:1px solid var(--hm-border-subtle)}.ag-card-policy{display:flex;flex-wrap:nowrap;gap:4px;min-width:0}.ag-policy-chip{font-family:var(--hm-font-mono);font-size:.62rem;padding:1px 7px;color:var(--hm-text-muted);background:var(--hm-surface-elevated);border:1px solid var(--hm-border);border-radius:3px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ag-policy-chip:not(.ag-policy-effort){flex:0 1 auto}.ag-policy-effort{flex:0 0 auto;color:var(--hm-accent);border-color:#c58b3247}.ag-card-body{display:flex;flex-direction:column;gap:var(--hm-space-2);text-align:left;border-radius:var(--hm-radius-md)}.ag-card-clickable{cursor:pointer;transition:background .12s ease,box-shadow .12s ease}.ag-card-clickable:hover{background:var(--hm-surface-hover)}.ag-card-clickable:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}.modal-content.ag-detail-modal{width:min(1000px,94vw);max-width:min(1000px,94vw);max-height:88vh;display:flex;flex-direction:column;gap:var(--hm-space-3);overflow-y:auto}.ag-detail-header{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-3)}.ag-detail-title-row{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-2);min-width:0}.ag-detail-title{margin:0;font-size:1rem;font-weight:650;letter-spacing:-.015em;color:#f2f3f6}.ag-detail-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:var(--hm-space-2) var(--hm-space-3)}.ag-detail-meta-item{display:flex;flex-direction:column;gap:1px;min-width:0}.ag-detail-meta-label{font-size:.6rem;color:var(--hm-text-dim);text-transform:uppercase;letter-spacing:.05em}.ag-detail-meta-value{font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);color:var(--hm-text);word-break:break-word}.ag-detail-source{margin:0;font-size:.66rem;color:var(--hm-text-dim);font-style:italic}.ag-detail-section{display:flex;flex-direction:column;gap:4px}.ag-detail-section-head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-2)}.ag-detail-text{margin:0;padding:var(--hm-space-2);max-height:34vh;overflow:auto;font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--hm-text-muted);background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.ag-detail-pending{margin:0;font-size:var(--hm-text-xs);color:var(--hm-text-dim)}@media(max-width:640px){.modal-content.ag-detail-modal{width:96vw;max-width:96vw;max-height:92vh}.ag-detail-text{max-height:40vh}}.ag-card-meta{display:flex;justify-content:space-between;gap:var(--hm-space-2)}.ag-card-result,.ag-card-error{padding:var(--hm-space-2);border-radius:var(--hm-radius-md);background:var(--hm-bg);border:1px solid var(--hm-border)}.ag-result-label{font-size:.6rem;color:var(--hm-text-dim);text-transform:uppercase;margin-bottom:2px}.ag-result-text{font-size:var(--hm-text-xs);white-space:pre-wrap;word-break:break-word;max-height:6rem;overflow:hidden;color:var(--hm-text-muted)}.ag-card-error{border-color:#ef444433}.ag-card-actions{display:flex;justify-content:flex-end}.loop-status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0}.loop-status-running{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success);animation:loop-pulse 2s ease-in-out infinite}.loop-status-error{background:var(--hm-danger);box-shadow:0 0 6px var(--hm-danger)}.loop-status-stopped{background:var(--hm-text-dim)}@keyframes loop-pulse{0%,to{opacity:1;box-shadow:0 0 6px var(--hm-success)}50%{opacity:.5;box-shadow:0 0 2px var(--hm-success)}}.loop-history{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4);max-height:200px;overflow-y:auto}.log-filter-field{flex:1 1 0%;min-width:0}.log-filter-row{display:flex;align-items:center;gap:.375rem;min-width:0}.log-filter-input{min-width:120px}@media(max-width:640px){.log-filter-field{flex:1 0 100%}.log-filter-input{min-width:0}}.loop-history-entry{font-size:var(--hm-text-sm);font-family:var(--hm-font-mono);color:var(--hm-text-muted);padding:var(--hm-space-1) 0;border-bottom:1px solid rgba(31,41,55,.3);white-space:pre-wrap;word-break:break-word}.loop-history-entry:last-child{border-bottom:none}.loop-card{display:flex;flex-direction:column;gap:var(--hm-space-3)}.loop-card-main{min-width:0;cursor:pointer;border-radius:var(--hm-radius-md);transition:background .12s ease,box-shadow .12s ease}.loop-card-main:hover{background:var(--hm-surface-hover)}.loop-card-goal{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;margin-bottom:var(--hm-space-2);color:var(--hm-text);font-size:var(--hm-text-sm);line-height:1.45}.loop-card-main:focus-visible{outline:2px solid var(--hm-accent);outline-offset:3px}.loop-card-actions{display:flex;justify-content:flex-end;gap:var(--hm-space-2)}.loop-card-preview{display:flex;flex-direction:column;gap:3px;margin-top:var(--hm-space-3);padding:var(--hm-space-2);border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);background:var(--hm-bg);font-size:var(--hm-text-xs);color:var(--hm-text-dim);max-height:4.5rem;overflow:hidden;white-space:pre-wrap;word-break:break-word}.loop-detail-modal{gap:var(--hm-space-4)}.loop-detail-goal{max-height:22vh}.loop-detail-condition{max-height:14vh}.loop-detail-history-head{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--hm-space-3);border-top:1px solid var(--hm-border);padding-top:var(--hm-space-3)}.loop-detail-history-head h3{margin:0 0 2px;font-size:var(--hm-text-sm)}.loop-detail-notice{padding:var(--hm-space-2);color:var(--hm-text-muted);font-size:var(--hm-text-xs);background:#c58b3214;border:1px solid rgba(197,139,50,.2);border-radius:var(--hm-radius-md)}.loop-detail-iterations{display:flex;flex-direction:column;gap:var(--hm-space-3)}.loop-detail-iteration{display:flex;flex-direction:column;gap:var(--hm-space-2);padding:var(--hm-space-3);border:1px solid var(--hm-border);background:var(--hm-surface-elevated);border-radius:var(--hm-radius-lg)}.loop-detail-iteration-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-2)}.loop-detail-iteration-head>div{display:flex;flex-direction:column;min-width:0}.loop-detail-turn-meta{display:flex;flex-wrap:wrap;gap:5px var(--hm-space-3);font-family:var(--hm-font-mono);font-size:.65rem;color:var(--hm-text-dim)}.loop-detail-response{max-height:30vh}.loop-detail-tools{display:flex;flex-wrap:wrap;gap:4px}.loop-context-details{padding:var(--hm-space-3);border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg);background:var(--hm-bg)}.loop-context-details summary{cursor:pointer;color:var(--hm-text-muted);font-size:var(--hm-text-xs);font-weight:600}.loop-context-details[open] summary{margin-bottom:var(--hm-space-2)}.loop-context-list{display:flex;flex-direction:column;gap:var(--hm-space-2);margin-top:var(--hm-space-2)}.loop-context-entry{max-height:18vh}@media(max-width:640px){.loop-card-actions{justify-content:stretch}.loop-card-actions .btn{flex:1}.loop-detail-history-head{align-items:flex-start}.loop-detail-turn-meta{display:grid;grid-template-columns:1fr 1fr}}.process-output-preview{background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);padding:var(--hm-space-3) var(--hm-space-4);font-size:var(--hm-text-sm);font-family:var(--hm-font-mono);color:var(--hm-text-muted);max-height:120px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;margin:0}.hm-input:focus-visible,.hm-select:focus-visible,.chat-input:focus-visible,.toggle-switch input:focus-visible+.toggle-slider,.session-checkbox:focus-visible,.memory-checkbox:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}.page-fade-in{animation:page-fade .2s ease-out}@keyframes page-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.empty-state{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-3);padding:var(--hm-space-8) var(--hm-space-5);text-align:center}.empty-state-icon{font-size:1.75rem;opacity:.4}.empty-state-text{color:var(--hm-text-muted);font-size:var(--hm-text-md)}.empty-state-hint{color:var(--hm-text-dim);font-size:var(--hm-text-sm)}.cron-preset-btn{display:inline-block;padding:.125rem .5rem;background:var(--hm-gold-dim);border:1px solid rgba(217,119,6,.2);border-radius:var(--hm-radius-sm);font-size:var(--hm-text-xs);color:var(--hm-text-muted);cursor:pointer;transition:all var(--hm-transition-base)}.cron-preset-btn:hover{background:var(--hm-accent-dim);color:var(--hm-text);border-color:#d9770666}.hm-card-accent{border-top:2px solid var(--hm-accent)}.hm-divider{height:1px;background:linear-gradient(90deg,transparent,var(--hm-accent-dim),transparent);border:none;margin:var(--hm-space-5) 0}.hm-mono{font-family:var(--hm-font-mono)}.hm-section-title{font-size:var(--hm-text-sm);font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--hm-text-dim)}.ws-indicator{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;transition:background .3s,box-shadow .3s}.ws-connected{background:var(--hm-success);box-shadow:0 0 6px var(--hm-success)}.ws-disconnected{background:var(--hm-danger);box-shadow:0 0 6px var(--hm-danger)}.ws-connecting,.ws-reconnecting{background:var(--hm-warning);box-shadow:0 0 6px var(--hm-warning);animation:ws-pulse 1.2s ease-in-out infinite}@keyframes ws-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.75)}}.ws-toast{position:fixed;bottom:var(--hm-space-5);left:50%;transform:translate(-50%);z-index:9999;padding:var(--hm-space-3) var(--hm-space-5);border-radius:var(--hm-radius-lg);font-size:var(--hm-text-sm);font-weight:500;box-shadow:var(--hm-shadow-lg);pointer-events:none}.ws-toast-success{background:#22c55e26;border:1px solid rgba(34,197,94,.3);color:#4ade80}.ws-toast-warn{background:#eab30826;border:1px solid rgba(234,179,8,.3);color:#fbbf24}.ws-toast-info{background:#3b82f626;border:1px solid rgba(59,130,246,.3);color:#60a5fa}.ws-toast-enter-active{animation:ws-toast-in .3s ease-out}.ws-toast-leave-active{animation:ws-toast-out .25s ease-in forwards}@keyframes ws-toast-in{0%{opacity:0;transform:translate(-50%) translateY(12px)}to{opacity:1;transform:translate(-50%) translateY(0)}}@keyframes ws-toast-out{0%{opacity:1;transform:translate(-50%) translateY(0)}to{opacity:0;transform:translate(-50%) translateY(12px)}}.toast-stack{position:fixed;bottom:var(--hm-space-5);right:var(--hm-space-5);z-index:9999;display:flex;flex-direction:column;align-items:flex-end;gap:var(--hm-space-2);pointer-events:none}.toast-item{display:flex;align-items:center;gap:var(--hm-space-2);max-width:380px;padding:var(--hm-space-3) var(--hm-space-4);border-radius:var(--hm-radius-lg);font-size:var(--hm-text-sm);font-weight:500;box-shadow:var(--hm-shadow-lg);cursor:pointer;pointer-events:auto;background:var(--hm-surface);border:1px solid var(--hm-border);color:var(--hm-text)}.toast-item .toast-icon{flex-shrink:0}.toast-item .toast-text{overflow-wrap:anywhere}.toast-success{background:#22c55e1f;border-color:#22c55e4d;color:#4ade80}.toast-error{background:#ef44441f;border-color:#ef444459;color:#f87171}.toast-info{background:#3b82f61f;border-color:#3b82f64d;color:#60a5fa}.toast-enter-active{transition:all .25s ease-out}.toast-leave-active{transition:all .2s ease-in}.toast-enter-from,.toast-leave-to{opacity:0;transform:translateY(10px)}.modal-enter-active{transition:opacity .15s ease-out}.modal-leave-active{transition:opacity .12s ease-in}.modal-enter-from,.modal-leave-to{opacity:0}.confirm-dialog{max-width:420px}.palette-overlay{align-items:flex-start;padding-top:14vh}.palette{width:90%;max-width:520px;background:var(--hm-surface);border:1px solid var(--hm-border);border-radius:var(--hm-radius-xl);box-shadow:var(--hm-shadow-lg);overflow:hidden}.palette-input{width:100%;background:transparent;border:none;border-bottom:1px solid var(--hm-border);padding:var(--hm-space-4) var(--hm-space-5);font-size:var(--hm-text-base);color:var(--hm-text);outline:none}.palette-input::-moz-placeholder{color:var(--hm-text-muted)}.palette-input::placeholder{color:var(--hm-text-muted)}.palette-results{max-height:320px;overflow-y:auto;padding:var(--hm-space-2)}.palette-item{display:flex;align-items:center;gap:var(--hm-space-2);width:100%;text-align:left;padding:var(--hm-space-2) var(--hm-space-3);border-radius:var(--hm-radius-md);font-size:var(--hm-text-sm);color:var(--hm-text);background:transparent;border:none;cursor:pointer}.palette-item.selected{background:var(--hm-surface-hover)}.palette-item .palette-icon{flex-shrink:0}.palette-item .palette-group{color:var(--hm-text-muted)}.palette-empty{padding:var(--hm-space-4);text-align:center;color:var(--hm-text-muted);font-size:var(--hm-text-sm)}.palette-footer{display:flex;gap:var(--hm-space-3);padding:var(--hm-space-2) var(--hm-space-4);border-top:1px solid var(--hm-border);font-size:var(--hm-text-xs);color:var(--hm-text-muted)}.palette-footer kbd{padding:0 .3em;background:var(--hm-bg);border:1px solid var(--hm-border);border-radius:var(--hm-radius-sm)}.item-enter{animation:item-slide-in .25s ease-out}@keyframes item-slide-in{0%{opacity:0;transform:translate(-8px)}to{opacity:1;transform:translate(0)}}.dash-stat-value{transition:color var(--hm-transition-slow)}.badge{transition:transform var(--hm-transition-fast),background var(--hm-transition-base)}.badge-pop{animation:badge-pop .3s var(--hm-transition-spring)}@keyframes badge-pop{0%{transform:scale(1)}50%{transform:scale(1.3)}to{transform:scale(1)}}.action-pending{opacity:.6;pointer-events:none}.action-success{animation:action-flash-ok .5s ease-out}.action-error{animation:action-flash-err .5s ease-out}@keyframes action-flash-ok{0%{box-shadow:0 0 #22c55e66}to{box-shadow:0 0 0 0 transparent}}@keyframes action-flash-err{0%{box-shadow:0 0 #ef444466}to{box-shadow:0 0 0 0 transparent}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.skip-nav{position:absolute;top:-100%;left:var(--hm-space-5);z-index:1000;padding:var(--hm-space-3) var(--hm-space-5);background:var(--hm-accent);color:#fff;border-radius:var(--hm-radius-md);font-size:var(--hm-text-md);font-weight:600;text-decoration:none;transition:top var(--hm-transition-base)}.skip-nav:focus{top:var(--hm-space-3)}.ag-filter-btn:focus-visible,.kb-tree-header:focus-visible,.kb-chunk-item:focus-visible,.cron-preset-btn:focus-visible,.dash-health-item:focus-visible,[role=tab]:focus-visible,[role=button]:focus-visible,a:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}.spinner{animation:none;border-top-color:var(--hm-accent)}.page-fade-in{animation:none}.hm-sidebar,.dash-ring-progress{transition:none}.ws-connecting,.ws-reconnecting,.ws-toast{animation:none}}@media(forced-colors:active){.status-dot,.ag-status-dot,.dash-health-dot,.loop-status-dot{forced-color-adjust:none}.btn-primary{border:1px solid ButtonText}.hm-card{border:1px solid CanvasText}.nav-item.active{border-left:3px solid Highlight}}.hm-main .p-6{max-width:1600px}@media(min-width:1200px){:root{--hm-text-md: .9375rem}}@media(max-width:768px){.hm-main .p-6{padding:var(--hm-space-5)}.hm-table{font-size:var(--hm-text-sm)}.hm-table th,.hm-table td{padding:var(--hm-space-2) var(--hm-space-3)}.mobile-hide{display:none!important}.chat-bubble{max-width:90%}.btn{min-height:36px}.hm-input,.hm-select{min-height:36px;font-size:1rem}.toggle-switch{width:44px;height:24px}.toggle-slider:before{width:18px;height:18px}.toggle-switch input:checked+.toggle-slider:before{transform:translate(20px)}.modal-content{padding:var(--hm-space-5);width:95%}.config-key-col{width:auto!important;min-width:80px}.log-chip{padding:var(--hm-space-1) .625rem;font-size:var(--hm-text-sm)}.session-checkbox,.memory-checkbox{width:18px;height:18px}.cron-preset-btn{padding:var(--hm-space-1) .625rem;font-size:var(--hm-text-sm)}}@media(max-width:640px){.dash-hero-left{flex-basis:100%}}@media(max-width:480px){.chat-bubble-wrap{max-width:90%}.chat-avatar{width:24px;height:24px}.chat-input-row{flex-direction:column;align-items:stretch}.chat-input{width:100%}.chat-send-btn{width:100%;min-width:unset}.chat-suggestions{flex-direction:column}.chat-suggestion{width:100%}.stat-grid-mobile{grid-template-columns:repeat(2,1fr)!important}h1{font-size:1.125rem!important}.hm-main .p-6{padding:var(--hm-space-4)}}.fts-result-user{background:#11182780;border-color:#1f2937}.fts-result-assistant{background:#312e814d;border-color:#312e814d}.fts-result-summary{background:#451a0333;border-color:#78350f4d}.fts-result-fts{background:#022c2233;border-color:#064e3b4d}.fts-result-channel{background:#3b076433;border-color:#581c874d}.fts-result-default{background:#1118274d;border-color:#1f293780}.fts-highlight{background:#d977064d;color:var(--hm-gold);border-radius:2px;padding:0 2px}.health-card{padding:1rem;border-left:3px solid transparent;transition:border-color .2s}.health-card-ok{border-left-color:#22c55e}.health-card-degraded{border-left-color:#eab308}.health-card-down{border-left-color:#ef4444}.health-card-unconfigured{border-left-color:var(--hm-border)}.health-card-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.375rem}.health-card-icon{font-size:.875rem;flex-shrink:0}.health-card-name{font-size:.8125rem;font-weight:600;flex:1}.health-card-detail{font-size:.75rem;color:var(--hm-text-muted);line-height:1.4}.health-card-meta{margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--hm-border-subtle)}.health-meta-row{display:flex;align-items:center;gap:.5rem;padding:1px 0}.health-host-item{display:flex;align-items:center;gap:.375rem;padding:2px 0}.health-host-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.health-host-dot.dot-connected{background:#22c55e}.health-host-dot.dot-idle{background:#6b7280}.health-host-dot.dot-unknown{background:#374151}.res-bar-bg{height:8px;background:#1e293b;border-radius:4px;overflow:hidden}.res-bar-fill{height:100%;border-radius:4px;transition:width .4s ease;min-width:2px}.res-bar-blue{background:#3b82f6}.res-bar-purple{background:#a855f7}.res-bar-emerald{background:#10b981}.res-bar-amber{background:#f59e0b}.discord-global-card{display:grid;gap:var(--hm-space-4)}.discord-global-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-4)}.discord-global-heading p{margin-top:.2rem;color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.discord-global-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-3)}.discord-global-toggle,.discord-global-list{padding:var(--hm-space-4);border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);background:#04070b40}.discord-global-toggle{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.discord-global-list{display:grid;gap:var(--hm-space-2)}.discord-global-list strong{color:var(--hm-text);font-size:var(--hm-text-xs)}.discord-global-list>p{color:var(--hm-text-dim);font-size:.64rem;line-height:1.45}.discord-global-footer{padding-top:var(--hm-space-3);display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);border-top:1px solid var(--hm-border-subtle)}.discord-global-footer span{color:var(--hm-text-dim);font-size:var(--hm-text-xs)}@media(max-width:760px){.discord-global-heading,.discord-global-footer{align-items:stretch;flex-direction:column}.discord-global-grid{grid-template-columns:minmax(0,1fr)}.discord-global-footer .btn{width:100%}}.llm-advanced{margin-top:var(--hm-space-4);overflow:hidden;background:#04070b45;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.llm-advanced>summary{min-height:50px;padding:var(--hm-space-3) var(--hm-space-4);display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);color:var(--hm-text);cursor:pointer;list-style:none}.llm-advanced>summary::-webkit-details-marker{display:none}.llm-advanced>summary:after{content:"+";flex:none;color:var(--hm-accent);font-size:1rem}.llm-advanced[open]>summary:after{content:"−"}.llm-advanced>summary span{font-size:var(--hm-text-sm);font-weight:650}.llm-advanced>summary small{color:var(--hm-text-dim);font-size:var(--hm-text-xs);text-align:right}.llm-advanced-body{padding:var(--hm-space-4);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-4);border-top:1px solid var(--hm-border-subtle)}.llm-advanced-group{padding:var(--hm-space-4);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-3);background:#111720a6;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.llm-advanced-group>header{grid-column:1 / -1;display:grid;gap:.1rem}.llm-advanced-group>header strong{color:var(--hm-text);font-size:var(--hm-text-xs)}.llm-advanced-group>header span{color:var(--hm-text-dim);font-size:.62rem}.llm-advanced-group>label{min-width:0;display:grid;align-content:start;gap:.3rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.llm-advanced-group>label small{color:var(--hm-text-dim)}.llm-advanced-group.single{grid-template-columns:minmax(0,360px)}.llm-advanced-toggle{grid-template-columns:minmax(0,1fr) auto;align-items:center}.llm-advanced-footer{grid-column:1 / -1;display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4)}.llm-advanced-footer p{max-width:680px;color:var(--hm-text-dim);font-size:var(--hm-text-xs);line-height:1.5}.llm-advanced-state{grid-column:1 / -1;margin:0;color:var(--hm-text-dim);font-size:var(--hm-text-xs);line-height:1.45}.llm-advanced-state.pending{color:var(--hm-warning-text)}.llm-advanced.compact .llm-advanced-body{grid-template-columns:minmax(0,1fr) auto;align-items:end}.llm-advanced.compact .llm-advanced-footer{grid-column:auto}.llm-field-note{display:block;margin-top:.3rem;color:#9a8255;line-height:1.4}@media(max-width:760px){.llm-advanced>summary{align-items:flex-start}.llm-advanced>summary small{text-align:left}.llm-advanced-body,.llm-advanced.compact .llm-advanced-body,.llm-advanced-group{grid-template-columns:minmax(0,1fr)}.llm-advanced-footer,.llm-advanced.compact .llm-advanced-footer{grid-column:1;align-items:stretch;flex-direction:column}.llm-advanced-footer .btn{width:100%}}.config-center-page{--cfgc-rail-w: 218px;--cfgc-panel-border: rgba(49, 57, 71, .86);height:calc(100vh - var(--hm-topbar-h) - var(--hm-section-tabs-h));min-height:0!important;padding-bottom:0;display:flex;flex-direction:column;overflow:hidden}.cfgc-page-header{position:relative;z-index:16;flex:none;display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-6);margin-bottom:var(--hm-space-5)}.cfgc-eyebrow{margin-bottom:.28rem;color:var(--hm-accent);font-size:.625rem;font-weight:700;letter-spacing:.115em;line-height:1.2;text-transform:uppercase}.cfgc-page-summary{margin-top:.35rem;color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.cfgc-header-actions{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-2);flex-wrap:wrap}.cfgc-header-actions .btn{display:inline-flex;align-items:center;gap:var(--hm-space-2)}.cfgc-loading{display:grid;gap:var(--hm-space-4)}.cfgc-loading-grid{display:grid;grid-template-columns:var(--cfgc-rail-w) minmax(0,1fr);gap:var(--hm-space-5)}.cfgc-loading-grid .skeleton:last-child{grid-column:2}.cfgc-health{flex:none;margin-bottom:var(--hm-space-5);padding:var(--hm-space-5);background:radial-gradient(circle at 100% 0,rgba(197,139,50,.09),transparent 30%),linear-gradient(150deg,#131821fa,#0c1017fa);border:1px solid var(--cfgc-panel-border);border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-sm)}.cfgc-health-heading{display:flex;align-items:center;gap:var(--hm-space-4);margin-bottom:var(--hm-space-4)}.cfgc-health-heading>div:first-child{flex:1;min-width:0}.cfgc-health-heading h2,.cfgc-category-panel-heading h2,.cfgc-empty h2,.cfgc-review-header h2{color:var(--hm-text);font-size:var(--hm-text-lg);font-weight:650;letter-spacing:-.015em}.cfgc-health-ok,.cfgc-unsaved-pill{display:inline-flex;align-items:center;gap:var(--hm-space-2);padding:.3rem .55rem;border-radius:var(--hm-radius-full);font-size:var(--hm-text-xs);font-weight:650;white-space:nowrap}.cfgc-health-ok{color:#83d6a6;background:#22c55e14;border:1px solid rgba(34,197,94,.19)}.cfgc-unsaved-pill{color:#ecc16f;background:#d977061a;border:1px solid rgba(217,119,6,.24)}.cfgc-health-filters{display:grid;grid-template-columns:repeat(7,minmax(112px,1fr));gap:var(--hm-space-2);overflow-x:auto;scrollbar-width:thin}.cfgc-health-filter{min-width:0;min-height:56px;padding:.55rem .6rem;display:flex;align-items:center;gap:var(--hm-space-3);color:var(--hm-text-muted);background:#06090d61;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);text-align:left;cursor:pointer;transition:color var(--hm-transition-base),border-color var(--hm-transition-base),background var(--hm-transition-base)}.cfgc-health-filter:hover{color:var(--hm-text);border-color:var(--hm-border);background:#ffffff06}.cfgc-health-filter.active{color:var(--hm-text);border-color:#c58b326b;background:#c58b3213;box-shadow:inset 0 0 0 1px #c58b320d}.cfgc-health-icon{width:28px;height:28px;display:inline-grid;place-items:center;flex:none;color:#a5afbd;background:#ffffff0a;border-radius:var(--hm-radius-md)}.cfgc-health-icon.state-applied{color:#69c78e;background:#22c55e14}.cfgc-health-icon.state-pending_restart{color:#e1aa55;background:#d977061a}.cfgc-health-icon.state-dormant{color:#a792d8;background:#8b5cf617}.cfgc-health-icon.state-invalid{color:#e17878;background:#ef444417}.cfgc-health-icon.state-drift{color:#e1bd65;background:#eab30817}.cfgc-health-icon.state-unknown{color:#9aa6b6;background:#94a3b817}.cfgc-health-copy{min-width:0;display:grid;gap:.1rem}.cfgc-health-copy>span{font-size:var(--hm-text-xs);font-weight:650;line-height:1.25;overflow-wrap:anywhere;white-space:normal}.cfgc-health-copy small{color:var(--hm-text-dim);font-size:.625rem;white-space:nowrap}.cfgc-health-alert{display:flex;align-items:flex-start;gap:var(--hm-space-3);margin-top:var(--hm-space-3);padding:var(--hm-space-3) var(--hm-space-4);border-radius:var(--hm-radius-md);font-size:var(--hm-text-xs)}.cfgc-health-alert>.odin-icon{flex:none;margin-top:.1rem}.cfgc-health-alert>div{display:grid;gap:.1rem}.cfgc-health-alert>div span{color:var(--hm-text-muted)}.cfgc-health-alert.danger{color:#eb8a8a;background:#ef444413;border:1px solid rgba(239,68,68,.19)}.cfgc-health-alert.warning{color:#e3bb69;background:#eab30811;border:1px solid rgba(234,179,8,.17)}.cfgc-workspace{min-height:0;flex:1;display:grid;grid-template-columns:var(--cfgc-rail-w) minmax(0,1fr);align-items:stretch;gap:var(--hm-space-5);overflow:hidden}.cfgc-category-rail{position:relative;top:auto;height:100%;max-height:none;padding:var(--hm-space-3);overflow:hidden auto;background:#0c1017e6;border:1px solid var(--cfgc-panel-border);border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-sm)}.cfgc-rail-label{padding:var(--hm-space-2) var(--hm-space-3) var(--hm-space-3);color:var(--hm-text-dim);font-size:.625rem;font-weight:700;letter-spacing:.105em;text-transform:uppercase}.cfgc-category-scroll{display:grid;gap:.2rem}.cfgc-category{width:100%;min-width:0;padding:.55rem var(--hm-space-3);display:grid;grid-template-columns:26px minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-muted);background:transparent;border:1px solid transparent;border-radius:var(--hm-radius-md);text-align:left;cursor:pointer}.cfgc-category:hover{color:var(--hm-text);background:#ffffff07}.cfgc-category.active{color:#edd19a;background:#c58b3218;border-color:#c58b323d}.cfgc-category-icon{width:26px;height:26px;display:inline-grid;place-items:center;color:currentColor;background:#ffffff09;border-radius:var(--hm-radius-md)}.cfgc-category-copy{min-width:0;display:grid;gap:.05rem}.cfgc-category-copy>span{overflow:hidden;font-size:var(--hm-text-sm);font-weight:600;text-overflow:ellipsis;white-space:nowrap}.cfgc-category-copy small{color:var(--hm-text-dim);font-size:.6rem}.cfgc-category-counts{display:flex;align-items:center;justify-content:flex-end;gap:.18rem;flex-wrap:wrap;max-width:50px}.cfgc-category-counts>span,.cfgc-rail-key b{display:inline-grid;min-width:18px;height:18px;place-items:center;padding:0 .2rem;border-radius:4px;font-size:.55rem;font-weight:750}.cfgc-category-counts .modified,.cfgc-rail-key .modified{color:#edc171;background:#d977061f}.cfgc-category-counts .restart,.cfgc-rail-key .restart{color:#dfaa5d;background:#b4681b21}.cfgc-category-counts .invalid,.cfgc-rail-key .invalid{color:#e38282;background:#ef44441c}.cfgc-category-counts .dormant,.cfgc-rail-key .dormant{color:#b09adf;background:#8b5cf61f}.cfgc-rail-key{margin-top:var(--hm-space-3);padding:var(--hm-space-3) var(--hm-space-2) var(--hm-space-1);display:grid;grid-template-columns:1fr 1fr;gap:.32rem;color:var(--hm-text-dim);border-top:1px solid var(--hm-border-subtle);font-size:.56rem}.cfgc-rail-key span{display:flex;align-items:center;gap:.25rem}.cfgc-rail-key b{font-style:normal}.cfgc-main{min-width:0;min-height:0;height:100%;overflow-y:auto;padding-bottom:5rem}.cfgc-toolbar{position:sticky;top:0;z-index:12;margin:0 0 var(--hm-space-4);padding:var(--hm-space-3);display:flex;align-items:center;gap:var(--hm-space-3);background:#090c11f0;border:1px solid var(--cfgc-panel-border);border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-sm);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}.cfgc-search{min-width:0;height:38px;display:flex;align-items:center;flex:1;gap:var(--hm-space-3);padding:0 .45rem 0 .75rem;color:var(--hm-text-dim);background:#04070b8c;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.cfgc-search:focus-within{color:var(--hm-accent);border-color:#c58b3275;box-shadow:0 0 0 3px #c58b3214}.cfgc-search input{min-width:0;height:100%;flex:1;color:var(--hm-text);background:transparent;border:0;outline:0;font-size:var(--hm-text-sm)}.cfgc-search input::-moz-placeholder{color:#5f6977}.cfgc-search input::placeholder{color:#5f6977}.cfgc-search .icon-btn{width:28px;height:28px}.cfgc-category-panel{display:grid;gap:var(--hm-space-3)}.cfgc-category-panel+.cfgc-category-panel{margin-top:var(--hm-space-6)}.cfgc-category-panel-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--hm-space-4);padding:0 var(--hm-space-1)}.cfgc-category-panel-heading>span{color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.cfgc-empty{min-height:280px;display:grid;place-items:center;align-content:center;gap:var(--hm-space-3);color:var(--hm-text-dim);text-align:center}.cfgc-empty p{max-width:520px;color:var(--hm-text-muted);font-size:var(--hm-text-sm)}.cfgc-empty code{color:#c5a569}.cfgc-section{overflow:clip;background:linear-gradient(150deg,#121720f5,#0d1118fa);border:1px solid var(--cfgc-panel-border);border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-sm);transition:border-color var(--hm-transition-base),box-shadow var(--hm-transition-base)}.cfgc-section:hover{border-color:#394252}.cfgc-section.modified{border-color:#c58b3266;box-shadow:inset 3px 0 #c58b328c,var(--hm-shadow-sm)}.cfgc-section.editing{border-color:#c58b3294;box-shadow:inset 3px 0 var(--hm-accent),var(--hm-shadow-glow-sm)}.cfgc-section-header{width:100%;min-width:0;min-height:66px;padding:var(--hm-space-4) var(--hm-space-5);display:grid;grid-template-columns:20px minmax(150px,.65fr) minmax(220px,1.35fr) auto;align-items:center;gap:var(--hm-space-3);color:inherit;background:transparent;border:0;text-align:left;cursor:pointer}.cfgc-section-header:hover{background:#ffffff05}.cfgc-section-chevron{color:var(--hm-text-dim)}.cfgc-section-title{min-width:0;display:grid;gap:.12rem}.cfgc-section-title>span{overflow:hidden;color:var(--hm-text);font-size:var(--hm-text-sm);font-weight:650;text-overflow:ellipsis;white-space:nowrap}.cfgc-section-title small{overflow:hidden;color:#67717f;font-family:var(--hm-font-mono);font-size:.6rem;text-overflow:ellipsis;white-space:nowrap}.cfgc-section-summary{min-width:0;overflow:hidden;color:var(--hm-text-muted);font-size:var(--hm-text-xs);line-height:1.45;text-overflow:ellipsis;white-space:nowrap}.cfgc-section-badges{min-width:0;display:flex;align-items:center;justify-content:flex-end;gap:.3rem;flex-wrap:wrap}.cfgc-section-badges .badge{padding:.15rem .4rem;font-size:.56rem}.cfgc-badge-restart{color:#dba454;background:#d9770617;border:1px solid rgba(217,119,6,.22)}.cfgc-badge-dormant{color:#ac98d7;background:#8b5cf617;border:1px solid rgba(139,92,246,.2)}.cfgc-field-count{min-width:27px;height:22px;display:inline-grid;place-items:center;padding:0 .35rem;color:var(--hm-text-dim);background:#ffffff09;border-radius:var(--hm-radius-full);font-family:var(--hm-font-mono);font-size:.6rem}.cfgc-section-body{border-top:1px solid var(--hm-border-subtle)}.cfgc-section-actions{min-height:58px;padding:var(--hm-space-3) var(--hm-space-5);display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);background:#04070b47;border-bottom:1px solid var(--hm-border-subtle)}.cfgc-section-actions>div:first-child{min-width:0;display:grid;gap:.1rem}.cfgc-section-actions strong{color:var(--hm-text);font-size:var(--hm-text-xs);font-weight:650}.cfgc-section-actions span{overflow:hidden;color:var(--hm-text-dim);font-size:.625rem;text-overflow:ellipsis;white-space:nowrap}.cfgc-section-actions .btn{display:inline-flex;align-items:center;gap:var(--hm-space-2)}.cfgc-search-hits{padding:var(--hm-space-3) var(--hm-space-5);display:flex;align-items:center;gap:var(--hm-space-2);flex-wrap:wrap;color:var(--hm-text-dim);background:#c58b320a;border-bottom:1px solid rgba(197,139,50,.12);font-size:.625rem}.cfgc-search-hits button{padding:.22rem .4rem;color:#dbbd84;background:#c58b3214;border:1px solid rgba(197,139,50,.18);border-radius:5px;cursor:pointer}.cfgc-search-hits code{margin-left:.2rem;color:#7b8491}.cfgc-owner-card{margin:var(--hm-space-5);padding:var(--hm-space-5);display:grid;grid-template-columns:36px minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-4);background:#c58b320e;border:1px solid rgba(197,139,50,.18);border-radius:var(--hm-radius-md)}.cfgc-owner-card.compact{margin-bottom:0}.cfgc-owner-icon{width:36px;height:36px;display:inline-grid;place-items:center;color:#ddba78;background:#c58b321a;border-radius:var(--hm-radius-md)}.cfgc-owner-card strong{color:var(--hm-text);font-size:var(--hm-text-sm)}.cfgc-owner-card p{margin-top:.18rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs);line-height:1.5}.cfgc-owner-card .btn{display:inline-flex;align-items:center;gap:var(--hm-space-2)}.cfgc-fields{padding:0 var(--hm-space-5)}.cfgc-field{min-width:0;padding:var(--hm-space-5) 0;display:grid;grid-template-columns:minmax(210px,.95fr) minmax(240px,1.05fr);gap:var(--hm-space-6);scroll-margin-top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h) + 70px);border-bottom:1px solid var(--hm-border-subtle)}.cfgc-field:last-child{border-bottom:0}.cfgc-field.changed{margin-inline:calc(var(--hm-space-3) * -1);padding-inline:var(--hm-space-3);background:#c58b3209;border-radius:var(--hm-radius-md)}.cfgc-field.invalid{background:#ef444409}.cfgc-field-copy{min-width:0;padding-inline:var(--hm-space-4)}.cfgc-field-copy>label{display:block;color:var(--hm-text);font-size:var(--hm-text-sm);font-weight:630}.cfgc-field-copy>code{display:block;margin-top:.2rem;overflow-wrap:anywhere;color:#687382;font-family:var(--hm-font-mono);font-size:.6rem}.cfgc-field-copy>p{max-width:620px;margin-top:.42rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs);line-height:1.52}.cfgc-field-meta{margin-top:var(--hm-space-3);display:flex;align-items:center;gap:var(--hm-space-2);flex-wrap:wrap;color:var(--hm-text-dim);font-size:.6rem}.cfgc-apply-pill{display:inline-flex;align-items:center;min-height:20px;padding:.14rem .42rem;color:#9ea8b5;background:#ffffff09;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-full);font-size:.58rem;font-weight:650;line-height:1.2}.cfgc-apply-pill.apply-live-read,.cfgc-apply-pill.apply-live-apply{color:#78cb98;background:#22c55e12;border-color:#22c55e2b}.cfgc-apply-pill.apply-live-for-new-work{color:#78b9d6;background:#0ea5e912;border-color:#0ea5e92e}.cfgc-apply-pill.apply-restart{color:#dfa858;background:#d9770614;border-color:#d9770630}.cfgc-apply-pill.apply-activation-required,.cfgc-apply-pill.apply-dormant{color:#af99df;background:#8b5cf614;border-color:#8b5cf62e}.cfgc-apply-pill.apply-legacy-control{color:#d8bd70;background:#eab30812;border-color:#eab3082e}.cfgc-sensitive{display:inline-flex;align-items:center;gap:.22rem;color:#b6a06f}.cfgc-apply-details{margin-top:var(--hm-space-3);display:grid;gap:var(--hm-space-2)}.cfgc-section-apply-details{margin:0 var(--hm-space-5) var(--hm-space-4);display:grid;gap:var(--hm-space-2)}.cfgc-apply-detail{padding:.55rem .65rem;background:#04070b42;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.cfgc-apply-detail-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-2)}.cfgc-apply-detail-heading strong{color:#aeb8c5;font-size:.64rem;font-weight:650}.cfgc-apply-detail>p{margin-top:.3rem;color:var(--hm-text-dim);font-size:.62rem;line-height:1.45}.cfgc-apply-detail>code{display:block;margin-top:.3rem;overflow-wrap:anywhere;color:#c8ad75;font-family:var(--hm-font-mono);font-size:.6rem}.cfgc-apply-detail.detail-restart{border-color:#d9770629}.cfgc-apply-detail.detail-activation{border-color:#8b5cf62b}.cfgc-field-control{min-width:0;display:grid;align-content:start}.cfgc-field-control .hm-input,.cfgc-field-control .hm-select{width:100%}.cfgc-field-control .hm-input[type=number]{max-width:250px}.cfgc-value{display:block;min-width:0;padding:.52rem .65rem;overflow:hidden;color:#c2c9d2;background:#04070b5e;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs);text-overflow:ellipsis;white-space:nowrap}.cfgc-value-block{max-height:160px;margin:0;padding:var(--hm-space-3);overflow:auto;color:#aeb7c3;background:#04070b5e;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);font-family:var(--hm-font-mono);font-size:.65rem;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere}.cfgc-boolean-control{min-height:38px;padding:.42rem .6rem;display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);color:var(--hm-text-muted);background:#04070b57;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md);font-size:var(--hm-text-xs)}.cfgc-json-input{resize:vertical;line-height:1.5}.cfgc-expert-note{margin-top:.35rem;color:var(--hm-text-dim);font-size:.6rem}.cfgc-field-error{margin-top:.35rem;color:#e27c7c;font-size:var(--hm-text-xs)}.cfgc-write-only{padding:var(--hm-space-3);display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.15rem var(--hm-space-4);background:#c58b320b;border:1px solid rgba(197,139,50,.15);border-radius:var(--hm-radius-md)}.cfgc-write-only>span{display:inline-flex;align-items:center;gap:var(--hm-space-2);color:#cdb57e;font-size:var(--hm-text-xs);font-weight:620}.cfgc-write-only>small{grid-column:1;color:var(--hm-text-dim);font-size:.6rem}.cfgc-write-only>button{grid-column:2;grid-row:1 / span 2}.cfgc-mobile-action-bar{display:none}.cfgc-review-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:80;display:flex;justify-content:flex-end;background:#000000a8;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.cfgc-review-tray{width:min(680px,94vw);height:100%;display:grid;grid-template-rows:auto minmax(0,1fr) auto;color:var(--hm-text);background:#0c1017;border-left:1px solid #303846;box-shadow:-24px 0 70px #0000006b;outline:0;animation:cfgc-tray-in .18s ease-out}@keyframes cfgc-tray-in{0%{transform:translate(20px);opacity:.55}to{transform:translate(0);opacity:1}}.cfgc-review-header{padding:var(--hm-space-6);display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-4);border-bottom:1px solid var(--hm-border)}.cfgc-review-header p{margin-top:.32rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.cfgc-review-body{min-height:0;padding:var(--hm-space-5) var(--hm-space-6);overflow-y:auto}.cfgc-review-group{overflow:hidden;background:#121720bd;border:1px solid var(--hm-border);border-radius:var(--hm-radius-lg)}.cfgc-review-group+.cfgc-review-group{margin-top:var(--hm-space-4)}.cfgc-review-group>header{padding:var(--hm-space-3) var(--hm-space-4);display:flex;align-items:center;justify-content:space-between;background:#05080c5c;border-bottom:1px solid var(--hm-border-subtle)}.cfgc-review-group>header>span:last-child{min-width:23px;height:23px;display:inline-grid;place-items:center;color:var(--hm-text-dim);background:#ffffff0a;border-radius:var(--hm-radius-full);font-family:var(--hm-font-mono);font-size:.6rem}.cfgc-review-entry{padding:var(--hm-space-4);display:grid;grid-template-columns:minmax(160px,.7fr) minmax(200px,1.3fr);align-items:center;gap:var(--hm-space-4);border-bottom:1px solid var(--hm-border-subtle)}.cfgc-review-entry:last-child{border-bottom:0}.cfgc-review-entry>div:first-child{min-width:0;display:grid;gap:.17rem}.cfgc-review-entry strong{overflow:hidden;font-size:var(--hm-text-xs);font-weight:620;text-overflow:ellipsis;white-space:nowrap}.cfgc-review-entry code{overflow-wrap:anywhere;color:var(--hm-text-dim);font-size:.58rem}.cfgc-review-values{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:var(--hm-space-2)}.cfgc-review-values span{min-width:0;padding:.38rem .48rem;overflow:hidden;color:#9da7b4;background:#04070b66;border:1px solid var(--hm-border-subtle);border-radius:5px;font-family:var(--hm-font-mono);font-size:.6rem;text-overflow:ellipsis;white-space:nowrap}.cfgc-review-values span:last-child{color:#d5bd89;border-color:#c58b322b}.cfgc-review-values .odin-icon{color:var(--hm-text-dim)}.cfgc-review-footer{padding:var(--hm-space-5) var(--hm-space-6);display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:var(--hm-space-3);background:#080b10f5;border-top:1px solid var(--hm-border)}.cfgc-review-footer>div{min-width:0;display:grid;gap:.1rem}.cfgc-review-footer strong{color:var(--hm-text);font-size:var(--hm-text-xs);font-weight:620}.cfgc-review-footer span{color:var(--hm-text-dim);font-size:.6rem}.config-center-page{max-width:1600px;margin-inline:auto}.cfgc-health-filters{grid-template-columns:repeat(auto-fit,minmax(128px,1fr))}.cfgc-restart-banner{flex:none;margin-bottom:var(--hm-space-5);padding:var(--hm-space-4) var(--hm-space-5);display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-4);color:#e6ba6c;background:#b4681b17;border:1px solid rgba(217,119,6,.28);border-radius:var(--hm-radius-lg)}.cfgc-restart-banner>div:nth-child(2){min-width:0;display:grid;gap:.15rem}.cfgc-restart-banner strong{color:#f0cf92;font-size:var(--hm-text-sm)}.cfgc-restart-banner span{color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.cfgc-restart-actions{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-2);flex-wrap:wrap}.cfgc-restart-dialog{width:min(620px,calc(100vw - 2rem));margin:auto;padding:var(--hm-space-6);color:var(--hm-text);background:#0f141c;border:1px solid #3a4352;border-radius:var(--hm-radius-lg);box-shadow:var(--hm-shadow-lg)}.cfgc-restart-dialog h2{font-size:var(--hm-text-xl);font-weight:650}.cfgc-restart-dialog>p{margin-top:var(--hm-space-3);color:var(--hm-text-muted);font-size:var(--hm-text-sm);line-height:1.55}.cfgc-restart-dialog-actions{margin-top:var(--hm-space-5);display:flex;justify-content:flex-end;gap:var(--hm-space-2);flex-wrap:wrap}.cfgc-field-groups{display:grid;gap:var(--hm-space-4);padding:0 var(--hm-space-5) var(--hm-space-5)}.cfgc-field-group{overflow:clip;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.cfgc-field-group:not(.nested){border-color:transparent}.cfgc-field-group-header{padding:var(--hm-space-3) var(--hm-space-4);display:flex;align-items:flex-start;justify-content:space-between;gap:var(--hm-space-4);background:#04070b59;border-bottom:1px solid var(--hm-border-subtle)}.cfgc-field-group-header>div{min-width:0;display:grid;gap:.18rem}.cfgc-field-group-header strong{color:var(--hm-text);font-size:var(--hm-text-sm)}.cfgc-field-group-header code{color:var(--hm-text-dim);font-size:.58rem}.cfgc-field-group-header p{color:var(--hm-text-muted);font-size:var(--hm-text-xs);line-height:1.45}.cfgc-field-group-header>span{flex:none;color:var(--hm-text-dim);font-size:.6rem}.cfgc-field-group .cfgc-fields{padding:0}.cfgc-field{grid-template-columns:minmax(180px,4fr) minmax(190px,5fr);align-items:start}.cfgc-field-runtime-note{grid-column:1 / -1;min-width:0;margin-top:calc(var(--hm-space-2) * -1);padding:var(--hm-space-3);display:grid;gap:.25rem;background:#8b5cf60b;border:1px solid rgba(139,92,246,.16);border-radius:var(--hm-radius-md)}.cfgc-field-runtime-note strong{color:#b9a6df;font-size:.6rem;text-transform:uppercase;letter-spacing:.055em}.cfgc-field-runtime-note p{color:var(--hm-text-muted);font-size:.66rem;line-height:1.45}.cfgc-field-runtime-note .btn{margin-top:var(--hm-space-2);justify-self:start}.cfgc-runtime-summary-list{margin-top:var(--hm-space-3);display:grid;gap:var(--hm-space-2)}.cfgc-runtime-summary{padding:var(--hm-space-3);background:#04070b45;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.cfgc-runtime-summary strong{color:#b8c0cb;font-size:.62rem}.cfgc-runtime-summary p{margin-top:.25rem;color:var(--hm-text-muted);font-size:.66rem;line-height:1.45}.cfgc-group-apply-details{padding:0 var(--hm-space-4) var(--hm-space-4)}.cfgc-group-apply-details summary{color:var(--hm-text-dim);cursor:pointer;font-size:var(--hm-text-xs)}.cfgc-apply-detail-list{margin-top:var(--hm-space-3);display:grid;gap:var(--hm-space-2)}.discord-user-combobox{position:relative;width:100%}.discord-user-combobox>.hm-input{width:100%}.discord-user-combobox-options{position:absolute;z-index:60;top:calc(100% + .25rem);left:0;right:0;max-height:15rem;overflow-y:auto;background:#0d121a;border:1px solid #3b4555;border-radius:var(--hm-radius-md);box-shadow:var(--hm-shadow-lg)}.discord-user-combobox-option{width:100%;min-height:38px;padding:.45rem .65rem;display:flex;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-muted);background:transparent;border:0;cursor:pointer;font-size:var(--hm-text-xs);text-align:left}.discord-user-combobox-option:hover,.discord-user-combobox-option.active{color:var(--hm-text);background:#c58b3217}.discord-user-combobox-option img,.discord-user-combobox-avatar{width:22px;height:22px;flex:none;border-radius:var(--hm-radius-full)}.discord-user-combobox-avatar{display:inline-grid;place-items:center;color:var(--hm-text-dim);background:#293140;font-size:.6rem}.discord-user-combobox-name{min-width:0;overflow:hidden;color:inherit;text-overflow:ellipsis;white-space:nowrap}.discord-user-combobox-username{min-width:0;overflow:hidden;color:var(--hm-text-dim);text-overflow:ellipsis;white-space:nowrap}.discord-user-combobox-bot{margin-left:auto;padding:.08rem .25rem;color:#c4b5fd;background:#6366f133;border-radius:3px;font-size:.52rem}.discord-global-user-picker{display:block}.discord-global-list-full{grid-column:1 / -1}.cfgc-chip-editor{display:grid;gap:var(--hm-space-3)}.cfgc-chip-list{min-height:32px;display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}.cfgc-chip{max-width:100%;padding:.25rem .35rem .25rem .55rem;display:inline-flex;align-items:center;gap:.35rem;overflow-wrap:anywhere;color:#d8c28e;background:#c58b3214;border:1px solid rgba(197,139,50,.2);border-radius:var(--hm-radius-full);font-family:var(--hm-font-mono);font-size:var(--hm-text-xs)}.cfgc-chip button{width:20px;height:20px;color:#a9956e;background:transparent;border:0;border-radius:50%;cursor:pointer}.cfgc-chip button:hover{color:#fff;background:#ffffff14}.cfgc-chip-empty{color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.cfgc-chip-add{display:flex;align-items:center;gap:var(--hm-space-2);flex-wrap:wrap;color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.cfgc-chip-add .hm-input{width:min(170px,100%)}.cfgc-structured-summary{padding:var(--hm-space-3);display:grid;gap:.15rem;background:#04070b52;border:1px solid var(--hm-border);border-radius:var(--hm-radius-md)}.cfgc-structured-summary span{color:var(--hm-text);font-size:var(--hm-text-xs)}.cfgc-structured-summary small{color:var(--hm-text-dim);font-size:.62rem;line-height:1.4}@media(max-width:1180px){.config-center-page{--cfgc-rail-w: 190px}.cfgc-health-filters{grid-template-columns:repeat(7,minmax(106px,1fr))}.cfgc-category-copy small{display:none}.cfgc-section-header{grid-template-columns:20px minmax(0,1fr) auto}.cfgc-section-summary{grid-column:2;white-space:normal}.cfgc-section-badges{grid-column:3;grid-row:1 / span 2}.cfgc-field{grid-template-columns:minmax(180px,5fr) minmax(220px,7fr)}}@media(max-width:900px){.config-center-page{--cfgc-rail-w: 100%;height:auto;min-height:100%!important;overflow:visible}.cfgc-workspace{min-height:auto;flex:none;grid-template-columns:minmax(0,1fr);align-items:start;gap:var(--hm-space-3);overflow:visible}.cfgc-main{height:auto;overflow:visible;padding-bottom:5rem}.cfgc-category-rail{position:sticky;top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h));height:auto;z-index:13;max-height:none;padding:var(--hm-space-2);overflow:visible}.cfgc-rail-label,.cfgc-rail-key{display:none}.cfgc-category-scroll{display:flex;gap:var(--hm-space-2);overflow-x:auto;scroll-snap-type:x proximity}.cfgc-category{width:auto;min-width:150px;grid-template-columns:24px minmax(0,1fr) auto;flex:0 0 auto;scroll-snap-align:start}.cfgc-category-copy small{display:block}.cfgc-toolbar{top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h) + 59px)}.cfgc-section-header{grid-template-columns:20px minmax(120px,.8fr) minmax(150px,1.2fr) auto}.cfgc-field{grid-template-columns:minmax(180px,.85fr) minmax(210px,1.15fr);gap:var(--hm-space-4)}}@media(max-width:760px){.section-tabs-wrap{position:sticky;overflow:hidden}.section-tabs-wrap:after{content:"";position:absolute;top:0;right:0;bottom:0;width:34px;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(9,12,17,.98));border-right:2px solid rgba(197,139,50,.28)}.config-center-page{padding-bottom:6.75rem!important}.cfgc-page-header{align-items:stretch}.cfgc-header-actions{flex:none}.cfgc-header-actions .cfgc-desktop-history,.cfgc-header-actions .btn-primary{display:none}.cfgc-health{margin-inline:-.15rem;padding:var(--hm-space-4)}.cfgc-health-heading{align-items:flex-start;flex-wrap:wrap}.cfgc-health-filters{grid-template-columns:repeat(7,122px);margin-inline:calc(var(--hm-space-4) * -1);padding-inline:var(--hm-space-4);padding-bottom:.2rem;scroll-snap-type:x proximity}.cfgc-health-filter{scroll-snap-align:start}.cfgc-category-rail{top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h));margin-inline:-.1rem}.cfgc-category-scroll{padding-right:1.5rem}.cfgc-category-scroll:after{content:"";flex:0 0 .1rem}.cfgc-toolbar{top:calc(var(--hm-topbar-h) + var(--hm-section-tabs-h) + 58px);padding:var(--hm-space-2)}.cfgc-toolbar>.btn{display:none}.cfgc-search input{font-size:1rem}.cfgc-category-panel-heading{align-items:center}.cfgc-section-header{min-height:72px;padding:var(--hm-space-4);grid-template-columns:18px minmax(0,1fr) auto;gap:var(--hm-space-2)}.cfgc-section-summary,.cfgc-section-badges .badge{display:none}.cfgc-section-actions{padding:var(--hm-space-3) var(--hm-space-4);align-items:stretch;flex-direction:column}.cfgc-section-actions>div:last-child{justify-content:flex-end}.cfgc-search-hits{padding-inline:var(--hm-space-4)}.cfgc-owner-card{margin:var(--hm-space-4);grid-template-columns:34px minmax(0,1fr);padding:var(--hm-space-4)}.cfgc-owner-card .btn{grid-column:1 / -1;width:100%;justify-content:center}.cfgc-field-groups{padding-inline:var(--hm-space-4)}.cfgc-fields{padding:0}.cfgc-field{grid-template-columns:minmax(0,1fr);gap:var(--hm-space-3);padding-block:var(--hm-space-4)}.cfgc-field-copy>p{line-height:1.45}.cfgc-write-only{grid-template-columns:minmax(0,1fr)}.cfgc-write-only>small,.cfgc-write-only>button{grid-column:1;grid-row:auto}.cfgc-write-only>button{width:100%;margin-top:var(--hm-space-2)}.cfgc-restart-banner{grid-template-columns:auto minmax(0,1fr);padding:var(--hm-space-4)}.cfgc-restart-actions{grid-column:1 / -1;justify-content:stretch}.cfgc-restart-actions .btn{flex:1}.cfgc-restart-dialog-actions{display:grid}.cfgc-mobile-action-bar{position:fixed;z-index:55;left:0;right:0;bottom:0;min-height:68px;padding:var(--hm-space-3) max(var(--hm-space-4),env(safe-area-inset-right)) calc(var(--hm-space-3) + env(safe-area-inset-bottom)) max(var(--hm-space-4),env(safe-area-inset-left));display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;align-items:center;gap:var(--hm-space-2);background:#090c11fa;border-top:1px solid #394252;box-shadow:0 -12px 32px #0006;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.cfgc-mobile-action-bar .btn{min-width:0;padding-inline:.55rem}.cfgc-mobile-overflow{position:relative}.cfgc-mobile-overflow-menu{position:absolute;right:0;bottom:calc(100% + var(--hm-space-2));width:170px;padding:var(--hm-space-2);display:grid;gap:.15rem;background:#111720;border:1px solid #3a4352;border-radius:var(--hm-radius-md);box-shadow:var(--hm-shadow-lg)}.cfgc-mobile-overflow-menu button{min-height:38px;padding:0 var(--hm-space-3);display:flex;align-items:center;gap:var(--hm-space-2);color:var(--hm-text-muted);background:transparent;border:0;border-radius:5px;font-size:var(--hm-text-sm);text-align:left}.cfgc-mobile-overflow-menu button:hover:not(:disabled){color:var(--hm-text);background:#ffffff0a}.cfgc-mobile-overflow-menu button:disabled{opacity:.38}.cfgc-review-tray{width:100%;max-width:none}.cfgc-review-header{padding:var(--hm-space-5) var(--hm-space-4)}.cfgc-review-body{padding:var(--hm-space-4)}.cfgc-review-entry{grid-template-columns:minmax(0,1fr);gap:var(--hm-space-3)}.cfgc-review-footer{padding:var(--hm-space-4);grid-template-columns:1fr 1fr}.cfgc-review-footer>div{grid-column:1 / -1}}@media(max-width:420px){.cfgc-page-header{gap:var(--hm-space-3)}.cfgc-header-actions .btn{padding-inline:.65rem}.cfgc-page-summary{max-width:220px}.cfgc-mobile-action-bar{grid-template-columns:minmax(70px,1fr) minmax(90px,1fr) 36px}.cfgc-section-title>span{white-space:normal}.cfgc-review-values{grid-template-columns:minmax(0,1fr)}.cfgc-review-values .odin-icon{margin:-.1rem auto;transform:rotate(90deg)}}.llm-context-summary{min-height:58px;padding:.48rem .65rem;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.2rem var(--hm-space-2);background:#070a0f61;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.llm-context-summary>span:first-child{color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.llm-context-summary strong{color:#e7d39d;font-size:var(--hm-text-sm);font-variant-numeric:tabular-nums}.llm-context-summary strong small{color:var(--hm-text-dim);font-size:.58rem;font-weight:500}.llm-context-summary .llm-budget-provenance{justify-self:end}.llm-context-summary>small{grid-column:1 / -1;color:var(--hm-text-dim);font-size:.58rem}.llm-context-budget-panel{grid-column:1 / -1;overflow:hidden;background:radial-gradient(circle at 100% 0,rgba(201,162,78,.075),transparent 32rem),#080c12c7;border:1px solid rgba(116,96,57,.58);border-radius:var(--hm-radius-md)}.llm-context-budget-heading{padding:var(--hm-space-4);display:flex;align-items:end;justify-content:space-between;gap:var(--hm-space-5);border-bottom:1px solid var(--hm-border-subtle)}.llm-context-budget-heading>div{min-width:0;display:grid;gap:.15rem}.llm-context-budget-heading strong{color:var(--hm-text);font-size:var(--hm-text-sm)}.llm-context-budget-heading>div>span{color:var(--hm-text-dim);font-size:var(--hm-text-xs)}.llm-utilization-field{min-width:176px;display:grid;gap:.35rem;color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.llm-utilization-input{display:grid;grid-template-columns:minmax(0,1fr) 2rem;align-items:center}.llm-utilization-input .hm-input{border-radius:var(--hm-radius-md) 0 0 var(--hm-radius-md);text-align:right;font-variant-numeric:tabular-nums}.llm-utilization-input small{align-self:stretch;display:grid;place-items:center;color:var(--hm-accent);background:#c9a24e14;border:1px solid var(--hm-border);border-left:0;border-radius:0 var(--hm-radius-md) var(--hm-radius-md) 0}.llm-context-budget-copy{margin:0;padding:var(--hm-space-3) var(--hm-space-4);color:var(--hm-text-dim);background:#070a0f8c;border-bottom:1px solid var(--hm-border-subtle);font-size:var(--hm-text-xs);line-height:1.55}.llm-context-budget-loading,.llm-context-budget-error{min-height:112px;padding:var(--hm-space-5);display:flex;align-items:center;justify-content:center;gap:var(--hm-space-3);color:var(--hm-text-muted);font-size:var(--hm-text-xs)}.llm-context-budget-loading .spinner{width:16px;height:16px}.llm-context-budget-error{color:var(--hm-danger)}.llm-context-budget-table-wrap{overflow-x:auto}.llm-context-budget-table{min-width:1120px;table-layout:fixed}.llm-context-budget-table th{padding:0 var(--hm-space-3);font-size:.58rem}.llm-context-budget-table td{padding:var(--hm-space-3);font-size:var(--hm-text-xs)}.llm-context-budget-table th:nth-child(1){width:15%}.llm-context-budget-table th:nth-child(2){width:11%}.llm-context-budget-table th:nth-child(3){width:19%}.llm-context-budget-table th:nth-child(4){width:12%}.llm-context-budget-table th:nth-child(5){width:13%}.llm-context-budget-table th:nth-child(6){width:14%}.llm-context-budget-table th:nth-child(7){width:16%}.llm-context-budget-table tbody tr.has-clamp td{background:#d3943109}.llm-context-budget-table code,.llm-clamp-card code{color:#e4e8ef;font-size:.72rem;letter-spacing:-.015em}.llm-context-budget-table td>small{display:block;margin-top:.2rem;color:var(--hm-text-dim);font-size:.58rem;line-height:1.3}.llm-budget-value{color:var(--hm-text-muted);font-variant-numeric:tabular-nums}.llm-budget-effective{color:#e7d39d;font-weight:650}.llm-budget-override{display:flex;align-items:center;gap:var(--hm-space-2)}.llm-budget-override .hm-input{min-width:0;height:34px;padding:0 var(--hm-space-2);font-size:var(--hm-text-xs);font-variant-numeric:tabular-nums}.llm-budget-reset{padding:0;color:var(--hm-text-dim);border:0;background:transparent;font-size:.62rem;cursor:pointer}.llm-budget-reset:hover{color:var(--hm-accent)}.llm-budget-warning{color:var(--hm-warning-text)!important}.llm-budget-pending{display:inline-flex;margin-top:.3rem;padding:.12rem .42rem;color:var(--hm-warning-text);background:#d977061a;border:1px solid rgba(217,119,6,.23);border-radius:var(--hm-radius-full);font-size:.56rem;font-weight:650}.llm-budget-provenance{display:inline-flex;align-items:center;min-height:22px;padding:.16rem .48rem;border:1px solid transparent;border-radius:var(--hm-radius-full);font-size:.58rem;font-weight:650;white-space:nowrap}.llm-budget-provenance.is-built-in{color:#94a0b0;background:#7e889817;border-color:#7e88982e}.llm-budget-provenance.is-override{color:#ccb474;background:#c9a24e17;border-color:#c9a24e33}.llm-budget-provenance.is-clamp{color:var(--hm-warning-text);background:#d977061a;border-color:#d977063b}.llm-clamp-list{padding:var(--hm-space-4);border-top:1px solid rgba(217,119,6,.2)}.llm-clamp-list-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-4);margin-bottom:var(--hm-space-3)}.llm-clamp-list-heading>div{display:grid;gap:.1rem}.llm-clamp-list-heading strong{color:var(--hm-warning-text);font-size:var(--hm-text-xs)}.llm-clamp-list-heading>div>span{color:var(--hm-text-dim);font-size:.6rem}.llm-clamp-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-3)}.llm-clamp-card{min-width:0;padding:var(--hm-space-3);display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.25rem var(--hm-space-3);background:#d977060b;border:1px solid rgba(217,119,6,.16);border-radius:var(--hm-radius-md)}.llm-clamp-card>div{min-width:0;display:flex;align-items:baseline;gap:var(--hm-space-2);overflow:hidden}.llm-clamp-card>div span{color:var(--hm-warning-text);font-size:var(--hm-text-xs);font-variant-numeric:tabular-nums;white-space:nowrap}.llm-clamp-card p{grid-column:1;margin:0;color:var(--hm-text-dim);font-size:.58rem}.llm-clamp-card .btn{grid-column:2;grid-row:1 / span 2}@media(max-width:1180px){.llm-context-budget-table{min-width:900px}.llm-clamp-grid{grid-template-columns:minmax(0,1fr)}}@media(max-width:900px){.llm-context-budget-panel+.llm-advanced-footer{align-items:stretch;flex-direction:column}.llm-context-budget-panel+.llm-advanced-footer .btn{width:100%;justify-content:center}.llm-context-budget-heading{align-items:stretch;flex-direction:column}.llm-utilization-field{min-width:0}.llm-context-budget-table-wrap{overflow:visible}.llm-context-budget-table,.llm-context-budget-table tbody,.llm-context-budget-table tr,.llm-context-budget-table td{display:block;width:100%;min-width:0}.llm-context-budget-table thead{display:none}.llm-context-budget-table tbody{padding:var(--hm-space-3);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--hm-space-3)}.llm-context-budget-table tr{margin-bottom:0;overflow:hidden;background:#111720b3;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md)}.llm-context-budget-table td{min-height:44px;padding:var(--hm-space-2) var(--hm-space-3);display:grid;grid-template-columns:minmax(104px,.85fr) minmax(0,1.15fr);align-items:center;gap:var(--hm-space-3);text-align:right}.llm-context-budget-table td:before{content:attr(data-label);color:var(--hm-text-dim);font-size:.58rem;font-weight:650;letter-spacing:.045em;text-align:left;text-transform:uppercase}.llm-context-budget-table td:first-child{min-height:48px;background:#070a0f73}.llm-context-budget-table td>small{margin:-.1rem 0 0;grid-column:2}.llm-budget-override{justify-content:flex-end}.llm-budget-override .hm-input{width:150px}.llm-clamp-list-heading{align-items:flex-start}.llm-clamp-card{grid-template-columns:minmax(0,1fr)}.llm-clamp-card p,.llm-clamp-card .btn{grid-column:1;grid-row:auto}.llm-clamp-card .btn{width:100%;justify-content:center;margin-top:var(--hm-space-2)}}@media(max-width:600px){.llm-context-budget-table tbody{grid-template-columns:minmax(0,1fr)}.llm-budget-override{gap:.35rem}.llm-budget-override .hm-input{width:min(105px,100%)}}:root{--hm-topbar-h: 76px;--hm-rail-h: 107px;--hm-section-tabs-inner-h: 48px;--hm-section-tabs-border: 1px;--hm-section-tabs-h: calc(var(--hm-section-tabs-inner-h) + var(--hm-section-tabs-border));--hm-bg: #07090d;--hm-bg-raised: #0b0e14;--hm-surface: #10141c;--hm-surface-hover: #171d28;--hm-surface-elevated: #151a24;--hm-border: #252c39;--hm-border-subtle: #1a202b;--hm-accent: #c58b32;--hm-accent-hover: #dda84f;--hm-accent-dim: rgba(197, 139, 50, .13);--hm-accent-glow: rgba(197, 139, 50, .16);--hm-gold: #e0b766;--hm-gold-dim: rgba(197, 139, 50, .08);--hm-text: #edf0f5;--hm-text-muted: #9aa4b3;--hm-text-dim: #667182;--hm-success: #3db983;--hm-warning: #d8a342;--hm-danger: #e06464;--hm-info: #668fd7;--hm-radius-sm: 5px;--hm-radius-md: 7px;--hm-radius-lg: 10px;--hm-radius-xl: 14px;--hm-shadow-sm: 0 1px 2px rgba(0,0,0,.24), 0 0 0 1px rgba(255,255,255,.015);--hm-shadow-md: 0 10px 30px rgba(0,0,0,.28);--hm-shadow-lg: 0 24px 64px rgba(0,0,0,.48);--shell-sidebar: 252px;--shell-sidebar-collapsed: 72px}html{background:var(--hm-bg)}body{color:var(--hm-text);background:radial-gradient(circle at 72% -20%,rgba(197,139,50,.055),transparent 34rem),linear-gradient(180deg,#080a0f 0%,var(--hm-bg) 35%)}button,input,select,textarea{font:inherit}::-moz-selection{background:#c58b3252;color:#fff}::selection{background:#c58b3252;color:#fff}.odin-icon{display:block;flex:none}.app-loading{min-height:100vh;display:grid;place-items:center}.brand-loader{color:var(--hm-accent-hover);animation:brand-pulse 1.4s ease-in-out infinite}@keyframes brand-pulse{50%{opacity:.45;transform:scale(.94)}}.app-shell{display:flex;min-height:100vh}.hm-sidebar{position:relative;width:var(--shell-sidebar);min-width:var(--shell-sidebar);height:100vh;min-height:0;background:#0d1017f5;border-right:1px solid var(--hm-border-subtle);box-shadow:10px 0 35px #0000001f;transition:width .22s ease,min-width .22s ease,transform .22s ease;display:flex;flex-direction:column;z-index:40}.hm-sidebar:before{content:"";position:absolute;inset:0 auto 0 0;width:2px;background:linear-gradient(180deg,transparent 3%,var(--hm-accent) 24%,rgba(197,139,50,.15) 68%,transparent);opacity:.65;pointer-events:none}.hm-sidebar.collapsed{width:var(--shell-sidebar-collapsed);min-width:var(--shell-sidebar-collapsed)}.sidebar-brand{min-height:76px;padding:0 16px 0 18px;display:flex;align-items:center;gap:11px;border-bottom:1px solid var(--hm-border-subtle)}.brand-mark{width:34px;height:34px;display:grid;place-items:center;color:var(--hm-accent-hover);background:linear-gradient(145deg,#c58b3229,#c58b3209);border:1px solid rgba(197,139,50,.3);border-radius:9px;flex:none}.sidebar-brand-copy{display:flex;flex-direction:column;min-width:0;line-height:1}.brand-wordmark{font-size:.83rem;letter-spacing:.23em;font-weight:700;color:#f2dfb8}.brand-caption{margin-top:7px;font-size:.61rem;letter-spacing:.1em;text-transform:uppercase;color:var(--hm-text-dim)}.sidebar-toggle-btn{margin-left:auto}.hm-sidebar.collapsed .sidebar-brand{padding-inline:18px;justify-content:center}.hm-sidebar.collapsed .sidebar-brand-copy,.hm-sidebar.collapsed .nav-label,.hm-sidebar.collapsed .nav-section-label,.hm-sidebar.collapsed .connection-copy,.hm-sidebar.collapsed .shortcut-hint span,.hm-sidebar.collapsed .shortcut-hint kbd,.hm-sidebar.collapsed .brand-mark{display:none}.hm-sidebar.collapsed .sidebar-toggle-btn{margin-left:0}.sidebar-nav{flex:1;min-height:0;padding:13px 10px;overflow:auto}.nav-group+.nav-group{margin-top:18px}.nav-section-label{padding:0 10px 6px;color:#555f6f;font-size:.625rem;line-height:1;text-transform:uppercase;letter-spacing:.14em;font-weight:700}.nav-item{position:relative;min-height:40px;display:flex;align-items:center;white-space:nowrap;overflow:hidden;margin:2px 0;padding:0 10px;gap:11px;color:#8d97a7;border-radius:7px;font-size:.79rem;font-weight:500;text-decoration:none;cursor:pointer;transition:background var(--hm-transition-base),color var(--hm-transition-base)}.nav-item:hover{background:#ffffff09;color:#d9dee7}.nav-item.active{background:linear-gradient(90deg,#c58b3224,#c58b320b);color:#ebc982;box-shadow:inset 2px 0 0 var(--hm-accent)}.nav-item.active:after{content:"";position:absolute;right:10px;width:4px;height:4px;background:var(--hm-accent-hover);border-radius:50%;box-shadow:0 0 8px var(--hm-accent)}.nav-icon{width:20px;display:grid;flex:none;place-items:center;color:#717d8f}.nav-item:hover .nav-icon{color:#aeb7c4}.nav-item.active .nav-icon{color:var(--hm-accent-hover)}.hm-sidebar.collapsed .sidebar-nav{padding-inline:12px}.hm-sidebar.collapsed .nav-item{padding:0;justify-content:center}.hm-sidebar.collapsed .nav-item.active:after{right:5px}.sidebar-footer{min-height:var(--hm-rail-h);padding:12px;border-top:1px solid var(--hm-border-subtle);display:grid;gap:8px;align-content:center}.connection-card{min-height:42px;padding:8px 10px;display:flex;align-items:center;gap:9px;border:1px solid var(--hm-border-subtle);background:#00000024;border-radius:8px}.connection-copy{min-width:0;display:flex;flex:1;justify-content:space-between;align-items:baseline;gap:8px}.connection-label{font-size:.7rem;color:var(--hm-text-muted)}.connection-latency{font-family:var(--hm-font-mono);font-size:.58rem;color:var(--hm-text-dim)}.shortcut-hint{width:100%;min-height:32px;display:flex;align-items:center;gap:7px;padding:0 9px;border:0;border-radius:6px;background:transparent;color:var(--hm-text-dim);font-size:.67rem;cursor:pointer}.shortcut-hint:hover{color:var(--hm-text-muted);background:#ffffff08}.shortcut-hint kbd{margin-left:auto}.hm-sidebar.collapsed .connection-card,.hm-sidebar.collapsed .shortcut-hint{justify-content:center;padding-inline:0}.hm-main{flex:1;min-width:0;height:100vh;max-height:none;overflow-y:auto;background:transparent}.hm-topbar{position:sticky;top:0;display:flex;align-items:center;z-index:25;height:var(--hm-topbar-h);padding:0 28px;gap:22px;background:#07090de0;border-bottom:1px solid var(--hm-border-subtle);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.topbar-context{display:flex;flex-direction:column;min-width:140px}.topbar-kicker{margin-bottom:2px;color:var(--hm-accent);font-size:.57rem;line-height:1;text-transform:uppercase;letter-spacing:.16em;font-weight:700}.topbar-title-row{display:flex;align-items:center;gap:10px}.topbar-title-row h1{margin:0;color:#f3f4f7;font-size:1rem;line-height:1.25;font-weight:650;letter-spacing:-.018em}.topbar-description{margin:0;padding-left:22px;border-left:1px solid var(--hm-border);color:var(--hm-text-dim);font-size:.72rem}.topbar-actions{margin-left:auto;display:flex;align-items:center;gap:8px}.uptime-label{margin-right:5px;color:var(--hm-text-dim);font-family:var(--hm-font-mono);font-size:.62rem}.status-pill{display:inline-flex;align-items:center;gap:5px;color:var(--hm-text-muted);font-size:.58rem;text-transform:capitalize;font-weight:600}.status-pill .status-dot{width:6px;height:6px;box-shadow:none}.command-trigger{min-height:34px;padding:0 8px 0 10px;display:flex;align-items:center;gap:7px;color:var(--hm-text-muted);background:#ffffff06;border:1px solid var(--hm-border);border-radius:7px;font-size:.68rem;cursor:pointer}.command-trigger:hover{color:var(--hm-text);border-color:#3a4352;background:#ffffff0b}kbd{padding:2px 5px;color:#818b99;background:#090c11;border:1px solid #2b3340;border-radius:4px;font-family:var(--hm-font-sans);font-size:.57rem;box-shadow:inset 0 -1px #ffffff09}.page-viewport{min-height:calc(100vh - var(--hm-topbar-h))}.icon-btn.mobile-menu-btn,.mobile-scrim{display:none}.icon-btn{width:34px;height:34px;padding:0;display:inline-grid;place-items:center;flex:none;color:var(--hm-text-muted);background:transparent;border:1px solid transparent;border-radius:7px;cursor:pointer;transition:color .15s ease,border-color .15s ease,background .15s ease}.icon-btn:hover{color:var(--hm-text);background:#ffffff0b;border-color:var(--hm-border)}.icon-btn-danger{color:#d67a7a}.icon-btn-danger:hover{color:#f08a8a;background:#e064641a;border-color:#e064643d}.section-shell{min-height:calc(100vh - var(--hm-topbar-h))}.section-tabs-wrap{position:sticky;top:var(--hm-topbar-h);z-index:20;padding:0 24px;background:#090c11eb;border-bottom:var(--hm-section-tabs-border) solid var(--hm-border-subtle);-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.section-tabs{display:flex;align-items:stretch;gap:2px;min-height:var(--hm-section-tabs-inner-h);overflow-x:auto}.section-tab{position:relative;padding:0 13px;white-space:nowrap;color:#7f8998;background:transparent;border:0;font-size:.72rem;font-weight:550;cursor:pointer}.section-tab:hover{color:#c8ced7}.section-tab.active{color:#e9ca8e}.section-tab.active:after{content:"";position:absolute;left:13px;right:13px;bottom:0;height:2px;background:var(--hm-accent);border-radius:2px 2px 0 0;box-shadow:0 -3px 12px #c58b3233}.section-panel>*{min-height:100%}.hm-card{background:linear-gradient(150deg,#121720f5,#0e1219fa);border-color:var(--hm-border);box-shadow:var(--hm-shadow-sm)}.hm-card:hover{border-color:#303847}.hm-table th{background:#07090d4d;color:#7e8898;font-size:.64rem;letter-spacing:.075em;font-weight:650}.hm-table td{border-bottom-color:var(--hm-border-subtle)}.hm-table tr:hover td{background:#ffffff05}.btn{min-height:34px;padding:7px 12px;border:1px solid transparent;font-weight:600}.btn-primary{color:#171006;background:linear-gradient(180deg,#d4a14e,#b77b29);border-color:#dcaa57;box-shadow:inset 0 1px #ffffff21,0 5px 14px #83521324}.btn-primary:hover{background:linear-gradient(180deg,#e2b35f,#c88a31);box-shadow:0 7px 18px #83521333}.btn-ghost{border-color:var(--hm-border);background:#ffffff04}.btn-ghost:hover{border-color:#394251;background:#ffffff0a}.hm-input{min-height:38px;background:#090c11;border-color:#2a3240}.hm-input:hover{border-color:#36404f}.hm-input:focus{border-color:var(--hm-accent);box-shadow:0 0 0 3px #c58b321a}.login-shell{min-height:100vh;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at 50% 20%,rgba(197,139,50,.09),transparent 30rem)}.login-panel{width:100%;max-width:380px;padding:34px;background:linear-gradient(150deg,#131821f7,#0b0e14fc);border:1px solid var(--hm-border);border-radius:14px;box-shadow:var(--hm-shadow-lg)}.login-brand{width:48px;height:48px;margin-bottom:26px;display:grid;place-items:center;color:var(--hm-accent-hover);background:var(--hm-accent-dim);border:1px solid rgba(197,139,50,.3);border-radius:11px}.login-eyebrow{margin:0 0 7px;color:var(--hm-accent);font-size:.63rem;font-weight:700;letter-spacing:.15em;text-transform:uppercase}.login-title{margin:0;color:#f2f3f6;font-size:1.65rem;line-height:1.2;font-weight:650;letter-spacing:-.035em}.login-subtitle{margin:7px 0 25px;color:var(--hm-text-muted);font-size:.8rem}.modal-overlay{background:#030508c2;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}.modal-content,.palette{background:linear-gradient(155deg,#171c25,#0f131a);border-color:#303847;box-shadow:var(--hm-shadow-lg)}.confirm-heading{display:flex;gap:13px;margin-bottom:22px}.confirm-heading h3{margin:2px 0 6px;font-size:.92rem;font-weight:650}.confirm-heading p{margin:0;color:var(--hm-text-muted);font-size:.76rem;line-height:1.55}.confirm-icon{width:38px;height:38px;display:grid;place-items:center;flex:none;color:var(--hm-info);background:#668fd71a;border:1px solid rgba(102,143,215,.22);border-radius:9px}.confirm-icon.danger{color:var(--hm-danger);background:#e064641a;border-color:#e0646438}.toast-item{min-width:260px;padding:11px 13px;background:#141923;border-color:#2b3442;border-left-width:3px;color:var(--hm-text)}.toast-success{border-left-color:var(--hm-success)}.toast-error{border-left-color:var(--hm-danger)}.toast-info{border-left-color:var(--hm-info)}.toast-success,.toast-error,.toast-info{background:#141923;color:var(--hm-text)}.toast-success .toast-icon{color:var(--hm-success)}.toast-error .toast-icon{color:var(--hm-danger)}.toast-info .toast-icon{color:var(--hm-info)}.palette-overlay{padding-top:12vh}.palette{max-width:570px}.palette-search{display:flex;align-items:center;gap:10px;padding-left:17px;color:var(--hm-text-dim);border-bottom:1px solid var(--hm-border)}.palette-input{border:0;padding:17px 17px 17px 0;font-size:.82rem}.palette-results{max-height:390px;padding:8px}.palette-item{min-height:48px;gap:11px;padding:6px 10px}.palette-item.selected{background:linear-gradient(90deg,#c58b321f,#c58b3209)}.palette-icon{width:30px;height:30px;display:grid;place-items:center;color:#8994a4;background:#ffffff09;border:1px solid var(--hm-border-subtle);border-radius:7px}.palette-item.selected .palette-icon{color:var(--hm-accent-hover);border-color:#c58b3240}.palette-copy{display:flex;flex-direction:column;gap:2px}.palette-label{color:#dfe3e9;font-size:.76rem;font-weight:550}.palette-group{color:var(--hm-text-dim);font-size:.61rem}.palette-arrow{margin-left:auto;color:#4e5867}.palette-footer{justify-content:flex-end;gap:16px;padding:10px 14px}.palette-footer span{display:flex;align-items:center;gap:5px}.error-icon,.empty-state-icon{display:grid;place-items:center;color:var(--hm-text-dim)}.empty-state{min-height:220px;border-style:dashed}.empty-state-icon{width:44px;height:44px;font-size:initial;background:#ffffff06;border:1px solid var(--hm-border-subtle);border-radius:10px}@media(max-width:900px){.hm-sidebar{position:fixed;left:0;top:0;transform:translate(-102%);width:min(280px,86vw);min-width:min(280px,86vw);box-shadow:20px 0 60px #00000080}.hm-sidebar.mobile-open{transform:translate(0)}.hm-sidebar.collapsed{width:min(280px,86vw);min-width:min(280px,86vw)}.hm-sidebar.collapsed .sidebar-brand-copy,.hm-sidebar.collapsed .nav-label,.hm-sidebar.collapsed .nav-section-label,.hm-sidebar.collapsed .connection-copy,.hm-sidebar.collapsed .shortcut-hint span,.hm-sidebar.collapsed .shortcut-hint kbd{display:flex}.hm-sidebar.collapsed .sidebar-brand{justify-content:flex-start}.hm-sidebar.collapsed .brand-mark{display:grid}.hm-sidebar.collapsed .nav-item{padding:0 10px;justify-content:flex-start}.mobile-scrim{display:block;position:fixed;top:0;right:0;bottom:0;left:0;z-index:35;background:#0000009e;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.icon-btn.mobile-menu-btn{display:inline-grid}.hm-sidebar .sidebar-toggle-btn{display:none}.hm-topbar{padding:0 16px;gap:12px}.topbar-description,.uptime-label,.command-trigger span{display:none}.command-trigger{width:34px;padding:0;justify-content:center}.command-trigger kbd{display:none}}@media(max-width:640px){:root{--hm-topbar-h: 64px}.section-tabs-wrap{padding:0 12px}.topbar-kicker{display:none}.topbar-title-row h1{font-size:.9rem}.status-pill{display:none}.section-tab{padding-inline:10px}.section-panel .p-6{padding:1rem}.login-panel{padding:26px}.toast-stack{left:12px;right:12px;bottom:12px}.toast-item{width:100%;max-width:none}}@media(prefers-reduced-motion:reduce){.brand-loader{animation:none}}.page-viewport .page-fade-in{width:100%;max-width:1600px;margin-inline:0}.page-viewport .p-6{padding:clamp(1rem,2vw,1.75rem)}.page-viewport h1.text-xl{color:#f2f3f6;font-size:1.12rem;line-height:1.3;letter-spacing:-.025em;font-weight:650}.page-viewport h2,.page-viewport h3{letter-spacing:-.012em}.page-lede{margin-top:4px;max-width:64ch;color:var(--hm-text-dim);font-size:.72rem;line-height:1.55}.form-panel{position:relative;overflow:hidden;border-color:#c58b3238;background:linear-gradient(145deg,#c58b320b,#0d1118fa 35%)}.form-panel:before{content:"";position:absolute;inset:0 auto 0 0;width:2px;background:linear-gradient(180deg,var(--hm-accent),transparent 75%)}.provider-choice-list{display:grid;gap:8px}.provider-choice{padding:11px 12px;border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-md);background:#0000001f}.provider-choice:has(input:checked){border-color:#c58b324d;background:var(--hm-accent-dim)}.provider-choice-label{display:flex;align-items:center;gap:9px;cursor:pointer}.provider-control{accent-color:var(--hm-accent)}.hm-card{padding:1rem;border-radius:var(--hm-radius-lg)}.hm-card>h2:first-child,.hm-card>h3:first-child{color:#e5e8ee}.table-responsive{border:1px solid var(--hm-border-subtle);border-radius:var(--hm-radius-lg);background:#06080c3d}.hm-table{width:100%;border-collapse:collapse}.hm-table th{height:38px;padding:0 12px;white-space:nowrap}.hm-table td{padding:10px 12px;vertical-align:middle}.hm-table tbody tr:last-child td{border-bottom:0}.hm-select,select.hm-input{min-height:38px;background-color:#090c11;border-color:#2a3240}textarea.hm-input{min-height:90px;line-height:1.55}label{accent-color:var(--hm-accent)}input[type=checkbox]{width:15px;height:15px;accent-color:var(--hm-accent);border-radius:4px;cursor:pointer}input[type=checkbox]:focus-visible{outline:2px solid var(--hm-accent);outline-offset:3px}input[type=checkbox]:disabled{cursor:not-allowed;opacity:.45}.btn-danger{color:#f6b2b2;background:#e0646414;border-color:#e0646452}.btn-danger:hover{color:#ffd0d0;background:#e0646429;border-color:#e0646480}.btn:focus-visible,.icon-btn:focus-visible,.nav-item:focus-visible,.section-tab:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}.modal-content{width:min(92vw,520px);padding:22px;border-radius:var(--hm-radius-xl)}.modal-content h2,.modal-content h3{color:#f0f2f5}.modal-content .btn{min-width:76px}.error-state{min-height:180px;border-color:#e0646442!important;background:linear-gradient(150deg,#34141852,#0e1219fa)}.error-icon{width:42px;height:42px;border-radius:10px;background:#e0646417;border:1px solid rgba(224,100,100,.2)}.sess-preset-icon,.tl-group-icon,.cfg-group-icon,.chat-tool-icon{display:inline-grid;place-items:center;flex:none}.sk-action-btn{display:inline-grid;place-items:center}.sk-card-icon{display:inline-grid;color:var(--hm-accent-hover)}.health-card-icon{display:inline-grid}.provider-status{display:inline-flex;align-items:center;gap:6px}.provider-status .status-dot{width:7px;height:7px;box-shadow:none}.row-expander{width:28px;height:28px;display:inline-grid;place-items:center;border:0;border-radius:6px;color:var(--hm-text-dim);background:transparent;cursor:pointer}.row-expander:hover{color:var(--hm-text);background:#ffffff0b}.row-expander:focus-visible{outline:2px solid var(--hm-accent);outline-offset:2px}.tool-expand-icon .odin-icon,.chat-tools-toggle-icon .odin-icon,.cfg-group-arrow .odin-icon,.sess-expand-icon .odin-icon,.mem-tree-arrow .odin-icon,.kb-tree-arrow .odin-icon{transition:transform var(--hm-transition-base)}.chat-markdown .chat-code-copy{opacity:0}.chat-markdown pre:hover .chat-code-copy,.chat-markdown .chat-code-copy:focus-visible{opacity:1}.fts-highlight,.knowledge-highlight{background:#e0b7663d;color:#fff0c8;border-radius:2px;padding-inline:1px}@media(max-width:640px){.page-viewport .p-6,.hm-card{padding:.85rem}.hm-table th,.hm-table td{padding-inline:9px}.modal-content{padding:18px}} diff --git a/ui/dist/index.html b/ui/dist/index.html index 0e102f8f..89e59e2a 100644 --- a/ui/dist/index.html +++ b/ui/dist/index.html @@ -9,8 +9,8 @@ Odin — Management - - + +
    Skip to main content diff --git a/ui/js/llm-config-payloads.js b/ui/js/llm-config-payloads.js index ec82a81f..a8daf6ad 100644 --- a/ui/js/llm-config-payloads.js +++ b/ui/js/llm-config-payloads.js @@ -12,6 +12,8 @@ const CODEX_ADVANCED_FIELDS = Object.freeze([ 'retry', 'connection_pool', 'context_compression', + 'context_budget_overrides', + 'context_utilization', ]); const OLLAMA_BASIC_FIELDS = Object.freeze([ diff --git a/ui/js/pages/llm-config.js b/ui/js/pages/llm-config.js index 7f039777..895a7eef 100644 --- a/ui/js/pages/llm-config.js +++ b/ui/js/pages/llm-config.js @@ -171,6 +171,12 @@ export default { +
    + Effective context + {{ formatCount(activeContextBudget?.effective?.effective_budget) }} tokens + {{ activeContextBudget?.provenance || 'unavailable' }} + Expires {{ formatExpiry(activeContextBudget.clamp_expires_at) }} +

    The Auxiliary Model runs the background jobs (compaction, reflection, consolidation, @@ -185,7 +191,7 @@ export default {

    Advanced Settings - Transport, retries, connection pool, and context compression + Transport, retries, and model-aware context policy
    @@ -228,7 +234,7 @@ export default {
    Context compressionLong-conversation compaction

    - Saved values need a restart. This process still uses compression {{ llmStatus.codex.effective_context_compression?.enabled ? 'on' : 'off' }}, {{ Number(llmStatus.codex.effective_context_compression?.max_context_chars || 0).toLocaleString() }} characters, and {{ llmStatus.codex.effective_context_compression?.keep_recent_iterations }} recent iterations. + Saved values need a restart. This process still uses compression {{ llmStatus.codex.effective_context_compression?.enabled ? 'on' : 'off' }}, {{ formatContextCeiling(llmStatus.codex.effective_context_compression?.max_context_chars) }}, and {{ llmStatus.codex.effective_context_compression?.keep_recent_iterations }} recent iterations.

    Saved values match this process. Future changes take effect after restart. @@ -244,8 +250,90 @@ export default {

    +
    +
    +
    + Context budgets + Capability, working-set policy, and temporary evidence +
    + +
    +

    + Overrides describe usable input capability. Utilization is the working-set policy applied to larger models; budgets at or below 272,000 tokens keep legacy behavior. Learned clamps are temporary evidence from successful overflow recovery, not operator policy. +

    +
    + Loading context budgets… +
    +
    + {{ contextWindowsError }} + +
    + +
    -

    Transport and retry changes apply to the primary client now. An existing auxiliary client keeps the transport and retry settings captured when it was built until it is rebuilt. The primary client’s connection pool and context compression are saved for the next restart.

    +

    Transport and retry changes apply to the primary client now. Context budgets and utilization apply to the next logical generation. An existing auxiliary client keeps the transport and retry settings captured when it was built until it is rebuilt. Connection-pool and context-compression changes are saved for the next restart.

    @@ -507,11 +595,12 @@ export default { // (the server normalizes ''/null to inherit; distinct from the literal // effort "none") const codexForm = ref({ - enabled: false, model: 'gpt-5.5', reasoning_effort: 'medium', agent_reasoning_effort: '', agent_model: '', + enabled: false, model: 'gpt-5.6-sol', reasoning_effort: 'xhigh', agent_reasoning_effort: 'auto', agent_model: '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: true, max_context_chars: 750000, keep_recent_iterations: 30 }, + context_compression: { enabled: true, max_context_chars: null, keep_recent_iterations: 30 }, + context_budget_overrides: {}, context_utilization: 60, }); // Codex model catalog — ONE ordered list renders the Model, Agent Model, @@ -586,6 +675,24 @@ export default { } const savingAux = ref(false); const advancedOpen = ref({ codex: false, ollama: false, kimi: false }); + const contextWindows = ref(null); + const contextWindowsLoading = ref(false); + const contextWindowsError = ref(''); + const clearingClamp = ref(null); + const contextPolicyDirty = ref(false); + let contextWindowsRequestSeq = 0; + const contextBudgetRows = computed(() => Object.entries(contextWindows.value?.models || {}).map(([model, details]) => ({ + model, + floor: details.floor, + override: details.override, + effectiveBudget: details.effective?.effective_budget, + configuredPrimaryChars: details.configured?.primary_chars, + primaryChars: details.effective?.primary_chars, + provenance: details.provenance, + clampExpiresAt: details.clamp_expires_at, + }))); + const activeClampRows = computed(() => contextWindows.value?.clamps || []); + const activeContextBudget = computed(() => contextWindows.value?.models?.[codexForm.value.model] || null); const ollamaForm = ref({ enabled: false, base_url: '', model: '', api_key: '', max_tokens: 4096, timeout: 300 }); const kimiForm = ref({ enabled: false, api_key: '', model: '', max_tokens: 4096 , timeout: 300 }); const ollamaKeyDirty = ref(false); @@ -635,10 +742,60 @@ export default { return (bytes / (1024 * 1024)).toFixed(0) + ' MB'; } + function formatCount(value) { + return Number.isFinite(Number(value)) ? Number(value).toLocaleString() : '—'; + } + + function formatContextCeiling(value) { + return value == null + ? 'automatic (model-derived)' + : Number(value).toLocaleString() + ' characters'; + } + + function formatExpiry(value) { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? 'unknown' : date.toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }); + } + + function shortAccountKey(value) { + return typeof value === 'string' && value.length > 12 ? value.slice(0, 8) + '…' + value.slice(-4) : value; + } + + function provenanceClass(value) { + if (value === 'temporary learned clamp') return 'is-clamp'; + if (value === 'override') return 'is-override'; + return 'is-built-in'; + } + + function overrideAboveFloor(row) { + const value = codexForm.value.context_budget_overrides[row.model]; + return row.floor != null && Number.isFinite(Number(value)) && Number(value) > row.floor; + } + + function setContextOverride(model, event) { + const next = { ...codexForm.value.context_budget_overrides }; + if (event.target.value === '') delete next[model]; + else next[model] = Number(event.target.value); + codexForm.value.context_budget_overrides = next; + contextPolicyDirty.value = true; + } + + function setContextUtilization(event) { + codexForm.value.context_utilization = event.target.value === '' ? '' : Number(event.target.value); + contextPolicyDirty.value = true; + } + + function resetContextOverride(model) { + const next = { ...codexForm.value.context_budget_overrides }; + delete next[model]; + codexForm.value.context_budget_overrides = next; + contextPolicyDirty.value = true; + } + // --- Fetch all --- async function fetchAll() { loading.value = true; - await Promise.all([fetchLLMStatus(), fetchOllamaStatus(), fetchKimiStatus(), fetchCodexStatus()]); + await Promise.all([fetchLLMStatus(), fetchOllamaStatus(), fetchKimiStatus(), fetchCodexStatus(), fetchContextWindows()]); loading.value = false; } @@ -652,7 +809,7 @@ export default { if (data.codex && !saveCodexConfigDebounced.pending()) { if (!preserveBasic) { codexForm.value.enabled = data.codex.enabled; - codexForm.value.model = data.codex.model || 'gpt-5.5'; + codexForm.value.model = data.codex.model || 'gpt-5.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 || ''; @@ -664,6 +821,10 @@ export default { codexForm.value.retry = { ...codexForm.value.retry, ...(data.codex.retry || {}) }; codexForm.value.connection_pool = { ...codexForm.value.connection_pool, ...(data.codex.connection_pool || {}) }; codexForm.value.context_compression = { ...codexForm.value.context_compression, ...(data.codex.context_compression || {}) }; + if (!contextPolicyDirty.value && !savingCodex.value) { + codexForm.value.context_budget_overrides = { ...(data.codex.context_budget_overrides || {}) }; + codexForm.value.context_utilization = data.codex.context_utilization ?? codexForm.value.context_utilization; + } } } if (data.ollama && !saveOllamaConfigDebounced.pending()) { @@ -696,6 +857,32 @@ export default { } } + async function fetchContextWindows() { + const requestSeq = ++contextWindowsRequestSeq; + contextWindowsLoading.value = true; + contextWindowsError.value = ''; + try { + const data = await api.get('/api/context/windows'); + if (requestSeq !== contextWindowsRequestSeq) return; + contextWindows.value = data; + // GET is the derivation authority. Hydrate the editable Advanced + // fields only when no provider save is in flight; rows always render + // server truth and never recompute targets in the browser. + if (!savingCodex.value && !contextPolicyDirty.value) { + codexForm.value.context_budget_overrides = Object.fromEntries( + Object.entries(data.models || {}).filter(([, details]) => details.override != null).map(([model, details]) => [model, details.override]) + ); + codexForm.value.context_utilization = data.utilization ?? codexForm.value.context_utilization; + } + } catch (e) { + if (requestSeq === contextWindowsRequestSeq) { + contextWindowsError.value = e.message || 'Failed to load context budgets'; + } + } finally { + if (requestSeq === contextWindowsRequestSeq) contextWindowsLoading.value = false; + } + } + async function fetchOllamaStatus() { try { ollamaStatus.value = await api.get('/api/ollama/status'); @@ -848,12 +1035,20 @@ export default { const submitted = codexAdvancedPayload(codexForm.value); try { await api.put('/api/llm/codex/config', submitted); + const policyUnchanged = JSON.stringify({ + context_budget_overrides: codexForm.value.context_budget_overrides, + context_utilization: codexForm.value.context_utilization, + }) === JSON.stringify({ + context_budget_overrides: submitted.context_budget_overrides, + context_utilization: submitted.context_utilization, + }); + if (policyUnchanged) contextPolicyDirty.value = false; showToast('Codex advanced settings saved'); - await Promise.all([fetchLLMStatus({ preserveBasic: true, preserveAdvanced: true }), fetchCodexStatus()]); + await Promise.all([fetchLLMStatus({ preserveBasic: true, preserveAdvanced: true }), fetchCodexStatus(), fetchContextWindows()]); } catch (e) { showToast(e.message || 'Failed', 'error'); const changedWhileSaving = JSON.stringify(codexAdvancedPayload(codexForm.value)) !== JSON.stringify(submitted); - await Promise.all([fetchLLMStatus({ preserveBasic: true, preserveAdvanced: changedWhileSaving }), fetchCodexStatus()]); + await Promise.all([fetchLLMStatus({ preserveBasic: true, preserveAdvanced: changedWhileSaving }), fetchCodexStatus(), fetchContextWindows()]); } finally { savingCodex.value = false; } } @@ -945,6 +1140,21 @@ export default { const saveOllamaAdvancedConfigNow = () => saveOllamaAdvancedConfig(); const saveKimiAdvancedConfigNow = () => saveKimiAdvancedConfig(); + async function clearContextClamp(clamp) { + const key = clamp.account_key + ':' + clamp.model; + clearingClamp.value = key; + try { + const result = await api.post('/api/context/windows/clear', { account_key: clamp.account_key, model: clamp.model }); + showToast(result.cleared ? 'Temporary clamp cleared' : 'Clamp was already inactive'); + await fetchContextWindows(); + } catch (e) { + showToast(e.message || 'Failed to clear clamp', 'error'); + await fetchContextWindows(); + } finally { + clearingClamp.value = null; + } + } + // --- Codex account management --- async function activateAccount(index) { try { @@ -1048,6 +1258,7 @@ export default { ollamaStatus, ollamaModels, ollamaSelectedModel, reloading, settingModel, kimiStatus, kimiModels, kimiSelectedModel, reloadingKimi, settingKimiModel, codexLoading, codexError, codexData, refreshing, editingLabel, labelValue, + contextWindows, contextWindowsLoading, contextWindowsError, contextBudgetRows, activeClampRows, activeContextBudget, clearingClamp, contextPolicyDirty, deviceState, deviceLoading, deviceInfo, deviceResult, deviceError, fetchAll, switchProvider, reloadOllama, setOllamaModel, reloadKimi, setKimiModel, probeOllamaModels, @@ -1058,6 +1269,8 @@ export default { saveCodexAdvancedConfigNow, saveOllamaAdvancedConfigNow, saveKimiAdvancedConfigNow, activateAccount, refreshAccount, startEditLabel, saveLabel, deleteAccount, startDeviceLogin, cancelDeviceLogin, formatSize, + fetchContextWindows, clearContextClamp, setContextOverride, setContextUtilization, resetContextOverride, overrideAboveFloor, + formatCount, formatContextCeiling, formatExpiry, shortAccountKey, provenanceClass, }; }, };