Skip to content

fix(bot/place): auto-shift placement target out of bot's own bounding box - #17

Open
Pablomonte wants to merge 2 commits into
nicoechaniz:feat/canonical-loopfrom
Pablomonte:fix/place-block-auto-shift-when-bot-occupies-target
Open

fix(bot/place): auto-shift placement target out of bot's own bounding box#17
Pablomonte wants to merge 2 commits into
nicoechaniz:feat/canonical-loopfrom
Pablomonte:fix/place-block-auto-shift-when-bot-occupies-target

Conversation

@Pablomonte

Copy link
Copy Markdown
Contributor

Summary

The `place` action in `agents/bot/server.js` (canonical embodied-side dispatcher target for `place_block` tool calls) silently fails when the requested `(x, y, z)` is inside the bot's own bounding box. The Minecraft server rejects `_genericPlace` calls that would clip the player hitbox, but the rejection is a no-op (no exception), so the only signal is the post-place `blockAt()` check failing with the opaque:

"Placed crafting_table at (X, Y, Z) but block did not materialize. Retry or choose different coordinates."

The captain LLM then has no useful information — Gemma-Andy retries the same coordinates, hits the same silent reject, and after a few loops the bot tells the player "no puedo construir, el sistema sigue roto".

Repro (live trace from a Hermes-K2.6 + DaemonCraft session)

```
Captain → embodied_plan(intent=
"Place a crafting_table near my position, then craft a stone_shovel.")
→ Gemma-Andy plan: [place_block(crafting_table, x=bot.x, y=bot.y, z=bot.z),
craft_item(stone_shovel)]
→ bot place action: target == bot feet cell → silent server reject
→ materialize check fails → throws "did not materialize"
→ craft_item then fails: "needs crafting table nearby"
→ captain to player: "no puedo construir"
```

The fix

Before the existing occupied / neighbour-search logic in `place`, check whether the floor of the requested target equals the bot's feet cell (`floor(pos)`) or head cell (`feet + 1`). If yes, pick the first candidate from:

```
[(+1,0,0), (-1,0,0), (0,0,+1), (0,0,-1), ← four horizontal adjacents
(0,+2,0), ← two above feet (clear of head)
(0,-1,0)] ← below feet (rare, bot floating)
```

…that is:

  • not in the bot's own occupied cells
  • `air` / `cave_air`
  • has at least one solid neighbour to place against

…and rewrite `(x, y, z)` to that. Log the shift so operators see it in dispatch traces:

```
[place] target (X,Y,Z) overlaps bot — shifted to (X',Y',Z')
```

If no candidate fits, raise a clear error explaining the placement cannot proceed without first moving the bot — not the opaque `did not materialize` the captain previously had to guess at.

After

```
Captain → embodied_plan(intent=
"Place a crafting_table near my position, then craft a stone_shovel.")
→ Gemma-Andy plan: [place_block(..., x=bot.x, y=bot.y, z=bot.z),
craft_item(stone_shovel)]
→ bot place: overlap detected → shifted to (bot.x+1, bot.y, bot.z)
→ _genericPlace succeeds, materialize check passes
→ craft_item: finds crafting_table within 4 blocks → succeeds
→ captain to player: "listo, pala de piedra hecha"
```

Companion PRs (build-with-confidence stack)

These four together close the loop from "captain emits build/craft intent" to "action succeeds in the world" for fresh DaemonCraft installs.

Test plan

  • Repro the failure: bot standing at `(0,71,0)`, ask captain to place a crafting_table — see "did not materialize".
  • Apply the patch, restart bot, repeat — see `[place] target (0,71,0) overlaps bot — shifted to (1,71,0)` in log; block materializes; follow-up craft succeeds.
  • Edge: bot in a 1-wide tunnel (no horizontal neighbours) — should fall back to `above-head` candidate. Manual test pending.
  • Edge: bot floating mid-air with no solid neighbours anywhere — should raise the clear "no adjacent empty cell has a solid neighbour" error, not "did not materialize".

Scope

No new dependencies. No API change — `place` still accepts the same `{block, x, y, z}` shape. Logs are additive. `place_fill` and `place_sign` are intentionally not touched in this PR; `fill_volume` may need the same guard but its semantics differ (bulk fill across a rectangular region) so it warrants its own analysis.

… box

The `place` action in agents/bot/server.js (the canonical
embodied-side dispatcher target for `place_block` tool calls) had a
load-bearing failure mode: when the requested (x, y, z) was inside the
bot's own bounding box, the server silently rejected the placement and
the materialize-verification at the bottom surfaced an opaque
"did not materialize. Retry or choose different coordinates." error.

The captain LLM then had no useful information — Gemma-Andy retried
the same coordinates, hit the same silent reject, and after a few
loops told the player "no puedo construir, el sistema sigue roto".

Root cause: the action accepted (x, y, z) as the literal target and
walked the placement neighbour search without first checking whether
the bot itself occupied that cell. Servers reject `_genericPlace`
calls that would clip the player's hitbox, but the rejection comes
back as a no-op (no exception), so the only signal was the
post-place blockAt() check failing.

Fix: before the existing occupied / neighbour-search logic, check
whether the floor(target) cell equals the bot's feet cell (floor(pos))
or head cell (feet+1). If yes, pick the first candidate from
[(+x), (-x), (+z), (-z), feet+2 above-head, feet-1 below-feet] that
is:
  - not in the bot's own occupied cells
  - air / cave_air
  - has at least one solid neighbour to place against
…and rewrite (x, y, z) to that. Log the shift so operators can see it
in dispatch traces.

If no candidate fits, raise a clear error explaining the placement
cannot proceed without first moving the bot — not the opaque
"did not materialize" the captain previously had to guess at.

End-to-end repro before the fix:

  Captain (Kimi-K2.6) → embodied_plan(intent=
    "Place a crafting_table near my position, then craft a stone_shovel.")
  → Gemma-Andy plan: [place_block(crafting_table, x=bot.x, y=bot.y, z=bot.z),
                      craft_item(stone_shovel)]
  → bot place action: target == bot feet cell → silent server reject
  → materialize check fails → throws "did not materialize"
  → craft_item fails: "needs crafting table nearby"
  → captain to player: "no puedo construir"

After: place action detects the overlap, shifts target to the cell
immediately east of the bot (or first viable adjacent), places the
crafting_table, and the follow-up craft_item finds it within the
4-block radius.

