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
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `GOCLAW_PROFILE` — per-command profile selection precedence between `--profile` and active config.
- `goclaw sessions compact <key>` — invokes WS RPC `sessions.compact` behind destructive confirmation.
- `goclaw health` — uses WS RPC `health` when authenticated, retaining unauthenticated HTTP `/health` fallback.
- `goclaw traces list --since --agent --status --root-only --limit` — expanded filters for automation-friendly trace search.
- `goclaw traces list --agent --user --session-key --status --channel --limit --offset` — server-aligned filters for paginated trace listing.

**P4 — UX polish**
- `goclaw codex-pool activity --agent=<id>|--provider=<id>` — unified Codex pool activity lookup; legacy agent/provider commands remain as deprecated aliases.
Expand All @@ -48,6 +48,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

**P6 — Backend-unblocked surfaces (gateway `v3.12.0-beta.20`+)**
- `goclaw traces follow --session-key|--agent [--since RFC3339] [--limit N]` — one-shot incremental trace polling (`GET /v1/traces/follow`). Re-invoke with returned cursor to advance; no WS stream, no watch loop.
- `goclaw traces timeline <run-id> [--session-key K] [--limit N] [--offset N]` — read archived run timeline items (`GET /v1/runs/{runID}/timeline`) without replaying or mutating a run.
- `goclaw providers reconnect <provider-id>` — hot-reconnect a provider, bumping the registry without touching credentials (`POST /v1/providers/{id}/reconnect`).
- `goclaw sessions branch <session-key> --up-to-index N [--new-session-key K] [--label L] [--metadata k=v ...]` — branch a chat session at a 1-based message index into a new session (`POST /v1/chat/sessions/{key}/branch`). `--up-to-index=0` is preserved on the wire.
- `goclaw sessions follow <session-key> [--cursor N] [--limit N]` — one-shot cursor-based history poll (`GET /v1/chat/sessions/{key}/history/follow`). Not a stream; `--cursor=0` is preserved literally in the query string.
Expand All @@ -57,7 +58,10 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Fixed

- `goclaw traces get <id>` — TTY mode now renders a human-readable summary (header card + span tree + events list) instead of dumping raw JSON. JSON-mode payload unchanged. Decode failures surface as wrapped errors instead of an empty `{}`. Trace ids are validated against `^[A-Za-z0-9._-]+$` and reserved tokens (`.`, `..`) are rejected before any HTTP call. Distinct exit codes per failure: not-found → 3, permission-denied → 2, malformed-id → 4, server-failure → 5. Latent retry-body bug in `internal/client/http.go` fixed: the final 5xx/429 response body is now preserved so the typed `APIError` reaches the caller (previously collapsed to exit 1). Closes #17.
- `goclaw traces list` now decodes the current server payload `{traces,total,limit,offset}`. JSON/YAML mode preserves that envelope; table mode renders rows from `traces` using `id`, `total_input_tokens`, `total_output_tokens`, and `total_cost`.
- `goclaw traces get <id>` — TTY mode now renders a human-readable summary (header card + span tree) instead of dumping raw JSON. JSON-mode payload unchanged. Decode failures surface as wrapped errors instead of an empty `{}`. Trace ids are validated against `^[A-Za-z0-9._-]+$` and reserved tokens (`.`, `..`) are rejected before any HTTP call. Distinct exit codes per failure: not-found → 3, permission-denied → 2, malformed-id → 4, server-failure → 5. Latent retry-body bug in `internal/client/http.go` fixed: the final 5xx/429 response body is now preserved so the typed `APIError` reaches the caller (previously collapsed to exit 1). Closes #17.
- `goclaw traces get <id>` now handles the current server detail payload `{trace,spans}` while preserving the server envelope in JSON/YAML mode.
- `goclaw traces export <id>` now validates trace IDs and path-escapes the export route before making the HTTP request.

### Notes
- All new commands honor the AI-first ergonomics contract: `--output=json` envelope, central error handler, `--yes` for destructive ops, `--quiet` for CI.
Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,21 @@ echo "Analyze this log" | goclaw chat myagent

### Backend-Unblocked Surfaces (P6)

Seven one-shot subcommands wired to backend PRs `#37` and `#44`:
Backend-unblocked one-shot subcommands wired to backend PRs `#37` and `#44`
plus the run timeline archive endpoint:

```bash
# Paginated trace listing with server-supported filters
goclaw traces list [--agent <id>] [--user <id>] [--session-key <key>] \
[--status <status>] [--channel <name>] [--limit <n>] [--offset <n>]

# Incremental trace polling (one shot; rerun with returned cursor)
goclaw traces follow --session-key <key> [--since <RFC3339>] [--limit <n>]
goclaw traces follow --agent <id> [--since <RFC3339>] [--limit <n>]

# Archived run timeline (read-only)
goclaw traces timeline <run-id> [--session-key <key>] [--limit <n>] [--offset <n>]

# Provider hot-reconnect (bumps registry without recreating credentials)
goclaw providers reconnect <provider-id>

Expand Down Expand Up @@ -127,11 +135,14 @@ All are one-shot HTTP — no watch loops or WS streams. `logs aggregate` is admi
### Reading a Trace by ID

```bash
# Human-readable: header + span tree + events
# Human-readable: header + span tree
goclaw traces get <trace-id>

# Machine-readable JSON (also auto-selected when stdout is piped)
goclaw traces get <trace-id> -o json

# Export gzipped trace tree
goclaw traces export <trace-id> --output trace.json.gz
```

Exit codes for `traces get`: `0` on success, `2` on permission denied, `3` on not-found, `4` on malformed id (rejected before any HTTP call — allowlist `^[A-Za-z0-9._-]+$`), `5` on upstream server failure, `6` on rate-limit / network-resource exhaustion.
Expand Down Expand Up @@ -475,7 +486,7 @@ One-shot profile override:

```bash
goclaw --profile staging agents list
GOCLAW_PROFILE=staging goclaw traces list --since=1h --root-only -o json
GOCLAW_PROFILE=staging goclaw traces list --session-key=session-1 --channel=telegram -o json
```

## Claude Code Skill
Expand Down
25 changes: 18 additions & 7 deletions cmd/p3_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,26 +86,37 @@ func TestTracesListAddsP3Filters(t *testing.T) {
return
}
q := r.URL.Query()
if q.Get("agent_id") != "agent-1" || q.Get("status") != "error" ||
q.Get("since") != "1h" || q.Get("root_only") != "true" || q.Get("limit") != "5" {
if q.Get("agent_id") != "agent-1" || q.Get("user_id") != "user-1" ||
q.Get("session_key") != "session-1" || q.Get("status") != "error" ||
q.Get("channel") != "telegram" || q.Get("limit") != "5" ||
q.Get("offset") != "10" {
t.Fatalf("unexpected query: %s", r.URL.RawQuery)
}
okJSON(t, w, []map[string]any{{"trace_id": "trace-1"}})
rawJSON(t, w, map[string]any{
"traces": []map[string]any{{"id": "trace-1"}},
"total": 1,
"limit": 5,
"offset": 10,
})
}))
defer srv.Close()
setupP3CommandTest(srv.URL)

_ = tracesListCmd.Flags().Set("agent", "agent-1")
_ = tracesListCmd.Flags().Set("user", "user-1")
_ = tracesListCmd.Flags().Set("session-key", "session-1")
_ = tracesListCmd.Flags().Set("status", "error")
_ = tracesListCmd.Flags().Set("since", "1h")
_ = tracesListCmd.Flags().Set("root-only", "true")
_ = tracesListCmd.Flags().Set("channel", "telegram")
_ = tracesListCmd.Flags().Set("limit", "5")
_ = tracesListCmd.Flags().Set("offset", "10")
t.Cleanup(func() {
_ = tracesListCmd.Flags().Set("agent", "")
_ = tracesListCmd.Flags().Set("user", "")
_ = tracesListCmd.Flags().Set("session-key", "")
_ = tracesListCmd.Flags().Set("status", "")
_ = tracesListCmd.Flags().Set("since", "")
_ = tracesListCmd.Flags().Set("root-only", "false")
_ = tracesListCmd.Flags().Set("channel", "")
_ = tracesListCmd.Flags().Set("limit", "20")
_ = tracesListCmd.Flags().Set("offset", "0")
})

if err := tracesListCmd.RunE(tracesListCmd, nil); err != nil {
Expand Down
69 changes: 36 additions & 33 deletions cmd/testdata/trace_detail_get.json
Original file line number Diff line number Diff line change
@@ -1,54 +1,57 @@
{
"_TODO_refresh": "stub fixture derived from traces follow payload shape; refresh against goclaw.zuey.me before merge per phase-03 reviewer gate",
"trace_id": "trace_FIXTURE_001",
"agent_id": "agent_FIXTURE_001",
"session_key": "session_FIXTURE_001",
"user_id": "user_REDACTED",
"tenant_id": "tenant_REDACTED",
"status": "success",
"started_at": "2026-05-28T10:00:00Z",
"ended_at": "2026-05-28T10:00:02Z",
"duration_ms": 2000,
"input_tokens": 120,
"output_tokens": 80,
"cost": "0.0042",
"trace": {
"id": "trace_FIXTURE_001",
"agent_id": "agent_FIXTURE_001",
"session_key": "session_FIXTURE_001",
"run_id": "run_FIXTURE_001",
"user_id": "user_REDACTED",
"status": "completed",
"start_time": "2026-05-28T10:00:00Z",
"end_time": "2026-05-28T10:00:02Z",
"duration_ms": 2000,
"total_input_tokens": 120,
"total_output_tokens": 80,
"total_cost": 0.0042,
"span_count": 3,
"llm_call_count": 1,
"tool_call_count": 1
},
"spans": [
{
"span_id": "span_001",
"id": "span_001",
"trace_id": "trace_FIXTURE_001",
"parent_span_id": null,
"span_type": "agent",
"name": "agent.run",
"kind": "agent",
"started_at": "2026-05-28T10:00:00Z",
"ended_at": "2026-05-28T10:00:02Z",
"start_time": "2026-05-28T10:00:00Z",
"end_time": "2026-05-28T10:00:02Z",
"duration_ms": 2000,
"status": "success"
"status": "completed"
},
{
"span_id": "span_002",
"id": "span_002",
"trace_id": "trace_FIXTURE_001",
"parent_span_id": "span_001",
"span_type": "llm",
"name": "llm.call",
"kind": "llm",
"started_at": "2026-05-28T10:00:00Z",
"ended_at": "2026-05-28T10:00:01Z",
"start_time": "2026-05-28T10:00:00Z",
"end_time": "2026-05-28T10:00:01Z",
"duration_ms": 1500,
"status": "success",
"status": "completed",
"input_tokens": 120,
"output_tokens": 80
},
{
"span_id": "span_003",
"id": "span_003",
"trace_id": "trace_FIXTURE_001",
"parent_span_id": "span_001",
"span_type": "tool",
"name": "tool.call",
"kind": "tool",
"started_at": "2026-05-28T10:00:01Z",
"ended_at": "2026-05-28T10:00:02Z",
"tool_name": "web_fetch",
"start_time": "2026-05-28T10:00:01Z",
"end_time": "2026-05-28T10:00:02Z",
"duration_ms": 400,
"status": "success"
"status": "completed"
}
],
"events": [
{"event_id": "ev_001", "span_id": "span_002", "type": "llm.prompt", "timestamp": "2026-05-28T10:00:00Z"},
{"event_id": "ev_002", "span_id": "span_002", "type": "llm.completion", "timestamp": "2026-05-28T10:00:01Z"},
{"event_id": "ev_003", "span_id": "span_003", "type": "tool.invoke", "timestamp": "2026-05-28T10:00:01Z"}
]
}
Loading
Loading