avenor watch
Read and digest event logs produced by --on-event. The raw log is newline-delimited JSON (NDJSON); watch parses it and outputs human-readable digest lines, optional classification tags, or pass-through JSON.
watch is the tool to inspect what happened in a completed run, or to tail a live run from a separate process. An agent or operator uses it to detect milestones, read findings, and react to structural events without parsing raw JSON.
Basic usage
Read a completed event log and print digest lines to stdout:
avenor watch /tmp/run.ndjsonOutput looks like:
EVENT agent.status ses_123 thinking
EVENT agent.thought_chunk ses_123 confidence: 85%, need to read the code
EVENT avenor.phase.start ses_123 phase: code_review (iter 1)
EVENT tool.call ses_123 read:/repo/main.go [pending]
EVENT tool.call_update ses_123 read [completed]
EVENT agent.message_chunk ses_123 the bug is in the loop condition
EVENT avenor.phase.end ses_123 phase: code_review → end_turn
EVENT session.end ses_123 stop_reason=end_turnEach digest line is: EVENT <event-name> <session-id> <excerpt>
Follow mode
Tail a live event log as it grows:
avenor watch --follow /tmp/run.ndjsonwatch polls the file every 250ms (configurable with --poll-interval) and outputs new lines as they arrive. It terminates in two ways:
- Gracefully: When the underlying session ends,
watchreads thesession.endevent and exits with status 0. - Signal: Press Ctrl+C (SIGINT) or send SIGTERM.
watchflushes output and exits cleanly.
If the log is rotated or truncated while tailing, watch will read whatever remains and exit cleanly on the next poll cycle when the file does not reopen.
Output formats
Plain format (default)
--format plain outputs digest lines. Each line contains:
EVENTprefix- Event name (
agent.status,tool.call, etc.) - Session ID
- Excerpt (context-specific summary of the event)
Excerpts are trimmed to 120 characters and have line breaks replaced with spaces:
EVENT agent.message_chunk ses_123 I found three issues with the review logic
EVENT avenor.phase.end ses_123 phase: analyze → end_turnJSON format
--format json passes through the original NDJSON with no digestion:
avenor watch --format json /tmp/run.ndjsonOutput is NDJSON (one JSON object per line), identical to the input:
{"event":"agent.thought_chunk","session_id":"ses_123","content":{"text":"confidence: 92%"},"ts":1234567890}
{"event":"tool.call","session_id":"ses_123","toolCallId":"call_1","kind":"read","title":"/repo/main.go"}Classification
The --classify flag adds a three-level classification tag to each line:
- MILESTONE — Structural events: loop start/end, permission gates, session end, retries, phase end, errors, agent status transitions to
waitingordone. - FINDING — Content signals: message or thought text with confidence ≥60%, explicit
[finding]marker, phrases like "reviewer flagged", "correction needed", "failed test". - ACTIVITY — Everything else: routine tool calls, message chunks, phase transitions to
thinkingorworking.
Plain format with classification
avenor watch --classify /tmp/run.ndjsonOutput:
MILESTONE EVENT agent.status ses_123 waiting
FINDING EVENT agent.thought_chunk ses_123 confidence: 75% that the fix is correct
ACTIVITY EVENT tool.call ses_123 read:/repo/file.go [pending]
MILESTONE EVENT session.end ses_123 stop_reason=end_turnJSON format with classification
avenor watch --classify --format json /tmp/run.ndjsonEach JSON object gets an injected top-level classify field:
{"event":"agent.thought_chunk","session_id":"ses_123","content":{"text":"I found a bug"},"classify":"FINDING","ts":1234567890}
{"event":"session.end","session_id":"ses_123","stop_reason":"end_turn","classify":"MILESTONE","ts":1234567890}Use classification to wake up a consumer that only cares about milestones or findings. For example, a supervisor agent might tail a run and only log lines where classify is FINDING or MILESTONE.
Cursor-based reading
For incremental polling (reading new events since last check), use the cursor feature:
avenor watch --since-cursor /tmp/watch.cursor /tmp/run.ndjsonOn first run: The cursor file does not exist. watch reads from byte offset 0 (beginning of the log).
On subsequent runs: watch reads the saved offset from the cursor file, seeks to that position in the log, and processes only new lines.
On exit: watch writes the current byte offset to the cursor file atomically (write to .tmp, then rename). If the log file was rotated (cursor offset exceeds file size), watch will fail at startup with an error message.
The cursor file is plain text, containing a single decimal byte offset and a newline:
12345Rotation handling
If the log is rotated while watch is running with a cursor, the next invocation will detect that the cursor offset is beyond the rotated log's size and exit with an error:
avenor watch: log /tmp/run.ndjson shorter than cursor (offset 50000 > size 5000) (cursor /tmp/watch.cursor)In that case, delete the cursor file and restart — watch will read the new log from the beginning.
Pattern: polling a live run
Use the cursor to implement incremental polling from a separate script:
#!/bin/bash
LOG=/tmp/run.ndjson
CURSOR=/tmp/watch.cursor
while true; do
avenor watch --classify --since-cursor "$CURSOR" "$LOG" | while read line; do
if [[ "$line" =~ MILESTONE|FINDING ]]; then
notify-operator "$line"
fi
done
sleep 5
doneEach call to avenor watch reads only the delta since the last call, processes it, and updates the cursor. The script can react to milestones and findings without re-reading the entire log.
Flags reference
All flags (with defaults):
| Flag | Default | Type | Meaning |
|---|---|---|---|
--follow | false | bool | Poll and tail the log (exit on session.end or SIGINT) |
--poll-interval | 250ms | duration | Sleep between polls in follow mode |
--format | plain | string | Output format: plain or json |
--classify | false | bool | Prefix each line with MILESTONE/FINDING/ACTIVITY tag |
--since-cursor | (empty) | string | Cursor file path: seek and save offset |
Practical patterns
Tail a live run with milestones only
avenor watch --classify --follow /tmp/run.ndjson | grep MILESTONEExits when the session ends.
Read a completed run and export findings
avenor watch --classify --format json /tmp/run.ndjson | \
jq 'select(.classify == "FINDING")'Output each event classified as FINDING as a JSON object.
Incremental polling with classification
avenor watch --classify --since-cursor /tmp/watcher.cursor /tmp/run.ndjsonOn each invocation, processes only new events since the last run, and updates the cursor file.
Relationship to tracked state
avenor watch is useful for operators and shell scripts, but orchestrating code usually wants a stable in-memory view instead of individual lines. Inside the Avenor codebase, use internal/events.SessionTracker to reduce the raw --on-event stream into a single SessionState snapshot.
That gives controllers one canonical interpretation of channel lifecycle fields such as channel_ready, prompt queueing/submission, permission waits, report state, and terminal finish/session-end state.
Relationship to raw events
See events.md for the full specification of event types, fields, and semantics. watch digests the raw NDJSON by:
- Parsing each line as JSON.
- Extracting the
eventfield and domain-specific fields (liketitle,kind,phase). - Formatting a human-readable excerpt (120 chars max, whitespace normalized).
- Optionally prepending a classification tag.
The digest is lossy — it discards most fields — but captures the narrative of the run. For detailed inspection of a particular event, use --format json and pipe to jq.