Matrix logo

Data Model

Package chronos/pkg/types defines the wire contracts and chronos/internal/store implements the Postgres persistence. The alarms table IS the durable timer — state lives in the D...

Package chronos/pkg/types defines the wire contracts and chronos/internal/store implements the Postgres persistence. The alarms table IS the durable timer — state lives in the DB, never in memory, so a restart never loses a scheduled wake (invariant i1).

Source files: pkg/types/types.go, internal/store/alarms.go, migrations/001_init.sql.


The alarms table

One row per scheduled wake. The row itself is the timer — the dispatch worker polls next_fire_at, not an in-memory heap.

CREATE TABLE alarms (
    id               UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    owner_did        TEXT NOT NULL,
    user_id          TEXT NOT NULL,
    label            TEXT NOT NULL DEFAULT '',
    kind             TEXT NOT NULL CHECK (kind IN ('once', 'cron')),
    fire_at          TIMESTAMPTZ,
    cron_expr        TEXT NOT NULL DEFAULT '',
    timezone         TEXT NOT NULL DEFAULT 'UTC',
    next_fire_at     TIMESTAMPTZ NOT NULL,
    conversation_id  TEXT NOT NULL DEFAULT '',
    wake_message     TEXT NOT NULL DEFAULT '',
    payload          JSONB NOT NULL DEFAULT '{}'::jsonb,
    status           TEXT NOT NULL DEFAULT 'active'
                     CHECK (status IN ('active', 'fired', 'cancelled', 'failed')),
    idempotency_key  TEXT NOT NULL DEFAULT '',
    max_failures     INT  NOT NULL DEFAULT 5,
    failure_count    INT  NOT NULL DEFAULT 0,
    last_error       TEXT NOT NULL DEFAULT '',
    claimed_at       TIMESTAMPTZ,
    created_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
    last_fired_at    TIMESTAMPTZ
);

Indexes

IndexDefinitionPurpose
alarms_due_idxnext_fire_at WHERE status = 'active'The dispatch worker's hot claim path
alarms_owner_idxowner_did, created_at DESCOwner-scoped listing
alarms_idempotency_idxowner_did, idempotency_key WHERE idempotency_key <> ''Per-owner dedup — re-posting the same key is a no-op

Column reference

ColumnTypeMeaning
idUUIDPrimary key, auto-generated
owner_didTEXTFull agent DID: did:matrix:<user_id>:<keyfp>
user_idTEXTSupabase user UUID extracted from the DID label — the router wake target
labelTEXTShort human label, e.g. "daily portfolio summary"
kindTEXTonce or cron
fire_atTIMESTAMPTZFor once: the absolute moment to fire (computed from delay or explicit time at post)
cron_exprTEXTFor cron: standard 5-field expression, @descriptor, or @every Nm
timezoneTEXTIANA timezone for cron evaluation, default "UTC"
next_fire_atTIMESTAMPTZThe next scheduled fire; the dispatch worker's claim key
conversation_idTEXTConversation to resume into; empty → agent opens a fresh conversation on wake
wake_messageTEXTAgent-authored resume text delivered as the chat turn
payloadJSONBOpaque state the agent stashed (task state, ids, cursors) — echoed back verbatim on wake
statusTEXTLifecycle: active, fired, cancelled, or failed
idempotency_keyTEXTOptional per-owner dedup key; re-posting the same key returns the existing alarm
max_failuresINTWake-delivery retry ceiling before status=failed (default from config)
failure_countINTRunning count of failed wake deliveries
last_errorTEXTLast wake-delivery error text (honest failure surfacing)
claimed_atTIMESTAMPTZDispatch lease timestamp; NULL = unclaimed
created_atTIMESTAMPTZRow creation time
updated_atTIMESTAMPTZLast mutation time
last_fired_atTIMESTAMPTZLast successful fire (observability / time-tracking)

Go types

Alarm

Alarm is the internal representation, mapping 1:1 onto a row:

type Alarm struct {
    ID             string
    OwnerDID       string
    UserID         string
    Label          string
    Kind           string         // "once" | "cron"
    FireAt         *time.Time
    CronExpr       string
    Timezone       string
    NextFireAt     time.Time
    ConversationID string
    WakeMessage    string
    Payload        json.RawMessage
    Status         string
    IdempotencyKey string
    MaxFailures    int
    FailureCount   int
    LastError      string
    ClaimedAt      *time.Time
    CreatedAt      time.Time
    UpdatedAt      time.Time
    LastFiredAt    *time.Time
}

View

View is the JSON projection returned to the agent. High-entropy fields (payload, wake_message, ids) are passed through verbatim (invariant i4):

type View struct {
    ID             string          `json:"id"`
    Label          string          `json:"label"`
    Kind           string          `json:"kind"`
    CronExpr       string          `json:"cron_expr,omitempty"`
    Timezone       string          `json:"timezone,omitempty"`
    NextFireAt     *time.Time      `json:"next_fire_at,omitempty"`
    ConversationID string          `json:"conversation_id,omitempty"`
    WakeMessage    string          `json:"wake_message"`
    Payload        json.RawMessage `json:"payload,omitempty"`
    Status         string          `json:"status"`
    IdempotencyKey string          `json:"idempotency_key,omitempty"`
    MaxFailures    int             `json:"max_failures"`
    FailureCount   int             `json:"failure_count"`
    LastError      string          `json:"last_error,omitempty"`
    CreatedAt      time.Time       `json:"created_at"`
    LastFiredAt    *time.Time      `json:"last_fired_at,omitempty"`
}

ViewOf(a Alarm) projects an Alarm onto its wire shape. NextFireAt is only included when status == "active".


Lifecycle statuses

                  ┌─────────┐
                  │  active  │
                  └────┬─────┘
         ┌─────────────┼─────────────┐
         ▼             ▼             ▼
    ┌────────┐   ┌──────────┐   ┌────────┐
    │ fired  │   │ cancelled│   │ failed │
    └────────┘   └──────────┘   └────────┘
    (once only)  (explicit)    (once only,
                                retries
                                exhausted)
StatusMeaningTransition
activeScheduled and waiting to fireInitial state on create
firedOnce alarm that has fired successfullyRetained for audit, not deleted
cancelledExplicitly cancelled by the ownerVia DELETE /v1/alarms/{id}
failedOnce alarm whose wake delivery exhausted max_failuresTerminal; never retried

Cron alarms stay active indefinitely — they never self-delete. Each fire advances next_fire_at and the alarm remains active. Cancellation is explicit.


Idempotency

When idempotency_key is non-empty, CreateAlarm uses a partial unique index:

CREATE UNIQUE INDEX alarms_idempotency_idx
    ON alarms (owner_did, idempotency_key)
    WHERE idempotency_key <> '';

The insert uses ON CONFLICT (owner_did, idempotency_key) WHERE idempotency_key <> '' DO NOTHING. If the row already exists, the existing alarm is returned with deduped=true. No duplicate is created.


Store operations

All alarm mutations go through store.Store methods:

MethodSQLNotes
CreateAlarmINSERT … ON CONFLICT DO NOTHING RETURNINGIdempotent; returns (Alarm, deduped, error)
ListAlarmsSELECT … WHERE owner_did ORDER BY created_at DESC LIMITOwner-scoped, capped at 500
GetAlarmSELECT … WHERE id AND owner_didReturns ErrNotFound for unknown/unowned
CancelAlarmUPDATE status='cancelled' WHERE id AND owner_did AND status='active'Idempotent; already-terminal alarms return success
ClaimDueUPDATE … WHERE id IN (SELECT … FOR UPDATE SKIP LOCKED)Atomic lease; HA-safe
MarkFiredUPDATE status='fired', last_fired_at=now()Once alarms only
RescheduleUPDATE next_fire_at, last_fired_at=now()Cron alarms only
RecordRetryUPDATE failure_count++, next_fire_at=retry_timeBounded backoff
MarkFailedUPDATE status='failed', last_errorOnce alarms, retries exhausted
RescheduleAfterFailureUPDATE next_fire_at, last_errorCron skip-and-advance