pa-types/pa-cli/pa-tui: daemon incident forensics — prime-agent incident CLI + agents-view incident notices (TS #2406 port) - #2866
Conversation
…ncident + agents-view incident notices (TS #2406 port) Port TS PR #2406 ([RSI, feature] Daemon incident forensics) to Rust: - pa-types::incident (new): the shared classifier of TS src/cli/incident.ts — agent.jsonl + per-daemon log-line parsing, worker pid attribution, event classification (the TS regexes kept 1:1 via the regex crate), the stall/burst/gap anomaly computation, and the severity/category vocabulary. Shared by the CLI and the agents-view notice, like the TS module (pa-tui depends on pa-types alone). - pa-cli::incident (new): the CLI half — --since/--until time-bound parsing (UTC, per the daemon log), the timeline report (aggregation, sections, chalk-honoring severity colors), log-source discovery (agent.jsonl plus its .old rotation, falling back to the newest per-daemon log), the options/window parsing, and the run entry. Routed as the "incident" command in public-command with a one-shot window resolution; the command spec joins the registry. - pa-tui::incident_notices (new): port of src/modes/agents-view/incident-notices.ts — reuse of the classifier (never re-implemented), the 30s poll with rotation-safe bounded incremental reads (the .old bridge, the same-generation guard, the mid-rotation offset continuation), the collapse to one dismissible warning line, and the dismissal horizons. - pa-tui agents_view: the notice renders under the splash (wrap, one-column gutter, warning color, pointer to the CLI); Esc (tui.select.cancel) dismisses it when the search is empty and no delete confirm is armed (the confirm wins, as in TS); the 30s poll re-derives and renders on change; the state rides the options/outcome so the pa-cli agents-view flow carries it across re-entry (TS persistentState.incidentNoticeState). - tests: the TS incident.test.ts suites split by crate ownership (parse/ classify/anomaly units in pa-types; report/window/session-filter, the crash+recovery fixture, log-source discovery, and dispatch in pa-cli), the agents-view-incident-notice.test.ts derivation/rotation units in pa-tui, and a headless e2e over a mock supervisor covering render, Esc dismissal, dismissal persistence across re-entry, and the armed-delete-confirm case. - pa-types gains the regex dependency (already in the tree via pa-tui) for the TS patterns; pa-types/pa-cli README scope entries updated. Rust-only: no TS edits. No host cargo (fmt --check only).
|
Macroscope skipped reviewing this pull request. Per-review cost limit exceeded (workspace setting). This review would cost an estimated $13.75, which exceeds your per-review limit of $10.00. The top 3 files driving up this estimate:
Tip To get this pull request reviewed, you can:
|
Prime Agent performance — partialPR Benchmark execution did not complete successfully. Missing measurements are not performance wins. Failure diagnostics:
See the saved per-trial logs and terminal transcripts for details. Overall: 0 regressed · 0 improved · 0 no clear change · 42 unavailable.
Python runtime
Session transport
UI interactions
Sandbox cost: ~$0.0498 — no inference calls. Methodology and samplesMain resolved at 2026-09-26T19:31:20.310727+00:00. Harness
Failures:
|
The first CI pass (no host cargo on this lane — the compiler is CI) found four pa-types errors and the audit behind them found more downstream: - pa-types parse: the take_digits `?` yields u32 into an i64 annotation. - pa-types anomaly tests: contains() takes a reference to the element, not the element's reference. - pa-types classify tests: no moving a tuple out of a shared reference (build the fields Vec instead). - pa-cli time: the same u32/i64 year typing on the time-bound parser (found in the audit, ahead of the second CI pass). - pa-cli source tests: an array mixing &str literals with a &String from a helper call; the future-mtime decoy-dir test is unix-gated (Windows cannot open a directory with File::open) per the #2833 pattern. - The epoch fixtures were miscalculated by 5h20m (2026-09-10T20:00Z is 1_789_070_400_000, 2026-09-16T22:30Z is 1_789_597_800_000): every constant now verified against an authoritative datetime; the is_daemon_log_file_name expectation that contradicted the TS hash-suffix pattern is corrected. - report_for rode Default::default() (an empty window) instead of the default 20:00-20:30 window. - clippy (-D warnings) hygiene: map().unwrap_or*/map().unwrap_or_else chains -> map_or/map_or_else (map_unwrap_or), the two-arm match with a wildcard in the rotation read -> a let-else filter (single_match_else), a mid-function use item -> std::fs::write (items_after_statements), and an unused Arc import in the e2e. - The e2e mock serves one connection per view run (the dismissal test runs the view twice; a selection-less exit closes the link).
pa-types and pa-cli compile clean at the round-2 head; pa-tui had two root errors behind twelve diagnostics: - format_incident_notice_time addressed the date parts as named fields on the notice_parts tuple — destructure them instead. - AgentsViewMode::new takes the state out of options (the TS ??= lazy init) — the parameter is mut now.
The workspace compiles at the round-3 head; the remaining failures
were seven test bugs and one runtime root cause:
- IncidentSeverity's Display impl used write_str, which ignores the
report's {:8} severity padding — f.pad honors it (the layout test's
missing four spaces).
- The startup-blocked lock check is case-insensitive in TS
(/lock file is already being held/i): real log lines carry
'Lock file is already being held', so the lowercase compare restores
the arm (two source-test failures).
- The date-only timestamp fixture is UTC midnight, not 20:00.
- The named-pipe crash fixture rode the supervisor component without a
socket path; the TS test uses the daemon component (daemonLine).
- TS's duration formatter keeps only hours+minutes once hours overflow
(1h1m, never 1h1m40s).
- A notice expiring out of its window IS a changed line (Some -> None),
so the poll reports it; the test asserted the opposite.
- The newest-per-daemon fallback pick now pins the fixture's mtimes
(the write-order timing raced the runner's filesystem granularity).
- The e2e respond_failure call carries its 4th argument; the unused
crash-line helper in the notice tests is gone (dead-code warning).
- The fractional-seconds scaling in timestamp_to_ms scaled the wrong way (.7 parsed as 7ms, not 700ms): keep the first three digits and pad to the right, like Date.parse and the TS padEnd(3, "0"). - needless_borrow: error_message(first_line(err)) is already a &str. - manual_repeat_n: repeat().take() is repeat_n in the recovery-breakdown fixtures (pa-types + pa-cli).
unwrap_or_else(IncidentNoticeState::new) is unwrap_or_default() (the state implements Default).
- The env-serialization lock is a tokio mutex now: each test holds the guard across its view-run awaits (a std guard across an await is a clippy error, and the tokio lock is the right tool for an async test). - The mock command loop is a while-let over read_line (while_let_loop). - The module doc backticks PRIME_AGENT_CODING_AGENT_DIR (doc_markdown).
…size+mtime An append changes both size and mtime, so the Windows identity flipped every poll - re-tailing consumed bytes into phantom update restarts (Cursor Bugbot: Windows log identity unstable on append). Per the platform identity precedent (pa_types::platform::identity's None), the non-Unix arm now has no identity: appends stay same-generation and a rotation still re-tails through the offset-past-size check (the daemon recreates the live log empty), at the cost of the .old bridge on that platform. Unix behavior is unchanged.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d455301. Configure here.
…ent_notice_state carrier
… torn tail scan_incident_log_files read with read_to_string, which fails the whole file on any invalid UTF-8 - a torn multi-byte write at the live log's tail (costing every structured line around it, exactly when the operator runs the CLI during an active incident). Node's readFile + toString keeps the tear as replacement characters - the agents-view reader's own lossy rule - so the scan now reads bytes and decodes with from_utf8_lossy: complete lines still classify, the torn line fails the parse like any non-line, and only a read error skips the file whole. Regression test: a third line cut two bytes into its three-byte U+26A0 keeps the two complete events (2 in window, 1 unreadable skipped).
The stderr-forward classifier keys on exactly 12 hex worker id chars (STDERR_FORWARD), so the 10-char fixture id fell to the generic supervisor event and the regression test asserted the wrong summary.
#2866 incident forensics) — conductor-directed refire head; no code change (zero-conflict fold)
… CLI, #2878 supervisor split 2/8; pa-core session untouched upstream - clean fold)

Summary
Rust port of TS PR #2406 — daemon incident forensics: the
prime-agent incidenttimeline CLI and the agents-view incident notices, built on one shared classifier.prime-agent incident [--since <time>] [--until <time>] [--session <id>]reconstructs daemon-log forensics for a time window into an operator timeline — supervisor events, session anomalies, and recovery — every line timestamped and severity-tagged, repeated identical events aggregated (x4, until ...), no raw-log spam. The agents view reuses the same classifier to pollagent.jsonlevery 30 seconds (rotation-safe, bounded, incremental) and shows one collapsed, dismissible warning line in the header when the recent log shows a worker crash, a command-timeout burst, or an update restart, pointing at the CLI for the full timeline. Esc dismisses the notice when the editor is idle and never steals priority from an armed delete confirmation.TS → Rust mapping
src/cli/incident.ts(1279L) — shared corecrates/pa-types/src/incident/{mod,parse,patterns,classify,classify_tests,anomaly}.rssrc/cli/incident.ts— CLI half (time bounds, report, log discovery, run)crates/pa-cli/src/incident/{mod,time,report,report_tests,source_tests}.rssrc/cli/command-registry.ts(+13)crates/pa-cli/src/command_registry.rs(theincidentspec, verbatim TS help strings)src/cli/public-command.ts(+24)crates/pa-cli/src/public_command.rs(routing, one-shot window resolution, dispatch tests)src/modes/agents-view/incident-notices.ts(+466)crates/pa-tui/src/incident_notices.rs(+incident_notice_tests.rs)src/modes/agents-view/agents-view-mode.ts(+84/-2)crates/pa-tui/src/agents_view.rs+crates/pa-cli/src/interactive_mode.rs(state carried across the view/session loop = TSpersistentState.incidentNoticeState)test/incident.test.ts(+735)test/agents-view-incident-notice.test.ts(+297)crates/pa-tui/src/incident_notice_tests.rs+ newcrates/pa-tui/tests/agents_view_incident_notice_e2e.rs(mock supervisor, headless frames)test/public-command.test.ts(+70)crates/pa-cli/src/public_command.rsincident_dispatch_tests.changes/*.md(+2),test/agents-view-mode.test.ts(+1 mock line).changesdir; the mock change is TS-harness-only)Crate placement: the classifier lives in
pa-types::incidentbecause pa-tui depends on pa-types alone (AGENTS.md: pa-types is the only shared crate) — the TS product keeps the same single classifier incli/incident.tswith both halves importing it, and the port preserves that: the agents-view notice reuses the classifier, never re-implementing it (TS incident-notices.ts imports from../../cli/incident.js). The CLI's log-file discovery,--since/--untilwindow parsing, and report rendering belong to pa-cli; the notice polling, rotation-safe incremental reads, and dismissal horizons belong to pa-tui. README scope entries updated (pa-types + pa-cli).Semantic parity carried from the TS PR
~/.prime/agent/logs/agent.jsonlincluding its.oldrotation; fallback to the newest per-daemon log (<socket>.<hash>.log, hash-suffix match so.sock-less basenames and Windows named-pipe sockets match), with theis_fileguard so a directory matching the pattern cannot hide valid logs. Window filtering happens later; the fallback gates on parse yield only.regexcrate — supervisor start/failed-spawn (lock-held arm), supervisor/daemon command failures (timeout/auth/starting/recovering/update-prep/unknown-session/quoted names), stderr forward lifecycle (start/crash/shutdown exit/signal/stop-requested/passivation with the empty-name token guard), catch-up and heartbeat-list timeouts, recovery replay (the ranked, capped operation breakdown), adopt/recover failures, unresponsive park, stale-registration reclaim, job migration, supervisor replacement, scheduled wake, evictions, worker-own lifecycle lines, unknown diagnostics."N command timeouts over X"), error bursts (3+ within 10 minutes,"N warnings/errors over X"), and 10-minute session event gaps. Per-subject keying keeps daemons sharing one agent.jsonl separate.--session: UTC time-bound parsing (ISO date-time, date, bareHH:MMtoday, fractional seconds,Z/±HH[:MM]offsets with RFC-3339 minute validation and roundtrip date validation); session filters prefix-match in both directions over session ids, worker ids, and quoted session names; the empty-token guard keepsname=""from matching every filter.Prime Agent incident timelineheader,Window: MM-DD HH:MM:SS → MM-DD HH:MM:SS UTC (30m), theSource:line with scanned/in-window/skipped counts, the three sections with(none)placeholders,(xN, until ...)aggregation, and chalk-honoring severity colors (red 31 / yellow 33 / dim 2, off-TTY and underNO_COLOR— the daemon_discovery format.rs rule).kind|subject(a timeout-burst notice anchors at its stall cluster's latest timeout so a later timeout extending the burst re-surfaces it, while an isolated stray timeout never moves the anchor), the first-read.oldbridge with the same-generation guard (a rename rotation between the live read and the bridge read must not double supervisor starts into a phantom update restart), the mid-rotation offset continuation (the un-consumed tail of a generation that rotates out between polls), a missing/unreadable log keeping the consumed offset (never fabricating a restart) while the notice ages out with its window.tui.select.cancel) dismisses only with an empty search prompt and no armed delete confirmation; the confirm wins (as in TS); the status line readsIncident notice dismissed. The state survives view re-entry through the flow (TSpersistentState.incidentNoticeState).Tests
is_fileviaFile::set_times,.sock-less socket basenames, empty/unreadable agent.jsonl, no-fallback-when-it-parses, missing logs), options/window parsing (the TS parseIncidentTimeBound suite incl. offset-minute rejection), and the public-command dispatch (routing + exit-1 usage arms + the help listing)..oldtail without a trailing newline, the hard-link same-generation bridge skip, the mid-rotation stranded tail), the missing-log offset preservation, the tail-bound cut; plus a headless e2e over a mock supervisor: the rendered collapsed line with the pointer, Esc dismissal with the status line, dismissal persistence across a second view run with the carried state, and the armed-delete-confirm case keeping the notice.Date.now(); the rest are pinned to the TS fixtures' 2026-09-10/16 instants.Ownership compliance
incidentmodule — the shared classifier; README scope entry added; the only crate pa-tui can share with pa-cli. Type: newpubsurface (the classifier API: parse/classify/collect/anomaly entry points + types).incidentCLI module (crate-private), the public-command route, the registry spec; README scope entry added.incident_noticesmodule (pub — the agents view and the flow carryIncidentNoticeState), the agents-view wiring.regex = "1"added to pa-types. Rationale: the TS classifier matches ~30 log-message regexes; theregexcrate keeps their semantics 1:1 where hand-rolled parsers would risk behavioral drift.regex1.13.1 is already in the tree (pa-tui), so no new crates enter the build — the lock gains exactly thepa-types → regexedge.make denyshould stay clean (MIT/Apache-2.0, no advisories).Cargo.lockupdated in the same change with exactly the edgecargowould write.Deliberate deviations from the TS PR
formatIncidentNoticeTimerenders UTC, not the local wall clock. The Rust tree is UTC end-to-end (the daemon'snow_iso()logs UTC; no timezone layer exists anywhere in the crate graph), so the notice'scrashed at HH:MMreads UTC and the day/year prefixes follow the UTC calendar. The TS shape (same-day bare time,M/D HH:MM,YY/M/D HH:MM) is preserved. Documented at the function..changes/*.mdchangelog entries have no Rust counterpart (the Rust repo has no.changesdirectory; release notes come from the PR/commit stream).setIntervalpoll is atokio::select!wake-up plus a deadline drain in the view loop (a deadline-driven interval, so a busy input stream cannot starve the 30s cadence — the TS interval fires between event-loop turns regardless).Validation
cargo fmt --all --checkclean on the folded head (182d1532conrusttip1ce0d94b1).incidentcommand output was the fixture basis for the exact-layout report test); the daemon-log fallback reads the same per-daemon log format the Rust supervisor writes (paths.rsdaemon_log_path+RotatingLog,[<ISO>] msglines — the TSsupervisor:prefix arm is kept for the TS-emitted logs the fallback may encounter).Note
Medium Risk
Large new surface area (log parsing, rotation, and classification) with many edge cases, but read-only forensics and local log reads—no auth or remote data changes.
Overview
Ports TS daemon incident forensics to Rust:
prime-agent incidentrebuilds an operator timeline from daemon logs, and the agents view surfaces a dismissible header warning when recent logs show crashes, timeout bursts, or update restarts.pa-types::incidentadds the shared classifier (JSONL and per-daemon log parsing, event classification, pid attribution, stall/burst/gap anomalies) withregexfor TS-parity message patterns.pa-cliimplements log discovery (agent.jsonl+.old, fallback to newest*.hash.log), UTC--since/--until/--sessionhandling, and the colored, aggregated report.pa-tui::incident_noticespollsagent.jsonlon a 30s cadence with rotation-safe incremental reads and dismissal horizons;agents_viewrenders the notice, Esc dismisses when safe, andinteractive_modecarriesincident_notice_stateacross view re-entries.Help/registry wiring, README scope updates, and broad unit/e2e tests mirror the TS fixtures and dispatch behavior.
Reviewed by Cursor Bugbot for commit 068259a. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add
incidentforensics CLI and agents-view incident notices (TS #2406 port)Ports the TypeScript daemon incident forensics feature to Rust.
prime-agent incidentwith--since,--until, and--session. Defaults to the last 24 hours and rejects windows where--untilis at or before--since(public_command.rs).pa-types: log parsing (JSONL and plain-text daemon lines), log-message classifiers, worker-pid attribution, and anomaly computation for timeout stalls, error bursts, and session gaps (parse.rs, classify.rs, anomaly.rs).NO_COLORor non-terminal stdout), aggregated repeats, and source line counts (report.rs).AgentsViewOptionsgains a new incident-notice state field; all existing test fixtures updated. Esc handling gains an early dismissal path before delete handling. ANSI styling and a new 30-second poll are added to the agents view.Macroscope summarized 068259a.