Commit Graph

2344 Commits

Author SHA1 Message Date
Martin Vogel 3491a8e83b fix(mem): restore mimalloc's Linux arena-commit default (#1654)
Since #1360 routed ordinary malloc/new through mimalloc on Linux, the
arena policy governs every allocation in the process rather than just the
bound sqlite/tree_sitter populations. cbm sets arena_eager_commit=0, so
mimalloc commits sub-ranges with mprotect(PROT_READ|PROT_WRITE) over a
PROT_NONE reservation, and each partial commit SPLITS the reserved VMA.

Measured on the Go corpus, Linux arm64, shipped binaries:

  v0.9.0   10 mappings, at ANY worker count
  v0.10.5  ~22k mappings, peak; the count tracks CONCURRENCY
           (999 at 1 worker, 8460 at 4, 11965 at 18)

Two consequences, both of which #1654 reported from a 96-CPU/376 GB host:
the mmap/mprotect churn serialises on the kernel's per-process mmap_lock,
and the VMA count climbs toward vm.max_map_count, after which mmap fails
for ANY size -- so mimalloc reported it could not allocate 10 KB while
`free -g` still showed 246 GB available.

mimalloc's own default for this option is 2, meaning "eager-commit arenas
only on an OS that overcommits (i.e. linux)", precisely because commit is
free there until pages are touched. Overriding it to 0 opted Linux out of
the default written for Linux. Restore it on Linux only; every other
platform keeps the lazy setting, where commit is NOT free and the
upfront-memory reason still holds (Windows especially, #581).

Measured effect, same corpus and host, baseline build vs this build:

  mappings  22450 -> 17312  (-23%)
  wall       92.4s -> 92.6s (unchanged)
  peak RSS  19.14 -> 19.22 GB (unchanged)

This is a partial mitigation, not a cure: the remaining ~17k mappings are
individual 64 KB-3 MB extraction buffers, each taking its own mmap (the
worker reserves ~40 GB of address space for ~19 GB of RSS). Pooling those
is the durable fix and is deliberately left out of this change.

Guard: mem_arena_eager_commit_follows_platform_commit_cost pins the
platform split so the Linux default cannot be silently opted out again.

Reproduction and controlled 2x2 (only vm.max_map_count varied) are
recorded on #1654.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-16 03:23:05 +02:00
Martin Vogel 49d928be67 Merge pull request #1658 from DeusData/fix/eof-missing-terminator 2026-08-15 20:19:00 +02:00
Martin Vogel 8c84a1e7db Merge pull request #1659 from DeusData/fix/vt-stop-rescanning-binaries 2026-08-15 20:18:45 +02:00
Martin Vogel 63f0a6c0e7 test(parse-coverage): free the extraction results in the #1610 tests
LeakSanitizer on CI caught all five new tests leaking their CBMFileResult:

    Indirect leak of 24 byte(s) ... ts_tree_new
      cbm_extract_file_ex cbm.c:1256
      do_extract test_parse_coverage.c:39
      test_dockerfile_missing_final_newline_not_flagged_issue1610:272
    SUMMARY: AddressSanitizer: 706504 byte(s) leaked in 189 allocation(s)

Every pre-existing test in this suite calls cbm_free_result before PASS; the new
ones did not. The local run could not have found it - LeakSanitizer reports
"detect_leaks is not supported on this platform" on macOS arm64, so this class
of defect is CI-only here.

Each test now captures what it asserts, frees, and only then decides, so the
early-FAIL paths do not leak either. The cross-grammar loop prints its
diagnostic before freeing so the failure message keeps naming the grammar.

While correcting the guard, a first attempt left ASSERT_TRUE(flagged ||
has_ranges || true) in real_error_before_eof_still_flagged - always true, and it
would have silently disarmed the guard that stops the EOF suppression from being
over-broad. Removed. The guard is re-proven binding: forcing
cbm_is_eof_terminator_miss to return true makes EIGHT tests fail, including both
guards, and restoring it returns the suite to green.

parse_coverage 14 passed.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 17:22:59 +02:00
Martin Vogel bdb99d7750 fix(release): stop re-scanning bytes VirusTotal has already scanned
The verify pass submitted every extracted object, selected executables included,
on the stated grounds that "VirusTotal is content-addressed, so identical bytes
return the analysis it already holds instead of re-running 70+ engines".

That is measurably false. On v0.10.5 all EIGHT re-submissions produced a NEW
analysis - same VirusTotal file-id, timestamp 47 minutes later:

    candidate: file-id=2c00f485...  ts=1786795957  (12:12:37Z)
    verify   : file-id=2c00f485...  ts=1786798758  (12:59:18Z)

Re-analysing identical bytes re-rolls a probabilistic classifier, and Microsoft's
ML engine answered differently within that hour, in BOTH directions:

    82750cd1 (linux-amd64)   microsoft-ml -> clean
    6d3c5be6 (darwin-arm64)  clean        -> microsoft-ml

The published notes are generated from the candidate scan, so v0.10.5 shipped a
table calling linux-amd64 flagged when VirusTotal had it clean, and darwin-arm64
clean when VirusTotal was reporting Trojan:Script/Wacatac.B!ml. Every hash in
that table links to the page that contradicted it. Corrected in place after
publication; this removes the cause.

The second scan proved nothing the first did not. Identity is settled by hash
before this step runs: verify-release-selection.py reconciles every published
container to the selected bytes, and checksums.txt binds the same digests
publicly. A re-scan adds no assurance - only another roll.

What still gets scanned is exactly what the candidate pass never saw: install.sh,
install.ps1, LICENSE, THIRD_PARTY_NOTICES.md, the MCPB manifest.json and the
unpacked UI assets. install.sh and install.ps1 are the highest-consequence
non-executable bytes we publish - users pipe them straight into a shell - and
that coverage is untouched. Measured on the v0.10.5 object set: 16 objects in,
8 withheld, 8 still scanned.

The withheld set is recorded as evidence (cbm-virustotal-withheld-v1) naming each
sha256 and pointing at virustotal-candidate-results.tsv, so the published
evidence still accounts for every shipped object.

Fails closed three ways, each with an actionable message: no object matches a
selected sha (the containers do not carry the recorded bytes), everything is
withheld (the surface scan would be a no-op), or the selection names no shas at
all. The zero-match grep is wrapped rather than left to pipefail, because a
guard that aborts silently is not a guard - found by testing the guards rather
than assuming them.

Also drops 8 VirusTotal submissions per release.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 16:13:13 +02:00
Martin Vogel 8669ba8f9e fix(extract): a missing final newline is not a partial parse
A file that does not end with a newline leaves the grammar's mandatory line
terminator MISSING. cbm_collect_error_regions counted that node, so the file was
reported parse_partial with the last line as its error range.

It is not a miss. The node is ZERO-WIDTH and sits at EOF: the parser consumed no
source for it, so by construction nothing was dropped - no construct can live in
a zero-byte span - and every real instruction above it parsed normally. Proven
by dumping the tree: the reporter's two-line Dockerfile yields
(source_file (from_instruction ...) (entrypoint_instruction ...) (MISSING "\n"))
with both instructions intact and the MISSING node spanning bytes 73-73.

It was never Dockerfile-specific. Stripping the trailing newline from the 156
linkable grammar fixtures flips 13 of them to has_error, and SIX produce regions:
dockerfile, tcl, fish, gomod, hyprlang - and makefile, which is a genuinely
different case (its ERROR has WIDTH; the recipe really is lost).

Worse, the ones that stayed silent did so for no principled reason. ini, fsharp,
beancount, requirements, gitignore, sshconfig and kconfig omit the same
terminator, but theirs is a HIDDEN node and hidden nodes are invisible to
ts_node_child(). Whether a user was told their file was partially parsed came
down to whether that grammar's author declared the terminator visible.

The cost was not cosmetic: a phantom parse_partial writes a "<project>::missed"
shadow row, and until #1609 that row made the project fail cross-repo validation
as BOTH source and target. A single absent byte could remove an entire
repository from cross-repo intelligence with no error shown anywhere.

The suppression is deliberately narrow - zero-width AND at EOF. A MISSING or
ERROR node with width still counts even at EOF, and anything before EOF is
untouched. Both callers pass the raw root, so one source_len is correct for
both; verified rather than assumed, since root is bound once and never
reassigned.

Reported by @vitaliy-shatskiy, who could not share the original file and instead
rebuilt the property from scratch with a byte-exact script - an editor would
have silently re-added the newline and hidden it. Their isolation matrix ruled
out BOM, CRLF vs LF, exec-form vs shell-form and file length before we looked at
it once.

Reproduce-first, revert-checked: the Dockerfile and cross-grammar tests fail on
the previous tree and pass with the fix; forcing the new predicate to return
false brings the identical REDs back. Two guards pin the boundary and hold in
both directions - a width-bearing failure at EOF (makefile) and a real
mid-file ERROR in a file that ALSO lacks its final newline (built from
C_IFDEF_SPLIT, the fixture this suite already proves is flagged).

parse_coverage 14, extraction 276, language 217, infrascan 3,
grammar_regression 1 - 511 passed, 0 failed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 16:09:04 +02:00
Martin Vogel 3162f09173 Merge pull request #1638 from astandrik/codex/fix-1633-codex-hook-preflight-diagnostics
fix(cli): explain Codex hook preflight refusals
2026-08-15 15:44:03 +02:00
Martin Vogel b416d30c7c Merge pull request #1653 from DeusData/fix/update-names-missing-installer
fix(cli): only name an installer that is actually there
2026-08-15 15:43:57 +02:00
Martin Vogel 77195634e1 Merge pull request #1651 from DeusData/fix/checksums-and-license-gate
fix(release,ci): cover ui-* aliases in checksums.txt, and stop the licence gate depending on a live fetch
v0.10.5
2026-08-15 10:45:39 +02:00
Martin Vogel d58962c010 fix(cli): only name an installer that is actually there
`update` hands off to install.sh (install.ps1 on Windows) and prints the command
to run. It built that command from cbm_detect_self_path - the BINARY's directory
- and treated "I resolved my own location" as "the installer is beside me".

Those are different questions. install.sh is placed beside the binary by
install.sh itself, but a binary that was moved, packaged by a distro, or built
from source has no installer next to it. We printed the path anyway:

    bash "/home/<user>/.local/bin/install.sh"
    /usr/bin/bash: /home/<user>/.local/bin/install.sh: No such file or directory

Reported on discussion #1560 (#1632) by a user who was already three releases
deep in install trouble and had just been told, by us, to run a file that does
not exist.

`update` exists to tell someone how to proceed. Ending the interaction on a
command that cannot run is the one outcome it must not produce - and the
fallback text was already there and already correct, naming install.sh as
shipping in the release archive without asserting a path.

The probe goes through cbm_path_info_utf8 so a non-ASCII install directory
resolves on Windows, and rejects a DIRECTORY of that name, because `bash <dir>`
is not a command either. A symlink still counts: it is reported rather than
followed, and the shell runs it perfectly well.

The Windows branch gets the same treatment; it had the identical assumption
about install.ps1.

Reproduce-first and revert-checked: with the probe forced to return true - the
old behaviour - the new test fails with "a directory with no installer must not
be named as one", and passes once it is restored. cli: 276 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 10:43:15 +02:00
Martin Vogel 6a701b5d09 Merge pull request #1652 from DeusData/fix/cross-repo-shadow-row
fix(cross-repo): stop a `::missed` shadow row making a project unresolvable
2026-08-15 10:21:39 +02:00
astandrik d0351fd99a test(cli): cover v0.10.2 Codex hook upgrades
Signed-off-by: astandrik <astandrik@yandex-team.ru>
2026-08-15 10:37:24 +03:00
astandrik 00e0cf38d6 fix(cli): harden hook diagnostic contract
Signed-off-by: astandrik <astandrik@yandex-team.ru>
2026-08-15 10:37:24 +03:00
astandrik 7d4dc779d0 fix(cli): explain Codex hook preflight refusals
Signed-off-by: astandrik <astandrik@yandex-team.ru>
2026-08-15 10:37:24 +03:00
Martin Vogel 6ecbc5f046 Merge pull request #1605 from ertankucukoglu/perf/search-code-counters
perf(mcp): report search phase timings
2026-08-15 09:02:32 +02:00
Martin Vogel e51f7b5f07 Merge pull request #1394 from jstar0/fix/cypher-repeated-node-unification
fix(cypher): unify repeated variable-length nodes
2026-08-15 08:57:18 +02:00
Martin Vogel a3ae7513a4 Merge pull request #1400 from AmirF194/fix/1249-binary-concat-route-url
fix(extract): resolve Go route paths built via string concatenation
2026-08-15 08:57:12 +02:00
Martin Vogel fda23bd0c5 Merge pull request #1604 from ertankucukoglu/perf/search-code-extension-filter
perf(mcp): prefilter simple suffix globs on Windows
2026-08-15 08:57:06 +02:00
Martin Vogel eb2c67a2e8 Merge pull request #1628 from DeusData/fix/windows-publish-diagnostics
fix(windows): report why an atomic publish failed instead of blaming the repo
2026-08-15 08:57:00 +02:00
Martin Vogel c82e69efc6 Merge pull request #1637 from ertankucukoglu/fix/search-code-path-regex-cleanup
fix(mcp): release path filter on search launch failure
2026-08-15 08:56:54 +02:00
Martin Vogel 22de979b9d fix(ci): pin the canonical Apache-2.0 text by digest instead of fetching it
audit-license-provenance.py ran `curl https://www.apache.org/licenses/LICENSE-2.0.txt`
every time the gate executed and byte-compared the result against
vendored/nomic/LICENSE, with capture_output and no error check. Any fetch
failure therefore produced an empty string, compared unequal, and reported:

    vendored/nomic: DIFFERS [apache.org canonical LICENSE-2.0.txt]
    PROVENANCE AUDIT FAILED: 1 unexplained verdict(s)

which is indistinguishable from a real licence discrepancy.

A gating verdict must be a pure function of the tree, not of whether a web
server answered. This one was neither reproducible nor attributable: it
reddened `security / license-gate` on PR #1337 - a branch touching cli.h,
hook_augment.c and test_cli.c, and no licence at all - and left that
contributor blocked for over two weeks on a signal that had nothing to do with
their change.

Verified before pinning: our vendored copy is byte-identical to the upstream
canonical text, 11358 bytes on both sides, diff clean. The Apache-2.0 text is
immutable and versioned, so a digest is the honest way to express "this is that
text". A mismatch now means our vendored copy changed, which is exactly - and
only - what this audit exists to detect.

The audit passes locally with no network access on the nomic entry.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 08:49:59 +02:00
Martin Vogel c360440203 fix(release): cover the legacy ui-* aliases in checksums.txt
The ui-* archives are byte-identical copies of the canonical ones, published
after verify so they inherit the hash-bound VirusTotal verdicts. They were
absent from checksums.txt, and publish-legacy-aliases.sh documented that as
intentional: "checksums.txt covers the canonical names current installers
request."

That reasoning has a hole. The aliases exist only for 0.9.x updaters (#1538),
and those verify the NAME they asked for. So the alias fixed the 404 and moved
the failure one step later - the updater downloads the archive, cannot find its
name in checksums.txt, and refuses:

    warning: codebase-memory-mcp-ui-darwin-arm64.tar.gz not found in checksums.txt
    error: refusing to install an unverified download

Reported by AmooAti in #1134. Confirmed on the live v0.10.4 release: eight ui-*
archives published, zero of them listed. Every pre-0.10 user who answered the
old variant chooser with "ui" is hard-blocked from updating by any path.

The same digest is now emitted under the legacy name before the attestation
step, so the attested artifact covers both names. No new bytes and no new scan
surface: an alias is a copy, so its sha256 is by construction the one already
computed.

The rule lives in scripts/ci/append-legacy-alias-checksums.sh rather than inline
in the workflow, because the venue-parity contract requires it: a venue may
provision, plumb artifacts, or call a canonical leg script, and text
transformation is none of those. Keeping it beside publish-legacy-aliases.sh
also puts the two halves of the alias rule in one place, which matters because
they must stay in step - .tar.gz and .zip only, never an already-ui-* name. It
fails closed when it matches nothing, since a name with no asset is as broken as
an asset with no name.

Validated against the real v0.10.4 checksums file: the generated set is exactly
the eight ui-* assets that release published - no phantom names, none missing -
and the empty case exits non-zero.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 08:49:59 +02:00
Martin Vogel 329cfc3c36 fix(cross-repo): stop a "::missed" shadow row making a project unresolvable
Indexing writes an internal "<name>::missed" miss-graph row into the SAME db as
the primary project whenever a file parses partially. cr_store_has_exact_project
required `count == 1` over ALL rows returned by cbm_store_list_projects, which
does not filter those rows - so any project that had ever recorded a parse miss
failed validation, as SOURCE and as TARGET, and the whole feature reported:

    project is not indexed

for a project that plainly was. There is no user-level workaround: a partial
parse is not something the operator controls, and re-indexing reproduces the
shadow row.

This is the same defect mcp.c fixed for list_projects in #1044 ("requiring
n == 1 over ALL rows made every project with a miss graph vanish"); the
cross-repo site never learned it. The fix ports that primary-row filter.

The single-primary requirement itself is deliberately kept: it is what proves
the db belongs to the project we were asked about rather than being a shared or
mislabelled store. Only "::" shadow rows stop counting toward it.

Reported by vitaliy-shatskiy in #1609, whose diagnosis named the exact function
and the exact reason.

Reproduce-first, and revert-checked both ways:
  - the new test fails on origin/main with `ASSERT(!(result.failed))`
    (tests/test_cross_repo.c), for the behaviour under test rather than a setup
    error;
  - it passes with the fix;
  - reverting ONLY src/pipeline/pass_cross_repo.c and keeping the test brings
    the identical RED back, so the test binds to the production change.

The existing pair without shadow rows is the control: those tests already prove
that path returns edges, so this cannot pass vacuously on a fixture that never
matched.

cross_repo 8 passed; pipeline 249 passed; store_edges 25 and store_nodes 67
passed - no collateral change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 08:44:20 +02:00
Martin Vogel a598a5a5d8 Merge pull request #1641 from DeusData/fix/uninstall-help-destroys
fix(cli): stop `uninstall --help` from performing a real uninstall
2026-08-15 08:04:29 +02:00
Martin Vogel 37051b81f1 Merge pull request #1645 from DeusData/fix/product-runtime-dir-override
fix(daemon): let a shipped build relocate the rendezvous via CBM_RUNTIME_DIR
2026-08-15 08:04:23 +02:00
Martin Vogel e513beb487 Merge pull request #1643 from DeusData/fix/no-stdin-slurp-without-flags
fix(cli): never read stdin for a tool that declares no arguments (#1359)
2026-08-15 02:52:29 +02:00
Martin Vogel a530c8aabe fix(cli): stop uninstall --help from performing a real uninstall
`codebase-memory-mcp uninstall --help` removed the binary and every agent
configuration (#1038). It did the destructive thing to someone asking what the
command does.

The top-level dispatcher matches the subcommand at argv[1] and forwards the rest,
so its own --help check at src/main.c:1047 never sees argv[2]. Nothing downstream
looked either, and cbm_cmd_uninstall went straight to parse_auto_answer.

The guard is checked FIRST, before parse_auto_answer, so a `-y` elsewhere on the
line cannot auto-confirm the destruction we are trying to prevent. It prints real
usage, states plainly that the command is destructive, and points at --dry-run.

--help is the flag a person types precisely BECAUSE they are unsure what a
command does. It must never be the thing that destroys their install.

cli suite: 271 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 02:43:27 +02:00
Martin Vogel f8b2c6fa47 Merge pull request #1640 from DeusData/fix/json-mcp-extra-keys
fix(cli): accept an MCP entry the client annotated, instead of refusing it
2026-08-15 02:42:24 +02:00
Martin Vogel e59ce4a3c9 Merge pull request #1646 from DeusData/fix/refusal-names-wrong-dir
fix(daemon): name the directory that actually refused, not the one after it
2026-08-15 02:41:38 +02:00
Martin Vogel 092c77e9ed Merge pull request #1644 from DeusData/fix/worker-log-flush
fix(index): make a crashed worker's log survive and name the run
2026-08-15 02:41:33 +02:00
Martin Vogel 1ab5512f0e Merge pull request #1639 from DeusData/fix/yaml-interior-anchor-chars
fix(yaml): treat interior `*` and `&` as text, not as alias/anchor indicators
2026-08-15 02:41:27 +02:00
Martin Vogel 7cd262b95e Merge pull request #1629 from DeusData/fix/agent-config-error-detail
fix(cli): say what the agent-config target IS when a write is refused
2026-08-15 00:29:06 +02:00
Martin Vogel 03948e2d25 Merge pull request #1627 from DeusData/fix/cli-coordination-detail
fix(cli): name the rule that refused CLI coordination, not just the stage
2026-08-14 21:14:19 +02:00
Martin Vogel a036a310f0 Merge pull request #1625 from DeusData/fix/installer-staging-acl
fix(installer): give the Windows staging directory an owner-only DACL
2026-08-14 20:54:36 +02:00
Martin Vogel daac6bd424 fix(daemon): name the directory that actually refused, not the one after it
posix_directory_parent_secure(current_fd) validates the directory we are ALREADY
IN, but the refusal message printed `component` - the child about to be entered.
Every reporter has therefore been sent to inspect the wrong directory:

  #1537 read "ancestor '.cache'"          when /Users/<user> was refusing
  #1621 read "ancestor 'cbm-daemon-501'"  when /private/tmp was refusing

Both inspected the named directory, found it clean, and said so. They were
right. #1537 has been open for weeks with the reporter repeatedly confirming a
correct `.cache` - including on v0.10.4 today - because we kept pointing at it.

The message now names the CONTAINING directory and says explicitly not to check
the component itself.

This fixes no permission logic. It changes weeks of talking past each other into
a report someone can act on in a minute, which for this class of bug is the
whole game: the refusal is invisible from outside, so the message IS the
diagnosis.

Found while reviewing the CBM_RUNTIME_DIR work; the original detail was added in
the earlier half of #1537 and named the wrong variable from the start.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-14 20:07:19 +02:00
Martin Vogel 7f3e30e1e1 fix(daemon): let a shipped build relocate the rendezvous via CBM_RUNTIME_DIR
The daemon/CLI rendezvous directory is created under %LOCALAPPDATA% (Windows) or
/tmp -- /private/tmp on macOS -- and every ancestor of it must pass the
private-directory walk. That ancestry is not always acceptable, and when it is
not, EVERY invocation fails, `config list` included, so the settings surface
cannot be reached either:

    codebase-memory-mcp: secure daemon endpoint could not be created

#1623 narrowed the Windows side of this by admitting AppContainer package and
capability SIDs on ancestors, and named the remainder explicitly: a live local
group, Authenticated Users inherited from a secondary volume root, and orphaned
unresolvable SIDs still refuse, and "those need CBM_RUNTIME_DIR or a separate
change". #1621 is the POSIX shape of the same dead end -- /private/tmp/cbm-daemon-<uid>
refused with no way to move it.

There was no way to move it in a shipped build. The only relocation hook,
CBM_TEST_DAEMON_RUNTIME_PARENT, is compiled out unless CBM_ENABLE_TEST_SEAMS is
defined, so a test build started while the shipped build did not; CBM_CACHE_DIR
is no help either, because it moves the cache and never the rendezvous.

CBM_RUNTIME_DIR names the parent directory the rendezvous is created under. It
does NOT relax the check: the directory it names goes through exactly the same
validation as the default -- ancestors owned by you or root, not world-writable,
no allow-ACL; the rendezvous directory itself still forced to owner-only -- and a
value that fails is refused rather than silently replaced by the default. The
operator only chooses an ancestry that passes. cbm_safe_getenv never truncates,
so no half of an over-long value can become a runtime parent.

The override is resolved in cbm_daemon_bootstrap_endpoint_new(), the one function
every product endpoint goes through: the daemon, the MCP client, the local CLI,
the index worker, and the install/update/uninstall activation path in cli.c. No
call site can silently keep the default, and the detached daemon inherits the
value with the rest of its environment. An explicit parent still wins, so the
compile-time test seam and the lifecycle guards' isolated namespace behave
exactly as before.

Approach and variable name from #1576 by Leonardo trindade miranda, resolved one
layer lower so the activation path is covered too.

Refs #1574
Refs #1621

Co-Authored-By: Leonardo trindade miranda <tmonestudio@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-14 20:04:08 +02:00
Martin Vogel 87717b0aec fix(index): make a crashed worker's log survive and name the run
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>
2026-08-14 20:01:06 +02:00
Martin Vogel 9ca108fce9 fix(cli): never read stdin for a tool that declares no arguments (#1359)
`cli <tool>` resolved its JSON arguments from stdin whenever stdin was not a
terminal and no --args-file, raw-JSON positional or --flag form was supplied,
and cli_slurp_stream reads to EOF. An ordinary automation caller never sends
that EOF: Node's child_process.spawn defaults to stdio ['pipe','pipe','pipe']
and the parent must call child.stdin.end() explicitly, which almost nobody does
for a command it is not writing to. fd 0 then stays open with no writer and the
read never returns. The reporter's shell script was still parked in fread(0)
fourteen minutes later.

`list_projects` advertises "properties":{} — stdin could never have carried
anything it accepts, so the read was pure deadlock with nothing to gain. Gate
the stdin path on the tool's input_schema actually declaring properties, in a
seam (cbm_cli_args_from_stdin_allowed) that main.c's resolution chain calls.
An unknown tool also stops blocking: dispatch rejects it by name and no stdin
content can change that verdict. Tools that do declare properties keep the
documented `echo '<json>' | cli <tool>` channel untouched, and interactive runs
are unchanged because isatty(0) already short-circuited them.

Measured on the pre-fix binary, stdin held open by a writer that never writes,
15-second watchdog: `cli list_projects` was killed at 15s with 0 bytes of
output, while `cli list_projects < /dev/null` returned in 3s with 90 bytes.
After the fix the same open-pipe invocation returns in 3s with 90 bytes.

Regression tests (tests/test_cli.c): the reported hang, the piped-argument
channel that must survive, the interactive path, unknown/NULL tool names, and a
sweep asserting the gate tracks the advertised schema across the whole tool
table so a future zero-argument tool cannot reintroduce this silently.
Revert-check: with the schema gate reverted to the pre-fix `return
!stdin_is_tty`, both tests fail on the list_projects assertion and the suite
reports 2 failed; with the gate applied, 272 passed / 0 failed.

Known limit, not addressed here: a tool that DOES declare properties, invoked
with its required flag omitted (`cli index_status` without --project), still
blocks on an open pipe. Distinguishing "arguments are coming" from "nobody will
ever write" needs a bounded wait, which is a public-surface decision (deadline
value, and the cross-platform pipe-readiness probe Windows would need) rather
than a mechanical fix.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-14 19:55:24 +02:00
Martin Vogel dbd20eaa48 fix(cli): accept an MCP entry the client annotated, instead of refusing it
OpenCode writes "enabled": true beside the "command" and "type" we write. Our
ownership check required the entry's key set to match EXACTLY
(config_json_like.c: member_count != found_count), so a three-key entry with two
recognised keys was classified as FOREIGN - and we refused to touch an entry we
had written ourselves. install then failed with:

  error: agent_config agent=OpenCode op=mcp_install path=.../opencode.json

Confirmed on two independent configs: Linux (#1630) and Windows (#1582). In
gotspatel's file EVERY MCP server carries the key - mssql, forgetful,
chrome-devtools and ours - so this is OpenCode's normal shape, not an unusual
hand-edit. Anyone who has ever toggled a server on or off in the UI was hit.

Two of my own hypotheses were wrong before the reporters' files settled it: it is
not JSONC comment parsing, and it is not the .jsonc targeting that #1575 fixed.
#1575 fixed WHICH file we open; this happens after, on what we find inside.

The distinction now reported is MATCH_WITH_EXTRAS, and the caller treats it as
ALREADY SATISFIED - success, without touching the file. That is deliberate and it
is the safe half of the fix: cbm_json_like_upsert_entry REPLACES an entry
wholesale, so writing our canonical shape over an annotated entry would silently
delete the client's keys. A refusal the user can see beats a deletion they
cannot. Doing nothing is also correct on the merits: the entry already names this
binary with the right type, which is the entire content of the install.

Merging our fields into an annotated entry while preserving the rest is the
fuller fix and stays tracked in #1630. This makes the common case work without
risking anyone's configuration tonight.

Ownership is NOT loosened otherwise: an entry whose command points at a different
binary is still foreign and still refused, byte-identically, and that direction
is pinned by its own test.

Tests use the reporters' actual entry shape. cli suite: 272 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-14 19:19:02 +02:00
Martin Vogel b9cde7e025 Merge pull request #1618 from moffermann/fix/spawn-backoff-clamp
fix(subprocess): bound the spawn backoff by the wait, not by the budget
2026-08-14 18:35:02 +02:00
Martin Vogel 436707f8ff fix(yaml): treat interior * and & as text, not as alias/anchor indicators
yaml_range_has_unsupported rejected `&` and `*` anywhere in a plain scalar, with
no positional test at all. So a value containing prose asterisks - `use
*emphasis* here`, a kaomoji, a glob inside a description - was refused as if it
were an alias.

That is one of four constructs that made a real 16 KB hand-maintained Hermes
config permanently un-editable by us (#1631). It is not YAML we cannot model; it
is YAML we declined to read.

An indicator only counts where a NODE begins. The test is now positional and
judged by what PRECEDES the character, because the scanned range covers a whole
line including its key: in `command: &shared` the `&` is an anchor even though
`command:` came first. A node begins at the range start, after a mapping colon,
or after a block-sequence dash. Everything else is text.

`{` and `}` keep their existing treatment. Empty flow mappings (`key: {}`) are
the next item in #1631 and carry their own semantics; bundling them here would
mix a positional correction with a value-semantics change.

Root-caused by @rg6304, who reproduced it in isolation, read this source, and
corrected the maintainer's hypothesis - the constructs I had guessed (inline
comments, anchors, `---` separators) appear nowhere in the failing file.

TESTING NOTE, recorded because it nearly went wrong: the first version of the
regression test put the asterisk in an untouched foreign section and passed with
the fix REVERTED - the editor only validates the range it writes, so the test
proved nothing. Rewritten onto the entry block, it now fails without the fix
(`== -1, expected 0`) and passes with it. The companion test pins the other
direction: a LEADING `*` is still an alias and still refused, byte-identically.

config_yaml_edit + yaml suites: 117 passed, 0 failed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-14 18:27:06 +02:00
Martin Vogel a6afd156b7 Merge pull request #1521 from lukiod/test/mcp-fuzz-wrong-types
test: cover wrong JSON types in the MCP robustness layer
2026-08-14 17:04:16 +02:00
Ertan 8daa1120fa fix(mcp): release path filter on search launch failure
Signed-off-by: Ertan <ertan.kucukoglu@gmail.com>
2026-08-14 17:51:45 +03:00
Ertan 088b96b32d perf(mcp): prefilter simple suffix globs on Windows
Signed-off-by: Ertan <ertan.kucukoglu@gmail.com>
2026-08-14 17:50:54 +03:00
Ertan 2eb46cffa9 perf(mcp): report search phase timings
Signed-off-by: Ertan <ertan.kucukoglu@gmail.com>
2026-08-14 17:47:07 +03:00
Martin Vogel 076c8aeb4a Merge pull request #1623 from DeusData/fix/ancestor-appcontainer-sids
fix(windows): tolerate AppContainer SIDs on ancestors, keep the runtime dir strict
2026-08-14 16:22:55 +02:00
Martin Vogel 995760b105 Merge pull request #1617 from SunneeYang/agent/fix-coverage-truncated-ignored
fix(mcp): preserve exact-path coverage when ignored records truncate
2026-08-14 15:48:53 +02:00
Martin Vogel 1beb6d8c46 Merge pull request #1615 from moffermann/fix/sanitized-build-detection
fix(build): give sanitized-build detection one spelling and a backstop
2026-08-14 15:47:17 +02:00
Martin Vogel 5805769087 Merge pull request #1600 from phyrexia/test/hermetic-client-env
test(cli): neutralize ambient client home overrides in the C runner
2026-08-14 15:46:38 +02:00
Martin Vogel 9c9716264c Merge pull request #1606 from ertankucukoglu/fix/search-temp-isolation
fix(platform): isolate concurrent temporary directories
2026-08-14 15:46:31 +02:00