Skip to content

[pull] main from DeusData:main - #150

Merged
pull[bot] merged 44 commits into
TKaxv-7S:mainfrom
DeusData:main
Sep 28, 2026
Merged

pull[bot] merged 44 commits into
TKaxv-7S:mainfrom
DeusData:main

Conversation

@pull

@pull pull Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

kavish-19 and others added 30 commits September 5, 2026 22:54
cbm_store_count_nodes and cbm_store_count_edges treat every sqlite3_step
result other than SQLITE_ROW as a count of zero. A read that failed —
SQLITE_CORRUPT, SQLITE_BUSY, SQLITE_IOERR — is therefore indistinguishable
from a project that genuinely holds no rows, and index_status renders it
as the positive assertion status "empty". A user or agent reading that
concludes the repository was never indexed and starts a multi-minute
re-index, while the corruption itself is never surfaced.

Both functions already have an error channel: each returns CBM_STORE_ERR
when prepare_cached fails. Only the step result was not reported through
it. index_status is already written for that value — it sets degraded on
a negative node count and clamps a negative edge count — so the guard
existed and simply never fired.

Initialise count to CBM_STORE_ERR so a non-row step is reported as a
failed read. A successful step still overwrites it with the real count,
so the healthy path is unchanged.

Closes #2012

Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
The store change alone does not close #2012. handle_index_status reads
the counts directly and renders status as `nodes > 0 ? "ready" : "empty"`,
so a negative count from a failed read still answered "empty" — now with
a bare -1 printed as the count, and with no hint at all, because the
"Project is empty" branch is guarded on `nodes == 0`.

The guard cited earlier lives in build_index_success_response, which is
index_repository's response builder, not this path.

Treat a negative node or edge count as a failed read: report status
"error", suppress the negative numbers rather than emitting them as
counts, and replace the re-index hint with one that names the table that
could not be read.

Closes #2012

Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
cppcheck 2.20 flags the nested ternary in handle_index_status:

    src/mcp/mcp.c:6705:30: style: Condition 'nodes<0' is always false
    [knownConditionTrueFalse]

After the `nodes < 0 && edges < 0` arm, cppcheck's value flow takes
`nodes < 0` to be the false half of that conjunction and rejects the
second test. Select the hint with a plain if / else-if / else chain into
a `const char *hint` and add it once, rather than suppressing the check.

The three messages are byte-identical to before — only the line wrapping
of the adjacent literals moved — so status, counts and hint text are
unchanged and the tests added in the previous commit still describe the
behaviour.

Verified with cppcheck 2.21 locally using the flags from
Makefile.cbm's lint-cppcheck target: the finding is reproduced on the
parent commit and gone here, with no new ones. clang-format reports no
drift over the changed lines.

Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
Create missing parent directories recursively and require fixture nodes before
negative graph assertions. Cover exact bytes, deep paths and blocked parents.

Fixes #2034

Signed-off-by: astandrik <astandrik@yandex-team.ru>
Merge upstream main at aa44c28 into the issue #2034 branch. Preserve the existing one-file fixture patch and validate the merged pipeline suites and CI lint entrypoint.

Signed-off-by: astandrik <astandrik@yandex-team.ru>
Signed-off-by: astandrik <astandrik@yandex-team.ru>
OpenCode's V2 loader validates only the module's default export,
expecting {id, setup()|effect()}. cbm_client_adapter_opencode still
emitted the V1 named export (export const CodebaseMemory = ...), so
every server start failed with SchemaError: Missing key at ["default"].

Wrap the existing hook logic in a default export with an id and a
setup(ctx) function; the returned hooks object is unchanged.

Fixes #2077

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
…bfolder

resolve_git_common_dir() only stats repo_path/.git directly, so indexing a
subfolder with no .git of its own never consulted any ancestor repo's
.gitignore. Add resolve_enclosing_git_root() to walk up parent directories
the way git itself does, and merge that ancestor's .gitignore (and
info/exclude) ahead of the subfolder's own so the more specific file still
wins on conflict.

