// agentStateLoop

agent state machine specification // v1.0 // goulard3120 // 2026-05-07

1. overview

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.

2. states

IDLE

purpose: low-cost background presence. agent is alive but not actively engaged.

poll interval30 minutes (email) + localMesh nudge listener
email queryis:unread from:c.t.gaughan@gmail.com (broad, callsign-filtered)
mesh checkevery poll cycle + instant on nudge
outputsilent unless message found
costminimal -- 2 API calls per 30 min
SLAresponse within 30 min worst case, instant on nudge
PREP

purpose: operator signal detected. agent is warming up for engagement. rapid polling to catch follow-up messages.

poll interval60 seconds
timeout5 minutes with no operator response = back to IDLE
entry actionsend ack email: "goulard3120 standing by. what do you need?"
exit action (timeout)send "going quiet" email, transition to IDLE
SLAresponse within 60 seconds
ACTIVE

purpose: operator is engaged in multi-turn conversation. full context, rapid response, concierge mode.

poll interval60 seconds
timeout10 minutes with no operator message = transition to IDLE
entry actionnone (seamless from PREP)
exit actionsend "going quiet -- ping me when you need me" email, transition to IDLE
SLAresponse within 60 seconds
contextfull session context maintained. multi-turn conversation.

3. transitions

  +--------+                              +--------+
  |  IDLE  |---[inbound message/nudge]--->|  PREP  |
  | 30min  |                              |  60s   |
  +--------+                              +--------+
      ^                                       |
      |                                       |
      +---[5min timeout, no response]---------+
      |                                       |
      |                                  [operator replies]
      |                                       |
      |                                       v
      |                                  +--------+
      +---[10min silence]----------------|ACTIVE  |
                                         |  60s   |
                                         +--------+
IDLE -> PREP
trigger: inbound email with agent callsign, OR localMesh nudge, OR localMesh message
action: cancel current sleep timer, immediately poll, send ack, switch to 60s loop
PREP -> ACTIVE
trigger: operator sends a second message (confirms engagement)
action: maintain 60s loop, enter full-context mode
PREP -> IDLE
trigger: 5 minutes with no operator message
action: send "going quiet" email, switch to 30min loop
ACTIVE -> IDLE
trigger: 10 minutes with no operator message
action: send "going quiet" email, switch to 30min loop

4. nudge mechanism

localMesh already supports nudge -- when a message is sent to an agent, the mesh sends a notification. the missing piece is agent-side handling.

implementation

agents need a nudge listener that runs alongside the cron loop. when a nudge arrives:

  1. if in IDLE: cancel remaining sleep, immediately poll, transition to PREP
  2. if in PREP or ACTIVE: no action needed (already polling at 60s)

nudge delivery options

methodmechanismlatency
localMesh webhookmesh POSTs to agent endpoint on nudge<1s
file watchmesh writes nudge file, agent watches with inotify/polling1-5s
named pipemesh 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.

cron interval switching (the key innovation)

# 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

5. credential resilience

agents MUST NOT halt on credential failure. instead:

  1. attempt API call
  2. if 401/403: message keymaster via localMesh with the failing credential name
  3. continue polling (skip email processing this cycle, but dont die)
  4. on next cycle, retry. if keymaster has rotated the credential, it works. if not, skip again.
  5. after 3 consecutive failures: send a "credential blocker" email to operator via a DIFFERENT channel (e.g. if Gmail fails, use localMesh to alert another agent who can email)

6. SLA summary

stateemail SLAmesh SLAcost/hr
IDLE<30 min<30 min (next poll)~0 (2 API calls/30min)
PREP<60 sec<60 seclow (2 API calls/min)
ACTIVE<60 sec<60 seclow (2 API calls/min)

7. operator contact methods

methodhowbest for
emailsend to artificer3120@gmail.com with @{handle}3120 in subjectasync tasks, instructions, reviews
localMeshPOST to http://127.0.0.1:8801/message (from any agent or script)inter-agent coordination, nudges
direct CLItype in the agent's terminal on Rockyinteractive work, debugging

8. implementation plan

phase 1: state tracking (this session)

phase 2: cron switching

phase 3: credential resilience

phase 4: nudge integration

agentStateLoop v1.0 // goulard3120 // QB pending // 2026-05-07