Skip to content

Auto-fixes for feat/ivan/spy#2748

Merged
leshy merged 15 commits into
feat/ivan/spyfrom
feat/ivan/spy-autofixes
Jul 6, 2026
Merged

Auto-fixes for feat/ivan/spy#2748
leshy merged 15 commits into
feat/ivan/spyfrom
feat/ivan/spy-autofixes

Conversation

@paul-nechifor

Copy link
Copy Markdown
Contributor

These are automated fixes. Each fix is a separate commit. Use git rebase -i to drop any you disagree with.

It is imported by dimos/robot/cli/dimos.py, so it is public API of
run_spy, not a module-private helper.
import sys in run_spy and the test-only imports had no reason to be
inline. The lazy backend imports in core.py stay inline but now carry a
comment saying why (an unavailable backend must not break the others).
The 60.0s default was duplicated in TopicStats.__init__ and
TransportSpy.__init__.
- App -> App[None], **kwargs: Any, DataTable -> DataTable[Text]
- cursor_type=None -> the documented "none" CursorType literal; drop
  zebra_stripes=False (the default)
- narrow self.table once at the top of refresh_table instead of three
  union-attr ignores
_parse_transports and SpyApp.__init__ built near-identical error
messages; both now use validate_transport_names in core, with the CLI
converting the ValueError to SystemExit.
Source construction/validation, the autoconf warning, and spy.start()
move to a new start_spy() called from main(), which now owns the spy's
lifecycle with try/finally around app.run(). SpyApp just receives the
started TransportSpy, so a failed start can no longer leave a half-built
app. Tests exercise start_spy() through a fixture that always stops the
spy on teardown.
Replace the two print(..., file=sys.stderr) warnings in core with
structured logger.warning calls. Tests assert on caplog (via a shared
spy_warnings fixture that hooks the non-propagating dimos logger)
instead of capsys.
If one untap() or source.stop() raised, the remaining sources were
never stopped. Each failure is now logged and the shutdown loop
continues.
Only tests called it; the TUI shows freq, bandwidth, total and age.
total_bytes/total_msgs/last_seen are written under the stats lock by
transport threads but were read unlocked from the UI thread, and each
refresh traversed+copied the history twice per row (freq +
bytes_per_sec). window_stats() now returns a consistent WindowStats
reading computed in a single locked pass without building intermediate
lists; refresh_table sorts and renders from it. freq/bytes_per_sec stay
as thin wrappers for the contract tests.
The feature is implemented on this branch; TASK.md and the
agent/spy-architect branch no longer exist.
Buses, sources, publishers and the zenoh session pool were cleaned up
with hand-rolled try/finally (or a contextmanager) in test bodies; move
them to yield-fixtures / request.addfinalizer so teardown also runs
when an assertion fails before the cleanup line.
Replace the fixed establishment sleeps with a probe loop: publish on a
probe topic until the tap/wildcard subscription delivers one, with a
deadline. The 0.01s pacing sleeps in publish loops stay (throttling,
not waiting). Cuts the spy test suite from ~3.2s to ~1.4s.
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates the spy CLI and its tests. The main changes are:

  • Refactored spy startup into start_spy().
  • Moved SpyApp to receive an already-started spy.
  • Added structured window stats for table refreshes.
  • Switched skipped-transport diagnostics to the project logger.
  • Added test fixtures and lifecycle tests for spy transports.

Confidence Score: 4/5

This is close, but these issues should be fixed before merging.

  • A failed transport stop can still be removed from retry tracking.
  • Skipped-transport startup diagnostics still do not reach stderr.
  • The rest of the refactor is scoped and covered by updated tests.

dimos/utils/cli/spy/core.py

Important Files Changed

Filename Overview
dimos/utils/cli/spy/core.py Adds windowed stats and logger-based lifecycle handling, but failed transport stops can still be dropped from retry tracking and skipped-transport diagnostics still miss stderr.
dimos/utils/cli/spy/run_spy.py Moves spy construction and startup out of SpyApp and into start_spy() and main().
dimos/utils/cli/spy/conftest.py Adds a fixture that captures the spy core logger directly for warning assertions.
dimos/utils/cli/spy/test_run_spy.py Updates spy CLI tests around start_spy() and the renamed LCM argv helper.
dimos/utils/cli/spy/test_spy.py Updates core spy tests for window stats, transport startup, warning capture, and stop continuation.
dimos/robot/cli/dimos.py Updates the lcmspy alias to use the renamed lcm_only_argv helper.
dimos/robot/cli/test_dimos.py Adjusts CLI tests for the spy helper import.
dimos/codebase_checks/test_get_logger.py Whitelists direct logger access in the spy test fixture.

Reviews (2): Last reviewed commit: "add fix" | Re-trigger Greptile

Comment on lines 336 to 341
for source in self._live:
source.stop()
try:
source.stop()
except Exception as exc:
logger.warning("Error stopping spy source", transport=source.name, error=str(exc))
self._live.clear()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Failed Stops Lose Tracking

When source.stop() raises after leaving its transport session or subscription active, this path logs the error and then clears _live. Later stop() calls cannot retry that source, so a long-lived caller that creates and stops spies can leak an active LCM or Zenoh subscription after one transient stop failure.

Comment on lines +315 to 319
logger.warning(
"Skipping spy transport that failed to start",
transport=source.name,
error=str(exc),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Skipped Transport Warning Moves

This warning used to go directly to stderr, but the spy logger is configured with a stdout console handler and propagate=False. A user running dimos spy 2>errors.log, or a wrapper that watches stderr for startup diagnostics, no longer sees that a transport was skipped.

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!

Comment on lines 328 to 341
def stop(self) -> None:
"""Untap and stop everything; an error in one source never skips the rest."""
for untap in self._untaps:
untap()
try:
untap()
except Exception as exc:
logger.warning("Error untapping spy source", error=str(exc))
self._untaps.clear()
for source in self._live:
source.stop()
try:
source.stop()
except Exception as exc:
logger.warning("Error stopping spy source", transport=source.name, error=str(exc))
self._live.clear()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Failed stops disappear
When source.stop() raises after only partially shutting down its transport, this method logs the error and then clears _live anyway. A later stop() call has no source left to retry, so an LCM or Zenoh subscription that failed during teardown can stay active for the rest of the process. Keep failed sources tracked until their stop succeeds, while still stopping the healthy sources.

Suggested change
def stop(self) -> None:
"""Untap and stop everything; an error in one source never skips the rest."""
for untap in self._untaps:
untap()
try:
untap()
except Exception as exc:
logger.warning("Error untapping spy source", error=str(exc))
self._untaps.clear()
for source in self._live:
source.stop()
try:
source.stop()
except Exception as exc:
logger.warning("Error stopping spy source", transport=source.name, error=str(exc))
self._live.clear()
def stop(self) -> None:
"""Untap and stop everything; an error in one source never skips the rest."""
remaining_untaps = []
for untap in self._untaps:
try:
untap()
except Exception as exc:
logger.warning("Error untapping spy source", error=str(exc))
remaining_untaps.append(untap)
self._untaps = remaining_untaps
remaining_live = []
for source in self._live:
try:
source.stop()
except Exception as exc:
logger.warning("Error stopping spy source", transport=source.name, error=str(exc))
remaining_live.append(source)
self._live = remaining_live

Comment on lines +315 to 319
logger.warning(
"Skipping spy transport that failed to start",
transport=source.name,
error=str(exc),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Warnings miss stderr
These skipped-transport warnings still go through the spy logger, which is configured with propagate=False and a stdout console handler. A user running dimos spy 2>errors.log, or a wrapper that watches stderr for startup diagnostics, will not see that a backend was skipped. Emit these startup degradation messages to stderr or add an explicit stderr sink for this path.

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!

@leshy leshy merged commit bf819c1 into feat/ivan/spy Jul 6, 2026
5 checks passed
@leshy leshy deleted the feat/ivan/spy-autofixes branch July 6, 2026 23:06
@leshy

leshy commented Jul 6, 2026

Copy link
Copy Markdown
Member

Merged into feat/ivan/spy at bf819c1 — took all 15 commits, none dropped; authorship preserved (merge, not squash).

Reviewed each one: the type-resolution, import-hoisting, DRY validation extraction, start_spy() extraction (side-effects out of SpyApp.__init__, lifecycle owned by main()), logger-based degrade warnings + conftest, non-aborting stop(), one-pass locked WindowStats read, gradient/history-window constants, unused avg_size removal, poll-instead-of-sleep, and the codebase-check whitelist all look correct and preserve the best-effort + reject-root-transport semantics.

Two of my later commits on the base (try/finally test cleanup; sys-hoist + App[None]) were superseded by your more complete versions (fixture teardown; full type resolution), so I resolved those conflicts in favor of yours. My cli.md docs and reject-root---transport commits are retained on top.

Gate after merge: 54 spy/CLI/codebase-check tests pass, mypy + ruff clean. Thanks! This PR can be closed.

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.

2 participants