feat(cli): dimos log command for viewing per-run logs (DIM-698)#1497
feat(cli): dimos log command for viewing per-run logs (DIM-698)#1497spomichter merged 6 commits intodevfrom
Conversation
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 SummaryThis PR adds a Key findings:
Both issues affect the follow mode specifically and can be fixed with coordinated changes to add a Confidence Score: 3/5
Sequence DiagramsequenceDiagram
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
Last reviewed commit: 2dffd0a |
dimos/core/log_viewer.py
Outdated
| if count is not None: | ||
| lines = lines[-count:] |
There was a problem hiding this comment.
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.
| if count is not None: | |
| lines = lines[-count:] | |
| if count is not None and count > 0: | |
| lines = lines[-count:] | |
| elif count == 0: | |
| lines = [] |
|
@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.
dimos/core/log_viewer.py
Outdated
| 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) |
There was a problem hiding this comment.
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)
dimos/robot/cli/dimos.py
Outdated
| if follow: | ||
| try: | ||
| for line in follow_log(path): | ||
| typer.echo(format_line(line, json_output=json_output)) | ||
| except KeyboardInterrupt: | ||
| pass |
There was a problem hiding this comment.
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
Summary
Adds
dimos logcommand to view per-run logs without manually finding the log path.Closes DIM-698
Usage
Output
Human-readable (default):
Architecture
dimos/core/log_viewer.py: Log resolution, JSONL parsing, formatting, follow modedimos/robot/cli/dimos.py: Thin 15-line CLI wrapperLog 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: Addlogcommand (+30 lines)Tested
dimos log— full output, human-readable ✅dimos log -n 3— last 3 lines ✅dimos log --json -n 2— raw JSONL ✅dimos logwith no runs — "No log files found" ✅Contributor License Agreement