Commit Graph

203 Commits

Author SHA1 Message Date
Scott Idem
0c1cf3989a feat: aider multi-provider credentials + test suite green (182/182)
aider_run multi-provider credentials (tools/aider.py):
- _resolve_credentials() — general credential resolver; replaces the previous
  OpenRouter-only injection; resolution priority: Anthropic model hint → explicit
  host_label → model prefix (openrouter/*, groq/*, deepseek/*, …) → OpenRouter
  default → Anthropic API key → any keyed cloud host → local/generic host
- _host_flags() — generates --api-key slug=key for known cloud providers (OpenRouter,
  OpenAI, Groq, Together, Fireworks, X.ai, DeepSeek, Mistral); generates
  --openai-api-base + --openai-api-key for generic/local hosts (Open WebUI, Ollama);
  appends /api suffix for openwebui host_type; auto-prefixes model with 'openai/'
  for generic endpoints when model has no / prefix
- Anthropic API keys from providers.anthropic.credentials (not a host entry)
- host_label param added to aider_run and FunctionDeclaration — pick a configured
  host by partial label match (e.g. 'OpenRouter', 'Local', 'scott-lt-i7-rtx')
- 16 unit tests for _resolve_credentials covering all resolution paths

main.py: move @app.get("/health") before app.include_router(ui.router) — the
/{username} catch-all in ui.router was swallowing the /health path

Test suite: 37 pre-existing failures → 182/182 passing
- test_tools.py: _task_list() missing priority arg (6 callsites); cron ID regex
  c_\w+ → c_[\w-]+ (token_urlsafe includes '-', causing intermittent truncation)
- test_webhooks.py: rewritten for per-user channel config architecture —
  patch routers.nextcloud_talk/google_chat.get_user_channels instead of removed
  settings fields; corrected endpoints /webhook/nextcloud/scott and
  /channels/google-chat/scott; non-empty cfg dicts so falsy-guard passes
- test_health.py: test_unknown_route_404 now uses 3-segment path (/{u}/{p}/x)
  since single-segment paths hit the /{username} UI catch-all
- test_api_files.py: removed '../config.py' from not-in-allowed test (ASGI
  normalizes it to /config.py which hits /{username} catch-all, not files router)
- test_security.py: same webhook patch target fix; per-user endpoint URLs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 23:00:45 -04:00
Scott Idem
658c508925 feat: multi-level agent management — background agents, lifecycle tools, 3-level hierarchy
agent_manager.py (new):
- AgentRecord dataclass: agent_id, level (1/2/3), role, task, status, started,
  parent_id (lineage), finished, result, notify, _task_ref
- register() / finish() / cancel_agent() / list_agents() / get() / set_task_ref()
- Calls notification.notify() on completion when notify=True (same channel as
  reminders and cron completions)
- 24-hour pruning of completed records on each new registration

spawn_agent (tools/agents.py):
- background=True: fires asyncio.create_task(), registers in agent_manager, returns
  agent_id string immediately — sync path unchanged (no regression)
- notify=True: push/Talk notification when the background task completes
- Level enforcement: _agent_level param tracks hierarchy depth; when spawning from
  Level 2, child automatically gets spawn_agent + aider_run denied so Level 3 agents
  cannot delegate further

New lifecycle tools (tools/agents.py + __init__.py):
- agent_status(agent_id) — status, role, level, elapsed, task, result preview; user-level
- agent_list(status, limit) — all agents for current user, newest first; user-level
- agent_cancel(agent_id) — kills background task; admin-only, confirm-required

tests/test_agent_manager.py (new, 41 tests):
- agent_manager CRUD, pruning, notification hook
- spawn_agent background: returns immediately, completes async, timeout, failure
- Level enforcement: L1→L2 permits spawn, L2→L3 auto-denies; explicit tool_list path
- agent_status / agent_list / agent_cancel output formatting
- aider_run background: returns agent_id, completes async, sync path unchanged
- All tests run without browser or Cortex service (~2.5s total)
  Run: cd cortex && .venv/bin/python -m pytest tests/test_agent_manager.py -v

Docs: ARCH__FUTURE.md §13 (full design), ROADMAP.md, TODO__Agents.md, MASTER.md,
HELP.md (orchestrator description corrected, tool schema line updated to reflect
keyword routing), CLAUDE.md tool count 66→69.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 22:40:20 -04:00
Scott Idem
29d8aa4aae feat: tool schema optimization, keyword routing, aider_run coding agent
Tool schema optimization (PLAN__Tool_Schema_Optimization.md Phases 1-3):
- model_registry.py: ROLE_DEFAULT_TOOLS — distill gets [], research/coder get
  narrow tool lists by default; applied in get_role_config() when user hasn't
  configured a custom list
- openai_orchestrator.py: keyword routing via narrow_tools_by_keywords() — scans
  user message + last assistant turn; narrows active schemas to matched categories
  only (e.g. "weather" → 3 web tools instead of 69); zero tools sent for pure chat
- openai_orchestrator.py: _get_cached_tools() — module-level schema cache keyed by
  (role, sorted_tool_list, risk_params); eliminates redundant schema rebuilds
- openai_orchestrator.py: _TOOL_SCHEMA_OVERHEAD 3000 → 500 tokens (schemas now
  excluded from the per-call fixed estimate since they're cached separately)
- tools/__init__.py: CATEGORY_TOOL_MAP + _KEYWORD_CATEGORY_MAP + classify_tool_categories()
  + narrow_tools_by_keywords() — the classifier logic lives here so both orchestrators
  can share it

aider_run tool (cortex/tools/aider.py):
- Invokes Aider as a subprocess with --message --yes-always --no-pretty --no-stream
- Project aliases: cortex / aether_api / aether_frontend / aether_container
- Auto-injects OpenRouter API key from Cortex model registry (no ~/.env needed)
- background=True fires async + registers in agent_manager; notify=True sends push
  notification on completion
- admin-only, confirm-required, TOOL_RISK=high
- .gitignore: added .aider.chat.history.md / .aider.input.history / .aider.llm.history

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 22:39:44 -04:00
Scott Idem
29940c299b docs: fix Chat and Tools section accuracy in HELP.md
- Copy button appears on all messages (user + assistant), not just assistant
- Delete requires a confirm step — clarified in description
- Model tag position is below-left for assistant messages, not bottom-right
- Tool calls render as expandable per-call cards, not a single  N summary note
- help.html: default-open sections updated to Getting Started / Chat / Sessions / Model Registry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 21:36:24 -04:00
Scott Idem
105ff8507f fix: restore proper tab button CSS in help.html (browser-reset issue with Tailwind preflight: false)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 21:19:08 -04:00
Scott Idem
c2a12a895a docs: rename Backends → Switching Models in HELP.md; fix stale Backup 2 reference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 21:14:55 -04:00
Scott Idem
df1f358912 docs: fix role slots description in HELP.md (Primary + Backup 1, not Backup 2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 21:11:03 -04:00
Scott Idem
7a27190ffe feat: custom roles, Tailwind settings pages, pg.css fixes, doc cleanup
Model Registry:
- Add per-user custom roles (add/remove via UI); required roles chat/orchestrator/distill
  are always present and cannot be removed
- Auto-migrate legacy .env-defined roles to custom_roles on first access
- Role config panel (gear): Remove role button moved inside panel; required badge below name
- Role select: Primary + Backup slots only (was three)

Settings pages — Tailwind CSS migration (CDN, preflight: false):
- local_llm.html, settings.html, help.html, notifications.html, tools_settings.html,
  crons.html, integrations.html all migrated; pg-* color tokens; dark/light via data-theme

pg.css fixes:
- input[type=checkbox/radio]: width: auto — prevents pg.css width:100% from stretching checkboxes
- btn-submit: responsive sizing via Tailwind w-full md:w-96 on each button (no longer full-width on desktop)

Documentation:
- MASTER.md, TODO__Agents.md: remove "/ Inara" from titles; description updated to "per-user AI personas"
- HELP.md: persona-agnostic language throughout (NC Talk, Google Chat, push, schedules, HA sections);
  roles section restructured to show required vs. custom roles with examples
- notifications.html: subtitle and HA description use "your persona" not "Inara"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 21:03:11 -04:00
Scott Idem
070f1ce156 fix: align settings/models nav and heading with all other settings pages
local_llm.html had a stale nav (hardcoded / and /help, missing Notifications /
Tools / Schedules / Integrations links) and used a different heading markup
(.page-header) than the rest of the settings pages (.page-title/.page-subtitle).

- Add pg.css link; strip duplicate base CSS (vars, reset, body, nav) from inline
  <style> — page-specific styles (provider blocks, model rows, roles, badges) kept
- Nav: use {{ back_href }} / {{ help_href }} / {{ integrations_nav }} placeholders;
  add Notifications, Tools, Schedules, Integrations (admin) links
- Heading: .page-header → <h1 class="page-title"> + <p class="page-subtitle">
- Body structure: move <nav> outside the .page wrapper; rename .page → .page-wrap
- local_llm.py: _render() accepts request, injects back_href / help_href /
  integrations_nav; adds _preferred_persona(), _integrations_nav() helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:51:06 -04:00
Scott Idem
a92fd90f0d feat: Anthropic SDK backend — API key alternative to Claude CLI OAuth
Adds `anthropic_api` model type so users can authenticate with a direct
Anthropic API key instead of (or alongside) the CLI OAuth session.

- model_registry.py: `anthropic_api` type; `save/get/remove_anthropic_api_key()`
  mirroring the Google account pattern; `save_cloud_model()` now picks type
  based on credential type (cli → claude_cli, api_key → anthropic_api);
  `_resolve_model()` merges api_key from the credential entry
- llm_client.py: `_anthropic_api()` backend (AsyncAnthropic SDK); dispatch
  and fallback wiring; usage tracking
- routers/local_llm.py: Anthropic API key management routes
  (POST /settings/local/anthropic-key, /anthropic-key/{id}/remove);
  `anthropic_api` badge and edit-form credential selector
- static/local_llm.html: Anthropic Cloud Provider block now shows API key
  management (add/remove); Add Model → Anthropic tab has credential selector
  (CLI vs API key)
- requirements.txt: enable anthropic>=0.40.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:30:56 -04:00
Scott Idem
70665fadff feat: schedules UI, task cron type, monthly/yearly schedules, AE DB tools, integrations page
- Schedules web UI (/settings/crons): list, add, edit, pause/resume, delete jobs
- cron task type: full orchestrator tool loop on a schedule, result → notification channel
- parse_schedule: monthly/yearly formats (monthly:DD:HH:MM, yearly:MM:DD:HH:MM)
- HA inbound webhook tools toggle: orchestrator loop vs. direct LLM, configurable in UI
- ae_db_query/describe/show_view: SELECT-only Aether MariaDB access (admin, per-user creds)
- /settings/integrations: admin-only page for Aether DB credentials
- Schedules nav link added to all settings pages
- pymysql added to requirements
- Docs updated: HELP.md, MASTER.md, CLAUDE.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 21:06:43 -04:00
Scott Idem
96b3c796c5 feat: file attachment support in chat (images + text/code files)
Text files (.md, .py, .js, .json, etc.): read client-side and injected
into the message body as a fenced code block — works with all backends
with zero model capability requirements.

Images (PNG/JPG/WebP/GIF, max 5 MB): encoded as base64 data URL on the
client and sent as a separate attachment field. Backend formats them as
OpenAI multimodal content (text + image_url) for local_openai backends.
Claude CLI and Gemini CLI see the text message with a "📎 filename.png"
note; image data is never written to session history.

- index.html: 📎 button + hidden file input in mode-select row;
  attachment-row preview area with thumbnail (images) or filename chip
- app.js: _resolveAttachment(), file reader, clearAttachment();
  sendMessage/sendOrchestrate updated to allow no-text sends when a
  file is pending; attachment spread into chat payload for images
- chat.py: Attachment model; attachment field on ChatRequest;
  llm_attachment extracted in _stream_chat and passed to complete()
- llm_client.py: attachment param through complete()/_dispatch()/_local();
  _local() builds multimodal content array for vision calls
- style.css: #attach-btn, #attachment-row, #attachment-preview, thumb

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:46:50 -04:00
Scott Idem
50c1997e91 docs: mark Phase 2/3 done; add file_diff, git tools, spawn_agent restrictions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:34:26 -04:00
Scott Idem
3716e5974f feat: Phase 3 model toggle — cycle chat-role slot models in UI
Replaces the role-cycle toggle with a slot model toggle in the Context &
Memory panel. The active model label is shown on the button; clicking cycles
through Primary → Backup 1 → Backup 2 slots configured for the Chat role.

- app.js: remove activeRole()/availableRoles role-cycling; add
  activeChatModel()/chatModels slot cycling; update send/orchestrate
  payloads to send slot + chat_role:"chat"; fix updateSendBtnTitle and
  startRunTimer to use activeChatModel()
- chat.py: add slot field to ChatRequest; pass slot= to complete();
  resolve backend_label from slot config; add _chat_slot_models() helper;
  include chat_models in GET /backend response
- HELP.md: update Model toggle description, tool count (62/16),
  Backends section, API chat payload example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:32:43 -04:00
Scott Idem
85e13314a2 feat: add confirm step to message-level delete
Clicking del now shows 'confirm delete / cancel' inline in the action
bar. Cancel rebuilds the original buttons; confirm proceeds as before.
Matches the session delete pattern added in the prior commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:20:13 -04:00
Scott Idem
20f3fe4f71 docs: mark completed items in TODO__Agents.md
task_list priority filter, session delete confirm, spawn_agent tool
restrictions, HA API tools/config/confirm-required, file_diff,
git_status/log/diff — all completed 2026-05-12.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:17:42 -04:00
Scott Idem
f336ae9687 feat: task_list priority filter, session delete confirm, spawn_agent tool restrictions
- task_list: add priority param ('low'/'normal'/'high') alongside existing status filter
- Session delete: inline confirm row (Delete / Cancel) instead of immediate delete
- spawn_agent: allow_tools and deny_tools per-call params; role config remains ceiling;
  deny_tools falls back to confirm_deny gate when no explicit tool_list is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:09:50 -04:00
Scott Idem
76fef827c5 docs: update tool count to 62 and current state to 2026-05-12
Reflects file_diff and git_status/log/diff additions, pg.css refactor,
and reasoning level controls added this session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 00:14:12 -04:00
Scott Idem
b7144d5903 feat: add git_status, git_log, git_diff orchestrator tools
Read-only wrappers around git commands, project-scoped. Covers working
tree status, commit history browsing (with optional path filter), and
diffs between refs or the working tree — cleaner than shell_exec for
code review and change verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 00:12:00 -04:00
Scott Idem
3c9b8f5909 feat: add file_diff orchestrator tool
Runs diff -u on two project-scoped files. Low risk, no admin required.
Covers code review, config comparison, and before/after verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 00:08:36 -04:00
Scott Idem
54eef73b74 docs: add spawn_agent per-invocation tool restriction design (ARCH__FUTURE §12)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 23:23:17 -04:00
Scott Idem
4c3d9a7a65 refactor: extract shared settings stylesheet (pg.css) and clean up inline styles
- Create cortex/static/pg.css with shared variables, nav, containers, form
  elements, button classes, text utilities, and feedback messages
- All four settings pages (settings, notifications, tools, help) now link to
  pg.css and have page-specific-only <style> blocks
- Style block line counts reduced: settings 244→70, notifications 189→42,
  tools 126→88, help 122→75
- Inline style= attributes reduced: settings 45→4, notifications 7→3,
  tools 12→4, help 0
- Introduced shared CSS classes: .btn-submit, .btn-save, .btn-cancel,
  .btn-secondary, .action-link, .hint, .section-note, .page-title,
  .page-subtitle, .page-wrap, .usage-table, .tool-cat-row
- Fix tools_settings.py: replace server-generated inline styles on category
  header rows with .tool-cat-row CSS class
- Fix settings.py: add btn-save/btn-cancel/persona-rename-cancel classes to
  server-rendered persona rename form buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 23:22:59 -04:00
Scott Idem
8ab1942514 refactor: migrate Tool Permissions from Settings to /settings/tools
- Remove Tool Permissions form from settings.html; replace with a
  "Tool Settings →" link that redirects to /settings/tools
- Add Confirmation Gate section to tools_settings.html (allow/deny
  textareas) inside the same form as risk policy — one save covers all
- tools_settings.py save handler now writes allow/deny alongside
  max_risk/whitelist/blacklist into tool_policy.json
- Remove /settings/tool-policy POST route from settings.py (no longer needed)
- Remove get_tool_policy, save_tool_policy, CONFIRM_REQUIRED imports
  from settings.py (now owned by tools_settings.py)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 22:50:48 -04:00
Scott Idem
69ec2f667d feat: tool risk policy UI + wiring through all orchestrators
- New /settings/tools page: max_risk selector (low/medium/high) + per-tool
  override dropdowns (Default / Force include / Force exclude) for all 58 tools
  grouped by category with color-coded risk badges; JS updates Auto status live
- get_tools_for_role() + get_openai_tools_for_role() now accept max_risk,
  whitelist, blacklist; _apply_risk_policy() handles the filtering logic
- get_risk_policy() helper in auth_utils reads from tool_policy.json
- Risk policy wired through orchestrator.py, openai_orchestrator.py,
  orchestrator_engine.py, nextcloud_talk.py, homeassistant.py
- Tools nav link added to settings.html and notifications.html
- CLAUDE.md and ARCH__SYSTEM.md updated: tool count 50→58, risk system docs,
  tool access control three-layer model documented

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 22:45:04 -04:00
Scott Idem
c9c1ca7de6 feat: TOOL_RISK ratings for all 58 orchestrator tools
Add informational security risk ratings (low/medium/high) to every tool.
Groundwork for future auto-allow tiers (max_risk + whitelist + blacklist).
Breakdown: 36 low, 12 medium, 10 high.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 22:32:22 -04:00
Scott Idem
ac06b3bc7b feat: project-scoped file tools — grep, stat, syntax_check, offset reads
Add five project-scoped tools (user-level, no admin required):
  project_file_read — read with 1-based offset for paging large files
  project_file_list — list with sizes + timestamps
  file_stat         — size, modified time, line count / entry count
  file_grep         — regex search with context lines, up to 50 matches
  file_syntax_check — py_compile (.py) or json.loads (.json)

Also add offset support to existing file_read (system scope).
Rename "Files" tool category to "System Files"; add "Project Files" category.
Project scope restricted to Cortex_and_Inara_dev/ project root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 22:23:50 -04:00
Scott Idem
fc6600c33e feat: Home Assistant API tools (ha_get_state, ha_get_states, ha_call_service)
Register three HA orchestrator tools so Inara can read device states and
control devices via the HA REST API. ha_call_service requires admin role
and user confirmation. Also includes accumulated UI fixes (setProcessing
helper, wasNewSession flag cleanup).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:39:35 -04:00
Scott Idem
ba91de37c5 feat: Home Assistant settings UI + fix channels.json
notifications.html: add Home Assistant section with two collapsible
blocks — Connection (HA URL + Long-Lived Access Token) and Inbound
webhook (webhook ID with endpoint URL hint showing the username).
Token field uses keep-existing pattern (blank = no change).

settings.py: wire ha_url, ha_token, ha_webhook_id through
_notifications_page() template substitution and save_notifications()
POST handler. Preserves existing HA config fields (persona, tier,
role, tools) on save.

TODO__Agents.md: add Home Assistant integration planning section
(event design, richer payload template, HA API tools).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:18:45 -04:00
Scott Idem
1d361fe809 feat: NCT orchestrator support + Home Assistant webhook
nextcloud_talk.py:
- Fix missing import hmac / import hashlib (NameError bug in _verify_signature)
- Add orchestrator routing when channels.json "tools": true — sends
  " Working on it…" immediately, then runs the full tool loop and
  replies with the result; checkpoint case gets a web UI confirmation note
- Read tier and role from channel config (defaults: default_tier / "chat")
- Pass cfg through to _process_message

homeassistant.py (new):
- POST /webhook/ha/{username}/{webhook_id}
- Auth: webhook_id path segment matched against channels.json
- Accepts JSON or form-encoded body from HA automations
- Builds natural-language task from payload (uses "message" key if present,
  otherwise serialises full body as context)
- Same orchestrator/direct dispatch as NCT
- Delivers response via notify() — NC Talk, web push, or configured channel
- Session key: ha_{username} for continuity across HA events
- Registered in main.py; /webhook/ prefix already public in auth_middleware

channels.json schema addition:
  "homeassistant": {
    "webhook_id": "your-secret-id",
    "persona": "inara",
    "tier": 2,
    "role": "chat",
    "tools": false
  }

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 19:45:59 -04:00
Scott Idem
19d6f004ed feat: reasoning level select (Off/Light/Moderate/High/Max)
Replace free-form reasoning_budget_tokens number input with a 5-level
select in both the edit form (local_llm.py) and add-model form
(local_llm.html). Values: 0 / 1024 / 4096 / 8192 / 32768 tokens.
Edit form pre-selects the stored value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 21:44:20 -04:00
Scott Idem
a66c5a7f84 feat: reasoning token budget + session name in header
- reasoning_budget_tokens: optional int field on local_openai models;
  when set, injects {"reasoning": {"budget_tokens": N}} via extra_body
  into every OpenRouter API call (both tool-loop and confirmation-gate
  rounds). Field exposed in the model edit form in Settings.

- session name moved from standalone full-row div between #messages
  and #input-area into the persona-switcher block in the header, as a
  third dim line under "Cortex · Local". Collapses when empty via
  :empty CSS. No JS changes required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 21:35:23 -04:00
Scott Idem
85792a7bcf feat: per-role inject_mode, OTR fixes, hover metadata, send/stop tooltip
- inject_mode: per-role toggle (parallel to inject_datetime) gates the
  "Current mode: Off The Record" line in the system prompt; wired through
  model_registry, context_loader, chat router, orchestrator router, and
  local_llm settings UI

- OTR orchestrator fix: OrchestrateRequest now carries off_record;
  _finalize_job stores it per message and gates log_turn on it; JS
  orchestrate payload sends off_record correctly

- Per-message hover metadata: removed always-visible .model-tag; replaced
  with .msg-meta strip in the action bar (hover-only); shows model label,
  host, fallback indicator, and OTR badge; stored in session JSON

- Send/stop button tooltip: shows role + model and (when tools on)
  separate orchestrator model + engine label; live elapsed timer on stop
  button via startRunTimer/stopRunTimer

- OrchestratorResult.backend_label: new field; openai_orchestrator fills
  it; finalize_job propagates it to job dict and session messages

- GET /backend: exposes orchestrator_model label so the frontend tooltip
  can show both models separately

- TODO: session delete confirmation added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 16:12:03 -04:00
Scott Idem
0afa135ce9 docs: document System block and OTR mode in ARCH__PERSONA.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 14:40:21 -04:00
Scott Idem
128d8a7c1e feat: inject session mode into persona system prompt
context_loader.load_context() now accepts a mode param ("chat"|"otr").
In OTR mode, the --- System --- block gains a second line:

  Current mode: Off The Record — this conversation is private
  and will not be logged or included in memory distillation

routers/chat.py passes mode="otr" when req.off_record is True.
Normal chat and all orchestrator calls stay at mode="chat" (no change
to the System block). The System block consolidates date/time and mode
in one place, matching the existing timestamp pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 14:35:09 -04:00
Scott Idem
3a4f518300 docs: add SELF_UPDATE.md — agent self-maintenance bootstrap
Short reference covering: git repo location, Syncthing fleet sync,
ignore files (.gitignore / .stignore), helper scripts (install.py,
dev-restart.sh, backup.sh), standard change workflow, doc update
checklist, pip dependency process, and key paths on the service host.
Linked from MASTER.md document map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 14:00:28 -04:00
Scott Idem
348ca120c1 feat: full channels.json UI + http_allowlist settings
Notifications page:
- NC Talk section expanded: url, bot_secret, notification_room,
  nc_username, nc_app_password — all fields from channels.json now editable
- Per-channel sections use <details>/<summary> collapsibles; auto-open
  when values are present
- Secrets use type=password with "leave blank to keep" semantics
- Google Chat outbound webhook in its own collapsible section

Account settings:
- HTTP POST Allowlist section added (same textarea pattern as email allowlist)
- POST /settings/http-allowlist route saves home/{user}/http_allowlist.json
- Example placeholder shows ha.dgrzone.com and n8n patterns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:57:18 -04:00
Scott Idem
7b443b40a4 feat: http_post tool, nc_talk_history tool, local orchestrator retry
- http_post: POST to external URLs with per-user URL prefix allowlist
  (home/{user}/http_allowlist.json); admin-only, confirm-required
- nc_talk_history: read recent NC Talk messages via Basic Auth (requires
  nc_username + nc_app_password in channels.json under nextcloud)
- openai_orchestrator: _chat_with_retry() wraps both API calls with
  exponential backoff (3 attempts, 1s/2s) on connection errors and
  transient status codes (429, 500, 502, 503, 504)
- Docs updated: CLAUDE.md, HELP.md, TODO, MASTER, ROADMAP (50 tools)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:38:38 -04:00
Scott Idem
b9a78819ac docs: add LLM wiki concept (Karpathy pattern) to ARCH__FUTURE.md
Inara's exploration of a living-wiki knowledge compilation architecture
as an alternative to RAG — three-layer model, ingest/query/lint ops,
and a mapping to existing Cortex concepts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:22:55 -04:00
Scott Idem
3672fa1506 docs: comprehensive doc audit — sync all docs to current state
- MASTER.md: tool count 40→47, add proactive notifications + spawn_agent rows, date bump
- ROADMAP.md: mark local orchestrator/web push/proactive notifs/spawn_agent/web_read/session_read as done, date bump
- ARCH__CHANNELS.md: rewrite notification channel config section — all 4 channels, all triggers, on-demand endpoints
- ARCH__SYSTEM.md: update tools/ module list to include files, agents
- README.md: update LLM backends in architecture diagram, add browser push to channels table
- CLAUDE.md: add doc update checklist to Documentation Philosophy section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:13:45 -04:00
Scott Idem
52c19afbcc fix: raise web_read and http_fetch max_chars cap to 128K
Both tools now accept max_chars up to 131072 to accommodate long
documentation pages and large API responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:08:17 -04:00
Scott Idem
17e8869d12 docs: update tool count (45→47), HELP.md, and TODO for new web/file tools
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:05:04 -04:00
Scott Idem
7c3291960a feat: web_read (trafilatura), session_read, http_fetch max_chars
web_read(url, max_chars=16000) — fetches a URL and extracts clean article
text via trafilatura, stripping ads/nav/boilerplate. Returns markdown.

session_read(date) — reads a full session log by YYYY-MM-DD date; lists
available dates if the requested one is not found.

http_fetch gains a max_chars param (default 8192, max 32768) so the cap
is configurable instead of hardcoded.

Tool count: 45 → 47.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:04:24 -04:00
Scott Idem
a99ebb8c30 feat: retry button for orchestrator errors + explicit client timeout
Extract orchestrator inner loop into _doOrchestrate() so the retry button
can re-run without re-adding the user message to DOM or history — same
pattern as the existing chat retry.

Also set AsyncOpenAI(timeout=settings.timeout_local) so slow remote models
(OpenRouter/DeepSeek) get the same 300s budget as local chat calls instead
of the SDK default which varies by connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:39:34 -04:00
Scott Idem
ff154b1ec0 docs: update CLAUDE.md, HELP.md, and TODO for notifications page + push fix
- CLAUDE.md: date → 2026-05-08, add Proactive notifications row to channel table
- HELP.md: update Notifications settings entry, expand Push Notifications section
  with channel config link, add test API endpoints to reference table
- TODO__Agents.md: mark notifications dedicated page and pywebpush fix as done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:58:47 -04:00
Scott Idem
c21f9a23ec fix: use Vapid.from_pem() instead of passing PEM string to webpush()
pywebpush 2.x routes string keys through Vapid.from_string() which only
handles raw/DER base64 — not PEM. Pre-build the Vapid object so the key
deserializes correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:56:17 -04:00
Scott Idem
19475610be feat: move Notifications to its own settings sub-page
Adds GET /settings/notifications (dedicated page with channel form + two
test buttons) and updates POST /settings/notifications to render that page.
Settings page now shows a compact link card instead of the full form.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:43:52 -04:00
Scott Idem
3c7ecf4e4f feat: notification test endpoints — POST /api/push/test and /api/push/reminders/check
- POST /api/push/test: sends "Test notification from Cortex" via the
  user's configured notification channel (web_push / NCT / email / etc.)
- POST /api/push/reminders/check: runs the daily reminder check immediately
  for the current user, returns reminders_found count

Both require an active session cookie. Useful for verifying channel setup
without waiting for the 09:00 scheduler job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:34:58 -04:00
Scott Idem
64020ad982 feat: proactive notifications — web_push channel + daily reminder check
Routes web_push through notification.py alongside NCT/email/Google Chat,
and fires daily reminder summaries via the scheduler.

- notification.py: _notify_web_push() + "web_push" case in notify();
  all four channels (web_push/email/nextcloud/google_chat) now routable
- scheduler.py: _run_reminder_check() daily at 09:00 — reads due reminders
  per persona via set_context(), formats up to 3 entries, calls notify()
- routers/settings.py: "web_push" added to valid notification_channel values
- static/settings.html: "Browser Push Notification" option in channel selector
- TODO__Agents.md: proactive notifications section marked complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:28:49 -04:00
Scott Idem
47d23a7b2f feat: per-model max_rounds for Gemini orchestrator engine
Mirrors the pattern already in openai_orchestrator.py. The Gemini engine
was still hardcoded to the global orchestrator_max_rounds setting.

- orchestrator_engine.py: max_rounds param on run() and _run_from_contents();
  effective_limit = min(per_model_limit, global_limit); stored in checkpoint
  so resume() respects it across confirmation gates
- routers/orchestrator.py: passes orch_model.get("max_rounds") to run()
- tools/agents.py: passes model_cfg.get("max_rounds") for gemini_api spawns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:54:37 -04:00
Scott Idem
09d775b47b feat: spawn_agent tool + host max_concurrent + docs
Adds a synchronous sub-agent spawning tool that lets the orchestrator
delegate tasks to a specific role's model and tool set.

- cortex/tools/agents.py: spawn_agent(task, role, tier, timeout, max_rounds)
  - Supports local_openai and gemini_api model types
  - Per-host asyncio semaphore (keyed by host_id or model type)
  - asyncio.wait_for() enforces timeout; admin-only tool
- cortex/model_registry.py: max_concurrent field in host schema (default 3,
  clamped 1-20); backfilled on _normalize() for existing hosts
- cortex/routers/local_llm.py + local_llm.html: "Max parallel" number input
  in host add/edit forms
- cortex/tools/__init__.py: spawn_agent registered in TOOL_CATEGORIES["Agents"],
  _CALLABLES, TOOL_ROLES (admin), and _ALL_DECLARATIONS
- Docs: TOOLS.md count 44→45, spawn_agent section; HELP.md tool table updated;
  ARCH__FUTURE.md Round 2 completed items; TODO__Agents.md spawn_agent checked;
  CLAUDE.md tool count and list updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:48:21 -04:00