This is one of three concurrent layers being hardened for "build with
confidence" alongside the canonical-loop policy patches
(nicoechaniz#16 — craft category + narrow inventory_query)
and the canonical-loop toolset wiring
(nicoechaniz/hermes-agent#7 + nicoechaniz#8).

No new dependencies, no API change. Pure additive guard inside the
existing place action body.
…rtial fail

place_fill used to report `ok: true` with "Placed N/M blocks" even when
the server silently rejected the placements — the captain saw success
and only discovered the structure was a ghost when a follow-up
place_block found no solid neighbour where the wall/pillar should be.

Three additive changes inside the inner placement loop:

1. Add `await new Promise(r => setTimeout(r, 150))` after `_genericPlace`
   so the next iteration's support-check (`b.blockAt(adjacent)`) reads
   fresh state. Without this, tall pillars silently fail level-by-level
   because the y+1 support check fires before the y block-update packet
   arrives.

2. Re-read `b.blockAt(pos)` after the wait; only increment `placed++`
   when the cell is actually non-air. Increment `failed++` otherwise.
   Previously placed was incremented unconditionally on no-throw.

3. Throw a structured error (rather than return ok:true) when
   `placed < openPositions.length * 0.5`. The error message includes
   the partial summary so the captain can decide to recover (move
   closer, split into smaller boxes, switch material) instead of
   assuming the structure is up.

Repro before fix:
  fill_volume(cobblestone, 2,71,2 → 2,74,2)   # 1×1×4 pillar
  → returns {ok: true, result: "Placed 4/4 cobblestone blocks (solid)"}
  → only block at y=71 actually materialized (server quirk: y+1 support
    check ran before y block-update packet arrived)
  → follow-up place_block(crafting_table, 2,75,2) fails with
    "no solid adjacent block" because the pillar is ghost

After: same call returns a clear partial error when materialization
fails, so the captain pivots immediately instead of building on air.

Companion to the post-goto re-shift in place (this same PR branch).
@Pablomonte
Pablomonte force-pushed the fix/place-block-auto-shift-when-bot-occupies-target branch from abaab2e to cb158d7 Compare May 17, 2026 14:35
Pablomonte added a commit to Pablomonte/DaemonCraft that referenced this pull request May 17, 2026
…ad of fictional error_types

The previous §6 promised three canonical error_types to the captain LLM:
"target_occupied", "no_solid_neighbor", "bot_in_target". These are referenced
in agents/local_agent/embodied.py (SPATIAL_ERRORS set) and used to gate Tier
2a auto-retry. But the embodied service dispatcher (foldBotResponse in
lib/dispatcher.js) collapses ALL bot errors to a generic
`error_type: "bot_action_failed"`. The canonical labels are never emitted,
so the captain's recovery path keyed on them was dead. The bug is preexisting
upstream — neither the dispatcher nor the bot were re-classifying placement
failure strings into canonical error_types, so the recovery contract the
SOUL described did not match runtime reality.

This commit closes the gap at the SOUL layer (lowest blast radius — zero
code change, deploys via profile sync). The captain is now taught to
pattern-match the `details` string against a 9-row catalog of real bot
diagnostic phrases and emit a corrective next intent. Each pattern was
verified against the actual error throw site in agents/bot/server.js:

  | pattern in details                | source                               |
  |-----------------------------------|--------------------------------------|
  | target space is occupied          | server.js:2363 (place)               |
  | inside my own body / footprint    | server.js:2354 (place — PR nicoechaniz#17)      |
  | no solid adjacent block / place against | server.js:2407 + 2358 (place)  |
  | did not materialize               | server.js:2343 (place)               |
  | No {item} in inventory            | server.js:2227 (equip) + 2317 (place)|
  | crafting_table nearby             | server.js:2163, 2169 (craft_item)    |
  | Mined K/N                         | dispatcher.js:detectSoftFailure      |
  | Can't ... / Failed to ...         | bot soft-failure prefix              |
  | timeout                           | generic                              |

Also updates the §1 worked example (line 39-48) to reflect what the
dispatcher actually emits today (error_type=bot_action_failed +
descriptive details) instead of the fictional error_type=target_occupied.

Doesn't preclude a future dispatcher-side classifier PR. With this in
place, the SOUL is grounded in the runtime shape; a follow-up that
emits canonical error_types deterministically is now strictly additive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pablomonte added a commit to Pablomonte/DaemonCraft that referenced this pull request May 17, 2026
… player intent

The previous §6 promised three canonical error_types to the captain LLM:
"target_occupied", "no_solid_neighbor", "bot_in_target". These are
referenced in agents/local_agent/embodied.py (SPATIAL_ERRORS set) and
used to gate Tier 2a auto-retry. But the embodied service dispatcher
(foldBotResponse in lib/dispatcher.js) collapsed ALL bot errors to a
generic `error_type: "bot_action_failed"`. The canonical labels were
never emitted, so the captain's recovery path keyed on them was dead.
The bug is preexisting upstream — neither the dispatcher nor the bot
re-classified placement failure strings into canonical error_types, so
the recovery contract the SOUL described did not match runtime reality.

This commit closes the gap at the SOUL layer (lowest blast radius —
zero code change, deploys via profile sync). Two things change:

1. **Pattern-match `details` rather than `error_type`.** The captain is
   taught to read the bot's free-text diagnostic string against a
   9-row catalog of real bot phrases. Each pattern was verified against
   the actual error throw site in agents/bot/server.js.

2. **Spatial recovery: think before relocating.** The first iteration
   of this SOUL listed "place at adjacent cell" as the default response
   to `target_occupied`. Field test exposed the flaw: the original
   coordinate often carried player intent (sealing a hole, hitting a
   named cell, completing a structure). Silently placing one cell over
   is a worse failure than refusing — it leaves the player with an
   incorrect world AND the false belief the task succeeded.

   New shape: §6 starts with a "pause and decide" checklist asking
   whether the cell was load-bearing, approximate, or just blocked by
   the body. The table lists OPTIONS, not prescriptions. Default
   tie-breaker is "ask the player" via a short chat message.

Pattern table (each row anchored in a real bot string):

  | pattern in details                | source                               |
  |-----------------------------------|--------------------------------------|
  | target space is occupied          | server.js:2363 (place)               |
  | inside my own body / footprint    | server.js:2354 (place — PR nicoechaniz#17)      |
  | no solid adjacent block / against | server.js:2407 + 2358 (place)        |
  | did not materialize               | server.js:2343 (place)               |
  | No {item} in inventory            | server.js:2227 + 2317 (equip/place)  |
  | crafting_table nearby             | server.js:2163, 2169 (craft_item)    |
  | Mined K/N                         | dispatcher.js:detectSoftFailure      |
  | Can't ... / Failed to ...         | bot soft-failure prefix              |
  | timeout                           | generic                              |

§1 worked example also rewritten: instead of silently relocating to an
adjacent cell, the captain now thinks aloud about whether relocation
serves the player's intent — and asks if it doesn't.

Companion PR with the dispatcher-side fix that ALSO emits canonical
error_types (so SOUL pattern matching + canonical error_types both
work, defense-in-depth): nicoechaniz#19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant