Matrix logo

Wake Delivery

Wake delivery is how Chronos hands a due alarm to the agent. Chronos never talks to Fly or the daemon directly; it reuses the router's battle-tested EnsureStarted + waitDaemonReady + 6PN reverse-proxy path via one new router surface: POST /internal/wake.

Wake delivery is how Chronos hands a due alarm to the agent. Chronos never talks to Fly or the daemon directly; it reuses the router's battle-tested EnsureStarted + waitDaemonReady + 6PN reverse-proxy path via one new router surface: POST /internal/wake.

Source file: internal/wake/wake.go.


The Waker interface

type Waker interface {
    Wake(ctx context.Context, req Request) error
}

The Waker interface abstracts the delivery mechanism. The concrete implementation is HTTPWaker, which POSTs to the router's internal wake endpoint. The interface returns a non-nil error on any non-2xx response so the dispatch retry ladder can act (honest failure, invariant i6).


Wake request

The Request struct is the body POSTed to the router's /internal/wake endpoint:

type Request struct {
    UserID         string          `json:"user_id"`
    ConversationID string          `json:"conversation_id,omitempty"`
    Message        string          `json:"message"`
    Payload        json.RawMessage `json:"payload,omitempty"`
    AlarmID        string          `json:"alarm_id"`
    Origin         string          `json:"origin"` // always "chronos"
}
FieldPurpose
user_idThe Supabase user UUID extracted from the agent's DID. The router uses this to resolve the user's machine.
conversation_idThe conversation to resume into. Empty means a fresh conversation.
messageThe wake_message from the alarm, delivered verbatim (invariant i4). This is the contextful turn the agent receives.
payloadOpaque JSON state from the alarm, echoed back so the agent can recover context without re-derivation.
alarm_idThe alarm's UUID. Rides along for downstream deduplication (invariant i3) and tagging as a timer wake.
originAlways "chronos". Set automatically by HTTPWaker.Wake.

HTTPWaker

type HTTPWaker struct {
    URL    string
    Token  string
    Client *http.Client
}

The HTTPWaker is constructed with the router's internal wake URL and the shared wake token:

func New(url, token string) *HTTPWaker {
    return &HTTPWaker{
        URL:    url,
        Token:  token,
        Client: &http.Client{Timeout: 60 * time.Second},
    }
}

The default timeout is 60 seconds, which accounts for the router's EnsureStarted call (cold-starting a suspended Fly machine can take 30-45 seconds).


Wake flow

When the dispatch worker fires an alarm, the complete delivery path is:

1. dispatch.Worker.fire()
   Calls waker.Wake() with the alarm's user_id, conversation_id, wake_message, payload

2. HTTPWaker.Wake()
   POSTs to router /internal/wake with Bearer wake-token auth
   Sets origin="chronos" automatically

3. Router receives /internal/wake
   Resolves user_id -> machine
   Calls EnsureStarted (cold-start if suspended)
   Waits for daemon readiness (waitDaemonReady)
   POSTs {message, conversation_id, payload} to daemon /chat over Fly 6PN

4. Daemon /chat
   Injects the wake_message as a new chat turn in the conversation
   The agent sees the message and resumes with full context

Chronos is responsible only for steps 1-2. The router handles all machine lifecycle and network routing.


Error handling

Any transport failure or non-2xx response from the router is returned as an error. The response body is surfaced (truncated to 300 chars) for honest failure recording in the alarm's last_error field.

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    return fmt.Errorf("wake: router returned %d: %s", resp.StatusCode, truncate(respBody, 300))
}

The dispatch worker's retry ladder acts on these errors: retry with backoff (Tier 1), permanently fail for once alarms (Tier 2), or skip-and-advance for cron alarms (Tier 3).


Auth

The wake request uses a shared bearer token (WakeToken in config, matching the router's ROUTER_WAKE_TOKEN). This is transport-level auth only: it proves the request comes from a legitimate Chronos instance. There is no principal-level auth on the wake path because Chronos has already verified the agent's DID at alarm creation time and the user_id is derived from that verified identity.


Key invariant

Chronos never fabricates a successful wake. If the router returns an error, the alarm is retried or marked failed. The last_error field on the alarm records exactly what went wrong for observability.