Skip to content

feat(core): filter discover actions across namespaces - #112

Closed
kunkunGames wants to merge 3 commits into
tumourlove:masterfrom
kunkunGames:agent/core-minimal-find
Closed

feat(core): filter discover actions across namespaces#112
kunkunGames wants to merge 3 commits into
tumourlove:masterfrom
kunkunGames:agent/core-minimal-find

Conversation

@kunkunGames

@kunkunGames kunkunGames commented Jul 25, 2026

Copy link
Copy Markdown

Goal

Extend monolith.discover with the smallest possible change so that a non-empty filter without a namespace searches matching actions across the existing live action registry.

Plain-language explanation

A client that knows only part of a capability name can find candidates with one existing discover call instead of iterating over every namespace. This reuses the current registry, filtering, and pagination behavior; it does not add a new search engine or MCP tool.

Improvements

  • discover(filter="...") searches actions across all namespaces.
  • The existing case-insensitive substring rule and registry order are preserved.
  • Existing offset, limit, terse descriptions, and detail/verbose behavior are reused.
  • The 12 input-schema fields are synchronized across the Python and native proxy seeds and the stale tools/list overlay.
  • Slice arithmetic is protected from overflow even with a very large limit.

Before → After

Before After
Clients could not search all actions without first knowing a namespace A non-empty filter searches actions across all namespaces
A stale tools/list cache could omit current discover inputs Current seed fields are overlaid and deduplicated
One-line description formatting was duplicated A shared terse-description helper is used

Side-effect analysis

  • No arguments, a limit alone, or a whitespace-only filter still returns the existing namespace inventory.
  • Namespace-scoped discovery and its error contract are unchanged.
  • No semantic ranking, proxy dispatch change, alternate registry, or new public tool is introduced.
  • Stale-cache repair updates only the monolith_discover descriptor and preserves other Editor tools and live metadata.

Current status

Re-reviewed the already minimized public head, 5fdfbe0951782e66095d50bbb11fab97dbc318a9, against the maintainer-requested scope. All 15 review threads are resolved, and the latest Codex review of this exact head found no material issue. This maintenance pass required no additional code change.

Verification

  • Protected UE 5.8 Editor build: PASS.
  • Monolith.Discover: 9/9 PASS, with 0 test warnings, 0 errors, and process exit 0.
  • Latest report: D:\P4\MonolithPR112ProtectedHost\Saved\Automation\PR112MaintenanceFinalUE58\index.json.
  • Isolated UE 5.7 Editor build and the same 9/9 automation tests: PASS.
  • Python proxy syntax, seed overlay, stale-cache refresh, metadata preservation, deduplication, and idempotence checks: PASS.
  • Native C++17 proxy build and Python/native 12-field schema deep equality: PASS.
  • Differential run with the same current static-check configuration: base and branch each reported 36 blockers and 802 advisories; 0 findings were introduced or resolved by this PR.
  • git diff --check: PASS. The worktree was clean and the branch was 0 commits behind the latest tumourlove/master.
  • Verification record: Docs/testing/2026-07-26-cross-namespace-discover.md.

WorkFingerprint

  • agent: Codex
  • category: discovery / orchestration
  • module: MonolithCore
  • component/action/helper: monolith.discover, terse action description, proxy seed overlay
  • intended files: MonolithCoreTools.cpp, MonolithToolText.{h,cpp}, MonolithDiscoverTerseTest.cpp, proxy seed/schema surfaces, matching Core docs
  • risk type: discovery regression, stale schema cache, pagination overflow
  • public API impact: yes, additive behavior on the existing filter
  • docs/spec impact: yes

Duplicate check

On 2026-07-27, open PRs #104, #112, #113, and #114, their related remote branches, and their actual changed files were checked again. #104 targets updater binary selection, #113 targets project FTS search and repair, and #114 targets persistent activation and the index-writer lifecycle. #114 overlaps physically in MonolithCoreTools.cpp and Core documentation, but it changes different actions and sections; no implementation overlaps this PR's cross-namespace discovery contract. Mechanical rebasing may be required depending on merge order, but semantic ownership remains separate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e4d0e0054

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithCoreTools.cpp Outdated
Comment thread Scripts/monolith_proxy.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af26d3f63f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithFindTool.cpp Outdated
Comment thread Source/MonolithCore/Private/MonolithFindTool.cpp Outdated
Comment thread Scripts/monolith_proxy.py Outdated
@kunkunGames kunkunGames changed the title feat: add bounded monolith.find action search feat(core): add monolith_find MCP action for bounded task-text action search Jul 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9d5f942f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithFindTool.cpp Outdated
@kunkunGames

