In v0.2.x the reflex was purely polling — every ~2s it took a snapshot
and decided what to do. Anything happening between ticks was
invisible. The concrete failure: when the operator dug a path that let
the bot fall to a new area, the bot continued executing its prior explore.far against stale assumptions until the next tick. By then
it had wandered further off course, and the cycle never broke. Same
problem for hostile spawns and HP plunges — the reflex saw them only
after the current skill ran its 30-90s timeout.
This rc gives the reflex an event-driven layer that preempts the
in-flight skill within ~100ms of an environmental shock. It closes the
v0.3.0 loop:
awareness fires preempt → dispatch aborts
reflex tick re-evaluates → manifesto (rc.2) walks the ladder
new dispatch picks the right skill for the new world state
Changes
runtime/awareness/events.js (new) — attachAwareness(bot, {onPreempt})
wires direct mineflayer listeners:
health: HP drop ≥ 2 in one tick → health_plunge + preempt
entitySpawn: hostile mob within 12 blocks → hostile_added + preempt
blockUpdate: nearby block change → env_changed (informational,
NOT preempting — throttled 800ms; otherwise gather skills would
self-preempt every dig action)
runtime/skills/index.js:
New RUNNER_CODES.PREEMPTED
runSkill() now races execute() with ctx.abortSignal. If the
signal fires mid-await, the skill returns { ok: false, code: "preempted" } within one microtask. No skill code change
needed — existing skills get preemption for free.
runtime/bot.js:
dispatchAction creates a fresh AbortController per dispatch
and stores it on reflexCtx.currentAbort / reflexCtx.abortSignal
attachAwareness(bot, {onPreempt}) fires controller.abort() when
something disrupts the active skill
reflexCtx.lastPreempt records the most recent shock for telemetry
Behavioural diff
Situation
v0.2.x → v0.3.0-rc.2
v0.3.0-rc.3
Operator digs a path; bot falls 8 blocks
explore.far continues with stale target
Aborted → next tick replans from new pos
Creeper spawns 6m away mid-gather.logs
Continues chopping for ~30s
Aborted → defendReflex / manifesto L0
Skeleton arrow drops HP by 4 mid-skill
Continues; reflex sees it on next tick
Aborted → manifesto L0 → survive.flee
Block placed by another player nearby
Invisible
env_changed flag (no preempt)
Test plan
npm test — 332 green (was 315 on rc.2, +17 new)
12 tests in awareness/events.test.js (each event + thresholds)
3 preempt tests in skills/contract.test.js (mid-flight, pre-armed,
clean signal)
Expect to see preempt: aborting <skill> due to <reason> lines whenever
the env shifts. Frequent preempts are a sign the awareness layer is
working — should correlate with faster recovery from wedged states.
Followup (post-v0.3.0)
Wire fast-advisor.js auto-trigger when awareness.hasPreempting() && recent skills repeat. Tactical LLM call ("you just got preempted by
X, what now?") was the third leg in the original rc.1 design but
punted to keep this PR shippable. Becomes a v0.3.1 or v0.4.0 PR.
Vision (multimodal LLM on prismarine-viewer screenshots) when wedged > 60s — also v0.3.1 candidate
Persist recentPreempts count to scenario-memory so we can learn
"in this situation, preempts cluster" patterns
## What and why
In v0.2.x the reflex was purely polling — every ~2s it took a snapshot
and decided what to do. Anything happening **between** ticks was
invisible. The concrete failure: when the operator dug a path that let
the bot fall to a new area, the bot continued executing its prior
`explore.far` against stale assumptions until the next tick. By then
it had wandered further off course, and the cycle never broke. Same
problem for hostile spawns and HP plunges — the reflex saw them only
after the current skill ran its 30-90s timeout.
This rc gives the reflex an **event-driven layer** that preempts the
in-flight skill within ~100ms of an environmental shock. It closes the
v0.3.0 loop:
```
awareness fires preempt → dispatch aborts
reflex tick re-evaluates → manifesto (rc.2) walks the ladder
new dispatch picks the right skill for the new world state
```
## Changes
- `runtime/awareness/events.js` (new) — `attachAwareness(bot, {onPreempt})`
wires direct mineflayer listeners:
- `move`: single-tick Δposition ≥ 5 blocks → `forced_move` + preempt
- `health`: HP drop ≥ 2 in one tick → `health_plunge` + preempt
- `entitySpawn`: hostile mob within 12 blocks → `hostile_added` + preempt
- `blockUpdate`: nearby block change → `env_changed` (informational,
NOT preempting — throttled 800ms; otherwise gather skills would
self-preempt every dig action)
- `runtime/skills/index.js`:
- New `RUNNER_CODES.PREEMPTED`
- `runSkill()` now races `execute()` with `ctx.abortSignal`. If the
signal fires mid-await, the skill returns `{ ok: false, code:
"preempted" }` within one microtask. **No skill code change
needed** — existing skills get preemption for free.
- `runtime/bot.js`:
- `dispatchAction` creates a fresh `AbortController` per dispatch
and stores it on `reflexCtx.currentAbort` / `reflexCtx.abortSignal`
- `attachAwareness(bot, {onPreempt})` fires `controller.abort()` when
something disrupts the active skill
- `reflexCtx.lastPreempt` records the most recent shock for telemetry
## Behavioural diff
| Situation | v0.2.x → v0.3.0-rc.2 | v0.3.0-rc.3 |
|---------------------------------------------|-----------------------|----------------------------------------|
| Operator digs a path; bot falls 8 blocks | `explore.far` continues with stale target | Aborted → next tick replans from new pos |
| Creeper spawns 6m away mid-`gather.logs` | Continues chopping for ~30s | Aborted → defendReflex / manifesto L0 |
| Skeleton arrow drops HP by 4 mid-skill | Continues; reflex sees it on next tick | Aborted → manifesto L0 → `survive.flee` |
| Block placed by another player nearby | Invisible | `env_changed` flag (no preempt) |
## Test plan
- [x] `npm test` — 332 green (was 315 on rc.2, +17 new)
- 12 tests in `awareness/events.test.js` (each event + thresholds)
- 3 preempt tests in `skills/contract.test.js` (mid-flight, pre-armed,
clean signal)
- [ ] Post-merge: deploy and watch live logs:
```bash
tail -F state/.../logs/$(date -I).log | grep -E "(preempt|forced_move|health_plunge|hostile_added)"
```
Expect to see `preempt: aborting <skill> due to <reason>` lines whenever
the env shifts. Frequent preempts are a sign the awareness layer is
working — should correlate with faster recovery from wedged states.
## Followup (post-v0.3.0)
- Wire fast-advisor.js auto-trigger when `awareness.hasPreempting() &&
recent skills repeat`. Tactical LLM call ("you just got preempted by
X, what now?") was the third leg in the original rc.1 design but
punted to keep this PR shippable. Becomes a v0.3.1 or v0.4.0 PR.
- Vision (multimodal LLM on prismarine-viewer screenshots) when
`wedged > 60s` — also v0.3.1 candidate
- Persist `recentPreempts` count to scenario-memory so we can learn
"in this situation, preempts cluster" patterns
See [`dev/v0.3.0/PLAN.md`](dev/v0.3.0/PLAN.md) for the full v0.3.0
design and [`dev/v0.3.0/STATUS.md`](dev/v0.3.0/STATUS.md) for shipped
status per rc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What and why
In v0.2.x the reflex was purely polling — every ~2s it took a snapshot
and decided what to do. Anything happening between ticks was
invisible. The concrete failure: when the operator dug a path that let
the bot fall to a new area, the bot continued executing its prior
explore.faragainst stale assumptions until the next tick. By thenit had wandered further off course, and the cycle never broke. Same
problem for hostile spawns and HP plunges — the reflex saw them only
after the current skill ran its 30-90s timeout.
This rc gives the reflex an event-driven layer that preempts the
in-flight skill within ~100ms of an environmental shock. It closes the
v0.3.0 loop:
Changes
runtime/awareness/events.js(new) —attachAwareness(bot, {onPreempt})wires direct mineflayer listeners:
move: single-tick Δposition ≥ 5 blocks →forced_move+ preempthealth: HP drop ≥ 2 in one tick →health_plunge+ preemptentitySpawn: hostile mob within 12 blocks →hostile_added+ preemptblockUpdate: nearby block change →env_changed(informational,NOT preempting — throttled 800ms; otherwise gather skills would
self-preempt every dig action)
runtime/skills/index.js:RUNNER_CODES.PREEMPTEDrunSkill()now racesexecute()withctx.abortSignal. If thesignal fires mid-await, the skill returns
{ ok: false, code: "preempted" }within one microtask. No skill code changeneeded — existing skills get preemption for free.
runtime/bot.js:dispatchActioncreates a freshAbortControllerper dispatchand stores it on
reflexCtx.currentAbort/reflexCtx.abortSignalattachAwareness(bot, {onPreempt})firescontroller.abort()whensomething disrupts the active skill
reflexCtx.lastPreemptrecords the most recent shock for telemetryBehavioural diff
explore.farcontinues with stale targetgather.logssurvive.fleeenv_changedflag (no preempt)Test plan
npm test— 332 green (was 315 on rc.2, +17 new)awareness/events.test.js(each event + thresholds)skills/contract.test.js(mid-flight, pre-armed,clean signal)
preempt: aborting <skill> due to <reason>lines wheneverthe env shifts. Frequent preempts are a sign the awareness layer is
working — should correlate with faster recovery from wedged states.
Followup (post-v0.3.0)
awareness.hasPreempting() && recent skills repeat. Tactical LLM call ("you just got preempted byX, what now?") was the third leg in the original rc.1 design but
punted to keep this PR shippable. Becomes a v0.3.1 or v0.4.0 PR.
wedged > 60s— also v0.3.1 candidaterecentPreemptscount to scenario-memory so we can learn"in this situation, preempts cluster" patterns
See
dev/v0.3.0/PLAN.mdfor the full v0.3.0design and
dev/v0.3.0/STATUS.mdfor shippedstatus per rc.
🤖 Generated with Claude Code
Superseded by single v0.3.0 PR
Pull request closed