Skip to content

fix: construct admission control independently of smart extraction#940

Draft
gorkem2020 wants to merge 5 commits into
CortexReach:masterfrom
gorkem2020:fix/admission-without-smart-extraction
Draft

fix: construct admission control independently of smart extraction#940
gorkem2020 wants to merge 5 commits into
CortexReach:masterfrom
gorkem2020:fix/admission-without-smart-extraction

Conversation

@gorkem2020

Copy link
Copy Markdown
Contributor

Admission control could silently do nothing when smartExtraction: false, even with admissionControl.enabled: true. The AdmissionController was only ever constructed inside the smartExtraction init block, so any write path other than SmartExtractor had no controller to use and stored content ungated.

This decouples admission-controller construction from the extraction engine choice, so its availability depends only on admissionControl.enabled.

Changes

  • src/admission-control.ts: adds createAdmissionController(store, llm, config, debugLog), a factory that builds a controller purely from config, independent of any extractor.
  • src/smart-extractor.ts: SmartExtractor no longer builds its own AdmissionController from config. It now accepts a pre-built controller via a new admissionController constructor option and uses whatever it is given (or none).
  • index.ts:
    • The LLM client (and now the admission controller) is constructed whenever smartExtraction !== false or admissionControl.enabled === true, instead of only the former.
    • The standalone controller is exposed on the plugin's singleton state alongside smartExtractor, so other write paths (for example a reflection-mapped-rows gate, or a regex-fallback gate) can reference it directly instead of reaching through smartExtractor.
    • Extends the existing plugin registered log line with an admissionControl: ON|OFF marker, making the fix directly observable.

Behavior

  • smartExtraction: false, admissionControl.enabled: true → a controller is now constructed and available.
  • smartExtraction: false, admissionControl.enabled: false → still fully off, content still stored ungated. That is the configured meaning of "off," unchanged.
  • smartExtraction: true → decisions and audits are unchanged; the controller is built from the exact same store/llm/config/debugLog inputs it always was, just at a different call site.

Tests (all new, TDD red-first)

  • test/admission-controller-standalone.test.mjs: createAdmissionController returns null when disabled, and a working controller when enabled.
  • test/smart-extractor-admission-controller-injection.test.mjs: SmartExtractor calls whatever controller it is given, and skips gating entirely when none is configured (today's off-behavior, unchanged).
  • test/admission-without-smart-extraction.test.mjs: registers the plugin with smartExtraction: false + admissionControl.enabled: true and asserts the new log marker; also covers the fully-off and smartExtraction: true cases as regression guards.

Full local test chain green (npm test, minus the pre-existing host-Ollama port conflict on 11434, unrelated to this change) and npm run build green.

@gorkem2020
gorkem2020 force-pushed the fix/admission-without-smart-extraction branch from f8e40ac to 2eceb34 Compare July 15, 2026 21:02
gorkem2020 added a commit to gorkem2020/memory-lancedb-pro that referenced this pull request Jul 15, 2026
processCandidate's dedup-merge branch called handleMerge and then
unconditionally incremented stats.merged, even though handleMerge has two
paths that persist nothing: the existing row's getById read failing
(falls back to queuing the candidate as a new entry instead) and the
merge-memory LLM call returning null/unparseable (logs and returns with
the existing row untouched). handleProfileMerge's own call to handleMerge
had the identical bug one layer up — it always reported "merged" back to
processCandidate's profile branch regardless of what handleMerge actually
did.

handleMerge now returns which of its three real outcomes happened
("merged" | "created" | "llm-failed") instead of void, and both call
sites branch on it: a genuine merge counts as merged, the read-failure
fallback counts as created (a new entry really was queued), and an
LLM failure counts as neither — handleMerge already logs that case, so
this doesn't lose visibility, just stops claiming a merge happened when
nothing was written.

Picked this branch over fix/admission-without-smart-extraction (CortexReach#940) for
this fix: CortexReach#941 is already this batch's "truthful accounting" branch (item
3's malformed-batch-entry fix and item 6's transcript removal are both
about not claiming an LLM call did more than it did), so this fix extends
the same theme, while CortexReach#940's own scope (decoupling AdmissionController
construction from SmartExtractor) has no thematic overlap with merge
outcome accounting. Both branches carry the identical bug in identical
code; either would have worked mechanically.

Red-proved: both new failure-path tests failed against the prior
unconditional-increment code (stats.merged was 1 instead of 0 in both
cases) before the fix, and pass after. The happy-path test confirms
today's correct behavior stays correct (handleMerge's own two
store.update calls — merge content, then a best-effort support-stats
update — both still fire).
gorkem2020 added a commit to gorkem2020/memory-lancedb-pro that referenced this pull request Jul 17, 2026
processCandidate's dedup-merge branch called handleMerge and then
unconditionally incremented stats.merged, even though handleMerge has two
paths that persist nothing: the existing row's getById read failing
(falls back to queuing the candidate as a new entry instead) and the
merge-memory LLM call returning null/unparseable (logs and returns with
the existing row untouched). handleProfileMerge's own call to handleMerge
had the identical bug one layer up — it always reported "merged" back to
processCandidate's profile branch regardless of what handleMerge actually
did.

