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]>
This commit is contained in:
Henry Park
2026-03-07 08:33:09 +00:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 45ec691f4c
commit 633b234e44
6 changed files with 1034 additions and 133 deletions
+171
View File
@@ -0,0 +1,171 @@
# 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: `Session``Thread``Turn`. 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)