node_navigation_history_spec - mark-ik/graphshell GitHub Wiki
Date: 2026-03-18 Status: Implementation-ready spec Track: History subsystem β NodeNavigationHistory (Β§2.2 of unified architecture plan)
Related:
SUBSYSTEM_HISTORY.md-
2026-03-08_unified_history_architecture_plan.mdΒ§2.2, Β§3, Β§7.3 -
../../technical_architecture/2026-02-18_universal_node_content_model.mdΒ§5 -
edge_traversal_spec.mdβ TraversalHistory (separate track) -
history_timeline_and_temporal_navigation_spec.mdβ Stage F temporal navigation
NodeNavigationHistory records the address evolution of a single node over
its lifetime. Each time a node navigates to a new URL (same-tab navigation,
redirect, Back/Forward), one NavigateNode entry is appended to the WAL.
This is not the same as TraversalHistory:
| TraversalHistory | NodeNavigationHistory | |
|---|---|---|
| What moves | User navigates between nodes | URL changes within a single node |
| WAL entry | AppendTraversal |
NavigateNode |
| Truth carrier | Edge payload traversal records | Per-node WAL log |
| Surface | History Manager timeline | Node history panel |
| Archive | Traversal archive keyspace | Node-scoped query over WAL |
LogEntry::NavigateNode {
node_id: String, // UUID string of the node
from_url: String, // URL the node held before this navigation
to_url: String, // URL the node navigated to
trigger: PersistedNavigationTrigger, // reuse existing enum
timestamp_ms: u64, // wall-clock ms since UNIX epoch
}This is a new variant alongside (not replacing) UpdateNodeUrl.
Relationship to UpdateNodeUrl: UpdateNodeUrl is a snapshot-style
mutation entry that moves the node's canonical URL forward. NavigateNode
is a history-style append that additionally records the from_url so the
full address lineage is reconstructable from the WAL alone. Both may be
emitted for the same navigation event:
-
NavigateNodeis emitted first (to record the transition) -
UpdateNodeUrlis emitted after (to update the canonical URL field)
A future migration step may retire UpdateNodeUrl once WAL replay correctly
derives node state from NavigateNode entries. Until then both coexist.
LogEntry uses rkyv for serialization. Adding a new variant to the enum
extends the wire format. Existing WAL files written without NavigateNode
remain readable β rkyv's archived enum uses discriminant-based dispatch and
unknown discriminants should be handled at read sites with _ => continue
(already the pattern in timeline_index_entries and replay code).
The prototype accepts the schema change without a versioned migration.
NavigateNode is emitted from the same code path that currently emits
UpdateNodeUrl β the URL-change handler in graph_mutations.rs
(update_node_url or equivalent). The emit sequence is:
// 1. Record the navigation history entry
store.log_mutation(&LogEntry::NavigateNode {
node_id: node_id.to_string(),
from_url: current_url.to_string(),
to_url: new_url.to_string(),
trigger, // from the intent or Unknown if not available
timestamp_ms: Self::unix_timestamp_ms_now(),
});
// 2. Update canonical URL (existing path β unchanged)
store.log_mutation(&LogEntry::UpdateNodeUrl {
node_id: node_id.to_string(),
new_url: new_url.to_string(),
});The trigger field reuses PersistedNavigationTrigger. At the
UpdateNodeUrl call sites that don't have trigger context, use
PersistedNavigationTrigger::Unknown.
NavigateNode emission must route through the reducer. Render code and
direct field writes must not call log_mutation directly for navigation
events. The existing UpdateNodeUrl intent is the mutation boundary.
Add to GraphStore:
pub fn node_navigation_history(
&self,
node_id: &str,
limit: usize,
) -> Vec<LogEntry>Scans the log keyspace in reverse (newest-first), collects NavigateNode
entries where entry.node_id == node_id, and returns up to limit entries.
This is an O(log_size) scan with no secondary index. Acceptable for prototype
panel display (typical limit: 50β200 entries). If it becomes a performance
concern, add a secondary index keyspace node_nav_history:{node_id}:{seq} in
a follow-on step.
pub fn node_navigation_history_entries(
&self,
node_id: Uuid,
limit: usize,
) -> Vec<LogEntry>Delegates to store.node_navigation_history(&node_id.to_string(), limit).
The timeline_index_entries function (already extended in task A to cover
AddNode / RemoveNode) should also index NavigateNode entries:
ArchivedLogEntry::NavigateNode { timestamp_ms, .. } => (*timestamp_ms).into(),This means the Stage F scrubber will include node URL changes as timeline events alongside structural mutations and traversal events.
The node history panel is accessible from:
- The node pane context menu: "History"
- The History Manager pane: clicking a traversal row's "from" node opens that node's history panel in a split or overlay (future)
Each row shows one NavigateNode entry:
[time_label] [trigger_icon] [from_url short] β [to_url short]
-
time_label: human-relative (same format as History Manager rows) -
trigger_icon: same trigger icons as History Manager row rendering -
from_url short/to_url short: hostname or path-truncated display
Clicking a row navigates the node to that historical URL:
- Emits
GraphIntent::NavigateNodeToUrl { key, url: entry.to_url } - Does NOT enter preview mode (this is live navigation to a historical URL, not temporal graph preview)
The node history panel renders inside the node pane as a collapsible section or via a tab-like affordance within the existing node pane UI. It does not require a new ToolPaneState variant for the initial implementation.
-
WAL-only truth: Node navigation history is derived entirely from WAL
replay. It must not be stored as a separate in-memory
VeconNodeStateor any workspace field β this would create a second mutable store that drifts from WAL truth. -
Not in TraversalHistory:
NavigateNodeentries must not appear in the traversal archive keyspace. They are separate fromAppendTraversal. -
Not in dissolved archive:
NavigateNodeentries are not dissolved. They are permanent WAL records tied to the node's lifetime. -
Preview isolation:
NavigateNodemust not be emitted during history preview mode (history_preview_mode_active = true). The existingas_graph_mutation()block gate already coversUpdateNodeUrlintents β theNavigateNodeToUrlintent must also be classified as a graph mutation so it is blocked in preview. -
Reducer boundary:
NavigateNodelog writes occur only inside the reducer, not from the render or shell layer.
TraversalHistory and NodeNavigationHistory are complementary, not competing:
- TraversalHistory answers: "What path did the user take across nodes?"
- NodeNavigationHistory answers: "What URLs has this specific node visited?"
A user can navigate from Node A to Node B (one AppendTraversal entry on the
AβB edge) and then navigate Node B's URL three times within the same node
(three NavigateNode entries on Node B, no new traversal entries).
The mixed-history timeline (showing both tracks interleaved by timestamp) is
explicitly deferred per 2026-03-08_unified_history_architecture_plan.md Β§6.2.
-
LogEntry::NavigateNodevariant added withnode_id,from_url,to_url,trigger,timestamp_msfields - rkyv Archive/Serialize/Deserialize derived for new variant
-
timeline_index_entriesindexesNavigateNodeentries
-
NavigateNodeemitted byupdate_node_urlmutation path beforeUpdateNodeUrl -
NavigateNodeNOT emitted during history preview mode -
NavigateNodeusesunix_timestamp_ms_now()(not zero)
-
GraphStore::node_navigation_historyreturnsNavigateNodeentries for a givennode_id, newest-first, up tolimit - Empty result for nodes with no navigation history (no entries β error)
-
replay_to_timestamphandlesNavigateNodeentries correctly (appliesUpdateNodeUrl-equivalent state update if needed, or skips if navigation history is replay-read-only) - Pattern match sites with
_ => continuedo not panic onNavigateNode
- Node history panel renders
NavigateNodeentries in newest-first order - Each row shows time, trigger icon, fromβto URL display
- Row click emits
NavigateNodeToUrlintent (live navigation, not preview)
- Mixed-history timeline (TraversalHistory + NodeNavigationHistory interleaved)
- Per-node address history stored on
NodeState(WAL-only authority) - Node audit log (metadata change history) β see
node_audit_log_spec.md - Back/Forward in-session state β this is in-memory renderer state, not NodeNavigationHistory (which is durable WAL-based per-node address history)
- Archive/eviction policy for
NavigateNodeentries (deferred until volume makes it necessary)