Copy link
Copy Markdown
Author

@codex review

All five comments from your pass are addressed, and one follow-up defect found afterwards is fixed in c9d5f942: project was in the filler list but is also a registered namespace, so naming a namespace lost that narrowing signal.

Worth re-checking specifically, since the obvious fix is wrong: exempting registry identifiers from the filler list globally reintroduces the original defect you reported — build the project then requires project to be covered and no longer reaches editor.trigger_build. That was verified by Monolith.Find.NaturalTaskText failing. Filler is now decided per candidate: filler tokens still create no coverage requirement, but a candidate whose own namespace/action tokens contain one claims it as +0.01 corroboration capped at 0.90, which sits above the best coverage score (0.88) and below the weakest phrase score (0.92).

@kunkunGames

Copy link
Copy Markdown
Author

Conflict/merge-order note for my six open PRs is in #104 (comment #104 (comment)).

Short version: a pairwise trial merge shows these PRs conflict only in CHANGELOG.md (plus Docs/specs/SPEC_MonolithCore.md for the #104 × #112 pair) — no source conflicts. Merge in any order that suits your batch; I will rebase the remainder and re-verify with a build plus automation tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab60f8424e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithFindTool.cpp Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 279bab051e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithFindTool.cpp Outdated

kunkunGames commented Jul 26, 2026

Copy link
Copy Markdown
Author

@codex review

Please re-review the latest head, 1ac77c26. All existing and newly added inline findings have been addressed with replies and verification, and all threads are resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ac77c26b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithFindTool.cpp Outdated
Comment thread Source/MonolithCore/Private/MonolithFindTool.cpp Outdated
Comment thread Source/MonolithCore/Private/MonolithFindTool.cpp Outdated

Copy link
Copy Markdown
Author

@codex review

Latest head: a8d4fde2a5cb30f8bb1fc81c47a1392be2c9dfa8. All current review threads are resolved; exact-head UE 5.8 build and the focused 23/23 automation suite pass.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: a8d4fde2a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@tumourlove

Copy link
Copy Markdown
Owner

Queued — the gap you identified is real and I am taking it, though in a much smaller form than this PR.

Verified: discover's filter predicate sits inside the !FilterNamespace.IsEmpty() branch at MonolithCoreTools.cpp:322-329, and the no-namespace branch returns namespaces and counts only. So there genuinely is no cross-namespace keyword search today and you have to guess the namespace first. That is worth closing.

What I am not taking is the ranking engine. Handing an MCP client a filtered candidate list lets its own model rank semantically, which beats six hand-tuned float tiers and a curated English filler list — and the tiers become a permanent maintenance surface ("why did find rank X above Y"). The plan is to extend the no-namespace branch to honour filter plus offset/limit, reusing the existing predicate and pagination. Roughly thirty lines, no new tool, no new schema surface, no proxy change.

Two things from the review worth passing on regardless of what ships:

The sort predicate at MonolithFindTool.cpp:726-737 uses FMath::IsNearlyEqual for equality. That is non-transitive across three near-scores, so it is not a strict weak ordering and TArray::Sort is undefined behaviour on it. Real bug, worth knowing wherever else that pattern gets used.

FuzzyNeedle at :477 is left un-normalized while everything else is normalized, so it keeps the trailing ?. Levenshtein of multi-word task text against a short action name never clears the threshold — meaning the most expensive stage in the pipeline is a guaranteed no-op for the headline "build the project?" case.

Also taking the MonolithToolText hoist and the README create_asset correction; the latter shipped in v0.21.3 already. Leaving this open until the discover change lands, and you will be credited.

@kunkunGames
kunkunGames force-pushed the agent/core-minimal-find branch from a8d4fde to 379993c Compare July 26, 2026 14:44
@kunkunGames kunkunGames changed the title feat(core): add monolith_find MCP action for bounded task-text action search feat(core): filter discover actions across namespaces Jul 26, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 379993c3c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithCoreTools.cpp
@kunkunGames
kunkunGames force-pushed the agent/core-minimal-find branch 2 times, most recently from 2ea2243 to 5437cdc Compare July 26, 2026 15:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5437cdc43a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Scripts/monolith_proxy.py Outdated
@kunkunGames
kunkunGames force-pushed the agent/core-minimal-find branch from 5437cdc to 63be521 Compare July 26, 2026 16:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63be521be1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Scripts/monolith_proxy.py Outdated
@kunkunGames
kunkunGames force-pushed the agent/core-minimal-find branch from 63be521 to 936e2c2 Compare July 26, 2026 16:27

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 936e2c2e8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Scripts/monolith_proxy.py Outdated
@kunkunGames
kunkunGames force-pushed the agent/core-minimal-find branch from 936e2c2 to 0323239 Compare July 26, 2026 16:55
@kunkunGames
kunkunGames force-pushed the agent/core-minimal-find branch from 0323239 to 6d6a988 Compare July 26, 2026 17:47
Drops everything outside the minimal shape agreed in review: extend the
no-namespace branch to honour `filter` plus `offset`/`limit`, reusing the
existing predicate and pagination.

Removed:

- The five response-shaping properties (`_fields`, `_omit`, `_row_fields`,
  `_path_fields`, `_compact_json`) added to the registered `discover` schema.
  `FMonolithParamSchema` still accepts them universally; no other registered
  action advertises them, so advertising them only here was new schema surface
  rather than a fix.
- The proxy-seed synchronization in `Scripts/monolith_proxy.py` and
  `Tools/MonolithProxy/monolith_proxy.cpp`, including the cached-descriptor
  overlay. Both files are now byte-identical to master. The offline seed
  already lags the live schema on master (it predates `filter`, `offset`,
  `limit`, `detail`, and `verbose`), so this branch neither introduces nor
  widens that drift; refreshing the seeds belongs to its own change.

The `detail=true` test assertion that depended on the removed properties now
asserts the reused `filter`/`offset`/`limit` contract instead, so the
advertised schema still cannot drift from the handler.

Verified at this exact head on both engines:

- UE 5.8 (`MonolithPR112ProtectedHost`): editor build Succeeded,
  `Monolith.Discover` 9/9, 0 failed, exit 0.
- UE 5.7 (`MonolithPR112FinalUE57Host`): editor build Succeeded,
  `Monolith.Discover` 9/9 under `-RenderOffscreen`, 0 failed, exit 0.
@kunkunGames

Copy link
Copy Markdown
Author

Rescoped to exactly what you described, in 00364b9e. The branch is now the ~30-line version: extend the no-namespace branch to honour filter plus offset/limit, reusing the existing predicate and pagination.

Removed since your comment:

  • The ranking engine is gone. MonolithFindTool.cpp no longer exists in the tree — no float tiers, no filler list, no monolith_find tool. Both bugs you called out there went with it, so there is nothing left to fix in that file; worth noting the non-transitive FMath::IsNearlyEqual comparator pattern separately though, since you're right that TArray::Sort on it is UB wherever else it appears.
  • The proxy change is gone. Scripts/monolith_proxy.py and Tools/MonolithProxy/monolith_proxy.cpp are byte-identical to master again. Codex had asked for seed synchronization; you said no proxy change, and you're the one merging. Worth knowing the seed already lags master independently of this PR — it predates filter, offset, limit, detail, and verbose — so a cold-start client can't construct the new call until the first live tools/list. That is pre-existing drift this branch neither introduces nor widens, and it belongs to its own change.
  • The five response-shaping properties are gone (_fields, _omit, _row_fields, _path_fields, _compact_json). FMonolithParamSchema still accepts them universally; no other registered action advertises them, so advertising them only on discover was new schema surface rather than a fix.

Kept: the no-namespace branch, the MonolithToolText hoist you said you were taking, and one real fix — slice arithmetic is now SliceStart + FMath::Min(Limit, Total - SliceStart) instead of FMath::Clamp(SliceStart + Limit, ...), which signed-overflows on a large limit.

Diff against master is 9 files, +430/−132, and the only source file touched besides the new MonolithToolText pair is MonolithCoreTools.cpp.

Verified at this exact head on both engines:

UE 5.8 UE 5.7
Editor build Result: Succeeded Result: Succeeded
Monolith.Discover 9/9, 0 failed, exit 0 9/9, 0 failed, exit 0 (-RenderOffscreen)

The detail=true test that used to assert the removed properties now asserts the reused filter/offset/limit contract instead, so the advertised schema still can't drift from the handler.

The cross-namespace branch answered "which actions match" but not the question
that actually blocks a caller: which namespace to open. With a small `limit`
that was unanswerable without paging the whole candidate list.

The filtered response now also carries `matched_namespaces` — every namespace
containing at least one match, each with its `match_count`, in registry order.
It is computed before pagination, like `total`, so `limit=1` still describes the
whole filtered set. The intended flow is: filter, read `matched_namespaces`, pick
a namespace, then call `monolith_discover(namespace="<ns>")`.

`match_count` is the size of the filtered set within a namespace, not a relevance
score. Namespaces are never ordered by it, and the change introduces no distance
function, weight, scoring tier, or curated word list. Semantic ranking stays a
client concern, which is the whole point of handing back a bounded candidate list.

No new request parameter: this is a response field on an existing mode. The
no-argument namespace inventory is unchanged and does not carry it.

Verified at this head on both engines: editor builds succeed and
`Monolith.Discover` passes 9/9 with 0 failed on UE 5.7 (`-RenderOffscreen`) and
UE 5.8. Coverage asserts every row is a distinct namespace with a positive count,
that the counts sum to the pre-pagination total under `limit=1`, and that the
no-argument inventory does not gain the field.
@kunkunGames

Copy link
Copy Markdown
Author

Your point about the ranking engine landed, and following it through exposed that the trimmed version had only solved half the problem. Fixed in 5fdfbe09.

The branch answered which actions match, but the question that actually blocks a caller is which namespace to open — and with a small limit that was unanswerable without paging the whole candidate list. So discover(filter=...) now also returns:

"matched_namespaces": [
  { "namespace": "blueprint", "match_count": 7 },
  { "namespace": "ui",        "match_count": 2 }
]

Every namespace containing at least one match, in registry order, computed before pagination like total — so limit=1 still describes the whole filtered set. The flow is: filter, read matched_namespaces, pick one, then monolith_discover(namespace="<ns>").

To be explicit about the thing you rejected, since a count next to a name can look like a score: match_count is the size of the filtered set within that namespace. Namespaces are emitted in registry order and are never sorted by it. There is no distance function, no weight, no float tier, and no curated word list anywhere in the branch — the two bugs you found in the old MonolithFindTool.cpp are moot because that file no longer exists in the tree. Semantic ranking stays the client's job, which is the point of handing back a bounded candidate list rather than an ordered one.

It is also not new request surface: matched_namespaces is a response field on an existing mode, no parameter was added, and the no-argument namespace inventory is unchanged and does not carry it.

Verified at this head on both engines:

UE 5.8 UE 5.7
Editor build Result: Succeeded Result: Succeeded
Monolith.Discover 9/9, 0 failed, exit 0 9/9, 0 failed, exit 0 (-RenderOffscreen)

Coverage asserts every row is a distinct namespace with a positive count, that the counts sum to the pre-pagination total under limit=1, and that the no-argument inventory does not gain the field.

Diff against master is 9 files; the only source touched is MonolithCoreTools.cpp plus the MonolithToolText hoist you said you were taking. Proxies remain byte-identical to master.

@kunkunGames

Copy link
Copy Markdown
Author

@codex review

Latest head 5fdfbe09. Since your last pass on 936e2c2e the branch was rescoped to the maintainer's stated shape — the ranking engine and both proxy files are gone — and then gained matched_namespaces so a filtered call answers which namespace to open without any scoring. UE 5.7 and 5.8 both build with Monolith.Discover 9/9.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 5fdfbe0951

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kunkunGames kunkunGames changed the title feat(core): filter discover actions across namespaces 🔎 Codex: filter discover actions across namespaces Jul 27, 2026
@kunkunGames kunkunGames changed the title 🔎 Codex: filter discover actions across namespaces feat(core): filter discover actions across namespaces Jul 27, 2026
tumourlove added a commit that referenced this pull request Aug 1, 2026
…ading as zero results

Three changes to discovery and project search, plus their offline mirrors.

CROSS-NAMESPACE DISCOVER (#112)

discover's filter predicate lived inside the namespace branch, and the
no-namespace branch never parsed filter, offset, limit or detail at all. So
discover(filter="set_property") with no namespace was SILENTLY IGNORED -- the
caller got the full namespace inventory back and no error, and finding a
capability by partial name meant iterating every namespace by hand.

A non-empty filter with no namespace now searches the whole registry in
registry order, tagging each row with its owning namespace and reporting
matched_namespaces pre-pagination, so "which namespace owns this" stays
answerable even when rows are capped. Unlike the per-namespace path this one
is not naturally bounded -- ~150 actions worst case there versus ~1,400+ here
-- so an absent limit caps at 50 while an explicit limit=0 still means ALL,
matching the existing contract. Uncapped it would emit a row per match across
the entire registry, from the tool whose terse redesign existed to cut payload.

PAGINATION OVERFLOW (#112, independently shippable)

SliceEnd = FMath::Clamp(SliceStart + Limit, ...) formed SliceStart + Limit as
int32 before clamping. offset=1 with limit=2147483647 signed-overflows -- and
that call is well-formed, because TryGetNumberField range-checks against
TNumericLimits<int32> and so accepts INT32_MAX verbatim while rejecting
anything larger. On MSVC it wrapped negative, Clamp collapsed the slice, and
the caller got an EMPTY actions array plus a NEGATIVE next_offset. Min() is
now taken first so every intermediate stays in range, and next_offset emits
SliceEnd, which is value-identical and structurally overflow-free.

SEARCH ERROR CLASSIFICATION AND VALIDATION (#113 slices)

while (Step() == Row) treats a statement error as end-of-results, and
Stmt.Create / SetBindingValueByIndex returns were unchecked, so a genuine
FTS5 syntax error or a prepare failure surfaced as "no matches" -- the caller
could not tell a bad query from an empty index. Caller errors now return
-32602 and storage or schema failures -32603, with LIMIT bound as a parameter
rather than formatted into the SQL. Adds query validation, a limit clamp and
a 4096-character query cap.

DELIBERATE DEVIATION FROM THE PLAN, on review of live SQLite behaviour: the
plan's literal classification would have regressed column-qualified search.
search runs two independent statements, and a query like node_name:Branch
errors on the asset table before returning real hits from the node table --
so it works today only because the error is swallowed as end-of-results.
Treating any per-table error as fatal would turn a working feature into a
hard error. A table reporting "no such column" is therefore treated as not
applicable and skipped; the query is only rejected when BOTH tables reject it,
or when the error matches the table-independent syntax patterns. That
behaviour is now documented rather than accidental, and has a regression test.

PR #113's 1449-line FTS5 grammar parser is NOT taken. It introduced an
unguarded recursive AST teardown with no query-length cap, which kills the
editor process at roughly 32KB of chained terms -- reachable over MCP, on the
game thread, with no recovery -- and a 15,935-query differential against real
SQLite showed the safety property it claimed was actually delivered by an
eight-line error classifier. Queries continue to bind straight into MATCH ?,
where SQLite's own bounded grammar handles them.

Reported and prototyped by @kunkunGames (#112, #113).
@tumourlove

Copy link
Copy Markdown
Owner

Shipped in v0.22.0. I land contributor fixes as my own commits rather than merging the branch — I keep the shipped history single-author for release integrity, and credit you in the release notes instead. No reflection on the patch.

Thanks for rescoping this yourself after the earlier review — the trimmed version is the right change, and 00364b9e cut exactly what needed cutting. My notes from the first pass were stale by the time I got back to it.

The gap was as you described: discover's filter predicate sat inside the namespace branch and the no-namespace branch never parsed filter, offset, limit or detail at all — so a filter with no namespace was silently ignored and you got the namespace inventory back with no error.

Two deviations. I kept the terse-description helper as a file-static rather than extracting a new header/source pair — it has exactly one call site in one translation unit, and two new files add surface to the release's full-unity collision gate for no gain. And I added a default cap: absent limit caps at 50 on the cross-namespace path, while an explicit limit=0 still means all. Unlike the per-namespace path this one is not naturally bounded — ~150 actions worst case there versus ~1,400+ here — and an uncapped discover(filter="set") would emit a row per match across the whole registry, from the tool whose terse redesign existed specifically to cut payload. matched_namespaces is pre-pagination and never truncated, so "which namespace owns this" stays answerable even when rows are capped.

Your pagination overflow fix shipped as its own change and it is a real bug in released code: SliceStart + Limit was formed as int32 before clamping, so offset=1 with limit=2147483647 signed-overflowed to an empty result and a negative next_offset. Reachable from a well-formed call, since TryGetNumberField range-checks against TNumericLimits<int32> and so accepts INT32_MAX verbatim while rejecting anything larger. There is a regression test that asserts the specific wrong values, since a test that only checked "succeeds" would have passed against the bug.

@tumourlove tumourlove closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants