refactor(extensions): replace allExtensionMethodsAttached with fingerprint-aware tracking set (swamp-club#318)#1365
Conversation
…print-aware tracking set (swamp-club#318) The allExtensionMethodsAttached guard re-imported extension bundles and re-parsed their schemas on every attachPendingExtensionsForType call just to check if methods were already present — O(import+parse) per extension per invocation. Replace it with a cheap Map<type, Map<sourcePath, fingerprint>> tracking set that records successful attachments and skips re-attachment in O(1). Three additional fixes discovered during the audit: 1. buildIndex now collects extension target types from stale secondary exports, so adding a new extension file without changing the base model correctly triggers attachment (pre-existing gap). 2. importAndExtendBundle treats "already exists on model type" as an idempotent no-op rather than a warning, covering the cross-path case where load() attaches via processSecondaryExport and attachPendingExtensionsForType re-attempts via catalog entries. 3. Fingerprint-aware tracking ensures stale extensions (changed source) get re-attached even when the (type, sourcePath) pair is already tracked — the old fingerprint won't match the new catalog entry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Clean, well-tested refactor that replaces an expensive O(import+parse) idempotency guard with an O(1) fingerprint-aware tracking set, and fixes a real bug where buildIndex didn't trigger extension attachment when only an extension file (not the base model) was stale.
Blocking Issues
None.
Suggestions
-
String-based error matching is fragile (
model_kind_adapter.ts:631): TheString(failure.error).includes("already exists on model type")check inimportAndExtendBundlecouples this code to the exact error message inmodelRegistry.extend()(model.ts:933,943). If that message ever changes, this silently breaks. Consider introducing a typed error class (e.g.,ExtensionAlreadyAttachedError) or an error code so the check is structural rather than string-based. Not blocking since it's defense-in-depth behind the tracking set, and the error string is specific enough to avoid false positives. -
Module-level mutable state: The
attachedExtensionsmap is module-level state with no production reset path. Fine for a CLI where process lifetime is short, but worth noting — if this adapter is ever used in a long-running process (e.g., watch mode, daemon), the map grows unbounded. TheclearAttachedExtensionsexport exists for tests; if a production reset path is ever needed, it's already wired up.
What looks good
- The
extensionTargetreturn fromrebundleAndUpdateCatalogcorrectly closes the gap where stale extension files didn't triggerattachPendingExtensionsForType - Defense-in-depth: tracking set prevents most duplicate work, and
importAndExtendBundlegracefully handles "already exists" for cross-path scenarios (load()→attachPendingExtensionsForType) - Four regression tests cover the key scenarios: cross-path idempotency (#123), hot-load, selective A/B attachment, and
resetLoadedFlaginvariant (ADV-2) - Tests properly handle Windows cleanup with
.catch(() => {})and use unique timestamped type IDs for isolation - DDD structure is sound: tracking state lives in the adapter (domain service), catalog mutations stay in the infrastructure layer
There was a problem hiding this comment.
Adversarial Review
Medium
-
loadSingleTypebypasses tracking map (extension_loader.ts:359-372):loadSingleTypeiteratesfindExtensionsForTypeand callsimportAndExtendBundledirectly — it never consultsisExtensionAttachedand never callsmarkExtensionAttached. IfattachPendingExtensionsForTyperuns later for the same type (as happens in thehotLoadModelsflow atauto_resolver_adapters.ts:268), the tracking map says "not attached" → the bundle is re-imported andextend()throws "already exists" → caught by the string-matching fallback. Functionally correct thanks to the idempotent fallback, but the claimed O(1) optimization does not apply to this cross-path. The unnecessary re-import is the same cost as the oldallExtensionMethodsAttachedapproach in this specific scenario. -
clearAttachedExtensionsnot wired into production reload paths (open.ts:74-90,doctor.ts:309-311):reloadExtensionRegistries()callsresetLoadedFlag()andresetExtensionLoadWarnings()but notclearAttachedExtensions(). Currently safe becauseresetLoadedFlagdoesn't clear registered models — the tracking map truthfully reflects that those extensions are already on the model. However, this creates an undocumented invariant:attachedExtensionsandmodelRegistry.modelsmust stay in sync for the process lifetime. If any future change introduces model unregistration or registry clearing, extensions for re-registered models would be silently skipped. Consider either (a) callingclearAttachedExtensions()alongsideresetLoadedFlag()in production, or (b) documenting this invariant in a code comment near theattachedExtensionsdeclaration. -
String matching as idempotency mechanism (
model_kind_adapter.ts:631):String(failure.error).includes("already exists on model type")is the load-bearing safety net for cross-path idempotency (tested in test 2 and test 4). If the error message format inmodel.ts:932-934changes, this silently breaks —importAndExtendBundlewould start reporting genuine failures for idempotent re-attachment. This coupling crosses file boundaries with no compile-time guarantee. Consider exporting a sentinel string or error subclass frommodel.tsto make the coupling explicit.
Low
-
Fingerprint tracking after idempotent catch records stale state (
model_kind_adapter.ts:671-677): When an extension with a changed fingerprint is re-attached,extend()throws "already exists" (old method stays), butmarkExtensionAttachedrecords the new fingerprint. The tracking map now claims the new code is active when the old execute function is still on the model. This is the same behavior as the old approach (which checked method names, not code), so not a regression — but the fingerprint-aware tracking map gives false confidence that code updates are reflected. A comment noting this limitation would prevent future confusion. -
load()path doesn't mark extensions (extension_loader.ts:220-232): Extensions attached viaprocessSecondaryExportin theload()path are not tracked inattachedExtensions, same gap as finding #1. ThehotLoadModelsscenario test covers this explicitly, confirming the string-matching fallback works, but the pattern means everyload()→attachPendingExtensionsForTypetransition pays the full import cost once.
Verdict
PASS — No critical or high severity findings. The refactoring is functionally correct: the fingerprint-aware tracking set eliminates the expensive allExtensionMethodsAttached guard for the primary buildIndex → attachPendingExtensionsForType path, and the "already exists" string-matching fallback correctly handles the three cross-path scenarios (load() → attach, loadSingleType → attach, hotLoadModels → attach). The four regression tests provide solid coverage of the key invariants. The medium findings are coupling/maintainability concerns, not correctness bugs.
Summary
allExtensionMethodsAttachedidempotency guard (which re-imported extension bundles and re-parsed schemas on every call) with a cheap fingerprint-awareMap<type, Map<sourcePath, fingerprint>>tracking set — O(1) lookup instead of O(import+parse)buildIndexdidn't trigger extension attachment for new extension files unless the base model file also changed —rebundleAndUpdateCatalognow returnsextensionTargetfor stale secondary exportsimportAndExtendBundletreats "already exists on model type" as an idempotent no-op, covering theload()→attachPendingExtensionsForTypecross-path inhotLoadModelsresetLoadedFlaginvariant (ADV-2)Audit findings (from triage)
Six model registration paths were audited. Full path consolidation was ruled out — extensions and base models are discovered from different sources (local, pulled, catalog), making attachment inherently a separate step. The tracking set approach delivers the same end state (no expensive guard, idempotent attachment) via a structurally honest path.
Adversarial review dispositions
resetLoadedFlagstale-tracking — tested empirically, not just documented.resetLoadedFlagdoesn't un-attach extensions, so the tracker's claim remains correctMap<string, Map<string, string>>instead of null-byte separator — more readable and greppableeagerlyRegisteredTypesnarrowing — superseded by theextensionTargetapproach which is strictly betterTest Plan
Closes swamp-club#318
🤖 Generated with Claude Code