v0.3.0-rc.3: event-driven awareness + skill pre-emption #26

Closed
halofourteen wants to merge 2 commits from v0.3.0-rc.3 into v0.3.0-rc.2
halofourteen commented 2026-05-27 17:57:12 +03:00 (Migrated from github.com)

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

  • 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:
    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 for the full v0.3.0
design and dev/v0.3.0/STATUS.md for shipped
status per rc.

🤖 Generated with Claude Code

## 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)
halofourteen commented 2026-05-27 18:09:43 +03:00 (Migrated from github.com)

Superseded by single v0.3.0 PR

Superseded by single v0.3.0 PR

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.