fix(bot/place): auto-shift placement target out of bot's own bounding box - #17
Open
Pablomonte wants to merge 2 commits into
Conversation
… 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
force-pushed
the
fix/place-block-auto-shift-when-bot-occupies-target
branch
from
May 17, 2026 14:35
abaab2e to
cb158d7
Compare
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>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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:
…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
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.