Skip to content

Architecture — Jarela

C4 — Container

C4Container
    title Containers — Jarela
    Person(user, "Developer", "Browser / installed PWA")
    Person_Ext(wa_user, "WhatsApp peer", "Phone number paired via Baileys")

    System_Boundary(b, "Jarela (Next.js process)") {
    Container(ui, "Web UI", "React 19 + Tailwind", "Chat, agents, models, memory, connections, and tools panels")
      Container(guard, "Origin / CSRF Guard", "lib/auth", "Rejects cross-origin mutating requests; same-origin enforcement")
      Container(routes, "API Routes", "Next.js Route Handlers", "REST + SSE endpoints under /api/v1")
      Container(agents, "Agent Runtime", "LangGraph + @langchain/*", "State-machine orchestration of LLM + tools; streams completions through providers")
      Container(advertisements, "Derived Agent Metadata", "lib/agents/advertisement-service + lib/stores", "Opt-in configured-model queue, encrypted fingerprints/state, bounded authorized catalog (Unreleased)")
      Container(providers, "Provider Adapters", "lib/providers", "Per-vendor SDK glue (Anthropic, OpenAI, Google, Cohere, DeepSeek)")
      Container(pricing, "Pricing and Model Updates", "lib/pricing + lib/stores/pricing + lib/stores/model-metadata", "Refreshes provider pricing, separate aggregator signals, and source-backed API model metadata")
      Container(embed, "Embeddings", "lib/embeddings", "Vector embedding generator for semantic memory recall")
      Container(docs, "Document RAG", "lib/documents (+ lib/documents/remote)", "Folder indexer + remote indexers for GitHub, Gmail, Outlook, iCloud, Jira, and Confluence; chunked + embedded into the documents tables; surfaced via documents_search (ADR-0024, ADR-0026, ADR-0029, ADR-0110)")
      Container(voice, "Voice", "lib/voice", "Gemini STT (push-to-talk) + TTS for the generate_voice tool (ADR-0017)")
      Container(mcp, "MCP Adapter", "@langchain/mcp-adapters", "Discovers & invokes external MCP tool servers")
      Container(sched, "Scheduler", "cron-parser", "Runs scheduled tasks + polls event-driven watchers; persists in DB (ADR-0022, ADR-0025, ADR-0027)")
      Container(bridges, "Bridges", "lib/bridges", "Inbound transports (WhatsApp/Baileys) routed to agents")
      Container(registry, "Run Registry", "lib/agents/run-registry", "In-memory pub/sub of in-flight agent chunks; replay buffer for reattaching EventSource clients")
      Container(notifications, "App Event Bus", "lib/notifications/bus", "Replayable app notifications and transient run activity updates")
      Container(crypto, "Crypto Envelope", "lib/crypto", "AES-GCM-at-rest for sensitive memory + OAuth tokens; OS keychain or .secret-key fallback")
      Container(proxy, "Proxy Dispatcher", "lib/proxy", "undici GlobalDispatcher; reads HTTP_PROXY env vars + encrypted proxy_config row; gates all outbound HTTP (ADR-0009)")
      Container(envsync, "Env Sync", "lib/env", "Caches user shell env for exec, terminal, delegation and MCP children; checked skill runners deliberately do not inherit it")
      Container(workflows, "Checked Execution and Workflows", "lib/skills + lib/workflows", "Exact-code consent, encrypted durable CAS state, typed choices and observed reconciliation (Unreleased)")
      ContainerDb(db, "SQLite", "node:sqlite writer + @appthreat/sqlite3 worker readers", "Local checkpoints, memory, settings, schedules, proposals, bridges; selected heavy reads use WAL reader workers")
      ContainerDb(filestore, "File Store", "lib/files + ~/.jarela/files/", "Content-addressed image/file refs plus generated binary artifacts; served by /api/v1/files/[name]")
      ContainerDb(extdir, "Extension dirs", "filesystem (~/.jarela/{providers,tools}/)", "Drop-in .cjs files for external providers + tools, hot-loaded per request (ADR-0013)")
    }

    System_Ext(anthropic, "Anthropic", "Claude")
    System_Ext(openai, "OpenAI", "GPT")
    System_Ext(google, "Google GenAI", "Gemini (LLM + STT/TTS)")
    System_Ext(deepseek, "DeepSeek", "Chat completions + optional standalone Search/Fetch APIs")
    System_Ext(cohere, "Cohere", "Embeddings")
    System_Ext(openrouter, "OpenRouter", "Model route pricing catalog")
    System_Ext(litellm, "LiteLLM", "Model pricing catalog")
    System_Ext(mcps, "MCP Servers", "External tool providers (stdio / SSE)")
    System_Ext(mcpreg, "MCP Registry", "registry.modelcontextprotocol.io — discovery only (ADR-0014)")
    System_Ext(github, "GitHub API", "Issues / PRs / Repos (native github_* tools, ADR-0015) + Copilot OAuth (model provider)")
    System_Ext(atlassian, "Atlassian Cloud", "Jira REST + Confluence REST (tools + document-RAG ingest, ADR-0026)")
    System_Ext(mailproviders, "Mail Providers", "Gmail, Microsoft Graph, and iCloud Mail (remote document indexing)")
    System_Ext(whatsapp, "WhatsApp Web", "Baileys-paired endpoint")
    System_Ext(usershell, "User shell rc / Windows User env", "Credential/env source for general subprocess tools; deliberately excluded from checked skill execution")
    System_Ext(browserext, "Jarela Browser Extension", "Chrome MV3 — element picker, posts captures to localhost (ADR-0018)")
    System_Ext(skillowner, "Skill Process Owner", "Transient Windows Job / Linux delegated cgroup broker; full-trust, not a sandbox (Unreleased)")

    Rel(user, ui, "HTTPS")
    Rel(ui, guard, "fetch + EventSource")
    Rel(guard, routes, "allow same-origin")
    Rel(routes, agents, "invoke")
    Rel(routes, advertisements, "local operator policy / inspect / regenerate")
    Rel(advertisements, providers, "explicit configured generation only")
    Rel(advertisements, db, "encrypted derived state + bounded usage ledger")
    Rel(agents, advertisements, "fresh authorized descriptive catalog")
    Rel(routes, workflows, "unlocked local execution and workflow controls")
    Rel(agents, workflows, "explicit permission-gated tools")
    Rel(workflows, db, "encrypted results/state and approval metadata")
    Rel(workflows, skillowner, "captured bytes, exact argv and bounded raw streams")
    Rel(routes, voice, "STT/TTS + generate_voice")
    Rel(agents, mcp, "tool calls")
    Rel(agents, providers, "stream completion")
    Rel(agents, pricing, "invoke pricing maintenance")
    Rel(pricing, proxy, "fetch pricing and official model documentation")
    Rel(pricing, db, "persist model/version/capability observations")
    Rel(routes, pricing, "read catalog display metadata")
    Rel(proxy, openrouter, "HTTPS pricing catalog")
    Rel(proxy, litellm, "HTTPS model pricing catalog")
    Rel(agents, registry, "broadcast chunks")
    Rel(agents, notifications, "publish run activity lifecycle")
    Rel(routes, registry, "subscribe (GET SSE) / submit (POST 202)")
    Rel(routes, notifications, "subscribe / replay via GET SSE")
    Rel(routes, filestore, "POST /attachments/{images,files} + GET /files/[name]")
    Rel(routes, sched, "register / trigger")
    Rel(sched, agents, "run scheduled job")
    Rel(wa_user, whatsapp, "message")
    Rel(whatsapp, bridges, "stream events")
    Rel(bridges, agents, "deliver as user turn")
    Rel(agents, db, "checkpoint / memory (via crypto)")
    Rel(agents, crypto, "encrypt sensitive at rest")
    Rel(agents, embed, "embed memory writes")
    Rel(agents, filestore, "materialize attachment refs for provider calls")
    Rel(routes, filestore, "GET /api/v1/files/[name]")
    Rel(crypto, db, "store ciphertext")
    Rel(providers, anthropic, "HTTPS")
    Rel(providers, openai, "HTTPS")
    Rel(providers, google, "HTTPS")
    Rel(providers, deepseek, "HTTPS")
    Rel(agents, deepseek, "HTTPS via web tools, only with separate DeepSeek Search & Fetch opt-in")
    Rel(voice, google, "HTTPS (Gemini STT/TTS)")
    Rel(embed, cohere, "HTTPS (embed)")
    Rel(mcp, mcps, "stdio / SSE")
    Rel(routes, mcpreg, "HTTPS (picker search)")
    Rel(routes, github, "HTTPS")
    Rel(routes, atlassian, "HTTPS (tools)")
    Rel(docs, github, "HTTPS (remote indexer)")
    Rel(sched, docs, "sweep ~10 min")
    Rel(docs, mailproviders, "HTTPS / IMAP (remote indexers)")
    Rel(docs, atlassian, "HTTPS (remote indexer)")
    Rel(docs, embed, "embed new chunks")
    Rel(docs, db, "document_sources / documents / document_chunks")
    Rel(agents, docs, "documents_* tools")
    Rel(agents, extdir, "scan per request (cache-busted require)")
    Rel(envsync, usershell, "read on boot")
    Rel(envsync, db, "write encrypted via crypto")
    Rel(proxy, db, "read proxy_config (via crypto)")
    Rel(providers, proxy, "outbound via GlobalDispatcher")
    Rel(routes, proxy, "outbound via GlobalDispatcher")
    Rel(browserext, routes, "POST /api/v1/page-capture (loopback)")

