Skip to content

ADR Index

github-actions[bot] edited this page Oct 31, 2026 · 27 revisions

Canonical source: docs/13-adr/adr-index.md — this page is rendered from it on every publish; edit the source.

Architecture Decision Records

This document records the key architectural decisions made for the ClawAI platform, their context, rationale, and consequences. Each ADR follows the format: Context, Decision, Consequences, Status.

See also: docs/adrs/ for earlier ADRs written during initial development.


ADR-001: Microservices Over Monolith

Date: 2025-Q1 Status: Accepted

Context

ClawAI integrates with multiple AI providers, manages user auth, stores conversation history, handles file processing, logs audit events, and runs local AI models. These domains have different scaling profiles, different data stores, and different failure modes. A monolithic NestJS application would become a maintenance burden as features grow.

Decision

Split the backend into 13 independently deployable NestJS microservices, each owning a single bounded context:

  1. auth -- Authentication, authorization, user management
  2. chat -- Conversation threads, messages, context assembly, AI execution
  3. connector -- Cloud provider configuration, health, model sync
  4. routing -- Routing decisions, policies, Ollama-assisted routing
  5. memory -- Memory records, extraction, context packs
  6. file -- File upload, storage, chunking
  7. audit -- Audit events, usage tracking
  8. ollama -- Local model management, generation
  9. health -- Aggregated health monitoring
  10. client-logs -- Frontend log collection
  11. server-logs -- Backend log aggregation
  12. image -- Image generation
  13. file-generation -- AI-driven document generation

Consequences

  • Positive: Independent deployability; each service can be updated, scaled, or restarted without affecting others. Clear ownership boundaries. Different databases per service prevent coupling. Failure isolation -- a crash in the audit service does not affect chat.
  • Positive: Teams can work on different services in parallel without merge conflicts.
  • Negative: Operational complexity -- 17 services to monitor, deploy, and debug. Distributed tracing needed (X-Request-ID implemented).
  • Negative: Network overhead for inter-service communication. Context assembly requires HTTP calls to memory and file services.
  • Negative: Development environment requires Docker Compose with 22+ containers.

ADR-002: PostgreSQL Per Service (Data Isolation)

Date: 2025-Q1 Status: Accepted

Context

Microservices that share a database are coupled at the data layer -- schema changes in one service can break another. We need strong data isolation between services while supporting relational queries, ACID transactions, and vector similarity search (for memory/embeddings).

Decision

Each service that needs relational storage gets its own PostgreSQL database instance:

  • claw_auth, claw_chat, claw_connectors, claw_routing, claw_memory (with pgvector), claw_files, claw_ollama, claw_image, claw_filegen

MongoDB is used for services with document-oriented workloads:

  • claw_audit (audit logs, usage ledger), claw_client_logs, claw_server_logs

Consequences

  • Positive: Complete schema independence. Each service evolves its schema via Prisma migrations without affecting others.
  • Positive: pgvector extension available per-database for services that need vector similarity (memory service).
  • Positive: MongoDB TTL indexes handle automatic log cleanup without affecting relational databases.
  • Negative: 8 PostgreSQL + 3 MongoDB instances consume significant resources (~2GB RAM minimum for databases alone).
  • Negative: No cross-database joins. Data that spans services requires HTTP or RabbitMQ communication.
  • Negative: Backup and monitoring complexity scales linearly with database count.

ADR-003: RabbitMQ Over HTTP for Async Communication

Date: 2025-Q1 Status: Accepted

Context

Services need to communicate for workflows like: chat publishes a message, routing processes it, chat receives the decision and executes. Some communication must be synchronous (context assembly fetches from memory service), but most event flows are inherently async and benefit from decoupling, retry logic, and failure isolation.

Decision

Use RabbitMQ with a topic exchange (claw.events, durable) for all asynchronous inter-service communication. Use HTTP for synchronous data fetching where a response is needed immediately.

  • RabbitMQ: message.created, message.routed, message.completed, connector.synced, user.login, etc.
  • HTTP: Chat service fetching memories, file chunks, and context packs during context assembly.

Implement DLQ (Dead Letter Queue) with 3 retries and exponential backoff for all consumers.

Consequences

  • Positive: Temporal decoupling -- publisher and consumer do not need to be available simultaneously.
  • Positive: Retry with DLQ handles transient failures without losing messages.
  • Positive: Topic routing allows flexible event subscription (e.g., audit service subscribes to multiple event types).
  • Positive: Load leveling -- consumers process at their own pace.
  • Negative: Eventual consistency -- routing decisions are not instant (typically <100ms, but can be up to 10s with Ollama router).
  • Negative: Debugging distributed event flows is harder than tracing a single HTTP call. Requires correlation IDs and structured logging.
  • Negative: RabbitMQ is another infrastructure dependency to operate and monitor.

ADR-004: Ollama for Local AI Runtime

Date: 2025-Q1 Status: Accepted

Context

