Skip to content

feat(console): Activity view + local outcome log (Stage 7 — closes Phase 2)#15

Merged
temrjan merged 6 commits into
mainfrom
feat/console-v2-phase2-s7-activity
Jul 14, 2026
Merged

feat(console): Activity view + local outcome log (Stage 7 — closes Phase 2)#15
temrjan merged 6 commits into
mainfrom
feat/console-v2-phase2-s7-activity

Conversation

@temrjan

@temrjan temrjan commented Jul 14, 2026

Copy link
Copy Markdown
Member

The last stage of Console v2 Phase 2: the fourth view Activity/History (h, filter on f), the client side of the §3.9 activity op (server side shipped in core#93, canon in #14), and a local outcome journal — history deeper than the core's 60-min retention window.

Spec: .claude/specs/2026-07-14-console-v2-phase2-s7-activity.md (Gate-1: both signatures; /check 7 findings applied). Report incl. Gate-2 delta: .claude/reports/2026-07-14-console-v2-phase2-s7-activity.md.

What

  • protocol/transport: Request::Activity + parse_activity (mirror of positions; OutcomeState is deliberately NOT TerminalState — §3.9 promises never "pending", so one fails the parse; no degradable errors: wallet_locked is not in the vocabulary).
  • scheduler: the op rides the existing list-first slot when the Activity view is on top (same STALE_TICKS cadence; the Dashboard's two-valued alternation is untouched and resumes where it left off — tested).
  • history pipeline: the model owns a pure Vec<HistoryEntry> (unix desc, id asc; ties inside one second are id-ordered by documented design — Gate-1 finding); the run loop owns the clock, the JSONL file (RUSTOK_CONSOLE_LOG$RUSTOK_DATA_DIR/console-activity.jsonl → session-only with a visible note) and the id-dedup set. A decision made here is recorded RICH in apply_resolve while the card is still open (to/amount/chain — the only moment they are known); server outcomes stamp arrival − age (§3.9) and fill missing fields of known rows. Decision/decision_line (ADR Console v2 UX + visual redesign: human-readable card, brand palette, animated tx flow, persistent dashboard #7) untouched — a separate drain, proven by test and smoke.
  • ui: fourth tab, filter cycle, shortened addresses (the one display list ТЗ §4.1 allows — signing surfaces keep full addresses), honest "(details not recorded)" for server-only rows, exact-fit/"+N more" budget.

Gate-2 round (2 blockers found by review, fixed in 718c594 with true-red proofs)

  1. Load-time dedup replaced whole records by the bigger unix — but a rich record (unix = now) and a poor server-window record (unix = arrival − age) come from different formulas: an ordinary two-console session lost rich details on reload. Now: field-by-field merge in file order through the one shared seam HistoryEntry::fill_missing_from. True-red both directions.
  2. read_to_string flattened every read error into "no history yet". Now: byte-wise read (a torn non-UTF8 tail poisons one line, counted + noted), NotFound distinguished from unreadable (surfaced as a note). True-red.

Minors folded in: sidecar-lockfile serializing writers (compaction re-reads fresh under the OS advisory lock — TOCTOU closed by construction), raw-line compaction trigger + periodic re-compaction in a resident session, per-pid tmp, 0600 on log/lock/tmp, fill_history_detail(&HistoryEntry).

Evidence

fmt / clippy -D warnings / cargo deny check advisories licenses bans sources clean · cargo test 211+30+1 (was 184+19+1; zero existing tests edited) · 37 new tests, 15 red proofs (5 true-red incl. both blockers, 10 targeted mutations) · DoD smoke 11/11 (real binary in a pty, two runs over one log file: instant rich row at decision, exactly one ADR-#7 decision line on piped stdout, full address absent from the frame, restart reloads history with an empty server window).

Open, deliberately deferred question (Captain's word, not blocking): same-uid tamper-integrity of the journal — 0600 done, the rest tracked separately.

temrjan added 6 commits July 14, 2026 14:52
Phase-1 backlog item: first 6 + last 4 hex, EIP-55 casing verbatim,
non-ASCII/short/unprefixed input returned as-is. For the Activity list
rows exclusively — signing surfaces keep full addresses (ТЗ §4.1).
…ctivity

OutcomeState is deliberately not TerminalState: §3.9 promises never
'pending', so a reply carrying one fails the parse (default-deny).
OutcomeEntry mirrors §3.9 verbatim (tx_hash/reason absent => None);
parse_activity mirrors parse_positions but has NO degradable answer —
wallet_locked is not in the §3.9 vocabulary, any error fails closed.
…se 2 read layer)

- transport/app: Request::Activity + Reply::Activity; the activity op rides
  the existing list-first slot when the Activity view is on top (same
  STALE_TICKS cadence; the Dashboard's two-valued alternation is untouched
  and resumes where it left off).
- History pipeline: the model owns a pure Vec<HistoryEntry> (unix desc, id
  asc — ties inside one second are id-ordered by documented design); the
  run loop owns the clock, the JSONL file on RUSTOK_CONSOLE_LOG ->
  $RUSTOK_DATA_DIR/console-activity.jsonl, and the id-dedup set. A decision
  born here is recorded rich (to/amount from the still-open card in
  apply_resolve); server outcomes stamp arrival-age (§3.9) and fill missing
  fields of known rows (never overwrite, file keeps its first record).
  Corrupt lines skip with a visible note; a failed write switches to
  session-only with a visible note; oversized files compact atomically.
- ui: fourth tab (h), filter cycle (f), shortened addresses — the one
  display list ТЗ §4.1 allows; exact-fit/'+N more' budget (Stage-5 lesson).
- Decision/decision_line (ADR #7) untouched — the history drain is a
  separate channel, proven by test.
…lockers)

BLOCKER-1: load_history deduped by whole-record replace on the bigger unix
— but a rich decision record (unix = now) and a poor server-window record
(unix = arrival − age) come from DIFFERENT formulas, so an ordinary
two-console session could trade rich details for a poor duplicate on the
next reload. Load now merges field-by-field in file order through the one
shared seam HistoryEntry::fill_missing_from (present fields are never
overwritten); Model::fill_history_detail takes &HistoryEntry (five
positional Options were a silent-swap hazard).

BLOCKER-2: read_to_string flattened EVERY read error into 'no history yet'
— a torn non-UTF8 tail after a SIGKILL mid-append silently hid the whole
file. The log is now read as bytes (a torn write poisons one line, counted
and noted), and NotFound (normal empty) is distinguished from unreadable
(surfaced as a note, never a healthy empty view).

Minors folded in: writers serialize on a sidecar lockfile (compaction
re-reads fresh under the lock — the TOCTOU race; the sidecar because rename
would strand a waiter on the old inode) · compaction triggers on RAW line
count and re-fires every HISTORY_CAP appends in a resident session ·
per-pid tmp name · 0600 on log/lock/tmp.
@temrjan
temrjan merged commit ad437ad into main Jul 14, 2026
4 checks passed
@temrjan
temrjan deleted the feat/console-v2-phase2-s7-activity branch July 14, 2026 11:32
temrjan added a commit that referenced this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant