daemon_runtime_process_fingerprint_never_hashes_replacement_path (POSIX
variant) copied /bin/cat to a temp file named "image" and executed the
copy. On Ubuntu 25.10+/26.04 and any other uutils system, /bin/cat is a
multi-call binary: a copy invoked under the name "image" prints
"coreutils: unknown program 'image'" and exits 1, so the fixture died at
ASSERT(setup). Reproduced on WSL Ubuntu 26.04 (uutils coreutils 0.8.0).
The Windows variant of the same test already avoids this class of donor:
it copies the test runner itself and parks it in a
__cbm_runtime_image_holder argv mode. The POSIX variant now does the
same. The holder mode gains a POSIX branch that blocks reading stdin
until EOF — exactly the behaviour the cat donor provided — and
runtime_test_spawn_blocked_executable passes the holder argument, so its
target must now be a copy of this runner (it has exactly one caller).
The /bin/echo replacement donor is gone too: it was only ever hashed,
never executed, and on a uutils system cat and echo can be the same
multi-call bytes, which would have broken the fixture's
original != replacement precondition. The replacement is now a small
data file, mirroring the Windows variant.
No signing change is needed on macOS: the image copy stays
byte-identical to the runner, and the identical-copy fixture already
proves such a copy executes.
daemon_runtime suite on WSL Ubuntu 26.04: 44 passed / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: OhOkThisIsFine <102485413+OhOkThisIsFine@users.noreply.github.com>
Score every Function/Method/Class node during indexing and persist the
result as a numeric "importance" key inside the node's existing
properties_json:
importance = sqrt(num_refs) * priv * generic * distinct * test_penalty
num_refs is the incoming CALLS + USAGE degree. The multipliers demote
private (leading-underscore) and generic names (defined in >= 5 distinct
files), promote distinctive identifiers (snake_case or camelCase, len >=
8), and demote test scaffolding -- symbols in a test file per the graph's
own cbm_is_test_path() classifier, or targets of an incoming TESTS edge.
Weighted degree only; a transitive (PageRank) refinement is deliberately
not built here until it can be shown to beat this on a fixed judgment
set.
No schema change and no CBM_INDEX_FORMAT_VERSION bump: the score lives in
the existing properties_json TEXT column, so no index is forced to
rebuild and indexes written by older builds simply lack the key.
Three failure modes this shape invites, all of them silent, are addressed
directly:
* Cost. The generic-name multiplier needs |{files a name is defined in}|.
Computing that per NODE with a pairwise file-path scan is O(k^3) in a
same-name group of size k -- ruinous on real corpora, where a single
Java name group reaches k > 4000. Here the count is computed once per
distinct NAME and memoized, and file paths are deduplicated through a
hash set, so total distinct-file work is linear in the graph. Measured
cold full index of a 666 MB Java corpus, same -O2 binary: 85 s with the
pass disabled, 80 s with this implementation, 481 s with the per-node
shape.
* Registration. PREDUMP_PASS_COUNT is now derived from the pass table
with sizeof instead of being hand-written. A hand-written count that
lags an appended entry silently skips the last-registered pass with
every test still green.
* Incremental. The incremental post-pass sequence runs the pass too, over
a graph rehydrated from the store whose nodes already carry the key.
The write-back overwrites an existing key in place instead of appending
a second one, which would otherwise corrupt properties_json into
{"importance":1.0,...,"importance":2.0}.
tests/test_importance.c covers the formula and binds all three failure
modes; the complexity suite gains a GATED counter asserting the pass's
same-name-group work stays linear (~2x, not ~4x) under a corpus doubling.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: petercoxphoto <20230727+petercoxphoto@users.noreply.github.com>
Maintainer follow-up on top of the contributor's rebased commit,
implementing the Half-B strategy decision from the 2026-08-21 review:
- Linux: when the self-resolved path fails the executable check after an
atomic rename-over, hand back the /proc/self/exe magic link. The link
still executes the in-memory OLD build, which is the only spawn the
worker's build-fingerprint gate accepts. Deliberately NOT the saved
launch path: after a replacement that path holds a DIFFERENT build, so
preferring it swaps an ENOENT for a fingerprint refusal.
- macOS: no magic link exists; fail closed. The supervisor logs
index.supervisor.no_self_path and indexing resumes on daemon restart
instead of spawning a missing binary. A rename-over that leaves a VALID
binary still resolves (and the fingerprint gate refuses downstream, with
its own coverage).
- The prefer-saved-launch-path branch from the original head is dropped
per the same ruling.
Also fixes both Half-A review items:
- Aliasing in cbm_http_server_set_binary_path: `path` may alias
g_binary_path on a repeated call. The overlapping snprintf cannot
actually fire today — resolve's leading out[0]='\0' zeroes the shared
buffer first — but that same reset means the call silently DISCARDS the
stored path it was asked to re-resolve, and the in-place write is one
reset-reorder away from real UB. Resolve into a local first: correct
input, no aliasing trap.
- Coverage asymmetry: the probe and driver ran only on Linux while the
production edit covered all of POSIX. Both now run on macOS too, and the
Linux probe first proves the "(deleted)" state via readlink so its
assertions cannot pass vacuously on an intact image.
Revert-check: with main's http_server.c restored, the driver fails at
WEXITSTATUS 44 (resolver returned a dead path) on macOS; with the guard it
passes 63/63.
Co-authored-by: sunmingqiang <sunmingqiang@bytedance.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Prefer the saved executable launch path when it still points to an
executable, and reject a deleted /proc/self/exe path before spawning
index workers.
Add a Linux integration regression that forks, atomically replaces the
running test binary on disk, and verifies that subprocess resolution
uses the saved launch path instead of the now-deleted self path.
Signed-off-by: David Musk <davidmusk93@users.noreply.github.com>
Signed-off-by: sunmingqiang <sunmingqiang@bytedance.com>
Finding #1669 took an 11-corpus A/B across two release binaries. This
suite makes that bug class fail a unit test in seconds, on every
platform, from tiny corpora.
Method: build k and 2k REPLICATED module copies, run the full in-process
pipeline on both, assert counter RATIOS. Independent copies mean every
extensive quantity — nodes, edges, Σ per-file registry defs — must grow
linearly (ratio ~2). A files x corpus coupling makes per-file work itself
grow with k and lands at ratio ~4. Ratios expose the exponent regardless
of absolute scale, so 60-120 files suffice.
Verdicts are pure functions of (code, input): gates ride ONLY on
deterministic work counters and data-product counts, never on wall time.
Throughput (nodes/s, edges/s) is information-only, written to
private/benchmarks/complexity-<ts>.json (local, gitignored; skipped
under CBM_SKIP_PERF where rates are meaningless).
Two corpus shapes, both needed:
- Independent modules (java/py/go/ts templates): catches cross-module
contamination and dedup breakage. The #1669 bug is GREEN here — fully
closed modules filter perfectly, which is exactly why it survived.
- The growing shared package (bigpkg): one Java package whose file count
scales with k — the real-repo shape (files concentrate in large
packages). The JVM namespace filter branch makes per-file work track
package size, so this corpus is the honest #1669 reproducer:
ratio 4.00 RED on the pre-fix tree, 4.00 RED for a module-scoped
overlay, 2.00 GREEN for the own-file overlay. It discriminated the
correct fix design before the fix was written.
Both legs of every pair exceed MIN_FILES_FOR_PARALLEL(50): below it the
sequential path runs, which builds no shared registries and would be the
wrong code path to gate (its per-file cost is bounded by the 50-file
ceiling).
Recorded but deliberately NOT gated, with reasons at the case:
tail_candidates and fallback_rows are legitimately superlinear under
replication until those scans are bounded, and measured ~1 ns/unit.
Every ratio gate carries a non-vacuousness floor on the base counter so
broken counter wiring fails loudly instead of green-washing
(cbm_pxc_count_perfile_defs feeds the overlay path into the same
counter the fallback path already used; wired for Java, extend to the
TS overlay when touching ts_lsp).
Dynamic coverage: languages iterate CBM_LANG_COUNT; embedded templates
cover the LSP-hybrid languages, and tests/fixtures/complexity/<lang>/
dirs are auto-discovered so a new language joins the guard by dropping
fixtures. Uncovered languages are listed in the report with the reason.
The local report additionally carries per-language node/edge counts with
ratios and a per-pass elapsed_ms table per run (captured via a TEE log
sink during the in-process pipeline runs) — trend data for humans, still
never a gate.
Refs #1669.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Six reports describe the same dead end: the indexing worker dies, the
supervisor points at `logs/.worker-<pid>.log`, and the file is 0 bytes.
Nothing was ever flushed, so not one of #1070, #1130, #1132, #1133,
#1145 or #1450 is reproducible or attributable -- the hint says "crashed
on a file" and the file is never named.
Root cause is buffering, not the crash. The worker's stderr is redirected
to that log file by the supervisor, and every diagnostic goes through
`fprintf(stderr, ...)` with no flush. The C standard only promises stderr
is "not fully buffered"; the Windows CRT gives a redirected stderr FULL
buffering, which is why five of the six reports are Windows. A worker
that aborts, is SIGKILLed by the OOM killer, or is terminated after a
hang takes its whole buffer with it.
- cbm_log_set_crash_durable(): setvbuf(_IONBF) plus a per-line flush in
emit_line. Both, deliberately -- setvbuf covers every writer to the
stream including the plain fprintf startup errors that explain a
worker which never got as far as logging, and the flush covers the
case where setvbuf is refused because the stream was already written
to. Enabled for the worker role only, claimed in main() before the
process writes anything.
- cbm_index_worker_log_begin(): a startup header written and flushed as
the worker's first act -- version, build fingerprint, pid, repo path,
and the worker's own arguments. A control record rather than an info
line, so CBM_LOG_LEVEL cannot restore the 0-byte log.
- Under a crash-durable log the parallel extract pass logs every file
it starts, not just the first two rounds of workers, so the log ends
with the files that were in flight when the worker died. One line per
file, never per node; unchanged for every non-worker caller.
This fixes no crash. It converts six unreproducible reports into reports
we can act on, and every future one arrives with the run identified.
Test: a worker started with a fully buffered stderr -- the state the
Windows CRT hands it, forced on POSIX so the contract binds on all three
legs -- writes diagnostics and is then SIGKILLed. The retained log must
be non-empty, carry the header with the version, pid, repo path and args,
and still hold the line written after it. Reverting the production change
leaves that log at 0 bytes.
The probe kills rather than aborts because Darwin's abort() runs the
stdio cleanup handler and flushes the very buffer the repro depends on
stranding: under abort the reverted build still produced a populated log
and the test was quietly toothless. SIGKILL runs no cleanup, and is
literally #1070's death (signal=9).
Refs #1070
Refs #1130
Refs #1132
Refs #1133
Refs #1145
Refs #1450
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The CLI resolves each client's config home from its own environment
variable before falling back to $HOME — cbm_codex_config_dir() honours
$CODEX_HOME and ignores the home_dir it is handed. A test that redirects
only HOME therefore does not isolate those clients: it resolves, reads
and WRITES the developer's real configuration.
On a machine with $CODEX_HOME exported (any Codex or Orca user) 27 of
269 cli tests failed for that reason alone. Every one of them asserted a
successful rc while the CLI was reporting genuine failures against the
ambient config: an unbalanced managed marker left in the real
config.toml, an AGENTS.md that is a symlink and is refused by the
O_NOFOLLOW writer, and user-modified agent profiles that are correctly
preserved. The behaviour under test passed in all of them; only the rc
was poisoned. Worse than the red: the suite mutated that real config,
installing and removing SKILL.md under the user's Codex home.
The individual tests that already neutralize CODEX_HOME are exactly the
ones that pass, and the file carries a comment admitting one suite was
"green only via env leaked from earlier suites". Rather than extend those
ad-hoc per-test lists again, neutralize the whole inventory once in the
runner, and reuse the inventory the shell fixtures are already required
to satisfy.
tests/test_smoke_fixture_contract.sh grows one check so the two cannot
drift: a client isolated in the smoke scripts but forgotten in the C
runner now fails the contract.
Verified on macOS arm64 with $CODEX_HOME and $OPENCODE_CONFIG_DIR
exported: cli went from 241 passed / 28 failed to 268 passed / 0 failed,
with no change outside tests/.
Signed-off-by: Gabriel Gonzalez Pineda <64bits@gmail.com>
Adds cbm_workspace_classify_root, the breadth half of the workspace
boundary. It answers "is this path obviously too broad or too sensitive to
index as one root", not "is this caller allowed to index it" — that second
question needs the user-level grant store, since it is the only input a
caller cannot write. The header says so, because a reader who mistakes one
for the other would draw the wrong conclusion about what is enforced.
Three rules, each doing what the others cannot:
- Depth below the volume. Two components on POSIX refuses every top-level
tree in one rule with no list to maintain. Windows and UNC count
drive/share-relative and require one, because there the first component is
already user space ("D:/repos") — the system trees that sit at the same
depth ("C:/Windows") are covered by name instead, which is tractable
because that set is small and stable.
- The home directory itself, which is two deep on macOS and Linux and so is
invisible to depth.
- Credential directory names, matched against every component so both
"…/.ssh" and "…/.ssh/keys" are refused. This list is deliberately additive
so extending it needs no design authority.
A path that is or contains the cache directory is refused outright: indexing
such a tree would pull every other project's graph database into this
project's index.
Classification order is load-bearing and commented as such. The home
directory normally contains the cache directory, so testing the cache first
reported every home as "holds the cache" and made it non-overridable, which
contradicts the intent that a person may override it. Depth precedes the
cache test for the same reason: "/Users" satisfies both and "too broad" is
the reason that helps the reader.
Takes home and cache as parameters rather than reading the environment, so
the policy is a pure function and the tests need no filesystem. Twelve table
tests; three of them failed on the first implementation and drove the
ordering fix above.
Not yet wired to any caller — deliberately, so the policy can be reviewed on
its own.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Windows shipped a PAIR: a small permanent launcher (codebase-memory-mcp.exe)
plus the real product binary (codebase-memory-mcp.payload.exe). The launcher
existed for exactly one reason — a running .exe cannot replace its own image
on Windows, so an in-process self-update needs a second resident binary to do
the swap.
That stub is statically indistinguishable from a dropper: a small, unsigned,
zero-prevalence PE whose whole job is verify-and-execute another binary.
Defender's ML scored it Trojan:Win32/Wacatac.B!ml and blocked the v0.9.1-rc.1
release at the VirusTotal gate. It is not fixable in our code on x64 —
bcrypt-free, stripped, VERSIONINFO'd, minimal-resource and even
resource-FREE builds on CI's own MSYS2 CLANG64 toolchain were all flagged,
while the product binary scans clean on every platform.
So remove the stub and move self-update OUT of the process into install.ps1,
which runs while cbm is NOT running: Windows' image lock only blocks a
process from replacing ITSELF. now prints the exact PowerShell
command (with the Unblock-File hint for Mark-of-the-Web); install.ps1 is
idempotent, so re-running it IS the update — it stops the daemon, renames the
running binary aside (the one mutation Windows permits on a running image),
publishes the new one, and sweeps retired copies.
Windows now matches Linux and macOS: ONE binary per platform.
* packaging, install.ps1, npm and PyPI wrappers all carry a single binary
* the launcher/payload ABI contract and ~2500 lines of stub state machinery
are deleted
* every daemon start, CLI call and hook fire loses a process spawn, a named
pipe handshake and an stdio relay
* test_windows_bundle_contract.sh is rewritten as an INVERTED contract: it
now asserts no shipped surface can reintroduce a launcher/payload pair,
and that install.ps1 retires the running binary before publishing
Verified: VirusTotal 0/67 on the packaged binary and 0/58 on install.ps1 (no
certificate involved); macOS and Linux full suites green; Windows guards all
green including the new update-handoff contract; npm 10/10; PyPI 3/3.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Daemon lifecycle and Windows correctness, verified on a real Windows 11
ARM64 VM through the maintained test-infrastructure/vm drivers, plus the
macOS and Linux arm64 suites and the container lint gate.
Daemon lifecycle:
- daemon start/stop/status subcommands. `daemon start` launches a
PERMANENT daemon (spawn shape is byte-exact argv; survives idle
periods and session ends) and reports an already-active daemon
instead of failing. Permanence is honored at every stop latch:
last-committed-client disconnect, host initial-client window,
coordinator release, and application final-session close — a
permanent daemon also keeps admitting new sessions after its last
one closes.
- daemon stop refuses while sessions are active and lists the blocking
peers (pid/role) that must finish first; an idle daemon drains
through the activation-shutdown machinery with the ACK ordered after
connection interrupts. A second stop is idempotent. The wire ops are
no-cohort first-frame requests with peer fingerprint authentication,
so stop/status never conflict with an exact-build admission gate.
- One-shot CLI commands now execute through the daemon (index workers
keep their local supervised path). A cold CLI run that had to spawn a
temporary daemon prints a hint that `daemon start` removes the
per-command startup tax; a warm daemon is recycled silently.
- Hooks are connect-only fail-open: with no daemon present the hook
emits a visible, rate-limited notice (Claude-dialect systemMessage
plus stderr for other dialects) and always exits 0 — augmentation is
never allowed to block the caller's tool use.
- Version skew: a newer-build client automatically drains an
older-build permanent daemon (strict semantic-version triples only;
dev builds never auto-drain) and the build-conflict message names
`cbm daemon stop` as the manual escape hatch.
Windows IPC/runtime (real-VM verified):
- ipc(win): persistent pending overlapped ConnectNamedPipe. The accept
path used to destroy its listening pipe instance on every 20 ms poll
timeout; a client attaching in the teardown window was severed or left
on an orphaned pipe object whose HELLO no server handle could ever
read, absorbing the connect until the client's own timeout expired.
The pending connect now survives poll timeouts and nothing is
destroyed while a client could be attaching.
- ipc(win): drain-before-close for final responses. Closing a named-pipe
server handle can discard a just-sent response before the peer reads
it (POSIX stream sockets never lose buffered data on close). A bounded
cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX,
immediate on interrupted connections) now precedes close in
runtime_worker_finish and runtime_reject_inline, so hello-conflict,
capacity and disconnect acknowledgements reliably reach the peer.
- runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no
transport half-close, so close_begin now announces departure with an
explicit frame (ordered after APPLICATION_CANCEL, before the local
interrupt); the server releases the client's admission on receipt
instead of waiting for the handle to close. Admission-drop timing is
now identical to POSIX shutdown() semantics on every platform.
- runtime(win): client close cancellation. close_begin serializes with
request publication under the send lock, best-effort sends the active
token's APPLICATION_CANCEL frame, then interrupts local I/O; the
server cancels MCP/subprocess work promptly. Contract tests accept
both correct outcomes (interrupted transport or decoded CANCELLED).
- runtime: activation acknowledgement ordering. The activation ACK is
the requester's license to act on "snapshotted and draining", so every
connection interrupt is now initiated before the ACK is sent; a
session could previously get one more request serviced after the
requester observed the ACK.
- service(win): deadline-bounded private-file prepare. The conflict-log
prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms
timer granularity) burned ~1.6 s against permanently obstructed paths,
stalling hello rejections past the client's timeout. The retry budget
is now a 250 ms deadline; transient share collisions still retry.
- subprocess(win): cmd.exe /C payload encoder quotes metacharacters
correctly (root cause of the git-on-Windows failure cluster).
- watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native
Windows stop/unwatch cancellation test with exact-image verification.
- httpd: send_all writes in bounded 64 KiB slices. A single giant
nonblocking send() on Windows is absorbed wholesale into AFD kernel
buffering regardless of SO_SNDBUF, so send deadlines and interrupts
could never engage against a slow peer (and the full payload was
pinned in nonpaged pool). Slicing restores a deterministic
backpressure point; a test hook pins SO_SNDBUF for the deadline and
interrupt tests.
- ui/http: shutdown lifecycle — interrupt checks, response-wide send
deadline, explicit connection states, refusal to free a server while
a listener-owned connection is active.
Windows long-path support:
- Central path-aware wide conversion (canonicalize via GetFullPathNameW
and prepend the extended-length prefix for absolute paths >=240) at
the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite
store opens, and the daemon build-fingerprint/log paths. Deep managed
installs (a 64-hex generation directory routinely exceeds MAX_PATH)
now index, stage and activate correctly.
- activation transaction: its own file APIs and the component-walking
ancestry validators now operate in the extended-length namespace;
the launcher path is canonicalized (and prefixed when deep) once at
entry so every downstream exact-string comparison stays
form-consistent.
- Executable self-resolution uses the wide APIs (GetModuleFileNameW,
GetFileAttributesW) so non-ASCII install paths survive argv[0]
resolution.
Windows launcher install/uninstall transaction:
- FileRenameInfoEx names are NUL-terminated in an over-allocated
buffer. FileNameLength governs per the contract, but filter drivers
read FileName as NUL-terminated and appended adjacent heap bytes to
created names — a flaky, garbage-suffixed rename target. Both the CLI
and the launcher rename helpers are fixed.
- Uninstall retires state via rename-aside (.cbm ->
.cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex
chars so the bare rename target stays under the FileRenameInfoEx
NT-conversion ceiling at guard depths; 64 bits still uniquely
identify the generation.
- When the running launcher's mapped generation backings pin .cbm
against rename, the backings are relocated to activation-<pid>-N
.retired tombstones beside the install (a mapped image may be renamed,
never deleted; the launcher's liveness-guarded sweep reclaims stale
tombstones). Every relocation is recorded, and a FAILED uninstall
reverses the moves after restoring .cbm — via MoveFileExW with
extended-length paths on both arguments, since the deep generation
target is beyond the handle-based rename's bare-path reach — so a
restored install keeps its generation backings and stays runnable.
- After a committed uninstall the retired tree's backings are relocated
out so the tree is shallow enough for the detached cleanup's rd, and
the cleanup's working directory strips the extended-length prefix
(CreateProcessW lpCurrentDirectory silently ignores prefixed paths).
- Files created under Administrators-default-owner directories
(CopyFileW destinations, CREATE_NEW tombstones, probe directories)
are explicitly owner-stamped so the exact-owner validators hold on
runner images; guard fixtures stamp hand-built trees the same way.
Diagnostics, tests and infra:
- diagnostics: discovery is now an always-delivered JSON control record
(new cbm_log_control) that survives CBM_LOG_LEVEL suppression and
paths containing spaces; placement honors $TMPDIR with /tmp fallback
via a diagnostics-local helper; the soak parser reads the JSON record;
documented in docs/CONFIGURATION.md. Red-first coverage for suppressed
log levels, TMPDIR-with-spaces, and native Windows output-contract
assertions.
- tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint
parents with production-shaped ancestry (LocalAppData on Windows, via
th_secure_runtime_parent_new) — the runtime ancestry validation
correctly refuses temp roots whose ancestors grant mutation rights to
Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and
drive the documented startup-owner publication flow before reading
generation-bound endpoint addresses. This turns the 26 Windows
failures previously visible in CI's full-test job green without
weakening any validation.
- tests(win): the launcher guard covers the full permanent-launcher
contract including failed-uninstall restore and immediate reinstall
after uninstall; new daemon lifecycle and reworked hook-augment
guards run the start/recycle/stop flow end to end.
- tests: CBM_SKIP_PERF is now actually consumed by the test runner
(it was set by CI but never read, so perf suites ran everywhere);
four throughput/bench suites are classified as perf, the heavy
store_arch suite moved to the slow-timeout tier, and two
wall-clock-sensitive assertions were rewritten as invariant checks
with coarse hang-detector backstops.
- build/test infra: build-dir safety contract, UI dev-proxy security
contract, soak daemon-recovery contract, path-safety helper, the
Windows VM worktree-sync contract wired into scripts/test.sh, and
vm/win.sh guards building its clean embedded-UI product in an
isolated BUILD_DIR so it cannot clobber the incremental test build.
provision-windows.sh now installs Node.js for the guards UI build.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Every spawned worker/daemon re-hashes the entire executable image for
the exact-build fingerprint; for the ASan test-runner (hundreds of MB)
that is tens of seconds per spawn on constrained CI runners and was the
sole cause of the whole daemon-family readiness-timeout flake class
(index_supervisor, daemon_frontend, cli, and much of the Windows
daemon surface). Add a test-only seam under CBM_CLI_ENABLE_TEST_API:
when CBM_TEST_BUILD_FINGERPRINT holds a valid stub the hash is skipped.
test_main.c sets it once; every forked child and re-exec'd worker
inherits it, so exact-build match AND mismatch behaviour is preserved
(a mismatch test still passes a different fingerprint via argv). Never
compiled into production. Local mac run: index_supervisor 6/6, the four
daemon suites 111/0, exact-build test green. The generous frontend
catastrophic-alarm values are kept as backstops.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The runner executed all 104 suites sequentially (~10 min locally, the bulk
of every CI test leg). The suites are process-isolated already (per-process
mkdtemp HOME sentinel; two full parallel runs produced zero cross-suite
failures), so the serialization was pure convention.
- test-runner --list-suites: prints every registered suite, one per line,
emitted by the SAME macro table that executes suites — the list cannot
drift from the run set by construction.
- scripts/run-tests-parallel.sh: runs each suite as its own process
(jobs = CPU count; CBM_TEST_PAR_JOBS overrides) under a ZERO-LOSS
CONTRACT: a union guard fails the gate if the set of suites that
produced a result differs from --list-suites (nothing can be silently
dropped, a newly added suite is picked up automatically); per-suite
pass/fail/skip are summed into the sequential runner's exact summary
format; any suite crash, failure, or omission exits nonzero. Per-suite
wall times are printed for balance tracking.
- make test-par: the parallel target. make test (sequential) is unchanged
and remains the escape hatch (CBM_TEST_SEQUENTIAL=1 in test.sh).
- scripts/test.sh: builds, then routes through test-par — every CI test
leg gets the speedup with zero workflow-topology change (same jobs,
same gates, same billing; the legs just finish sooner).
- The stack_overflow suite is split into a/b/c (7+6+7 of its 20 tests,
pure re-registration): as one suite it was the wall-clock critical path
of any parallel run — every other suite finished underneath its ~4
minutes.
Measured locally (Apple Silicon, ASan runner): sequential ~10 min vs
parallel 232 s, totals identical (6,361 passed / 0 failed / 1 skipped),
union guard clean. The TSan leg keeps its dedicated subset runner
(sequential) — out of scope here.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Run the test process under a private temporary HOME and clear inherited CBM_CACHE_DIR so production index paths and legacy fixtures resolve to the same isolated cache.
Make lang-contract fixtures honor an explicit cache override and remove the sentinel tree at process exit, including after early assertion returns.
Validated on Windows with the full runner: 5828 passed, 22 skipped; a 500-project user cache had zero files added or removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tmonestudio <tmonestudio@users.noreply.github.com>
Replace the plan-01 placeholder with the full Perl LSP test suite covering the
ten foundational resolution scenarios from 22-RESEARCH.md, plus the extract_perl
/ find_resolved / require_resolved / find_resolved_with_strategy helpers cloned
from test_php_lsp.c:
1. method via bless-assignment 6. use parent MRO
2. constructor class-method type 7. use base MRO
3. static package call 8. Exporter import (use Mod qw(f))
4. $self method dispatch 9. require fallback
5. @ISA inheritance 10. unresolvable receiver -> zero edges
Assertions match the resolver's actual QN scheme (module_qn.subname, dotted —
the Perl package governs dispatch, not the emitted QN), and the negative test
confirms the zero-edge guarantee for untyped scalar and unindexed package
receivers. Register suite_perl_lsp in test_main.c so scripts/test.sh runs it.
All 10 perl_lsp tests pass (3563 passed, 1 pre-existing unrelated failure:
search_code_multi_word in tests/test_mcp.c).
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Files whose parse tree contains tree-sitter ERROR/MISSING regions were
silently indexed as if complete — constructs inside those regions are
absent from the graph with no signal anywhere (classic trigger: the
preprocessor-blind #ifdef-split-brace pattern in C).
- cbm_extract_file: set parse_incomplete and record the 1-based line
ranges of the top-most error regions (bounded at 64), serialized as
"start-end,start-end" into the result arena
- pipeline: record such files under the new phase "parse_partial"
(distinct from skips — the file IS indexed) in both the parallel and
sequential extraction paths; stamp the File node with
{parse_incomplete:true, error_ranges} on full and incremental runs
via a new cbm_gbuf_set_node_props (upsert survivor rules bypass)
- mcp: report parse_partial {files, count, truncated, note} separately
from skipped[] (skipped_count semantics unchanged); the per-run
logfile lists parse_partial rows; the index_repository tool
description documents the signal and the query_graph pattern to find
flagged files
- tests: new parse_coverage suite (RED-first, 7 cases incl. false-
positive guards and the region cap) plus an end-to-end resilience
guard asserting response fields, logfile, File-node marker, and the
advertised query_graph query
The signal is explicitly best-effort: a flag means constructs in the
listed ranges were dropped (prefer grep there); the absence of a flag
is NOT a completeness guarantee.
Refs #963
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The supervised-worker fast path deliberately skips the pipeline
teardown (the OS reclaims the process wholesale) and pairs that with
_Exit, which bypasses atexit/LeakSanitizer. The production worker in
run_cli has both halves; the test binary's worker emulation
(tf_maybe_run_index_worker) only had the first: it returned normally
through main(), LeakSanitizer ran at exit, reported the intentionally
unfreed pipeline and forced exit code 1 — so the supervisor read a
healthy index as worker_failed and IDX832 went red on exactly the
platforms whose ASan builds run a leak pass (all four Linux legs;
macOS/Windows ASan have none). Reproduced and verified fixed in an
ubuntu:22.04 arm64 container: reap outcome=clean, mcp suite 140/0.
Mirror the production exit: deliver the response file, fflush, _Exit.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Closes the gap flagged in the distill review (test_security.c: "NOT proven
here: the full UI repro — listening socket + MSYS2 handle walk under a
single-threaded server"). Test-only; no production code changes.
Guard 1 — deterministic socket-isolation probe (any Windows, RED-able):
popen_isolates_listening_socket (tests/test_security.c) opens a real
inheritable AFD/listening socket — the exact handle class that deadlocked
git in #798 — then spawns THIS binary through cbm_popen (a cmd.exe
grandchild, git's spawn shape) in a new __cbm_sockprobe re-exec mode
(tests/test_main.c). The child reports via exit code whether that socket
handle is live in its address space: isolated spawn -> getsockopt fails ->
exit 0 (GREEN); raw-_popen regression leaks it transitively through
cmd.exe -> getsockopt succeeds -> exit 42 (RED). Verified RED with a local
spawn-inherits-all revert, GREEN with the isolated spawn.
Guard 2 — end-to-end liveness under live UI sockets (tests/test_httpd.c):
- ui_server_list_projects_responds_under_watchdog: POST /rpc list_projects
to the running single-threaded UI server with a client SO_RCVTIMEO
watchdog; a wedge -> no 200 -> hard FAIL, never an infinite CI hang.
- git_context_resolve_no_hang_under_live_ui_sockets: while the UI server
holds live listening/AFD handles in-process, run cbm_git_context_resolve
(the exact path list_projects takes) on a worker thread under a
WaitForSingleObject(30s) watchdog; a hang -> FAIL. SKIPs where git cannot
init a repo via system().
All three run in the existing security/httpd suites on the test-windows CI
leg; no new build wiring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018g9f9jr5S46BfKaDvDykBp
Signed-off-by: Flipper <jacobphilipp@ymail.com>
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>
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>
New src/foundation/subprocess.{h,c}: spawn a child, tail its output, and
classify how it ended — clean / exit-nonzero / crash / hang / killed — from
POSIX WIFSIGNALED/WTERMSIG and the Windows NTSTATUS exception exit codes
(0xC0000005 access violation, 0xC00000FD stack overflow). Adds an EINTR-safe
reap loop, a quiet-timeout that kills and reports a hung child (no new output
within the window), and partial-line-safe log tailing.
Generalized from the ad-hoc index spawn in src/ui/http_server.c so the
crash/hang supervisor can reuse one primitive across platforms. The exit-code
to outcome mapping lives in a pure cbm_proc_classify() so the Windows
crash-code path is unit-tested on every platform, not just an untested
Windows branch.
tests/test_subprocess.c: 13 tests — the classifier on all platforms plus real
POSIX spawn/reap of clean, non-zero, SIGSEGV-crash and hang children. Green
under ASan/UBSan.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
index_repository silently dropped files that failed to index: CBMFileResult.has_error
was set but never read, oversized files (>100 MB) were dropped with no signal, and
read/extract failures only bumped a logged-only counter — a file that couldn't be
indexed just vanished from the graph.
Collect and surface them:
- New retained {path, reason, phase} error list on struct cbm_pipeline (mirrors the
excluded_dirs pattern) + accessor + a back-pointer on cbm_pipeline_ctx_t so both
extraction paths can append. Wired in BOTH the sequential (pass_definitions) and
parallel (per-worker, merged lock-free) paths — small repos take the sequential
path, so wiring only one would leave the guard vacuous.
- Feeds: read-fail, extract-fail, the newly-CONSUMED has_error (parse timeout /
parse failed, with error_msg), and oversized. The cross_lsp phase is reserved for
the crash supervisor (Track C) and not fed here (the cross-LSP passes are
best-effort with no failure return; feeding the no-source case would be false
positives).
- MCP/CLI response gains top-level "skipped_count" (0 on clean) and, when >0, a
capped "skipped":{files[<=50],count,truncated} + "logfile". Status stays "indexed"
— a reported skip is the expected, handled outcome, not a degradation.
- Per-run logfile (full uncapped list) written ONLY when >=1 file is skipped:
$CBM_INDEX_LOG override else <cache_dir>/logs/<project>-<epoch>.log.
- Generous env-configurable caps (src/foundation/limits.c): CBM_MAX_FILE_BYTES,
default raised 100 MB -> 512 MiB; over-cap files are REPORTED (phase oversized) +
WARNed, never silently dropped.
Reproduce-first: tests/test_index_resilience.c (gating) — an oversized file (cap
lowered via env) must appear in skipped[] with the 2 good files still indexed and a
logfile written; a clean run has skipped_count 0 and no logfile. Genuine guard:
no-op'ing the recorder makes the oversized file silently vanish (RED). Full suite
5750/0, no ASan/UBSan.
Part of the resilient-indexing effort (Track B surfacing layer). Refs #668.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
detect_changes returned an empty impacted_symbols set for projects indexed
inside a git worktree or from a repo subdirectory (#659): canonical_root was
computed wrong. `git rev-parse --git-common-dir` emits a path relative to the
directory passed via -C (input_path), NOT to worktree_root, and the "/.git"
suffix was stripped textually without resolving ".." components. So e.g.
"../.git" joined against worktree_root, or "/ws/scripts/../.git" strip, left an
unresolved ".." and never matched the project's stored root path.
Fix (src/git/git_context.c): join the relative --git-common-dir against
input_path (the -C dir) instead of worktree_root, and realpath()/_fullpath()-
normalize the result BEFORE stripping "/.git" (the .git dir always exists for a
valid repo, so resolution succeeds). This is consistent with the existing
realpath'd root_path used for prefix matching (mcp.c).
Reproduce-first: tests/test_git_context.c. The subdirectory case is the genuine
guard — a repo indexed from a subdir yields a *relative* --git-common-dir, so the
unfixed code leaves an un-normalized "<root>/subdir/.." (verified RED; GREEN after
the realpath fix). The linked-worktree case is a supporting invariant, not the
#659 reproducer: git that emits an *absolute* worktree --git-common-dir (e.g.
2.48.x) doesn't manifest the bug, so it passes either way; it still enforces the
worktree->main-root invariant. All three git-based tests SKIP_PLATFORM on Windows
(the CI shell there cannot init a repo via system()).
Distilled from #672 (thanks @anivaryam); that PR's production fix is taken
verbatim. Its repo_root/subdir tests ran git unconditionally and failed the
Windows CI leg ("failed to init git repo"); here all three git-based tests are
Windows-skipped so ci-ok is green cross-platform. Supersedes #696, which only
realpath'd after the strip without changing the join base and so still yielded
the workspace parent for the subdirectory case.
Closes#659
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: anivaryam <anivaryam.dev@gmail.com>
The CI lint gate (scripts/lint.sh --ci, clang-format-20) flagged 7 files whose
new lines were not run through the project formatter. Reformat only those lines
(line wrapping / continuation alignment) — no behavior change.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Route QNs (__route__METHOD__/path) are the rendezvous key for joining a client
HTTP_CALLS to the server handler that serves it, but they were built from the raw
path. Frameworks write path parameters with different placeholder syntaxes
(":id" Express/clients, "{id}" Axum/Spring/OpenAPI, "<id>" Flask/Rocket), so a
client call and its handler produced different QNs and never joined. Static
routes joined; parameterized routes (most of a REST surface) did not.
Add cbm_route_canon_path() which collapses each parameter token (":name",
"{name}", "<name>", "${...}") to a single "{}" token, and apply it to the path
at every HTTP-route QN construction site (pass_route_nodes, pass_parallel,
pass_calls, pass_cross_repo). Broker/async/infra QNs are left untouched. Only the
QN (identity/dedup key) is canonicalized; the Route node display name keeps the
original path. Parameter names are intentionally discarded so the same logical
endpoint matches across services that name the variable differently.
Adds tests/test_route_canon.c (11 cases). Fixes#505.
Signed-off-by: imateusdev <imateusdev@gmail.com>
Compare persisted SQLite node counts to in-memory dump counts after
index_repository completes so partial WAL/durability loss surfaces as
status:"degraded" instead of silent indexed.
Signed-off-by: Sam Li <yangsec888@gmail.com>
The web server behind the graph UI is now a purpose-built in-house
module (src/ui/httpd.c): localhost-only listener, strict HTTP/1.1
parsing with fixed request caps, a per-connection receive deadline,
and Connection: close semantics. http_server.c keeps the routing and
handlers, rewritten against the new transport API; the public server
API and main.c are unchanged. The previously vendored third-party
server is removed entirely.
Comes with a new 28-test transport + routing suite (tests/test_httpd.c)
covering parsing edge cases (strict CRLF, Content-Length limits, raw
path matching, percent-decode rules) and live-socket behavior (CORS
policy, RPC dispatch, receive deadline, clean shutdown). The security
audit scripts now check the new file layout and treat any network call
in vendored code as a failure.
Final 4 probe suites (grammar_probe_d/e/f/g, 250 cases) bring node/edge-creation coverage to all 159 grammars. Triage fixed 4 fixture mislabels (objc Method label, smithy $version order, json non-ignored filename, sql DDL-only Variables) and confirmed ~46 real reds. Findings map to known classes 2/3 (import/include-edge resolution missing for ~34 grammars: agda/bicep/elm/func/move/nasm/pony/purescript/qml/smali/tlaplus/vhdl/protobuf/thrift/capnp/wit/smithy/astro/nix/nickel/pkl/jsonnet/hyprlang/devicetree/cmake/makefile/meson/gn/just/bitbake/kconfig/html/css/scss), 16 (node-extraction incompleteness: wolfram multi-def/set_top, wit world funcs, bicep resource, just recipe), plus infra gaps (INFRA_MAPS hcl/k8s only from YAML topic bindings; DEPENDS_ON only from Helm Chart.yaml — go.mod/requirements produce none). NEW class 17 = file-index routing gap: .env/.gitignore/.gitattributes/.re have no EXT_TABLE/FILENAME_TABLE entry in discover/language.c so index_repository never indexes them (direct extraction works). Vue/Svelte/Astro component imports: extraction works but JS/TS path resolver doesn't handle those extensions. All 159 grammars now have reproduce-first guards.
Extends the node/edge-creation hunt to ~35 grammar-only code languages (129 graph/extraction cases, 73 green guards / 56 real RED reproductions; triage confirmed 0 fixture errors — all reds are genuine product gaps). Findings map to: class 2 import-EXTRACTION missing (cbm_extract_imports dispatch in extract_imports.c covers only ~38 langs + default:break, so ~25 grammar-only langs' configured *_import_types are never consumed → 0 imports); class 1 INHERITS missing (crystal/julia/pascal/powershell/squirrel/solidity); and NEW class 16 = per-grammar NODE-extraction incompleteness (class_types configured + grammar node present, but extract_defs.c emits 0 type nodes for D struct/class, F# record/union, Gleam type, Julia/Odin struct, Pascal record, PowerShell class, Hare struct, ReScript type, squirrel class, sway struct/abi, tcl namespace, wgsl struct). Affected-grammar matrix in the suite comments. Brings code-grammar coverage to ~61 of 66.
Final hunt batches (convergence_probe 47, matrix_known_classes 44, matrix_new_constructs 63 = 154 graph-level cases) plus triage. Result: ZERO genuinely-new root-cause classes — every confirmed red maps to the already-known taxonomy (classes 1,4,5,6,7,12,13,14). Triage greened 5 fixture mis-builds: recursion self-calls (Go/Python/Rust/Java) produce 0 CALLS because self-loop edges are SUPPRESSED BY DESIGN (pass_calls.c/pass_parallel.c src->id==dst->id) — not a bug, fixtures now add a non-self caller; Ruby bare 'describe' parses as identifier not call (fixed to describe()). 26 reals kept as reproductions with // REAL BUG: root-cause comments. Confirms the bug hunt has converged: aggressive probing across new constructs + more languages surfaces only instances of the ~15 mapped classes.
Reproduce-first probes (215 graph-level cases) hunting node/edge-creation + LSP-pass bugs across 26 languages + web frameworks. node_creation_probe: 80/81 green (node creation is solid; only Go method-on-struct under threshold). lsp_resolution_probe: 72/83 green — RED reproductions for the cross-LSP dispatch gap (Rust/Kotlin S1 cross-file call) and constructor/static/virtual/generic resolution edge cases (C/C++/TS/Java/C#/PHP S3-S7). edge_types_probe: 29/51 green — candidate RED reproductions for HANDLES (7 web frameworks), HTTP_CALLS (5 clients), WRITES (all 5 langs), Kotlin throws/raises, Go DEFINES_METHOD/OVERRIDE (these need real-bug-vs-fixture verification). Green cases retained as regression guards throughout. No unrelated suite regressed.