ClawAI's core value proposition is intelligent routing with privacy guarantees. Some data must never leave the user's machine. Additionally, the routing engine needs a fast, local AI model to classify messages and select providers in AUTO mode. Using a cloud API for routing would add latency, cost, and a privacy paradox (sending data to the cloud to decide if it's safe to send to the cloud).

Decision

Run Ollama locally as the AI runtime for:

  1. Routing: gemma3:4b classifies messages and selects optimal provider (temp=0, Zod-validated output).
  2. Memory extraction: gemma3:4b extracts facts, preferences, instructions, and summaries from completed messages.
  3. Fallback chat: Local models serve as the ultimate fallback when cloud providers are unavailable.
  4. Privacy-sensitive requests: LOCAL_ONLY and PRIVACY_FIRST modes route exclusively through Ollama.

Auto-pull 5 models on startup: gemma3:4b, llama3.2:3b, phi3:mini, gemma2:2b, tinyllama.

Consequences

  • Positive: Zero data leaves the machine for local-only operations. True privacy guarantee.
  • Positive: No API cost for local operations. Routing decisions are free.
  • Positive: No internet dependency for core routing logic.
  • Positive: Configurable model roles allow swapping models without code changes.
  • Negative: Requires significant local resources (~10GB disk for models, 4-8GB RAM during inference).
  • Negative: Local model quality is lower than cloud models (4B params vs 100B+).
  • Negative: Cold start latency (10-30s to load a model into memory).
  • Negative: No GPU acceleration in Docker by default (requires nvidia-docker).

ADR-005: Zod Over class-validator for Validation

Date: 2025-Q1 Status: Accepted

Context

NestJS traditionally uses class-validator with decorators for DTO validation. However, class-validator has limitations: no runtime type inference, verbose decorator syntax, no composability, and the validated output type must be manually synchronized with the class definition.

Decision

Use Zod for all input validation across all services. Define schemas as z.object() declarations, infer TypeScript types from schemas using z.infer<>. Use a custom ZodValidationPipe in NestJS to integrate with the standard validation pipeline.

// DTO pattern
export const CreateThreadSchema = z.object({
  title: z.string().min(1).max(200),
  routingMode: z.nativeEnum(RoutingMode),
});
export type CreateThreadDto = z.infer<typeof CreateThreadSchema>;

Consequences

  • Positive: Single source of truth -- schema defines both validation rules and TypeScript type.
  • Positive: Runtime validation matches compile-time types automatically.
  • Positive: Composable -- schemas can extend, merge, and pick from each other.
  • Positive: Used in both frontend (form validation) and backend (DTO validation) for consistency.
  • Positive: Better error messages out of the box.
  • Negative: Not the NestJS default -- requires custom pipe integration.
  • Negative: Team members familiar with class-validator need to learn Zod patterns.
  • Negative: Some NestJS Swagger/OpenAPI generators expect class-validator decorators.

ADR-006: Event-Driven Routing (Decoupled, Async)

Date: 2025-Q1 Status: Accepted

Context

The message flow requires the chat service to send a message to the routing service for provider selection, then receive the result to execute the AI call. Two approaches were considered: (1) synchronous HTTP call from chat to routing, or (2) event-driven via RabbitMQ.

Decision

Use event-driven routing:

  1. Chat publishes message.created event.
  2. Routing consumes event, makes decision, publishes message.routed event.
  3. Chat consumes message.routed, proceeds with execution.

Consequences

  • Positive: Chat and routing services are fully decoupled. Either can be restarted independently.
  • Positive: Routing decisions are recorded as events, creating an audit trail.
  • Positive: Multiple consumers can react to message.created (e.g., future analytics service).
  • Positive: Routing service can be replaced or upgraded without touching chat service.
  • Negative: Additional latency (RabbitMQ publish + consume + publish + consume adds ~10-50ms).
  • Negative: More complex error handling -- what if routing never responds? (Handled by timeout + heuristic fallback.)
  • Negative: Debugging requires following events across services via correlation IDs.

ADR-007: SSE Over WebSocket for Streaming

Date: 2025-Q1 Status: Accepted

Context

AI model responses should stream to the frontend token-by-token for a responsive UX. Two options: WebSocket (bidirectional, persistent connection) or Server-Sent Events (unidirectional, HTTP-based).

Decision

Use Server-Sent Events (SSE) for streaming AI responses from the chat service to the frontend.

Consequences

  • Positive: SSE works over standard HTTP -- compatible with Nginx reverse proxy without special configuration (beyond buffering disabled).
  • Positive: Automatic reconnection built into the SSE protocol.
  • Positive: Simpler server implementation (no WebSocket upgrade, no connection state management).
  • Positive: Works with standard HTTP authentication and CORS.
  • Negative: Unidirectional only (server to client). Client-to-server communication still uses HTTP POST.
  • Negative: HTTP/1.1 has a 6-connection-per-domain limit in browsers (not an issue with HTTP/2).
  • Negative: Less efficient than WebSocket for very high-frequency bidirectional messaging (not our use case).

ADR-008: Nginx Reverse Proxy (Single Entry Point)

Date: 2025-Q1 Status: Accepted

Context

With 17 backend services on different ports, the frontend cannot manage connections to each individually. We need a single entry point that routes requests to the correct service based on URL path.

Decision

Use Nginx as a reverse proxy on port 4000. All API requests go through Nginx, which routes based on URL prefix:

  • /api/v1/auth/* -> auth-service:4001
  • /api/v1/chat-threads/* -> chat-service:4002
  • /api/v1/connectors/* -> connector-service:4003
  • (20+ route mappings total)

Configure Nginx with SSE support (proxy_buffering off, chunked transfer encoding).

Consequences

  • Positive: Frontend connects to a single URL (port 4000) for all API calls.
  • Positive: Nginx handles SSL termination, request buffering, and static file serving.
  • Positive: SSE support with proper buffering configuration.
  • Positive: Easy to add rate limiting, IP allowlisting, or caching at the proxy layer.
  • Negative: Nginx is an additional component to configure and maintain.
  • Negative: Route configuration is in nginx.conf, separate from application code -- must be updated when adding new endpoints.
  • Negative: Nginx becomes a single point of failure (mitigated by its high reliability and simple configuration).

ADR-009: Monorepo with npm Workspaces

Date: 2025-Q1 Status: Accepted

Context

ClawAI has 17 backend services, 1 frontend, and 5 shared packages that all need to share enums, types, and utilities. Options: polyrepo (each service in its own repository), monorepo with Turborepo/Nx, or monorepo with npm workspaces.

Decision

Use a monorepo with npm workspaces. All 18 packages live in a single repository under apps/ and packages/.

Consequences

  • Positive: Single npm install installs all dependencies across all packages.
  • Positive: Shared packages (shared-types, shared-constants, shared-rabbitmq, shared-auth) are workspace dependencies -- changes are immediately available to all consumers.
  • Positive: Atomic commits -- a change to a shared type and all its consumers can be in one commit.
  • Positive: Unified CI/CD pipeline (lint, typecheck, test, build all at once).
  • Positive: No need for package publishing or version management for internal packages.
  • Negative: Large repository size; npm install is slower than single-package install.
  • Negative: CI runs all checks even when only one service changed (no incremental builds without Turborepo).
  • Negative: All developers need the full repo; cannot clone just one service.

ADR-010: fetch-Based SSE Over EventSource API

Date: 2025-Q1 Status: Accepted

Context

The browser EventSource API is the standard way to consume SSE streams. However, EventSource has a critical limitation: it does not support custom HTTP headers. ClawAI requires the Authorization: Bearer <token> header on all API requests, including SSE connections.

Decision

Use the fetch API with ReadableStream to consume SSE streams instead of the native EventSource API. Implement a custom SSE parser that reads the stream, splits on \n\n delimiters, and extracts data: fields.

Consequences

  • Positive: Full control over request headers -- Authorization header is sent with SSE requests.
  • Positive: Can include any custom headers (e.g., X-Request-ID for correlation).
  • Positive: Works identically across all modern browsers.
  • Positive: Can implement custom retry logic and connection management.
  • Negative: No automatic reconnection (must implement manually).
  • Negative: More code than new EventSource(url) -- requires stream reading, parsing, and error handling.
  • Negative: Must handle edge cases: partial chunks, multi-line data fields, keep-alive comments.

ADR Summary Table

ID Decision Status Key Driver
001 Microservices (17 services) Accepted Failure isolation, independent deployment
002 PostgreSQL per service Accepted Data isolation, independent schemas
003 RabbitMQ for async Accepted Reliability, retry/DLQ, decoupling
004 Ollama local AI Accepted Privacy, cost, routing independence
005 Zod over class-validator Accepted Type inference, composability
006 Event-driven routing Accepted Decoupling, auditability
007 SSE over WebSocket Accepted Simplicity, HTTP compatibility
008 Nginx reverse proxy Accepted Single entry point, SSE support
009 npm workspaces monorepo Accepted Shared code, atomic changes
010 fetch-based SSE Accepted Auth header support
018 Universal webhook receiver Accepted One signed entry-point per provider
019 Auto-suggest scheduler Accepted Cron + advisory locks across replicas
020 Suggestion factory pipeline Accepted Single entry-point for events → queue
021 Write-action adapter pattern Accepted Uniform executeWriteAction per adapter
022 HTML email sanitisation Accepted DOMPurify + iframe sandbox; service-token /upload-internal
023 Calendar providers Accepted GoogleCalendar + OutlookCalendar adapters; MEETING object
024 Inbox + pgvector search Accepted Cross-provider inbox + memory-service embeddings table
025 Digest dashboard Accepted Hourly cron + advisory lock + Intl.DateTimeFormat tz match
026 User-pref intersection Accepted Most-restrictive-wins (admin > user)
027 Memory learning loop Accepted Heuristic v1; LLM classifier v1.x
028 IMPL_PROMPT handoff Accepted Workspace ↔ chat/agent bridge with secret scanner
029 Capability framework Accepted Generalises agent + workspace approvals
040 Router model registry Accepted Canonical model identity store (Phase 1)
041 Cost confidence annotation Accepted EXACT/ESTIMATED/UNKNOWN; uncertainty penalty
042 RoutingDecisionV2 Zod schema Accepted Strict output contract for the router (Phase 7)
043 Route-only contract Accepted Filter router-only models before scoring
044 Learned scores split table Accepted Per-(profile, domain, taskFamily) bounded updates
045 Persisted circuit breakers Accepted DB-backed; survives container restart
046 Simulator same code path Accepted Phase 13 dry-run uses production evaluator
047 14-dim scoring weights Accepted Per-RoutingMode weight vectors sum to 1
048 Workflow vs. model decision Accepted Workflow selection inside router (Phase 9)
049 Local TLS everywhere (mkcert) Accepted One install command, end-to-end HTTPS incl. service-to-service
050 Critic as sibling plan feature of Judge Accepted allowCriticReview flag + user-selectable critic model + parse-failed marker
051 Narrow workspace VIEW + CONNECT permissions for USER Accepted WORKSPACE_VIEW + WORKSPACE_APP_CONFIG_VIEW; partial-relax over admin-only
052 Shared RichPromptTextarea + use-sticky-bottom-scroll hook Accepted One autosize textarea everywhere; one auto-follow scroll behaviour everywhere
053 File retention sweeper + ZIP archive guardrails Accepted Nightly cron sweep + 4 ZIP bomb hard caps + tmpfs sandbox
054 file_delivery_records extracted from JSON Accepted Per-model delivery records → typed table; 30-day dual-write window
055 Canonical AI authority hierarchy Accepted CLAUDE.md > rules/00 > context/arch+stack > rules > skills > context+memory > .ai > routers
056 Generated .ai/ knowledge layer Accepted 19 manifests + BOOTSTRAP + workspace AGENTS.md, all derived from source
057 Deterministic context resolver Accepted Lexical/structural retrieval, no external AI dependency
058 Compact AI routers, not mirrors Accepted ~90-line per-tool routers; canonical wins on conflict
059 Split ESLint architecture + plugin Accepted (plugin) / Proposed (root split) Tested custom rules now; root-config decomposition deferred to its own slice
060 Affected-workspace validation Accepted Diff → owning workspace + dependents; root changes stay local-scoped
061 Git-hook policy — no --no-verify Accepted Hooks call the affected lane; bypass recommendations removed + scanned
062 Testing-runner retention Superseded by ADR-099 Keep Jest (backend) + Vitest (frontend) + Playwright (E2E); no forced migration
063 Coverage targets Accepted ≥95%/90% branches target, ratcheted in, 100% branch on pure critical logic
064 Refund ledger and entitlement policy Accepted Partial preserves access; cumulative full refund revokes; DB-locked aggregate
065 Immutable invoice documents and durable delivery Accepted DB-immutable facts; transactional delivery intent; owned PDF download; shared SMTP adapter
066 Purpose-constrained checkout sessions Accepted One callback path; DB-enforced subscription/setup purpose invariant
067 Owner-token Redis locks for scheduled jobs Accepted NX lease + atomic owner compare/delete; bounded idempotent work
068 Session-bound user tokens Accepted Hashed refresh rotation, replay revocation, strict JWT claims, one-time sign-out
069 Router learned-score retirement Accepted RouterModelProfile/RouterTopicProfile is the one production learning system; RouterLearnedScore retired dead-but-harmless
070 Workspace-tier hierarchical priors Accepted Workspace tier only (user tier deferred, no userId concept exists); inert until chat-service threads a real workspace id
071 Discovery stylesheet and feed content negotiation Accepted xml-stylesheet on every discovery document; feeds pick their type from Accept with Vary: Accept
072 AI answer-engine crawler policy and the comparison cluster Accepted Named crawler groups sharing one allow/disallow pair; /llms.txt; /compare/* on fixed axes, translated, no fabricated review markup
073 Super-administrator authority Accepted One pure scope predicate + one DB actor read; per-scope self-exemption; no JWT claim; three refusal codes
074 Plan signup flag and popular badge Accepted isPopular + nullable unique popularKey; migration never writes isDefault; both flags stay behind their own endpoint
075 Public share assets Accepted A share owns copies of its images; unscanned images cost ad and index eligibility, never readability
076 Chat stream durability Accepted The chat SSE bus stays in-process; the database plus polling is the record, and chat-service runs exactly one replica
077 chat-service horizontal scaling Accepted The stream bus and Stop move to Redis so chat-service runs 4 replicas; deploys roll one replica at a time
078 PAYG connector credit Accepted Credit IS the promoted monthlyProviderCostCeilingMicroUsd; 1 weighted token == 1 micro-USD, so a third column would be a third name for one number
079 Auth learns model prices over a cached internal call Accepted routing-service stays the price owner; auth caches 300 s, busted by routing.model_cost.published; fails closed
080 One reservation, not two Accepted RESERVE_QUOTA_LUA 7 to 9 windows + 3 WeightedUsageRecord columns; no CreditReservation table, no second atomicity domain
081 Retire the routing cost-budget scaffold Accepted (supersedes routing stream R.4) Never registered, no schema, SCAFFOLD-R4 throw, 7 unguarded handlers; per-user spend capping is the auth wallet
082 PAYG classification grain Accepted Server-side in auth only, at PROVIDER grain rolled up from Connector.isPayAsYouGo; never in shared-entitlements
083 Credit top-up as a third checkout purpose Accepted (amends ADR-066) Third CHECK branch; fixed server-priced packages; price-to-credit ratio a column seeded at 0.60, never 1:1
084 SEO clusters fan out from one dynamic route Accepted One [topic] segment + order array per cluster; all five layers fan out; sitemap-coverage learns dynamic segments; Lighthouse samples on PR
086 The Context Composer, and why relevance may never remove a message Accepted Relevance ranks, only the token budget removes; turns not messages; maxTokens is output-only; every generation emits a context manifest
087 Cross-thread retrieval, and why it is off by default Accepted Opt-in per thread, never read when off; two stages; a coined identifier is the precision gate; ranks on message evidence, not the title
088 The chat composer sizes itself, and the drag handle is gone Accepted Row range replaces a fixed pixel height; reuses RichPromptTextarea; composer stays in normal flow so rule 36 clearance never applies
089 Client telemetry ships as batches to a second endpoint Accepted POST /client-logs/batch + insertMany({ordered:false}); collapse repeats to an occurrences count; DEBUG stops at the network in production; telemetry exempt from the auth-refresh flow
090 The model picker opens at the current choice, and its trigger always names it Accepted cmdk's value is the highlight, so seeding it on open IS scroll-to-selection; item value becomes the id with the label in keywords; a narrow trigger shows a short label instead of nothing
091 A URL the user writes is opened, not searched for Accepted Direct fetch before search in the research pipeline; pasted pages outrank discovered ones; the prompt now states what was ATTEMPTED and which tools ran
092 Multi-page site crawl reuses FetchService, no new fetch path Accepted SITE_CRAWL workflow; robots.txt/sitemap.xml/pages all go through the same FetchService; sitemap first, homepage links only when it's thin; crawled pages are source:'fetch', no new schema
093 Mid-generation crawl retrieval bypasses the callProvider chokepoint Accepted get_crawled_page reuses the previously-unwired runOllamaCloudToolLoop, Ollama Cloud only; resolved from metadata.research.bundle in memory, no network call; bypasses callProvider's single hold because the loop takes its own hold per turn
094 Headless-browser rendering is a fallback INSIDE FetchService Accepted HeadlessFetchAdapter (Playwright/Chromium) retries a thin plain-fetch result, never a second fetch path; every in-page subrequest gets the same anti-SSRF check the top-level navigation gets, not just the initial URL
095 Attachments are extracted to text, and the text is a column Accepted processFile had no caller from upload; extracted_text is its own column, not a chunk rejoin; no bulk backfill (it would poll forever), legacy rows heal on use; OOXML read in-memory under ADR-053-equivalent bounds
096 A login tells you why, only after you have proved the account is yours Accepted Password verified FIRST, before any account-state branch, so unknown-address and wrong-password are identical in body AND cost (burnPasswordVerification); EMAIL_NOT_VERIFIED/ACCOUNT_SUSPENDED are safe to name only post-verification; registration lands on /check-email, never /login; every auth email localised to 13 languages
097 Display FX is a separate subsystem from settlement FX Accepted Three amounts named and never merged — CANONICAL (immutable USD price), DISPLAY (current rate, commercially rounded, fails OPEN to USD) and SETTLEMENT (frozen FxQuote, safety margin, fails CLOSED); DisplayFxRate carries no quoteId/expiresAt/safetyMarginBps so it cannot reach a gateway adapter; display currencies are a separate, wider allowlist than billing currencies
098 AUTO research is an AI-driven, narrated loop that runs after the request returns Accepted A planner (admin's ordered models, next model on a bad reply) chooses answer/crawl/search/crawl-then-search and is asked again after a crawl; runs after the POST with message.created in finally; chat calls research over an internal service-token route (the user route 403'd every non-admin); one shared URL detector with bare domains; every step narrated to a deduped Redis log and stored as metadata.narration
099 Vitest in every workspace; Playwright stays for E2E Accepted Supersedes ADR-062. All 20 apps + 6 packages run vitest run since 63e7f7806 (2026-09-17); backend configs use unplugin-swc for decorator metadata; coverage thresholds in vitest.config.ts; Playwright unchanged for E2E; gates stay runner-agnostic
100 The AUTO router picks from exposed models; every prompt fits its window Accepted Candidates = admin-EXPOSED chat models (health + plan filtered, ACTIVE first, round-robin by provider, cap 30) instead of 4 ACTIVE Gemini-heavy rows; OLLAMA_CLOUD planner calls ollama.com with the connector key (prod has no ollama-service); one knownContextWindow table in shared-utilities; research/files/packs/memories get 45/25/10/5% of the window; fast path escalates on length
101 Every container log goes to one store, shipped read-only by Vector Accepted x-claw-logging anchor (json-file, 20m x5, claw.service label) on every service; Vector reads json-file logs READ-ONLY (never the Docker socket); /server-logs/ingest/containers parses pino/Nest/plain into real levels; the public log-write routes now need the service token
102 Read-only ops tokens are the no-SSH channel into production logs Accepted auth-service mints claw_ops_ tokens (SHA-256 stored, shown once, LOGS_READ only, 1-90 day expiry, 10 per admin, revocable, use-counted); GET /api/v1/ops/logs* on server-logs with Authorization: Ops; guard verifies via auth internal route, caches 30 s, fails closed; the Ops scheme never opens a session route
103 File requests need a file word; file writers are admin-managed; file jobs are owner-only Accepted detectFileIntent (whole words, negation, question guard, strong vs soft words, 41 cases) replaced substring verb+format; FILE_WRITER assistant role replaced 3 hard-coded models; cloud router records routerModel and every AUTO answer says who routed; retry/events owner-only, internal routes service-token
104 Generated files download through an owner-only proxy and expire after an hour Accepted /file-generations/:id/assets/:assetId/download (owner guard, streamed, attachment + no-store + nosniff, sanitised name, no storageKey in JSON); 1-hour expiresAt, 410 FILE_EXPIRED, 5-min sweep deletes bytes; rebuild from saved text; first-ever init migration (prod had no tables)
105 Any AI answer exports as a file through the file pipeline, with no model call Accepted md/txt built in the browser; html/docx/pdf via POST /file-generations/export (provider EXPORT, no tokens, ADR-104 link); expand dialog rendered/raw; JSON body limit tied to DTO bounds by a spec (100kb default failed every file over ~100k chars as a 500); prompt bound 4,000 → 100k to match chat
106 Sessions survive many tabs and windows, and "Remember me" sets the session's lifetime Accepted 30 s refresh-reuse grace issues a sibling instead of revoking the family; one refresh across tabs (Web Locks) that adopts a token another tab got; storage is the truth (setUser reads it, storage events rehydrate); only 400/401/403 end a session; rememberMe → session.persistent (7 d vs 12 h + browser-close)
107 Answers render through one Markdown parser, and PDFs are typeset by Typst Accepted parseMarkdownDocument → typed blocks for every format; PDF via Typst (answer text only as escaped string literals, empty workspace, Noto fonts in image); DOCX real runs/lists/tables/bidi; HTML title+dir; CSV/JSON from the first table; pdfkit removed
108 Answers export as spreadsheets, slide decks and zip bundles Accepted XLSX written as SpreadsheetML (inline strings, no formulas, sheets per table); PPTX via pptxgenjs; ZIP of README + code files + table CSVs with zip-slip-safe paths; CSV formula-injection guard; chat detects formats by whole words and gives the writer per-format instructions; whole-fence unwrap bug fixed
109 The AI names its files, from its own title Accepted writer opens with a # title; deriveFileIdentity → title (humanTitle), description (first paragraph), unicode filename; request fallback; RFC 5987 filename*; card shows title + description; no extra model call
110 AI-written files have a daily allowance per plan; exports are free Accepted FILE_GENERATION feature key (Free 15, Starter 30, Plus 75, Pro 200 a day, Team+ uncapped); first metered feature that refuses: reserve before the model writes, consume/release after; refusal is a translated in-chat notice; fail-open if auth is down
111 What the model matrix changed about writing files Accepted Found by F4 (1,500 real files) + F5 pentest: one Content-Disposition builder (non-Latin downloads 500d), no INSTRUCTION memories in CSV/JSON writers, CSV as a Markdown table, names cleaned of what file-service refuses, not-found is 404
112 A revoked session stops at the guard, not at the token's expiry Accepted TD-033: auth-service publishes revoked session ids to Redis with the access lifetime as TTL; a second global SessionRevocationGuard in all 17 services asks, fails open, and only ever refuses
113 Prometheus holds operational metrics; health-service exports them Accepted One Prometheus container, never published, scraping health-service's existing fan-out over TLS; 30-day retention tied to the log TTL by a test; the snapshot is cached so a scrape is not a fan-out; a metric carries only allowlisted labels
114 Every archive format opens through 7-Zip compiled to WASM; ZIP keeps node-stream-zip Accepted 7z/RAR/tar/gz/bz2/xz via 7z-wasm (no native addon, lists before extracting, -smemx, passwords for A3); one shared bomb/traversal/link policy for both engines; streams bounded while writing; magic bytes decide what an upload is, so an octet-stream archive is expanded and a lying archive MIME is rejected; LGPL + unRAR (read-only use complies)
115 Grafana sits behind the admin session, not its own login Accepted auth-service mints claw_grafana (httpOnly, Secure, Lax, Path=/grafana, ≤ access-token life) for ADMIN only, signed with a key derived from JWT_SECRET; nginx auth_request checks it and overwrites X-WEBAUTH-EMAIL; Grafana auth.proxy only, no built-in admin, never published; same-origin XSS exposure is TD-041
116 Compare's judge scores every lane in one comparative call Accepted ONE judge call ranks every completed lane, anonymised A/B/C in a seeded shuffle, on a shared 0-10 scale, parsed against strict cross-field invariants; UNAVAILABLE/SKIPPED never picks a fake winner; fair max-min truncation to the judge's window; single hold via the billing chokepoint (compare-judge workflow); single-lane critic still runs per lane, its score never reaches the judge
117 OpenAI-compatible connector presets are one registry and one adapter Accepted CONNECTOR_PRESETS in @claw/shared-utilities is the only place a preset's base URL/display name/links live; one OpenAICompatibleAdapter for 15 new providers; explicit enum values (not a generic OPENAI_COMPATIBLE + presetKey) to keep existing provider-keyed tables working; base URL resolved once in getExecutionConfig; repo test enforces no duplicate URL/name outside the registry
118 Orchestration lanes share the grounding signal, not just the evidence text Accepted Compare/consensus/escalation merged research evidence into systemPrompt as prose but never set researchRequested/researchEvidence, so hasResearchGrounding() never fired and the final-user-turn reminder (proven necessary 2026-09-11) silently never reached any orchestration lane; new injectResearchEvidenceIntoContext (replaces 3 duplicate copies) sets researchGroundingInjected; shared "do not invent" instruction now in both evidence-block builders' non-empty branch; consensus synthesis now prefers cited lanes over longer uncited fabrications when the run had evidence
119 File and image requests work in every routing mode Accepted A picked model got text only (0/52 live): routing now detects file/image intent before every non-AUTO handler; MANUAL_MODEL's model writes its own file (fileWriter), local-only modes use local writers; Unicode intent in 13 locales; writer <thought> blocks stripped; separator-less pipe tables repaired
120 ClawAI owns multimodal orchestration Accepted The selected model is not the media executor: chat-service reads per-model image/audio/video capability from the connector catalog (ModelCapabilityClient, shared modelMatchKey), resolves every attachment per lane (resolveAttachmentDelivery), and the one decision drives both the payload (no image_url to a model that cannot see) and the FileDeliveryMode record; UNKNOWN falls back to the old provider-level behaviour; compare budgets for its smallest lane
121 Every research fetch goes through a DB-gated escalation chain Accepted robots.txt (RFC 9309, now with */$) on every fetch; chain official API → plain → impit TLS → Patchright → Crawl4AI/FlareSolverr/Firecrawl sidecars (compose profiles, DB-disabled) → Jina Reader → labelled Wayback copy; refusals (401/451) stop, captcha/429/404/robots-unreachable go archive-only; ≤6 attempts, ≤60 s, per-host politeness; every redirect hop SSRF-checked; one Readability extraction; fetch.served log + fetch_jobs.servedBy; Crawlee not adopted
122 Media features are plan-gated server-side at the executing service Accepted Free keeps image understanding, voice notes and 60 s video; paid tiers get allowImageGeneration, allowHelperVision, allowTextToSpeech and 600 s (maxVideoSeconds, null unlimited / 0 disabled); image-service refuses 403 PLAN_FEATURE_DISABLED before any row, hold or provider call on generate/retry/retry-alternate; chat turns it into a translated notice; helper vision keeps OCR + note; fail closed
123 Release version bumps are not deploy changes Accepted deploy-prod.sh drops a package.json/package-lock.json whose only diffs are release versions (own version, lockfile workspace versions, exact @claw/* pins) via a python3 JSON compare, fail-safe without python; frontend manifest exempt (baked APP_VERSION); BuildKit cache bounded before every build and on every exit, not only after health
124 Provider-key credit: pre-flight cap, one retry, no provider text Accepted OpenRouter 402 "can only afford N" + raw body with key-hash URL reached the user: one classifier (toProviderHttpFailure) for every provider hop maps credit refusals to PROVIDER_CREDIT_EXHAUSTED (503, translated, chain continues) and never forwards URL-carrying text; hosted default output 16,384 not ~31.7k; OpenRouter key credit read via preset creditHeadroom (connector-service, 60 s cache, service-token route) and priced from routing rate card to cap max_tokens before the PAYG hold (fail open); one retry at 90% of N as a distinct hold
125 Provider refusals recover at the chokepoint, for every provider Accepted Groq/Ollama output-limit, OpenRouter :free 429, OpenAI/Anthropic/Gemini credit outages hit users (some as raw JSON via compare/consensus Error: {…}): classifier adds output-limit and rate-limit outcomes and account-wide credit; one retry per turn (stated ceiling / 1.5 s backoff / 90% N), own hold :provider-retry; output ceilings stored on connector_models (catalog + learned, only lowered) and pre-clamped via the models-snapshot; 10-min half-open in-process breaker skips a provider whose account is empty; lane errors sanitized
126 Every AI, one workspace: ClawAI is positioned as a full AI workspace Accepted Owner decision 2026-09-26: slogan "Every AI, one workspace", four pillars (workspace, pay-as-you-go, teams, local-first); one canonical home (product-vision.md) and a code-traced flagship catalog (flagship-features.md); teams copy bounded by what exists — no SSO, no multi-member team billing; no price changes
127 Memories and context packs are stored whole and retrieved by relevance Accepted A 256-char redaction preview was stored as the memory; type stripped on update; 50K limits + 100 KB body cap; packs reached chat only if attached; REDACTED memories retrieved as null and big ones head-cut. Now: length-preserving masking, 250K, owner-scoped for-chat route with USER-scope auto-apply, relevance-ranked markdown chunks inside the rule-51 shares
128 Free local speech: one CPU container, offered as the LOCAL provider Accepted speech (speaches 0.8.3-cpu, local-ai profile, no port) serves OpenAI-compatible STT + TTS; models installed by the compose entrypoint (no lazy pull: uninstalled = 404); LOCAL reuses the OpenAI adapters, is last in STT and read-aloud order, offered only while /health answers, and unmetered via PAYG_EXEMPT_PROVIDERS; LOCAL_SPEECH_BASE_URL; image-only so no workspace/ci entry
129 Runtime V2 turns offer the admitted tools natively Accepted The native tool transport was built and never used: agent turns asked for JSON in text and gpt-oss scored 0/8. Turns now declare the admitted catalog; a native call wins over text under the same schema + admitted-tool checks; unmappable native calls get the repair turn; over-budget catalogs fall back instead of failing
130 Branch lineage, and a branch family never feeds itself Accepted Branch copy lost metadata/attachments, cost and createdAt (undefined order), reset privacy switches, and cross-thread retrieval let a branch read its source's post-fork messages. Now: branched_from_thread_id/branched_from_message_id/branch_root_thread_id (plain ids), full-field copy, switches inherited, whole branch family excluded from cross-thread retrieval, owner-scoped GET /chat-threads/:id/lineage + lineage strip
131 Quotes are structured, provenance-checked, and assembled per request Accepted Select text in one message, Quote, chips in the composer, quotes: [{sourceMessageId, text}] (at most 3 x 2,000) on POST /chat-messages; each id checked under the thread (404 QUOTE_SOURCE_NOT_FOUND); stored in metadata.quotes, content unchanged; every prompt builder wraps quoted turns with withQuotedContext; routing and memory relevance see the quote
132 Regenerate with a chosen model, and edit without losing history Accepted Regenerate takes an optional {routingMode AUTO/MANUAL_MODEL, provider, model} and now passes assertCanSendMessage + carries allowedModels/modelAccessMode like a new message (it skipped both); "Try again with…" reuses the composer's ModelPicker; branch takes cut: INCLUDE/BEFORE; "Edit in a new branch" branches BEFORE the message and prefills the edited text without sending
133 Inline citations link only through the evidence the model was shown Accepted [n] rendered as plain text; the transcript is not the list the model saw. Now the answer stores metadata.citations (the prompt's own numbering, from the same context), a remark plugin links [n] only for a stored index (never in code), only http(s) opens, memo compares citations by value
134 A model decides "remember this / add this to my context", and asks which pack Accepted Owner direction 2026-09-30, supersedes ADR-127's "no model path": multilingual pre-filter → planner model JSON verdict (memory type+sentence, pack source/name, both); saves before the answer, model confirms from a platform note; metadata.contextSave card with deep links; unnamed pack + existing packs → "which pack?" card, POST /chat-messages/:id/context-save claimed atomically; keyword path kept as fallback
135 Term discounts are per-plan settings that derive the price versions Accepted Owner direction 2026-09-30: quarterly 10% / semiannual 15% / yearly 20% off by default, per-plan basis-point columns an admin edits; longer-term PlanPriceVersion rows are derived from the monthly price and minted in one transaction (publishPriceSet); a hand-typed longer-term price is refused; existing plans re-priced once by a seeder; the badge is read off the stored prices
136 Every model is told, invisibly, where it is running Accepted Owner direction 2026-09-30: a hidden platform-identity block first in every assembled system message (never a memory or context item, never in a receipt or API response, address from PUBLIC_SITE_URL, budgeted in the window); a question about the app crawls the platform's own site; coding-agent threads still to do
137 Video generation is its own metered surface, priced per second Accepted (built, run live 2026-09-30) Owner direction 2026-09-30: PaygSurface.VIDEO, videoSeconds x videoPerUnitMicroUsd (Veo 3.1 Lite/Fast/Standard, Grok Imagine Video), seed v11, OpenAI Sora not offered (API shut down 2026-09-24); image-service jobs, chat routing and the video bubble ship with it
138 A per-user prompt library in chat-service Accepted Saved prompts with {{variables}}, tags and favourites; prompt_templates owned by chat-service, scoped by JWT user (foreign id = 404), 200-per-user cap, server extracts/validates variables only, nginx route chat-prompt-templates
139 PAYG credit unlocks image and video generation Accepted Owner direction 2026-10-01: a Free-plan user with credit may generate images and video, paid from credit; hasPaygCredit in entitlements, only allowImageGeneration is credit-unlockable
140 The free-trial length is a per-plan setting Accepted Owner direction 2026-10-01: Plan.trialDurationDays 1-3650 editable in the admin dashboard, admin can add trial days and set a user to Free for N days, days left shown in the banner and pricing
141 Public and authenticated feedback are two APIs Accepted Owner direction 2026-10-01: POST /feedback/public (no login, name+email, honeypot, per-IP and per-email limits, no uploads) and the guarded POST /feedback (saved with the user's full name); admin sees source, name, email
142 Free accounts get monthly requests on credit connectors Accepted Owner direction 2026-10-01: Plan.creditConnectorFreeRequestsPerMonth (Free = 10, null unlimited, 0 off), credit first then allowance, chat-type surfaces only, atomic per-user per-provider UTC-month counter, count given back on release
142 The runner version policy is staged, fail-open and unsigned Accepted (staged) Owner decision 2026-10-01: minRunnerVersion/allowedRunnerPlatforms/runnerPolicyMode (off default, report, enforce)/requireVersionReport on org policy; report never rejects, enforce rejects only a definite violation, any read error fails open; self-reported, attestation waits for the signing-key decision
143 Per-routine secrets are encrypted, write-only and bound to their routine Accepted (server side built; runner pending) Owner direction 2026-10-01 (F099 step 2): routine_secrets ciphertext (AES-256-GCM, ENCRYPTION_KEY, per-row nonce, AAD routine+owner+name), owner CRUD with write-only values and a foreign routine = missing 404, delivered only in the runner claim response (WEBHOOK runs need an owner opt-in), output scrubbed on completion; threat model
144 A narrower mobile device token class Accepted (backend built; no native client) Owner direction 2026-10-01 (F097): DeviceTokenClass.MOBILE in agent-service (where device tokens live), scopes runs:read/approve/cancel only, signed with a key derived from JWT_SECRET, default-deny @MobileRoute() allow-list of ten routes, per-device revoke, 600 s / 7 d / 30 d lifetimes, agent.mobile_action audit
146 Local model inference requires a login or the service token Accepted Owner decision 2026-10-02: ollama generate/chat, llama.cpp v1/chat/completions/v1/completions and llama.cpp pull-progress SSE take a user JWT or Authorization: Service <INTER_SERVICE_AUTH_TOKEN> (@AllowServiceToken(), opt-in per route); /internal/ollama/* and /internal/llamacpp/* are service-token only (ServiceTokenGuard); health stays public; every chat/routing/memory/workspace caller sends the token
147 Per-route rate limits on sign-in and sign-up Accepted Owner direction 2026-10-02: login 10/15 min per IP+address and 30 per IP, register 5/h per IP and 3/h per address, refresh 60/min; hashed Redis keys, 429 + Retry-After, no lockout, fail open; successful login refunds; global throttler keyed by user or real client IP (trusted proxy only), service token skips
150 Runtime clients get a plan-gated, metered, capped crawl Accepted (backend built; tool pending) Coding-agent audit 2026-10-02: /research/crawl/runs (crawl + extract) for user tokens; ResearchAccessGuard plan unlock before anything, owner-scoped 404, DB-level caps (runtime_crawl_configs, no env), existing WEB_FETCH meter, truthful FAILED runs; threat model
151 A picked model that fails gets a labelled substitute, then three suggestions Accepted Owner direction 2026-10-02: routing-service ranks pickedModelSubstitutes (same provider first, pricier last and marked), chat-service tries at most two after a provider failure only (never after 402/403/429), labels the answer (metadata.pickedModelFallback), and when all fail stores PICKED_MODEL_FAILED with three untried suggestedModels the bubble offers as regenerate buttons plus the model picker
152 Attachments are always read, whatever the user types Accepted Owner direction 2026-10-02: the planner and the final model always see every attachment (bounded digest, helper description for blind models), earlier files stay in follow-ups, scanned PDFs are OCR'd page by page, no second helper charge for earlier images
153 Anthropic prompt caching, billed from the cache-write rate only when it exists Accepted Owner approved 2026-10-02: native Anthropic /v1/messages transport with cache_control behind a per-model catalog switch (default OFF, admin API, no env var); a cache write is billed from cacheWritePerMillionMicroUsd only when that row exists (else plain input), the PAYG hold is sized for the premium, integer micro-USD throughout
154 Inline generation mentions are text; a named model gets the prompt without the directive Accepted Owner direction 2026-10-03: generationRequestText guards image/file/video detection, namedModelPrompt + namedModelNotice on message.routed, save needs real material
155 Every chat surface is one pipeline Accepted Owner direction 2026-10-03: Compare/Consensus/Escalation/seven labs/judge are surfaces over normal chat; one thread resolver, shared DTO fragments, one composer spread, guard spec (rule 59)
156 QA evidence is mandatory and machine-checked Accepted Owner direction 2026-10-03: 15 QA lanes per batch recorded in docs/qa-evidence, tools/qa/evidence.mjs + CI job qa-evidence, QA-WORKFLOW.md entry point (rule 60)
157 Runtime managed connector providers Accepted Additive provider-definition catalog for OpenAI-compatible runtime providers; provider key flows through connector snapshots, routing and chat; built-in enum identities and encrypted credentials stay intact
158 The model list is virtualised and the picker owns its highlight Accepted
159 ClawAI Threads uses publication and generation services Accepted
160 Threads account deletion propagates through a durable event Accepted
161 A used-up credit model falls back to an included model, with a notice Accepted
162 The free allowance is a count and a meter, and it does not cover expensive models Accepted
163 Product tours are data, point at data-tour elements, and never act for the person Accepted
164 User notifications live in auth-service and are requested by event Accepted
165 Provider keys are only served to services and prices are synced from providers Accepted
166 Web Push is an opt-in channel of user notifications Accepted
167 Thread generation progress is an owner-only step log Accepted
168 Save requests are proposed before they are saved Accepted
169 Observability is split into pages and lists every user Accepted
170 AGENTS.md is the canonical router, gates are scoped and strict, docs move with code Accepted
171 Plugins are a separate service with a curated catalog Accepted
172 Plugin installs are owner-scoped, consent is a stored grant, credentials are write-only Accepted
173 Threads assumes a 32k window for an unknown model and fails only on overflow Accepted
174 Threads refuses what cannot succeed before it queues a job Accepted
175 Threads model calls are bounded by silence, not by the clock Accepted
176 Thread generation runs jobs concurrently and fairly, and names the real cause of a failure Accepted
177 Threads Board search is a transactional Postgres projection with trigram fuzzy matching Accepted
178 The Board is a portal surface; SEO stays on /threads/[slug] Accepted
179 User MCP servers are SSRF-guarded and consent-gated Accepted
180 Threaded comments, comment votes, mentions and community notifications Accepted
181 Thread generation heals itself by failure class and a retry resumes at the failed phase Accepted
182 All-threads sitemaps list every language under each app locale Accepted
183 Threads production bug batch: prose-only safety scan, script-aware topics and slugs, health-aware teams, resolved markers, a wider queue Accepted
184 Own MCP servers reach chat through the pinned snapshot Accepted 2026-10-15
185 The in-chat consent card parks the turn on a stored, argument-bound request Accepted 2026-10-15
186 Plugins are imported from GitHub or an upload as inert, reviewed documents Accepted 2026-10-15
187 Admins curate the catalog and decide user servers from the database Accepted 2026-10-15
188 Reader change requests are answered by the owner, told to the reader and credited once applied Accepted 2026-10-15
189 Thread articles are typeset by a guarded format stage and rendered as rich, safe HTML Accepted 2026-10-16
190 Thread author preferences are arbitrated by the Judge Accepted 2026-10-25
191 Unavailable Judge results are never verified Accepted 2026-10-25
192 GitHub repository crawls stay inside the requested repository Accepted 2026-10-25
193 A failed Thread review names its real cause and fills evidence gaps once Accepted 2026-10-27
194 The owner can accept disclosed evidence gaps and the article says so Accepted 2026-10-27
195 Research uses short targeted queries, accepted gaps are verified, and reviewers stay independent Accepted 2026-10-27
196 The Node.js runtime floor is enforced at build and at deploy Accepted 2026-10-30
197 A plan limits the size and cost of one platform-paid call Accepted 2026-10-30
198 Model pickers show the plan decision and dim what it excludes Accepted 2026-10-30
199 Aggregator models are priced, and the low plans stop short of the dearest classes Accepted 2026-10-31

Clone this wiki locally