Skip to content

feat(mcp): report truthful index freshness from checkout evidence - #1561

Open
tmonestudio wants to merge 1 commit into
DeusData:mainfrom
tmonestudio:codex/bt-240-truthful-index-freshness
Open

tmonestudio wants to merge 1 commit into
DeusData:mainfrom
tmonestudio:codex/bt-240-truthful-index-freshness

Conversation

@tmonestudio

Copy link
Copy Markdown
Contributor

What does this PR do?

Makes verbose index_status distinguish the live checkout from the generation that was actually indexed.

  • persists indexed_checkout_sha only at the staged-generation publication boundary
  • reports bounded tracked/untracked checkout evidence
  • returns machine-readable current, stale, or fail-closed unknown
  • preserves the lean default response and legacy databases
  • handles Windows porcelain-v2 rename/copy records and bounded git subprocess failures

This is intentionally separate from #1181 and #1065.

Local verification

  • production Windows/MinGW build passed with the repository warnings-as-errors flags
  • focused test runner: git_context mcp store_nodes exited 0
  • existing database smoke: 27,369 nodes / 78,605 edges; legacy generation returned unknown/indexed_checkout_unavailable
  • database SHA-256 was identical before and after the smoke

Checklist

  • Every commit is signed off (git commit -s)
  • Full test suite is delegated to CI
  • Focused tests pass locally
  • New behavior is covered by reproduce-first tests

No runtime binary, cache, ACL, or corpus was modified.

@tmonestudio
tmonestudio requested a review from DeusData as a code owner August 12, 2026 04:21
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData

Copy link
Copy Markdown
Owner

Thank you @tmonestudio. The underlying issue is confirmed on current main: verbose index_status reports live checkout context, while the indexed generation stores no corresponding checkout identity. Persisting identity at the publication boundary is the right contract to review.

This is a broad correctness change despite the focused product claim: 11 files and about 1,300 added lines across git subprocess handling, MCP output, pipeline publication, store migration, and tests. It therefore needs a full storage/schema and fail-closed review rather than a quick UI pass. The current head is mergeable, but lint / lint and consequently ci-ok are red and the main test matrix was skipped. Please clear the lint failure before review.

Thank you for the reproduce-first coverage and legacy-database checks. The queue is full, so detailed feedback may take some time, but this is labeled and queued.

@DeusData

Copy link
Copy Markdown
Owner

Checking in — this has been quiet since 12 August and it is still open and still wanted, so here is where it stands with the friction removed.

The lint blocker is six clang-format violations in two files:

src/store/store.c:3171:94
src/store/store.c:3172:34
src/mcp/mcp.c:4504:80
src/mcp/mcp.c:4505:57
src/mcp/mcp.c:4505:78
src/mcp/mcp.c:4506:60

make -f Makefile.cbm lint-format reproduces it, and running clang-format over just those two files fixes it. One caveat that has bitten people here: it must be the Homebrew LLVM build — a standalone clang-format-20 produces spurious whole-file drift.

And an honest note on the rest, so the delay does not read as all yours. Even with lint green, this is not a fast merge: 11 files and ~1,300 lines spanning git subprocess handling, MCP output, pipeline publication, store migration and tests. It needs a full storage/schema and fail-closed review, and that review is ours to do and has not happened yet. Clearing lint is what lets the main test matrix actually run — it was skipped entirely on the last CI pass, so nobody has seen this change tested.

The underlying problem you identified is confirmed on current main and the contract you proposed — persisting checkout identity at the publication boundary — is still the right shape. Your reproduce-first coverage and the legacy-database checks were noted at the time and still stand.

If you have moved on, say so and we will take it from here rather than leaving it to age. If not, clear the lint and the matrix will finally have something to say.

@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

An update, and a decision that went in your favour.

#1727 independently added a top-level freshness to index_status as well — a string (fresh / stale / unknown) rather than your object, derived from clean Git snapshots taken before and after indexing. Neither of you could have seen the other: the PRs are independent, and git would have merged them without a conflict, leaving index_status with one key emitted twice at different types depending on merge order.

Your object stands. The reasoning is that a caller which learns why the graph is not current, and what to do about it, is better served than one that gets a single word — your reason, reasons[] and recommended_action are the difference. This PR is also standalone and already reviewed, so it does not wait on a six-PR stack. #1727 has been asked to drop its field and keep its last_index_attempt record, which is genuinely additive to yours.

One idea from that PR worth folding into yours if you touch the docs: "a dirty tree is never treated as evidence in either direction". Your implementation already behaves that way — a dirty tree cannot reach current because tracked changes force stale, and status_unavailable prevents a confident answer — but that sentence states the principle more clearly than anything currently written down here.

Where this stands otherwise: the storage/schema and fail-closed review you were owed is done and it passes — the three-DB-state migration (fresh, legacy-writable, legacy read-only) and the verdict ladder that never reaches current without indexed SHA, live HEAD, status availability and zero tracked/untracked. The only thing between this and merge is a clearance marker on the three cbm_popen calls in your test files, which is a maintainer step, not something you need to change.

Thanks for your patience — this has been open a long time, and it is the design that is being kept.

@DeusData

DeusData commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Clearance is done and I've now verified this end to end on today's main rather than on the August base: trial-merged onto fe85a6b2, built with ASan/UBSan, and ran git_context (10/10, seven of them yours), store_nodes 71/0, index_resilience 6/0, daemon_frontend 13/0, mcp 318/0, lint clean; then with the real binary in an isolated cache: index → current with the SHA matching HEAD, tracked edit → stale / tracked_changes_present, commit → stale / indexed_checkout_mismatch, untracked-only → unknown, re-index → current with the new SHA. The revert-check binds exactly where it should (never reading the persisted SHA reddens the store round-trip and four MCP verdict tests; dropping the tracked≠stale rule reddens the tracked tests and rename_counts_once). The fit is right too: one top-level freshness object, nothing else on the surface beyond the nullable indexed_checkout_sha column with its idempotent migration.

Two things are needed before it merges, both mechanical, and both caused by main moving under you:

  1. One conflict, src/mcp/mcp.c:44-53 (the enum block): your branch adds MCP_STATUS_SAMPLE_MAX / MCP_REASONS_MAX, main added MCP_QUERY_MAX_VISIBLE_ROWS / MCP_OUTPUT_BYTES_PER_TOKEN_ESTIMATE in the same spot. Keep both.
  2. Nine tests need "format":"json" in their index_status call: feat: make CLI and MCP output lean by default #1597 (merged this week) made the tree format the default for tool output, so JSON-substring assertions no longer match the default response. The calls are at tests/test_mcp.c:5711, 5736, 5843, 5881, 5937, 5988, 6046, 6076, 6131; main's own tool_index_status_includes_git_metadata shows the pattern. With that, mcp is 318/0. Line 5736 then runs past 100 columns, so run clang-format (the Homebrew LLVM one) over the file.

Non-blocking, take or leave: the verbose description at src/mcp/mcp.c:680-683 still says only "add worktree/shadow Git paths" — worth a clause about freshness; and recommended_action maps every unknown to reindex_to_record_indexed_checkout, which misleads for untracked_not_indexed / status_unavailable where the identity is recorded — a follow-up is fine.

Keep the sign-offs on the rebased commits and this merges on green. Thanks for the fast turnaround on the lint fix in August and for sticking with it.

@tmonestudio
tmonestudio force-pushed the codex/bt-240-truthful-index-freshness branch from ca7442b to b939a2d Compare September 7, 2026 20:26
@tmonestudio

Copy link
Copy Markdown
Contributor Author

Thanks for the CI report. I traced both failures to the same underlying issue: ci-ok was only aggregating the failed Unix shard, whose sole failing suite was complexity.

The failure was scheduler-sensitive, not a freshness regression: the shared-package scaling test observed nodes 244 -> 842 (ratio 3.45) while edges stayed 659 -> 1319 (ratio 2.00), exceeding the 2.75 node-ratio guard under the parallel wave. Re-running the suite locally produced the expected nodes 244 -> 484 (ratio 1.98) consistently.

Fix pushed in commit 6875483f: classify complexity as a serial/quiet-tail suite in scripts/run-tests-parallel.sh, alongside extraction, because both measure replicated-corpus scaling and are sensitive to concurrent allocator/CPU contention. No product logic or freshness behavior was changed.

Local verification on the updated source:

  • git_context: 7 passed, 3 platform skips
  • store_nodes: 71 passed
  • index_resilience: 6 passed, 1 platform skip
  • mcp: 315 passed, 11 platform skips
  • complexity: 5 passed repeatedly

Please rerun the Unix shard/CI checks for 6875483f.

@DeusData

DeusData commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Thank you for tracking down the failing shard and recording both the parallel and isolated counts. That evidence is useful.

Please separate the serial-suite workaround in 6875483 from this freshness PR. The reported change from 484 to 842 nodes is a difference in graph contents, not just elapsed time; moving the suite to the quiet tail may avoid the symptom, but does not yet establish whether the cause is shared test state, extraction nondeterminism, or another defect. Please preserve the failing run and fixture details so we can investigate that discrepancy separately rather than treating a serial green run as a correctness fix.

This does not withdraw our agreement on the freshness design. We also owe you the final integration review after our earlier merge commitment. Thank you for the repeated updates and your patience.

@tmonestudio
tmonestudio force-pushed the codex/bt-240-truthful-index-freshness branch from a7941a4 to e0b9642 Compare September 13, 2026 00:52
Signed-off-by: tmonestudio <tmonestudio@users.noreply.github.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for splitting the complexity-suite workaround out as we asked. The branch is now the freshness change alone, which is the version we want to merge.

main has moved on since, and the branch now conflicts in two files: src/pipeline/pipeline.c and src/pipeline/pipeline_internal.h. Most of the churn comes from today's markdown-link pass (#1832) and the memory-core work. Could you rebase onto the current main? Everything else merges cleanly.

One heads-up so it doesn't surprise you: a rebase produces a new head, so we re-run our security clearance and verification on it before merging. That is routine and nothing you need to do. The design agreement from 2026-09-01 (your freshness object is the one that ships) still stands. Thank you for your patience with how long this one has taken.

DeusData added a commit that referenced this pull request Sep 30, 2026
#2167)

The background watcher only watches the project an open MCP session is
rooted in, and drops the watch when the last session for that project
closes. A repository indexed with `cli index_repository` (or from a
session rooted elsewhere) is never watched, but README promised
"Auto-sync keeps it fresh after that" for every index_repository target
and the MCP instructions said "Indexes auto-refresh". Nothing in the
status output said which projects were actually watched, so a stale
CLI-indexed repo looked like a watcher bug.

What gets watched is unchanged. Instead:

- index_status gains a `watch` object: `watched`, plus `strategy`,
  `poll_interval_ms` and `last_scan_at` when watched, or a `reason`
  (not_session_project, cli_session, auto_watch_off, watcher_disabled,
  not_registered, no_watcher) and a re-index hint when not. It does not
  touch the #1561 `freshness` key.
- The watcher publishes per-project strategy, cadence and last completed
  scan time through atomics and exposes them via
  cbm_watcher_project_info(); the daemon answers index_status through a
  session-scoped provider.
- README, docs/CONFIGURATION.md, docs/index.html, the index_status tool
  description and the MCP instructions now state the real scope.

`watcher_enabled` already shows in `config list` since v0.11.0 (680bc72).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
TFSebben pushed a commit to TFSebben/codebase-memory-mcp that referenced this pull request Sep 30, 2026
…a#2144)

index_repository blocks until the whole index completes. MCP clients with
a per-call deadline (Copilot for IntelliJ) give up on a large repository;
their cancel drops the daemon job's last subscriber, the daemon cancels
the worker, and every retry starts over, so the index never finishes.

Add an opt-in async mode and a status query to the same tool; the default
synchronous behaviour is unchanged:

- async: true starts the project's index job in the daemon, or joins the
  one already running for it, and returns at once with its state. The
  application holds one subscriber reference for an async job until its
  terminal publish, so the request returning, the client cancelling or
  the starting session closing no longer cancels it. Only daemon shutdown
  (or the final session of a non-permanent generation) ends it. Async
  requests never queue behind the physical job limit; a full daemon
  answers busy. async/status are stripped from the worker args, so an
  async request coalesces with an identical synchronous one.
- status: true reports the running or last job for the project
  (queued/running/cancelling/succeeded/failed/cancelled, started_at,
  finished_at, error summary) from a small per-project record kept in the
  daemon's job registry after the job itself is reaped. An unknown
  project is an error; a project indexed before this daemon generation
  reports idle. No freshness key is added: index_status keeps the DeusData#1561
  freshness object.
- async + status together, non-boolean values, and async with
  cross-repo-intelligence are refused. In-process servers (index worker,
  embedders) refuse both modes: nothing there outlives the call.
- On a temporary (non-permanent) daemon, async is refused when the
  request arrives on the one-shot `cli` tool channel and that session is
  the daemon's only live session: the daemon would stop and cancel the
  job the moment the command exits. The error points at `daemon start`
  (or an open MCP session). A long-lived MCP session stays a valid host
  even when it is the only one, which is the IDE case this exists for;
  status stays allowed everywhere. The daemon lifecycle is unchanged.
- New allocations go through the memory core (cbm_alloc/cbm_calloc/
  cbm_mem_strdup/cbm_free); blocks returned by non-core APIs are released
  with safe_free, so no file's raw allocator count grows.
- A cut-short synchronous call now offers the async alternative: in the
  cancellation text the daemon or frontend delivers (including the
  -32800 reply to a cancelled index_repository), and, because a timed-out
  client never reads that reply, as a notice on the next index_repository
  or status call for the project.
- The tool description and schema, the CLI help and the README explain
  the flags, the deadline problem and the polling pattern.

