Commit Graph
529 Commits
Author SHA1 Message Date
Martin Vogel e549b44daf Merge pull request #865 from DeusData/chore/remove-libgit2
chore: drop optional libgit2 dependency (keep git log fallback)
2026-07-05 01:55:17 +02:00
Martin Vogel 9a03ed3fd1 Merge pull request #664 from mvanhorn/feat/651-feat-auto-install-support-for-the-junie-
feat: auto-install support for the Junie agent
2026-07-05 01:23:46 +02:00
Martin Vogel 2e055d771f chore: drop optional libgit2 dependency, keep git log fallback
libgit2 is licensed GPLv2-with-linking-exception, and this project is
deliberately GPL-free (see scripts/license-policy.json). libgit2 was only
an OPTIONAL, faster git-history code path guarded by HAVE_LIBGIT2 and
auto-detected via pkg-config; it always shipped with a popen("git log ...")
fallback that release binaries already used (they were built without
libgit2). Making that popen fallback the SOLE git-history implementation
means there is NO change to shipped behavior.

Changes:
- pass_githistory.c: collapse the HAVE_LIBGIT2 #ifdef so only the popen
  parse_git_log remains; drop the now-unused <git2.h>/<time.h> includes.
- cbm.c / cbm.h / main.c / subprocess.c / index_supervisor.h: remove the
  libgit2 mimalloc allocator bind and its >=1.7.0 version guards; keep the
  tree-sitter + sqlite3 binds; correct the comments.
- Makefile.cbm: remove the optional-libgit2 pkg-config detection block,
  the REQUIRE_LIBGIT2 error gate, and all LIBGIT2 CFLAGS/LIBS/FLAGS.
- .github/workflows/_test.yml: drop the REQUIRE_LIBGIT2 test-matrix leg
  plus its libgit2-dev/pkg-config apt install (lower CI cost, no new
  gating, no trigger change).
- flake.nix: drop libgit2/pkg-config from the dev shell (now-dead dep).

HAVE_LIBGIT2 no longer exists anywhere in the tree.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-05 00:37:59 +02:00
Martin Vogel 74b27f3e02 Merge pull request #859 from DeusData/distill/782-slab-aligned-pages
fix(slab): O(1) aligned-page cross-thread frees + retire-on-live-count (closes #852)
2026-07-05 00:20:05 +02:00
Martin Vogel b0b7d5102b Merge pull request #863 from DeusData/distill/789-deadcode-ui
feat(graph-ui): dead-code filtering, node code preview, GitHub deep-links
2026-07-05 00:16:16 +02:00
Martin VogelandAndy Zehady a27680c3a9 feat(graph-ui): dead-code filtering, node code preview, GitHub deep-links
Distilled from #789 (safe features only). Adds backend dead-code classification
(status + in_calls in the layout JSON), a GET /api/repo-info endpoint for GitHub
deep-links, and the frontend dead-code filters, node code preview, and deep-links.

The render-cap revert was dropped (kept 2000 for DEFAULT_MAX_NODES/HARD_MAX_NODES
and GRAPH_RENDER_NODE_LIMIT); the sidebar regex-search refactor was omitted.

Security fixes over the original: repo-info strips credentials from any returned
remote_url; the legitimate https blob-URL construction is allow-listed so the
static gate passes; libgit2 is not re-initialized/shutdown per request (reuses the
process-wide init from cbm_alloc_init); deep-link path segments are URL-encoded.

Co-authored-by: Andy Zehady <azehady@ciroos.ai>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 22:19:53 +02:00
Martin Vogel 19296d75b9 Merge remote-tracking branch 'origin/main' into distill/776-linux-rss-undercount
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 21:36:27 +02:00
Martin Vogelandpetercoxphoto 26cd4678a8 fix(mem): use real /proc RSS on Linux (mimalloc current_rss undercounts, blinding backpressure)
Distilled from #776's 132460f5. This project sets arena_eager_commit=0 +
purge_decommits=1 + purge_delay=0 in cbm_mem_init to reduce upfront memory, so
mimalloc's committed-page counter reads low. On Linux mi_process_info() never
sets current_rss (vendored/mimalloc/src/prim/unix/prim.c only fills peak_rss),
so current_rss defaults to that low committed counter — cbm_mem_rss() returned a
few MB while true RSS was multiple GB, leaving cbm_mem_over_budget() backpressure,
the memory ceiling, and the host RAM tier blind on Linux.

Prefer os_rss() (/proc/self/statm) as the primary source on Linux; macOS and
Windows are unchanged (their mi_process_info current_rss is accurate via
task_info / GetProcessMemoryInfo). cbm_mem_peak_rss() is untouched. Complements
#752 by making the RAM tiers actually bite on Linux.

Reproduce-first: mem_rss_reflects_external_resident_memory pins mimalloc's
committed counter low with a live mi_malloc, then grows true RSS via a raw 256MB
mmap (invisible to mimalloc); unfixed Linux returns the ~few-MB committed counter
(RED), fixed Linux returns /proc RSS (GREEN). macOS/Windows pass either way, so
the RED manifests on the Linux CI leg.

Co-authored-by: petercoxphoto <info@petercox.ie>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 21:33:58 +02:00
Martin Vogel 7884ccb9ed Merge pull request #853 from DeusData/feat/index-mem-subprocess-isolation
fix(index): supervised-subprocess RSS isolation for background auto-index + watcher
2026-07-04 20:50:14 +02:00
Martin Vogel a960d3d255 Merge pull request #752 from Andy11-cpu/fix/ram-tier-budget
Tier RAM budget by host size
2026-07-04 20:43:23 +02:00
Martin Vogel c095a4a82e Merge remote-tracking branch 'origin/main' into distill/782-slab-aligned-pages
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 20:38:00 +02:00
Martin VogelandSS-42 487377459f fix(slab): O(1) aligned-page cross-thread frees + retire-on-live-count (no global hot-path lock)
Distilled from #782 keeping its retire-on-live_count correctness, replacing the
global-lock+linear-scan with aligned-page O(1) lookup + per-page lock-free
remote-free queue (no hot-path contention). Each 64KB slab page is allocated
aligned to its own size, so the owning page of any chunk is recovered by masking
the pointer; a lock-free 3-level radix page map (cold-path writes only) tells
slab_free whether a masked base is a real slab page or a plain heap pointer
without dereferencing an unrelated address. Owner frees hit the thread-local
free list; foreign frees CAS onto that page's lock-free MPSC remote-free stack.
Pages carry refcount = handed-out chunks + owner guard; reclaim/destroy retire a
page that still holds foreign-live chunks and free it when the final chunk
returns, so a page holding a live tree-sitter lexer chunk is never freed under
it. Keeps #782's resolve-worker terminal slab teardown.

Closes the #852 use-after-free and the cross-thread invalid free. Reproduce-first
guards in test_mem.c are RED on main (heap-use-after-free / bad-free under ASan)
and GREEN with this fix.

Closes #852
Supersedes #782

Co-authored-by: SS-42 <47749027+SS-42@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 20:35:33 +02:00
Martin Vogel 6404747bb2 Merge pull request #854 from DeusData/distill/685-retention-reshape
fix(index): bound + source-cap parallel retention with re-read fallback (low-RAM peak RSS)
2026-07-04 19:55:25 +02:00
Martin Vogel 40157834f8 fix(index): gate supervised auto-index watcher registration on auto_watch (keystone×#849)
The #832 supervisor keystone added a second watcher-registration site in
autoindex_thread's supervised-success branch, but wired it as a direct
cbm_watcher_watch() guarded only by `if (srv->watcher)`. srv->watcher is set
unconditionally (cbm_mcp_server_set_watcher), so that guard does NOT honour the
auto_watch config. PR #849 had routed ALL watcher registration through
register_watcher_if_enabled() (which checks auto_watch_enabled() and logs
watcher.register.skipped reason=auto_watch_off). On a supervisor-marked prod
host with a fresh (unindexed) project and `config set auto_watch false`, the
supervised path therefore registered a background watcher against the user's
explicit opt-out — defeating #849's multi-project containment. The in-process
twin ~20 lines below already used register_watcher_if_enabled() correctly.

Fix: replace the ungated block in the supervised-success branch with
register_watcher_if_enabled(srv), matching the in-process branch. Surrounding
supervised-success logging is unchanged.

Reproduce-first: mcp_auto_watch_false_skips_supervised_autoindex_issue853 drives
the real public entry initialize -> maybe_auto_index -> autoindex_thread on a
supervisor-marked host (kill switch off) with a FRESH project and
auto_watch=false, in a fork+alarm-bounded child (like the #832/#845 gate tests).
cbm_mcp_server_free() joins the autoindex thread so the registration decision has
run before the watch count is read. It asserts spawn_count advanced (so the probe
cannot pass vacuously by skipping the supervised branch) and watch_count == 0.
RED on the unfixed code (watch_count == 1, exit 61); GREEN after the fix
(register_watcher_if_enabled skips → watcher.register.skipped reason=auto_watch_off).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 19:13:25 +02:00
Martin Vogel 2d7f6e3a89 Merge remote-tracking branch 'origin/main' into feat/index-mem-subprocess-isolation
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

# Conflicts:
#	tests/test_mcp.c
2026-07-04 18:42:43 +02:00
Martin Vogelandnguyentamdat e85c7d69f0 fix(index): bound + source-cap parallel retention with re-read fallback (low-RAM peak RSS)
Distilled from #685 (nguyentamdat) rebased onto current main, plus two
research-driven refinements and a genuine reproduce-first guard.

The parallel extract retains each file's source text so the fused cross-file
LSP resolve can re-parse it. That retention is transient but a peak-RSS driver.
On main the caps are flat (100 MiB/file, 2 GiB total) and a file over the cap is
silently unretained AND its cross-file resolution is skipped -- a graph-quality
gap. This change bounds retention AND keeps every cross-file edge.

- Source-text cap as a FLOOR, not just a ceiling: retention total defaults to
  min(cbm_mem_budget()/8, 1 GiB), per-file min(32 MiB, total). Following the
  rust-analyzer memory model, the RAM-derived default is clamped to a small
  absolute ceiling so a huge-RAM host does not hold tens of GB it would re-read
  cheaply. Both caps env-overridable via CBM_RETAIN_TOTAL_MB /
  CBM_RETAIN_PER_FILE_MB (limits.c convention); ceilings bound only the
  auto-derived default, never an explicit operator/caller choice. A dropped file
  emits one index.retain_capped WARN per run.
- Bounded re-read fallback (the correctness guarantee): resolve_worker re-reads
  an unretained file's source on demand (bounded, freed immediately) instead of
  skipping resolution, wired at every cross-LSP site that consumes source. The
  cap now only trades retained RAM for a bounded re-read, never a lost edge.
- cbm_parallel_extract_ex + opts struct (cbm_parallel_extract is now a wrapper
  passing NULL -> env-derived defaults); malloc/calloc NULL-check hardening.

Reproduce-first: parallel_cross_file_reread_preserves_unretained_edges uses a
Java<->Kotlin pair whose cross-file lsp edges are genuinely source-dependent;
the edges are lost when the caller is unretained and the fallback is absent
(RED), present with it (GREEN), with a retained CONTROL scenario proving
non-vacuity. #685's original Python red test was a false guard (per-file py_lsp
already resolves those calls) and is replaced. Peak-bound guards
(retained_bytes <= total_cap; retain_sources=false retains nothing) in test_mem.c.

Verify: make -f Makefile.cbm cbm && make -f Makefile.cbm lint-ci; test-runner
parallel pipeline incremental py_lsp ts_lsp java_lsp kotlin_lsp c_lsp cs_lsp
go_lsp rust_lsp mem -> 2323 passed.

Co-authored-by: nguyentamdat <nguyentamdat@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 18:38:58 +02:00
Martin Vogel 7d00f77372 fix(index): run background auto-index + watcher re-index in the supervised subprocess (RSS isolation)
The long-lived MCP server ran the full index pipeline in-process on two
background paths -- the session auto-index (autoindex_thread, mcp.c) and the
watcher re-index (watcher_index_fn, main.c). The parallel pipeline's worker
threads abandon their mimalloc pages at thread exit, and mimalloc v3 defaults
page_reclaim_on_free=0, so those pages are never reclaimed when the main thread
later frees their blocks (mi_collect cannot touch abandoned pages either). RSS
then ratchets across re-index cycles and never comes back down (#832).

The index_repository tool path already avoided this by running the index in a
supervised worker subprocess: on child exit the kernel reclaims 100% of the
child's RSS. Route the two background paths through that same supervised runner:

- supervisor_invalidate_store() is now NULL-safe (the watcher path has no MCP
  server / cached store).
- New static index_run_supervised_path() builds {"repo_path": root} and reuses
  the existing spawn + skip-and-continue recovery loop (no duplication). New
  public cbm_mcp_index_run_supervised_path() is the srv-less entry main.c calls.
- autoindex_thread and watcher_index_fn gate on cbm_index_supervisor_should_wrap():
  supervised on a marked host with the kill switch off, else fall through to the
  unchanged in-process pipeline (kill switch / spawn-failure degrade). The
  watcher keeps its non-blocking try_lock gate + g_shutdown check exactly; the
  lock still serialises re-indexes. cbm_mem_collect() stays on the in-process
  branch only.

Also fold in the in-process allocator fallback: cbm_mem_init sets
mi_option_page_reclaim_on_free=1, restoring v2 behaviour so any path that stays
in-process (kill switch, degrade, embedders) also reclaims abandoned worker
pages. The subprocess is the primary cure (child exit returns RSS every cycle);
this is the fallback.

Tests: a deterministic gating guard (test_mcp.c) proves the background paths now
spawn a worker child and index the fixture (RED when routed in-process:
spawn_count unchanged; GREEN after). The test-runner acts as a real in-process
worker when spawned as `cli --index-worker` (public APIs only, no production
seam). A non-gating prod-binary RSS reproduction (tests/repro/issue832_rss.py)
shows the supervised server flat at ~10 MB vs ~131 MB in-process.

Scope: JobObject, the #841 spurious-trigger fix, retention caps, budget tiering
and the backpressure busy-spin are follow-ups.

Refs #832, #841.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 17:23:43 +02:00
Martin Vogel 4fff22fb3b Merge remote-tracking branch 'origin/main' into fix/845-supervisor-host-gate
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

# Conflicts:
#	tests/test_mcp.c
2026-07-04 16:41:14 +02:00
Martin Vogel 54efcef783 Merge pull request #849 from DeusData/distill/625-auto-watch
feat(watcher): auto_watch config gate for background watch registration (default on)
2026-07-04 15:52:17 +02:00
Martin Vogel 2e8f0f0247 Merge pull request #848 from DeusData/distill/821-octree-guard
fix(ui): bound octree recursion for coincident points (3D layout)
2026-07-04 15:27:13 +02:00
Martin Vogel cfd474dbff Merge pull request #840 from DeusData/recover/836-tsjs-suppression
fix(resolver): suppress weak short-name strategies for TS/JS (recovers #836)
2026-07-04 15:13:44 +02:00
Martin Vogel d2a5975dce Merge pull request #846 from DeusData/distill/799-win-handles
fix(windows): spawn git without inheriting handles (UI hang)
2026-07-04 15:06:04 +02:00
Martin Vogel 896aa914f6 Merge pull request #844 from DeusData/distill/828-usage-ownership
fix(graph): stable USAGE-edge ownership for directory-module nodes
2026-07-04 15:00:16 +02:00
Martin Vogel fbd7287f4c fix(supervisor): wrap only marked host binaries; watchdog in worker mode
The index supervisor gate (cbm_index_supervisor_should_wrap) wrapped ANY
embedder of cbm_mcp_handle_tool: it resolved the CURRENT executable and
re-invoked it as '<self> cli --index-worker index_repository ...'. In a
test binary 'self' is the test runner, which ignores those args and
re-runs its suites instead, producing recursive spawn chains (observed
11-minute hangs, and multiplied kernel VM-map/memory load during the
2026-07-04 host panics). The gate is now opt-in: only a process that
called cbm_index_supervisor_mark_host() — the real binary's main(),
before any subcommand dispatch — may spawn workers. Embedders index
in-process by default; the test mains additionally pin
CBM_INDEX_SUPERVISOR=0 as belt-and-suspenders. Guarded by
index_supervisor_gate_requires_marked_host_issue845 (fork+alarm harness,
new spawn-count test hook; red on the old gate, green without the env
kill switch on the new one).

A supervised worker whose supervisor died also kept indexing as an
orphan: the parent-death watchdog only ran in MCP-server mode. run_cli
now starts the same watchdog (detached; on ppid change it only writes
to stderr and _exit(0)s, so it is safe outside server mode) when it
detects --index-worker. Guarded by tests/test_worker_watchdog.sh
(scripts/test.sh Step 5b): a CBM_TEST_HANG_ON busy-spin pins the worker
mid-index, the wrapper parent is killed, and the worker must self-exit
within 15s — red without the wiring, green with it.

Fixes #845.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 14:38:25 +02:00
Martin VogelandAndy11-cpu aa153166d7 feat(watcher): auto_watch config gate for background watch registration (default on)
Add the auto_watch config key (default: true) and route both session
watcher-registration sites in mcp.c (autoindex completion and the
already-indexed connect path) through a register_watcher_if_enabled()
helper. When disabled, the skip is logged as a short structured line
(watcher.register.skipped reason=auto_watch_off).

Default is TRUE (opt-out): existing users keep background auto-sync
unchanged; the knob exists for multi-project users who want each
session contained to explicit indexing.

Distilled from the auto_watch half of PR #625. The PR's bundled
project-index dedup commit is excluded here — it belongs to #754's
own review. Unlike the original PR, no watch registration is added
to the index_repository handler (none exists on main; adding one
would be new behavior beyond the gate), and the default is flipped
from off to on to preserve current behavior.

Tests: default-unset registers the watcher on connect (guards the
no-behavior-change promise); auto_watch=false registers nothing —
verified RED against the ungated production code, GREEN with the gate.

Co-authored-by: Andy11-cpu <canada11@duck.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 14:09:00 +02:00
Martin VogelandLjove02 2b12f0001c fix(ui): bound octree recursion for coincident points (3D layout)
Coincident (or sub-ULP-separated) bodies made octree_insert subdivide
forever in the graph-UI 3D layout, calloc-ing one cell per level until
the process crashed (stack overflow) or froze the machine allocating
(the 34GB-swap reports). Guard distilled from #821: octree_insert now
carries a depth and stops at depth 26 or half_size < 1e-4f, folding the
body into the cell as a mass-weighted centroid aggregate
(body_index = -1). octree_repulse already clamps d to 0.01 before the
dx/d division, so folded coincident bodies get exactly zero force and
no NaN.

The default-cap raise bundled in #821 (DEFAULT_MAX_NODES and
GRAPH_RENDER_NODE_LIMIT 2000 -> 60000) is a UX policy change deferred
to its own discussion per review; HARD_MAX_NODES is raised to 200000 so
opt-in CBM_UI_MAX_RENDER_NODES users get the new ceiling.

Guard test layout_coincident_nodes_bounded drives the public layout API
with same-file nodes whose distinct qualified names share one 32-bit
FNV-1a hash (bit-identical coincident positions on every platform), in
a fork+alarm child so the unfixed runaway cannot take down the runner.

Refs #498, #726, #402

Co-authored-by: Ljove02 <135197334+Ljove02@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 13:51:35 +02:00
Martin VogelandFlipper 40e317edd7 fix(windows): spawn git without inheriting handles (UI hang)
Windows cbm_popen wrapped _popen, whose CreateProcess(bInheritHandles=TRUE)
leaks every inheritable handle - including the UI listening socket and
Winsock/AFD helpers - into git children. Git-for-Windows' MSYS2 runtime
walks inherited handles with NtQueryObject at startup and deadlocks on
socket/AFD handles, wedging the single-threaded UI server (#798).

Read-mode cbm_popen now spawns via CreateProcessW with STARTUPINFOEXW and
PROC_THREAD_ATTRIBUTE_HANDLE_LIST restricted to the stdout pipe write end
and a NUL handle for stdin/stderr; cbm_pclose reaps the child via a handle
table + WaitForSingleObject + GetExitCodeProcess.

Distilled from #799 with three corrections:
- no silent fallback to _popen: a failed isolated spawn logs a structured
  warning (compat.popen_isolated_failed stage/gle/errno) and returns NULL
  (all call sites handle NULL); the composed command line is heap-allocated
  instead of a fixed 2048-byte buffer whose overflow forced the fallback
- cmd.exe resolved explicitly from %COMSPEC% (GetSystemDirectoryW\cmd.exe
  fallback) and passed as lpApplicationName - no search-path lookup
- GetExitCodeProcess failure reports -1 instead of success; a failed NUL
  open fails the spawn instead of feeding INVALID_HANDLE_VALUE into
  STARTF_USESTDHANDLES

Adds two windows-only regression tests (test_security.c) driving the
cbm_popen/cbm_pclose round-trip against real git and asserting the isolated
path was taken via the cbm_popen_last_was_isolated() hook.

Closes #798

Co-authored-by: Flipper <jacobphilipp@ymail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 13:36:45 +02:00
Martin VogelandTommy Corbett f6b9ac9d65 fix(graph): stable USAGE-edge ownership for directory-module nodes
Since #667 every file in a Java/Go package shares a directory-based module
QN that collides with the pipeline's Folder/Project node. Class-level
references resolved their enclosing scope to that shared node, whose
file_path was clobbered by each file's always-emitted Module def — last
writer wins (sequential upsert) or last-merged worker wins (parallel merge)
— so USAGE-edge sources conflated across same-package files and varied run
to run.

Three coordinated changes:
- cbm_gbuf_upsert_node: a Module def colliding with a Project/Folder node
  no longer updates ANY field (the #667 guard only protected the label).
- merge_update_existing: same guard on the parallel worker-gbuf merge path;
  the ID remap stays outside the guard so worker edges still redirect onto
  the canonical node.
- Source-node finders (pass_usages, pass_parallel, pass_calls) treat a
  lookup landing on a Folder/Project container as a miss and fall back to
  the per-file File node (new cbm_pipeline_node_is_dir_container helper).

Adds tests/repro/repro_issue787.c: exact-source-set, 5-run stability, and
>50-file parallel-path guards (red on the unfixed code in both sequential
and parallel flavors).

Distilled from #828: the author's commits lacked DCO sign-off trailers and
could not be cherry-picked; content applied verbatim plus one clang-format
fix in graph_buffer.c.

Closes #787.

Co-authored-by: Tommy Corbett <spde89@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 13:34:54 +02:00
Martin VogelandDustin Persek 96cb6d188c fix(ui): unwatch projects on UI delete; harden deferred-free OOM corner
DELETE /api/project removed the project's .db file but never told the
watcher; the zombie watch kept polling the repo and the next change
silently resurrected the deleted DB (#803). Wire the watcher into the
UI HTTP server (non-owned ref; created before the server, freed after
it) and unwatch after a successful unlink and on ENOENT (clears an
already-orphaned watch, still 404). Invalid name stays a plain 404
with no unwatch; a non-ENOENT unlink failure stays 500 and keeps the
watch. The cbm_file_exists pre-check is replaced by unlink-errno
handling, removing the TOCTOU window.

Distilled from #830, rebased over the defer_state_free refactor
(#817): the OOM hardening of the deferred-free path moves into
defer_state_free itself so BOTH unwatch and stale-root prune get it.
On pending-free realloc failure the helper no longer frees the state
immediately (a potential use-after-free against an in-flight poll_once
snapshot) — it logs watcher.unwatch.oom and returns failure, and the
callers keep the entry registered; the prune path retries on the next
poll cycle.

Carries six httpd regression tests from #830 driving the live UI
server with a real watcher wired in.

Closes #803.

Co-authored-by: Dustin Persek <dustin.persek@protonmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 11:56:43 +02:00
Hitoshi OkazakiandClaude Fable 5 26689e72e6 fix(resolver): suppress weak short-name matches for TS/JS method calls
A member call `x.foo()` reaches the registry's weak textual cascade only when
the TS-LSP could not resolve the receiver type — type-resolved calls win via
lsp_* strategies before the registry runs. Binding such a call to a project
symbol by a weak short-name strategy fabricates a CALLS edge (`re.test()` ->
SalesforceRestClient.test, `date.toISOString()` -> any project toISOString).
On a 6k-file monorepo this produced ~21.6k false edges (~12.6% of CALLS).

Add cbm_tsjs_suppress_weak_method_match: for a TS/JS/TSX member call flagged
is_method, the weak strategies (suffix_match, unique_name, field_type_hint,
fuzzy) are noise. It uses an EXPLICIT drop-list, not keep-list + default-drop,
because the resolver runs lsp_* through the same code path — a default-drop
would silently kill lsp_ts_method. Gated on the file language so no other
language is affected.

The suppression is applied at the PLAIN-CALLS emission point, not before it:
emit_classified_edge (sequential) and emit_service_edge (parallel) take a
suppress_plain_calls flag and skip ONLY the final plain CALLS fall-through
(and emit_http_async_edge's no-URL fall-through). Every service classification
that runs first — the #523 callee-name HTTP/ASYNC bypass, route registration,
gRPC/GraphQL/tRPC/CONFIG, and emit_service_edge's unconditional
detect_url_in_args — is therefore byte-identical to main by construction. This
replaces an earlier attempt that dropped the call at a language guard and tried
to re-derive "is this a service edge?" with a predicate: that predicate drifted
from the emit path's classification and lost ~399 HTTP_CALLS (verb-suffix
clients like api.patch / page.goto / request(app).get, whose HTTP signal is not
a library name in the callee) plus ~63 CONFIGURES on the monorepo. Suppressing
at the emit point cannot drift.

Tests: unit tests pin the keep/drop split (including lsp_* -> keep). A
reproduce-first sequential E2E fixture asserts the regex-receiver false edge is
gone while a typed-receiver call (lsp_ts_method) and a bare local call survive.
A >=50-file parallel fixture (CBM_WORKERS forces the parallel resolver) asserts
that axios.get and dev.load('/api/data') keep their HTTP_CALLS (the latter via
detect_url_in_args — the class the old predicate lost), that api.patch,
request(app).get and router.get keep their Route registrations, and that the
regex and dev.load weak plain-CALLS edges stay suppressed. The ts/S6
inherited-method probe, which used to pass via a fragile unique_name fallback,
now asserts exactly zero CALLS with a store-opened guard (the INHERITS edge IS
extracted; the gap is ts_lsp_cross's cross-file resolution, which the guard
keeps once wired).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hitoshi Okazaki <okazaki.hitoshi@tecnos.co.jp>
2026-07-04 14:34:26 +09:00
Martin Vogel 9871e7e086 Merge pull request #827 from DeusData/feat/index-supervisor-subprocess
feat(index): crash/hang supervisor — quarantine the culprit file, index the rest
2026-07-04 05:12:58 +02:00
Martin Vogel 836f90dec7 Merge pull request #817 from DeusData/distill/738-watcher-prune
feat(watcher): prune projects whose root stays missing (ENOENT-only, grace window)
2026-07-04 04:58:18 +02:00
Martin Vogel 4cd8d3e846 Merge pull request #810 from DeusData/distill/536-cross-repo
fix(cross-repo): normalize URLs, match templates, run both directions
2026-07-04 04:37:38 +02:00
Martin Vogel 2e7c64b30c Merge pull request #811 from DeusData/distill/793-walk-exclusions
fix(pipeline): honor discovery exclusions in pkgmap/path-alias/envscan walks
2026-07-04 04:03:53 +02:00
Martin Vogel 010ac9b266 Merge pull request #812 from DeusData/distill/cli-hygiene
fix(cli): overflow-safe ZIP bounds check; env-bash shebangs
2026-07-04 03:46:45 +02:00
Martin Vogel bb2510eac3 Merge pull request #806 from DeusData/distill/560-architecture-overview
fix(mcp): get_architecture overview subset, aspects enum + validation (+ .cbmignore how-to)
2026-07-04 03:44:29 +02:00
Martin Vogel 9fbdac25eb Merge pull request #813 from DeusData/distill/702-installer-utf8
fix(windows): unswallow installer failures, quote CreateProcessW args, document MotW
2026-07-04 03:35:07 +02:00
Martin Vogel 5f6461e902 Merge pull request #818 from DeusData/distill/684-jvm-callgraph
fix(jvm): resolve mixed Java/Kotlin callgraph edges via declared packages
2026-07-04 03:13:37 +02:00
Martin Vogel 85349a8833 Merge pull request #826 from DeusData/fix/smoke-invariants-triage
fix(mcp): exit promptly on stdin EOF over a FIFO; smoke tools-list follows pagination
2026-07-04 03:09:12 +02:00
Martin Vogel 541ff613af Merge pull request #814 from DeusData/distill/770-imports-dedup
fix(graph): IMPORTS edges keyed by local_name across buffer, store and dump
2026-07-04 03:08:07 +02:00
Martin Vogel 6e59736711 lint: clang-format compliance, fork allowlist entry, reduce bin scope
- format the include trailing comments and wrap the over-long reap log line
  (clang-format-20 violations flagged by lint / lint)
- allowlist subprocess.c's fork with justification (security-static): the
  supervisor primitive forks and execs immediately; no code runs in the
  forked image
- move bin into the fork-child block alongside argv (cppcheck variableScope
  consistency)

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 03:01:12 +02:00
Martin Vogel 416e4f2315 Merge pull request #807 from DeusData/distill/543-cbmignore-neg
fix(discover): .cbmignore negation can un-skip built-in dirs (safety core excluded)
2026-07-04 02:35:35 +02:00
Martin Vogel b6bd28c954 lint: reduce argv scope to the fork child in cbm_run_posix (cppcheck variableScope)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-04 01:07:51 +02:00
Martin Vogel 78dab4cc18 docs: use scanner-neutral URL example in cross-repo comment
The hardcoded-URL security audit flags http:// literals in src/; the comment
only illustrates the shape of stored url_path values.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 21:07:27 +02:00
Martin Vogelandyutianyu111602-glitch b4070c5d44 perf(mcp): prefilter scoped search_code filelist by path_filter
Apply path_filter while writing the scoped filelist instead of only after
grep, so large indexed projects do not scan files whose hits the post-grep
filter would discard anyway. The prefilter runs the SAME compiled regex via
the same cbm_regexec call against the same root-relative path (Windows
separators normalized first) as the post-grep filter in collect_grep_matches,
which is kept as belt-and-suspenders — results-preserving by construction.

Guard the edge the prefilter introduces: a path_filter matching zero indexed
files now short-circuits with the normal empty result instead of invoking
xargs on an empty filelist (GNU execs grep once with no operands, BSD skips).

Tests: positive guard (filter matching the hit's file still returns it,
out-of-filter file excluded) and zero-match guard (clean empty response,
grep subprocess skipped). Both green pre-change too — perf-only change.

Distilled from #756 rebased onto the post-#751 filelist writer.

Co-authored-by: yutianyu111602-glitch <yutianyu111602@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 21:04:20 +02:00
Martin Vogelandnguyentamdat 5ff29caddc fix(jvm): resolve mixed Java/Kotlin callgraph edges via declared packages
Mixed Gradle/Maven source roots (src/main/java + src/main/kotlin) share
JVM packages that path-derived module QNs cannot join: cross-file LSP
emitted package-shaped names while graph nodes stayed path-derived, and
same-package defs across roots were filtered out, so Java<->Kotlin calls
fell back to weak or wrong textual edges.

The declared package is JVM ground truth. Normalize JVM defs to their
declared package (inferring it from conventional source roots only when
no package was extracted), index defs by declared namespace alongside
the path module so same-package defs survive per-file filtering, restrict
JVM callers to JVM defs, register cross-def return-type signatures for
Kotlin receiver typing, and consume qualified member-call overrides by
leaf name in the parallel lsp_idx fast path.

Distilled from #684 with one design change: the new unique-Class.method
tail-match fallbacks in lsp_resolve.h are gated to JVM callers
(cbm_pipeline_lsp_allow_tail_match, Java/Kotlin only). Tail-matching by
unique leaf is safe where class-per-file package semantics hold; in
other languages a single wrong-module coincidence would fabricate a
CALLS edge. The leaf-key lsp_idx lookup stays global: it aligns the
fast path with the build-side key that already used leaf names.

Tests: carried mixed Java/Kotlin regression (red on main, green here)
plus a gate guard asserting the tail fallbacks stay off for non-JVM
callers and on for JVM.

Co-authored-by: nguyentamdat <nguyentamdat@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:50:46 +02:00
Martin Vogelandpcristin e5bb74916a feat(watcher): prune projects whose root stays missing (ENOENT-only, grace window)
Distilled from #738: the watcher now prunes a watched project whose root
directory has genuinely disappeared - deleting the cached DB (+wal/shm,
validated name, cache-dir-only paths) and removing the watch entry - so
vanished worktrees stop being watched forever (#286).

Hardened beyond the original PR to remove a data-loss hazard (the cached
DB can hold user-authored data such as the ADR, unrecoverable once
deleted):

- Only ENOENT/ENOTDIR stat failures count as missing. Any other failure
  (EACCES, EIO, transient mounts, macOS TCC revocation) resets the
  streak and logs watcher.root_stat_error with the errno; Windows
  (mingw/UCRT) maps not-found to ENOENT so the check holds there.
- Pruning requires BOTH >=3 consecutive missing polls AND a sustained-
  absence grace window since the streak's first miss (default 600s,
  override via CBM_WATCHER_PRUNE_GRACE_S), tracked with monotonic
  cbm_now_ms.
- Root reappearance resets streak + timestamp (watcher.root_restored).
- The pruned entry is released via the deferred-free list poll_once
  drains (one freeing model shared with cbm_watcher_unwatch).

Limitation: only currently-watched projects are pruned; stale DBs left
by older sessions are out of scope.

Co-authored-by: pcristin <xxxokzxxx@protonmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:40:22 +02:00
Martin VogelandAlexandros Pappas 6012f4626e fix(graph): IMPORTS edges keyed by local_name across buffer, store and dump
Distills PR #770 (graph-buffer dedup key) and completes the edge-uniqueness
contract it left partial: the contract lives in THREE places, and changing
only the buffer ships databases that violate their own UNIQUE constraint
(PRAGMA integrity_check: non-unique entry in sqlite_autoindex_edges_1).

A single 'import { A, B } from ./lib' produced ONE IMPORTS edge: the graph
buffer dedups edges on (source_id, target_id, type) and merge-replaces
properties on collision, so the second symbol silently overwrote the first.
pass_calls.c, pass_usages.c, pass_semantic.c and pass_lsp_cross.c parse one
local_name per IMPORTS edge for cross-file resolution, so the dropped
symbol's calls failed to resolve too — not just "who imports X" queries.

Changes, kept in sync across all three sites:

- graph_buffer.c (from #770): make_edge_key() folds local_name into the
  dedup key for IMPORTS edges only; all three key call-sites updated.
  Hardened beyond #770: EDGE_KEY_BUF bumped to 256 and oversized
  local_names are re-keyed with an FNV-1a hash of the full name instead
  of being silently truncated (two long names sharing a prefix must not
  collide back into one edge).
- store.c: edges gains local_name_gen, a VIRTUAL generated column
  (IMPORTS -> coalesce(json_extract(properties,'$.local_name'),''),
  else '' — NOT NULL because NULLs never conflict in a UNIQUE index);
  uniqueness widened to UNIQUE(source_id, target_id, type,
  local_name_gen) and the insert upsert's conflict target matches.
  init_schema probes pre-#768 DBs (no local_name_gen) and fails the
  open: SQLite cannot ALTER a table constraint in place, and an
  unopenable DB already takes the existing repair path — full index
  deletes + rebuilds, artifact import refuses and falls back to a
  reindex. Read-only query opens skip init_schema and keep working.
- sqlite_writer.c/.h: dump DDL matches the widened schema; the hand-built
  sqlite_autoindex_edges_1 comparator and entry builder include the
  local_name column; CBMDumpEdge carries local_name extracted via real
  JSON parsing (yyjson) so index entries match json_extract's unescaped
  values exactly, per the idx_edges_url_path precedent.
- artifact.h: CBM_ARTIFACT_SCHEMA_VERSION 1 -> 2 so old binaries refuse
  artifacts carrying the widened schema (their 3-column conflict target
  can no longer prepare against it).

Tests (reproduce-first, all red on the unfixed code):
- gbuf tier: multi-symbol import -> 2 IMPORTS edges; long-local_name
  truncation guard.
- store tier: distinct local_name coexists as 2 rows, same local_name
  still upserts, non-IMPORTS dedup unchanged.
- writer tier: dumped DB passes integrity_check with 2 sibling imports
  and exposes matching local_name_gen values.
- end-to-end: TS fixture through the real pipeline -> 2 queryable
  IMPORTS edges AND integrity_check ok; with only the buffer half of
  the fix this test still fails on integrity_check, proving the schema
  half is required.

Closes #768.

Co-authored-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:16:39 +02:00
Martin VogelandShauryaaSharma cf32e6c540 fix(windows): unswallow installer failures, quote CreateProcessW args, document MotW
Distilled from PR #702 (fixes #697):

- install.ps1: replace the try/catch that swallowed `codebase-memory-mcp
  install` failures with a $LASTEXITCODE check that reports the exit
  code and fails the installer, instead of silently leaving no coding
  agent configured.
- README: document Unblock-File for the Mark-of-the-Web restriction
  that blocks the downloaded install.ps1, plus the execution-policy
  escape hatch.
- cbm_exec_no_shell (Windows): switch from _spawnvp, whose CRT does not
  quote arguments containing spaces (the taskkill filter "IMAGENAME eq
  codebase-memory-mcp.exe" arrived as three bare tokens), to
  CreateProcessW over a two-pass MSVC-convention quoted command line
  (cbm_build_cmdline, exposed for tests via compat_fs_internal.h).
- UTF-8-correct widening: the quoted command line is assembled in UTF-8
  bytes and converted once via cbm_utf8_to_wide, so non-ASCII arguments
  (e.g. a non-ASCII %USERPROFILE%) survive intact instead of being
  byte-widened as Latin-1 mojibake.
- tests: regression guard for the #697 taskkill filter, MSVC quoting
  edge cases (empty arg, embedded quote, trailing backslashes), UTF-8
  widening guards (2-byte sequence with explicit code points; mixed
  2-/3-byte round-trip via cbm_utf8_to_wide), and live CreateProcessW
  spawn tests. All Windows-only, exercised by the Windows CI leg.

Co-authored-by: ShauryaaSharma <shauryasofficial27@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:15:52 +02:00
Martin VogelandSandro Jäckel 50392a4188 fix(scripts): use /usr/bin/env bash shebangs (NixOS has no /bin/bash)
On NixOS (and other non-FHS systems) /bin/bash does not exist, so
scripts with an absolute shebang fail to run. Switch the remaining
holdouts to /usr/bin/env bash: eleven scripts/*.sh,
test-infrastructure/run.sh, and the three Claude Code hook scripts
emitted by src/cli/cli.c (gate, session reminder, subagent reminder).

Distilled from PR #674, with parser-test coverage preserved: the
infra_parse_shell* fixtures in tests/test_pipeline.c intentionally keep
#!/bin/bash so absolute-path shebang extraction stays covered, and
tests/repro fixtures are untouched.

Also replace the GitHub-PAT-shaped fixture string flagged in the #674
thread with an obviously fake placeholder (ghp_FAKE...) that still
matches the ghp_ + 36-alnum secret detector.

Co-authored-by: Sandro Jäckel <sandro.jaeckel@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:11:49 +02:00