Files
optimclaw/src/agent/CLAUDE.md
T
633b234e44 docs: add comprehensive subdirectory CLAUDE.md files and update root (#589)
* docs: add comprehensive subdirectory CLAUDE.md files and update root

The repo has grown significantly. This adds module-level CLAUDE.md files
for the five most complex subsystems, and updates the root CLAUDE.md to
reflect the actual current state of the codebase.

New files:
- src/agent/CLAUDE.md — full module map (19 files), session/thread/turn
  model, agentic loop flow, compaction strategies with correct thresholds,
  scheduler invariants, self-repair details, complete submission command
  reference table
- src/channels/web/CLAUDE.md — complete API route table (50+ endpoints),
  SSE event type reference, auth/rate limiting gotchas, connection limits,
  CORS headers, step-by-step endpoint addition guide
- src/db/CLAUDE.md — dual-backend build commands, sub-trait structure
  (7 sub-traits, ~67 methods), SQL dialect differences, boolean/timestamp
  gotchas, complete schema table, in-memory test helper, shared handle pattern
- src/llm/CLAUDE.md — corrected LlmProvider trait signatures, provider
  chain decorator order, NEAR AI dual-auth and session renewal details,
  circuit breaker thresholds, previously undocumented smart_routing.rs
  and recording.rs
- tests/e2e/CLAUDE.md — conftest fixtures and async scoping, environment
  injected into the binary, mock_llm canned responses, writing guide with
  correct asyncio usage, gotchas section

Root CLAUDE.md updates:
- Added E2E test setup and integration test commands
- Documented ~15 undocumented modules: cli/, registry/, hooks/, tunnel/,
  observability/, webhook_server.rs, cost_guard.rs, job_monitor.rs, etc.
- Corrected libSQL backend path (libsql/ directory, 8 sub-modules)
- Updated Database trait method count (~67, split across 7 sub-traits)
- Fixed stale references: config.rs → config/channels.rs, main.rs → app.rs
- Added Hook, Observer, Tunnel traits to extensibility section
- Added tunnel and observability env vars to Configuration section
- Removed resolved TODO (webhook trigger is now shipped)
- Added Module Specifications entries for all 5 new CLAUDE.md files

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* docs: address PR review comments and reduce CLAUDE.md size

- Fix 7-sub-trait count (was 6) and ~78 async methods (was ~60/~67) in
  both CLAUDE.md and src/db/CLAUDE.md
- Add missing types.rs to secrets/ file tree (CLAUDE.md)
- Add missing tls.rs to src/db/CLAUDE.md Files table
- Fix method counts: ConversationStore 12, JobStore 13, RoutineStore 15
- Add Windows venv activation note to E2E setup commands
- Collapse agent/, web/, llm/, db/ file trees to one-liners (detail
  lives in their respective CLAUDE.md files)
- Replace verbose Database and LLM Providers sections with summaries
  linking to src/db/CLAUDE.md and src/llm/CLAUDE.md
- Root CLAUDE.md: 43,868 → 35,270 chars (fixes >40k perf warning)

[skip-regression-check]

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-03-07 08:33:09 +00:00

12 KiB
Raw Blame History

Agent Module

Core agent logic. This is the most complex subsystem — read this before working in src/agent/.

Module Map

File Role
agent_loop.rs Agent struct, AgentDeps, main run() event loop. Delegates to siblings.
dispatcher.rs Agentic loop for conversational turns: LLM call → tool execution → repeat. Injects skill context. Returns Response or NeedApproval.
thread_ops.rs Thread/session operations: process_user_input, undo/redo, approval, auth-mode interception, DB hydration, compaction.
commands.rs System command handlers (/help, /model, /status, /skills, etc.) and job intent handlers.
session.rs Data model: SessionThreadTurn. State machines for threads and turns.
session_manager.rs Lifecycle: create/lookup sessions, map external thread IDs to internal UUIDs, prune stale sessions, manage undo managers.
router.rs Routes explicit /commands to MessageIntent. Natural language bypasses the router entirely.
scheduler.rs Parallel job scheduling. Maintains jobs map (full LLM-driven) and subtasks map (tool-exec/background).
worker.rs Per-job execution for background scheduler jobs: calls LLM, runs tools, handles the reasoning loop. Distinct from dispatcher.rs.
compaction.rs Context window management: summarize old turns, write to workspace daily log, trim context. Three strategies.
context_monitor.rs Detects memory pressure. Suggests CompactionStrategy based on usage level.
self_repair.rs Detects stuck jobs and broken tools, attempts recovery.
heartbeat.rs Proactive periodic execution. Reads HEARTBEAT.md, notifies via channel if findings.
submission.rs Parses all user submissions into typed variants before routing.
undo.rs Turn-based undo/redo with checkpoints. Checkpoints store message lists (max 20 by default).
routine.rs Routine types: Trigger (cron/event/webhook/manual) + RoutineAction (lightweight/full_job) + RoutineGuardrails.
routine_engine.rs Cron ticker and event matcher. Fires routines when triggers match. Lightweight runs inline; full_job dispatches to Scheduler.
task.rs Task types for the scheduler: Job, ToolExec, Background. Used by spawn_subtask and spawn_batch.
cost_guard.rs LLM spend and action-rate enforcement. Tracks daily budget (cents) and hourly call rate. Lives in AgentDeps.
job_monitor.rs Subscribes to SSE broadcast and injects Claude Code (container) output back into the agent loop as IncomingMessage.

Session / Thread / Turn Model

Session (per user)
└── Thread (per conversation — can have many)
    └── Turn (per request/response pair)
        ├── user_input: String
        ├── response: Option<String>
        ├── tool_calls: Vec<ToolCall>
        └── state: TurnState (Pending | Running | Complete | Failed)
  • A session has one active thread at a time; threads can be switched.
  • Turns are append-only. Undo rolls back by restoring a prior checkpoint (message list, not a full thread snapshot).
  • UndoManager is per-thread, stored in SessionManager, not on Session itself. Max 20 checkpoints (oldest dropped when exceeded).
  • Group chat detection: if metadata.chat_type is group/channel/supergroup, MEMORY.md is excluded from the system prompt to prevent leaking personal context.
  • Auth mode: if a thread has pending_auth set (e.g. from tool_auth returning awaiting_token), the next user message is intercepted before any turn creation, logging, or safety validation and sent directly to the credential store. Any control submission (undo, interrupt, etc.) cancels auth mode.
  • ThreadState values: Idle, Processing, AwaitingApproval, Completed, Interrupted.
  • SessionManager maps (user_id, channel, external_thread_id) → internal UUID. Prunes idle sessions every 10 minutes (warns at 1000 sessions).

Agentic Loop (dispatcher.rs)

The dispatcher.rs module handles direct conversational turns (user messages processed inline by the main agent). Background scheduler jobs use worker.rs instead — these are two separate execution paths.

run_agentic_loop()  [dispatcher.rs — conversational turns]
  1. Load workspace system prompt (identity files: AGENTS.md, SOUL.md, etc.)
  2. Detect group chat from metadata; exclude MEMORY.md if group chat
  3. Select active skills (keyword/pattern scoring against message content)
  4. Build skill context block (injected before user message)
  5. LLM call → text response OR tool calls
  6. If tool calls:
     a. Check tool approval (session auto-approvals, pending approval queue)
     b. Execute tools (parallel via JoinSet)
     c. Sanitize results through SafetyLayer
     d. Feed results back → goto 5
  7. Return AgenticLoopResult::Response or NeedApproval

Tool approval: Tools flagged requires_approval pause the loop and return NeedApproval. The web gateway stores the PendingApproval in session state and sends an approval_needed SSE event. The user's approval/deny resumes the loop.

worker.rs vs dispatcher.rs: dispatcher.rs runs the agentic loop for user-initiated conversational turns (holds session lock, tracks turns). worker.rs is spawned by the Scheduler for background jobs created via CreateJob / /job — it runs independently of the session and has its own LLM reasoning loop with planning support (use_planning flag).

Command Routing (router.rs)

The Router handles explicit /commands (prefix /). It parses them into MessageIntent variants: CreateJob, CheckJobStatus, CancelJob, ListJobs, HelpJob, Command. Natural language messages bypass the router entirely — they go directly to dispatcher.rs via process_user_input. Note: most user-facing commands (undo, compact, etc.) are handled by SubmissionParser before the router runs, so Router only sees unrecognized /xxx patterns that haven't already been claimed by submission.rs.

Compaction

Triggered by ContextMonitor when token usage approaches the model's context limit.

Token estimation: Word-count × 1.3 + 4 overhead per message. Default context limit: 100,000 tokens. Compaction threshold: 80% (configurable).

Three strategies, chosen by ContextMonitor.suggest_compaction() based on usage ratio:

  • MoveToWorkspace — Writes full turn transcript to workspace daily log, keeps 10 recent turns. Used when usage is 8085% (moderate). Falls back to Truncate(5) if no workspace.
  • Summarize (keep_recent: N) — LLM generates a summary of old turns, writes it to workspace daily log (daily/YYYY-MM-DD.md), removes old turns. Used when usage is 8595%.
  • Truncate (keep_recent: N) — Removes oldest turns without summarization (fast path). Used when usage >95% (critical).

If the LLM call for summarization fails, the error propagates — turns are not truncated on failure.

Manual trigger: user sends /compact (parsed by submission.rs).

Scheduler

Scheduler maintains two maps under Arc<RwLock<HashMap>>:

  • jobs — full LLM-driven jobs, each with a Worker and an mpsc channel for WorkerMessage (Start, Stop, Ping, UserMessage).
  • subtasks — lightweight ToolExec or Background tasks spawned via spawn_subtask() / spawn_batch().

Preferred entry point: dispatch_job() — creates context, optionally sets metadata, persists to DB (so FK references from job_actions/llm_calls are valid immediately), then calls schedule(). Don't call schedule() directly unless you've already persisted.

Check-insert is done under a single write lock to prevent TOCTOU races. A cleanup task polls every second for job completion and removes the entry from the map.

spawn_subtask() returns a oneshot::Receiver — callers must await it to get the result. spawn_batch() runs all tasks concurrently and returns results in input order.

Self-Repair

DefaultSelfRepair runs on repair_check_interval (from AgentConfig). It:

  1. Calls ContextManager::find_stuck_jobs() to find jobs in JobState::Stuck.
  2. Attempts ctx.attempt_recovery() (transitions back to InProgress).
  3. Returns ManualRequired if repair_attempts >= max_repair_attempts.
  4. Detects broken tools via store.get_broken_tools(5) (threshold: 5 failures). Requires with_store() to be called; returns empty without a store.
  5. Attempts to rebuild broken tools via SoftwareBuilder. Requires with_builder() to be called; returns ManualRequired without a builder.

Note: the stuck_threshold duration is stored but currently unused (marked #[allow(dead_code)]). Stuck detection relies on JobState::Stuck being set by the state machine, not wall-clock time comparison.

Repair results: Success, Retry, Failed, ManualRequired. Retry does NOT notify the user (to avoid spam).

Key Invariants

  • Never call .unwrap() or .expect() — use ? with proper error mapping.
  • All state mutations on Session/Thread happen under Arc<Mutex<Session>> lock.
  • The agent loop is single-threaded per thread; parallel execution happens at the job/scheduler level.
  • Skills are selected deterministically (no LLM call) — see skills/selector.rs.
  • Tool results pass through SafetyLayer before returning to LLM (sanitizer → validator → policy → leak detector).
  • SessionManager uses double-checked locking for session creation. Read lock first (fast path), then write lock with re-check to prevent duplicate sessions.
  • Scheduler.schedule() holds the write lock for the entire check-insert sequence — don't hold any other locks when calling it.
  • cheap_llm in AgentDeps is used for heartbeat and other lightweight tasks. Falls back to main llm if None. Use agent.cheap_llm() accessor, not deps.cheap_llm directly.
  • CostGuard.check_allowed() must be called before LLM calls; record_llm_call() must be called after. Both calls are separate — the guard does not auto-record.
  • BeforeInbound and BeforeOutbound hooks run for every user message and agent response respectively. Hooks can modify content or reject. Hook errors are logged but fail-open (processing continues).

Complete Submission Command Reference

All commands parsed by SubmissionParser::parse():

Input Variant Notes
/undo Undo
/redo Redo
/interrupt, /stop Interrupt
/compact Compact
/clear Clear
/heartbeat Heartbeat
/summarize, /summary Summarize
/suggest Suggest
/new, /thread new NewThread
/thread <uuid> SwitchThread Must be valid UUID
/resume <uuid> Resume Must be valid UUID
/status [id], /progress [id], /list JobStatus /list = all jobs
/cancel <id> JobCancel
/quit, /exit, /shutdown Quit
yes/y/approve/ok and aliases ApprovalResponse { approved: true, always: false }
always/a and aliases ApprovalResponse { approved: true, always: true }
no/n/deny/reject/cancel and aliases ApprovalResponse { approved: false }
JSON ExecApproval{...} ExecApproval From web gateway approval endpoint
/help, /? SystemCommand { "help" } Bypasses thread-state checks
/version SystemCommand { "version" }
/tools SystemCommand { "tools" }
/skills [search <q>] SystemCommand { "skills" }
/ping SystemCommand { "ping" }
/debug SystemCommand { "debug" }
/model [name] SystemCommand { "model" }
Everything else UserInput Starts a new agentic turn

SystemCommand vs control: SystemCommand variants bypass thread-state checks entirely (no session lock, no turn creation). Quit returns Ok(None) from handle_message which breaks the main loop.

Adding a New Submission Command

Submissions are special messages parsed in submission.rs before the agentic loop runs. To add a new one:

  1. Add a variant to Submission enum in submission.rs
  2. Add parsing in SubmissionParser::parse()
  3. Handle in agent_loop.rs where SubmissionResult is matched (the match submission { ... } block in handle_message)
  4. Implement the handler method (usually in thread_ops.rs for session operations, or commands.rs for system commands)