Skip to content

feat(graph): temporal-decay traversal scoring — agent-memory recency (P1–P4)#146

Merged
TinDang97 merged 12 commits into
mainfrom
feat/graph-temporal-decay
Jun 5, 2026
Merged

feat(graph): temporal-decay traversal scoring — agent-memory recency (P1–P4)#146
TinDang97 merged 12 commits into
mainfrom
feat/graph-temporal-decay

Conversation

@TinDang97

@TinDang97 TinDang97 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Wires the dormant temporal-decay scoring engine to user-facing knobs — the agent-memory recency primitive. Paths through recently created edges now win over stale ones, on demand, on two surfaces:

GRAPH.QUERY g "MATCH p = shortestPath((a)-[*..5]->(c)) RETURN p" --decay 0.1 [--time-weight 2.0]
FT.NAVIGATE idx "*=>[KNN 5 @vec $q]" PARAMS 2 q <vec> HOPS 2 DECAY 0.1

Before this stack, shortest_path.rs hardcoded lambda = 0 — the scorers and Dijkstra composite-cost machinery existed but were unreachable (the reason the public docs claim was downgraded in PR #145).

Commits (one phase each, red/green TDD)

Commit Phase What
7590ef3 P1a MutableEdge.created_ms stamped at insert from the shard-cached clock (zero syscall); surfaced via MergedNeighbor
0e189fb P1b GRAPH.QUERY --decay <λ> [--time-weight <w>]ExecutionContextrun_shortest_path (all 3 callers); WeightedCostFn::cost_ms; strict validation
c525ba1 P2 FT.NAVIGATE ... DECAY <λ> — discovery-edge age penalty on graph-expanded hits
40e28f6 P3 CSR segment format v3: per-edge created_ms array survives freeze → disk → mmap → compaction; back-compat with v1/v2 files; new csr_from_bytes fuzz target
2c32d6a P4 Docs (guides/temporal.mdx, commands.mdx, README), CHANGELOG, script coverage (test-commands.sh 6 cases, test-consistency.sh 1/4/12-shard parity)

Semantics

  • λ is 1/seconds; per-edge cost becomes |weight| + λ·w·age_seconds (lower = better).
  • Decay off (no flag) is exact legacy behavior — the age term contributes zero to every edge cost, so path choice is identical to pre-decay Moon (stamps are still surfaced by the CSR neighbor iterator; the contribution is what's zero — confirmed zero-delta in the decay-off bench).
  • created_ms == 0 (pre-upgrade edge, pre-v3 segment file) is neutral — no age penalty, never "maximally old". Future stamps saturate to age 0 (clock-skew safe).
  • Transaction-time recency, fully distinct from the user-owned bi-temporal valid_from/valid_to (VALID_AT).

Latent durability bug fixed (found during P3 tracing)

compact_segments stamped merged segments version: 1 while to_bytes always writes 48-byte v2+ NodeMeta records. A vacuumed segment written to disk by recovery save misparsed on reload with the 32-byte v1 stride — panic in boomphf MPH build on debug builds, silent node_meta corruption on release. RED test (test_compacted_segment_serialize_parse_roundtrip) reproduced it; merged segments are now v3 and additionally carry per-edge stamps through dedup (winner-by-LSN keeps its stamp).

Known gap (deferred, documented)

GRAPH.ADDEDGE WAL records carry no created_ms, so not-yet-frozen mutable edges are re-stamped to replay time after restart. Least-harmful direction (newest edges look new); CSR-resident edges keep exact age. Noted in guides/temporal.mdx Limitations; fix would be an additive trailing WAL field — happy to do as a follow-up.

Validation

  • Full lib suite: 3462 passed (every phase landed with new red/green tests: scorer units, Dijkstra path-flip, dispatch-level knob + validation, NAVIGATE re-rank, format round-trips + v1/v2 back-compat)
  • Linux VM (OrbStack moon-dev) CI parity: fmt --check, clippy -D warnings (default + runtime-tokio,jemalloc), cargo test --release, cargo test --no-default-features --features runtime-tokio,jemalloc — all green
  • Live server verification of all 6 shell-test cases (path flip on/off + 4 validation errors)
  • CSR decay-off bench sanity (Linux VM, criterion baseline vs main): graph_neighbor_1hop_csr no change (1.0 ns); bfs_csr/1000 and /10000 within ±2% noise band (sign flips between runs); graph_csr_freeze_64k no change (4.77 ms vs 4.83 ms baseline, p = 0.25)
  • Docs link check (mint, node 22): clean for touched files
  • New fuzz target csr_from_bytes wired into both fuzz.yml matrices; locally verified on the Linux VM — cargo +nightly fuzz run csr_from_bytes -- -max_total_time=60: 1.5M runs, zero crashes (the fuzz-pr CI job needs the ci-fuzz label, applied to this PR)

Follow-up (after PR #145 merges)

docs/design-advantages.mdx episodic-memory claim can be upgraded from "AS_OF only" to include the now-user-facing decay knob.

Summary by CodeRabbit

  • New Features

    • Temporal-decay traversal scoring: GRAPH.QUERY --decay [--time-weight] and FT.NAVIGATE DECAY to bias paths toward recently created edges; edges are stamped with wall-clock creation times at insert.
  • Bug Fixes

    • Fixed compaction/durability bug so per-edge creation timestamps persist across freeze/disk/mmap.
    • Known limitation: mutable (unfrozen) edges may be re-stamped on restart.
  • Documentation

    • Updated docs, guides, README and changelog (v0.2.0-alpha).
  • Tests

    • Expanded tests and cross-shard consistency checks for decay behavior.

…Neighbor

Phase 1a of user-facing temporal-decay scoring (agent-memory recency).

- MutableEdge.created_ms: u64 — Unix-millis creation stamp, written inside
  MemGraph::add_edge from the shard-cached thread-local clock
  (storage::entry::current_time_ms — zero syscall on the insert path,
  1ms-tick staleness budget). No add_edge signature change, so all
  call sites (GRAPH.ADDEDGE handler, Cypher CREATE, WAL replay) inherit
  the stamp automatically.
- created_ms is distinct from valid_from (user-owned bi-temporal
  valid-time, defaults to 0) — documented on the field.
- MergedNeighbor.created_ms propagates the stamp through the segment
  merge reader. MemGraph edges carry their exact stamp; CSR edges report
  0 (= unknown -> decay-neutral) until P3 persists per-edge stamps —
  deliberately NOT the segment compaction LSN, which would make old
  edges look freshly created (freshness inversion).
- Tests: add_edge stamps from pinned cached clock (ClockPin RAII guard
  resets the thread-local on drop, panic-safe); merge reader propagates
  created_ms end-to-end.

Plan + verified trace facts: .planning/feat-graph-temporal-decay/CONTEXT.md

author: Tin Dang
Phase 1b: GRAPH.QUERY ... --decay <lambda_per_sec> [--time-weight <w>]
steers Cypher shortestPath() toward recently-created edges — the
agent-memory recency primitive. Decay off (no flag) keeps the exact
distance-only behavior (time term is zero, edge ages never read).

- scoring: WeightedCostFn::cost_ms — wall-clock cost variant; age is
  (now_ms - created_ms)/1000 so time_weight reads as cost per SECOND of
  edge age. created_ms == 0 (unknown: pre-upgrade edge or CSR without
  per-edge stamps) drops the time term — neutral, never maximally old.
  Future stamps saturate to age 0 (clock skew safe).
- scoring: DecayConfig { lambda_per_sec, time_weight, now_ms } +
  cost_fn() builder (distance weight stays 1.0 — decay biases, it does
  not replace weight-based shortest paths).
- ExecutionContext.decay: Option<DecayConfig> — rides the same pattern
  as valid_time_as_of (VALID_AT). Threaded through run_shortest_path
  (all three callers: MATCH-form, expression-form in read.rs, and
  eval.rs ShortestPathCall) and eval_expr (write-path callers pass None
  — CREATE/SET expressions do not traverse).
- Dijkstra + bounded DFS now cost via cost_ms(created_ms, weight); all
  pre-existing call sites use time_weight = 0.0 so behavior is
  unchanged when decay is off.
- parse_decay in graph_read.rs (pattern of parse_params): strict
  validation — finite non-negative numbers only, dangling flags
  rejected, --time-weight without --decay rejected. Applied to the
  read, write-aware, and PROFILE execution paths.

Tests (red/green): cost_ms unit semantics (sec conversion, zero-stamp
neutrality, future saturation); Dijkstra decay flips stale-direct vs
fresh-detour path choice; dispatch-level GRAPH.QUERY --decay changes
the chosen path, garbage values error, --time-weight combos validated.
test_dijkstra_composite_cost migrated to pinned-clock ms semantics
(same expected costs — seconds units match the old unitless numbers).

Full lib suite: 3451 passed. Clippy clean.

author: Tin Dang
Phase 2: FT.NAVIGATE idx <query> HOPS N [HOP_PENALTY p] [DECAY
<lambda_per_sec>] ages graph-expanded hits by the wall-clock stamp of
the edge that discovered them: final_score += lambda * age_seconds
(lower = better, consistent with the additive hop penalty and with the
GRAPH.QUERY --decay cost semantics). KNN direct hits (hop 0) have no
discovery edge and are unaffected.

- ExpandedResult.edge_created_ms — the BFS result already carried the
  discovery edge (Option<MergedNeighbor>) per visited node;
  expand_results_via_graph now surfaces its created_ms alongside the
  min-hop bookkeeping instead of discarding it. 0 = unknown -> neutral,
  matching WeightedCostFn::cost_ms semantics.
- parse_decay (DECAY keyword, HOP_PENALTY pattern): strict validation,
  finite non-negative only, dangling keyword or garbage -> Frame::Error.
  Reuses graph::scoring::DecayConfig (one knob vocabulary across both
  surfaces); now_ms from the shard-cached clock.
- build_search_args also strips DECAY so the synthetic FT.SEARCH never
  sees the keyword.

Tests (red/green): expansion carries the discovery edge's created_ms
end-to-end (pinned clock); decay re-ranks stale-edge hit below
fresh-edge hit at equal hop depth and is a no-op when absent; DECAY
parse accepts valid values, Ok(None) when absent, rejects nan/negative/
garbage/empty.

Full lib suite: 3454 passed. Clippy clean.

author: Tin Dang
Phase 3: per-edge wall-clock stamps now survive freeze -> CSR -> disk ->
mmap -> compaction, so temporal-decay scoring (GRAPH.QUERY --decay,
FT.NAVIGATE DECAY) sees true edge age instead of 0 = unknown once data
leaves the mutable segment.

Format (follows the v1 -> v2 NodeMeta version-bump precedent):
- v3 layout: header | row_offsets | col_indices | edge_meta | node_meta
  | edge_created_ms (u64 LE per edge, parallel to col_indices).
- Header gains edge_created_ms_offset at byte 80 (informational; eats
  zero padding, struct stays 128B compile-asserted). Parsers stay
  positional and version-gated with >= thresholds (48B NodeMeta at >= 2,
  stamp section at >= 3) so a future v4 still reads v3 fields.
- Back-compat: v1/v2 files parse with an empty stamp vec (= unknown,
  decay-neutral); readers only use checked access (.get -> 0). Both
  parsers (heap from_bytes, mmap from_mmap_file) updated, including the
  checked-arithmetic expected_len and the mmap zero-copy pointer with
  the same runtime alignment-check + heap-fallback pattern as node_meta.

Fixes a latent durability bug found while tracing producers:
compact_segments stamped `version: 1` while to_bytes always writes
48-byte v2+ NodeMeta records — a vacuumed segment written to disk by
recovery save misparsed on reload with the 32-byte v1 stride (panic in
boomphf MPH build on debug, silent node_meta corruption on release).
Merged segments are now v3, and MergedEdge carries created_ms through
the dedup (winner-by-LSN keeps its stamp), so vacuum no longer zeroes
edge recency.

- CsrStorage::for_each_neighbor_edge_ms — zero-alloc neighbor iteration
  that also yields the stamp; SegmentMergeReader CSR fill now surfaces
  real per-edge created_ms on MergedNeighbor (was hardcoded 0).
- New fuzz target csr_from_bytes (heap + mmap decoders, per repo rule:
  modified decoders need fuzz coverage), wired into fuzz/Cargo.toml and
  both fuzz.yml matrices.

Known gap (deferred, recorded in plan context): GRAPH.ADDEDGE WAL
records carry no created_ms, so WAL-replayed not-yet-frozen edges are
re-stamped to replay time after restart — the least-harmful direction
(newest edges look new); CSR-resident edges keep true age.

Tests (red/green): compacted-segment serialize/parse round-trip (RED on
the version bug before the fix); from_frozen populates stamps; v3
round-trip via from_bytes and via mmap (alignment-verified Mmap
variant); hand-built v2 bytes parse with empty stamps on both parsers;
v2 re-serialization is byte-stable; compaction carries stamps and dedup
keeps the winning edge's stamp; merge reader surfaces CSR stamps.

Full lib suite: 3462 passed. Clippy clean (default + tokio,jemalloc).

author: Tin Dang
Phase 4 of the temporal-decay stack: user-facing documentation and shell
test coverage for GRAPH.QUERY --decay / FT.NAVIGATE DECAY.

- guides/temporal.mdx: "Temporal-decay traversal scoring" section — cost
  formula, lambda units (1/seconds), decay-off exactness, 0 = unknown =
  neutral semantics, CSR v3 stamp lifecycle, and the transaction-time vs
  valid-time distinction. Limitations gains the WAL-replay re-stamp note.
- commands.mdx: decay examples + summary under the Graph section.
- CHANGELOG: 0.2.0-alpha "Added" entry covering P1a..P3, including the
  compact_segments version-stamp durability fix.
- scripts/test-commands.sh: TEMPORAL DECAY block (6 cases) — wall-clock
  stale-direct vs fresh-detour path flip on/off, strict validation
  errors for --decay/--time-weight/DECAY, and valid-DECAY pass-through.
  All commands verified against a live dev server (path render is one
  node id per line; detour detected via B's node id).
- scripts/test-consistency.sh: GRAPH.QUERY --decay path-flip parity
  captured across 1/4/12 shard configs inside the temporal loop.

Docs link check (mint broken-links, node 22): no broken links in the
touched files; pre-existing parse failures in comparison-valkey.md and
production-guide.md are unrelated (present on main).

author: Tin Dang
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4fc01d0-3beb-4aac-ae26-67ed1e6478cb

📥 Commits

Reviewing files that changed from the base of the PR and between ce68490 and 82a90c6.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/graph/csr/mmap.rs
  • src/graph/csr/mod.rs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/graph/csr/mmap.rs
  • src/graph/csr/mod.rs

📝 Walkthrough

Walkthrough

Implements temporal-decay traversal scoring end-to-end: per-edge created_ms stamping, CSR v3 per-edge timestamp persistence (heap + mmap), compaction-preserved timestamps, decay-aware cost functions and Cypher wiring, FT.NAVIGATE DECAY ranking, tests, docs, and fuzz/CI updates.

Changes

Temporal-Decay Traversal Scoring

Layer / File(s) Summary
Decay scoring model and core types
src/graph/scoring.rs, src/graph/types.rs
Adds DecayConfig, WeightedCostFn::cost_ms(), and node/edge created_ms fields plus GraphSegmentHeader.edge_created_ms_offset.
Edge/node stamping and ClockPin test helper
src/graph/memgraph.rs, src/storage/entry.rs, src/graph/visibility.rs
MemGraph::add_edge stamps edges with created_ms from cached clock; ClockPin test RAII pins thread-local clock for deterministic tests; test helpers updated.
CSR v3 format, serialization, and mmap loader
src/graph/csr/mod.rs, src/graph/csr/mmap.rs, src/graph/csr/storage.rs
CsrSegment stores edge_created_ms; to_bytes/from_bytes and header updated for v3 trailing per-edge timestamps; MmapCsrSegment maps and exposes per-edge u64 timestamps; CsrStorage adds accessors and neighbor iteration with ms; duplicate-node detection added.
Compaction preserves timestamps
src/graph/compaction.rs
compact_segments propagates per-edge created_ms through merge/dedup and writes CSR v3 header; tests ensure preservation and deduplication semantics.
Traversal algorithms use decay-aware costs
src/graph/traversal.rs
MergedNeighbor includes created_ms; SegmentMergeReader uses neighbor-ms iteration; BoundedDfs and DijkstraTraversal use cost_ms() for decay-aware scoring.
Shortest-path evaluation and evaluator threading
src/graph/cypher/executor/shortest_path.rs, src/graph/cypher/executor/eval.rs, src/graph/cypher/executor/mod.rs
run_shortest_path accepts optional decay; eval_expr signature extended and threads decay; ExecutionContext adds optional decay.
Read/write executor decay wiring
src/graph/cypher/executor/read.rs, src/graph/cypher/executor/write.rs
Read executor passes ctx.decay into eval_expr and run_shortest_path; write executor calls eval_expr(..., None) to remain compatible.
GRAPH.QUERY command parsing and tests
src/command/graph/graph_read.rs, src/command/graph/mod.rs, scripts/test-commands.sh
Adds parse_decay for --decay and optional --time-weight with validation; read/profile set ExecutionContext.decay; write queries reject decay; tests validate parsing, rejections, and shortest-path flip.
Vector search expansion and FT.NAVIGATE DECAY
src/command/vector_search/graph_expand.rs, src/command/vector_search/navigate.rs, src/command/vector_search/tests.rs
ExpandedResult includes edge_created_ms; expansion tracks discovery-edge timestamp; FT.NAVIGATE parses DECAY, strips it from synthetic FT.SEARCH args, and applies age penalty to expanded candidates; tests added for re-ranking and parsing.
Integration tests and cross-shard consistency
scripts/test-consistency.sh, scripts/test-commands.sh
Adds temporal decay scenarios and cross-shard consistency checks asserting expected path-flip behavior across shard counts.
Docs, CHANGELOG, fuzz target and CI workflow
docs/guides/temporal.mdx, docs/commands.mdx, README.md, CHANGELOG.md, fuzz/Cargo.toml, fuzz/fuzz_targets/csr_from_bytes.rs, .github/workflows/fuzz.yml
Documents decay options/semantics, updates changelog and README, adds CSR-from-bytes fuzz target exercising heap and gated mmap decoding, and updates CI fuzz workflow to use prebuilt cargo-fuzz with explicit target.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pilotspace/moon#70: Introduced initial graph scoring and traversal machinery extended here with temporal-decay capabilities.

Poem

🐰 I nibble timestamps in the night,
Fresh hops glow with clearer light.
Millis hum, old routes fall away,
Newer paths win the rabbit way.
Hooray — the graph prefers the day!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature: temporal-decay traversal scoring for agent-memory recency. It clearly conveys the primary change with phase indicators (P1–P4) showing a structured implementation approach.
Description check ✅ Passed The PR description provides comprehensive details: feature overview with command syntax, detailed semantics, commit breakdown across phases, bug fixes, known gaps, and extensive validation results. It exceeds template expectations but provides necessary context for a complex feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/graph-temporal-decay

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@TinDang97 TinDang97 added the ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR label Jun 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@fuzz/fuzz_targets/csr_from_bytes.rs`:
- Around line 21-22: The code currently calls
tempfile::NamedTempFile::new().unwrap() and
temp.as_file().write_all(data).unwrap() which can panic and produce non-target
crashes; change both unwraps to propagate a safe early return on error (e.g.,
use match/if let Err(_) { return; } or .ok()? and similarly handle the write_all
Result) so the fuzz harness quietly skips this input on setup failure; locate
the temp binding and the calls to NamedTempFile::new and
temp.as_file().write_all in the csr_from_bytes fuzz target and replace unwrap()
usages with early-return error handling.

In `@src/command/graph/mod.rs`:
- Around line 350-354: The current assertion compares the full debug-formatted
Frames (format!("{off:?}") vs format!("{on:?}")) which can differ for unrelated
fields; instead assert on the returned path payload itself: locate the test
variables off and on (the Frame results) and replace the debug-string comparison
with an assertion that the chosen route/sequence of node ids or the path length
differs (e.g., compare off.path vs on.path or map their nodes to ids and
assert_ne on that), ensuring you reference the Frame fields that represent the
route rather than the whole Debug output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7969687a-851e-49af-907b-86736f5b6c54

📥 Commits

Reviewing files that changed from the base of the PR and between d5e796d and 2c32d6a.

⛔ Files ignored due to path filters (1)
  • fuzz/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • .github/workflows/fuzz.yml
  • CHANGELOG.md
  • README.md
  • docs/commands.mdx
  • docs/guides/temporal.mdx
  • fuzz/Cargo.toml
  • fuzz/fuzz_targets/csr_from_bytes.rs
  • fuzz/fuzz_targets/rdb_load.rs
  • fuzz/fuzz_targets/resp_parse.rs
  • scripts/test-commands.sh
  • scripts/test-consistency.sh
  • src/command/graph/graph_read.rs
  • src/command/graph/mod.rs
  • src/command/vector_search/graph_expand.rs
  • src/command/vector_search/navigate.rs
  • src/command/vector_search/tests.rs
  • src/graph/compaction.rs
  • src/graph/csr/mmap.rs
  • src/graph/csr/mod.rs
  • src/graph/csr/storage.rs
  • src/graph/cypher/executor/eval.rs
  • src/graph/cypher/executor/mod.rs
  • src/graph/cypher/executor/read.rs
  • src/graph/cypher/executor/shortest_path.rs
  • src/graph/cypher/executor/write.rs
  • src/graph/memgraph.rs
  • src/graph/scoring.rs
  • src/graph/traversal.rs
  • src/graph/types.rs
  • src/graph/visibility.rs

Comment thread fuzz/fuzz_targets/csr_from_bytes.rs Outdated
Comment thread src/command/graph/mod.rs Outdated
…ay consolidation

Code-review pass over the temporal-decay stack (PR #146) surfaced one
correctness gap plus consolidation/test-quality issues; all fixed here.

Correctness:
- GRAPH.QUERY write queries (CREATE/SET/DELETE/MERGE) now REJECT
  --decay/--time-weight instead of silently ignoring them (and skipping
  their validation). The guard runs before LSN allocation / any mutation,
  on both the live auto-routing write branch (graph_query_or_write) and
  the pub API graph_query_write. Red/green tests at the dispatch level.
- The decay path-flip test asserted format!("{off:?}") != format!("{on:?}"),
  which is vacuously true: the stats row embeds execution_time_us, so any
  two runs differ. It now asserts node B's id appears in the decay-on
  path rows and not in the decay-off rows.

Consolidation (one home per invariant):
- DecayConfig::age_penalty_ms(): single home for the decay formula and
  the 0-neutral rule; FT.NAVIGATE re-rank delegates instead of inlining
  the formula (cross-surface drift risk gone). Unit-tested against
  cost_ms equivalence.
- CSR_CURRENT_VERSION const stamped at both segment construction sites
  (from_frozen, compact_segments) — stale-literal stamping is exactly
  the bug class behind the v1-stamp durability bug this PR fixed. Parse
  gates stay numeric (they encode feature-introduction versions).
- ClockPin test helper consolidated from 3 copies into storage::entry
  (cfg(test)); all raw tl_clock_set set/reset pairs in this PR's tests
  replaced with the panic-safe RAII pin (an expect() failure no longer
  leaks a pinned clock into the next test on the thread).
- downgrade_to_v2() test helper extracts the duplicated v2-bytes
  construction in csr tests.

Hygiene:
- navigate.rs parse_decay: dropped a to_owned() String alloc in command
  dispatch (house rule); the f64 parse now happens inside the borrow.
- to_bytes() debug_assert: edge_created_ms is empty or exactly parallel
  to col_indices.
- csr_from_bytes fuzz target: tempfile/write unwraps replaced with early
  returns (env failures are not findings); doc-comment warning fixed.

Docs/scripts:
- temporal.mdx + commands.mdx: write-query rejection documented, and the
  steer-vs-re-rank asymmetry made explicit (GRAPH.QUERY --decay steers
  the Dijkstra traversal; FT.NAVIGATE DECAY re-ranks an already
  discovered expansion whose BFS frontier is not decay-aware).
- CHANGELOG wording updated; test-commands.sh DECAY-07 covers the
  write-path rejection.

Deliberately unchanged (profile-first rule): cost_ms decay-off
short-circuit — the decay-off bench showed zero delta, so no hot-path
churn without a measured win.

PR #146
author: Tin Dang
…ecay

# Conflicts:
#	.github/workflows/fuzz.yml
#	fuzz/Cargo.toml
@TinDang97 TinDang97 added ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR and removed ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR labels Jun 5, 2026
…ocked build

`cargo install cargo-fuzz --locked` stopped compiling on the current
nightly: the lockfile pins rustix 0.36.5, which uses `rustc_*` attributes
the new compiler rejects as reserved ("attributes starting with `rustc`
are reserved for use by the `rustc` compiler"). All 10 Fuzz PR jobs died
in the install step in ~20s; main's own tip (f76901a) failed identically,
so the breakage is environmental, not from this branch.

Switch both fuzz jobs (PR + nightly) to taiki-e/install-action, pinned by
commit SHA (house style per 601b2a7), which downloads the released
cargo-fuzz binary: immune to toolchain drift and ~2 min faster per job.

PR #146
author: Tin Dang
@TinDang97 TinDang97 added ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR and removed ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR labels Jun 5, 2026
With the prebuilt cargo-fuzz binary the install step passes, but every
target then dies in the run step with E0463 "can't find crate for
`core`": cargo-fuzz builds the fuzz crate with -Zbuild-std (sanitizer-
instrumented std), which requires the rust-src component that the
minimal dtolnay/rust-toolchain@nightly profile does not include.

Note: the Fuzz workflow has zero successful runs in its history (28
failures / 12 skipped in the last 40) — this job has been broken since
introduction, independent of any PR. This commit is the second of two
repairs (binary install + rust-src); if more breakage remains beyond
these, it is tracked as follow-up work, not a gate for PR #146 (the
csr_from_bytes target is validated locally: 1.5M execs, zero crashes).

PR #146
author: Tin Dang
@TinDang97 TinDang97 added ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR and removed ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR labels Jun 5, 2026
…o musl

The actual root cause of E0463 "can't find crate for `core`" (visible in
the failed build command): the taiki-e prebuilt cargo-fuzz binary is
musl-linked and self-detects x86_64-unknown-linux-musl as the default
build target, whose std is not installed on the runner. Not a build-std
/ rust-src issue — revert that addition and pass an explicit
`--target x86_64-unknown-linux-gnu` to both fuzz run commands instead.

PR #146
author: Tin Dang
@TinDang97 TinDang97 added ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR and removed ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR labels Jun 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/storage/entry.rs (1)

41-50: 💤 Low value

Consider adding #[must_use] and documenting the non-nesting constraint.

The RAII pattern is correctly implemented, but two optional improvements would make it more robust:

  1. #[must_use] attribute: Prevents accidental immediate drops if ClockPin::set(...) is called without assignment (e.g., forgetting the let _pin = ... binding).
  2. Non-nesting caveat: Nested ClockPin::set() calls would not restore the outer clock values when the inner guard drops—the inner Drop resets to (0, 0) unconditionally. Current usage (per snippets) is non-nested, but documenting this prevents future footguns.
📝 Suggested improvements
 /// RAII pin for the thread-local cached clock in tests; resets to 0 on drop
 /// (panic-safe), restoring the syscall fallback. Single shared home so test
 /// modules don't each carry their own copy or leak a pinned clock into the
-/// next test on the same thread.
+/// next test on the same thread. Do NOT nest ClockPin instances—the inner
+/// drop resets to (0,0) instead of restoring the outer pin's values.
 #[cfg(test)]
+#[must_use = "ClockPin does nothing unless assigned to a binding (e.g., `let _pin = ...`)"]
 pub struct ClockPin;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/storage/entry.rs` around lines 41 - 50, Add the #[must_use] attribute to
the ClockPin type and document its non-nesting constraint: explain on ClockPin
(and its pub fn set) that the returned guard must be held (e.g., let _pin =
ClockPin::set(...)) to avoid immediate drop, and note that nested
ClockPin::set() calls are unsupported because the Drop implementation
unconditionally resets the clock (tl_clock_set) and will not restore an outer
guard's state; update the struct and the set() API docs accordingly so callers
are warned.
.github/workflows/fuzz.yml (1)

53-53: 💤 Low value

Consider pinning download-artifact to a commit SHA for supply-chain hardening.

The actions/download-artifact@v4 references are not pinned to a hash, while taiki-e/install-action is properly pinned. For consistency with the blanket policy noted by static analysis and to harden the supply chain, consider pinning these actions to a specific commit SHA.

🔐 Example pinning approach
-        uses: actions/download-artifact@v4
+        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8

(Choose the appropriate SHA for the v4 release you want to pin to.)

Also applies to: 102-102

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/fuzz.yml at line 53, Replace the floating tag
"actions/download-artifact@v4" with a pinned commit SHA for supply-chain
hardening; locate all occurrences of the string "actions/download-artifact@v4"
in the workflow and change them to the corresponding SHA-pinned ref (e.g.
"actions/download-artifact@<commit-sha>") so the action version is immutable and
consistent with how "taiki-e/install-action" is pinned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/fuzz.yml:
- Line 53: Replace the floating tag "actions/download-artifact@v4" with a pinned
commit SHA for supply-chain hardening; locate all occurrences of the string
"actions/download-artifact@v4" in the workflow and change them to the
corresponding SHA-pinned ref (e.g. "actions/download-artifact@<commit-sha>") so
the action version is immutable and consistent with how "taiki-e/install-action"
is pinned.

In `@src/storage/entry.rs`:
- Around line 41-50: Add the #[must_use] attribute to the ClockPin type and
document its non-nesting constraint: explain on ClockPin (and its pub fn set)
that the returned guard must be held (e.g., let _pin = ClockPin::set(...)) to
avoid immediate drop, and note that nested ClockPin::set() calls are unsupported
because the Drop implementation unconditionally resets the clock (tl_clock_set)
and will not restore an outer guard's state; update the struct and the set() API
docs accordingly so callers are warned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e46b911e-f0a8-4bf3-9205-fc92c9fb172f

📥 Commits

Reviewing files that changed from the base of the PR and between 2c32d6a and ce68490.

📒 Files selected for processing (18)
  • .github/workflows/fuzz.yml
  • CHANGELOG.md
  • README.md
  • docs/commands.mdx
  • docs/guides/temporal.mdx
  • fuzz/Cargo.toml
  • fuzz/fuzz_targets/csr_from_bytes.rs
  • scripts/test-commands.sh
  • src/command/graph/graph_read.rs
  • src/command/graph/mod.rs
  • src/command/vector_search/navigate.rs
  • src/graph/compaction.rs
  • src/graph/csr/mod.rs
  • src/graph/memgraph.rs
  • src/graph/scoring.rs
  • src/graph/traversal.rs
  • src/graph/types.rs
  • src/storage/entry.rs
✅ Files skipped from review due to trivial changes (3)
  • docs/commands.mdx
  • README.md
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/graph/memgraph.rs
  • fuzz/fuzz_targets/csr_from_bytes.rs
  • fuzz/Cargo.toml
  • src/graph/scoring.rs
  • src/command/graph/graph_read.rs
  • src/graph/types.rs
  • docs/guides/temporal.mdx
  • src/graph/compaction.rs
  • src/command/graph/mod.rs
  • src/command/vector_search/navigate.rs
  • src/graph/traversal.rs
  • scripts/test-commands.sh
  • src/graph/csr/mod.rs

… finding

The repaired Fuzz PR job found a real crash in csr_from_bytes after
217K execs: a corrupted/malicious segment file whose node_meta carries
duplicate external_ids feeds duplicate keys into MphNodeIndex::build,
which panics inside boomphf ("attempt to shift left with overflow",
boomphf-0.6.0/src/lib.rs:62). Recovery must fail with CsrError on
corrupted files, never crash.

Both decoders (heap from_bytes + zero-copy mmap loader) now detect the
duplicate during node_id_to_row construction — a free is_some() check on
the existing HashMap insert — and return
CsrError::InvalidData("duplicate node external_id ...").

Red/green: test_duplicate_external_id_rejected_not_panic byte-patches a
valid v3 segment to duplicate an external_id (checksum recomputed) and
asserts both parsers reject it; before the fix it reproduced the exact
boomphf panic. The CI crash input also runs clean now, plus a 5-minute
seeded local fuzz session on the fixed parser.

Note: the panic predates this PR (any v1/v2 file with duplicate ids
triggered it); it surfaces now because this PR added the fuzz target and
repaired the never-green fuzz CI job.

PR #146
author: Tin Dang
@TinDang97 TinDang97 added ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR and removed ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR labels Jun 5, 2026
@TinDang97 TinDang97 added ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR and removed ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR labels Jun 5, 2026
@TinDang97 TinDang97 merged commit 458ac5d into main Jun 5, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-fuzz Run the fuzz-pr CI job (15 min/target) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants