Autonomous Agent Loop

Loop ที่รันอัตโนมัติ 24/7 — ดึงงานจากคิว, spawn worker, เช็คสถานะ, retry เมื่อ fail, dead-letter เมื่อเกิน max retry

Agent Loop อยู่ใน src/services/autonomous/agentLoop.ts — เป็น consumer ตัวเดียวที่อ่าน task queue และบริหาร lifecycle ของ worker process ทั้งหมด

เทคนิคและหลักการ

ทำไมต้องเป็น Loop แทนที่จะเป็น Message Queue?

ระบบ message queue แบบ RabbitMQ/Kafka มี overhead ในการ setup และ maintenance Agent Loop ใช้ไฟล์ JSON เป็น persistent queue — ทำงานได้โดยไม่ต้องมี external dependency:

  • Zero external deps — ใช้ fs.watch ตรวจจับการเปลี่ยนแปลงของไฟล์ queue
  • File-based atomicity — อ่าน/เขียนไฟล์เดียวภายใต้ lock ทำให้หลาย process แข่งกัน consume ได้
  • Self-debouncingourWriteInProgress flag ป้องกัน self-trigger เมื่อเราเป็นคนเขียนเอง
  • Cross-platform — ไฟล์ JSON ทำงานได้ทุก OS ไม่ต้องติดตั้ง message broker

Lease-Based Concurrency — ป้องกัน Task Duplicate

ปัญหาคลาสสิกของ distributed workers: มี 2 worker เห็น task เดียวกันและทำงานซ้ำ Lease แก้ปัญหานี้โดยไม่ต้องใช้ distributed lock:

Main Loop                    Queue File (.json)              Worker Process
    │                              │                              │
    │ getNextTask()                │                              │
    │─────────────────────────────►│                              │
    │◄─── task {id, status:pending}│                              │
    │                              │                              │
    │ leaseTask(id, agentId)      │                              │
    │─────────────────────────────►│                              │
    │  (atomic: check no lease    │                              │
    │   → write leaseOwner +      │                              │
    │   leaseExpiresAt)           │                              │
    │◄─────── true (leased) ──────│                              │
    │                              │                              │
    │ spawnWorker(prompt)         │                              │
    │─────────────────────────────────────────────────────────────►│
    │◄──── WorkerSession {id, pid} │                              │
    │                              │                              │
    │          ═══ LOOP ═══        │                              │
    │   while running:             │                              │
    │     checkWorker(sessionId)   │                              │
    │     ────────────────────────────────────────────────────────►│
    │     ◄─── "running" / "completed" / "failed"                 │
    │                              │                              │
    │                              │  (worker ทำงาน autonomously) │
    │                              │                              │
    │   [completed] → releaseLease │                              │
    │              → markCompleted │                              │
    │                              │                              │
    │   [failed] → markFailed      │                              │
    │           → retryTask()      │                              │
    │           → stopWorker()     │                              │
    │                              │                              │
    │   [timeout 30m] → stopWorker │                              │
    │                → releaseLease│                              │
    │                → retryTask() │                              │
  • Lease owner — ระบุว่า worker ไหนกำลังทำ task นี้
  • Lease expiry — ถ้า worker crash โดยไม่ release, lease จะหมดอายุเอง → worker อื่นรับงานต่อได้
  • Startup recovery — ตอน startLoop จะรอ 2 วินาที (ให้ process เก่าตายแน่ๆ) แล้วเรียก expireLeases() ล้าง stale lease ทั้งหมด

Retry with Exponential Backoff

Task ที่ fail ไม่ได้แปลว่าต้อง dead-letter ทันที:

Retry attempt     Backoff delay         Cumulative wait
─────────────     ─────────────         ──────────────
      1           base × 2¹ = 30s              30s
      2           base × 2² = 60s              90s
      3           base × 2³ = 120s            210s
      4           base × 2⁴ = 240s            450s
      5 (max)      base × 2⁵ = 480s            930s (~15 min)

After max retries → dead_letter queue
Dead-letter preserves: title, description, lastError, errorLog, retryCount
  • Exponential backoff — base = 15s, factor = 2 — ลดการถล่ม queue เมื่อ task fail ซ้ำๆ
  • Max retries — default 5 ครั้งต่อ task
  • Retry intervalretryAfter timestamp ป้องกันไม่ให้ retry ก่อนเวลาที่กำหนด
  • Dead-letter queue — task ที่ใช้ retry หมดจะถูกย้ายไปสถานะ dead_letter พร้อม deadLetterReason และ errorLog — ไม่หายไปไหน ตรวจสอบย้อนหลังได้

Worker Lifecycle + Concurrent Cap

Main loop มีกลไกจำกัดจำนวน worker พร้อมกัน:

  • MAX_CONCURRENT_WORKERS = 3 — ป้องกันไม่ให้ spawn worker มากเกินไปจนเครื่องพัง
  • Loop poll interval — ถ้า worker เต็ม → sleep LOOP_SLEEP_MS (5s) แล้วตรวจใหม่
  • Worker timeout = 30 นาที — task ที่รันนานเกินจะถูก kill เพื่อคืน resource
  • Worker poll = 10s — เช็คสถานะ worker ทุก 10 วินาทีผ่าน supervisor IPC

Supervisor Integration — Process Health

Worker ไม่ได้ถูกรันโดยตรง แต่ spawn ผ่านSupervisor process (child_process):

Agent Loop                    Supervisor                   Worker
    │                              │                         │
    │ sendRequest({type:'spawn'})  │                         │
    │─────────────────────────────►│                         │
    │                              │ spawn child_process     │
    │                              │────────────────────────►│
    │◄─── {ok:true, sessionId, pid}│                         │
    │                              │                         │
    │ sendRequest({type:'attach'}) │                         │
    │─────────────────────────────►│                         │
    │                              │ check process status    │
    │◄─── {status, isRunning}      │                         │
    │                              │                         │
    │ sendRequest({type:'stop'})   │                         │
    │─────────────────────────────►│                         │
    │                              │ kill + cleanup          │
    │                              │────────────────────────►│
  • Crash isolation — worker crash ไม่ทำให้ loop crash — supervisor แยก process คนละตัว
  • Output capture — supervisor เก็บ stdout/stderr ของ worker ไว้ใน ~/.claude/daemon/jobs/{sessionId}/output.log
  • Health monitoring — loop เช็คผ่าน supervisor แทนที่จะ monitor PID โดยตรง (PID reuse problem)

Integration Points — Peer + Cron + Watch

Agent Loop ไม่ได้อยู่เดี่ยวๆ — มัน integrate กับระบบอื่นของ Clew:

  • Peer todo listener — ฟัง /peer-todo HTTP endpoint → รับ task จาก remote peer → add เข้า queue
  • Cron schedulerdaemonCronScheduler fire task ตาม schedule → add เข้า queue
  • File watcherfs.watch บน queue file → ตรวจจับ task ใหม่จาก process อื่น (เช่นจาก CLI /task add)

architecture แบบนี้ทำให้ task เข้า queue ได้จากหลายช่องทาง — CLI, remote peer, cron — แต่มี consumer เดียว (loop) ที่คุมการ execute

Task Log Persistence

ทุก task มี log ของตัวเอง:

  • Per-task log~/.claude/daemon/logs/{taskId}.log — 500 บรรทัดสุดท้ายของ worker output
  • Error extraction — ตอน task fail, loop จะ extract non-noise lines (20 บรรทัดสุดท้าย) เก็บเป็น errorLog[] ใน queue entry
  • Worker exit code — บันทึกว่า worker จบด้วย exit code 0 (success) หรือ 1 (failure)

Crash Recovery Flow

ถ้า loop process ตาย (kill -9, power loss, OOM):

Previous Run Crash
      │
      ▼
startLoop() called
      │
      ├── loadQueue() — อ่านไฟล์ queue จาก disk
      │
      ├── sleep(2000ms) — รอให้แน่ใจว่า process เก่าตายแล้ว
      │
      ├── expireLeases() — ล้าง lease ทั้งหมดที่หมดอายุ
      │   (task ที่ถูก lease โดย processID เก่าจะถูก reset → pending)
      │
      ├── start heartbeat (ทุก 60s)
      │
      ├── start cron scheduler
      │
      ├── start peer sharing
      │
      ├── start file watcher
      │
      └── MAIN LOOP ──► getNextTask() → processTask() → loop

Task Lifecycle (State Machine)

                 ┌──────────┐
                 │ pending   │◄──────────────────────────────┐
                 └────┬─────┘                               │
                      │ leaseTask()                          │
                      ▼                                      │
                 ┌──────────┐                               │
                 │in_progress│                               │
                 └────┬─────┘                               │
          ┌───────────┼───────────┐                         │
          ▼           ▼           ▼                         │
    ┌──────────┐ ┌──────────┐ ┌──────────┐                 │
    │completed │ │ failed   │ │cancelled │                 │
    └──────────┘ └────┬─────┘ └──────────┘                 │
                      │ retryTask()                         │
                      ├── retryCount < max → backoff → ────┘
                      │
                      └── retryCount ≥ max
                              │
                              ▼
                         ┌──────────────┐
                         │ dead_letter   │
                         │ (preserved    │
                         │  for review)  │
                         └──────────────┘

ไฟล์ที่เกี่ยวข้อง

ไฟล์หน้าที่
src/services/autonomous/agentLoop.tsMain loop — start, stop, processTask, worker lifecycle
src/services/autonomous/taskQueue.tsQueue CRUD, lease management, retry, dead-letter, file watcher
src/services/autonomous/daemonMode.tsDaemon entry point — calls startLoop/stopLoop
src/Task.tsTask type definitions, state machine, task ID generation
src/tasks/LocalAgentTask/Local worker task — UI + lifecycle
src/tasks/RemoteAgentTask/Remote worker task — UI + lifecycle
src/components/AutonomousExecutionAccordion.tsxUI component สำหรับแสดง task queue ใน REPL