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>
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>
- 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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>
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>
Each role can now disable the current date/time header injected into the
system prompt. Default is true (all existing roles unchanged). Useful for
pure processing roles (summarizer, classifier, translator) where temporal
context is irrelevant or could cause unexpected model behavior.
Changes:
- model_registry: set_role_config/get_role_config gain inject_datetime field
- context_loader: load_context gains inject_datetime param (default True)
- orchestrator router: passes inject_datetime from role_cfg to load_context
- local_llm router: reads inject_datetime from POST body, passes to registry;
role_config_data_js includes the field
- local_llm.html: checkbox in role config panel; populate on open, save on submit
Session logs still timestamp every turn (HH:MM header in YYYY-MM-DD.md files)
regardless of this setting — the toggle only affects the system prompt header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reminders_add now accepts optional due: YYYY-MM-DD parameter.
Due date stored as first line of section body in REMINDERS.md.
context_loader.py calls load_due_reminders() instead of loading REMINDERS.md
wholesale — future-dated reminders are suppressed in the system prompt until
their date arrives. Undated reminders always surface (backward compatible).
reminders_list shows due status per entry: [OVERDUE by N days], [due TODAY],
or [due: YYYY-MM-DD] for future items. All reminders visible via the tool
regardless of date; only context surfacing is filtered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protects all models in the Primary/Backup chain regardless of context window:
- _context_budget(): 75% of model_cfg["context_k"] * 1000 (default 32k if unset)
- _estimate_tokens(): char count / 4 + 3k overhead for tool schemas
- _compact_messages(): truncates old tool results to 400 chars, keeps last 6
intact (~2 recent rounds), logs chars saved per compaction pass
- Compaction runs before every API call; log line now shows estimated token count
- Malformed tool call args logged with model/args detail instead of silent {}
- finish_reason check accepts "stop" and None alongside "tool_calls" (some
models return wrong reason even when tool_calls are present)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- tool_audit: ContextVars (engine, model) set at orchestrator run start; fields added to every entry
- orchestrator_engine: tool_audit.set_context("gemini", model_name) at run() start
- openai_orchestrator: tool_audit.set_context("openai", model label) at run() start
- audit table: Model column between Status and Args
- HELP.md: push notifications section, audit log in Files section, tool count 30→40, new API endpoints
- TODO__Agents.md: web_push and audit log marked complete with full detail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an "Audit Log" section (collapsed by default) at the bottom of the Files
panel showing tool_audit/YYYY-MM-DD.jsonl files for the current user.
- GET /api/audit/files — lists available dates (newest first, any auth user)
- GET /api/audit/day — returns entries for one date as JSON (any auth user)
- tool_audit.read_day() — reads a single day's JSONL file chronologically
- Clicking a date renders a read-only table: time / tool / status / args / result
- Status cells are colour-coded (green ok, red error, amber denied)
- Edit/Raw/Preview/Save buttons are hidden in audit view, restored on file switch
- Audit group starts collapsed; expands on click like other file groups
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every orchestrator tool invocation is recorded to home/{user}/tool_audit/YYYY-MM-DD.jsonl.
Each entry captures: timestamp, user, tool, args (truncated), status (ok/error/denied),
result length, and a 300-char result snippet.
- tool_audit.py: JSONL writer with per-file asyncio locks; read_recent / read_recent_all_users helpers
- tools/__init__.py: hook in call_tool() — fire-and-forget record on every dispatch
- routers/audit.py: GET /api/audit/recent and /api/audit/stats (admin-only)
- tools/files.py: add home_root() to file_read allowed roots so agents can read audit JSONL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/history/{session_id} now returns a 'name' field alongside messages.
resumeSession() uses data.name first, then the sessionNames map, then
raw ID as fallback — so named sessions display correctly even on page
load before the sessions panel has been opened.
'Resumed session X' message also now shows the friendly name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: 300px panel minus edit btn (28px) + meta (~130px) + delete
btn (28px) + gaps/padding left only ~70px (~7 chars) for the session name.
- Panel: 300px → 420px desktop, 300px → 380px mobile drawer
- Max-height: 340px → 400px
- Session item: name and meta now in a .session-body flex column, so the
name gets full body width (panel minus two buttons) — meta lives below
- Edit mode: hides .session-body + delete, input takes the full body slot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Edit button (✎) moved to left of row, separated from delete (×)
- Clicking ✎ hides name/meta/delete and expands input to full row width
- Button changes to ✓ (accent color) while editing
- Enter or ✓ click = save; Escape = cancel without saving
- Removed accidental-save-on-blur behavior
- Edit button: 30% opacity at rest, 75% on row hover, 100% on direct hover
- Touch devices: edit button always at 60% opacity (no hover to reveal it)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before any memory file is overwritten, _rotate_backup() keeps 2 rolling
backups: MEMORY_*.bak1.md (most recent) and MEMORY_*.bak2.md (older).
_sanity_check() now also guards against size anomalies: the new content
must be between 40% and 250% of the old file size — anything outside that
range looks like truncation or runaway output and aborts the write.
Existing checks (min length, refusal phrases) still apply.
Backup files exposed in the Files panel (ALLOWED set) so they can be
reviewed and manually restored if needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Distillation was silently operating on scott/inara for all users due to
ContextVar defaults. All three distill endpoints now require ?user=&persona=
query params and validate them via persona.validate(). Memory distiller
signatures changed from Optional to required positional args — no more
global settings fallback. Scheduler now iterates all users/personas instead
of hardcoding the primary user.
- context_loader: inject current date/time as first system prompt section
- session_logger: use get_user()/get_persona() from context instead of
settings globals so Holly/Brian sessions show correct speaker labels
- memory_distiller: system prompts now reference u.title()/p.title()
instead of settings.user_name/settings.agent_name
- distill router: Query(...) enforces params; _resolve() validates persona
- scheduler: _all_personas() helper iterates every user/persona for distill
- app.js: runDistill() now appends ?user=&persona= via _fileParams
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- usage_tracker.py: daily token/call buckets per user (home/{user}/usage.json)
- Hook into local backend (OpenAI usage field) and Gemini API (usage_metadata)
- Claude/Gemini CLI backends produce no structured token data and are not tracked
- Fix CLAUDE.md stale tool count (27 → 39) and refresh tool list
- scripts/import_knowledge.py: walk markdown dirs, chunk by H2, call local LLM
for summaries, create AE journal entries with path-derived tags; resumable via
state file; --dry-run and --limit flags for safe testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tools/__init__.py shrinks from 1,137 → 250 lines. Each domain file now
owns both its callables and its FunctionDeclarations (DECLARATIONS list),
so adding a new tool only touches one file.
New TOOL_CATEGORIES dict exported from __init__ — used by the UI for
grouped tool checkboxes.
Role config UI (Settings → Model Registry → Role Assignments):
- ⚙ button per role expands an inline configure panel
- Textarea for system_append (injected into system prompt for this role)
- Grouped checkboxes for tool allow-list (all checked = no restriction)
- POST /api/models/role-config saves both fields; updates ROLE_CONFIG_DATA
in-page so re-open reflects current state without a page reload
Backend:
- model_registry.set_role_config() writes system_append + tools to registry
- TOOL_CATEGORIES exported from tools/__init__ for UI rendering
- TOOLS.md header updated: 30 → 39 tools (ae_journal_* and cortex_* additions)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each role in model_registry.json can now carry two optional keys:
system_append — injected into the system prompt at position 7 (after
memory, closest to the turn) for the active chat_role
tools — explicit tool allow-list; intersected with the user's
access-level filter so it can only restrict, never elevate
No changes needed for existing users — missing keys fall back to current
behavior. Add keys to a role to give it a specialty focus:
"coder": {
"primary": "claude_cli",
"system_append": "You are in code-specialist mode...",
"tools": ["web_search", "file_read", "shell_exec", "scratch_write"]
}
Changes:
- model_registry.py: get_role_config() returns system_append + tools
- context_loader.py: role_append param appended as "--- Role Context ---"
- tools/__init__.py: get_tools_for_role/get_openai_tools_for_role accept
optional tool_list and intersect with access-level filter
- orchestrator_engine.py: tool_list threaded through run/resume/checkpoint
- openai_orchestrator.py: tool_list threaded through run/resume/checkpoint;
_build_client now calls get_openai_tools_for_role instead of returning
unfiltered OPENAI_TOOL_SCHEMAS
- routers/orchestrator.py: pulls role_cfg for chat_role, passes both
role_append and tool_list to context loader and engine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cortex/static/TOOLS.md — tool reference extracted from HELP.md; uses ##
headers so each category is collapsible. All 30 tools with descriptions.
- cortex/static/HELP.md — UI guide only; tools section replaced with a
one-line pointer to the Tools tab.
- help.html — three tabs (UI Guide / Tools / Persona); tab choice persists
in localStorage. Tools tab defaults all sections open. Persona tab shows
home/{user}/persona/{name}/HELP.md with an empty-state message if unset.
- context_loader.py — loads cortex/static/TOOLS.md into context at tier 2+
(replaces the previously empty persona HELP.md load). Persona HELP.md
still loaded if non-empty, as persona-specific additions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>