agnes-the-ai-analyst/cli/commands/status.py
ZdenekSrotyr 8784f10a6b fix(devin-review): stale-token override + status sessions counter + lock comment
Three Devin Review findings on PR #173 addressed in one commit since
they're in adjacent code paths:

1. cli/commands/init.py:99 (\u{1F534}): `agnes init --token NEW` ran
   step 2 verify against the OLD on-disk token because `get_token()`
   read `~/.config/agnes/token.json` before the env var, and
   `_override_server_env` only set the env var. So `agnes init --force`
   on a machine with a stale token.json failed 401 with a confusing
   'token expired' even though the --token arg was valid.

   Fix: ContextVar-based override in `cli.config._token_override`
   checked by `get_token()` BEFORE the on-disk read.
   `_with_token_override` context manager scopes the override.
   `_override_server_env` now also sets the contextvar via
   `_with_token_override(token)`, so both env var and contextvar
   carry the override (env for back-compat with anything bypassing
   get_token; contextvar is the authoritative source).
   Async-safe (each task sees its own override) and leak-proof
   (resets on context exit).
   2 new tests: regression on stale-disk-token + scope leak guard.

2. cli/commands/status.py:43 (\u{1F7E1}): sessions_pending_upload only
   checked legacy `<workspace>/user/sessions/` and always reported 0
   in workspaces bootstrapped with `agnes init` (Claude Code writes
   to `~/.claude/projects/`, not the legacy path). Same bug we fixed
   for `agnes push` in 08e49591.

   Fix: route through `cli.lib.claude_sessions.list_session_files()`
   so status and push agree on what counts as a pending session.

3. connectors/bigquery/extractor.py:111 (\u{1F7E1}): docstring claimed
   "a live holder still wins the second flock attempt" — incorrect on
   Linux. After `unlink()` + `open()`, the new file is a new inode;
   fcntl.flock keys per-inode, so the old holder's lock does NOT block
   the new acquisition. In a genuine TTL-overrun scenario two writers
   CAN race the parquet.tmp.

   Fix: documentation only. Comment now honestly describes the
   inode-recreation behavior, names the threading.Lock as the actual
   in-process guard, and flags pid-gating as the next-iteration fix
   if real corruption surfaces. The 24h default TTL is well above
   typical COPY durations so the practical risk is low.

Tests: 17/17 across test_cli_init.py + test_lib_pull.py + the broader
regression set.
2026-05-04 21:26:30 +02:00

70 lines
2.3 KiB
Python

"""`agnes status` — workspace status: initialized? data fresh? hooks active?
Server-health checks live under `agnes diagnose system` (see the
`agnes diagnose` group).
"""
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
from pathlib import Path
import typer
_INIT_MARKER = "AI Data Analyst"
status_app = typer.Typer(help="Show workspace status (initialized? data fresh? hooks active?)")
@status_app.callback(invoke_without_command=True)
def status(
as_json: bool = typer.Option(False, "--json", help="Machine-readable output"),
):
workspace = Path(os.environ.get("AGNES_LOCAL_DIR", ".")).resolve()
initialized = False
claude_md = workspace / "CLAUDE.md"
if claude_md.exists():
initialized = _INIT_MARKER in claude_md.read_text(encoding="utf-8")
parquet_dir = workspace / "server" / "parquet"
parquets = list(parquet_dir.glob("*.parquet")) if parquet_dir.exists() else []
db_path = workspace / "user" / "duckdb" / "analytics.duckdb"
last_synced = None
if db_path.exists():
last_synced = datetime.fromtimestamp(db_path.stat().st_mtime, tz=timezone.utc).isoformat()
# Sessions live in ~/.claude/projects/<encoded-cwd>/ (where Claude Code
# writes them), with `<workspace>/user/sessions/` as a legacy fallback.
# The helper unions both — same source of truth as `agnes push`.
from cli.lib.claude_sessions import list_session_files
session_count = len(list_session_files(workspace))
info = {
"workspace": str(workspace),
"initialized": initialized,
"parquet_tables": len(parquets),
"duckdb_exists": db_path.exists(),
"last_synced": last_synced,
"sessions_pending_upload": session_count,
}
if as_json:
typer.echo(json.dumps(info, indent=2))
return
typer.echo(f"Workspace : {workspace}")
typer.echo(f"Initialized: {'yes' if initialized else 'no'}")
typer.echo(f"Parquets : {info['parquet_tables']}")
typer.echo(f"DuckDB : {'yes' if info['duckdb_exists'] else 'no'}")
typer.echo(f"Last sync : {last_synced or 'never'}")
typer.echo(f"Pending uploads: {session_count} sessions")
if not initialized:
typer.echo("")
typer.echo("Run `agnes init --server-url <URL> --token <PAT>` to bootstrap.")