handleMerge now returns which of its three real outcomes happened
("merged" | "created" | "llm-failed") instead of void, and both call
sites branch on it: a genuine merge counts as merged, the read-failure
fallback counts as created (a new entry really was queued), and an
LLM failure counts as neither — handleMerge already logs that case, so
this doesn't lose visibility, just stops claiming a merge happened when
nothing was written.

Picked this branch over fix/admission-without-smart-extraction (CortexReach#940) for
this fix: CortexReach#941 is already this batch's "truthful accounting" branch (item
3's malformed-batch-entry fix and item 6's transcript removal are both
about not claiming an LLM call did more than it did), so this fix extends
the same theme, while CortexReach#940's own scope (decoupling AdmissionController
construction from SmartExtractor) has no thematic overlap with merge
outcome accounting. Both branches carry the identical bug in identical
code; either would have worked mechanically.

Red-proved: both new failure-path tests failed against the prior
unconditional-increment code (stats.merged was 1 instead of 0 in both
cases) before the fix, and pass after. The happy-path test confirms
today's correct behavior stays correct (handleMerge's own two
store.update calls — merge content, then a best-effort support-stats
update — both still fire).
@gorkem2020
gorkem2020 force-pushed the fix/admission-without-smart-extraction branch from 2eceb34 to accc6d2 Compare July 17, 2026 21:36
gorkem2020 added a commit to gorkem2020/memory-lancedb-pro that referenced this pull request Jul 18, 2026
processCandidate's dedup-merge branch called handleMerge and then
unconditionally incremented stats.merged, even though handleMerge has two
paths that persist nothing: the existing row's getById read failing
(falls back to queuing the candidate as a new entry instead) and the
merge-memory LLM call returning null/unparseable (logs and returns with
the existing row untouched). handleProfileMerge's own call to handleMerge
had the identical bug one layer up — it always reported "merged" back to
processCandidate's profile branch regardless of what handleMerge actually
did.

handleMerge now returns which of its three real outcomes happened
("merged" | "created" | "llm-failed") instead of void, and both call
sites branch on it: a genuine merge counts as merged, the read-failure
fallback counts as created (a new entry really was queued), and an
LLM failure counts as neither — handleMerge already logs that case, so
this doesn't lose visibility, just stops claiming a merge happened when
nothing was written.

Picked this branch over fix/admission-without-smart-extraction (CortexReach#940) for
this fix: CortexReach#941 is already this batch's "truthful accounting" branch (item
3's malformed-batch-entry fix and item 6's transcript removal are both
about not claiming an LLM call did more than it did), so this fix extends
the same theme, while CortexReach#940's own scope (decoupling AdmissionController
construction from SmartExtractor) has no thematic overlap with merge
outcome accounting. Both branches carry the identical bug in identical
code; either would have worked mechanically.

Red-proved: both new failure-path tests failed against the prior
unconditional-increment code (stats.merged was 1 instead of 0 in both
cases) before the fix, and pass after. The happy-path test confirms
today's correct behavior stays correct (handleMerge's own two
store.update calls — merge content, then a best-effort support-stats
update — both still fire).
gorkem2020 and others added 5 commits July 18, 2026 18:51
createAdmissionController builds a controller from config alone, with
no dependency on SmartExtractor. Availability depends only on
admissionControl.enabled, laying the groundwork for gating write paths
that don't go through extraction.
SmartExtractor no longer constructs its own AdmissionController from
config; it now uses whatever controller instance it is handed (or none).
This decouples admission gating from the extraction engine, so a
controller built independently (e.g. via createAdmissionController) can
be reused by write paths that don't go through SmartExtractor at all.
Construct the AdmissionController whenever admissionControl.enabled is
true, not only when smartExtraction is also on. Previously the LLM
client and controller were only ever built inside the smartExtraction
block, so any write path other than SmartExtractor (reflection-mapped
rows, the regex fallback) had no controller to borrow when smart
extraction was disabled and silently stored ungated.

The controller is now exposed on the plugin's singleton state alongside
smartExtractor, so those other write paths can reference it directly
once wired in, instead of reaching through smartExtractor.

Registers the three new test files in package.json's test chain and
scripts/ci-test-manifest.mjs (core-regression group).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gorkem2020
gorkem2020 force-pushed the fix/admission-without-smart-extraction branch from accc6d2 to 7c097d7 Compare July 18, 2026 15:51
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.

1 participant