Skip to content

feat(neighbors): filter by state and merge time before ranking - #239

Open
masatohoshino wants to merge 2 commits into
openclaw:mainfrom
masatohoshino:feat/neighbors-state-time-filters
Open

masatohoshino wants to merge 2 commits into
openclaw:mainfrom
masatohoshino:feat/neighbors-state-time-filters

Conversation

@masatohoshino

@masatohoshino masatohoshino commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Closes #238

What Problem This Solves

Checking whether an open pull request was already landed by a later one means finding similar pull requests merged after it was created, but neighbors cannot tell a merged row from a closed one or filter by time, so the only route is --include-closed with a very large --limit plus a separate threads dump, about 18 s per check on a warm cache.

User Impact

User impact: gitcrawl neighbors <repo> --number <n> --merged-after <time> answers that question in about 1.4 s (median; p95 4.3 s) on a 165k-thread archive instead of about 18 s, and each row now shows its state, URL, author and times. Existing fields and their order, the default query, the schema and the default timing are unchanged; the new fields are additive.

Why This Change Was Made

neighbors gains three filters, each applied in SQL before vectors are scored and before --limit, so rows that rank below the limit are no longer lost:

  • --merged-after <time>: pull requests merged after the time.
  • --open-at <time>: rows open at the time (created by then, not closed by then).
  • --created-after <time|ref>: rows created after the time; a thread reference compares numbers, which GitHub assigns in creation order.

--merged-after and --open-at imply --include-closed and take a time; a pull request's creation time comes from gitcrawl threads owner/repo --numbers N --json | jq -r '.threads[0].created_at_gh' (accepting a thread reference there too would be a small follow-up if wanted). Each row additionally carries state, html_url, is_draft and, when set, author_login, author_association, created_at_gh, closed_at_gh, merged_at_gh and closed_at_local. The output also reports state_as_of: the start of the newest successful complete open or all sync, read from the existing closed_sweep_through in sync_runs; sync --numbers, --limit and --since runs do not move it. Without the new flags, the shared vector filter builds the same SQL and arguments as before.

The archive keeps current values, not history, so the filters can only err in one direction each: --open-at can include a row that was closed at that moment and later reopened, never drop one that was open, and --merged-after can only miss merges after state_as_of. These directions, and each row's state, assume closed-thread sweeps have covered the archive since its rows were stored (an archive's first sweep reaches back only to the earliest pull of a still-open row, or 24 hours; sync --state all refreshes every row). docs/clustering.md documents this.

Cold-cache runs gain less than warm ones: the time columns sit after body and raw_json in each row, so the filter still reads every row, and the saving comes from decoding and scoring only the rows that pass. The schema is left as is.

Evidence

Built CLI on a read-only copy of an openclaw/openclaw archive (165,815 threads, 3,306 open pull requests, state_as_of 2026-10-10T09:06:46Z, qwen3-embedding:0.6b 1024-dimension vectors), this branch's code against main bd30790. For 10 open pull requests (one per decile of later-merge count) and two questions each, the top 10 matched main's fastest equivalent in every case, warm and cold:

Per check (median) This PR main: --include-closed --limit 32765, then a local filter*
Merged after the PR was created, warm 1.35 s (0.58–4.64 s), 0.43 GB RSS 18.3 s, 3.3 GB RSS
Merged after the PR was created, cold 20.9 s 39.0 s
Open when the PR was created, warm 1.48 s (1.24–1.79 s) 18.3 s
Open when the PR was created, cold 26.9 s 38.0 s