No server-side timeout or budget is introduced.

Tests (deterministic, held fake worker, bounded observation waits):
async returns before completion and status observes running then
succeeded; an async job survives its session closing while a sync job in
the same shape is still cancelled; validation errors; the cancellation
reply and the next-call notice carry the advice; a lone one-shot client
of a temporary daemon is refused async while status and an MCP session
on the same daemon are allowed; in-process refusal; schema; notice
helper.

Refs DeusData#2031

Fixes DeusData#2144

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit that referenced this pull request Sep 30, 2026
A project indexed from a directory that is not a git repository was never
refreshed after its first index: init_baseline classified it is_git=false
and poll_project/check_changes returned early for it forever, so the graph
silently went stale. The comment in init_baseline even claimed such folders
were "polled as a plain directory", which was never true.

Add an opt-in config key, watch_non_git (default false, read once at daemon
startup like watcher_enabled). When on, a non-git root is polled on the
existing adaptive cadence (cbm_watcher_poll_interval_ms) by a tree
signature: the indexer's own cbm_discover walk (same skip lists, .gitignore
stack and .cbmignore), sorted by path and folded over (relative path, size,
mtime). A changed signature takes the existing reindex path with the same
commit-after-success discipline as the #937 dirty signature. The baseline
leaves the signature "unknown", so the first poll reindexes once
(at-least-once, covering edits made while the daemon was down).

Runaway guard: only files discovery would index enter the signature, so
cbm's own .codebase-memory output (built-in skip) and the cache directory
(pruned by absolute path) can never make a finished reindex look like a
new change; a scratch folder under an unrelated dirty repository is polled
on its own files and never inherits the ancestor's dirty state.

cbm_file_info_t gains mtime_ns, filled from the stat discovery already
performs, so the signature needs no second stat per file and stays
UTF-8-correct on Windows (discovery's wide stat).

Docs: README and docs/CONFIGURATION.md now say non-git roots are not
watched by default and document watch_non_git. The staleness signal in
index_status is left to #1561's freshness object. Refs #1948.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit that referenced this pull request Sep 30, 2026
#2167)

The background watcher only watches the project an open MCP session is
rooted in, and drops the watch when the last session for that project
closes. A repository indexed with `cli index_repository` (or from a
session rooted elsewhere) is never watched, but README promised
"Auto-sync keeps it fresh after that" for every index_repository target
and the MCP instructions said "Indexes auto-refresh". Nothing in the
status output said which projects were actually watched, so a stale
CLI-indexed repo looked like a watcher bug.

What gets watched is unchanged. Instead:

- index_status gains a `watch` object: `watched`, plus `strategy`,
  `poll_interval_ms` and `last_scan_at` when watched, or a `reason`
  (not_session_project, cli_session, auto_watch_off, watcher_disabled,
  not_registered, no_watcher) and a re-index hint when not. It does not
  touch the #1561 `freshness` key.
- The watcher publishes per-project strategy, cadence and last completed
  scan time through atomics and exposes them via
  cbm_watcher_project_info(); the daemon answers index_status through a
  session-scoped provider.
- README, docs/CONFIGURATION.md, docs/index.html, the index_status tool
  description and the MCP instructions now state the real scope.

`watcher_enabled` already shows in `config list` since v0.11.0 (680bc72).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit that referenced this pull request Oct 4, 2026
#2167)

The background watcher only watches the project an open MCP session is
rooted in, and drops the watch when the last session for that project
closes. A repository indexed with `cli index_repository` (or from a
session rooted elsewhere) is never watched, but README promised
"Auto-sync keeps it fresh after that" for every index_repository target
and the MCP instructions said "Indexes auto-refresh". Nothing in the
status output said which projects were actually watched, so a stale
CLI-indexed repo looked like a watcher bug.

What gets watched is unchanged. Instead:

- index_status gains a `watch` object: `watched`, plus `strategy`,
  `poll_interval_ms` and `last_scan_at` when watched, or a `reason`
  (not_session_project, cli_session, auto_watch_off, watcher_disabled,
  not_registered, no_watcher) and a re-index hint when not. It does not
  touch the #1561 `freshness` key.
- The watcher publishes per-project strategy, cadence and last completed
  scan time through atomics and exposes them via
  cbm_watcher_project_info(); the daemon answers index_status through a
  session-scoped provider.
- README, docs/CONFIGURATION.md, docs/index.html, the index_status tool
  description and the MCP instructions now state the real scope.

`watcher_enabled` already shows in `config list` since v0.11.0 (680bc72).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

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

bug Something isn't working priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. ux/behavior Display bugs, docs, adoption UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants