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>
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>
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>
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_status: git branch/commit/ahead-behind + systemctl state — read-only
cortex_update: git pull + syntax check all .py files + report; does NOT auto-restart.
If syntax errors are found after pull, warns and blocks restart suggestion.
Call cortex_restart separately to apply a clean update.
Both are admin-only. cortex_update is confirm-required (modifies files on disk).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Filter key is "and" not "and_filters" (V3 API format)
- Entry IDs use journal_entry_id/id, not id_random (id_random is None)
- Dates use updated_on/created_on, not updated_at/created_at
- Total count lives in meta.data_list_count, not top-level total/count
- Inject query_string="%" when and filters present but no query, since
the V3 search engine requires query_string for filters to apply
- Normalize tags from string to list in both entry_read and entries_list
- Fix order_by to use updated_on (not updated_at) in entries_list
- Correct ARCH__AE_INTEGRATION.md: and_filters → and, or_filters → or
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ae_journal_entry_read: expose full entry content by id_random (title,
journal, tags, summary, full content with configurable truncation)
- ae_journal_entries_list: browse all entries in a journal newest-first,
numbered with id/title/tags/summary/date and pagination support
- ae_journal_search: richer output — tags, updated date, 400-char preview
(was 200), show summary OR preview (not both when summary exists)
_get_entry() was already implemented; read tool just exposes it properly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- reminders_remove(index) removes one reminder by 1-based index
- reminders_list now returns numbered output (1. heading / body)
so any model can easily identify which index to pass
- _parse_sections() / _sections_to_text() helpers for clean round-trip
- Not in CONFIRM_REQUIRED — targeted removal is safe without a gate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each entry in email_allowlist.json is treated as a re.fullmatch pattern
(case-insensitive). Allows domain wildcards, plus-addressing, and any
variation expressible as a regex. Invalid patterns are logged and skipped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reads home/{username}/email_allowlist.json (JSON array of addresses).
Fails safe: if file is missing or address not listed, send is blocked with
an informative message. home/ is gitignored; create the file manually per user.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps the existing email_utils.send_email helper as an admin-only tool.
Accepts to, subject, body (plain text); newlines converted to <br> for HTML part.
Registered in _CALLABLES, _ALL_DECLARATIONS, and TOOL_ROLES (admin).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth_utils: get_user_role() reads role from auth.json (admin|user, default user)
- manage_passwords: new `role` command to promote/demote users (admin-only by convention)
- tools/__init__: TOOL_ROLES map, CONFIRM_REQUIRED set, get_tools_for_role(),
get_openai_tools_for_role() — both orchestrators now filter tools by caller's role
- tools/system: cortex_restart (detached subprocess, 5s delay), cortex_logs (admin-only)
- tools/web: http_fetch — direct URL fetch, distinct from web_search
- tools/files: file_list (directory listing), file_write (restricted paths, admin-only)
- tools/notify: nc_talk_send — proactive outbound via notification.py
- orchestrator_engine + openai_orchestrator: user_role param; CONFIRM_REQUIRED tools
return a confirmation-request result instead of executing — loop breaks after Claude
asks user to confirm in a follow-up message
- home/scott/auth.json: role set to admin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four new tools for full journal entry lifecycle management:
- ae_journal_entry_update — PATCH any combination of fields (title,
content, summary, tags, enable); only provided fields are changed
- ae_journal_entry_disable — soft-delete via enable=false
- ae_journal_entry_append — fetch entry, append timestamped section
to the bottom (ideal for running logs / data logs)
- ae_journal_entry_prepend — fetch entry, prepend timestamped section
to the top (most-recent-first pattern)
Shared _get_entry / _patch_entry helpers keep the read-modify-write
logic DRY. Also fixed journal_entry_create to prefer the canonical
journal_entry_id field over the legacy id_random alias.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lists all Aether Journals for the configured account via
POST /v3/crud/journal/search with no filters (account scoped by header).
Returns name + id_random for each journal so the agent can discover
available journals before searching or writing entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add shell_exec to orchestrator tool suite (system.py + __init__.py)
Runs arbitrary shell commands on the Cortex host with timeout (1–120s),
combined stdout/stderr output, optional working_dir, and exit code reporting.
Enables system diagnostics (df, ls, ps, journalctl, etc.) from Agent mode.
- Fix orchestrator_engine.run() to use model_name from resolved registry entry
Previously used settings.orchestrator_model (.env hardcode) regardless of
what model was assigned to the orchestrator role. Now accepts model_name param
and falls back to settings value only when registry has no model_name.
- Update ARCH__FUTURE.md: date, running host, local orchestrator status,
model registry V2 progress, added Cortex Mesh concept (section 9)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- openai_orchestrator.py — new ReAct tool loop engine for any
OpenAI-compatible endpoint (OpenRouter, Open WebUI, Ollama, LiteLLM);
model handles both tool loop and final response, no Claude handoff needed
- tools/__init__.py — auto-derive OpenAI JSON Schema from existing Gemini
FunctionDeclarations so tool definitions have a single source of truth
- routers/orchestrator.py — route to openai_orchestrator when model registry
"orchestrator" role resolves to a local_openai type host
- routers/chat.py — pass role to _backend_label(); fix fallback_used logic
(only meaningful for explicit backend overrides, not auto-routing)
- static/app.js — add null/"auto" to backend cycle; fetch local model hint
without overriding the auto default on page load
- model_registry.py — _normalize() back-fills host_type on old registry files
- requirements.txt — add openai>=1.0.0
- ARCH__BACKENDS.md — document OpenAI-compat backend and routing logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New cortex/tools/reminders.py with reminders_add, reminders_list, reminders_clear
- reminders_clear moved here from cron.py (cron still imports from same file)
- __init__.py: wired up new callables and Gemini declarations
- Inara can now add/read reminders in Agent mode via the orchestrator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructures persona storage from a flat personas/{name}/ layout to
home/{username}/persona/{name}/, mirroring Linux home directories.
Changes:
- persona.py: two ContextVars (user + persona), Linux-style name validation,
set_context(), get_user(), get_persona(), validate(), list_users(),
list_user_personas(); persona_path() takes (username, name)
- config.py: replaces personas_dir with home_dir + home_root()
- git mv personas/inara → home/scott/persona/inara (history preserved)
- home/holly/persona/tina/: Holly's persona stub added
- cron_runner.py: all storage functions take (username, persona) params
- tools/cron.py: stamps user + persona on jobs; APScheduler IDs are
{user}:{persona}:{job_id} to prevent collisions across users
- memory_distiller.py: distill_short/mid/long take (username, persona);
added missing Path + settings imports
- scheduler.py: _load_user_crons() iterates home/*/persona/* (two-level)
- routers/chat.py, orchestrator.py: user field added; set_context() called
- tests/conftest.py: home_root fixture with two-level structure;
patches home_dir instead of personas_dir
- tests/test_persona.py: fully rewritten for two-level API
- tests/test_api_files.py: updated fixture name and path
- .env.default: documents HOME_DIR setting; scrubs stale API key
- CLAUDE.md, README.md: directory maps updated for new layout
All 80 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cortex/persona.py: ContextVar-based per-request routing with
path traversal protection and persona validation
- Migrate inara/ → personas/inara/ (git history preserved via git mv)
- config.py: add personas_root(), inara_path() delegates to personas/inara
- All 14 settings.inara_path() call sites replaced with persona_path()
- ChatRequest + OrchestrateRequest: add persona field (default: "inara")
with validation at request entry before any processing
- memory_distiller: add optional persona param for future per-persona distill
- cron_runner/tools/cron: stamp persona on jobs, prefix APScheduler IDs
(persona:job_id) to prevent collisions across personas
- scheduler: _load_user_crons() iterates all personas at startup
Adding a new persona: create personas/<name>/ with IDENTITY.md + SOUL.md.
Auth: handled at nginx level (inject X-Cortex-Persona header per subdomain).
Future: persona maps to Aether account_id_random for full integration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cortex/tools/scratch.py with scratch_read/write/append/clear tools
- Register all four scratch tools in the orchestrator tool registry
- Create inara/SCRATCH.md as the backing file (never distilled/archived)
- Fix auth.py: expiresAt reflects short-lived access token (~8h) not the
1-year refresh token — suppress expiry warning when refreshToken is present
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Agent mode toggle to web UI input row — routes through POST /orchestrate
instead of /chat; polls for result with live tool-call count in thinking bubble
- Add cortex/tools/system.py with claude_allow_dir tool; registers in tool registry
- Fix web search: duckduckgo_search renamed to ddgs, update import + requirements.txt
- Allow WebSearch and WebFetch in ~/.claude/settings.json for Claude CLI fallback
- Add claude-allow-dir script docs and security note to CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>