* main's rows carry no state or merge time, so the filter also needs one threads --include-closed --json dump (992 MB): 5.5 s warm, 32 s cold. main's time does not depend on --limit (18.0–18.5 s at 1,000, 5,000 and 32,765), because it scores all 165,815 stored vectors on every call; above 32,765 the CLI fails with too many SQL variables.

  • All 3,306 open pull requests with --merged-after their creation time, warm: median 1.41 s, p95 4.29 s, max 5.75 s, no errors.
  • Default path unchanged, on a day-older copy (165,467 threads), main 15998d8 vs this code: neighbors 1.05 s vs 1.06 s, neighbors --include-closed 17.58 s vs 17.57 s (medians of 5, alternating builds); search, clusters and cluster-detail output byte-identical; the schema of a freshly initialized database is identical.
  • Error direction checked against GitHub on the same copy: 0 wrong of 1,100 --open-at rows, and all 1,100 --merged-after rows match GitHub's mergedAt.
  • Tests: TestNeighborsTimeFilters checks the new filters by the rows neighbors returns, including rows exactly at each boundary and times with a fraction of a second. Only the default path is pinned at the SQL level: TestThreadVectorWhereWithoutTimeFiltersIsUnchanged compares the filter's SQL and arguments with the previous implementation for all 16 combinations of the existing options, and TestNeighborsDefaultOutputGolden pins the default neighbors output (JSON and text). The golden files generated with main's code differ from these only by added lines for the new keys (94 added, 0 removed). The new tests fail on main and catch 23 deliberately wrong variants (for example >= instead of >, times compared as text, a no-op clause on the default path, or state_as_of moved by sync --numbers).
  • go test ./..., go vet, govulncheck, deadcode and make smoke/docs pass locally, except TestRefreshPortableStoreRecoversFromStaleIndexLock, which fails the same way on unchanged main on this machine (portable store checkout has local changes). gofmt -l lists only the untouched internal/cli/cloud_ingest.go, also on main.
CLI output: one check, warm cache (… marks omitted lines)
# gitcrawl built from openclaw/gitcrawl main bd30790, copy of an openclaw/openclaw archive
# (165,815 threads, 3,306 open PRs, last complete sync 2026-10-10T09:06:46Z), warm page cache.
# Question: which similar pull requests merged after https://github.com/openclaw/openclaw/pull/160462 was opened (2026-09-28T12:59:02Z)?
# main has no state or time filter, so the only route is every closed row up to the CLI maximum:
$ /usr/bin/time -f "wall=%e s  maxrss=%M KB" gitcrawl --config <scratch config> neighbors openclaw/openclaw --number 160462 --include-closed --limit 32765 --json
{
  "neighbors": [
    {
      "kind": "pull_request",
      "number": 141718,
      "score": 0.8144019645626563,
      "thread_id": 142625,
      "title": "fix(agents): prevent lost replies after compaction and yield"
    },
    {
      "kind": "pull_request",
      "number": 162260,
      "score": 0.8017532835880109,
      "thread_id": 160434,
      "title": "fix(agents): replies fail after timeout compaction when the user turn was already saved"
    },
    {
      "kind": "pull_request",
      "number": 152958,
      "score": 0.7961461451254146,
      "thread_id": 134069,
      "title": "fix(chat): continue messages through compaction"
    },
    …
  ],
  "repository": "openclaw/openclaw",
  "thread": { … }
}
wall=18.66 s  maxrss=3487240 KB
# 32,765 rows, without state or merge time. They still have to be joined with a separate
# `threads --include-closed --json` dump (992 MB) to keep pull requests merged after 2026-09-28T12:59:02Z.
# Top 10 after that local filter: [162260, 167900, 167016, 160211, 160128, 150623, 164860, 167505, 161799, 163117]
# gitcrawl built from this branch, same archive copy, warm page cache.
$ gitcrawl --config <scratch config> threads openclaw/openclaw --numbers 160462 --json | jq -r '.threads[0].created_at_gh'
2026-09-28T12:59:02Z
$ /usr/bin/time -f "wall=%e s  maxrss=%M KB" gitcrawl --config <scratch config> neighbors openclaw/openclaw --number 160462 --merged-after 2026-09-28T12:59:02Z --json
{
  "neighbors": [
    {
      "author_association": "CONTRIBUTOR",
      "author_login": "steipete",
      "closed_at_gh": "2026-10-01T14:48:12Z",
      "created_at_gh": "2026-10-01T01:33:06Z",
      "html_url": "https://github.com/openclaw/openclaw/pull/162260",
      "is_draft": false,
      "kind": "pull_request",
      "merged_at_gh": "2026-10-01T14:48:12Z",
      "number": 162260,
      "score": 0.8017532835880109,
      "state": "closed",
      "thread_id": 160434,
      "title": "fix(agents): replies fail after timeout compaction when the user turn was already saved"
    },
    {
      "author_association": "CONTRIBUTOR",
      "author_login": "steipete",
      "closed_at_gh": "2026-10-09T18:34:19Z",
      "created_at_gh": "2026-10-09T18:06:23Z",
      "html_url": "https://github.com/openclaw/openclaw/pull/167900",
      "is_draft": false,
      "kind": "pull_request",
      "merged_at_gh": "2026-10-09T18:34:19Z",
      "number": 167900,
      "score": 0.7709146360582034,
      "state": "closed",
      "thread_id": 165396,
      "title": "fix(agents): sessions get stuck after auto-compaction on small context windows"
    },
    {
      "author_association": "CONTRIBUTOR",
      "author_login": "steipete",
      "closed_at_gh": "2026-10-08T07:26:25Z",
      "created_at_gh": "2026-10-08T07:04:33Z",
      "html_url": "https://github.com/openclaw/openclaw/pull/167016",
      "is_draft": false,
      "kind": "pull_request",
      "merged_at_gh": "2026-10-08T07:26:25Z",
      "number": 167016,
      "score": 0.7587193443643813,
      "state": "closed",
      "thread_id": 164397,
      "title": "fix(codex): explain rejected turns when history compaction fails"
    },
    …
  ],
  "repository": "openclaw/openclaw",
  "state_as_of": "2026-10-10T09:06:46.372965691Z",
  "thread": { … }
}
wall=0.91 s  maxrss=374976 KB
# Top 10: [162260, 167900, 167016, 160211, 160128, 150623, 164860, 167505, 161799, 163117] (same numbers, order and scores as main after the local filter)

