Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ This project follows a Keep a Changelog-style format and uses version tags for r

## [Unreleased]

## [1.6.5] - 2026-08-09

### Added
- **`--json --samples N` — bounded NDJSON emission for agent/script one-shots.**
`--json` previously streamed until interrupted, forcing an agent or script to
kill the process after reading its record. `--samples N` (with `--json`) now
emits exactly `N` snapshot records then exits 0; `--samples 0` (default)
keeps the streaming behavior. The first record already carries real deltas
(the `Monitor` primes the baseline at construction), so `--json --samples 1`
is a clean single-snapshot tool call.

## [1.6.4] - 2026-08-09

### Fixed
Expand Down Expand Up @@ -492,7 +503,7 @@ Reading-plane audit remediation (`docs/TODO-reading-plane-audit-2026-07-29.md`
(`capture-tui-diagram`, `record-tui-gif`).

### Chore
- Add the `record-tui-gif` Claude Code skill (`.claude/skills/record-tui-gif/`):
- Add the `record-tui-gif` Claude Code skill (`.agents/skills/record-tui-gif/`):
reproducible hero-GIF recording via `vhs` driven by a live GPU workload, so the
capture can be refreshed after any TUI/layout change.

Expand Down Expand Up @@ -726,7 +737,7 @@ Reading-plane audit remediation (`docs/TODO-reading-plane-audit-2026-07-29.md`
New must-have items (fan RPM via SMC, net/disk I/O via native ctypes) and a
deferred low-priority item (menu bar mode, explicitly after first
market-promo push per `docs/RUNBOOK-launch-and-growth.md`).
- `.claude/skills/run-actop`: documents driving the TUI via tmux send-keys/
- `.agents/skills/run-actop`: documents driving the TUI via tmux send-keys/
capture-pane for manual verification (Homebrew binary and local `.venv` dev
build), including the sampler-init ready marker and how to confirm live
updates vs. a static frame.
Expand Down
19 changes: 19 additions & 0 deletions actop/actop.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ def build_parser():
metavar="PORT",
help="Serve Prometheus metrics on http://0.0.0.0:PORT/metrics (no TUI)",
)
parser.add_argument(
"--samples",
type=_validate_samples,
default=0,
metavar="N",
help="With --json: emit N snapshot records then exit (0 = stream "
"indefinitely until interrupted)",
)
return parser


Expand Down Expand Up @@ -186,6 +194,16 @@ def _validate_sustain_samples(value):
return samples


def _validate_samples(value):
try:
samples = int(value)
except (TypeError, ValueError) as error:
raise argparse.ArgumentTypeError("samples must be an integer") from error
if samples < 0:
raise argparse.ArgumentTypeError("samples must be >= 0")
return samples


def _validate_port(value):
try:
port = int(value)
Expand Down Expand Up @@ -236,6 +254,7 @@ def _run_export(args):
export.run_json_stream(
interval_s,
subsamples,
max_samples=max(0, int(getattr(args, "samples", 0) or 0)),
include_processes=include_processes,
proc_filter=proc_filter,
)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "actop"
version = "1.6.4"
version = "1.6.5"
description = "Apple Silicon (M1–M4) power, GPU, ANE & memory-bandwidth monitor — sudoless TUI + Python API for profiling local LLM / MLX / CoreML inference"
readme = "README.md"
license = { text = "MIT" }
Expand Down
32 changes: 32 additions & 0 deletions tests/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,38 @@ def test_json_stream_proc_filter_implies_show_processes():
)


@pytest.mark.local
def test_json_samples_limits_records_and_exits_zero():
# Agent-facing contract: `--json --samples N` must emit exactly N NDJSON
# records and exit 0 on its own — an agent's one-shot tool call must not
# hang (infinite stream) nor report failure (130) on natural completion.
# Without the flag wired through _run_export the process streams forever
# and this test's timeout trips; with max_samples>0 run_json_stream breaks
# the loop and _run_export returns 0.
process = subprocess.run(
[
sys.executable,
"-m",
"actop.actop",
"--json",
"--samples",
"2",
"--interval",
"1",
],
capture_output=True,
text=True,
timeout=20,
)

lines = [ln for ln in process.stdout.splitlines() if ln.strip()]
assert process.returncode == 0, process.stderr
assert len(lines) == 2, f"expected exactly 2 records, got {len(lines)}"
record = json.loads(lines[0])
assert "cpu_watts" in record
assert "p_cores" in record


@pytest.mark.local
def test_serve_prometheus_includes_process_gauges_with_show_processes():
import urllib.request
Expand Down