Skip to content

Architecture

lacause edited this page Mar 29, 2026 · 3 revisions

Architecture

How OCC works under the hood.

System Overview

┌──────────────────┐         ┌────────────────────┐
│   Claude Code    │──MCP──▶│    MCP Server       │
│   Claude Desktop │  stdio  │    (index.ts)       │
└──────────────────┘         │  25 tools exposed   │
                             └────────┬───────────┘
                                      │
┌──────────────────┐         ┌────────▼───────────┐
│   curl / Browser │──HTTP──▶│    REST Server      │
│   Any client     │  :4242  │    (rest.ts)        │
└──────────────────┘         │  30+ endpoints      │
                             │  SSE streaming      │
                             └────────┬───────────┘
                                      │
                    ┌─────────────────┼─────────────────┐
                    ▼                 ▼                  ▼
           ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
           │    Loader     │  │   Executor   │  │  Scheduler   │
           │  (loader.ts)  │  │(executor.ts) │  │(scheduler.ts)│
           │              │  │              │  │              │
           │ YAML parsing  │  │ Chain runner  │  │ Cron jobs    │
           │ Zod validation│  │ Process mgmt  │  │ Auto-execute │
           │ Dep. graph    │  │ Caching       │  │              │
           └──────┬───────┘  └──────┬───────┘  └──────────────┘
                  │                 │
                  ▼                 ▼
           ┌──────────┐     ┌──────────────┐
           │ chains/   │     │  Claude CLI   │
           │  (YAML)   │     │  (subprocess) │
           └──────────┘     └──────────────┘

Module Breakdown

loader.ts — Chain Loading & Validation

  1. YAML parsingjs-yaml converts YAML to JavaScript objects
  2. Zod validation — Every field validated against strict schema
  3. Dependency graph — Topological sort (Kahn's algorithm) produces execution waves
Chain YAML → js-yaml → Zod safeParse → buildDependencyGraph → waves[]

Waves are groups of steps with no dependencies between them:

Wave 1: [step_a, step_b, step_c]     ← parallel
Wave 2: [step_d]                       ← depends on a,b
Wave 3: [step_e, step_f]              ← depends on d

executor.ts — Chain Execution Engine

The core execution loop:

for each wave in dependency_graph:
    await Promise.all(
        wave.map(step => executeStep(step))
    )

Step execution flow:

1. Check condition → skip if false
2. Check cache → return cached if valid
3. Run pre-tools → inject variables
4. Resolve {variables} in prompt
5. Spawn Claude CLI process
6. Stream stdout → SSE events
7. Validate output (guardrails)
8. Store result in variables map
9. Save to cache if enabled
10. Persist execution state

Process management:

  • Each step spawns claude --print --output-format stream-json
  • Timeout: configurable per-step, default 5 minutes
  • Escalation: SIGTERM → wait 3s → SIGKILL
  • Heartbeat: every 30s, checks if process is still alive
  • Cancellation: kills all active processes for an execution

Concurrency control:

  • Max 5 concurrent executions (configurable)
  • Returns HTTP 429 when exceeded
  • Each execution tracks its active child processes

rest.ts — HTTP Server

Express server with:

  • JSON + YAML body parsing (2MB limit)
  • CORS enabled (all origins)
  • SSE streaming with 30s heartbeat
  • Static file serving for optional frontend

scheduler.ts — Cron Scheduling

  • Uses node-cron for cron expression parsing
  • Schedules persisted to JSON file
  • Loads on startup, restores active schedules
  • Toggle on/off without deleting

pipeline-executor.ts — Pipeline Orchestration

Similar to chain executor but at the chain level:

  1. Load all referenced chains
  2. Build dependency graph between chains
  3. Execute chains in waves
  4. Pass outputs between chains via variable mapping

Data Flow

Variable Resolution

Variables flow through the system:

Chain Input: { topic: "AI" }
    ↓
Variables Map: { "input.topic": "AI", "topic": "AI" }
    ↓
Step 1 runs → output = "Research findings..."
    ↓
Variables Map: { "input.topic": "AI", "research": "Research findings..." }
    ↓
Step 2 prompt: "Summarize: {research}" → "Summarize: Research findings..."

Persistence

Executions → executions.json (last 200, purged after 7 days)
Schedules  → schedules.json
Cache      → cache/{chain_name}/{hash}.json (TTL-based)
Pipelines  → pipeline-executions.json (last 50)

All persistence is file-based (no database required). Files are written asynchronously (non-blocking).

Process Model

OCC Server (Node.js)
├── Express HTTP server (single thread)
├── MCP stdio transport (single connection)
├── Scheduler (node-cron timers)
└── Active executions
    ├── Execution A
    │   ├── Step 1 → claude process (PID 1234)
    │   └── Step 2 → claude process (PID 1235)
    └── Execution B
        └── Step 1 → claude process (PID 1236)

Each step is a separate claude CLI process. Node.js manages process lifecycle, collects output, and streams events.

Error Recovery

Scenario Recovery
Step timeout SIGTERM → 3s → SIGKILL, step marked error
Process crash Heartbeat detects within 30s, step marked error
Server restart Orphaned "running" executions marked error on load
Invalid YAML Rejected at load time with Zod error details
Claude CLI missing Detected at startup, clear error message
Execution cancelled All child processes killed, pending steps skipped
Cache corruption Silent fallback to re-execution

See Also

Clone this wiki locally