Fixes #510

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
DeusData asked (2026-09-19T20:25:34Z on PR #2089) to place the hooks in
the server entry rather than returning them from setup(), since the
server runtime reads default.server and dispatches the hooks it
returns, while the V2 config loader only reads id + setup()/effect()
and has no tool domain yet to register against.

Moved the existing hook body under server: async (ctx) => {...} and
added an empty setup() {}. Updated the emission test to check for both
and to assert the old async setup(ctx) { shape is gone.

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
…to their own directory

The previous commit found the enclosing repository when a git-less subfolder
is indexed, but merged its patterns into the matcher whose rel_path is
relative to repo_path. gitignore.c matches a rooted pattern with
`glob_match_bounded(p->pattern, rel_path)`, so every anchored pattern of the
enclosing repository was silently re-anchored onto the indexed subfolder.
That is wrong in both directions, and both were checked against real
`git check-ignore` run inside the subfolder:

  * enclosing root .gitignore `/secret.py`, indexing pkg/
    git INDEXES pkg/secret.py — we HID it. A rooted pattern means "in the
    repository root", and re-anchored it became "in pkg". Silent index loss:
    the file is gone from the graph with no diagnostic, the worst failure
    mode discovery has.
  * enclosing .git/info/exclude `pkg/scratch/`, indexing pkg/
    git IGNORES pkg/scratch/tmp.py — we INDEXED it. The pattern is anchored
    THROUGH pkg, so once rel_path lost the "pkg/" prefix it stopped matching
    and an excluded directory got walked.

Only the enclosing root's .gitignore was consulted, too; git reads the
.gitignore of every directory between the repository root and the indexed
one, deeper overriding shallower.

Mechanism. discover.c already had the chain that encodes "the deepest file
with an opinion wins": gitignore_link_t carries a `prefix`, the walk-relative
directory a nested matcher came from, which local_rel_path() strips off. An
ancestor is the mirror image of that, so the chain is extended rather than
duplicated: each link now also carries a `base`, the walk root's path
relative to the directory the matcher came from, which gitignore_chain_result
PREPENDS instead of stripping (discover.c:569, 610). Exactly one of the two
offsets is ever non-empty, and both live in the link's flexible tail, so no
allocation site is added.

  * ancestor_ignores_build() (discover.c:1282) loads the .gitignore of every
    directory from the enclosing repository root down to — but not including —
    the indexed one, shallow to deep, and chains them so the deepest wins,
    negations included. The enclosing repository's info/exclude is merged into
    the ROOT ancestor's matcher, which is where git anchors it, keeping the
    precedence info/exclude already has over .gitignore at the same level.
  * the indexed directory's own .gitignore stays its own matcher at the leaf
    of that chain (discover.c:1480), so it still overrides every ancestor.
    Nothing is merged into it any more, which also removes the earlier merge's
    failure mode of dropping the local patterns and keeping the ancestor's.
  * repo_path is canonicalized with cbm_canonical_path() before the lexical
    parent walk (discover.c:1414), so `cbm index pkg` and
    `cbm index /abs/path/pkg` discover the same files; pipeline.c strdups the
    caller's path unchanged.
  * walk_owned_gitignore_free() (discover.c:1019) is factored out of walk_dir
    and shared with the ancestor chain, which owns its matchers the same way.

Proof on real input, before the suites. Indexing scripts/ of this checkout —
a git-less subfolder whose root .gitignore carries the rooted patterns
`/memlab-*` and `/soak*/`:

  before   87 files   memlab-drive.py MISSING   memlab-report.py MISSING
  after    89 files   memlab-drive.py present   memlab-report.py present

`git check-ignore` inside scripts/ indexes both, so the two recovered files
are exactly the ones git keeps. No file is lost in the other direction.

RED on the previous commit, four new tests, each direction pinned to a
`git check-ignore` verdict on an identical fixture:

  test_discover.c:1471: count == 1, expected 2 == 2   rooted_pattern_not_reanchored
  test_discover.c:1498: count == 2, expected 1 == 1   info_exclude_rooted_subpath
  test_discover.c:1526: count == 2, expected 1 == 1   intermediate_gitignore
  test_discover.c:1555: count == 0, expected 1 == 1   deeper_ancestor_negation_wins
  discover: 120 passed, 4 failed

The three tests already on the branch use unanchored patterns, which match at
any depth and so cannot see this bug; they pass before and after.

GREEN: discover 124 passed, 0 failed; discover + gitignore + pipeline +
git_context 425 passed, 0 failed.

Revert-check: reverting only the discover.c hunks and rebuilding puts the
four new tests RED again on the same four lines with the same counts
(120 passed, 4 failed); restoring returns 425 passed. Memory-core linter:
none grew (31 raw sites in discover.c before and after). clang-format clean.

Co-authored-by: Amir Fathi <amirfathi.me@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…check (#1735)

cbm_subtract_macro_invocation_regions asked "is this region a call to a
file-defined macro?" before "is it inside a function?". The first question
(cbm_span_is_macro_invocation) walks the source from byte 0 to find the
region's lines, so every file with parse-error regions paid
regions x file bytes, whatever its language and whether or not it has a
single function. On a multi-MB SQL dump that is up to 256 walks over the
whole file.

Both predicates are pure, so asking the cheap one first changes no answer:
a region no function encloses never touches the source, and the regions
that do share one cbm_line_offsets table built on first use. The table
gives exactly the span the walk found; the walk stays as the fallback when
the table cannot be allocated.

#1071 behaviour is unchanged: same regions, same verdicts. Indexing this
repository's src/ before and after gives identical graphs (7,339 nodes,
28,924 edges) and the same 46 parse_partial files with byte-identical
ranges.

Tests count the bytes read, never time them (test-seam counter
cbm_test_macro_line_scan_bytes):
- 40 top-level junk regions: 39,870 bytes read -> 0, still 40 regions
- 60 in-body real errors: 85,725 bytes read -> 2,801 (one table = file
  size), still 60 regions

Refs #1735

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A mysqldump-style .sql file is a few CREATE TABLEs followed by megabytes of
`INSERT ... VALUES (..),(..),...` rows. Tree-sitter built a full tree for
every row (~70 bytes of tree per source byte; 0.26 s/MB with MySQL \'
escapes, which the grammar does not know and error-recovers from) until the
5 s parse budget ran out, and the whole file, tables included, was skipped
as "parse timeout". The reporter's database/sql/world.sql was dropped this
way. The rows contribute nothing to the graph.

A linear, quote- and comment-aware pre-scan (internal/cbm/sql_values.c)
finds INSERT/REPLACE ... VALUES lists and excludes every tuple after the
first that consists only of literals: strings (standard '' and MySQL \'
escapes, X'..'/B'..'/N'.., _charset introducers), numbers, NULL, TRUE,
FALSE, DEFAULT. A tuple holding a subquery, a function call, an identifier
or a quoted identifier stays in the parse, and so does one whose string
spans a raw newline. The excluded bytes go to ts_parser_set_included_ranges
as the complement, so byte offsets and line/column positions of all kept
text are unchanged. The ranges are cleared right after the parse because
the parser is thread-local and reused. This is a syntax rule, not a work
cap: the same file always gives the same ranges, and the parse budget is
unchanged.

Measured on generated mysqldump-shaped dumps, release build, same machine,
index worker's own numbers (base = origin/main):

  dump            extraction   pipeline    peak RSS   graph
  8 MB std ''     1310 ->  23 ms  1431 -> 139 ms   625 -> 37 MB   identical
  8 MB MySQL \'   2712 ->  24 ms  2827 -> 146 ms   474 -> 37 MB   identical
  32 MB MySQL \'  5145 ->  82 ms  5467 -> 396 ms  1132 -> 62 MB   timeout -> indexed
  59 MB MySQL \'  5130 -> 138 ms  5667 -> 674 ms  1157 -> 89 MB   timeout -> indexed

"identical" = every stored node and edge, Module end line included. For the
32/59 MB dumps the base run skipped the file as "parse timeout" (Module +
3 Tables + their DEFINES/USAGE missing). The fixed graph matches the 8 MB
graph except for the Module's end line.

With standard escapes the extraction and the stored graph are identical
to the full parse. With MySQL \' escapes the full parse's error recovery
swallows rows next to a misread escape, a subquery row among them, and the
cut parse keeps those rows. So it loses nothing the full parse found
outside the dropped rows and recovers what the full parse lost. The only
things the full parse extracted INSIDE the dropped rows were junk usages
(DEFAULT and _binary read as identifiers, fragments of misread strings)
that name nothing.

Tests (parse_coverage, pipeline; a CBM_TEST_SQL_FULL_PARSE_ON seam turns the
exclusion off for one file so the same source can be compared both ways):
- scanner cases: comments, strings, dollar quotes, ON DUPLICATE KEY
  UPDATE, VALUES outside INSERT, unclosed tuples, exact kept positions
- extraction equals the full parse on a small dump (standard escapes);
  nothing lost outside dropped rows (MySQL escapes)
- the subquery and function-call rows are still parsed
- the tree does not grow when the row count doubles (a count, not a clock)
- a 25 MB MySQL dump under the production budget is indexed, not
  "parse timeout" (asserts the outcome, not the time)
- stored nodes and edges of a pipeline index equal the full parse's

Refs #1735

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A 4.7 MB binary Godot resource (piko_walk_mesh.res) never finished
indexing: `.res` maps to ReScript, so the resource was parsed as ReScript
source and the definitions pass spun. Three defects, each fixed:

1. Quadratic usage walk (the hang). The ReScript `let` binding check
   (is_first_named_part_of, shared with Julia, Typst, Elm, PureScript
   and Nickel) climbed from every named leaf with ts_node_parent, which
   descends from the root and scans each level's children. A binary
   file parses into a flat error tree (~170k root children per MB), so
   each climb cost O(root children) and the file O(n^2): 1 MB spent
   46 s in cbm_extract_unified, 4.7 MB never returned. The check now
   climbs the unified walk's own cursor (O(1) per hop), like the other
   occurrence classifiers; the node-based climb stays only for callers
   outside the walk. 1 MB: 46 s -> 0.03 s; 4.7 MB: 2.2 s end to end.

2. Quadratic lexing in error recovery. tree-sitter retries the lexer at
   every byte of an unparseable stretch with every external token
   valid; the ReScript scanner's template-string loop then ran to the
   next backtick/$/backslash/NUL (end of file if none) and discarded
   the result. That cost O(n^2) inside lexing, where the parse budget's
   progress callback never runs, so such files were dropped by the
   5 s CPU budget instead of parsed -- plain text too (100 KB of
   non-ReScript text: 15 s -> 0.18 s). Local scanner patch: skip the
   template branch when NEWLINE is also valid, which only the ERROR
   state allows. Valid files parse to identical trees. Recorded in
   vendored/grammars/MANIFEST.md.

3. Binary content parsed as source. `.res` is also Godot's binary
   resource format and the Windows compiled-resource format. Following
   the .m/.cls/.frm/.inc/.cfc precedent, cbm_disambiguate_res reads the
   first 4 KB and reports a file holding a NUL byte as unsupported;
   ReScript text never contains one.

Tests (deterministic work counters, no clocks):
- extraction::extract_rescript_let_bindings_use_the_walk_cursor --
  zero root-descending parent hops at 128 and 1024 statements
  (before: 515 / 4099).
- complexity::complexity_rescript_error_recovery_lexing_is_linear --
  lexer bytes pulled through a chunked TSInput, 4 KB vs 8 KB, ratio
  must be linear (before 3.99, after 2.00).
- language::lang_res_binary_resource_unsupported and
  lang_res_rescript_stays_rescript.

Fixes #2176

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cppcheck 2.20 (the version the lint job pins) reports
knownConditionTrueFalse on two comparators in src/mcp/mcp.c whose
tie-breakers compare one strcmp result after another, each call with
inline "x ? x : \"\"" fallbacks. Neither function differs between these trees; which of
them is flagged depends on the rest of the lint source list. With
scripts/lint.sh --ci and cppcheck 2.20.0 built from source:

- main itself fails on search_result_cmp ("file_order != 0 is always
  false");
- adding one more language grammar and its sources moves the finding
  to project_record_compare ("by_root != 0 is always false");
- carrying each comparator's order in one variable instead still fails,
  now as redundantAssignment on project_record_compare.

Both comparators now call nullable_strcmp(), which orders NULL like ""
exactly as the inline fallbacks did. lint-cppcheck passes again on
main and with the additional sources. Comparison order and results are
unchanged for every input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Sarlette <282189207+SarletteD@users.noreply.github.com>
cbm_registry_add takes the symbol's `name`, discards it, and derives the
bare-name lookup key from the QN's last dot segment. That makes the QN's tail
load-bearing for the by-name index every language shares, and for one grammar
the tail is not a name anybody writes. rust_cfg_qualified_name mints a
`#[cfg(test)]` twin as "proj.lib.add#cfg(test)"; simple_name() has no '#'
handling, so that function is filed under the whole string "add#cfg(test)",
which no bare `add` callee can reach, leaving a test-only twin of a resolved
function absent from the by-name index entirely.

The caller always knows the name, because it is the argument. So the index
takes a second key from it, only where the derived key carries a fence:

    const char *derived = simple_name(qualified_name);
    index_under_name(r, derived, owned_qn);
    if (name && name[0] && strchr(derived, '#') && strcmp(name, derived) != 0) {
        index_under_name(r, name, owned_qn);
    }

The gate keys on the fence rather than on the file's language, and that is the
part worth reviewing. It makes "unchanged for every other grammar" checkable:
the derived key is written unconditionally exactly as before, and every by-name
lookup keys through simple_name(), which splits on '.' and "::" only. A passed
name that differs from the derived key by anything other than a fence is
therefore a key no lookup can reach, so adding it would only widen the bucket
the scorer walks. A grammar that mints no '#' sees no new key at all.

Two of the rows a language gate would have had to cover turn out not to reach
this function. Registry membership is decided by cbm_label_is_registry_symbol
at all three production callers (pass_definitions.c, pass_parallel.c,
pipeline_incremental.c), and it admits neither "Module" nor "Section". So a
file's Module node never registers, and neither does a Markdown heading. What
does register, under label "Class", is the dotted-name direction:

  grammar     def->name                     QN tail
  HCL         resource.aws_instance.web     web
  TOML        tool.poetry.dependencies      dependencies
  INI         tool.isort                    isort

These are why the derived key stays unconditional. `instance =
aws_instance.web.id` reaches the block through the tail `web`, so re-keying the
index on the passed name would drop that edge outright. The fence gate leaves
all three byte-identical.

`name` is NULL or empty only for callers that have no symbol name to give;
those have the derived key and nothing else.

tests/test_registry.c: registry_indexes_by_passed_name_not_qn_tail resolves a
bare `add` against a cfg-fenced QN and is the reproducer, failing without this
commit. registry_indexes_a_dotted_name_under_its_tail_too pins the unconditional
derived key for the HCL and Module shapes.
tests/test_pipeline.c: pipeline_hcl_block_reference_resolves_to_its_block indexes
a two-resource Terraform file end to end and asserts the surviving USAGE edge.
The latter two pass without this commit and are guards: what would break them is
re-keying the by-name index, and an HCL block name is dotted where its QN tail is
not, so they pin the shape that regression would take.

A throwaway probe walked tests/grammar_cases.h (one fixture per grammar, all 162)
comparing def->name with simple_name(def->qualified_name), then targeted fixtures
for the dotted shapes above. HCL is the only fixture-level mismatch among
registry-eligible labels; TOML and INI mismatch on targeted fixtures. XML
namespaced elements (`ns:root`) do not mismatch, because ':' is not a QN
separator.

Signed-off-by: Chad <4307099+BobbieBarker@users.noreply.github.com>
Distilled from #1789 by Ulises Millan Guerrero, unchanged except for the
five openspec/ scaffolding files that could not land.

The indexing pipeline already logged its own phase error when artifact
export failed after publication, but the MCP failure path returned the
generic "Pipeline failed. Check repo_path" regardless. A caller whose
export failed was told to check a repository path that was fine, while the
one log line naming the real phase sat somewhere they were not looking.

The fix carries the failing phase out of the pipeline and into the MCP
error, so the message names artifact export when that is what failed. The
existing fail-hard contract is preserved -- this changes what the caller is
told, not whether the run fails.

Why this is a distill rather than the original: five of the nine files in
on main, no other contributor uses it, and adopting a spec framework is a
project-level decision rather than something that rides in on a bug fix.
That was explained on 2026-09-02 and the code half was approved on merit
the same day; the scaffolding is the only reason it did not land then.

Co-authored-by: Ulises Millan Guerrero <ulises.millanguerrero@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Three iterative AST walkers kept their pending nodes in fixed-size stacks
and silently stopped pushing children once full:

  - cbm_compute_complexity and count_branching_iter (helpers.c,
    BRANCHING_STACK_CAP 4096) -> cyclomatic, cognitive, loop and access
    metrics
  - extract_body_ident_tokens (extract_defs.c, BT_STACK 512) -> the
    body-token sample that feeds semantic search / SEMANTICALLY_RELATED

Children are pushed last-to-first, so a full stack dropped a wide body's
FIRST children. The metrics undercounted, and the token sampler read a
mid-body window instead of the first identifiers. That let a work cap decide
graph content.

Real input: tree-sitter-llvm's generated ts_lex (a 4654-case switch)
reported cyclomatic 11133 / cognitive 29303. It really has 8776 if + 4654
case + 1 default + 1 switch = 13432 branches, which is what it reports now
(cognitive 35639). On the Linux kernel no function reaches the old 4096 cap,
so complexity is byte-identical for every function. Three functions with
bodies over 512 pending nodes now sample their tokens from the start of the
body (for example rimob_panel_on), which moves 11 SEMANTICALLY_RELATED edges.
Index time is unchanged within noise (main 214.4/207.9 s, fix 197.6/192.2 s).

Fix: cbm_walk_stack_reserve()/cbm_walk_stack_release() in helpers.c. Each
walker keeps a small on-stack first chunk (512 frames) and spills to the heap
through the memory core (cbm_alloc/cbm_realloc/cbm_free, class EXTRACT) only
for deep or wide trees. Traversal order is unchanged. If the stack cannot
grow, the failure is logged ("extract.walk_stack_alloc_failed") and the
walker returns a documented sentinel instead of a truncated result:
CBM_WALK_METRIC_UNAVAILABLE (-1) for the metrics, or no token sample.

The body-token sampler's caps of 128 identifiers and a 2048-byte buffer are
its sampling contract and stay. The 512-entry stack was never part of that
contract, because it broke the "first identifiers in source order" property.

Tests (extraction suite): a C function with 5000 top-level if/while
statements must report exact cyclomatic/cognitive/loop counts through both
the extractor and cbm_count_branching, and a 600-call body must sample from
f0 on. All three are RED on main (4095 / 4095 / sample starting at f91) and
GREEN with the fix.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…onflict (#2277)

A client refused because another CBM build owns the account daemon printed
only "Close all CBM sessions and commands, then retry." The reporter had a
0.10.3 daemon kept alive by a running Codex session while starting 0.11.0
from a terminal, and nothing in the message said which process that was,
that an agent session held it, or that `daemon stop` exists.

The refusal itself is correct and stays unchanged: session-managed daemons
serving live sessions must never be killed by a newer client, and
cbm_daemon_conflict_format() is validated byte-for-byte across generations
on the HELLO wire, so its text cannot change without breaking old/new
daemon pairs.

The fix adds a separate, client-side remedy sentence built from the
cross-build STATUS probe (cbm_daemon_conflict_remedy_format): active daemon
pid, version, permanent vs session-managed, "already stopping", the pids of
the CBM sessions holding it, the likely cause (another editor or agent
launched a different install; compare `which -a codebase-memory-mcp` with
each client's MCP config) and `daemon stop` once no session uses it. When
STATUS does not answer, the remedy points at `daemon status`.

It is appended on every user-facing conflict exit: the MCP/CLI bootstrap
path (after the older-permanent auto-drain policy declines), the one-shot
CLI exact-build cohort refusal and the MCP client cohort refusal (the path
the reporter hit). Hooks skip the extra probe to keep their fail-open
budget. The probe uses the `daemon status` budget (3 s): the daemon hashes
the ~300 MB peer image before answering, which overran the 1 s connect
budget in the real-binary proof.

No identity or admission check changes; STATUS is already the
authenticated, cross-build observability op behind `daemon status`.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Upgrading from v0.10.8 to v0.11.0 on a machine with OpenCode reported all
three OpenCode subagent profiles as "preserved modified profile" plus an
op=agent_install error each, and the activation stopped ("one or more agent
configurations failed; the published/current executable was kept"), although
nobody had edited those files.

Root cause: #1933 (v0.11.0) added the tool_search / tool_search_regex
permissions to the rendered OpenCode profile. Install and uninstall recognise
a profile as ours only when it matches the current rendering or one of the
known released renderings (the other access mode, the v0.9.1-rc.1 Codex
shape, the pre-tier Verify file). The v0.10.8 OpenCode shape was not in that
set, so cbm_text_migrate_owned_document classified the untouched v0.10.8
files as user-modified. Reproduced end to end: install with the v0.10.8
release binary, then install with v0.11.0/main, into an isolated HOME; the
three profiles are 1729/1880/1910 bytes, the same sizes as in the report.

cbm_render_graph_profile_opencode_v0108() renders the pre-#1933 shape
(both access modes), and install/uninstall now list it as a released
rendering, so those files are upgraded in place and uninstall removes them
as owned. A profile the user actually edited is still preserved. The
released-list assembly that install and uninstall duplicated moves into one
helper, and the two legacy renderers share one implementation (cli.c drops
three raw free sites; the memory-core baseline is tightened 173 -> 170).

The Codex and OpenCode op=mcp_install errors in the same report do not
reproduce from a pristine v0.10.8 config and depend on the reporter's own
config.toml / opencode.json; they are not addressed here.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Add cbm_scope_lookup_binding so callers can read lexical presence, type, and callable identity in one traversal. Route Python expression, call, and callable-reference resolution through that complete binding and use the existing checked bind APIs for allocation-failure detection.

The neutral 11-pair production matrix kept exact output parity. Python scale latency was 4.33% below merge parent 07255cf and 61.01% below upstream parent 0c64de9; incremental instructions were 3.09% and 54.07% lower. Focused ASan/UBSan scope and Python suites passed 33 and 125 tests.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
(cherry picked from commit 4d3a103)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Previous behavior: cbm_scope_lookup(), cbm_scope_contains(), and cbm_scope_lookup_callable() called an out-of-line complete-binding scan after the Python lookup consolidation.

What changed:
- internal/cbm/lsp/scope.h defines cbm_scope_lookup_binding() as the one static-inline scope-chain scan.
- internal/cbm/lsp/scope.c keeps the type, presence, and callable wrappers on that shared implementation without another function boundary.

Why: Python still reads type and callable identity in one traversal, while existing language resolvers retain the direct hot path used before the consolidation.

Verification:
- CBM_ONLY_SUITE=scope build/c/test-runner: 33 passed
- CBM_ONLY_SUITE=cs_lsp build/c/test-runner: 62 passed
- CBM_ONLY_SUITE=py_lsp build/c/test-runner: 125 passed
- neutral 11-pair extraction matrix: output parity; C# latency -2.97% vs first parent and -1.64% vs upstream; retired instructions +0.29% vs first parent and -1.01% vs upstream; Python scale latency -4.25% and instructions -2.81% vs first parent

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
(cherry picked from commit 150c88e)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…anner patch

security-static (scripts/security-vendored.sh) failed on this branch:
the #2176 fix patches internal/cbm/vendored/grammars/rescript/scanner.c
(skip the template-string branch in the error-recovery state) and records
that patch in vendored/grammars/MANIFEST.md, but the checked-in digests in
scripts/vendored-checksums.txt still described the unpatched files.

Both changes are intentional and reviewed in the parent commit; this
records their SHA-256 via `scripts/security-vendored.sh --update`. Only
the two affected lines change, and the integrity check passes again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Two CI failures, both in the tests this branch added:

1. Windows (MinGW clang64) did not compile the test runner:
   test_parse_coverage.c and test_pipeline.c called POSIX setenv/unsetenv,
   which the Windows CRT does not declare. They now use the repo's
   cbm_setenv/cbm_unsetenv from foundation/compat.h, as the other suites do.

2. sql_dump_of_many_megabytes_is_indexed_not_timed_out_issue1735 failed on
   the ubuntu-24.04-arm ASan leg with "parse timeout". It extracted a 26 MB
   dump under CBM_EXTRACT_BUDGET, which is CPU time, so the verdict
   depended on runner speed. It passed on the fast legs and tripped on the
   slow one, and the early FAIL return leaked the result (the LSan report
   in that log). The property behind "indexed, not timed out" is a count:
   parse work is set by the statements, not the rows. The test now extracts
   unbudgeted, asserts the outcome (the view and the subquery usage are in
   the graph), and bounds the tree by the same 330 statements with 4 rows
   each. Results are freed before any assertion.

   Binds to the fix: forcing the full parse for the big dump through the
   CBM_TEST_SQL_FULL_PARSE_ON seam fails the bound (5,158,222 tree nodes
   vs a bound of 52,872); with the row exclusion it passes.

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

# Conflicts:
#	tests/test_pipeline.c
fix(rescript): stop binary .res resources hanging the indexer (#2176)
fix(sql,coverage): make large SQL dumps cheap to parse (#1735)
fix(daemon): name the conflicting daemon and the way out on a build conflict (#2277)
fix(install): migrate v0.10.8 OpenCode agent profiles on upgrade (#2264)
…e-name

fix(registry): reach a cfg-fenced function from its bare name
fix(tests): create nested pipeline fixtures reliably
…rfacing

fix(mcp): name the artifact phase when a post-publish export fails
…t-cap

fix(extract): AST walker stacks grow instead of silently capping content
fix(store): report a failed COUNT read instead of returning zero
style(mcp): compare nullable strings through one helper
fix(cli): emit the OpenCode plugin as a V2 default export
…t-inherited

fix(discover): honor an enclosing repo's .gitignore for a git-less subfolder
perf(lsp): collapse repeated Python scope-chain lookups
@pull pull Bot locked and limited conversation to collaborators Sep 28, 2026
@pull pull Bot added the ⤵️ pull label Sep 28, 2026
@pull
pull Bot merged commit 80eb92a into TKaxv-7S:main Sep 28, 2026
2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants