agentStateLoop defines a three-state machine that governs how tunnelTime agents manage their polling frequency, responsiveness, and resource usage. the goal: concierge-level service when the operator is engaged, low-cost idle when they are not, and instant wake-up when they return.
purpose: low-cost background presence. agent is alive but not actively engaged.
| poll interval | 30 minutes (email) + localMesh nudge listener |
| email query | is:unread from:c.t.gaughan@gmail.com (broad, callsign-filtered) |
| mesh check | every poll cycle + instant on nudge |
| output | silent unless message found |
| cost | minimal -- 2 API calls per 30 min |
| SLA | response within 30 min worst case, instant on nudge |
purpose: operator signal detected. agent is warming up for engagement. rapid polling to catch follow-up messages.
| poll interval | 60 seconds |
| timeout | 5 minutes with no operator response = back to IDLE |
| entry action | send ack email: "goulard3120 standing by. what do you need?" |
| exit action (timeout) | send "going quiet" email, transition to IDLE |
| SLA | response within 60 seconds |
purpose: operator is engaged in multi-turn conversation. full context, rapid response, concierge mode.
| poll interval | 60 seconds |
| timeout | 10 minutes with no operator message = transition to IDLE |
| entry action | none (seamless from PREP) |
| exit action | send "going quiet -- ping me when you need me" email, transition to IDLE |
| SLA | response within 60 seconds |
| context | full session context maintained. multi-turn conversation. |
+--------+ +--------+
| IDLE |---[inbound message/nudge]--->| PREP |
| 30min | | 60s |
+--------+ +--------+
^ |
| |
+---[5min timeout, no response]---------+
| |
| [operator replies]
| |
| v
| +--------+
+---[10min silence]----------------|ACTIVE |
| 60s |
+--------+
localMesh already supports nudge -- when a message is sent to an agent, the mesh sends a notification. the missing piece is agent-side handling.
agents need a nudge listener that runs alongside the cron loop. when a nudge arrives:
| method | mechanism | latency |
|---|---|---|
| localMesh webhook | mesh POSTs to agent endpoint on nudge | <1s |
| file watch | mesh writes nudge file, agent watches with inotify/polling | 1-5s |
| named pipe | mesh writes to pipe, agent blocks on read | <1s |
recommendation: for Claude Code agents (which run in cron loops, not as persistent daemons), the pragmatic approach is: on each poll cycle, check localMesh mailbox. if a nudge was received since last poll, the NEXT cron fire (which is at most 60s in PREP/ACTIVE, 30min in IDLE) picks it up. for true instant wake, we need the cron interval itself to change -- which requires CronDelete + CronCreate with the new interval.
# when entering PREP from IDLE: CronDelete(idle_job_id) CronCreate(cron="*/1 * * * *", prompt=POLL_PROMPT) # 60s # when returning to IDLE from PREP/ACTIVE: CronDelete(active_job_id) CronCreate(cron="*/30 * * * *", prompt=POLL_PROMPT) # 30min
agents MUST NOT halt on credential failure. instead:
| state | email SLA | mesh SLA | cost/hr |
|---|---|---|---|
| IDLE | <30 min | <30 min (next poll) | ~0 (2 API calls/30min) |
| PREP | <60 sec | <60 sec | low (2 API calls/min) |
| ACTIVE | <60 sec | <60 sec | low (2 API calls/min) |
| method | how | best for |
|---|---|---|
| send to artificer3120@gmail.com with @{handle}3120 in subject | async tasks, instructions, reviews | |
| localMesh | POST to http://127.0.0.1:8801/message (from any agent or script) | inter-agent coordination, nudges |
| direct CLI | type in the agent's terminal on Rocky | interactive work, debugging |
agentStateLoop v1.0 // goulard3120 // QB pending // 2026-05-07