Module map

The C4 diagrams show runtime connections; this map describes source ownership and the boundaries new code should use.

Module Responsibility
app/ App Router screens and HTTP adapters; guard and validate requests before calling lib/.
components/, hooks/, contexts/ Domain UI and browser state; consume API contracts rather than persistence internals.
api/ Shared API client, response types, and request contracts.
lib/agents/ Agent construction, turn execution, prompt preparation, validation, and run lifecycle.
lib/providers/ Provider adapters, model routing, pacing, and vendor-specific SDK behavior.
lib/tools/, lib/mcp/ Capability-aware tool registry and domains; MCP discovery and invocation.
lib/stores/, lib/db/, lib/crypto/ Persistent state, SQLite access, checkpoints, and encryption.
lib/scheduler/, lib/bridges/, lib/streaming/, lib/notifications/ Background triggers, inbound transports, run updates, and alerts.
packages/ Separately scoped npm workspace packages.

Dependency direction: UI → API routes → runtime modules → registries, adapters, and stores → local persistence or configured external services.

The unreleased credential editor keeps API dispatch and form state in hooks/useIntegrationCredential.ts and authorization sessions in hooks/useIntegrationOAuth.ts. IntegrationCard composes form, action-bar, and setup-guide views without calling credential APIs. Its typed default/create/edit mode and lifecycle contracts are documented in the UI hook specification. This stays within the existing UI-to-API boundary; persistence and vendor authorization remain server-owned.

