OAKEN CORE
Alpha

Events, jobs and the scheduler

An in-memory event bus says what happened. A durable job queue does the work and survives a crash. A scheduler puts recurring work on that queue.

1

The pieces

Piece Lives in Survives a restart? Job
Event bus memory no tell listeners that something happened
Event log events.jsonl yes keep what the bus said
Job queues core.db yes run work at least once, with retries
Scheduler core.db yes enqueue recurring work on time, once
2

Queues

A queue is a named lane of jobs. default always exists and cannot be removed. Intake formatting runs there. store and embed are hidden stages core runs for you (the ingestor and the embedder do their work). You make more in config.toml as [queues.<name>], each the stage of one analyzer kind. Names match [a-z][a-z0-9_-]{0,40}. Each queue has a worker count (default one; default gets two). See repo:src/oaken_core/queues.py.

A job never picks its queue. It goes where its subject's queue label says, or to default. A label change does not move jobs already queued: use requeue with a target queue.

Pause. oaken queue pause NAME stops a queue handing out jobs. Running jobs finish. The switch is a row in queue_state, so it survives a restart. oaken queue resume NAME undoes it. Pausing is how you free a GPU or stop a stage while you fix a plugin. Each change is published as a QueueStateChanged event.

3

The job lifecycle

A job is a row in core.db. A worker claims it: one short transaction marks the best runnable job running and gives the worker a lease. A worker that dies stops renewing its lease. When the lease expires, the job is queued again.

sequenceDiagram
    participant P as Producer
    participant Q as Queue (core.db)
    participant W as Worker
    participant H as Handler
    P->>Q: enqueue(kind, payload, dedupe_key)
    Q-->>P: job id (or the existing one)
    W->>Q: claim(queue, kinds, limits)
    Note over Q: reclaim expired leases, skip paused queue,<br/>skip kinds at their limit, pick highest priority,<br/>set lease, attempts + 1
    Q-->>W: job
    W->>H: handler(payload, job)
    loop while it runs
        W->>Q: heartbeat (extends the lease)
    end
    alt success
        H-->>W: returns
        W->>Q: complete, status succeeded
    else error and attempts left
        H-->>W: raises
        W->>Q: fail, queued again after backoff
    else permanent error or out of attempts
        W->>Q: fail, status failed (last_error kept)
    end

States are queued, running, succeeded, failed and cancelled. Every change is published as a JobStateChanged event.

4

Numbers

All from [jobs] in config.toml unless noted.

Setting Default Meaning
lease_seconds 60 How long a worker holds a job before it must heartbeat
max_attempts 3 Tries before a job is failed
backoff_base_seconds 5 Delay after the first failure
backoff_max_seconds 900 Cap on the delay
backoff_jitter 0.2 Fraction taken off the delay, at random
handler_timeout_seconds 600 A handler that runs longer fails. 0 turns it off
max_result_bytes 65536 Cap on a stored result
requeue_cooldown_seconds 60 Per job, between requeues
kind_limits none Concurrency cap per job kind

Priority is a number, higher first: user 100, ingest 50 (the default), backfill 10. Within a priority, the job that became runnable first goes first.

5

Guarantees and rules

6

Events

The bus carries typed, frozen events: job state changes, queue pause changes, plugin health, events a plugin defines itself, and two private kinds (log lines and the trace). Two kinds of listener:

The API keeps the last [api] event_ring_size events (1000) for a client that reconnects.

7

The event log

The bus forgets on restart. The event log is one more handler that appends every event to events.jsonl in the data directory ([log] events_file; empty turns it off). A line is {"ts", "kind", "data"}. Data holds ids, states and numbers, never private text. The file is owner-only and rotates by size ([log] file_max_bytes, file_backups). oaken events tail reads the newest lines back. See repo:src/oaken_core/eventlog.py.

Log lines have their own home: the log_lines table, pruned after [log] keep_days (7), or the [log] file if you set one.

8

The trace

The trace records what a model or agent is told, and what it returns, in full text. It is for a person tuning triage. Because it holds private text it is off by default, goes to its own owner-only file (trace.jsonl, rotated like the event log), is never offered over MCP, and is read only through the authenticated API (/v1/trace/stream). Each row carries the plugin and job it ran under, so you can follow one job. See repo:src/oaken_core/trace.py.

9

Results, parents and actors

A job can store a result, checked against a schema for its kind before it is saved. It can name a parent job. It records its actor (user, agent or system). External agents can lease jobs too: see the swarm.

10

The scheduler

A schedule is a fixed interval or a five-field cron expression. It stores its next_run in core.db. A tick enqueues every due schedule and moves next_run forward in one transaction, so a restart neither repeats a run nor skips one. The job also carries a dedupe key naming the slot, as a second guard. Each schedule names a queue, a kind, a payload and a priority (default 50).

The cron parser has no dependency. It covers *, numbers, ranges, lists, steps and three-letter month and weekday names (weekday 0 and 7 are Sunday). If both day-of-month and day-of-week are restricted, either may match, as in classic cron. There is no @daily and no seconds field. See repo:src/oaken_core/cron.py and repo:src/oaken_core/scheduler.py.

11

Testing

Everything above runs against a fake clock with no sleeping: advance the clock, call drain() on the runner or tick() on the scheduler, and look at the database.