Add --merged-after, --open-at, and --created-after to neighbors. Each
takes a time (--created-after also takes an issue or pull request
reference) and is applied in SQL before vectors are scored and the limit
is taken, so rows that rank below the limit are no longer dropped by a
later filter.

Each row now carries state, html_url, is_draft and, when set, the
author, author association, and GitHub created, closed, and merged
times; the output also reports state_as_of, the start of the newest
complete open or all sync. Existing fields and their order are
unchanged. Without the new flags the vector filter emits the same SQL
and arguments as before, which a test pins against the previous
implementation, and golden files pin the default neighbors output.

docs/clustering.md describes how far row state and the time filters can
be trusted.
@clawsweeper

clawsweeper Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Oct 10, 2026
@clawsweeper

clawsweeper Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex review: blocked before merge.

What this changes

This PR adds three filters before neighbor ranking, thread metadata in results, and a complete-sync freshness timestamp.

Example: Find similar PRs merged after openclaw/openclaw pull request 160462 was created at 2026-09-28T12:59:02Z.

  • Before: Retrieve up to 32,765 neighbors and join a separate thread dump to identify later merges.
  • After: Use --merged-after 2026-09-28T12:59:02Z to return ranked candidates with merge times directly.

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) Strong shipped-CLI evidence and focused behavioral coverage support the patch; overall readiness remains capped by the unresolved product decision.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The supplied built-CLI trace exercises neighbors --merged-after on an existing 165,815-thread archive, returning matching ranked candidates with merge metadata and state_as_of; additional reported CLI comparisons cover open-at and unchanged defaults, and the later head changes only tests.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Product

Kind: Feature · Worth it: Needs a maintainer decision · Fix scope: Complete
User problem: Finding similar later-merged PRs requires scoring the entire archive and joining a separate thread dump, potentially losing eligible candidates below the retrieval limit.
Reason: The demonstrated benefit supports the feature, while the new public flags and output semantics require owner direction.

Merge readiness

⛔ Blocked before merge - 1 item remains

Keep this PR open: current main lacks these filters, the contribution demonstrates useful behavior, and no actionable introduced defect was found; the public CLI surface still needs owner direction.

Priority: P2
Reviewed head: 44f46b6549cbd98cbc62986fecf3eb6270c879cc
Owner decision: Required. See Decision needed.

Decision needed

  • Question: Should neighbors expose all three proposed filters and the additive thread metadata and state_as_of output contract?
  • Recommendation: Approve the proposed surface: Accept the opt-in filters and additive output with the documented archive-history limitations.
  • Why: The evidence establishes practical value, but no recorded owner decision authorizes the new public flags and freshness semantics.

Before merge

  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.

Findings

None.

Agent review details

How this fits together

The neighbors CLI reads cached SQLite thread embeddings, selects eligible vectors, ranks similarity, and returns hydrated thread metadata alongside recorded sync freshness.

flowchart LR
  A[CLI arguments] --> B[Neighbors command]
  C[SQLite archive] --> D[Filtered vectors]
  B --> D
  D --> E[Similarity ranking]
  E --> F[Neighbor output]
  C --> G[Sync freshness]
  G --> F
Loading

Technical review

Best possible solution:

Add approved filters within the existing neighbor query while preserving default selection, scoring, storage, and documented history limitations.

Do we have a high-confidence way to reproduce the issue?

Current-main source confirms the missing filters and metadata, and supplied baseline CLI output demonstrates the expensive workaround; this review executed no target code.

Is this the best way to solve the issue?

Filtering in the existing SQL query before decoding and scoring directly addresses the workload without a competing implementation or storage migration.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against bd30790d7735.

Provenance checked

Testing

Proof path: shipped entry point.

Security

None.

Evidence

What I checked:

  • Current main lacks the requested capability: The base command accepts number, limit, threshold, and include-closed; neighbor rows contain identifiers, kind, title, and score without state or merge timestamps. (internal/cli/neighbors.go:19, bd30790d7735)
  • Introduced filters use the existing query owner: Parameterized time and number predicates restrict candidates before vector decoding, scoring, and limiting; unset filters retain the base predicates. (internal/store/vectors.go:199, 44f46b6549cb)
  • Freshness follows complete successful acquisition: StateAsOf excludes checkpoints, failed runs, and targeted scopes; the existing sync producer records closed_sweep_through only after unrestricted successful acquisition. Documentation explains reopening and legacy-sweep limitations. (internal/store/runs.go:207, 44f46b6549cb)
  • Supplied production CLI proof: The full PR body supplies baseline and changed CLI terminal output on an existing 165,815-thread archive, showing matching ranked candidates with merge metadata and state_as_of. The later commit changes only tests, so this production-path evidence remains applicable. Captured body sourceRevision: 0c62a258ef17a3c1be4ec80b03581c2b11e7d0e8fe97a6249fc5b0f1c5e55e7f. (c9310e35663a)
  • Re-review test improvement: The head removes the test that mirrored new SQL fragments and adds returned-row boundary and fractional-time cases; the narrow legacy-query preservation oracle remains. (internal/cli/neighbors_test.go:65, 44f46b6549cb)
  • Feature history and original intent: REST commit patches verify neighbor lookup in 312ee8e and embedding recovery in 0f9386e. refactor: organize CLI commands by responsibility #188 records a behavior-preserving extraction; fix(sync): preserve completed items when another hydration fails #200 distinguishes retry checkpoints from successful freshness. Local content-history search encountered a missing promisor blob; REST reads recovered the relevant introduction evidence. (internal/store/vectors.go:185, 312ee8ed5eac)

Likely related people:

  • Vincent Koc: Raw commit 312ee8e adds internal/store/vectors.go:164 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 312ee8ed5eac; files: internal/store/vectors.go)
  • Peter Steinberger: Raw commit 777a404 adds internal/cli/neighbors.go:233 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 777a404d28a7; files: internal/cli/neighbors.go)

Review metrics

Metric Value Why it matters
Patch distribution Production +166/-17; tests +601; fixtures +342; documentation and changelog +30/-1 Most growth supports returned-row behavior and default-output compatibility; production changes remain in existing CLI and store owners.

Labels

Label changes:

No label changes.

Label justifications:

  • P2: A useful archive-search improvement with demonstrated benefit on large repositories and limited operational blast radius.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • proof: sufficient: Contributor real behavior proof is sufficient.

Rating scale

6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior.

Workflow

ClawSweeper edits this one comment on every review. Comment @clawsweeper re-review for a fresh review only; repair and merge need explicit maintainer commands such as @clawsweeper autofix or @clawsweeper automerge.

History

Review history (1 earlier review cycle)
  • reviewed 2026-10-10T16:12:49.464Z sha c9310e3 :: blocked before merge. :: none

Reviewed October 10, 2026, 12:56 PM ET / 16:56 UTC (Revision 2).

Drop TestThreadVectorWhereAppendsOnlySetFilters, which restated the SQL
fragments of the new filters. The boundaries it guarded are now rows of
TestNeighborsTimeFilters checked by what neighbors returns: a row created,
closed or merged exactly at the given time falls on the documented side
of each filter, and each filter compares instants, not text, for a time
with a fraction of a second. TestThreadVectorWhereWithoutTimeFiltersIsUnchanged
still pins the default query.

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

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

neighbors cannot tell merged rows from closed ones or filter by time, so a superseded-PR check takes ~18 s per PR

1 participant