The unreleased complexity cleanup also keeps component-local controllers responsible for loading, mutations, feedback, and resource lifetimes, with cohesive view sections handling rendering. Agent modules separate admission, context assembly, streaming, retries, persistence, and accounting into typed stages while retaining the existing queues and stores. Dashboard attribution uses the same accumulation operation for daily and global scopes. These are internal ownership changes, not additional containers, processes, persistence schemas, or external dependencies; the diagrams above are unchanged.

The unreleased internal skill-consent contract in lib/skills/execution-consent.ts owns run-local, bounded one-use decision state. It reuses the manifest JSON complexity guard; it does not modify lib/stores/pending-actions.ts, introduce persistence, or register an execution tool. Snapshot capture, human approval UI/API integration and process ownership remain blocked work under ADR-0112. The contract is not an immutable snapshot or a standalone authorization boundary.

Unreleased skill-bundle reading reuses the repository resolver in lib/skills/index.ts for instructions and resources. lib/skills/frontmatter.ts uses the yaml parser for metadata and opt-in compatibility diagnostics; it never rewrites source files. lib/skills/bundle-files.ts shares portable-path and bounded regular-file reads between resource access and execution-manifest inspection. These are read-only operations, not a script runner or immutable snapshot boundary; see the extension guide.

Unreleased deterministic built-in skill steps live in portable dependency-free modules under lib/skills/builtins/{personal-briefing,memory-organization}/scripts/. The fixed read-only tools validate_personal_briefing and plan_memory_cleanup import those implementations in-process; no user-controlled module resolution or subprocess is introduced. Their standalone JSON CLI adapters are for hosts that independently authorize Node execution, not a checked-runner workaround. The memory planner consumes supplied fact projections only and never accesses the store or mutates it. Bundle files are included in standalone build tracing. See script-backed built-ins. The unreleased Memory Organization execution manifest adapts those same two portable scripts for artifact/protocol contracts and explicit execution; bounded anyOf schemas represent nullable expiry and result variants. Ordinary discovery and the fixed typed tool are unchanged. Inspection remains execution-unavailable; inspection itself never authorizes or invokes the separate execution service. The internal lib/skills/execution-output.ts collector reuses result validation over bounded copied pipe bytes with strict UTF-8. It creates no process and cannot attest exit status, authorization or process-tree cleanup; a captured schema-valid value is not a successful execution.

The unreleased app integration follows ADR-0114. lib/stores/skill-run-staging.ts owns explicit run directories under the data directory; lib/skills/execution-runner.ts consumes the existing artifact, consent and output contracts and an available platform process-owner adapter. Local APIs, permission-gated tools and Skills controls share the live execution service. The bounded sequential definition/coordinator under lib/workflows/ uses an encrypted lib/stores/workflow-runs.ts SQLite CAS store, not generic memory or a second retry loop around model workflow progress. Startup performs bounded retention cleanup, never implicit recovery or replay. Shutdown drains workflows and script owners before closing SQLite. Consent grants/captured executable bytes stay in memory; restart invalidates pending executable approvals.

Unreleased W3/W4 primitives in lib/workflows/authoring.ts are pure bounded text builders, not another executor or persistence system. lib/tools/general/authoring.ts registers Config/read tools for inert external tool scaffolds and exact-diff local contribution review bundles. Discovery and repository/test/rights claims remain caller-supplied; output revisions bind the selected content and scope, never authorization. Built-in procedures require independent evidence and separate execution/install/commit/push/PR gates. The shared secret-marker list under lib/security/secret-patterns.mjs supports both the commit scanner and unsuppressible bundle checks; neither proves privacy.

flowchart LR
  Skills["Authoring / contribution skills"] --> TextTools["Config/read text builders"]
  TextTools --> Inert["Inert source / tests / local diff + revision"]
  Inert --> Review["Independent validation + human review"]
  Review --> Approval["Local exact-source single-stage approval"]
  Approval --> Lifecycle["Bounded extension / contribution services"]
  Lifecycle --> Existing["Existing owned runner / drop-in / permission / repository paths"]

lib/workflows/extension-service.ts owns bounded single-effect authoring operations, not a model loop or another general workflow executor. It reuses the checked staged Node runner, existing drop-in directory and permission stores. Current source, canonical directory identities, agent permissions and configuration bind each review. Pending-action approvals apply only one stage. Exact code/diff previews are local-only; result bytes use existing encryption. Ephemeral operation state/grants are invalidated on restart. The existing staging ownership format adds retained for user-owned inert source, so cleanup does not erase drafts or recover consent. See ADR-0115.

Unreleased lib/agents/advertisement-service.ts is an in-process coalescing metadata worker, not a second agent executor. Shared config-store post-save hooks invalidate routing sources; the existing canonical tool authority resolver separates permission from transient readiness. Explicit configured-model policy bounds egress and usage. Unreleased advertisement input selects only tools whose permission is exclusive to one saved agent; a single-agent roster contributes no tools. Config changes refresh all agents, and source fingerprints recheck the selected tool metadata before generation and publication without changing execution permissions. Encrypted rows use the existing memory_store, excluded from ordinary memory/recall; fingerprints and generation IDs reject stale publication after asynchronous work. The existing delegate catalog consumes only currently valid descriptive data with deterministic overflow and explicit legacy/error provenance. Dispatch and shared task budgets remain in the existing delegation path. See ADR-0116.

lib/workflows/contribution-service.ts independently inspects canonical clean Git state, selected bytes/base, existing local Vitest runtime and hook baselines. Approved tests use a user-owned isolated topic worktree under the workspace's ignored node_modules/.cache/jarela-contributions. Its canonical local bundle comes from observed validation, not a caller's test report. Distinct local approvals gate commit, create-only topic push and draft PR using existing Git and GitHub authentication. No package publication or second model executor is added; source/diff previews stay local while model inspection and approval results return metadata. Approved Git mutations/hooks/helpers use native-owned execution; inert discovery does not execute configured helpers or retrieve credentials.

flowchart LR
  UI["Local UI / explicit agent tools"] --> Coordinator["Bounded workflow service + trusted host"]
  UI --> Review["Exact-code / plan approval"]
  Coordinator --> RunStore["Encrypted SQLite CAS + retention"]
  Coordinator --> Runner["Live checked skill execution service"]
  Review --> Consent["Exact one-use consent and current policy"]
  Runner --> Staging["Run-owned captured bytes"]
  Runner --> Consent["Exact one-use consent and current policy"]
  Runner --> Owner["Windows Job / delegated Linux cgroup owner"]
  Owner --> Capture["Bounded raw output and schema checks"]

Unreleased skill execution consent policy is managed by the Security UI through app/api/v1/security/skill-execution/route.ts, restricted to unlocked loopback requests. lib/stores/app-settings.ts validates and persists it in the existing memory_store namespace; generic memory mutation paths reject this reserved key. Server-issued approval IDs/timestamps persist beside checksum-scoped trust; unchanged records survive restart, while revocation removes the grant. Receipt updates use the canonical lib/db/transaction.ts helper and ignore caller-supplied receipt metadata. Draft scopes without a receipt cannot waive consent. lib/skills/execution-policy-server.ts reads current settings/safety mode on each evaluation, while the pure policy module distinguishes blocking, invocation or workflow consent requirements, and exact scoped revision trust. No runner calls this policy yet: it grants neither sandboxing nor execution by itself. See ADR-0112.

Document retrieval applies the global recall policy from lib/stores/app-settings.ts inside lib/documents/search.ts, before returning chunks to tools. Exact source/path exclusions and semantic topic exclusions cover existing and future indexed content. Topic-filter failures block retrieval instead of falling back to unchecked text. Only the user-facing Documents search preview exposes an explicit override; documents_search has no override parameter. The policy uses the existing settings store. A nullable chunk embedding model signature ensures topic filtering never compares vectors from different models, even when dimensions match (ADR-0109); existing sources need reindexing before topic rules can be evaluated.

Mail document sources share a validated scope/limit/retention contract (ADR-0110) across Gmail, Outlook, and iCloud. The existing scheduler dispatches Gmail and Graph scans through their OAuth clients and iCloud scans through the optional iCloud package's read-only IMAP adapter. Credentials remain in the existing encrypted integration store (or configured environment); document archives remain in the existing document tables. New sources retain indexed mail after upstream moves/deletion; deleting the source removes its archive. Mail and calendar tools use the same topical categories and capability rules across providers: read for inspection, write for owned-content changes including trash/event deletion, and execute for mail sending where supported.

iCloud sending reuses the package's credential resolver and Nodemailer SMTP transport (smtp.mail.me.com:587, mandatory STARTTLS); calendar/reminders stay on the existing tsdav/ical.js CalDAV/iCalendar stack. The optional stored mail_address is the SMTP authentication and sender identity when it differs from the Apple ID; no agent-controlled From or server endpoint is exposed. The send tool follows the same explicit agent permission and execute classification as Gmail/Outlook, with no new per-send confirmation mechanism.

  • Keep API routes focused on transport, origin checks, and request validation.
  • Let lib/agents/ coordinate providers and tools; connect those capabilities through their adapters and registries rather than importing private internals.
  • Keep persistent state in stores and database modules; route sensitive values through lib/crypto/.
  • Reuse the existing process and agent queues for background work. Do not add a second runtime or broker for a feature-specific module.

For tool folder placement and registration rules, see the development guide.

Shared utilities

Two cross-cutting helper layers sit underneath every container above. They are not their own boundary on the diagram — every container reaches into them — but they are load-bearing:

  • lib/utils/ — getOrCreateGlobal (singleton pinning across HMR), parseJsonSafe, stripHtml, truncateBytes, createOAuthFlowStore. Replaces ~13 duplication patterns surfaced by the audit.
  • lib/ui/tabs.ts — the canonical tab vocabulary shared by AppContext and the pure deep-link parser, keeping URL recognition aligned with available tabs.
  • lib/api/ — errorResponse / notFoundResponse / createdResponse / validateBody and the per-resource *ToResponse row→JSON serializers shared between list and [id] route handlers.

Unreleased runtime-operation consolidation also shares:

  • Stored message decoding between chat rendering and history assembly. This preserves legacy attachment recognition and plain text fallback; it is not request-schema validation or a text projection.
  • Best-effort context deadlines between foreground recall and history. The helper bounds waiting, not execution, and returns the caller's fallback on rejection or expiry. History keeps its non-positive unlimited-wait adapter; foreground recall keeps an immediate timer at zero. Required work and authorization checks must not use this fallback contract.
  • Tolerant app-settings reads for settings whose owners permit a null fallback. Strict skill-execution and document-recall policy readers remain separate and propagate corruption or schema failures. Database errors are not converted to fallback settings.
  • Nullable agent enum overrides share omission/clear/allowed-choice handling in agent configuration; usage strategies reuse their canonical choice list. Omitted legacy values remain untouched, while explicit invalid values clear the override. Text, boolean and citation controls retain their distinct normalization rules.
  • Stored summary topics share array decoding in JSON utilities. UI and compaction pipeline topic types remain separate; decoding does not validate topic ranges or strip pipeline fields.
  • The update route reuses the lifecycle version reader. Health probes and image generation share the one-level cause formatter in error utilities; other error owners retain ordinary message-only formatting.

The pattern catalog lists focused behavior tests. Coverage must be checked for the particular helper and its callers, not inferred for an entire utility layer.

Review patterns and architecture drift

Before adding or reviewing a runtime operation, compare it with the curated machine-readable architecture pattern catalog. It records common implementations, their behavior tests, and important exceptions for request validation, response serialization, tolerant JSON parsing, bounded caches, and built-in tool registration. The catalog is deliberately small: it guides searches and review decisions; it does not replace this module map, define every dependency rule, or prove that a new implementation is architecturally sound.

Reviewers should compare behavior and ownership, not just syntax. Reuse an existing helper when its contract fits; extract only when implementations share a stable invariant; keep distinct behavior separate when validation, failure handling, ordering, security, or lifecycle differ. Record which decision was made and the evidence in the change review. npm run lint validates that catalog entries point to existing source and test files.

Architecture audit snapshot

The 2026-08 whole-repo audit found four areas that should shape future refactors and dependency upgrades:

  • State ownership: UI-only preferences still have direct localStorage writers. Any new persistent state should go through lib/stores or an explicit documented browser-cache exception.
  • Route validation: API routes mix direct zod safeParse() calls with the shared validateBody() helper. New route handlers should use the helper so error bodies stay consistent.
  • Tool/plugin capability metadata: built-in tools declare read/write/execute capability tiers, while external .cjs tools still default to execute. Extension-contract changes here should be handled with an ADR and deprecation window.
  • Secrets and external process boundaries: credentials, MCP env, proxy config, and hot-loaded plugins remain the highest-risk security surfaces. Upgrades must preserve encryption/redaction and the single Next.js process invariant.

C4 — Component (Agent Runtime)

