Skip to content

brain import aborts with UNIQUE(name, qe_domain, pattern_type); --dry-run says 0 conflicts #736

Description

@pacphi

Summary

aqe brain import matches qe_patterns rows by id only, but the table is unique on (name, qe_domain, pattern_type). Suppose the source holds a pattern with the same name, domain and type as one in the target, under a different id. The import then attempts a plain INSERT, fails with SqliteError: UNIQUE constraint failed: qe_patterns.name, qe_patterns.qe_domain, qe_patterns.pattern_type, rolls back the whole transaction, and exits 1. This happens with every --strategy (skip-conflicts, latest-wins, highest-confidence) and in both the JSONL and RVF formats. --dry-run only counts rows, so for the same pair it reports Imported: 2, Conflicts: 0.

Every store that seeded the starter patterns has this clash with every other such store, because QEReasoningBank gives each seeded pattern a fresh uuidv4(). So a store created in a subfolder (#735) cannot be merged into the project's root store with brain import.

Environment

Component Version Source of the value
macOS 27.0 (build 26A428), kernel Darwin 27.0.0 sw_vers, uname -r
Architecture arm64 uname -m
Node.js v26.4.0 (mise-managed) node --version
npm 11.17.0 npm --version
pnpm 11.17.0 (mise-managed standalone binary) pnpm --version
ruflo 3.45.0 (npm latest 3.45.0) <npm-root-g>/ruflo/package.json; ruflo --version → ruflo v3.45.0
@claude-flow/cli (bundled in ruflo) 3.45.0 (npm latest 3.45.0) <npm-root-g>/ruflo/node_modules/@claude-flow/cli/package.json (not installed globally on its own)
@claude-flow/memory (bundled in ruflo) 3.0.0-alpha.25 (npm latest 3.0.0-alpha.26) <npm-root-g>/ruflo/node_modules/@claude-flow/memory/package.json; nested copy under @claude-flow/cli is also 3.0.0-alpha.25
agentdb (bundled in ruflo) 3.0.0-alpha.20 (npm latest 3.0.0-alpha.20) <npm-root-g>/ruflo/node_modules/agentdb/package.json; nested copies under @claude-flow/cli and @claude-flow/memory are also 3.0.0-alpha.20
agentdb (global standalone) 3.0.0-alpha.17 <npm-root-g>/agentdb/package.json; agentdb --version → agentdb v3.0.0-alpha.17
ruvector (bundled in ruflo) 0.2.41 <npm-root-g>/ruflo/node_modules/ruvector/package.json (same version nested under @claude-flow/cli, @claude-flow/memory, agentdb)
ruvector (global) 0.3.3 (npm latest 0.3.3) <npm-root-g>/ruvector/package.json; ruvector --version → 0.3.3
agentic-qe 3.14.3 (npm latest 3.14.3) <npm-root-g>/agentic-qe/package.json; aqe --version → 3.14.3
ruvnet-brain plugin (Claude Code) 4.3.28 (active, user scope, commit e89ea1ba167d) ~/.claude/plugins/installed_plugins.json; ~/.claude/plugins/cache/ruvnet-brain/ruvnet-brain/4.3.28/.claude-plugin/plugin.json (older cache dirs 4.3.21 and 4.3.26 also present)
ruvnet-brain KB bundle brainVersion 4.3.28 (releaseTag v4.3.28, builtUtc 2026-09-21T03:14:37.838Z) ~/.cache/ruvnet-brain/kb/SOURCE.json
Claude Code 2.1.283 (native installer) claude --version; ~/.local/bin/claude → ~/.local/share/claude/versions/2.1.283
codex-cli 0.157.0 codex --version; <npm-root-g>/@openai/codex/package.json
agentic-kit (global) 4.0.0-alpha.55 <npm-root-g>/@pacphi/agentic-kit/package.json; ak --version
agentic-kit (repo HEAD) 3505a29c07b9 (branch fix/audit-237-238-remediation; package.json 4.0.0-alpha.55) git rev-parse HEAD

<npm-root-g> = ~/.local/share/mise/installs/node/26.4.0/lib/node_modules (npm root -g). npm latest dist-tags read with npm view <pkg> dist-tags. Captured 2026-09-26.

The same code is on main at ffc0c6a51738 (2026-09-26).

Steps to reproduce

This block ran on 2026-09-26 as written, in a throwaway directory with a temporary HOME under env -i.

Two notes on the setup:

  • Why one row per store is seeded with SQL. In a fresh HOME on this machine the reasoning bank logs Loaded foundational patterns {"count":22}, yet no row reaches qe_patterns. aqe hooks learn fails with VECTOR_SPACE_UNVERIFIED: refusing to persist or index an embedding without runtime provenance. So I insert one row per store directly: the starter pattern AAA Unit Test (test-template / test-generation) with its own random id, as QEReasoningBank would. How the rows got there does not matter to the import.
  • Why aqe hooks stats runs once per project. The unique index idx_patterns_unique_name_domain_type is created when the SQLite pattern store first initializes, not by aqe init. In a real project the AQE hooks initialize it on the first tool call. Without that step the same import "succeeds" by inserting a second AAA Unit Test row next to the first one (observed; 3 rows in B afterwards).
T=$(mktemp -d); mkdir -p "$T/home"; cd "$T"
aq() { env -i PATH="$PATH" HOME="$T/home" TMPDIR="$T" TERM=dumb NO_COLOR=1 aqe "$@"; }
count() { sqlite3 "$1" "select count(*) || ' patterns: ' || group_concat(name, ', ') from (select name from qe_patterns order by name);"; }
# The same starter pattern learned independently in two stores: same (name, qe_domain,
# pattern_type), each with its own random id, as QEReasoningBank's uuidv4() gives it.
seed() { sqlite3 "$1" "INSERT INTO qe_patterns(id, pattern_type, qe_domain, domain, name, description)
  VALUES (lower(hex(randomblob(16))), 'test-template', 'test-generation', 'test-generation', '$2', 'seeded for the import repro');"; }

# three fresh projects; `hooks stats` initializes the pattern store once, as the AQE hooks do
for P in A B C; do mkdir "$P"; (cd "$P" && aq init --auto --skip-code-index --no-statusline >/dev/null && aq hooks stats >/dev/null); done
for P in A B C; do echo "$P: $(sqlite3 $P/.agentic-qe/memory.db "select name from sqlite_master where name='idx_patterns_unique_name_domain_type';")"; done
seed A/.agentic-qe/memory.db 'AAA Unit Test'
seed A/.agentic-qe/memory.db 'repro-only-in-A'
seed B/.agentic-qe/memory.db 'AAA Unit Test'
count A/.agentic-qe/memory.db; count B/.agentic-qe/memory.db

aq brain export --db A/.agentic-qe/memory.db --format jsonl -o a-export
aq brain import -i a-export --db B/.agentic-qe/memory.db --dry-run
for S in skip-conflicts latest-wins highest-confidence; do
  aq brain import -i a-export --db B/.agentic-qe/memory.db --strategy "$S"; echo "exit=$?"; done
count B/.agentic-qe/memory.db

# control 1: the same export back into A (same ids)
aq brain import -i a-export --db A/.agentic-qe/memory.db --strategy skip-conflicts; echo "exit=$?"
# control 2: a fresh target
aq brain import -i a-export --db C/.agentic-qe/memory.db --strategy skip-conflicts; echo "exit=$?"
count C/.agentic-qe/memory.db

# the RVF format, same pair
aq brain export --db A/.agentic-qe/memory.db --format rvf -o a-export.rvf
aq brain import -i a-export.rvf --db B/.agentic-qe/memory.db --dry-run
aq brain import -i a-export.rvf --db B/.agentic-qe/memory.db --strategy latest-wins; echo "exit=$?"
count B/.agentic-qe/memory.db

Expected vs actual

Expected. A pattern that already exists in the target under its natural key counts as a conflict and is resolved by the chosen strategy: skipped, or the newer or more confident copy kept. The rest of the import goes through. --dry-run reports the same counts a real run would.

Actual (excerpt; [UnifiedMemory]-style init lines and stack frames removed; temporary path shown as <tmp>; # notes added):

A: idx_patterns_unique_name_domain_type
B: idx_patterns_unique_name_domain_type
C: idx_patterns_unique_name_domain_type
2 patterns: AAA Unit Test, repro-only-in-A
1 patterns: AAA Unit Test
  Export complete.
  Format:   jsonl
  Patterns: 2
  Output:   <tmp>/a-export
  Dry-run mode — no data will be written.
  Import complete.
  Imported:  2
  Skipped:   0
  Conflicts: 0
  Brain import failed: SqliteError: UNIQUE constraint failed: qe_patterns.name, qe_patterns.qe_domain, qe_patterns.pattern_type
exit=1                                        # skip-conflicts
  Brain import failed: SqliteError: UNIQUE constraint failed: qe_patterns.name, qe_patterns.qe_domain, qe_patterns.pattern_type
exit=1                                        # latest-wins
  Brain import failed: SqliteError: UNIQUE constraint failed: qe_patterns.name, qe_patterns.qe_domain, qe_patterns.pattern_type
exit=1                                        # highest-confidence
1 patterns: AAA Unit Test                     # B unchanged; repro-only-in-A was lost with the rollback
  Imported:  0                                # control 1 (same ids): conflicts are detected
  Skipped:   2
  Conflicts: 2
exit=0
  Imported:  2                                # control 2 (fresh target)
  Skipped:   0
  Conflicts: 0
exit=0
2 patterns: AAA Unit Test, repro-only-in-A
  Format:   rvf
  Patterns: 2
  Imported:  2                                # rvf --dry-run
  Skipped:   0
  Conflicts: 0
  Embeddings: 0
  Brain import failed: SqliteError: UNIQUE constraint failed: qe_patterns.name, qe_patterns.qe_domain, qe_patterns.pattern_type
exit=1                                        # rvf latest-wins
1 patterns: AAA Unit Test

Stack excerpt for the JSONL failure: dist/cli/chunks/chunk-4XPPMADQ.js:168 inside sqliteTransaction (<npm-root-g>/agentic-qe/node_modules/better-sqlite3/lib/methods/transaction.js:65:24), called from dist/cli/chunks/brain-handler-H5OVZ22L.js, with code: 'SQLITE_CONSTRAINT_UNIQUE'.

Field observation. Earlier the same day the same failure occurred on copies of a real project's root store and one of its subfolder stores (#735). The direct import aborted with this error for every strategy, while --dry-run reported 0 conflicts. A workaround did go through. In a scratch copy of the subfolder store, every pattern whose (name, qe_domain, pattern_type) already existed in the root was deleted, and that copy was exported. Importing it into a copy of the root succeeded (314 → 370 patterns). Editing the export itself does not work, because the manifest checksum rejects it.

Evidence

Installed agentic-qe 3.14.3, dist/:

  • integrations/ruvector/brain-shared.js:24-26: PK_COLUMNS has no qe_patterns entry, so the key column is id. :44-45: the qe_patterns entry in TABLE_CONFIGS has no dedupColumns. :215-226: mergeGenericRow runs SELECT * FROM qe_patterns WHERE id = ? and, on a miss, calls dynamicInsert. The strategy is applied only when the id matches. :278-279: the legacy mergePattern also keys on id.
  • integrations/ruvector/brain-exporter.js:222-228: the dry-run returns { imported: total, skipped: 0, conflicts: 0 } after only counting JSONL rows; it never looks at the target. :235-258: one transaction for the whole import. :251-254: idCol = PK_COLUMNS[table] || 'id', then mergeGenericRow.
  • integrations/ruvector/brain-rvf-exporter.js:382-395: the same count-only dry-run. :417 and :443: the same transaction and merge.
  • learning/sqlite-persistence.js:211-213 and :293-296: CREATE UNIQUE INDEX IF NOT EXISTS idx_patterns_unique_name_domain_type ON qe_patterns(name, qe_domain, pattern_type).
  • learning/pattern-store.js:635: id: uuidv4() for every created pattern. learning/qe-reasoning-bank.js:236-249: the 22 PRETRAINED_PATTERNS are stored through that path whenever a store is empty.
  • cli/handlers/brain-handler.js:40-41: the --strategy and --dry-run options. :144: prints the raw error.

Upstream main @ ffc0c6a51738:

  • src/integrations/ruvector/brain-shared.ts:118-120 (PK_COLUMNS), :142-143 (TABLE_CONFIGS), :357-370 (mergeGenericRow)
  • src/integrations/ruvector/brain-exporter.ts:337-343 (dry-run), :353 (transaction), :371-374
  • src/integrations/ruvector/brain-rvf-exporter.ts:532-544 (dry-run), :594
  • src/learning/sqlite-persistence.ts:328, :416 (unique index)
  • src/learning/pattern-store.ts:1033; src/learning/qe-reasoning-bank.ts:297-302, :352
  • src/cli/handlers/brain-handler.ts:66-67, :181

The RuvNet Brain corpus holds agentic-qe/src/integrations/ruvector/brain-exporter.ts and agentic-qe/src/integrations/ruvector/brain-shared.ts. agentic-qe/docs/brain-export-improvement-plan.md in the same corpus designs a dedupStrategy: 'id' | 'composite' with compositeColumns for exactly this case. qe_patterns ended up with id only.

Impact

  • brain import cannot merge any two stores that have ever seeded the starter patterns. That covers the common cases: subfolder stores (Running aqe from a subfolder creates and adopts a new .agentic-qe store (#516 follow-up) #735), an export from a teammate, and moving to another machine. skip-conflicts, the default, does not skip.
  • --dry-run gives a green light for an import that is certain to fail.
  • The rollback also discards every pattern that did not clash, so nothing gets merged. The error is a raw SqliteError with a stack trace and does not name the clashing pattern.
  • Without the unique index (a store whose pattern store has not initialized yet), the same import inserts duplicates of the natural key instead.

Suggested direction

  • Also match qe_patterns on (name, qe_domain, pattern_type), for example through dedupColumns or a composite lookup, and apply the chosen strategy to the existing row.
  • Make --dry-run use the same lookups as a real run, for example by running the merge in a transaction that is always rolled back, so its counts match.
  • On a constraint error, name the table and the clashing key, and suggest a strategy.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions