loop

The brain's entry point. A cognet declares exactly one, and it is the only thing the kernel calls.

loop(async ({ stimuli, signal, stop }) => { /* one tick */ })

type LoopCtx = {
    /** Stimuli committed since the last wake, in seq order. */
    stimuli: readonly AxonStimulusEntry[]
    /** The wake's abort leash — for the cognet's OWN work. */
    signal: AbortSignal
    /** End the wake after this tick completes. The brain stays warm. */
    stop(): void
}

Declaring two loops throws COGNET_LOOP_ALREADY_DECLARED. Declaring none throws COGNET_NO_LOOP.

main.ts is a raw script

// src/main.ts
import { Air } from "@arcforge/air"
import { state, sync } from "./state"

const air = Air({ protocol: "classic" })   // resident for the process

loop(async ({ stop }) => {
    await phase("render", async () => { /* ... */ })
    stop()
})

Not a module with exports. It runs once at load(), with the ambient globals live and the kernel already bound, and its job is to declare loop(). Everything it holds in module or closure scope is the brain's resident memory for the life of the process.

Imports are ordinary. Doing work at the top level is not — the kernel binds at load(), and touching a syscall before that throws COGNET_ACCESSED_BEFORE_LOAD.

Wake, tick, stop

A wake is one invocation of the brain. A tick is one iteration of the loop body inside that wake. The body runs repeatedly until it calls stop() or the wake aborts.

loop(async ({ stop }) => {
    // ... this runs again, and again, until:
    stop()   // end the wake AFTER this tick completes
})

stop() is a declaration that the wake is finished, not an immediate exit — the current tick runs to completion. Afterward the brain stays warm: module scope survives, and the next wake starts with everything still resident.

This is where a cognet expresses judgment about its own completion:

// Code this tick means another tick regardless of what the model said —
// the model must see its own results before the wake can honestly end.
if (done && blocks.length === 0) stop()

That policy belongs to the cognet. The kernel has no opinion about when thinking is finished.

stimuli

Narrowly typed to cognet:stimulus:*text, audio, visual, vector. The cognet's whole input contract, mirroring output() being its whole unmediated write contract. It never has to switch over every entry family that exists, only what it can actually receive.

An empty diff is ordinary, not an edge case. For a continuous cognet it is the steady state. For an invocation cognet the diff may hold more than one stimulus, when several arrived while the previous wake was running.

Only the first tick of a wake sees new arrivals — stimuli is the wake's diff, not a live feed. A cognet that wants history reads kernel.store.session.

signal

loop(async ({ signal, stop }) => {
    for (const item of work) {
        if (signal.aborted) return
        await fold(item)
    }
})

The wake's abort leash, for the cognet's own work only.

Everything the kernel mediates — run(), stream(), transform() — is cancelled by the kernel itself, unconditionally. A wake's cancellation is not something a cognet opts into, and threading a signal by hand meant one missing argument made an operation unkillable. Those verbs take no signal at all.

What remains is the loop's own units of work between those calls: folding state, evaluating a stop condition, anything running in cognet code. A JS function cannot be preempted, so honouring this is cooperative and always will be. Check it between units and return.

Invocation and continuous

export default defineCognet({
    mode: { kind: "invocation" },   // or { kind: "continuous" }
})
ModeWoken byEmpty stimuli
invocationA stimulus arrivingDoes not happen
continuousA clock the body owns, via kernel.wake()The ordinary steady state

Part of the cognet's own declared identity, never blueprint-overridable — same trust direction as abi. A cognet built for invocation-based wakes was never written to tolerate an empty-stimuli tick, so an agent author cannot flip this from outside.

Continuous carries no rate. It declares the shape of the cognet — "tick me, don't hand me a chat prompt" — and nothing more. A tickMs lived here once and was wrong in the same way a salience field on a stimulus is wrong: it had the brain asserting how fast its world turns, which it cannot know. The rate lives with whatever drives kernel.wake().

wakeOn — which entries wake you

export default defineCognet({
    wakeOn: ["cognet:stimulus:text"],   // absent = wake on everything
})

A conversational cognet declares nothing and hears all of it. One attached to a firehose sensor names the few kinds worth a thought. Overridable by the blueprint.

maxTicksPerWake — the runaway guard

export default defineCognet({
    maxTicksPerWake: 32,   // omitted = UNBOUNDED, and that is the right default
})

Exceeding it throws COGNET_MAX_TICKS.

Omitted means unbounded, which is correct for most cognets. A ceiling cannot distinguish a runaway loop from a long job — the only difference is whether the ticks accomplish anything, which a count cannot see. zero capped this at 32 once and killed a run mid-verification after forty clean turns of real work.

Set it only where a wake is genuinely expected to converge in a known number of steps — a classifier, a fixed pipeline — so exceeding it really is a bug. For open-ended work, what bounds a wake is <done/>, the user's interrupt, and the engine failing loudly.

Plugins

// plugins/clock.ts
export default definePlugin(({ hooks }) => {
    hooks.on("boot", () => {
        setInterval(() => void kernel.wake(), 1000 / 30)
    })
})
HookFires
bootOnce, after main() declared the loop
wakeBefore the first tick of each wake, with the wake
tickBefore each tick body, with { tick }
shutdownOnce, at unload()

Registered at import time, awaited to completion in order. tick is on the hot path — plugins there must stay cheap.

definePlugin is global-only and side-effecting: calling it registers the hooks.

Overlap

Two wakes of a continuous cognet routinely overlap — the clock fires whether or not the previous wake finished. Each gets its own tick counter and its own ambient scope, bound across every await, so a phase() in one never times against the other's clock.

Module scope is shared. That is the brain's resident memory, and treating it as single-writer is the cognet's problem to solve.