flowchart LR
    A[API Route /api/v1/*] --> G0[Origin Guard<br/>lib/auth]
    G0 --> B[Agent Factory<br/>lib/agents]
    B --> C[Provider Adapter<br/>lib/providers]
    C --> RL[Shared request pacing and concurrency<br/>lib/providers/rate-limit]
    B --> D[Tool Registry<br/>lib/tools<br/>category × capability]
    B --> E[MCP Client<br/>lib/mcp]
    C --> F[(LLM Provider)]
    C --> XP[External providers<br/>~/.jarela/providers/*.cjs<br/>hot-loaded]
    D --> G[Built-in tools]
    D --> XT[External tools<br/>~/.jarela/tools/*.cjs<br/>hot-loaded]
    D -."delegate_to_agent reaches Agent Runtime internals<br/>(run-thread, model-router, stream-collector) directly<br/>— the one intentional bidirectional edge".-> B
    D --> EM[Embeddings<br/>lib/embeddings]
    D --> FS[File Store<br/>lib/files<br/>~/.jarela/files/]
    A --> V[Voice<br/>lib/voice<br/>STT + TTS]
    V --> F
    EM --> F
    E --> H[(External MCP servers)]
    B --> I[Checkpoint Store<br/>lib/db]
    I --> J[(SQLite ~/.jarela)]
    B --> K[Memory Store<br/>lib/stores]
    B --> HR[Harness Resolver<br/>lib/agents/harness]
    HR --> K
    B --> PR[Prepare<br/>lib/agents/prepare<br/>system prompt + history window]
    G0 --> WC[Warm Context Coordinator<br/>lib/agents/context-boundary<br/>+ warm-summary-background]
    B --> WC
    WC --> C
    WC --> TS[Thread Store<br/>lib/stores/threads]
    TS --> J
    SG[System signal outbox and delivery leases<br/>lib/stores/system-signals] --> PR
    G --> SG
    SC --> SG
    SG --> J
    B -.transcript plus acknowledgment transaction.-> SG
    PR --> B
    B --> OV[Output Validator<br/>lib/agents/output-validator]
    OV -.post-turn check.-> B
    K --> CR[Crypto Envelope<br/>lib/crypto]
    CR --> J
    K --> J
    B --> L[Notifications<br/>lib/notifications]
    BR[Bridges<br/>lib/bridges] --> B
    SC[Scheduler<br/>lib/scheduler] --> B
    EN[Env Sync<br/>lib/env<br/>boot probe + Sync button] --> K
    EN -."full shell env<br/>(ADR-0072)".-> D
    EN -."full shell env<br/>(ADR-0072)".-> E
    RR[Run Registry<br/>lib/agents/run-registry] -.pub/sub + replay buffer.-> A
    RR -.activity lifecycle updates.-> L

Tool proxy execution (unreleased)

invoke_tool dispatches through the same executeTool runtime as direct calls. It preserves trusted run configuration, workspace overrides, progress writer and call correlation, cancellation, credential routing, permission overlays, delegation context, and coordinator-owned settlement.

The target owns its deadline, background receipt, and streaming controls; the proxy does not start a second timeout or background operation. Top-level deadline_ms, async_run, and stream are defaults for the target; values inside args or decoded args_json take precedence. Output-size safeguards remain in place, reserving proxy-envelope overhead for paged results. Permissions, category toggles, credentials, completion-turn read-only rules, and the target's argument schema are not bypassed.

Key Flow — User sends a chat turn

Durable lifecycle signals

The result cache/settlement retry state, per-thread queue, active-run registry, cold-attach waiters, and config snapshot are process-wide, not route-module locals. Separate Next bundles therefore serialize the same thread and can attach to its active run. Owner changes quarantine stale deliveries; durable deletion/expiry also revokes cached result access.

System signals use the existing SQLite database and agent queues. Operations, immutable outbox events, and delivery leases are separate records (ADR-0091). The dynamic prompt suffix receives only the owning agent/thread's eligible events, in journal sequence order, with bounded count and size. Redelivery can occur after failures; no exactly-once external tool effects are claimed.

Restart requests persist acceptance before exiting. At the end of bootstrap, after protected state is unlocked, the new runtime completes accepted restart operations from a different instance and marks unfinished background tools as outcome unknown. No interrupted tool is automatically replayed.

Successful assistant transcript insertion and matching lease acknowledgment share one SQLite transaction. Silent turns persist an internal receipt. Failed/aborted deliveries use delayed retries; exhausted attempts remain in dead-letter records. The existing read_agent_config tool reports delivery counts and oldest-event times, and dead-letter transitions appear in Logs.

Restart and authorized background-tool completion events wake the owning agent through the existing scheduler/thread queues, with a two-thread cap, backoff, and recursive-wake suppression. Config and approval/job events are consumed on the next eligible turn. Results are encrypted in SQLite and retrieved through the existing result tools with owner-thread checks; legacy process-local references are suppressed after restart. Signals describe only their named outcome, not permission to perform new actions.

sequenceDiagram
    actor U as User
    participant UI as Web UI
    participant G as Origin Guard
    participant ATT as /api/v1/attachments
    participant API as /api/v1/threads/:id/run
    participant THREAD as /api/v1/threads/:id
    participant PIN as /api/v1/threads/:id/context-pin
    participant AG as Agent Runtime
    participant DB as SQLite
    participant LLM as LLM Provider

    U->>UI: drags context boundary line
    UI->>PIN: PATCH { hot_since_seq } (ADR-0096)
    PIN->>DB: validate seq belongs to thread
    alt clear pin
      PIN->>DB: clear hot_since_seq immediately
      PIN-->>UI: cleared committed state
    else move pin
      PIN->>AG: enqueue background boundary compaction
      PIN-->>UI: committed state + pending_hot_since_seq
      AG->>AG: optionally align to a nearby topic start
      AG->>DB: read chat and automation rows before boundary
      AG->>LLM: build chat + automation summaries
      LLM-->>AG: summaries
      AG->>DB: compare-and-set transaction writes cursor + coverage
      Note over AG,DB: On failure, keep committed state and do not publish the candidate
      opt summary and commit succeed
        DB-->>AG: committed hot_since_seq
      end
    end
    loop while a cursor is pending
      UI->>THREAD: GET /threads/:id
      THREAD-->>UI: committed state + pending_hot_since_seq
    end
    Note over UI: Divider and Locate target the committed source seq

    U->>UI: types message + adds attachments
    opt image / binary file attachment
      UI->>ATT: POST multipart file
      ATT->>DB: persist metadata in message refs later
      ATT-->>UI: image_ref / file_ref
    end
    UI->>G: POST /threads/:id/run (message, hot_since_seq?, channels?)
    G->>G: check Origin / Sec-Fetch-Site
    G->>API: forward if same-origin
    API->>API: enqueue per-thread run
    API-->>UI: 202 Accepted + queue position
    API->>AG: startRun when the thread slot is available
    opt non-null requested cursor differs from committed cursor
      AG->>AG: schedule compaction, use committed cursor for this turn
    end
    AG->>DB: load checkpoint + thread.hot_since_seq
    AG->>DB: select candidate rows where rowid >= committed hot_since_seq
    Note over AG,DB: Message, hot-turn, and token budgets may narrow eligible rows
    AG->>ATT: materialize refs for the newest turn only (ADR-0090)
    Note over AG: Older image_ref/file_ref in the window become a<br/>placeholder, model calls view_attachment to re-read one
    opt no matching chat summary for the effective boundary
      AG->>LLM: build bounded ephemeral chat fallback if needed
      LLM-->>AG: summary for this prompt
      Note over AG: Automation summaries are included only on an exact coverage-seq match
    end
    UI->>API: GET /threads/:id/run (EventSource subscribe)
    AG->>LLM: stream completion
    LLM-->>AG: tokens
    AG-->>API: broadcast chunks (run-registry)
    API-->>UI: SSE: text_delta / tool_call / done
    UI-->>U: render
    AG->>DB: save checkpoint
    UI->>THREAD: GET /threads/:id (refetch incl. warm_summary)
    THREAD-->>UI: committed summary metadata
    UI-->>U: warm summary card hydrates

Key Flow — Inbound bridge message (WhatsApp)

sequenceDiagram
    actor P as WhatsApp peer
    participant WA as WhatsApp Web
    participant BR as Bridges (Baileys)
    participant DSP as bridges/dispatcher
    participant AG as Agent Runtime
    participant DB as SQLite
    participant N as Notifications

    P->>WA: send message
    WA-->>BR: stream event
    BR->>DSP: normalize → InboundMessage
    DSP->>DB: route JID → agent_id + thread_id
    DSP->>AG: deliver as user turn
    AG->>DB: append + run graph
    AG-->>DSP: reply text
    DSP->>BR: send outbound
    BR-->>WA: deliver to peer
    AG->>N: SSE push to any open UI

Key Flow — Agent-led integration setup (ADR-0010)

sequenceDiagram
    actor U as User
    participant UI as Web UI
    participant AG as Agent Runtime
    participant REG as Manifest Registry
    participant PA as pending_actions
    participant API as /api/v1
    participant DB as SQLite

    U->>AG: "connect my gmail"
    AG->>REG: list_integrations / get_integration_setup("gmail")
    REG-->>AG: manifest (steps + proposes kinds)
    AG->>PA: propose_config_change(enable_integration, payload)
    PA-->>UI: ApprovalsBanner shows pending row
    U->>UI: Approve → secret-collection modal
    UI->>API: POST /pending-actions/:id/approve { extras: { client_id, client_secret } }
    API->>DB: applyEnableIntegration(payload, extras)
    AG->>PA: propose_config_change(start_oauth, { integration_id })
    U->>UI: Approve
    UI->>API: POST /pending-actions/:id/approve
    UI->>API: POST /integrations/gmail/oauth/start
    API-->>UI: { authorize_url }
    UI->>U: window.open(authorize_url)
    Note over AG,DB: Agent never sees secrets — collected by approval UI directly

The same propose_config_change → ApprovalsBanner → applyAction loop also carries agent-driven harness edits (ADR-0036): the agent proposes upsert_harness to create or modify a custom preset, and update_agent (with harness_id) to point an agent at it. Built-in harnesses remain read-only and the global default pointer stays UI-only — both invariants enforced inside applyAction.

Tool registry — category × capability

Every built-in tool registers with the tool registry on two orthogonal axes (ADR-0038):

  • ToolCategory — topical group (Memory, Files, Mail, Atlassian, …). Drives the Agent editor sidebar layout. Says nothing about safety.
  • Capability — safety class (read | write | execute).
  • read: pure observation, no mutations anywhere (memory_read, web_fetch, jira_search).
  • write: mutates local Jarela-owned state — SQLite tables, the file store, files in user-controlled directories (memory_write, file_write, schedule_task, documents_add_local_source).
  • execute: invokes external systems with side effects users see outside Jarela, OR runs arbitrary code (local_exec, generate_image, delegate_to_agent, claude_delegate, codex_delegate, jira_create_issue, gmail_create_draft).

Files with mixed capabilities (memory, files, schedule, atlassian, github, gmail, outlook, calendar) declare every capability bucket in a single registerLangChainPackage({ category, integrationId?, tools: { read?, write?, execute? } }) call. integrationId names the INTEGRATIONS key backing the tools, so the permission layer can deny them when that integration is unconfigured (ADR-0078). External (JARELA_TOOLS_DIR) and MCP tools default to execute until a manifest field overrides it. Consumers — a planned per-capability approval gate, UI badges, the ADR-0037 validator's citation rules — switch on the three values exhaustively. The capability-coverage.test.ts runtime check asserts every registered tool has a capability so a new tool cannot land uncategorised.

Output validator (anti-fabrication)

lib/agents/output-validator post-checks every assistant turn before the terminal done chunk leaves stallRetryStream (ADR-0037). It cross-references the assistant text against the tool_call chunks issued in the same turn and flags four fabrication shapes — claim-without-tool, citation-of-an-unregistered-tool, citation-of-an-uncalled-tool, and summary-without-action. A flagged turn triggers the same retry path the stall detector uses (↻ separator + a reason-aware synthetic-user nudge); if the retry budget is exhausted, persistAssistantMessage appends a visible *⚠️ Output validator flagged: ...* footer to the persisted message. The validator runs entirely on regex — no extra LLM call per turn — and is exercised by both unit tests and a named-scenario regression set (npm run test:eval) seeded with real observed hallucinations.

Key Flow — Scheduled background task

sequenceDiagram
    participant S as Scheduler
    participant AG as Agent Runtime
    participant DB as SQLite
    participant N as Notifications

    S->>DB: poll due jobs (cron)
    S->>AG: run(jobId)
    AG->>DB: load state
    AG->>AG: execute graph
    AG->>DB: persist result
    AG->>N: notify (if configured)

Key Flow — Delegate a coding task to Claude Code (ADR-0071, ADR-0073)

sequenceDiagram
    participant AG as Agent Runtime
    participant CD as claude_delegate tool
    participant MB as claude-memory-bridge
    participant DB as SQLite (memory_store, claude_delegate_sessions)
    participant CLI as claude CLI (child process)
    participant FS as ~/.claude/projects/.../memory
    participant UI as Chat UI (ToolList card)

    AG->>CD: claude_delegate({ task, cwd, ... })
    CD->>CD: resolveSafetyGate() — JARELA_TOOL_SAFETY tier
    CD->>MB: syncIn(cwd, claude-sync:<hash>)
    MB->>DB: listMemory(namespace)
    MB->>FS: write *.md + MEMORY.md index
    CD->>DB: getSession(project_key) / rememberSession
    CD->>CLI: spawn -p --output-format stream-json --permission-mode <gated>
    loop each stream-json turn
      CLI-->>CD: stream-json event (text / tool_use)
      CD->>CD: armTimer() — resets the subprocess idle timer
      CD->>UI: config.writer() -> "custom" stream -> tool_progress chunk (live step, also resets the outer wallclock)
    end
    CLI-->>CD: stream-json result event
    CD->>MB: syncOut(cwd, namespace, manifest)
    MB->>FS: read *.md
    MB->>DB: putMemory / deleteMemory
    CD->>CD: gitDiffSummary(cwd) — verify loop
    CD-->>AG: { result, changes, permission_denials, sync, steps }

Key Flow — Supervise a Codex coding task (ADR-0086)

sequenceDiagram
    participant AG as Agent Runtime
    participant CD as codex_delegate tool
    participant DB as SQLite (codex_delegate_sessions)
    participant CLI as codex CLI (child process)
    participant UI as Chat UI (ToolList card)

    AG->>CD: codex_delegate({ task, cwd, feature?, background? })
    CD->>DB: getSession(project_key)
    CD->>CLI: spawn exec or exec resume with sandbox policy
    loop each JSONL event
      CLI-->>CD: command or agent-message event
      CD->>UI: tool_progress chunk (live transcript)
    end
    CLI-->>CD: thread id and final result
    CD->>DB: rememberSession(project_key, thread_id)
    CD->>CD: gitDiffSummary(cwd) — verify loop
    CD-->>AG: { result, changes, transcript, resumed }
    opt background run
      AG->>CD: codex_delegate_status(job_id, poll or cancel)
      CD-->>AG: steps, status, result
    end

Key Flow — Browser-extension page capture (ADR-0018)

sequenceDiagram
    participant U as User
    participant BX as Browser Extension (MV3)
    participant SW as Extension Service Worker
    participant CS as Content Script (picker)
    participant PR as proxy.ts
    participant API as /api/v1/page-capture
    participant DB as SQLite
    participant BUS as Notifications Bus
    participant UI as Web UI (open tab)

    Note over SW,API: Heartbeat — every 15s
    SW->>API: GET /api/v1/health
    API-->>SW: 200 OK ⇒ icon enabled

    U->>BX: Click toolbar icon
    BX->>SW: action.onClicked
    SW->>CS: scripting.executeScript (idempotent)
    CS-->>U: Overlay banner + element-tracking outline
    U->>CS: Click target element (or ESC to cancel)
    CS->>SW: runtime.sendMessage(jarela-capture-visible-tab)
    SW->>SW: chrome.tabs.captureVisibleTab → PNG dataURL
    SW-->>CS: dataURL
    CS->>CS: crop to element bounding rect (OffscreenCanvas, devicePixelRatio)
    CS->>SW: runtime.sendMessage(jarela-capture, {text, selector, screenshot})
    SW->>PR: POST /api/v1/page-capture (Origin: chrome-extension://…)
    PR->>PR: Loopback Host check ✓, carve-out skips Origin check
    PR->>API: forward
    API->>API: Truncate text to 100KB UTF-8, validate screenshot ≤ 4MB base64
    API->>DB: addMessage(thread, "user", [text, image] when screenshot present)
    API->>BUS: publish(thread_message_added)
    API-->>SW: 200 {thread_id, msg_id, truncated, originalBytes}
    SW-->>CS: ack
    CS-->>U: Flash + "✈ Sent" pill animation + success banner
    BUS-->>UI: SSE: thread_message_added
    UI->>UI: dispatch jarela:thread-updated → re-fetch messages (image renders inline)

Non-Functional Requirements

NFR Target Notes
Cold start (dev) < 5 s npm run dev
First token latency < 1.5 s p95 Network-bound on provider
Local-only operation required No telemetry, no required cloud backend
Persistence survive process restart All state in ~/.jarela/*.sqlite
API key handling never leave the host Stored in DB or env, not synced
Same-origin enforcement required CSRF / DNS-rebinding guard in lib/auth/access.ts
Secrets at rest required for sensitive namespaces AES-GCM envelope, master key in OS keychain or .secret-key fallback
Outbound proxy support required on corporate networks env vars or in-app proxy_config row, applied via undici setGlobalDispatcher (ADR-0009); env wins over DB
Stream resilience reattach within seconds of network change EventSource auto-reconnect + 4000-event replay buffer in lib/agents/run-registry.ts (ADR-0008)
CI on every push required .github/workflows/ci.yml: lint + tsc + build + live integration suite

Release documentation boundary

The manually dispatched .github/workflows/release-docs.yml runs a pinned Copilot CLI against a previous stable tag and exact main SHA. It uses a Copilot-enabled secret in a read-permission generation job, then passes a validated documentation-only patch to a separate PR-writing job. A maintainer, not the model, reviews and merges the PR. Accepted decisions live in docs/releases/<version>.json.

scripts/release-docs.mjs provides deterministic record/scope checks for the optional documentation workflow and local review. Ordinary PR CI, nightly preparation/resume/tagging, and app-tag publishing do not require documentation reviews or Copilot credentials. Source changes invalidate optional reviews; documentation and narrowly allowed generated release metadata/version-only bumps may follow them. Missing or stale reviews never block publication. Neither CI nor the nightly scheduler starts paid AI sessions. This optional authoring service is outside the Jarela process and stores no app state. See ADR-0106.

External Dependencies

Dependency Purpose Failure mode
Anthropic / OpenAI / Google / Cohere LLM inference Surface provider error to UI; allow model switch
Google GenAI (Gemini) — STT + TTS endpoints Push-to-talk voice input + generate_voice tool (ADR-0017) Voice surface degrades to text-only; chat continues
MCP servers External tools Tool call returns error; agent can recover or skip
External provider/tool .cjs files (~/.jarela/{providers,tools}/) User-authored extensions, hot-loaded Validation errors surfaced in GET /api/v1/extensions and the Extensions tab; loader skips invalid files (ADR-0013)
User shell rc (~/.zshrc/~/.bashrc) on macOS/Linux, User-scope env on Windows Source for credential env vars (ADR-0016); probed at boot + on demand Probe failure surfaces a warning, app falls back to whatever is already in process.env; tools surface "not configured" via the existing env-then-DB resolver
GitHub API (api.github.com) Native github_* tools — issues, PRs, repos (ADR-0015); Copilot OAuth for the model provider Tool call returns the API error; agent can recover or skip
SQLite (local) Persistence Fatal — startup fails fast with clear error
Copilot CLI / GitHub Copilot (optional documentation infrastructure only) On-demand documentation proposals with a 50-credit soft session limit Explicit documentation workflow failure; releases remain independent; manual documentation PRs remain available

Decisions

See docs/adr/. Significant choices on persistence, agent runtime, and provider strategy will be recorded as ADRs.