Skip to content

Say why each fork happened - #1

Open
jadenfix wants to merge 1 commit into
NovaSky-AI:mainfrom
jadenfix:skycap-check-port
Open

jadenfix wants to merge 1 commit into
NovaSky-AI:mainfrom
jadenfix:skycap-check-port

Conversation

@jadenfix

@jadenfix jadenfix commented Oct 17, 2026 •

Copy link
Copy Markdown

What does this PR do?

Names the cause of each fork in a skycap record, in the Tree, in /graph and in summary. TLDR: the viewer already says where a history forked; this says why.

This is the part of NovaSky-AI/SkyRL#2440 (skycap check) that the viewer did not already cover. That PR was closed with the suggestion that this inspection belongs here rather than in skycap; the forked flag, the branch points and summary --json already cover the rest.

How it works

Each branch's first node is compared with the earlier sibling it should have matched, and the difference is named:

  • resample: the model replied again to a history it had already answered
  • edited reply: the harness sent back a model reply with fields changed (e.g. reasoning_content dropped)
  • re-rendered: token mode, the same message rendered to different tokens
  • different message: a compaction, a subagent, a rewritten turn
  • tools or model: the same message sent with another tool set or model, which starts a new root

src/forks.js reads the document alone, walks only the roots and branch points, and normalizes messages the way skycap's match hash does (every field in text mode, the rendered fields in token mode, so client metadata such as provider_specific_fields is never named).

Usage

  • Tree: the first node of each branch carries a fork: <kind> badge; hover for the fields that changed and the sibling it was compared with.
  • /graph: each node has fork: {sibling_node_id, kind, detail} or null.
  • summary: a fork causes line, and stats.fork_kinds in --json, so a harness can be gated on the kind of fork it made and not only on forked == 0.

On the fixtures: the real Qwen3 records fork only by re-rendering (20 and 2), tr_forked_unbridged by stripped reasoning, tr_shared_reply by two different user follow-ups.

Test plan

  • test/forks.test.js (new): each kind on documents built the way skycap's graph commits them, the token-mode rules, the fixtures, and fork_kinds in the stats.
  • test/server.test.js: /graph carries the fork on the branch node and null elsewhere.
  • test/render.test.mjs: the tree shows one badge per branch.

Validation

node --test (Node 24.19): 124 passed / 4 failed with this change, 115 passed / 4 failed on main. The 4 failures are in test/app.test.mjs (route assertions about the groups fixture) and fail the same way on main; nothing here touches them.

Also checked by eye on test/fixtures: the badge sits on the row next to the other sign badges.

The tree, the /graph payload and summary --json already said where a
history forked, not why. Each branch's first node is now compared with the
earlier sibling it should have matched, and the difference is named:
resample, edited reply, re-rendered, different message, or tools or model,
with the fields that changed. The tree shows it as a badge on that node,
/graph carries it as the node's fork, and summary adds fork_kinds to the
stats so a harness can be gated on the kind of fork it made, not only on
forked == 0.
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