Skip to content

feat(cli): dimos log command for viewing per-run logs (DIM-698)#1497

Merged
spomichter merged 6 commits intodevfrom
feat/dim-698-dimos-log
Mar 10, 2026
Merged

feat(cli): dimos log command for viewing per-run logs (DIM-698)#1497
spomichter merged 6 commits intodevfrom
feat/dim-698-dimos-log

Conversation

@spomichter
Copy link
Contributor

Summary

Adds dimos log command to view per-run logs without manually finding the log path.

Closes DIM-698

Usage

dimos log              # last 50 lines, human-readable
dimos log -f           # follow/tail in real time
dimos log -n 100       # last N lines
dimos log --all        # full log
dimos log --json       # raw JSONL
dimos log --run <id>   # specific run by ID

Output

Human-readable (default):

15:55:06 [inf] blueprints.py     Building the blueprint
15:55:06 [inf] blueprints.py     Starting the modules
15:55:06 [inf] worker_manager.py Worker pool started.  n_workers=4
15:55:09 [inf] worker.py         Deployed module.  module=McpServer

Architecture

  • dimos/core/log_viewer.py: Log resolution, JSONL parsing, formatting, follow mode
  • dimos/robot/cli/dimos.py: Thin 15-line CLI wrapper

Log resolution: alive run → most recent run → error. No new dependencies.

Changes

  • dimos/core/log_viewer.py: New file (+97 lines)
  • dimos/robot/cli/dimos.py: Add log command (+30 lines)

Tested

  • dimos log — full output, human-readable ✅
  • dimos log -n 3 — last 3 lines ✅
  • dimos log --json -n 2 — raw JSONL ✅
  • dimos log with no runs — "No log files found" ✅
  • mypy clean ✅

Contributor License Agreement

  • I have read and approved the CLA

Add log_viewer.py with log resolution, JSONL formatting, and tail -f
follow mode. CLI wrapper in dimos.py is a thin 15-line command.

Usage:
  dimos log              # last 50 lines, human-readable
  dimos log -f           # follow in real time
  dimos log -n 100       # last N lines
  dimos log --all        # full log
  dimos log --json       # raw JSONL
  dimos log --run <id>   # specific run

Closes DIM-698
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR adds a dimos log CLI command for viewing per-run JSONL logs without manually locating the log file. The implementation is split cleanly between dimos/core/log_viewer.py (log resolution, JSONL parsing, tail logic) and a thin 30-line CLI wrapper in dimos/robot/cli/dimos.py. No new dependencies are introduced.

Key findings:

  • follow_log unconditionally seeks to EOF before yielding lines, meaning dimos log -f produces no output for existing log content — only lines written after the command starts. This contradicts standard tail -f behaviour.
  • As a consequence, the -n / --lines flag is silently ignored in follow mode. Running dimos log -f -n 100 does not replay the last 100 lines before tailing, which violates the common tail -n N -f contract.
  • The read_log implementation correctly uses deque(maxlen=count) to avoid loading the full file, which is a nice touch for large logs.

Both issues affect the follow mode specifically and can be fixed with coordinated changes to add a tail_lines parameter to follow_log and pass it from the CLI layer.

