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-debouncing —
ourWriteInProgressflag ป้องกัน 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 interval —
retryAftertimestamp ป้องกันไม่ให้ 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-todoHTTP endpoint → รับ task จาก remote peer → add เข้า queue - Cron scheduler —
daemonCronSchedulerfire task ตาม schedule → add เข้า queue - File watcher —
fs.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.ts | Main loop — start, stop, processTask, worker lifecycle |
src/services/autonomous/taskQueue.ts | Queue CRUD, lease management, retry, dead-letter, file watcher |
src/services/autonomous/daemonMode.ts | Daemon entry point — calls startLoop/stopLoop |
src/Task.ts | Task 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.tsx | UI component สำหรับแสดง task queue ใน REPL |