Skip to content

fix(exploration): do not block agent loop#1258

Open
paul-nechifor wants to merge 2 commits intodevfrom
paul/fix/exploration-agent-blocking
Open

fix(exploration): do not block agent loop#1258
paul-nechifor wants to merge 2 commits intodevfrom
paul/fix/exploration-agent-blocking

Conversation

@paul-nechifor
Copy link
Contributor

Problem

start_exploration blocks the loop.

Closes DIM-531

https://linear.app/dimensional/issue/DIM-531/make-start-exploration-not-block

Solution

  • Made it run in the background, without blocking.
  • Added thinking... to humancli so it's visible when the agent blocks.
  • Moved the exploration skills to out of NavigationSkillContainer and into WavefrontFrontierExplorer where they belong.

Breaking Changes

None

How to Test

uv run dimos run unitree-go2-agentic
uv run humancli
# say "start exploring" and "stop"

@greptile-apps
Copy link

greptile-apps bot commented Feb 14, 2026

Greptile Overview

Greptile Summary

This PR makes start_exploration non-blocking by moving the exploration skills (begin_exploration, end_exploration) out of NavigationSkillContainer and into WavefrontFrontierExplorer, where they delegate to the existing explore() / stop_exploration() RPC methods that already run in a background thread. Navigation via _navigate_to is also made non-blocking (fire-and-forget set_goal instead of polling until arrival). A "thinking..." indicator is added to the humancli UI, driven by a new agent_idle output stream on the Agent module.

  • begin_exploration ignores the return value of self.explore(), always reporting success even when exploration is already active
  • ThinkingIndicator.show() leaks timers when called repeatedly without an intervening hide(), which happens because agent_idle.publish(False) fires on every incoming message
  • The system prompt (dimos/agents/system_prompt.py:32) still references the old skill names start_exploration and stop_movement, which no longer exist — the agent may try to call non-existent tools
  • Test fixtures (test_start_exploration.json, test_stop_movement.json) and tests in test_navigation.py reference the old skill names and will need updating

Confidence Score: 3/5

  • The core non-blocking change is sound, but there are logic bugs in the new skill wrapper and UI indicator that should be fixed before merge.
  • The main architectural change (moving exploration to fire-and-forget) is correct and well-motivated. However, begin_exploration silently swallows the "already active" case, ThinkingIndicator.show() leaks timers on repeated calls, and the system prompt and tests still reference old skill names that no longer exist. These are not catastrophic but will cause incorrect agent behavior and UI glitches.
  • dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py (begin_exploration ignores explore() return), dimos/utils/cli/human/humancli.py (timer leak in ThinkingIndicator.show)

Important Files Changed

Filename Overview
dimos/agents/agent.py Adds agent_idle publish calls to _process_message. The publish(False) at line 134 fires on every message, which can trigger duplicate show() calls in the UI's ThinkingIndicator when messages arrive in quick succession.
dimos/agents/skills/navigation.py Removes blocking navigation loop and exploration skills. Navigation now fires-and-forgets via set_goal. Renames stop_movement to stop_navigation and removes follow_human stub. Exploration skills moved to WavefrontFrontierExplorer.
dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py Adds begin_exploration and end_exploration as @skill-decorated methods. begin_exploration ignores the return value of self.explore(), always claiming success even when exploration is already active.
dimos/utils/cli/human/humancli.py Adds ThinkingIndicator with throbbing animation and idle subscription. show() leaks timers when called multiple times without hide() in between. Uses private RichLog internals (_line_cache, direct .lines manipulation) which is fragile.
dimos/mapping/voxels.py Trivial change: replaces print() with logger.info() for device logging. No issues.

Sequence Diagram

sequenceDiagram
    participant User as User (humancli)
    participant Agent as Agent
    participant NavSkill as NavigationSkillContainer
    participant Explorer as WavefrontFrontierExplorer
    participant Nav as NavigationInterface

    User->>Agent: "start exploring"
    Agent->>Agent: agent_idle.publish(False)
    Agent-->>User: ThinkingIndicator.show()
    Agent->>Explorer: begin_exploration()
    Explorer->>Explorer: explore() → spawn thread
    Explorer-->>Agent: "Started exploration skill..."
    Agent->>Agent: agent_idle.publish(True)
    Agent-->>User: ThinkingIndicator.hide()

    loop Exploration loop (background thread)
        Explorer->>Explorer: detect frontiers
        Explorer->>Nav: goal_request.publish(goal)
        Nav-->>Explorer: goal_reached event
    end

    User->>Agent: "stop"
    Agent->>Agent: agent_idle.publish(False)
    Agent->>Explorer: end_exploration()
    Explorer->>Explorer: stop_exploration() → join thread
    Explorer-->>Agent: "Stopped exploration."
    Agent->>Agent: agent_idle.publish(True)
Loading

Last reviewed commit: b164a42

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 14, 2026

Additional Comments (1)

dimos/agents/skills/navigation.py
Leftover debug print statement

This print is a leftover from previous code — it was not converted to logger.info like the other print calls in this PR (e.g., the one on line 160 and the one in voxels.py). Lines 309-311 also still have print(metadata) and print(first) debug statements.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@paul-nechifor paul-nechifor force-pushed the paul/fix/exploration-agent-blocking branch from 30b7a85 to b164a42 Compare February 14, 2026 06:02
@paul-nechifor
Copy link
Contributor Author

@greptile

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

def _process_message(
self, state_graph: CompiledStateGraph[Any, Any, Any, Any], message: BaseMessage
) -> None:
self.agent_idle.publish(False)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate show calls on rapid messages

agent_idle.publish(False) is called at the start of every _process_message, but agent_idle.publish(True) at line 147 is only called when the queue is empty. If multiple messages arrive in quick succession, this will call publish(False) multiple times without an intervening publish(True), triggering multiple ThinkingIndicator.show() calls. In show(), each call creates a new timer via set_interval without stopping the previous one (since hide() hasn't been called), leading to multiple concurrent throbbing timers and duplicate "thinking..." lines in the UI.

Comment on lines 72 to 76
def show(self) -> None:
self.visible = True
self._throb_dim = False
self._write_line()
self._timer = self._app.set_interval(0.6, self._toggle_throb)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

show() leaks timers when called repeatedly

show() doesn't guard against being called while already visible. Each call overwrites self._timer with a new set_interval without stopping the previous one, leaking the old timer. This happens because agent_idle.publish(False) fires at the start of every _process_message — consecutive messages will trigger multiple show() calls.

Suggested change
def show(self) -> None:
self.visible = True
self._throb_dim = False
self._write_line()
self._timer = self._app.set_interval(0.6, self._toggle_throb)
def show(self) -> None:
if self.visible:
return
self.visible = True
self._throb_dim = False
self._write_line()
self._timer = self._app.set_interval(0.6, self._toggle_throb)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@paul-nechifor paul-nechifor force-pushed the paul/fix/exploration-agent-blocking branch from b164a42 to b2863bf Compare February 14, 2026 06:33
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