Confidence Score: 3/5

  • Safe to merge with low regression risk (additive feature only), but follow mode behavior contradicts user expectations and should be addressed.
  • The change is additive (new command only, no modifications to existing behaviour), so there is no regression risk. However, follow mode has functional limitations: it skips all existing log content and silently ignores the -n flag, making dimos log -f fundamentally incomplete for the primary follow use-case. These are user-facing issues that warrant fixing before shipping, but they are isolated to the new feature and do not affect existing functionality. The core log reading and formatting logic is solid.
  • dimos/core/log_viewer.py and dimos/robot/cli/dimos.py — coordinate a fix to add tail_lines parameter to follow_log and pass it from the CLI.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as dimos.py (log_cmd)
    participant LV as log_viewer.py
    participant RR as run_registry.py
    participant FS as File System

    User->>CLI: dimos log [options]
    CLI->>LV: resolve_log_path(run_id)
    LV->>RR: list_runs or get_most_recent
    RR->>FS: glob REGISTRY_DIR json files
    FS-->>RR: RunEntry JSON files
    RR-->>LV: list of RunEntry
    LV->>FS: check Path(log_dir)/main.jsonl exists
    FS-->>LV: Path or None
    LV-->>CLI: resolved path or None

    alt No log file found
        CLI-->>User: "No log files found" and exit 1
    else follow mode with -f flag
        CLI->>LV: follow_log(path)
        LV->>FS: open file and seek to EOF
        loop New lines appended to file
            FS-->>LV: readline
            LV-->>CLI: yield line
            CLI->>LV: format_line(line)
            LV-->>CLI: formatted string
            CLI-->>User: typer.echo output
        end
        User->>CLI: KeyboardInterrupt
        CLI-->>User: exit cleanly
    else read mode default
        CLI->>LV: read_log(path, count)
        LV->>FS: iterate lines into deque with maxlen count
        FS-->>LV: last N lines
        LV-->>CLI: list of strings
        loop Each line
            CLI->>LV: format_line(line)
            LV-->>CLI: formatted string
            CLI-->>User: typer.echo output
        end
    end
Loading

Last reviewed commit: 2dffd0a

Comment on lines +85 to +86
if count is not None:
lines = lines[-count:]
Copy link
Contributor

Choose a reason for hiding this comment

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

Zero count returns all lines instead of none

When count=0 is passed (e.g., dimos log -n 0), the expression lines[-0:] evaluates to lines[0:] because -0 == 0 in Python, returning all lines instead of zero lines. This is silently incorrect — a user passing -n 0 will get the entire log dumped to their terminal.

Suggested change
if count is not None:
lines = lines[-count:]
if count is not None and count > 0:
lines = lines[-count:]
elif count == 0:
lines = []

@spomichter
Copy link
Contributor Author

@greptile

Use SIGINT handler + stop callback instead of KeyboardInterrupt.
The follow_log generator checks the stop flag every 0.1s and exits
cleanly, ensuring the file handle is closed and the terminal is
restored.
Comment on lines +91 to +100
def follow_log(path: Path) -> Iterator[str]:
"""Yield new lines as they appear (``tail -f`` style)."""
with open(path) as f:
f.seek(0, 2)
while True:
line = f.readline()
if line:
yield line
else:
time.sleep(0.1)
Copy link
Contributor

Choose a reason for hiding this comment

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

follow_log unconditionally seeks to EOF before yielding lines, meaning dimos log -f produces no output for existing log content — only lines written after the command starts. This contradicts standard tail -f behaviour and is likely to confuse users debugging recent events.

A user expecting to see recent log output when running dimos log -f will get nothing until the next write occurs.

Suggested fix: before the loop, emit the last N lines (e.g. 10, or make it configurable), then follow new output:

def follow_log(path: Path, tail_lines: int = 10) -> Iterator[str]:
    """Yield new lines as they appear (``tail -f`` style)."""
    with open(path) as f:
        # Emit the last `tail_lines` lines before following new output.
        existing = list(deque(f, maxlen=tail_lines))
        yield from existing
        while True:
            line = f.readline()
            if line:
                yield line
            else:
                time.sleep(0.1)

Comment on lines +275 to +280
if follow:
try:
for line in follow_log(path):
typer.echo(format_line(line, json_output=json_output))
except KeyboardInterrupt:
pass
Copy link
Contributor

Choose a reason for hiding this comment

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

When --follow / -f is active, the --lines / -n parameter is accepted by the CLI but never forwarded to follow_log. A user running dimos log -f -n 100 will expect the last 100 lines to be replayed before live-tailing begins (matching the standard tail -n 100 -f contract), but currently gets no historical output at all.

This should be coordinated with updating follow_log to accept a tail_lines parameter (see note on log_viewer.py). Once that's done, pass it here:

if follow:
    try:
        tail_lines = 0 if all_lines else lines
        for line in follow_log(path, tail_lines=tail_lines):
            typer.echo(format_line(line, json_output=json_output))
    except KeyboardInterrupt:
        pass

@spomichter spomichter merged commit de9e4ba into dev Mar 10, 2026
@spomichter spomichter deleted the feat/dim-698-dimos-log branch March 10, 2026 16:05
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