Every decision made so far, written as one pass of the simulation clock — from the world mutating to a mind discovering it was wrong. Read top to bottom; each level is called by the one above it.
The spine. Order is the schedule — and the ordering is what makes the whole thing deterministic, because both boundaries between world and mind are queues drained at fixed points.
function worldTick(W):
W.tick += 1
// 1. apply what minds decided last tick
drainIntents(W) // ECS performs every mutation
// 2. what changed is what can be perceived
changes = collectChanges(W) // ECS change detection, not polling
// 3. daemons turn world changes into percepts
percepts = []
for d in W.daemons:
for (sub, ch) in d.match(changes, W.subscribersByNode):
percepts += d.sense(W, sub, ch)
// 4. percept filters transform the queue — fear, illusion, a fae realm
for f in W.perceptFilters:
percepts = f(percepts, W)
// 5. deliver into minds
for p in percepts:
kbAssert(W.minds[p.subject].kb, p)
// 6. the floor: every mind, every tick, no search
stalled = []
for m in W.minds:
if reflexArbiter(m, W) == STALL: stalled.push(m)
// 7. the only expensive thing, and it is rationed
scheduleDeliberation(W, stalled)
W. They read their own KB and push intents, drained at step 1 of the next tick. Perception is the mirror image: the world pushes percepts, drained at step 5. Two queues, no back-channel, and rollback stays possible.Fixed native code, not a per-agent program — at a million agents, interpreting a script per agent per tick is the floor cost we cannot afford. What varies per agent is the table: thresholds, goal weights, and which compiled reactions are installed. Personality lives in the installed set.
function reflexArbiter(m, W) -> CONTINUE | SWITCH | STALL:
// a. did the world already contradict us? cheap check only
if m.plan and nextStepInvalidated(m):
m.plan = nil
// b. a percept fired a compiled reaction that outranks us
r = matchReaction(m.table.reactions, m.freshPercepts)
if r and priority(r) > priority(m.activity):
m.activity = r; m.plan = r.plan
return SWITCH
// c. is this still the right goal?
g = topGoal(m)
if shouldSwitch(m.table.commitment, m.goal, g):
m.goal = g; m.plan = nil
return SWITCH
// d. keep going
if m.plan and holds(m.kb, preconditions(nextStep(m.plan))):
emitIntent(m, nextStep(m.plan))
advance(m.plan)
return CONTINUE
// e. unmet goal, nothing applicable. A STATE, not an act.
return STALL
Nothing here requests deliberation. An agent that knows it needs to think is doing implausible self-inspection. It runs out of reflexes; the scheduler notices.
insistence(m, goal) = drive(m, goal) × urgency(goal) × m.table.weight[goal.kind]
topGoal(m) = argmax insistence(m, g) over g in m.goals where not satisfied(m.kb, g)
function shouldSwitch(strategy, current, candidate):
if current == nil: return true
if satisfied(current) or impossible(current): return true
switch strategy: // Kinny & Georgeff: match the
BLIND: return false // strategy to the world's
SINGLE_MINDED: return false // rate of change
OPEN_MINDED: return insistence(candidate)
> insistence(current) × (1 + m.table.hysteresis)
drive, urgency and the default commitment strategy are unpinned. Insistence is called from four places — goal choice, switching, the leak threshold, and the deliberation queue's priority — so it is the single undefined function with the widest blast radius.The budget is global, not per-agent. Most agents never deliberate on a given tick and live on reflexes and compiled frames. "You have to wait until Sunday" is not flavour — it is this queue.
function scheduleDeliberation(W, stalled):
budget = W.deliberationBudget
// minds already mid-thought resume exactly where they stopped
for m in W.thinking:
budget -= runMachine(m.machine, W.envs, min(m.share, budget))
if m.machine.status == DONE: install(m, m.machine.result)
// newly stuck minds compete for what is left
for m in sortDesc(stalled, m => insistence(m, m.goal) × staleness(m)):
if budget <= 0: break // the rest wait, and leak while waiting
m.machine = makeMachine(deliberate(m.goal), fork(m.kb))
budget -= runMachine(m.machine, W.envs, min(SLICE, budget))
fork(m.kb) is a child frame over the mind's own beliefs, which are themselves forked from truth. Imagining eating the fish allocates one frame and cannot reach the world — enforced by data layout, not discipline.deliberate(goal) = // a Lisp form, run on the CEK machine
(or (goap-plan goal) // fast: bitwise, over a compiled frame
(fol-plan goal)) // slow: backward chaining over clauses
function goapPlan(m, goal):
frame = frameFor(m, situation(m)) // cached, or compiled on the spot
if frame == nil: return NO_FRAME // too big for working memory → go slow
return aStarBackward(project(goal, frame), frame)
Regression, which is the same operator used three ways — to search, to compute a macro's precondition, and to test relevance:
relevant(op, g) = (op.eff.mask & g.mask) != 0
and (op.eff.val ^ g.val) & op.eff.mask & g.mask == 0
function regress(g, op):
keep = g.mask & ~op.eff.mask // goal bits op leaves alone
if (g.val ^ op.pre.val) & keep & op.pre.mask:
return BOTTOM // preconditions fight the goal
return ( keep | op.pre.mask,
(g.val & keep) | (op.pre.val & op.pre.mask) )
reached(g, state) = (state.val & g.mask) == g.val
PLAN · EXHAUSTED — the space closed and no plan exists, so the goal is impossible and commitment drops it · OUT_OF_FUEL — still stuck, try again later, and leak in the meantime. Collapsing the last two makes an agent give up on solvable problems and keep grinding at impossible ones.function compileFrame(m, situation):
objects = entitiesWithin(m.kb, situation.node, K_HOPS) // the graph bounds it
lits = []
for s in m.schemas:
for b in groundings(s, objects):
lits += s.pre[b] + s.add[b] + s.del[b]
vocab = dedupe(lits)
if len(vocab) > FRAME_BITS: return nil // ← this is the whole boundary
frame = intern(vocab) // literal ↔ bit position
frame.ops = [ toOp(s, b, frame) for each grounding ]
frame.byEffect = index(frame.ops) // (bit,value) → ops. why it is fast
return frame
Naive grounding explodes — n objects by a k-ary predicate is nk propositions. The graph solves it: locality of world gives locality of grounding. And FRAME_BITS is not a tuning knob. A task that does not fit cannot be compiled and must be reasoned about the slow way, which makes working-memory capacity a per-agent number with real consequences.
KB = { facts: PersistentSet<Assertion>, clauses: [Clause] }
Assertion = { term, source: DaemonId | AgentId | SELF, tick, confidence }
Clause = { head: Term, body: [Term] } // Horn
function prove(kb, goal, fuel) -> { bindings, support: [Assertion|Clause] }
// backward chaining with unification, metered by the CEK machine.
// SUPPORT is the composed lineage — not a source tag. Without it an agent
// can notice it was wrong but cannot work out whose fault that was.
// decay is not a mechanism, it is a clause:
(percept ?sense ?t ?f) and (recent ?t) -> ?f
(at fish cave) need not be asserted to be believed. And because inference is fuel-metered, knowing is not binary — an agent can fail to reach something it technically knows.function emitIntent(m, step):
m.intents.push({ actor: m.id, action: step })
m.expectations.push({
tick: W.tick,
predicted: applyEffects(m.kb, step.schema),
support: step.support // WHY we believed this would work
})
function checkExpectations(m):
for e in m.expectations where matured(e):
d = diff(e.predicted, observe(m.kb, e.predicted.terms))
if d: attribute(m, e, d)
catch at the empty cave does not return an error. It runs. The error is a diff computed in the mind between a remembered prediction and a later percept — which is why it can be delayed, partial, and misattributed.Attribution is the personality program. Four candidates, all consistent with the same evidence, and nothing in the data picks a winner:
attribute(m, e, diff) = run(m.table.attributionPolicy, [e, diff]) // the percept was wrong → distrust that sense // my clause was wrong → retract or weaken it (this is learning) // my informant was wrong → update trust, and possibly call it betrayal // the world moved → retract the fact, blame nobody
matures. Checking immediately is the obvious answer and probably the wrong one — an agent that only checks immediately can never discover a delayed error.StressDaemon.sense(W, sub, _):
m = W.minds[sub]
if m.lastOutcome == STALL
and insistence(m, m.goal) > m.table.leakThreshold:
sym = m.table.leakSymbol[m.goal.kind]
return [ Assertion((distressed m.id sym), source=STRESS, tick=W.tick)
for each subscriber within N hops ]
Not a move in the arbiter and not an action in any schema. Being stuck makes stress observable; the agent has no say. That is what puts signals into the world before anyone has learned to read them — without an involuntary channel, no agent ever emits, so no agent ever learns meaning, and the culture cannot start.
Complaining is the learned trick built on top — deliberately producing that signal to cause help. A schema with abduced or transmitted provenance, plannable like anything else. And its mirror is equally learned: suppression, the skill of masking your own leak.
wp([], G) = G
wp(a:rest, G) = regress(wp(rest, G), a) // the planner IS the compiler
function onPlanSucceeded(m, plan, goal):
macro = { pre: wp(plan, goal), eff: goal, cost: sum(costs(plan)) }
macro = generalize(macro, plan.support) // lift only what the proof
// did not actually depend on
if utility(macro) > m.table.compileThreshold:
m.schemas.add(macro, provenance = DERIVED)
utility(macro) = savings(macro) × frequency(macro) - matchCost(macro)
// Minton's utility problem: an agent that never forgets a trick seizes up.
Goal choice, commitment switching, the leak threshold, and the deliberation queue's priority all call it. Nothing else has this blast radius.
Everything above is written against (at fish river) without pinning how a fact is stored, how an assertion wraps a term, or how lineage attaches. The renderer also keys emoji off term heads.
Small, but it sits in the arbiter's hot path and decides whether agents thrash or stubbornly finish bad plans.
Appears in level 7 as an undefined predicate. Delayed discovery is more interesting than immediate, which argues against the easy answer.
Gates renderer work. Substrate work proceeds regardless.
The smell-daemon shortcut means demo one is a single agent with no communication at all: a true percept, a bad clause, a confident plan, an empty cave. That leaves a lot legitimately stubbed — compilation (seed the schemas), attribution (one hardcoded policy; the paranoid-versus-humble split is the later twist), frame caching and eviction (one frame, no utility tracking), and every speech act.
One correction worth recording: tail calls matter less than previously claimed. The GOAP planner is propositional and runs as a primitive, so it never grows the continuation stack. Only the slow FOL layer loops inside the machine — still worth fixing, no longer urgent.