Skip to content

feat(*): redraw the trajectory as an agent waterfall with external agents' steps - #1147

Open
LivXue wants to merge 113 commits into
mainfrom
feat/trajectory_agent_waterfall
Open

LivXue wants to merge 113 commits into
mainfrom
feat/trajectory_agent_waterfall

Conversation

@LivXue

@LivXue LivXue commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

The trajectory page could show what happened and in what order, but not
who was doing it: one cost bar, a row list, and one Sub-agent Scope at a
time behind a tab strip. A sub-agent that outlived its dispatching turn
was invisible next to it. This replaces the bar, the list and the strip
with an agent-first waterfall, and makes an external agent's own steps
visible in it.

The page:

  • Every entry is drawn on one time axis shared by every agent, grouped
    by agent (the main session first, then each worker, a worker's runs
    folded into one by instance, else by name), then by turn, then by
    span. Bars are the span's real extent (operation_start to
    operation_end); charged_ms stays in the details pane.
  • Stretches longer than 60s in which no agent was busy collapse to a
    fixed, labelled width. Each row's busy time stands in a column of its
    own; an agent's is its runs of work merged, never its lifetime.
  • A header whose six figures each carry a second line (the session's
    ends, the main agent and its sub-agents, an external agent's run
    counted as one, the median turn, the model's share of the turns' time,
    the two tools called most; the error count leads to the earliest
    failure). Under it an activity strip draws the whole session's calls
    per bucket of real time, the model's under the tools', with each
    folded idle stretch saying how long it was. A ruler in clock time
    beside the window's name, windowed rows, and a snapshot walk that
    grows the tree and the axis page by page instead of rebuilding them. A
    turn's bar is swept once and kept while its rows and the clock stand
    still, so a scroll frame recomputes none.
  • With several agents each starts as one row: a capsule per run of work
    on a faint line from the first to the last (the main agent in gold), a
    diamond where a failure happened, an arrow where its work runs past
    the window, and a chip naming when it works next. A worker is named by
    its task; open, a second line names the agent that ran it, the turn
    that sent it and how many turns it took, and a line is drawn from the
    instant it was sent to where its work begins. A call row is keyed by a
    square in its kind's colour and named by its tool, an external agent's
    kind of call (execute, edit) in the page's language, beside what it
    worked on; hovering a turn says what its time went on. Otherwise what
    crosses between agents is marked on rows: a dispatching row's door
    arrow, and the worker's name on the host turn its report started.
  • The window: Fit, 3h, 1h and 15m in the header; on the activity strip
    a drag takes a window, moves it or moves one edge; Ctrl/Cmd + wheel
    zooms about the pointer, Shift + wheel pans, a plain wheel scrolls.
    With the window narrowed, workers wholly before or after it fold into
    one row for each side. The window and a stretch picked on the ruler
    are kept in real time, so a live session growing past its end leaves
    them where they are; a zoom, a pan or another conversation drops the
    pick.
  • The rows are a tree for the keyboard and for screen readers: the list
    is reached with Tab, each agent and turn folds from a button with
    aria-expanded, and the selected row is marked on screen and announced
    (aria-selected, the tree's active descendant). A failed operation is
    marked on its own row and on the turn that holds it, and the header
    counts each call and each failure once. A row the index reports
    running is drawn running.
  • The pane's jump on a door or a report turn, and a dispatching row's
    door arrow, land on the run they name: its external agent's entry,
    which opens on what the agent recorded, else its first row. They
    select across agents, so nothing is filtered and the selection is
    never left empty.
  • Removed on purpose: the duration threshold, the hidden-entries switch
    (every entry the index returns now has a row), the toolbar and reset
    button, the now line, the dispatch rules, and the Scope Tab strip; the
    door arrow on a dispatching row finds the worker instead. The store
    state, styles and 38 catalogue keys only those parts used are gone
    with them. The per-agent overview lanes and the elapsed-time ruler
    marks gave way to the activity strip and the clock ruler, with their
    two strings.
  • A frosted glass sheet over the chat's yellow-blue wash, in both
    themes, with the details pane floating over the rows' right side as a
    white card below the header -- opening it leaves the waterfall its
    whole width. The card carries the desk palette's edge, so it reads as
    solid paper on the nearly white glass; it moves by its head and sizes
    by its corner alone, as that palette does, and goes back where it
    opened when let go near there. A key-value list's key column widens
    with the card. Every ink in the waterfall comes from a theme token,
    and the stylesheet's color gate now covers its sheet. The light
    theme's wash is warmer and a step stronger, so the background behind
    the transcript changes too. While the trajectory is up the composer
    card, the task strip and the desk step aside, and a message is sent
    from the chat view; an approval or a question the conversation raises
    still shows over the trajectory.

The backend:

  • The ACP backend writes what an external agent said, thought and ran
    as llm.call / tool.call child spans of its subagent.external run, the
    same shape the main loop writes, at most 400 per run. A step ends
    where the next begins and every step lies inside its run. The steps
    are written however the turn ends once its prompt went out: answered,
    cancelled, past the prompt's deadline, or cut off by the connection.
    A tool call that failed is a failed step, marked the way the main
    loop marks one, and a model step is typed model.
  • One reduction turns a run's session/update frames into its steps,
    live and from the journal alike (acp_client/turn_events.py), and the
    journal is read under the dialect the connection's handshake chose.
    The journal stamps each frame with its UTC offset, so a run read
    back in another zone keeps its times.
  • raven trajectory backfill-frames writes those steps for runs recorded
    before, from the wire journal still on disk: ids derived from the run,
    so a second pass, like a dry run, writes nothing, neither a span nor
    an artifact; it closes a step an earlier pass left running past its
    run's end, and holds a step stamped outside its run inside it, so a
    second pass converges; it leaves alone a run that wrote its steps
    live (subagent.external.steps); and it reports a run whose recorded
    stretch reads back empty as unread, not as already on record. It
    reads a run from its own session/prompt on, so a resumed session's
    replayed history is not counted as the run's. With tracing turned
    off it writes nothing and says so; --force writes anyway.
  • The details pane's Replay block reads a run's own stretch of the wire
    journal, from its prompt on, and only from this store's acp-frames
    directory.
  • Replay and the cassette minimizer leave out everything under a
    subagent.external span: the tool that launched the agent is answered
    from the recording, so the agent never runs again.
  • A call that only thought no longer shows an empty output row; its
    thinking row carries the call's time instead.
  • The details pane gains a Reasoning tab on a model call: its reasoning
    text, or, when the provider counted reasoning tokens and returned
    none, a sentence saying so in the reader's language.
  • A redacted bundle keeps each file's own line endings: the redaction
    writes its copy back as bytes, so on Windows a CRLF file no longer
    gains a blank line between records. The redaction predates this
    change; this branch's cassette test is what met it there.

Docs: the ui-web glossary entry, the Timing Owner entry, the design doc,
and the public WebUI and trajectory-debugging pages (both languages) now
describe this. The charged_ms description in the RPC contract and both
generated clients no longer names the removed bar.

Checked and deliberately not changed:

  • An agent group's mark is chosen by its recorded name, meta.agent
    (else meta.instance); the Raven mark goes to the word raven, alone
    or followed by a separator, as in Raven-Research. An entry carries
    no preset, so the preset key AgentMark is built on is not available
    here. It is presentation only; nothing else derives from it.
  • The store still keeps a current Sub-agent Scope with per-scope views
    (follow, anchor, saved). No strip shows it; select and the pane's
    links still use it, and openScope, switchScope and closeScope are now
    reached only from their tests. Removing it is a refactor of its own
    and is left out.
  • Only ACP runs get steps: the live recorder sits in the ACP backend and
    the backfill reads ACP wire journals. A CLI-lane external agent still
    appears as one bar.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

At the rebased head, on origin/main 9d9bec9. This push changes no
Python; git range-diff against the previous head shows every commit's
patch unchanged but one, whose import context moved with main's trace
compression.

Python, proxy variables unset, with zstandard (the dependency main
added) layered over the venv: the trajectory, tracing and ACP suites
(tests/test_trajectory_.py, test_tracing_.py, test_subagent_acp.py,
test_acp_journal.py, test_cli_trajectory_commands.py and
test_cli_tracing_commands.py), 2354 passed and 1 skipped. The full suite
was last run at the previous head: 3 failed, 37022 passed, 255 skipped,
2 xfailed, the same three failing on origin/main on this machine (a
LibreOffice conversion that writes no .xls, npm on PATH, running as
root).

The tests for the replay, cassette, live-lane and backfill fixes were
seen failing before their fix, and a mutant of each fix fails them:
replay keeping an external step, the cassette keeping one, the subtree
walk cut to direct children, a live step starting before its run, the
backfill ignoring the live count, the live lane not writing it, and a
dry run or a skipped step persisting its artifacts (checked by the real
backfill over a real store, which a dry run and a second pass must leave
byte-for-byte as they found it). The same holds for the later fixes,
each seen failing first and each mutant killed: a cancelled run, one
past its prompt deadline and one whose agent died mid-turn writing their
steps, an unsent prompt writing none, a thought-only call keeping its
time, a late step held inside its run on the first pass and left alone
by the second, an unreadable stretch counted as unread, and withheld
reasoning sent as a reason code.

The fixes for the last review round were each seen failing first too:
the same frames read live and from a journal compared event by event
(a plan, a steer, revised arguments, a codex command that exited 1, a
command recovered from a permission request), a resumed run replayed
from its own prompt, the handshake's dialect reaching the Replay block
and the backfill, a failed external call drawn failed, model steps
typed model, a journal stamp read back the same in another zone, the
backfill writing nothing with tracing off, the sheet rack kept in the
trajectory view, the color gate reading the waterfall's sheet, a
running row drawn running, calls counted by span, and a turn's bar
kept across frames. Mutants of those fixes all fail them: 8 on the
reduction and the replay, 10 on the page and its gates. The commits
that split one file between two of these were checked on their own:
each passes its files' tests.

Gates: ruff check and ruff format --check on every lint target,
lint-imports (10 contracts kept), deptry, ty check, check_large_files.py
and check_source_language.py over origin/main...HEAD,
check_commit_messages.py and commitlint over origin/main..HEAD: all
pass.

ui-web: gen:check, type-check, eslint (0 errors; 4 warnings in pages
this change does not touch), vitest 272 files / 4591 tests, and
vitest --no-file-parallelism scripts/gates 57 files / 272 tests: all
pass. The tests for the scope landing, the failure marks, the tree's
roles and keys, the zoom and pick kept across a growing session, the
pointer-anchored zoom, the scroll cost, the ruler's rounding and the
reveal of a row outside every turn were seen failing first, and a
mutant of each fix fails them. This push's twenty commits were each
checked on their own tree (the trajectory tests and stylesheet gates,
the i18n key gate, tsc, check-css, eslint); every behaviour they add
was seen failing first, with a mutant of each failing its tests, and
the card's drag, corner, snap back and overlay bar were measured in
Chromium with real mouse input.
ui-tui: eslint on the regenerated catalogue (0 errors), lint:rpc,
lint:i18n and lint:shared in sync.

Windows: the line-ending fix is pinned by a test that applies Windows
text-mode newline translation at the write, so it fails without the fix
on any platform. The test that replay will not follow a link out of the
journals now skips, with the reason, on an account that cannot create
symbolic links instead of erroring: run here with the link denied it
skips, and with links allowed it passes as before.

The page's later fixes were checked in happy-dom tests, not by eye in a
browser. By hand on a real store, before them: the page served from
this change; an export and re-import round trip kept every entry,
artifact and message; the
backfill filled 151 older external runs, and a dry run after it
reported nothing left to write ("0 of 151 external run(s); 151 already
on record").

  • Relevant tests pass locally
  • Relevant lint / type checks pass locally
  • User-facing docs or screenshots are updated when needed

Risk

  • Disk: each step writes a span record and its input/output artifacts
    through the existing content-addressed artifact store, at most 400
    steps a run. Measured on a real store after the backfill: 151 runs,
    17,133 step records, 20.8 MB of span log (about a sixth of its 120 MB)
    and 11.6 MB of unique artifact blobs. There is no new reclaim policy:
    the steps live and go with the audit log they belong to.

  • Security: the Replay block resolves the journal path and refuses one
    outside this store's acp-frames directory, symlinks included, so a
    path inside an imported trajectory cannot make it read another file.
    The backfill reads only journals the local store recorded; imported
    trajectories are never scanned.

  • Compatibility: no RPC shape changes, only a field description. Bundles
    and cassettes recorded before this hold no steps under an external run
    and replay exactly as before. The wire journal's timestamps now carry
    their UTC offset; the one reader parses both forms, and an older naive
    stamp reads as it did. With tracing turned off, backfill-frames now
    writes nothing unless --force is passed. In the trajectory view an
    approval or a question the conversation raises now shows over the
    trace instead of being hidden.

  • Rollback: revert the squash commit. Steps already written stay in
    the audit log; an older build reads them as ordinary spans, and
    replaying a bundle that holds them would be fed them, which is what
    the replay change here prevents.

  • Security impact considered

  • Backward compatibility considered

  • Rollback path is clear for risky changes

Related Issues

N/A

@LivXue
LivXue requested review from 0xKT and gloryfromca October 29, 2026 12:57

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: make backfill-frames --dry-run leave the trajectory store unchanged.

I reviewed the full github/main...HEAD change and the relevant runtime, replay, trajectory, store, waterfall, and detail callers. I also checked the repository rules and canonical Runtime/Web UI terms, commit history, backward compatibility, the replacement tests for removed UI components, and the Runtime/Web UI RPC boundary. I found one concrete write-on-dry-run defect inline; no other blocker survived validation.

Verification:

  • uv run pytest tests/test_trajectory_frames_backfill.py tests/test_trajectory_frames_replay.py tests/test_trajectory_replay.py tests/test_trajectory_details.py tests/test_subagent_acp.py tests/test_cli_trajectory_commands.py -x - 604 passed, 1 pre-existing pytest-mark warning.
  • npm test -- --run src/features/trajectory src/chrome/ChatTop.test.tsx src/chrome/behaviour/scrollbars.test.ts src/state/scrollbars.test.ts - 335 passed.
  • npm run type-check - passed.
  • scripts/check_large_files.py github/main...HEAD and scripts/check_source_language.py github/main...HEAD - passed via the repository's uv commands.
  • A focused reproduction observed two persist_artifact calls from record_steps(..., dry_run=True) for one prompt and answer.

@LivXue
LivXue requested a review from gloryfromca October 29, 2026 13:42

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

I reviewed the complete delta from the previously reviewed revision and rechecked it against the full github/main...HEAD change, relevant callers, commit history, backward compatibility, replacement-test strength, repository rules and canonical terms, and the Runtime/Web UI architecture boundary. The reported dry-run defect and its second-pass orphan-artifact variant are fixed without changing real-write behavior. I replied to the author and resolved my original thread. No new finding survived validation.

Verification:

  • uv run pytest tests/test_trajectory_frames_backfill.py tests/test_trajectory_frames_replay.py tests/test_trajectory_replay.py tests/test_trajectory_details.py tests/test_subagent_acp.py tests/test_cli_trajectory_commands.py -x - 606 passed, with one unrelated existing pytest-mark warning.
  • git diff --check github/main...HEAD - passed.

@LivXue

LivXue commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility review — PR #1147 @ eb53222857b1cbe5cd8cf9f50d9a6f5a67e33a2f

Scope: the full base-to-head diff at this head (80 files, +6895/−5877): the ui-web trajectory redraw as an agent waterfall, external-agent run spans (raven/agent/subagent/backends/observability.py), frame-journal replay and backfill for runs recorded before spans (raven/trajectory/frames_replay.py, frames_backfill.py), the cassette minimizer's external-run exclusion, the new trajectory backfill-frames command, docs and tests. Reviewed and run on native Windows.

Findings, most severe first:

  1. Low–Medium — redact_bundle doubles CRLF line endings on Windows (reproduced; the function is inherited, but this PR's new test is what hits it). raven/trajectory/redact.py:594-604 reads each file as bytes and decodes UTF-8, keeping the \r\n the source already has, then writes it back with target.write_text(text, encoding="utf-8"), whose default newline=None translates every \n to \r\n again — producing \r\r\n. Minimal reproduction on this host: source b'{"a": 1}\r\n{"b": 2}\r\n' → redacted b'{"a": 1}\r\r\n{"b": 2}\r\r\n', and read_text().splitlines() gives lengths [8, 0, 8, 0] (a blank line between every record). The repo's own readers (load_recording, _deduped_spans) skip blank lines, so replay still works, but any strict JSONL consumer of a redacted cassette — including this PR's new tests/test_trajectory_cassette.py::test_minimize_drops_what_an_external_agent_did_in_its_own_run — fails with JSONDecodeError: Expecting value on the blank entries. Remedy direction: target.write_bytes(text.encode("utf-8")) (the redaction pass already works on bytes), or read with read_text(encoding="utf-8") and let a single translation happen.
  2. Low — a new test assumes POSIX symlink rights. tests/test_trajectory_details.py:2007 (test_replay_will_not_follow_a_link_out_of_this_stores_journals) calls link.symlink_to(...) and fails on a default Windows account with OSError: [WinError 1314] (creating a symbolic link needs SeCreateSymbolicLinkPrivilege). Guard the setup with try/except OSError: pytest.skip(...), or skip on Windows, so the containment check it covers is not silently left unexercised on such hosts.

Positive observations, checked by reading and by the runs: frames_replay._slice reads the journal in binary mode at byte offsets; _d_replay resolves both the journal and the store's journal root before is_relative_to, so Windows' case-insensitive path equality and junction resolution work in its favour; the new modules add no subprocess, signal, socket or permission surface.

Verification on native Windows 11, detached worktree D:\tmp\rv-1147 at the reviewed head, offline venv (uv sync --frozen --offline --all-extras --no-extra segmenter --dev), scratch HOME/USERPROFILE, PYTHONUTF8=1, PYTHONPATH pinned:

  • uv run --no-sync pytest tests/test_cli_trajectory_commands.py tests/test_trajectory_cassette.py tests/test_trajectory_details.py tests/test_trajectory_entries.py tests/test_trajectory_frames_backfill.py tests/test_trajectory_frames_replay.py tests/test_trajectory_replay.py tests/test_subagent_acp.py -q → 58 failed, 690 passed, 1 skipped
  • the six of those files that exist at base 3ba6179f → 56 failed, 668 passed, 1 skipped
  • the two head-only failing IDs are exactly the two findings above; every other failure is inherited (the CI's own suite already fails at base on this host).
  • minimal redact_bundle reproduction above.

Coverage gaps: the ui-web half of this PR (the waterfall rewrite, its 20+ new test files) could not be executed — the shared D:/projects/Raven/ui-web/node_modules is empty (0 entries) — so it is checked by reading only. The full suite was not rerun.

@LivXue
LivXue force-pushed the feat/trajectory_agent_waterfall branch from eb53222 to 2b8bfa3 Compare October 30, 2026 02:29
@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1147 @ 18e0c6ee35055cfa258a5b1984d4c0307436db47

Scope: the branch is rebased onto current main; the delta since the previously reviewed head eb532228 is 74 commits, of which two directly address the findings from that review — 97f0a7b93 (write a redacted copy back as bytes) and 2b8bfa3d5 (skip the journal-link test where the account cannot create links) — plus further trajectory/waterfall work and chore(acp): keep frame journals for 90 days. Re-verified on native Windows.

Result: both findings are addressed and verified on this host.

  1. CRLF doubling in redact_bundle — fixed. raven/trajectory/redact.py now writes the decoded text back with target.write_bytes(text.encode("utf-8")), so the file's own line endings are not translated a second time. The minimal reproduction used in the previous review now preserves them exactly: source b'{"a": 1}\r\n{"b": 2}\r\n' → redacted b'{"a": 1}\r\n{"b": 2}\r\n', and read_text().splitlines() gives [8, 8] where it previously gave \r\r\n and [8, 0, 8, 0]. The cassette test that failed on Windows at the previous head (test_minimize_drops_what_an_external_agent_did_in_its_own_run) now passes.
  2. Symlink privilege in the new journal-link test — fixed. The test catches OSError around symlink_to and skips with the reason; on this account it now reports SKIPPED ... this account cannot create a symbolic link, instead of erroring at setup.

Verification on native Windows 11, detached worktree D:\tmp\rv-1147b at the reviewed head, offline venv (uv sync --frozen --offline --all-extras --no-extra segmenter --dev), scratch HOME/USERPROFILE, PYTHONUTF8=1, PYTHONPATH pinned:

  • the two previously failing tests alone → cassette test passes, symlink test skips with its reason
  • uv run --no-sync pytest tests/test_cli_trajectory_commands.py tests/test_trajectory_cassette.py tests/test_trajectory_details.py tests/test_trajectory_entries.py tests/test_trajectory_frames_backfill.py tests/test_trajectory_frames_replay.py tests/test_trajectory_replay.py tests/test_subagent_acp.py tests/test_acp_journal.py tests/test_trajectory_redact.py -q → 58 failed, 743 passed, 2 skipped
  • the eight of those files that exist at base 6298840e → 61 failed, 705 passed, 1 skipped
  • no failing ID appears only at the head; three base-only test_trajectory_cassette.py failures now pass at the head, so this head also fixes them. The inherited failures are the known Windows cases already failing at base.
  • minimal redact_bundle reproduction above.

Remaining inherited Windows issue, not introduced here: three tests/test_trajectory_redact.py cases fail at base and at this head because the report records skipped binaries with OS separators (artifacts\blob.bin vs the expected artifacts/blob.bin, and a redaction.json key likewise). That is a pre-existing cross-platform metadata inconsistency worth a separate fix, not a regression from this PR.

Coverage gaps: the ui-web half of this PR (the waterfall work) still cannot be executed — the shared D:/projects/Raven/ui-web/node_modules is empty (0 entries) — so it is checked by reading only. The full suite was not rerun.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

I reviewed the nineteen follow-up commits after the previously clean revision and rechecked the rebased full github/main...HEAD change, relevant callers and history, backward compatibility, replacement-test strength, repository rules and canonical terms, and the Runtime/Web UI boundary. The follow-ups improve scope landing, failure and tree semantics, live-axis stability, cut-short ACP recording, backfill accounting and bounds, localized withheld reasoning, and Windows redaction behavior. No new finding survived validation, and my earlier thread remains resolved.

Verification:

  • uv run pytest tests/test_trajectory_frames_backfill.py tests/test_trajectory_frames_replay.py tests/test_trajectory_replay.py tests/test_trajectory_details.py tests/test_trajectory_entries.py tests/test_trajectory_redact.py tests/test_subagent_acp.py tests/test_acp_journal.py tests/test_cli_trajectory_commands.py -x - 790 passed, with one unrelated existing pytest-mark warning.
  • npm test -- --run src/features/trajectory - 303 passed.
  • npm run type-check - passed.
  • git diff --check github/main...HEAD - passed.
  • Repository large-file and source-language checks - passed.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the delta from 18e0c6e through 7736f0e. The only change translates two trajectory schema strings in the Chinese catalog and keeps the generated TypeScript catalog in sync; it does not alter callers, compatibility, test strength, or architecture. I also checked the repository rules, relevant history, and the previously resolved review thread.

Verification: uv run pytest tests/test_i18n_* -x (18 passed), git diff --check github/main...HEAD (passed), and the source-language gate (passed).

@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1147 @ 7736f0eb565fdf3932118c97fece1c7168f2264e

Scope: the delta since the previously reviewed head 18e0c6ee — one commit, 2 files (+4/−4): an i18n translation of the trajectory schema label. No source or test logic changes; the fixes verified at the previous head (redacted copies written back as bytes; the journal-link test skipping where the account cannot create links) are untouched.

Result: no Windows-specific issue. This is catalogue text; the generated TUI catalogue is the only derived artifact.

Verification on native Windows 11, detached worktree D:\tmp\rv-1147c at the reviewed head, ui-tui/node_modules junctioned read-only from the main checkout:

  • node scripts/gen-i18n.mjs --check → i18n: generated catalogue is up to date (exit 0), and git status --porcelain stays clean afterwards, so the checked-in generated module matches what the generator emits on Windows.
  • No Python files changed in this delta, so no test rerun was needed for it.

Coverage gaps: the ui-web half of the PR remains untestable here — the shared D:/projects/Raven/ui-web/node_modules is empty (0 entries) — unchanged from the previous review.

@0xKT

0xKT commented Oct 30, 2026

Copy link
Copy Markdown
Member

Not a blocker - non-blocking items from the T1 acceptance on eb532228 (onto fb0dd515). The live waterfall path and the kernel change (Span.identity() in raven/tracing/trace.py) read as necessary and correct.

Severity of this one finding, not a verdict on the pull request. Any blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.

  1. A failed external tool call is recorded as a succeeded step on the live path for some dialects, so the waterfall shows it ok (obs lane, reproduced with the stub agent).
  2. backfill-frames reads naive-local journal stamps in its own zone. Run under a different UTC offset than the writer, steps land outside their run, and the "close steps past the run's end" repair re-emits them on every pass without converging (observability.py:140-150, 312-314; frames_backfill.py:141-151).
  3. backfill-frames writes spans and artifacts with tracing turned off, through tracing internals instead of the normal emit path.
  4. Synthesized llm.call steps use span.type llm, outside the closed kind vocabulary the main loop uses.
  5. The waterfall decides "running" from a missing operation_end, so production running spans look finished.
  6. Header stats count entries, not calls or errors: one LLM call shows as 3, one failure as 2.
  7. turnSegments is O(k^2) and runs for every painted turn row on every render, which includes each scroll frame.
  8. Keyboard/focus regression vs the deleted list. The rows are out of the tab order and the selected row is never marked for assistive tech.
  9. Brand marks are picked by agent name, a second definition that AgentMark exists to prevent.
  10. CONTEXT.md / ui-web/CONTEXT.md still describe the deleted tabs and rules in places (Sub-agent Scope consumers, "Lane" reused for the waterfall), and Span.identity() is missing from the tracing standard API doc.

Tests here on the merged tree: the changed Python test files via the board runner gave 749 passed. ui-web src/features/trajectory, src/chrome and src/state: 77 files, 1177 passed.

Comment thread raven/trajectory/frames_replay.py Resolved
Comment thread raven/trajectory/frames_replay.py Resolved
Comment thread ui-web/src/styles/page.css Resolved

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: correct frame replay before backfill writes its output permanently.

The two new frame-replay findings change my stance on this unchanged revision. I confirmed the underlying code paths: frames_replay.events independently reduces updates with a hard-coded AcpDialect and filters only by session id, while the live path uses dialect_for(...), _TurnCollector's plan/steer/call-revision behavior, and attaches the collector only after session/load. That makes ordinary dialect-specific and resumed runs capable of producing different replay/backfill steps from their live steps; the backfill's idempotent skip then preserves the incorrect spans.

Verification: uv run pytest tests/test_trajectory_frames_replay.py tests/test_trajectory_frames_backfill.py tests/test_trajectory_replay.py tests/test_subagent_acp.py -x passes (414 passed, one existing warning), but those tests do not cover the reported replay divergences or resumed-session history. I am leaving the two open blocker threads to their author rather than duplicating or re-grading them.

@LivXue
LivXue force-pushed the feat/trajectory_agent_waterfall branch from 7736f0e to 1950ad7 Compare October 30, 2026 07:47
@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Thanks for the T1 list. Item by item, at 006c094:

  1. Failed external tool call recorded as a success -- fixed in 54445d5. record_steps marks a step whose result said ok=False the way the main loop marks a failed call: tool.error with the result's preview, and status ERROR, so the trajectory reads it as failed. Test: test_a_failed_external_tool_call_is_a_failed_step (operation_status error, one failure entry). Steps an earlier backfill wrote are not rewritten (unreleased branch, my local store only).
  2. Naive-local journal stamps -- fixed in 89cf8ca. The journal writes local time with its offset, so a backfill in another zone reads the same instant; an older naive stamp keeps today's reading. The non-convergence you saw at eb53222 was already gone at 7736f0e (steps are clamped into their run). Test: test_a_frame_stamp_names_the_same_instant_in_any_zone_that_reads_it (written under Asia/Shanghai, read under UTC: 8 h off before). The journal test that pinned naive stamps ("naive local, as the session log is") now pins the offset instead.
  3. Backfill writing with tracing off -- fixed in 5249517. A real pass with tracing off writes nothing and says so; --force writes anyway; a dry run still counts. Test: test_a_backfill_with_tracing_turned_off_writes_nothing_unless_forced, plus the CLI's message; the other backfill tests now pin tracing on, so a shell that exports RAVEN_TRACING=0 cannot fail them.
  4. span.type llm -- fixed in a8ae471: model, as trace.py maps llm.*. Test: test_an_external_runs_model_steps_are_typed_the_way_the_main_loop_types_them.
  5. Running read from a missing operation_end -- fixed in 1857d86. isLive reads operation_status running, and a running row ends at the present while its snapshot is fresh; the 30-minute window counts from the snapshot's end, so a run whose snapshots stopped does not tick forever. Test in model.test.ts.
  6. Header counting entries -- fixed for calls in b7a6017: LLM and tool calls are counted by span. Errors were already one per failed span at 7736f0e (failure_entry, one owner entry per span); if you still see 2 for one failure, a run id would let me look.
  7. turnSegments O(k^2), recomputed per frame -- fixed in bf4d67f: one sort of the rows' edges and a sweep (k log k), kept per turn and clock, so a scroll frame reuses the bar. One 20,000-row turn: 750 ms before, 17 ms after (5,000 rows: 71 to 8 ms). Tests: the same bar for the same turn and clock, and the sweep agrees with a stretch-by-stretch reading over 40 random turns.
  8. Keyboard and focus -- in place at 7736f0e, after the head you checked: the list is role="tree" with tabIndex={0} and aria-activedescendant, the rows are treeitems with aria-selected on the selected one, and every fold is a button with aria-expanded.
  9. Marks picked by name -- kept on purpose. A trajectory entry's meta carries agent, instance and model but no preset, so the preset key AgentMark is built on is not something the page can use; the mark is presentation only. ui-web/CONTEXT.md now says so (006c094). Carrying the preset on the subagent.external span would let it follow AgentMark; that changes the entry meta and would be a PR of its own.
  10. Docs -- in 006c094, Span.identity() is now in the Span table of docs/TRACING_STANDARD_API.md, and the glossary calls the overview's rows "a row of activity marks per agent" instead of reusing Lane. The other stale CONTEXT lines you listed were already updated at 7736f0e.

@LivXue
LivXue requested review from 0xKT and gloryfromca October 30, 2026 07:48

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the full delta since 7736f0e and verified the fixes for all four open blocker threads: live collection and journal replay now share TurnEvents and select the recorded handshake's dialect; replay starts at this run's first prompt and excludes session/load history; the trajectory view keeps #sheetRack visible; and the waterfall uses complete light/dark/system-dark token sets. I also checked the additional backfill policy, failed-step status, timestamp, span typing/liveness, header-count, segment-sweep, narrow-shell, documentation, and generated-gate changes.

Coverage included AGENTS.md and the relevant runtime/Web UI context rules, the changed code and callers, history, backward compatibility, test strength, and the Turn Rows/UI architecture constraints. The new tests strengthen rather than weaken the prior coverage. My earlier thread is resolved; the four newer threads belong to another reviewer and remain for that reviewer to resolve.

Verification: 782 focused Python tests passed; all 4,494 UI tests passed; TypeScript type-check passed; ESLint passed with 0 errors and four pre-existing warnings on untouched files; git diff --check, source-language, and large-file gates passed. The new real-browser integration module collected no tests because its optional Playwright environment is unavailable here.

@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1147 @ 1950ad79e1582bd858fb4733f5f5869f01fe9c25

Scope: the branch is rebased onto main 11ca10d8; git range-diff 11ca10d8..7736f0eb 11ca10d8..1950ad79 shows every previously reviewed commit unchanged, and the new commits are: journal frames stamped with their UTC offset, backfill skipped when tracing is off unless forced, a failed external tool call marked as a failed step and an external run's model steps typed as model, a resumed run replayed from its own prompt, replay using the live collector's reduction, waterfall UI fixes (theme tokens, header counts by span, a once-per-turn bar sweep), docs, and an import-order test fix. Reviewed and run on native Windows.

Windows angle: the new timestamp is datetime.now().astimezone().isoformat(), which takes the offset from the operating system (no tzdata dependency) and behaves the same on Windows; the reader accepts both offset-carrying stamps and the naive local times older journals carry. Nothing in the delta adds a path, subprocess, signal, socket, or permission surface, and the previously verified Windows fixes — redacted copies written back as bytes, and the journal-link test skipping where the account cannot create links — are unchanged in this head.

Verification on native Windows 11, detached worktree D:\tmp\rv-1147d at the reviewed head, offline venv (uv sync --frozen --offline --all-extras --no-extra segmenter --dev), scratch HOME/USERPROFILE, PYTHONUTF8=1, PYTHONPATH pinned:

  • uv run --no-sync pytest tests/test_cli_trajectory_commands.py tests/test_trajectory_cassette.py tests/test_trajectory_details.py tests/test_trajectory_entries.py tests/test_trajectory_frames_backfill.py tests/test_trajectory_frames_replay.py tests/test_trajectory_replay.py tests/test_subagent_acp.py tests/test_acp_journal.py tests/test_trajectory_redact.py -q → 58 failed, 756 passed, 3 skipped
  • the eight of those files that exist at base 11ca10d8 → 61 failed, 705 passed, 1 skipped
  • no failing ID appears only at the head; three base-only test_trajectory_cassette.py failures now pass at the head, which is this PR's redacted-bytes fix taking effect. The inherited failures are the known Windows cases; the skips are the POSIX-only journal-link case and the link-privilege skip.

Coverage gaps: the ui-web half of the PR still cannot be executed — the shared D:/projects/Raven/ui-web/node_modules is empty (0 entries) — so the waterfall work is checked by reading only. The full suite was not rerun.

LivXue added a commit that referenced this pull request Oct 30, 2026
## Summary

Three storage layers of the trace store, each with an idempotent, opt-in
migration:

1. **Payloads.** Every captured artifact and every content-addressed
message is written as a zstd frame under the path it already had. `raven
tracing compress` migrates an existing store in place and moves each
artifact hard link onto the one compressed blob; `--dry-run` compresses
in memory to report the real saving and writes nothing.
2. **Cold logs.** A rotated span or event log is compressed one rotation
later, never the log just rotated (another writer may still be appending
its last line to that inode), and by `raven tracing compress --logs`,
which leaves the newest rotated log of each kind for that next rotation;
a compressed log keeps its mtime, which is how the viewer orders logs.
The span-log readers decode by the frame and keep working in plaintext
byte offsets, which is the only coordinate the session index cursor and
the viewer shard sidecars record. Compression gives a log a new inode,
so the session index recognises it by the sha1 of its first 64 plaintext
bytes and carries the cursor over instead of reading it again.
3. **Message packs.** Content-addressed messages are packed into
multi-megabyte zstd frames with an `sha1 -> (pack, plaintext offset,
length)` index. `raven tracing pack-messages` writes the packs, commits
the index atomically, and only then removes the message files; a reader
that finds no file reads its slice.

A fourth commit bounds what a stored frame may decode to, after the
automated security review flagged decompression bombs. A later review
round fixed six defects, each with a regression test that failed before
its change: a frame without a content-size field was measured from the
wrong offset (a span log then reset the session index on every other
pass); a per-shell budget cut inside a multi-byte character raised
instead of degrading; the viewer accepted Node 23.0-23.7, which have no
zstd; the pack reader's streaming skip was unbounded and packs past the
cache budget were decoded whole rather than streamed; the pack cleanup
deleted a message file whose pack no longer decoded; and CONTEXT.md
still said the message store had no reclaim path. A third round fixed
four more the same way: compressing a rotated log replaced its inode, so
the session index read it again from offset 0 (a record without ids was
listed twice, a rotation mid-scan reset the index, as did ext4 handing
the freed inode to the next log, and a spilled trace was counted as
lost); compressing the log just rotated dropped a line another writer
had opened the file to append, at rotation and in `raven tracing
compress --logs` alike; the writer did not write a packed message again
when its pack was gone or cut short, so a store copied incompletely
never healed; and a PATH Node without zstd hid a zstd-capable one later
in the search order, such as the runtime under `~/.raven/runtime`. The
command table also stopped calling packs archives.

Why: the live store on this machine was 1357.7 MB, most of it block
slack and uncompressed JSON. On a clone of that store the archive logs
fell from 94.0 MB to 8.2 MB, and 40,056 messages fell from 225 MB of
disk to 12 packs (23.6 MB) plus a 3.9 MB index. Projected whole-store
result: about 548 MB, roughly 60 percent saved.

Key decisions:

- **Paths never change, the frame decides.** A span records
`artifact_path` when it is written and those records are the audit
trail, so a migration cannot rename anything. Readers sniff the zstd
magic, which is also what keeps a store written before this PR readable
and a half-migrated one coherent.
- **ACP frame journals stay plaintext on purpose.** A span records a
plaintext byte range into its journal (`subagent.external.frames.start`
/ `end`), and #1147's frames replay reads exactly those offsets.
Re-encoding a journal would silently invalidate that audit record, so
neither the migration nor rotation touches it.
- **Pack migration order is crash-safe.** Packs, then the index via
atomic replace, then deletion, then an orphan sweep. A crash at any
point leaves every address readable from one side or the other. The
writer skips an address whose pack still decodes and covers its slice,
so a resumed or repeated turn does not recreate files that were packed,
while an address whose pack was lost or cut short is written again. The
deletion pass keeps a file whose pack no longer decodes, so a store
copied incompletely does not lose its last readable copy.
- **Reader caches are bounded.** The pack index is cached per process
until its size or mtime changes; at most 32 MB of decompressed packs are
kept, and a pack larger than the whole budget is streamed per read.

Relationship to #1147: the two branches overlap in four files
(`CONTEXT.md`, `raven/trajectory/conversation.py`,
`raven/trajectory/details.py`, `tests/test_subagent_acp.py`), none of
them touched by the third-round fixes, and a merge-tree dry run against
its head `1950ad79` has no conflict. #1147 does not touch
`pyproject.toml` or `uv.lock`. This branch was rebased onto main; the
only conflict was main's `openai` dependency from #1108, resolved by
keeping both entries and re-locking. A second rebase, onto `7c12b93f`
(#1141), had no conflict.

Deployment: deploy this code before running either migration. The
bundled viewer needs Node 22.15 or newer (23.8 on the 23.x line) for the
zstd decoder, the same requirement its compressed-artifact path already
has. Code older than this PR cannot read a compressed payload, and the
write path compresses new artifacts by default, so the deployment is
one-way once anything new is written; the migrations themselves are
manual and opt-in.

Measurements on a clone of the live store (2026-10-29):

- Archive span/event logs: 94.0 MB -> 8.2 MB (11.4x); 74,098 spans read
identically before and after; a second pass reports `compressed 0,
already 33`.
- Messages: 40,056 addresses, 225 MB of disk -> 12 packs (23.6 MB) + 3.9
MB index; all 40,056 read-backs match the stored plaintext; a real v2
artifact resolves identically from files and from packs.
- Read overhead: +0.01 to 0.11 ms for small and medium payloads, +0.45
to 0.49 ms for a 2.1 MB payload read with a 512 KiB cap; 40,056 packed
reads in 1.1 s.
- Migration speed: 4,973 files/s, 38 MB/s; full dry run 24.3 s; 40,056
messages packed in 1.7 s.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

Commands and results:

- `uv run pytest tests/test_tracing_store_compression.py
tests/test_trajectory_index.py tests/test_cli_tracing_commands.py
tests/test_tracing_api.py tests/test_subagent_acp.py
tests/test_trajectory_conversation.py tests/test_trajectory_details.py
tests/test_trajectory_selection.py tests/test_trajectory_bundle.py
tests/test_trajectory_export.py tests/test_rpc_cli_dispatch.py
tests/test_utils_mirrors.py tests/test_node_server.py
tests/test_tracing_message_pack.py -q -n 4`: 1127 passed, 1 skipped.
- Review-fix round, the affected tracing and trajectory suites plus the
CLI, RPC and kernel-closure tests, before and after the rebase onto
main: 978 passed, 1 skipped. Each of the six fixes has a regression test
that failed at the reviewed head and passes now.
- Third review round: each of the four fixes has a regression test that
failed at `57447ef8` and passes now, and removing each fix again (nine
mutants: no cursor adoption, adopting only for a key that vanished, no
moved-key lookup in the rescan, compressing the just-rotated log, no
mtime kept at rotation, the migration compressing the newest log, the
migration not keeping mtimes, an index-only packed check, the TUI floor
in the viewer's Node search) turns exactly its own tests red. `uv run
pytest tests/test_trajectory_index.py
tests/test_tracing_store_compression.py
tests/test_tracing_compression_edges.py
tests/test_tracing_message_pack.py tests/test_cli_tracing_commands.py
-q`: 188 passed, and the same files pass on each of the six fix commits'
own tree.
- Fourth review round: the writer now verifies that a pack still decodes
and covers a packed address's slice, so a pack cut short heals like a
missing one. The new test in `tests/test_tracing_message_pack.py` failed
before the change (the message files were not written again) and passes
now; the decoded length is streamed and cached per pack, so a healthy
store pays one decompression per pack rather than one per address. The
affected tracing, trajectory, CLI and RPC set passed: 1050 tests. Full
suite at the fixed head: 37157 passed, 8 failed - the same known local
baselines as before.
- `uv run pytest tests/integration/test_tracing_viewer_e2e.py -m
integration -q`: 28 passed.
- `uv run pytest tests/integration/test_kernel_wheel_smoke.py -q`: 1
passed.
- Full suite at the fixed head: 37157 passed, 8 failed, 255 skipped, 2
xfailed in 7:29, after the rebase onto `7c12b93f`. The eight are the
known local baselines on this box (five proxy-environment tests, the
Excel parser, the install script, and the node runtime probe); the same
eight IDs also fail at `57447ef8` here, and none of the new tests is
among them. The new index tests also pass with the test temp directory
on ext4, which reuses a freed inode at once, and on tmpfs, which does
not.
- `uv run ruff check` and `uv run ruff format --check` on the changed
trees: clean.
- `uv run --frozen --all-extras ty check raven/tracing raven/trajectory
raven/cli/tracing_commands.py raven/cli/tui_commands.py
raven/rpc/methods/cli_dispatch.py`: all checks passed.
- `uv run --frozen --all-extras lint-imports`: 10 kept, 0 broken.
- `scripts/check_commit_messages.py origin/main..HEAD`, `commitlint
--from origin/main --to HEAD`, `scripts/check_source_language.py
origin/main...HEAD` and `scripts/check_large_files.py
origin/main...HEAD`: exit 0.
- The three feature commits were each verified on their own tree before
the next one was built; the fourth commit bounds decode output and is
covered by the same batch above.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

- Security: local files only; no new network surface, credentials, or
permissions. A trace store can be imported from another machine, so
decompression is bounded rather than trusted: every decode path stops at
256 MiB (including the pack reader's offset skip), far above the 32.9 MB
largest captured payload, and treats an oversized or corrupt frame as
unreadable, the viewer passes `maxOutputLength` to its zstd decode, and
`resolve_payload` carries a per-shell total budget so a shell naming
many addresses cannot pull in more than one bounded amount. The pack
index is validated (40-hex addresses, integer offsets) before any path
is built.
- Backward compatibility: readers tell the encoding from the frame, so
plain and half-migrated stores read. Older code cannot read a migrated
payload, which is why both migrations are manual and the deployment
order is stated above. ACP journals are not migrated at all.
- Performance: measured read overhead is sub-millisecond; the pack cache
is capped at 32 MB per process and the index is 3.9 MB for 40,000
messages. A rotation compresses at most one earlier log, and finding it
among 2,191 archives took 9 ms.
- Rollback: the write path stores every new artifact compressed, so a
revert is only clean if no artifact has been written by this version.
After any new write or a migration there is no decompress command in
this PR; any current reader still returns the original plaintext, and a
decompress command can be a follow-up if one is wanted.

## Related Issues

N/A. The relationship to #1147 is described in Summary.
LivXue added 22 commits October 30, 2026 16:20
Span.identity() is public API for backends that write child spans
after the fact, and was missing from the tracing standard's Span
table. The ui-web glossary no longer calls the overview's rows lanes,
a word Raven uses for something else, and says why an agent's mark is
picked by its recorded name. The design doc says how a backfill reads
a journal now.
perfectionist/sort-imports reads the blank line between node:fs and
vitest in scripts/gates/trajectory-tokens-css.test.mjs as a group break
and fails eslint on it.
A running row whose record carries its last snapshot's end runs to the
present while that snapshot is fresh (entryEnd), but the incremental
tree still re-cut only the turns of rows without an end on a clock
tick. Such a turn kept the end it was built with while the one-shot
tree moved it with the clock. setNow now marks dirty the turn of every
row whose end reads the clock: one without an end, and a running one.

Pinned by the incremental tree equalling the one-shot tree after a tick
moves a running row that carries its snapshot's end.
Each figure says what it is made of: the session's two ends, the agents
split into main, external and in-process ones, the median turn, the
model's share of the turns' time, and the two tools called most. The
error count leads to the earliest failure, opening what holds it.

The figures are worked out in summary.ts from the tree the rows are
drawn from, calls counted once per span; clock.ts writes instants in
the reader's clock and the page's language.
…ndow

The ruler marks round clock times (ticks.ts), from tenths of a second
to a day, at the shortest step that leaves a label its room. None falls
inside a folded idle stretch, none prints over a neighbour a fold pulled
close, and the next unit up is major on the ruler and in the rows' grid.
Labels stand centred over short ticks. The head names the window on
screen -- its day, both ends, whole session or its length -- and carries
it as data-start and data-end. The elapsed-time marks are gone.
Fit, 3h, 1h and 15m set a window of that length about the middle of
the one on screen (presetWindow); a middle that falls in an idle fold
moves to the nearer side of it, so the window opens on work. A preset
is pressed while the window is that long, and a window the wheel made
presses none. Collapse all folds every agent and closes every turn.
The duration that stood just past each bar moves to a BUSY column at
the end of the label: a call's and a turn's own length, and an agent's
runs of work merged (busyRuns) rather than its lifetime. The axis keeps
2% of trailing scale instead of 15%, now that no label needs it.
With several agents each starts folded, one row each: a capsule per run
of work on a faint line from the first to the last, the main agent in
gold and every other in one neutral, a diamond where a failure
happened, an arrow at an edge its work runs past, and a chip naming
when it works next, or last did, once none of it is in the window. A
lone agent starts open. A row selected from elsewhere opens its agent
and its turn in one pass.
Once the window is narrowed, the workers whose work lies wholly before
it, or wholly after it, step into one row for each side ("2 agents
finished earlier"), their marks stacked; a fold opens in place, and a
row selected from elsewhere opens the fold that hides it. The main
agent always keeps its own row.
A worker's row is named by its task, its agent left to the mark and the
hover. Open, it adds a second line: the agent that ran it, the turn that
sent it and how many turns it took. A worker that ran inside the process
is tagged sub-agent. The main agent keeps its name and model.
…colour

A call row opens on a small square in its kind's colour, the same ink
as its bar and the turn's stretch of that kind: the model's violet and a
tool's teal, saturated and checked against both themes; a failure in the
error ink, anything else neutral. It is named by its tool, or by the
part of a model call it is, beside what the call worked on.
A turn row under the pointer opens a card under it: the turn's span and
length, its model and tool calls with their time and share, and the
waiting (turnBreakdown, read off the turn's own bar). The row names the
card as its description.
…strip

The strip shows the whole session's calls per bucket of real time, the
model's under the tools', a mark over a bucket holding a failure, and
each folded idle stretch with how long it was -- on an axis of its own
that gives every fold room to say so. The window is a brush: a drag
takes a new one, moves it, or moves one edge. The per-agent lanes, their
wheel handling and their two strings go.
Once a worker is open, a line runs from the instant it was sent, on its
dispatcher's row, to where its work begins: gold from the main agent,
the agents' neutral otherwise, with a dot where it leaves. Worked out
when the rows or the window change, never per scroll frame.
The design spec's page shape, levels, axis and a new section on the
window; the web UI page in both languages.
The light theme's glass goes from 70% to 85% white, so the rows read on
a near-white sheet while the wash still shows at its edges; the details
pane laid over it comes to about 91%. The dark theme is unchanged.
…nguage

An external agent reports each call's kind from ACP's closed set --
execute, edit, read and the rest -- and the header's top tools and the
call rows printed it as it came. toolLabel (palette.ts) gives each kind
its word in the catalogue; a tool's own name, the identifier the model
called, is shown as it is.
…ectory

The header split the agents into main, external and in-process ones,
and a row tagged an in-process worker as a sub-agent. An external
agent's run is a sub-agent like any other: the header counts the main
agent and its sub-agents, and no row is set apart, since a worker's mark
already says who ran it.
The pane sat in the row beside the waterfall, so opening it narrowed the
axis under the reader's eye. It is now a white card laid over the rows'
right side, below the header -- whose window presets and fold button
stay in reach -- and the waterfall keeps its whole width. The grip that
resizes it is the card's own left edge; under 800px the card covers the
area, with the back button, as the pane did. The glass pane's token,
--traj-pane, goes with the rule that used it, and the design doc's
sketch stops drawing the pane as a column beside the rows.

A stylesheet gate pins what happy-dom cannot see: the pane floats, on
the page's paper, with rounded corners, starting below the header by
the same variable that sizes the header.
The details card now moves and sizes the way the desk palette does. A
press on its head moves it once it has travelled 4px, so the head's
buttons keep their clicks, and the click that ends a drag is swallowed.
The corner sizes it from the edges that are free: the bottom-left where
the card opens, the bottom-right once moved. Let go within 34px of
where it opened, the card goes back there, filling the room under the
header again unless the reader made it shorter.

The store holds the reader's place and size inside the area, which the
island now measures down as well as across, and keeps them across a
conversation switch. Like the desk palette, the card sizes by its
corner alone: the grip on its left edge goes, and with it the arrow-key
resize and the grip's label. While the hand is down the card moves
by a transform and is placed on release: the waterfall and the pane's
views read the details store, and a write per pointer move would redraw
them every frame. The pane's overlay bar is parked in a fixed layer and
placed from the pane's box, so the card asks for it again whenever it
moves or changes size. The island's body and the waterfall now read only
whether the card is open and whether the area is narrow: read whole, the
store redrew every row under the card on each move of the corner.

Under 800px the card still covers the area and stays put. A stylesheet
gate pins what happy-dom cannot see: the height cap under the header,
the handle's corner, and touch-action on the head and the handle.
The frosted sheet under the details card is 85% white, so the card's
own white paper read as more of the sheet: its border was 8% ink, and
its shadow, pulled in by negative spreads, fell under the bottom edge
only. The card now takes the desk palette's edge -- the page's line
and the palette's shadow, on every side -- and its own line token
goes. A stylesheet gate pins both: the border is the page's line, and
no layer of the card's shadow has a negative spread.
A key-value list sized its key column between 96px and 160px, so a
reader who widened the details card to read a long key, such as
subagent.external.session_id, still saw it cut: the column stopped at
160px whatever the width. It is now a share of the card, 40% and at
most 320px, the same share in every section so the values still line
up, and capped so a short key in a very wide card stays near its value.
@LivXue
LivXue force-pushed the feat/trajectory_agent_waterfall branch from 1950ad7 to c70fd7f Compare October 30, 2026 16:25
@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1147 @ c70fd7f442607486a37d40c8aa53f9a6addc95d1

Scope: the branch is rebased onto current main (which now carries the merged #1125, #1141 and #1149); the PR's own delta since the previously reviewed head 1950ad79 is 18 commits, all documentation or ui-web trajectory work: a floating details card that can be moved and sized by hand, the glass/sheet restyle, an activity strip replacing the overview lanes, a clock-time ruler with window presets and a fold-all control, folded-agent capsules, per-row busy columns, hover breakdowns, dispatch lines and external-agent labelling. The raven/ changes inside the raw range belong to the already-merged main commits, not to this PR.

Windows angle: the new interaction is pointer-driven — a press has to travel 4 px before it counts as a drag so the head's buttons keep their clicks, the pointer is captured, move/up listeners hang off an AbortController, a release within 34 px snaps back, and the finishing click is swallowed — the same patterns reviewed in #1150 and standard Chromium behaviour on Windows. Nothing here adds a path, subprocess, signal, socket, encoding, or filesystem surface.

Verification: no executable check was possible — the shared D:/projects/Raven/ui-web/node_modules is empty (0 entries), so the new waterfall tests (summary, ticks, the drag tests) cannot run. The PR-own delta contains no Python.

Coverage gaps: the visual result (card drag/snap, the clock ruler, the activity strip) was not rendered in a browser here; the review rests on code reading.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

The rebased portion is patch-equivalent to the previously reviewed revision; I reviewed the 20 new trajectory UI/docs commits separately. I covered AGENTS.md and the updated ui-web/CONTEXT.md, the base-to-head diff and focused post-rebase delta, the affected stores/model/projection callers, commit history and range-diff, backward compatibility across the rebase, test changes for weakening/skips, and the documented waterfall architecture.

The only follow-up is the DST clock-ruler drift marked inline; it is a narrow display edge case and does not hold up this change.

Verification: npm test --prefix ui-web passed (272 files, 4591 tests); npm run type-check --prefix ui-web passed; npm run lint --prefix ui-web passed with 0 errors and 4 pre-existing warnings in untouched files; git diff --check github/main...HEAD, the source-language gate, and the large-file gate passed.

@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

A heads-up on the push after 1950ad7: it redraws the waterfall to the owner's design, adds what the owner asked for while trying it on a live page, and touches nothing the four threads or the T1 list fixed, apart from one fix. The branch is rebased onto main 9d9bec9.

  • The fix comes first: N5 made a running row whose record carries its snapshot's end run to the present, but the incremental tree still re-cut only rows without an end on a clock tick, so such a turn kept the end it was built with. It now equals the one-shot tree after a tick (incremental.test.ts).
  • Then twelve commits, one change each: header figures with a second line and a jump to the first error; a clock-time ruler that names its window; Fit/3h/1h/15m and Collapse all; a busy column; folded agents drawn as capsules (several agents now start folded); fold rows for agents outside a narrowed window; workers named by task; call rows keyed by colour; a turn's breakdown on hover; an activity strip with a brush in place of the per-agent overview lanes; the dispatch line of an open worker; docs.
  • Then seven from the owner's live test: a whiter frosted glass; an external agent's kinds of call named in the page's language; an external agent's run counted as a sub-agent; the details pane as a white card floating over the rows, which keep their whole width; the card moved by its head and sized by its corner, as the desk palette is, with no edge grip; the card's edge set apart from the glass; a key-value list's key column widened with the card.

Each commit was built and checked on its own (trajectory tests, tsc, eslint, check-css). The full ui-web suite, the gates and the Python checks are in the description's Verification.

The pane's overlay bar runs the length of the pane's own box, and once
the card is moved its size handle sits in the bottom-right corner, so
the bar's foot ran under the handle. The pane now stops a handle's
height short of the card's foot -- the same --wf-corner that sizes the
handle -- so the bar ends above the handle on either side. A stylesheet
gate pins both to the one variable.
The strip drew every folded idle stretch 36px wide whenever all of them
fitted, and fell back to a share of the working time when they did not.
For a long session the first rule left the working time a few dozen
pixels -- hour-long buckets, flat bars -- while a strip a little
narrower switched to the second and drew the same session in half-hour
buckets: opening the rail changed the strip's whole shape. A fold now
keeps its 36px only while the folds together take no more than a third
of the strip, and narrows past that, so the working time holds two
thirds of the strip or more at any width and the shape follows the
width smoothly. The rule moves into activity.ts as stripAxis, where it
is tested on its own, and the strip's comment, CONTEXT.md and the
design doc now say a fold has room for its length only while the folds
keep to a third of the strip.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

The two-commit delta from c70fd7f44260 is sound: the details pane now reserves its resize corner from the overlay scrollbar, and the activity strip's axis formula preserves at least two thirds of the plot for working time while retaining 36px idle folds when they fit. I checked the base-to-head record through the previously reviewed head plus this delta, the affected CSS/axis callers and history, compatibility with existing viewport behavior, the changed tests for weakening or skips, AGENTS.md and the updated ui-web/CONTEXT.md, and the documented waterfall architecture.

The existing DST clock-ruler drift remains the only named follow-up; it is unchanged and nonblocking.

Verification: the focused Vitest selection passed (3 files, 49 tests), TypeScript type-check passed, and ESLint passed for the changed TypeScript files.

@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1147 @ b6be90115ccd0bb07b31a8d1703d4f2afb921c2d

Scope: the full base-to-head diff (9d9bec90..b6be90115c, 99 files, +11689/−6325) is the trajectory-waterfall feature previously reviewed at c70fd7f442. The delta since that head is 2 commits / 7 files (+109/−35), all ui-web: waterfall/activity.ts, waterfall/ActivityStrip.tsx, waterfall/activity.test.ts, features/trajectory/styles.css, scripts/gates/trajectory-details-card-css.test.mjs, ui-web/CONTEXT.md, and one design doc. No raven/, Python test, install, or packaging file changes in the delta.

Windows angle: no platform-relevant surface was added. The change moves the activity strip's own axis into activity.ts::stripAxis — pure interval arithmetic (foldPx = min(FOLD_PX, room·⅓/folds), gapWidth = foldPx·working / max(1, room − folds·foldPx)) — and adds CSS keeping the details pane's overlay scrollbar clear of the resize handle (--wf-corner: 18px, margin-bottom on the pane when the card is not moved over). Nothing here touches paths, separators, argv, executable discovery, subprocesses, signals, sockets, file locking, encoding/CRLF, or filesystem semantics. The scrollbar rule is visual only: it reserves the same 18 px strip on Windows classic scrollbars and on overlay scrollbars, so there is no behavioural divergence between platforms.

Verification: reviewed the complete delta — git diff --name-only c70fd7f442..b6be90115c returns exactly the 7 files above, and no raven/ or test file changed since the previously reviewed head, so the Windows pytest results posted at that head still apply unchanged. No executable check was possible here: D:/projects/Raven/ui-web/node_modules contains only .vite-temp (the dependency tree is still absent), so vitest cannot run activity.test.ts or the CSS gate trajectory-details-card-css.test.mjs.

Coverage gaps: the new axis math and the scrollbar clearance are verified by reading only, not by running the vitest cases; the strip and details card were not rendered in a browser on Windows.

No Windows-specific issue found at this head.

This branch has not been deployed

No deployments
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.

3 participants