mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
fix/issue-511-code-deps-persistence
317 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c3173a88ad |
feat(code-intelligence): parse TS/JS via bundled tree-sitter WASM — no typescript dependency (#511)
Code-intelligence (index/deps/impact/search) required the multi-megabyte
`typescript` compiler at runtime; without it every TS/JS file errored
("TypeScript is required for code analysis") and produced 0 nodes/edges — i.e.
the feature was broken out of the box for the entire JS/TS ecosystem.
Bundle tree-sitter WASM grammars for typescript/tsx/javascript and extract
entities/imports from them instead:
- assets/grammars/tree-sitter-{typescript,tsx,javascript}.wasm (shipped via
package `files: assets/**`).
- treesitter-ts-extractor.ts: extracts functions (incl. async arrows), classes +
methods (with visibility), interfaces, and imports (incl. require()).
- knowledge-graph.ts: TS/JS extraction now prefers tree-sitter (typescript-free),
falling back to the TypeScript compiler API only if tree-sitter is unavailable
AND `typescript` happens to be installed.
- tree-sitter-wasm-parser.ts: export loadWasmGrammar() reusing the proven WASM
init + grammar-path resolution.
Built with tree-sitter-cli 0.26.9 (matching web-tree-sitter ~0.26) via the new
scripts/build-grammars.sh (docker + emscripten/emsdk; reproducible — byte
identical across runs). Grammars built with the pre-0.25 emscripten dylink format
do NOT load with web-tree-sitter 0.26 (getDylinkMetadata error), which is why the
build pins the toolchain.
Verified on a real fresh project with NO typescript installed: code index → 8
nodes / 7 edges, deps → 2 connecting edges, search → 3 results. 232
code-intelligence tests + new extractor unit tests pass.
Note: the pre-existing assets/grammars/*.wasm for python/java/c#/rust/swift were
built with the old toolchain and currently fail to load with web-tree-sitter
0.26 (silently falling back to regex). They can be rebuilt with
`scripts/build-grammars.sh all` — tracked as a follow-up.
|
||
|
|
c981d84f5d |
fix(mcp): bound advisory routing with a timeout so a stalled learning engine can't hang domain tools
Defense-in-depth after the QEReasoningBank init recursion. Pattern search + model routing in createDomainHandler are optional hints, so they must never block a tool call. Wrap routeDomainTask() in withTimeout (ROUTE_ADVISORY_TIMEOUT_MS, default 5s, env-overridable): on timeout the tool proceeds with null routing (mapToPayload already tolerates null). Also cache the learning engine only AFTER a successful init, so a failed/slow init no longer serves a half-initialized engine on the next call. Adds unit tests for withTimeout (fallback on stall, real value when fast, fallback on reject). MCP code_index still returns normally. |
||
|
|
d6b19d087c |
fix(learning): stop QEReasoningBank.initialize() recursing → MCP domain tools hung on fresh install
Root cause: initialize() seeds pretrained patterns via loadPretrainedPatterns() → storePattern(), and storePattern() ensure-inits with `if (!this.initialized) await this.initialize()`. Because `this.initialized = true` was set AFTER seeding, the seed re-entered initialize() without bound — an async self-recursion that pinned a CPU core and never returned. Impact: EVERY MCP domain tool (code_index, quality_assess, defect_predict, …) routes through the advisory pattern-search path (createDomainHandler → routeDomainTask → searchPatternsForTask → getLearningEngine → AQELearningEngine → QEReasoningBank.initialize). On a fresh install (patterns seeded for the first time) that path recursed forever, so all MCP domain tools hung indefinitely while the CLI — which calls domain services directly — worked. The repo and in-memory unit tests never hit it (already seeded / bulk-load path). Fix: set `this.initialized = true` BEFORE loadPretrainedPatterns()/seeding. The core stores (patternStore + SQLite) are already up by then, so storePattern() short-circuits instead of recursing. Verified against a real tarball install: MCP code_index ∞-hang → 0.5s (6 symbols / 5 relations); quality_assess / defect_predict 0.8s; returns cleanly (0/0/0) when typescript is absent too. Adds a red-green regression test pinning the init-order invariant. |
||
|
|
cc5220c5a0 |
fix(code-intelligence): pin KG to a fixed namespace + evict stale file cache (#511)
Resolves the two remaining gaps from the audit.
Namespace divergence (CLI↔MCP parity):
KnowledgeGraphService persisted nodes/edges under the backend's DEFAULT
namespace — 'qe-kernel' for the CLI kernel, 'mcp-tools' for the MCP shared
backend — so a graph indexed by one tool was invisible to the other, and the
init existence check (`SELECT ... FROM kv_store WHERE namespace =
'code-intelligence:kg'`) always returned 0 because the data was elsewhere.
Now every KG kv op passes an explicit `{ namespace: 'code-intelligence:kg' }`
(via a single `nsOpts` getter so reads and writes can't drift apart — issue
#491 Bug 2). The two external readers that searched the default namespace by key
prefix (impact-analyzer test discovery, test-generator KG-context) now read that
namespace too. Verified: both `aqe code index` and the MCP `code_index` tool
land KG rows in `code-intelligence:kg`; deps/search still work; the init
`hasCodeIntelligenceIndex` check now finds the index.
FileReader stale content on incremental re-index:
indexFile() now evicts the file from the FileReader content cache before reading
it. The reader caches content for 5 min, so a re-index in a long-lived process
(the MCP server) could otherwise extract from pre-edit text. The two reads
within indexFile (entities + imports) still share one cache fill, so they stay
consistent. The incremental regression test now re-indexes on the SAME instance,
exercising both removeFileFromGraph and the cache eviction.
Adds a regression test asserting KG data is stored under 'code-intelligence:kg'
regardless of the backend's default namespace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
26e724b24e |
fix(code-intelligence): warn on unsupported langs, prune incremental stale edges, fix MCP code_index counts (#511)
Addresses the gaps a devil's-advocate audit surfaced in the #511 fixes: 1. Fail-loud warning (issue request #2): `index` now records how many files were indexed under a language with no dependency extractor and returns them as `IndexResult.warnings` (logged + printed by the CLI), so a downstream 0-edge `deps` result is explained rather than silently misleading. 2. Incremental stale state: `indexFile(..., incremental)` now calls removeFileFromGraph() to delete a file's prior node/entity/outgoing-edge keys before re-indexing, so `index --incremental` prunes removed imports and renamed entities instead of leaving phantom dependencies. (Note: within a single long-lived process the FileReader content cache can still serve stale text; the CLI re-indexes in a fresh process so this is not user-visible.) 3. MCP parity: the `index-code` task handler emitted `nodesCreated`/`edgesCreated` but the MCP result mapper reads `symbolsExtracted`/`relationsFound`, so `code_index` always reported 0/0. Map the KG counts to those fields and pass `warnings` through. Verified via a real MCP protocol call: filesIndexed 2, symbolsExtracted 5, relationsFound 4 (was 0/0). Adds regression tests: unsupported-language warning, incremental stale-edge removal (fresh-instance, mirroring the CLI), and the MCP field mapping. Known limitation (documented, not changed here): CLI persists the graph under the 'qe-kernel' namespace and MCP under 'mcp-tools', so a graph indexed by one tool is not read by the other. MCP exposes only `code_index` (no deps/search), so this has no MCP-internal reader; reconciling the namespaces is entangled with contradictory init/test-generation expectations and needs its own change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d7b82ff6e2 |
fix(code-intelligence): make aqe code search return results — content namespace round-trip (#511)
indexCode() stored retrievable content with a {namespace} store option, but
the key is already fully namespace-qualified and every read path
(semanticSearch/findSimilar/exact/fuzzy) plus the storeVector() embedding write
use the default namespace. On a real backend that honors the option, the
content written with {namespace} could not be read back without it (issue #491
Bug 2), so each matched vector's content fetch returned undefined and the result
was silently dropped — search returned 0 results for obviously-present terms.
Fix: drop the redundant {namespace} store option on the content write so it
round-trips with the reads.
Verified via CLI: `code search "calculator compute"` now returns the indexed
files (was 0). Adds a regression test using a namespace-honoring mock backend
(the existing mock ignored the option, which is why it never caught this); the
test fails with the option reintroduced and passes without it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8a0217e9aa |
fix(code-intelligence): persist KG so aqe code deps returns edges on TS/JS repos (#511)
`aqe code index` and `aqe code deps`/`search` run as separate CLI processes,
but the knowledge graph stored nodes/edges in-memory only ("cache-only"), so a
fresh `deps` process always saw an empty graph and reported 0 edges / degree-0
nodes — even though `index` reported edges created. This affected every
language; TS was incidental (the issue's "missing TS parser" theory is a red
herring — the TS Compiler API path is wired and does extract).
Fixes:
- Persist nodes/edges to memory.db in storeNode/createEdge; getEdges hydrates a
correct flat edge cache (the old source-keyed index could not serve
incoming/both lookups); clear() deletes persisted KG :node:/:edge: keys only.
- resolveImportPath(): resolve relative import specifiers to real files with
extension probing so file->file import edges connect (were dangling at
pathToNodeId('./math')).
- traverseDependencies: restrict to true dependency edge types
(import/call/extends/implements) and recurse directed-forward only, which
removes phantom A->B->A cycles surfaced once edges actually existed.
Verified via CLI: index -> 6 edges; fresh-process deps -> 2 connecting edges,
0 cycles (was 0 edges). Adds cross-instance regression tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f96159683f |
feat(init): ship the hook shim and wire aqe init to install + use it
Per request: aqe init now installs the cross-platform Node hook shim into each
user project and routes hooks through it (instead of `npx agentic-qe hooks`).
- package.json: add `.claude/hooks` to `files` so aqe-hook.cjs ships in the npm
tarball (verified via `npm pack --dry-run`).
- 07-hooks.ts: new installHookShim() copies aqe-hook.cjs into the project's
.claude/hooks/ (source: node_modules/agentic-qe, then the package's own copy
resolved relative to the compiled module — works for npx too).
- 07-hooks.ts + init-wizard-hooks.ts: generated hooks now run
`node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/aqe-hook.cjs" <cmd> ...`
(README/help examples left as `npx agentic-qe hooks` for manual use).
- aqe-hook.cjs: added an npx fallback (`npx -y --prefer-offline agentic-qe`)
so npx-only installs with no project-local bundle still work; disable with
AQE_HOOK_NPX=0 (tests use this for determinism). Header/scope updated (now a
shipped, init-copied file, not repo-internal).
Migration: isAqeHookEntry already matches the new `aqe-hook.cjs` command (via
the `\baqe\b` pattern) AND the old `npx agentic-qe hooks` form, so re-running
`aqe init` replaces a user's existing hooks with the shim form — no duplicates.
Verified end-to-end (real `aqe init --auto` in a temp project):
- aqe-hook.cjs copied into .claude/hooks/ ("Installed aqe-hook.cjs (copied)")
- all 11 generated hooks use `node .../aqe-hook.cjs <cmd>` (0 npx commands)
- post-task retains the #508 fix (--agent/--success true/--description)
- isAqeHookEntry matches new+old forms, not unrelated user hooks
- shim 7/7, settings-merge 17/17, task-hooks 5/5 tests pass; typecheck clean
|
||
|
|
1d0cbd626e |
refactor(hooks): rewrite hook shim as cross-platform Node (.cjs), drop POSIX .sh
A .sh shim is the least portable option — it forces a .cjs Windows twin and a node_modules/.bin shell-wrapper spawn. Node is already a hard dependency of AQE (CLI is Node >=20), so a single .cjs runs identically on Windows/macOS/Linux. aqe-hook.cjs resolves a project-local bundle and runs it via process.execPath (this node) — no shell, no .bin/.cmd wrapper: 1. node_modules/agentic-qe/dist/cli/bundle.js (installed dep — user case) 2. dist/cli/bundle.js (this source repo) else no-op (exit 0). Swallows stderr; emits only the top-level JSON object. Repo .claude/settings.json rewired: all 10 bundle hooks now invoke . Deleted aqe-hook.sh. Tests rewritten to run the .cjs via node; 7/7 pass incl. installed- dep precedence + stderr-swallow. This removes the Windows blocker that stood against ever shipping the shim to users. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e81b56d411 |
fix(hooks): make aqe-hook.sh portable (resolve project-local AQE, not hardcoded dist)
The shim hardcoded ${PROJECT}/dist/cli/bundle.js, which only exists in this
source repo — if ever copied into an installed project it would silently no-op.
Now resolves a PROJECT-LOCAL runner in priority order:
1. node_modules/.bin/aqe (installed project — the user case)
2. dist/cli/bundle.js (this source repo)
else no-op (exit 0, never block a turn)
No global-PATH / npx fallback by design (version drift + npx-per-fire crash).
Clarified in the header that the shim is REPO-INTERNAL: it is not in the npm
`files` list and `aqe init` does not copy it — user hooks are generated as
`npx agentic-qe hooks <cmd>` (cross-platform, npx-friendly). Portability here is
defense-in-depth, not a shipping decision.
Adds a regression test for branch-1 resolution + precedence (node_modules/.bin/aqe
preferred over dist). 6/6 shim tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
2ecc377241 |
fix(dream): kernel DreamScheduler reconciles dream-scheduler:hook-state per cycle (#509)
Reported by @ciprianmelian. ADR-094 moved dream cycles to the kernel-side
DreamScheduler, but the dream-scheduler:hook-state kv row that SessionStart
reads is only INCREMENTED by the post-task hook (experienceCount++) and was
never reset by the entity that actually dreams. Confirmed in source: the kernel
scheduler had ZERO references to that key (it persists its own
'dream-scheduler:state' instead). Result: SessionStart perpetually showed
lastDreamTime:null and a pendingExperiences counter that only grew (e.g.
11 -> 1747 -> 6489), reading like a stuck scheduler while the daemon dreamt fine.
Fix: after a successful executeDream cycle, reconcileHookState() stamps the
truth on the SAME key + DEFAULT namespace the hook path uses:
{ lastDreamTime: now, experienceCount: 0, totalDreamsThisSession: prev+1,
sessionStartTime: preserved }. Best-effort (never breaks a dream cycle). Key
is a local literal mirroring DREAM_STATE_KEY to avoid a learning->cli layering
import.
Verified: new unit test seeds the stale row (experienceCount: 2720,
lastDreamTime: null), triggers a dream, asserts the row becomes
experienceCount:0, lastDreamTime: <iso string>, totalDreamsThisSession:1,
sessionStartTime preserved. 39 dream-scheduler tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
201f5e8a95 |
fix(hooks): post-task --success eats --json -> every trajectory success=0/agent=unknown (#508)
Reported by @ciprianmelian. The aqe-init-generated PostToolUse hook ran `post-task --task-id ... --success --json`, but --success was declared `<bool>` (value REQUIRED), so Commander consumed --json as the success value: options.success='--json' -> parsed false -> EVERY qe_trajectories / captured_experiences row got success=0, the --json branch was swallowed, and no --agent meant agent='unknown'. Net: Task trajectories were unpromotable (distillation groups by agent/domain/success), so pattern count stayed pinned near zero. (Confirmed in source; same class as the learning gaps #510 hardened.) Fix (two layers): 1. Parser hardening — `--success <bool>` -> `--success [bool]` (optional value) in task-hooks/command-hooks/routing-hooks, so a bare --success yields boolean true and never eats the next token. Backward compatible: --success true/false and the 'true' default all still parse identically. 2. Generated templates (07-hooks.ts + init-wizard-hooks.ts): post-task now emits `--agent "$TOOL_INPUT_subagent_type" --success true --description "$TOOL_INPUT_prompt" --json` (real agent attribution + explicit success + pre-task bridge), and post-command emits explicit `--success true`. Verified: isolated Commander proof — OLD <bool>: success="--json" (false); NEW [bool]: success=true, --json fires. Plus 3 regression tests driving the EXACT broken argv through registerTaskHooks: bare --success -> success=true, explicit --success false -> false, --agent flows through. 5/5 task-hook tests pass (incl. existing #449); typecheck clean. (Full post-task CLI run OOMs in Codespace per CLAUDE.md — the parsing layer is the bug and is proven.) NOTE for existing installs: re-run `aqe init` (or hand-edit the PostToolUse hook in .claude/settings.json) to pick up the corrected command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ae7c8b7105 |
fix(learning): surface consecutive_failures so the 'failures' deprecation branch fires (#510 side-finding)
Pre-existing bug surfaced by the qe-devils-advocate audit and confirmed: checkDeprecation() reads (pattern as PatternWithDeprecation).consecutive_failures to trigger the 'failures' deprecation reason, but rowToPattern() built the QEPattern WITHOUT that column (and without deprecated_at), so getPattern() always surfaced undefined->0 and the 'failures' branch NEVER fired — a pattern that kept failing was never deprecated for failures. Fix (surgical): rowToPattern now surfaces consecutive_failures and deprecated_at (added to the PatternRow interface; returns a typed PatternWithDeprecation). No thresholds or deprecation logic changed. Verified: new tests assert a pattern with consecutive_failures==3 (== default threshold), not stale, confidence above min, deprecates with reason 'failures'; below threshold does not. Regression-proven (reverting rowToPattern fails the new test). 12 EWC tests pass; typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2969bb387c |
fix(test)+docs: address qe-devils-advocate audit of #510 loop
Audit (qe-devils-advocate) verdict: 10/10 items have real committed code + real passing tests at claimed counts; production changes (#2/#6/#7) genuinely wired into live paths; no real-store mutation; #8 honestly UNWIRED. Three fair discrepancies, now corrected: 1. #2 had one TAUTOLOGICAL test (asserted (1/3)*0.4 > (1/11)*0.4 — arithmetic on literals, not a code call). Rewritten to score a matching vs non-matching agent through calculateAgentScores and assert the delta equals the CAPPED domain term (~0.1333), which fails if the cap is removed. 4/4 still pass. 2. #2 status row softened to VERIFIED (PARTIAL): +5.6pp shipped, but the structural Q-maturity / fixed-perf-term ceiling on confidence remains. 3. #10 status row corrected to PARTIAL: only the smoke contract was delivered; the original multi-bundle 'subset packaging' scope is deferred. No production code changed by this commit. Side-findings re-confirmed real (dead consecutive_failures branch, MCP [object Object] error, RVF/SQLite drift). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3196c05edd |
feat(learning): pretrain-from-history bootstrap (#510 item 9)
Ports ruflo ADR-077: seed the pattern store from the repo's OWN git history
(commits = success trajectories) through the SAME live-learning code path, so
a fresh project starts with a non-empty, project-specific pattern store instead
of only the generic PRETRAINED_PATTERNS.
scripts/pretrain-from-history.ts (npm run pretrain:history):
- git log --no-merges -> per commit: domain = deriveTaskType(subject),
success = subject !startsWith 'revert' (a revert is a failure signal);
- records each via persistTaskOutcome (the live captured_experiences +
experience_applications + Q-update path, honoring AQE_PROJECT_ROOT);
- then consolidateExperiencesToPatterns() distills them into qe_patterns.
- flags: --limit N (default 500), --dry-run (classify+histogram, write nothing),
--verbose. Extracted a pure classifyCommit() helper (19 unit tests).
Correctness fix found during verification (in the script, not prod): a brand-new
store only has core schema, so persistTaskOutcome failed with
'no such table: experience_applications'. The script now calls the canonical
ensureAllBrainTables() + initializeExperienceCapture() before recording — the
same tables production creates lazily.
Verified against a THROWAWAY AQE_PROJECT_ROOT temp store (data protection):
--limit 150 -> captured_experiences 0->150, qe_patterns 0->4, exit 0;
--dry-run -> nothing recorded (captured_experiences stays 0);
real .agentic-qe/*.rvf byte-identical (sha256) before/after; 19 tests pass.
Side-finding: AQE's real embedding (ONNX) costs ~2.3s/commit here vs ruflo's
~3ms/trajectory, so bootstrapping 500 commits takes minutes — logged as a perf
follow-up (embedding backend / batching).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0d80eb56ec |
feat(perf): RaBitQ 1-bit signature utility + benchmark (#510 item 8)
Ports ruflo's perf-M4 (sign-random-projection + Hamming popcount). Adds
src/shared/utils/rabitq.ts: signSignature (384-bit packed into 12 Uint32
words, 48 bytes), hammingDistance (SWAR popcount), hammingSimilarity, and
topKBySignature (Hamming prefilter gated at N>=100 + exact-cosine rerank of a
recall-aware pool, so output equals exact topK when the true top-k fall in the
pool). 16 tests + scripts/benchmark-rabitq.ts (npm run benchmark:rabitq).
HONEST FINDING (verified, does NOT match ruflo's optimistic 2.7x/10.9x):
N=1000: speedup 1.31x, recall@10 100%, index 1.46MB -> 47KB (32x smaller)
N=5000: speedup 1.18x, recall@10 99.9%, 7.32MB -> 234KB (32x smaller)
The 32x INDEX-MEMORY reduction is real and unconditional. The SPEEDUP is only
~1.2-1.5x in JS, not ruflo's figure: a 1-bit sign signature in 384-dim is
coarse, so preserving recall@10 >= 0.95 needs a large exact-rerank pool
(~40-55% of N), which eats most of the prefilter win. ruflo's big win came
from replacing a LINEAR scan; AQE's main search is already native HNSW
(sublinear), so that win doesn't transfer. Tests pin this tradeoff (recall==1.0
with a sufficient pool; small pool sacrifices recall) so it can't be
mis-sold later.
NOT INTEGRATED — no zero-regression consumer exists:
- unified-memory-hnsw search() IS the ranking (no separate exact rerank to
prefilter), so Hamming-prefiltering its beam would drop true neighbors;
- coherence-gate-energy O(N^2) SUM and k-means clustering are numerically
sensitive (Hamming would change results).
Committed as a tested, benchmarked, ready-to-use utility (real memory lever for
patterns.rvf and a future prefilter->exact-rerank consumer), not wired into any
production path. No production code or .agentic-qe stores touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1a695f5651 |
feat(hooks): resilient hook shim — clean transcript + never block a turn (#510 item 5)
Ports ruflo's hook-shim discipline. AQE's project hooks ran
`node dist/cli/bundle.js hooks <cmd> --json` directly, which printed init
diagnostics to STDOUT before the JSON on every SessionStart/UserPromptSubmit:
[hooks] CoherenceService initialized with WASM engines
[RvfPatternStore] Initialized: .agentic-qe/patterns.rvf (dim=384)
[RVF] Migration adapter active at stage 2 (dual-sqlite)
[hooks] System initialized
polluting the Claude Code transcript on every turn, and a crash/slow init could
surface a hook error mid-turn.
New .claude/hooks/aqe-hook.sh wraps every bundle hook with the ruflo contract:
* prefer the LOCAL built bundle (no npx / network);
* no-op if the bundle isn't built (never block a turn);
* swallow stderr;
* emit ONLY the top-level JSON object on stdout (sed '/^{/,/^}/p' drops both
leading init noise AND async daemon log lines printed after the JSON, so the
--json contract stays parseable);
* ALWAYS exit 0.
All 10 bundle hook commands in .claude/settings.json rewired to the shim
(diff touches only those 10 command strings).
Verified with real commands: route/session-start/guard via the exact settings
invocation -> exit 0, 0 noise lines, valid JSON; missing-bundle and crashing-
bundle -> exit 0, empty stdout, no stderr leak. 5 regression tests (resilience +
deterministic noise-stripping via a fake bundle) pass in ~120ms.
Note: Windows .cjs parity (ruflo also ships one) is a separate follow-up — the
entire AQE hook config is POSIX `sh -c`, so a .cjs twin needs the settings
rewrite too. Observed side concern: the route/session-start hooks cold-start the
full system (~30-60s) on each fire — logged for a perf follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
43918b3ba8 |
feat(learning): contradiction detection in consolidation (#510 item 7)
AQE's Phase-1 consolidation merged near-duplicate experiences by cosine
similarity, but two experiences with the SAME context and OPPOSITE outcomes
(large quality delta) are a conflict, not a duplicate. Merging silently
absorbed the conflict; worse, a *used* low-quality contradictory experience was
never merged (merge requires application_count===0) and stayed in retrieval,
poisoning recall.
Now, when similarity >= contradictionSimilarityThreshold (0.85) AND the quality
delta >= contradictionQualityDelta (0.4), the pair is treated as a contradiction:
the lower-quality loser is SUPPRESSED from retrieval (consolidated_into =
'contradicted', excluded by the IS NULL filter) instead of merged — even if it
was applied. The survivor is NOT boosted (a conflict is not a duplicate). The
action is logged distinctly ('contradiction'). New count surfaced as
ConsolidationResult.contradicted. Both thresholds are configurable.
PRODUCTION IMPACT: affects the dream/consolidation merge phase. Genuine
near-duplicates still merge exactly as before; only same-context/opposite-outcome
pairs are newly suppressed (soft, reversible — consolidated_into can be cleared).
Verified with 6 tests against an in-memory DB matching the production
captured_experiences schema (suppress loser, suppress even-when-applied, no
survivor boost, near-duplicate still merges, dissimilar pair untouched, distinct
log action). 13 total consolidation tests pass; typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ef4738dec4 |
feat(learning): EWC++ importance-weighted forgetting protection (#510 item 6)
Confidence decay (applyConfidenceDecay) and stale/age deprecation
(checkDeprecation) were uniform — a proven high-success pattern was forgotten
at the same rate as noise, so an influx of new low-value patterns could crowd
out historically valuable ones (catastrophic forgetting).
Inspired by Elastic Weight Consolidation, high-IMPORTANCE patterns now resist
forgetting, weighted by an importance proxy = clamped success_rate gated on
usage_count >= EWC_MIN_USAGE_FOR_PROTECTION (recency already implied: only
unused patterns decay/go stale). Honest framing (as in ruflo's EWC adapter):
this is a heuristic, NOT true Fisher information — no model gradients exist here.
- Decay: per-row factor shielded by EWC_DECAY_PROTECTION*importance. A
fully-proven pattern decays up to 10x slower; importance-0 patterns decay
EXACTLY as before (no regression — locked in by test).
- Stale: effective window = staleDays * (1 + importance*EWC_STALE_PROTECTION_BONUS),
so a proven pattern unused 30d isn't deprecated like noise.
Protection covers ONLY stale/age/decay forgetting. A pattern that drops below
minActiveConfidence (or starts failing) is STILL deprecated — we blend
retention, we don't permanently shield dead weight. All three EWC_* knobs are
exported constants; set to 0 to restore exact prior behavior (reversible).
PRODUCTION IMPACT: affects the dream/consolidation lifecycle. Low/zero-importance
patterns behave identically; high-success patterns persist longer. Verified with
10 tests against an in-memory DB matching the production qe_patterns schema
(decay deltas, stale window, no-regression, dead-weight still removed). 46 total
lifecycle tests pass.
Side-finding (pre-existing, NOT this change): checkDeprecation reads
consecutive_failures via a cast, but rowToPattern doesn't surface that column,
so the 'failures' deprecation branch never fires through getPattern. Logged for
a dedicated fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1bb91b402c |
fix(routing): de-dilute domain score to recover routing confidence (#510 item 2)
Routing confidence (QERoutingResult.confidence) is the top agent's combined static score from calculateAgentScores. The domain term was (domainMatch / detectedDomains.length) * 0.4, but AQE's domain detector emits 7-12 domains per task, so a perfect single-domain match scored (1/11)*0.4 ~ 0.04 — relevance washed out by detector breadth, leaving confidence dominated by the fixed performance term. Fix: cap the denominator at DOMAIN_DENOM_CAP (3), modeling the few truly-relevant domains a task has. Precise detection (1-2 domains) is unchanged; broad detection no longer dilutes a real match below usefulness. Proportionality preserved. Tunable via the constant + the existing routingWeights.similarity. PRODUCTION IMPACT: shifts every routing decision's confidence telemetry upward for broad-domain tasks and strengthens domain relevance vs the fixed performance term in ranking. Reversible (raise DOMAIN_DENOM_CAP toward old behavior). Verified with real `aqe hooks pre-task` routes (not mocks): broad multi-domain task: 27.7% -> 33.3% (+5.6pp); narrow tasks unchanged (test 37.3%, security 30.3%); recommendations/ranking preserved. 77 existing routing tests pass; +4 regression tests lock in de-dilution. NOTE: ruflo ADR-142 (per-bucket Beta priors) did NOT apply — AQE confidence is not a Beta-bandit; root cause was this dilution. See docs/ruflo-adoption-plan.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
28fa643e4e |
test(routing): regression guard for ruflo router bugs #2229/#2239 (audit, #510 item 3)
Audited AQE routing against two router bugs ruflo fixed 2026-05-29: - #2229 stale route-cache: NOT PRESENT — Q-values read live from SQLite (qe-reasoning-bank buildQValueLookup) and in-memory qTable; no cached routing decision exists to go stale, so no invalidation needed. - #2239 state-encoder truncation: NOT PRESENT — state keys are plain string joins (agent-routing buildRoutingStateKey: taskType|priority|domain), no bit-folded quantized vector, no mask. Distinct keyword tasks map to distinct states. Adds a guard test locking in non-collapsing state encoding so a future refactor toward a bit-folding encoder fails here. 4 tests passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
95cd6ea7e1 |
fix(llm-router): post-audit sweep — update test for new fallback semantics + README
Two follow-up fixes after re-running the broader test suite (2064 tests): 1. tests/unit/shared/llm/router/hybrid-router.test.ts:619 — the pre-existing test "should not retry non-retryable errors" was asserting the OLD short-circuit-on-non-retryable behavior that Fix #3 in the audit corrected. Updated to assert the new (correct) semantics: non-retryable errors don't re-try the SAME provider but DO fall through to the NEXT provider in the fallback chain. Renamed accordingly. Both providers fail non-retryable so the request still rejects, preserving the original "rejects" assertion. 2. README.md "LLM Providers" section was under-documented relative to the wired implementation. Updated to: - List all 7 ExtendedProviderType providers with their env var names (including the GOOGLE_API_KEY alias added in Phase 1) - Document `aqe llm config --set` persistence path - Document the AQE_LLM_ROUTER_DISABLED kill-switch - Mention that apiKey can't be saved via CLI (env-only) - Drop Groq from the table since it's not actually in the supported provider list Verification - npx tsc --noEmit: clean - 2063/2063 unit + integration tests pass (1 skipped, no failures) - Includes 38/38 hybrid-router tests after the assertion update |
||
|
|
3bc09cda6d |
fix(llm-router): address 10 findings from qe-devils-advocate audit
A post-Phase-4 devil's-advocate audit identified 10 issues. All addressed
with code fixes + tests. Some were real bugs (silent billing regression,
2 unwired MCP tools, 2 HybridRouter routing bugs); others were robustness
improvements (split singletons, silent errors, decorative type fields).
Findings → fixes (severity tagged in audit report):
1. CRITICAL — silent billing regression on upgrade
Users with ANTHROPIC_API_KEY in env would silently start paying for
Claude API calls on every Fleet operation. The CHANGELOG's "opt out
via code changes" wasn't sufficient — needed an env-only escape.
Fix: AQE_LLM_ROUTER_DISABLED=1 env kill-switch honored in both
QEKernelImpl._initializeLLMRouter and src/mcp/tools/base.ts
getSharedLLMRouter. Truthy values disable; 'false'/'0'/'no'/'off'/''
pass. Unified parse rule between kernel and MCP base.
2. HIGH — 2 unwired MCP standalone tools (audit said 3; QualityCriteria
isn't LLM-aware so excluded)
src/mcp/tools/test-generation/generate.ts:82 was calling
createTestGeneratorService(memory) without router. Same for
visual-accessibility/index.ts:177. Both now use the
createXxxServiceWithDependencies factories with { memory, llmRouter }.
3. CRITICAL — E2E only tested manual routing mode, not production
Production default is rule-based. Adding two new E2E tests for
rule-based mode SURFACED two real bugs in HybridRouter that I fixed:
3a. executeWithFallback hardcoded ['claude','openai','ollama'] as the
only fallback providers; gemini/openrouter/azure-openai/bedrock
users had no fallback even though ProviderManager can construct
them. Plus the user's defaultProvider wasn't auto-included in the
chain. Both fixed at src/shared/llm/router/hybrid-router.ts:612.
3b. A non-retryable error from one provider (e.g. Claude "invalid
x-api-key") aborted the entire fallback loop via `throw error`,
defeating the whole point of a fallback chain. Fixed: skip the
inter-attempt delay for non-retryable but continue to the next
fallback entry.
4. HIGH — MCPToolContext.llmRouter field was decorative
Added the field but no code populated it. Every getLLMRouter(context)
call fell through to getSharedLLMRouter().
Fix: added llmRouter and memory to BaseMCPTool.invoke() options;
qe-tool-bridge.ts and registry.ts now explicitly forward the shared
singleton via { llmRouter } at invoke time.
5. HIGH — 14 of 15 wired services had only STRUCTURAL verification, not
behavioral (only TestExecutor had a chat() round-trip test)
Fix: new tests/integration/llm-router-behavioral.test.ts asserts
isLLMAnalysisAvailable()===true (the exact gate the audit named) for
all 14 services + 2 full behavioral round-trips. 16 tests total.
6. HIGH — MCP and kernel built SEPARATE HybridRouter instances
Different cost trackers, caches, circuit breakers per side.
routing_economics/routing_metrics would only see half the traffic.
Fix: kernel.ts._initializeLLMRouter calls setSharedLLMRouter(router)
on init so both paths share one instance. kernel.dispose() calls
resetSharedLLMRouter() so the next kernel doesn't inherit the
previous instance.
7. HIGH — Silent error swallowing in _initializeLLMRouter
console.warn'd then continued without LLM, with no signal for
monitoring. A misconfigured provider would silently degrade with no
alert.
Fix: now publishes kernel.llm-router.init-failed (caught error) and
kernel.llm-router.init-no-provider (enabled=true but nothing
available) events on the event bus. Test confirms event delivery.
8. MEDIUM — Service count mismatch in docs (ADR/CHANGELOG said "14
services / 12 domains"; test enumerates 15 services / 11
LLM-enhanced domains)
Fix: ADR-043 addendum + CHANGELOG corrected to 15/11. Added
explicit service list and noted the 2 non-LLM domains
(enterprise-integration, coordination).
9. MEDIUM — aqe llm config --set apiKey=... silently dropped the key
stripApiKeys() returned a successfully-saved file without the key,
user thought it was saved.
Fix: CLI updateConfig() now throws a clear error pointing to env
vars BEFORE calling save. stripApiKeys also now logs a console.warn
when it strips anything, so programmatic callers see the strip.
10. LOW — Env vs disk precedence was counterintuitive and undocumented
Force-enable-on-env overrides explicit `enabled: false` on disk.
Fix: doc comment in config-store.applyEnvProviderDetection +
ADR-043 "Precedence & env-var conventions" section explains.
Verification
- npx tsc --noEmit: clean
- 191 unit + integration tests pass (16 new behavioral tests for #5,
17 wiring tests, gemini provider, config-store, llm-router-service,
kernel-llm-router with 17 tests including all kill-switch cases,
CLI persistence with 28 tests)
- 5 real-provider E2E tests pass (Gemini 2.5 Flash, OpenAI gpt-4o-mini,
OpenRouter, plus 2 rule-based-mode tests that previously failed and
now pass with the HybridRouter fallback fixes)
Refs ADR-043 audit findings (2026-05-21)
Builds on
|
||
|
|
c206c81751 |
feat(llm-router): Phases 3 + 4 — MCP path, integration tests, real-provider E2E, ADRs
Phase 3 — MCP tool path
- 8 standalone MCP tools (coverage-analyze, coverage-gaps, contract-validate,
chaos-inject, code-intel-analyze, defect-predict, learning-optimize) now
resolve the kernel HybridRouter via getLLMRouter(context) and pass it to
their service via the dependency-bag form.
- src/mcp/tools/base.ts: added getSharedLLMRouter() singleton, mirroring the
existing getSharedMemoryBackend() pattern. resetSharedLLMRouter() for
tests. Added MCPToolContext.llmRouter field so future tools (and the MCP
runtime when wiring per-request kernel context) can inject explicitly
rather than via the shared singleton.
Phase 4a — Mock-provider integration tests
- tests/integration/llm-router-wiring.test.ts (new, 17 tests).
- Per-domain wiring assertions (15): for each LLM-enhanced
plugin → coordinator → service path, asserts service.llmRouter ===
kernel.llmRouter after kernel boot with an injected mock provider.
This is the precise structural test for the gap the author's friend
flagged — a regression in any domain's wiring fails immediately.
- Behavioral end-to-end (1): TestExecutorService.analyzeFailuresWithLLM
actually invokes the mock provider; assert generateCalls counter
incremented and the response surfaced.
- Sanity (1): kernel.llmRouter is defined when a router was built.
Phase 4b — Real-provider E2E smoke test
- tests/e2e/llm-router-real-providers.test.ts (new, 3 tests).
- Gated by AQE_LLM_E2E=1; each provider auto-skips when its API key
isn't in env.
- Uses mode: 'manual' to bypass DEFAULT_QE_ROUTING_RULES (which would
otherwise route generic tasks to Claude even when defaultProvider
is set to something else — worth knowing).
- Verified live on 2026-05-21 against:
Gemini (gemini-2.5-flash) → "Hi!"
OpenAI (gpt-4o-mini) → "Hi!"
OpenRouter (openai/gpt-4o-mini) → "Hi!"
Claude intentionally skipped — the ANTHROPIC_API_KEY in the dev env
is a 24-char stub (real Anthropic keys are ~108 chars), would 401.
Phase 4c — Documentation
- docs/implementation/adrs/ADR-043-vendor-independent-llm.md: added
"Wiring Verified" status entry (2026-05-21) + full addendum documenting
the problem found, the four-phase resolution, the verification tests,
and the behavioral consequence for users.
- docs/implementation/adrs/ADR-051-agentic-flow-integration.md: added
"LLM-Enhancement Wiring Verified" status entry pointing to ADR-043's
addendum.
- CHANGELOG.md: added [Unreleased] section under Changed — behavioral,
explicitly noting the behavior shift (fleets with provider keys in env
will now start routing LLM calls through that provider), the opt-out
path (KernelConfig.llmRouter: { enabled: false }), and the persistence
fix for `aqe llm config --set`.
Test totals across all four phases on this branch
- 39 unit tests across 5 files (Phase 1)
- 17 integration tests in 1 file (Phase 4a)
- 3 real-provider E2E tests in 1 file (Phase 4b, AQE_LLM_E2E=1)
- 34 pre-existing kernel tests still pass (no regression)
- 22 pre-existing test-generation coordinator tests still pass (no regression)
- Clean tsc --noEmit across the full project
Refs ADR-043, ADR-051
Closes the wiring gap identified by user audit on 2026-05-21
Builds on
|
||
|
|
94033e3500 |
feat(llm-router): Phase 1 — kernel singleton + persistence + Gemini key alias
Wires the foundation for ADR-043's HybridRouter into the kernel boot
sequence so domain plugins can finally receive an llmRouter dependency.
Phase 1 lands the kernel/CLI/provider changes; Phase 2 will plumb the
router through all 13 domains.
Background: the LLM-enhancement code paths added in ADR-051
(isLLMAnalysisAvailable() across 14 services) were unreachable because
no construction site ever passed an llmRouter — services accepted the
optional dependency but no caller injected it. ProviderManager +
HybridRouter were only instantiated from the `aqe llm advise` CLI
subcommand.
Changes
- src/shared/llm/providers/gemini.ts: read GOOGLE_API_KEY as a third
fallback after GOOGLE_AI_API_KEY and GEMINI_API_KEY, so users with
the conventional Google key name don't need to alias it themselves.
- src/shared/llm/router/config-store.ts (new): load/save
.agentic-qe/llm-config.json with env-detection layering
(defaults < disk < env < explicit override). Strips apiKey fields
defensively before persisting. Filters out providers the
ProviderManager doesn't yet construct (onnx) to keep boot logs
clean.
- src/shared/llm/llm-router-service.ts (new): builds a HybridRouter
from the loaded config, picks primary/fallbacks from the available
set honoring the user's defaultProvider when possible. Accepts an
injected ProviderManager for tests.
- src/kernel/interfaces.ts: extends QEKernel with llmRouter? and
KernelConfig with llmRouter.enabled ('auto'|true|false) +
configOverride + providerManager test-injection.
- src/kernel/kernel.ts: kernel-singleton llmRouter built during
initialize(), exposed via kernel.llmRouter, forwarded to all 13
DOMAIN_FACTORIES as a 4th arg. Phase 2 wires each factory through
to its coordinator/services; for now the wrappers accept-and-discard.
- src/cli/commands/llm-router.ts: replaces the
`// would be persisted in real implementation` stub with real
read/write through the config-store. `aqe llm config --set` now
survives a process restart. Adds AQE_CONFIG_ROOT env var as a test
seam so tests don't mutate the real .agentic-qe/.
Tests (110 total, all passing)
- gemini.test.ts: +3 new tests for each env-var alias, existing
tests updated to clear all three names
- config-store.test.ts (new, 29 tests): load/save/merge precedence,
env detection, secret stripping, atomicity
- llm-router-service.test.ts (new, 10 tests): pickEnabledProviders,
pickPrimaryAndFallbacks, end-to-end with injected mock provider
- kernel-llm-router.test.ts (new, 4 tests): kernel boot with router
enabled=false / auto-no-providers / injected mock, chat() round-trip
- llm-router.test.ts (CLI): +2 persistence-to-disk tests, env-isolated
- tests/mocks/index.ts: shared createMockLLMProvider() helper
Verification
- npx tsc --noEmit: clean
- 110 tests pass; no regressions in existing kernel.test.ts (34/34)
- Stderr noise from PersistentSONAEngine init is pre-existing and
unrelated to this change.
What this commit does NOT do
- Does NOT change behavior for any user yet — DOMAIN_FACTORIES
wrappers still discard the llmRouter param. Phase 2 lights up the
domain services.
Refs ADR-043 (vendor-independent LLM), ADR-051 (LLM-enhanced services)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a9f7082f15 |
fix(embeddings): harden ADR-097 endpoint client against devil's-advocate audit
Closes 11 findings from a post-merge audit of the external embedder endpoint implementation. The "no silent hash fallback" correctness promise was being contradicted by two callers; the connect timeout was actually an idle timeout; the identity probe could be bypassed in several paths; and identity drift across runs was undetectable. All hardened. Critical/High fixes: - sqlite-persistence backfill no longer falls back to hash embeddings when the endpoint is configured; re-raises to protect the HNSW index from non-comparable vectors (was poisoning silently on every endpoint blip). - qe-reasoning-bank.embed() propagates the throw under isUsingEndpoint() instead of silently substituting a hash vector. - EmbedderEndpointClient.embed() now gates on probe() — lazy probe on first use with in-flight dedup so 20+ concurrent embeds share one probe. - Circuit breaker recovery (window expires) clears cached identity so the next embed re-probes — endpoint restarts often coincide with model swaps. - True TCP-connect timeout enforced at the socket layer (was req.setTimeout, which is an idle-after-write timeout — SYN blackholes hung ~75s instead of 5s). - Endpoint identity persisted to memory.db kv_store (_system namespace) — cross- run fingerprint drift now surfaces as a loud warning on next boot. Medium fixes: - encoding_format: 'float' pinned in request; base64 responses fail loud. - TLS knobs (ca, cert, key, rejectUnauthorized, servername) passthrough for self-hosted https endpoints without NODE_TLS_REJECT_UNAUTHORIZED=0. - URL userinfo stripped + warned at parse; redacted from all error messages and logs. - Embedding cache key namespaced by mode (endpoint:<fingerprint>:<text> vs inproc:<text>) — cross-mode collisions structurally impossible. Tests: - 33 unit tests pass (was 23): adds probe-gate, breaker re-probe, connect timeout, DNS failure, concurrent embed dedup, base64 fail-loud, URL redaction, encoding_format wire format. - 6 live integration tests against real llama-server (all-MiniLM-L6-v2.Q8_0.gguf): probe, single embed, batch, lazy probe gate, dim mismatch, 50-input batch. Replaces the prior "9ms in-process fake" with honest numbers (cold 15ms, warm 2ms against localhost llama.cpp). - New tests/integration/embedder-endpoint-llamacpp.test.ts is gated on AQE_LLAMA_EMBED_URL so it skips cleanly in CI without the binary. ADR-097 updated with the per-caller audit table (12 writers verified non-poisoning) and "drop-in for 6 providers" claim softened to "verified against llama-server; OpenAI shape compat expected but unverified" per provider. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b17309b3f8 |
feat(embeddings): optional external embedder endpoint (ADR-097, #503)
Add opt-in OpenAI-compatible /v1/embeddings client to eliminate duplicate in-process model loads in ruflo/ruvector co-deployments and the per-hook cold-load cost in fresh agentic-qe CLI processes. When EmbeddingConfig.endpoint (or AQE_EMBEDDER_ENDPOINT) is set, the @huggingface/transformers import is bypassed entirely and feature-extraction routes through the remote service. Endpoint unset = today's behavior. Wire format is OpenAI's /v1/embeddings so any existing embedder server (TEI, vLLM, llama.cpp, Ollama, LocalAI, LM Studio, OpenAI) works as a drop-in, not just bespoke ruvector servers. Transport is HTTP or HTTP-over-Unix-socket. Errors hard-fail with a circuit breaker (3 failures / 60s window) — no silent hash-embedding fallback, which would poison the HNSW index by mixing non-comparable vectors. A startup probe asserts dim === 384 and computes a stable identity fingerprint that catches model/quantization drift at the boundary. - src/learning/embedder-endpoint-client.ts: OpenAI client with keep-alive agent, 5s/30s timeouts, Bearer auth from env, circuit breaker, probe - src/learning/real-embeddings.ts: endpoint branch placed strictly before the dynamic transformers import so the cold-load fix holds at runtime; new isUsingEndpoint() / getEndpointIdentity() observability helpers - 23 new unit tests (real http.createServer fixtures, no http mocking) - End-to-end smoke: 9ms for import + init + probe + embed, transformers never appears in the loaded-module list Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d698c58721 |
fix(routing): collapse Q-state to 3-dim (drop complexityBucket) (#499)
ADR-096 fix #3 (Option A): the previous 4-dim state_key taskType|priority|domain|complexityBucket fragmented semantically identical tasks across cells. complexityBucket was derived from raw description length (round(min(len/200,1)*10)) — so a 55-char and 130-char phrasing of the same task landed in different Q-rows, and QWEIGHT_RAMP_VISITS=20 was rarely reachable per cell even after the fix #1/#2 loop closure. State key is now 3-dim: taskType|priority|domain Same change applied at both ends of the loop: - buildRoutingStateKey (agent-routing.ts) — parameter and key shape - updateHookRouterQValue (hooks-dream-learning.ts) — state_key SQL - qe-reasoning-bank.ts reader-side call - task-hooks.ts post-task writer call - routing-hooks.ts post-route writer call (new in fix #1) `deriveComplexityBucket` stays exported (marked @deprecated for state_key use) — the pre-task bridge payload still carries `complexityBucket` for kv_store schema compatibility and any downstream telemetry. `TaskBridgePayload` interface unchanged. Migration: existing rl_q_values rows written under the previous 4-dim key are orphaned. Field reports show real installs have at most ~2 such rows (the loop was broken; no real Q-data accumulated), so we accept the orphans rather than ship a migration. If a future install carries substantial 4-dim data, a one-shot script that strips the trailing |N segment is straightforward. Test contract extended: - agent-routing-exploration.test.ts pins the 3-segment key shape - route-q-loop-closure.test.ts asserts complexityBucket does NOT leak into updateHookRouterQValue payloads (writer-side check) and that bridge.complexityBucket is ignored even when present. ADR-096 updated: complexity-bucket promoted from Open Questions to Decision (Option A). Open Questions now lists "re-introducing a coarse complexity tier if telemetry shows the 3-dim key conflating decisions that benefit from differentiation" as a wait-and-measure follow-up rather than a planned change. Refs: #499 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d41bbc22b0 |
fix(routing): close Q-loop on routing surface (#499)
The route hook (UserPromptSubmit) reads `rl_q_values` to produce qWeight, but the only writer was task-hooks `post-task` gated on `outcome.bridge`. On real projects, `routing_outcomes` reached 139 rows while `rl_q_values` stayed at 2 — the route surface that displays qWeight had nothing to read, so `qWeight: 0` was structurally pinned. Two complementary producer additions, ADR-096: Fix #1 — post-route trains rl_q_values When the Stop hook resolves a routing_outcomes sentinel, derive a state_key from the sentinel's `task_json.description` using the same exported helpers the route hook used (`deriveTaskType`, `deriveComplexityBucket`, `detectQEDomains`) and call `updateHookRouterQValue`. Writer and reader address byte-identical rows. Best-effort + try/catch: Stop hooks must never crash the host. Fix #2 — post-task fires even when bridge is absent Drop the `if (outcome.bridge)` gate. When bridge is missing, derive state from `options.description` instead. Adds a new `--description` option to `aqe hooks post-task` so the PostToolUse hook command can pass `tool_input.description` for Task tools. Skip the update only when neither bridge nor description is available (would pollute an "unknown" sentinel state). Tests: tests/unit/cli/commands/route-q-loop-closure.test.ts pins the producer/consumer state_key alignment contract — if a future refactor desynchronizes the three call sites (routing-hooks.ts post-route, task-hooks.ts post-task, qe-reasoning-bank.ts:530), assertions fail the build. Closes the Open Question flagged in ADR-095. The deriveComplexityBucket fragmentation Jordi noted is acknowledged in ADR-096's Open Questions and tracked for a follow-up — orthogonal to closing the loop. Refs: #499 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c6b3142f09 |
test(learning): split aqe-learning-engine.test.ts into three forks (issue #448 step 2)
Mirrors the qe-reasoning-bank split: the original 622-line file ran
~43 tests in a single fork, most of which spun up a full
AQELearningEngine (transformer + HNSW + WASM coherence) via
beforeEach. Distributing across three sequential forks (pool=forks,
maxForks=1, fileParallelism=false) caps per-fork heap.
- _aqe-engine-test-helpers.ts (NEW, not a test file)
Shared createMockMemoryBackend() and createMockEventBus() factories
pulled out of the original so the splits don't duplicate. Underscore
prefix keeps it out of vitest's test-file include glob.
- aqe-learning-engine-factory.test.ts (NEW, 4 tests, no init)
Factory Functions + DEFAULT_ENGINE_CONFIG. Tests only call the
constructor — no engine.initialize() — so transformer / HNSW /
WASM aren't loaded. Safe to include in test:unit:fast.
- aqe-learning-engine-patterns.test.ts (NEW, 15 tests, full engine)
The three heaviest sub-describes: Pattern Learning, Task Tracking,
Experience Capture Integration. Each test runs a full engine init.
Excluded from test:unit:fast until step 3's dispose hooks are
proven in CI.
- aqe-learning-engine.test.ts (SLIMMED, ~21 tests, full engine)
Init / Status / Task Routing / Model Routing / Codebase Analysis /
Guidance / Dispose. Header comment updated to reference the two
companion files. Inline mock factories replaced with imports from
the shared helper.
Coverage preserved: ~43 tests still run under test:ci, across three
forks instead of one.
Issue: #448
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f0a7b5acaa |
test(learning): release module singletons between tests (issue #448 step 3)
The heavy QEReasoningBank / AQELearningEngine test files re-init a full
bank in every `beforeEach`, but several module-level singletons in the
transitive graph survive across tests and accumulate per run:
- real-embeddings.ts: `pipeline`, `embeddingModel`, `embeddingCache`
(the embedding Map grows for every distinct text routed through
`computeRealEmbedding()` and is never cleared).
- audit/witness-chain.ts: `_instance` retains the WitnessChain with
accumulated entries and its better-sqlite3 handle.
Wire-in:
afterEach(() => {
clearEmbeddingCache(); // existing export, just unused
_resetWitnessChainForTests(); // new test-only reset added here
});
afterAll(() => {
resetInitialization(); // existing export, full pipeline reset
});
`clearEmbeddingCache()` and `resetInitialization()` already existed in
real-embeddings.ts as test hooks but nothing was calling them. The
witness-chain singleton had no reset path — this commit adds a
`_resetWitnessChainForTests()` helper next to the existing factory
(underscore-prefixed to mark it as internal/test API).
The hooks are applied to all three files in the split (qe-reasoning-bank
core, qe-reasoning-bank-feedback-loop, aqe-learning-engine).
Local validation note: this Codespace's heap ceiling is too tight to
reproduce CI's cumulative-growth regime — a single bank init alone
OOMs at 6GB here. CI is the canonical environment for measuring
whether the dispose hooks restore acceptance #1. Re-enabling the heavy
files in test:unit:fast (acceptance #2) is held until CI confirms.
Issue: #448
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ea3566f9a2 |
test(learning): split qe-reasoning-bank.test.ts into three forks (issue #448 step 2)
The original 1019-line file ran 58 tests in a single vitest fork, each
spinning up a full QEReasoningBank (transformer embeddings + HNSW + WASM
coherence). Cumulative per-test memory growth blew past the heap ceiling
and the publish workflow timed out on multiple v3.9.x releases.
This commit extracts the file into three forks so the heap pressure is
distributed across three sequential processes (vitest config:
pool=forks, maxForks=1, fileParallelism=false):
- qe-reasoning-bank-utilities.test.ts (NEW, 29 tests, no bank)
Pure-function tests for qe-patterns + qe-guidance. Module-graph weight
is negligible — runs in ~10ms with no transformer load. Safe to
include in test:unit:fast.
- qe-reasoning-bank-feedback-loop.test.ts (NEW, 9 tests, full bank)
recordOutcome / qe_pattern_usage feedback-loop tests with vi.doMock
churn over '../../../src/kernel/unified-memory.js'. Carries the
heaviest mocking surface, so isolated to its own fork.
- qe-reasoning-bank.test.ts (SLIMMED, 20 tests, full bank)
Init / Pattern Storage / Task Routing / Learning Outcomes /
Guidance Generation / Statistics / Embedding. Header comment
updated to reference the two companion files.
Both heavy files (-feedback-loop and the slim core) remain excluded from
test:unit:fast for now. Re-enabling them is acceptance #2 of the issue
and requires step 3 of the proposed solution (lazy / disposable hooks on
real-embeddings.ts and coherence-service.ts) — local repro in a 16GB
Codespace shows even the slim 9-test fork still OOMs at 6GB heap,
indicating per-test cumulative growth that file-splitting alone doesn't
cure. The split is preparatory and reduces the leak slope from
58×Δ to max(20,9,0)×Δ per fork, which should give CI headroom and
clear the way for the dispose-hooks fix in step 3.
Issue: #448
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f97e33186d |
test(integration): pin package.json exports contract for kernel subpaths
Regression guard for PR #497: a shipped subpath missing from the package.json `exports` map throws ERR_PACKAGE_PATH_NOT_EXPORTED under Node ESM strict resolution, even when the compiled file is present. Three assertions per declared subpath: 1. Entry exists with `import` and `types` keys. 2. Referenced files exist in dist/ (skipped when dist/ not built). 3. The documented symbol is reachable via dynamic import. Negative-control verified: removing the `./kernel/hnsw-adapter` entry causes the first assertion to fail with a "missing from exports map (see PR #497)" message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c83c0345b3 |
chore(release): bump version to v3.9.34
Stops unbounded growth of patterns.rvf / brain.rvf. - #495: AbortSignal in HybridMemoryBackend.initialize() so a 5s createHybridBackendWithTimeout abort actually cancels the work instead of leaking a 14-min runaway init (~20 GB / 12 MB/s leak observed in the field). UnifiedMemoryManager.initialize() also accepts {signal} with throwIfAborted() at each await seam. - Post-dream compaction in DreamScheduler — threshold-gated compact() against patterns.rvf and brain.rvf so append-only growth has a steady-state janitor. - Boot-time size guard in getSharedRvfAdapter() for one-shot recovery of installations already in the bad state. - Bounded SQLite→RVF backfill (top-1000 by last_used_at) so `: > patterns.rvf` for disk recovery doesn't trigger an unbounded replay (the "regrew to 59 GB" pathology). - RvfDualWriter.compact() for brain.rvf parity. Three operator env knobs added (no rebuild needed): - AQE_RVF_SIZE_GUARD_BYTES (default 256 MB) - AQE_RVF_DEAD_RATIO_THRESHOLD (default 0.30) - AQE_RVF_BACKFILL_LIMIT (default 1000) 29 new unit tests. Performance gate 15/15 pass. test:ci 4035 pass (3 pre-existing failures, verified identical on main). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b62eb83ad4 |
fix(learning): rehydrate Date fields at all kv-read seams (#493 follow-up)
#493 was not the last instance of the kv-Date hazard — it was the most reachable. Audit of the rest of the learning pipeline found 11 more unprotected kv-read sites where TypeScript declares `Date` but JSON round-trip through `memory.get` yields an ISO string at runtime. Two failure modes, both already documented at learning-coordinator.ts:898-904 (#491 Bug 3): 1. THROW — `record.someDate.getTime()` raises TypeError. Surfaces immediately on the first invocation once any record exists. 2. SILENT — `timeRange.contains(record.someDate)` or `new Date() > record.expiresAt` coerces string vs Date to NaN; all comparisons return false; records are silently filtered out or expiry is never recognised. No error, just empty/stale analytics. The silent variant is the worse one — #491 Bug 3 was the same class and shipped for a release cycle undetected. Strategic fix: a single `rehydrateDates<T>(record, dateFields[])` helper at `src/shared/utils/kv-date-rehydrate.ts` applied at every kv-read seam that touches Date-bearing types. Shallow-copies the record and coerces named string fields back to Date; pass-through for already-Date, null, undefined. Sites patched (11 total): src/domains/learning-optimization/services/ production-intel.ts — 4 sites (HIGH #2-#5 throw, MEDIUM #8 silent) getMetricsHistory, getRecentIncidents (×2), resolveIncident, getRecentMilestones metrics-optimizer.ts — 1 site (HIGH #1 throw) getMetricsHistory.sort learning-coordinator.ts — 1 site (HIGH #6 throw) getReplayBuffer.sort transfer-specialist.ts — 4 sites (HIGH #7 throw, MEDIUM #9 silent) queryKnowledge (×2), getKnowledgeById, bulkTransfer src/domains/learning-optimization/ coordinator-helpers.ts — 1 site (SILENT) getExperiencesForDomain coordinator.ts — 1 site (SILENT, duplicate of above) getExperiencesForDomain (private) Sites verified SAFE (kv-read but only access non-Date fields): - coordinator.ts:1045/1134/1175 — uses .domain/.confidence/.version - metrics-optimizer.ts:676 — uses .metrics map - learning-coordinator.ts:379/438/483/547/830/842 — uses .confidence/.successRate/.domain/.usageCount - learning-coordinator.ts:901 — already has #491 Bug 3 inline rehydration with instanceof guard - experience-capture.ts:520/563 — TaskExperience uses number timestamps, not Date Sites NOT patched (deferred per scope): - coordination/protocols/learning-consolidation.ts — defined but never imported anywhere. Dead code; would resurrect the bug if reactivated. Documented in the audit but not fixed. Regression tests (29 new, 6 files): tests/unit/shared/utils/kv-date-rehydrate.test.ts (10) tests/unit/domains/learning-optimization/ production-intel-date-coercion.test.ts (5) metrics-optimizer-date-coercion.test.ts (2) learning-coordinator-date-coercion.test.ts (2) transfer-specialist-date-coercion.test.ts (5) coordinator-helpers-date-coercion.test.ts (2) Each service test seeds its mock memory backend with kv-shape records (ISO strings instead of Date instances — matching the post-JSON.parse shape that better-sqlite3-backed memory yields in production) and exercises the kv-read → consumer path that would fail pre-fix. Both throw and silent failure modes are pinned: throws are asserted not to fire, silent failures are caught by behavioural assertions (e.g. the expired-knowledge test asserts `expired.score < live.score * 0.5 ±`, which would fail pre-fix because the 0.5 penalty never triggered). Verification: - 29/29 new regression tests green - 135/135 existing tests for patched services green (production-intel, metrics-optimizer, transfer-specialist, learning-coordinator, learning-consolidation, learning-consolidation-mining, learning-consolidation-liveness) - `tsc --noEmit` clean - `npm run build` produces dist with rehydrateDates at all 17 expected call sites across the 6 patched files Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
daa9fcf0a0 |
fix(learning): rehydrate lastUsedAt at kv seam to unblock consolidation (#493)
`LearningConsolidationWorker.getDomainPatterns()` mapped `lastSeen: p.lastUsedAt`, but `lastUsedAt` arrives from the `learning:pattern:*` kv as an ISO string (not a Date) after the JSON round-trip. The two downstream consumers — `consolidatePatterns` and `pruneIneffectivePatterns` — both call `.getTime()` on it, throwing `TypeError: lastSeen.getTime is not a function` every cycle on installs with non-empty pattern kv. This was dead code until v3.9.32: #491 Bug 1+2 starved the pattern store so `collectPatterns()` always threw `No learning patterns to consolidate yet` before reaching `consolidatePatterns`. Fixing the #491 chain made this line reachable for the first time and the same `Date`-lost-across- kv-JSON-round-trip class of bug as #491 Bug 3 surfaced. Other call sites were hardened in v3.9.32 (`pattern-store.ts:1301-1304`, `pattern-lifecycle.ts:592-594`, `sqlite-persistence.ts:891`); these two were missed because they were unreachable at the time. Fix: rehydrate at the kv-read boundary — `lastSeen: new Date(p.lastUsedAt)`. `new Date(x)` is safe for `Date | string | null | undefined` (yields Invalid Date — whose `.getTime()` returns `NaN` rather than throwing — on null-ish input). One-line change covers both downstream consumers. Test: `learning-consolidation-date-coercion.test.ts` exercises the full post-mining half of the loop (the gap Jordi flagged in his release-gate suggestion). Seeds a `learning:pattern:*` entry with ISO-string `lastUsedAt`, runs the worker through `getDomainPatterns` → `consolidatePatterns` → `pruneIneffectivePatterns` → full `doExecute`, and asserts: 1. The seam rehydrates `lastSeen` to a real Date. 2. `consolidatePatterns` completes without throwing. 3. The recent-high-performer filter math actually counts the pattern (not just "doesn't throw" — catches silent-suppression regressions). 4. `pruneIneffectivePatterns` completes and produces the stale finding. 5. Full `doExecute` reaches `Learning consolidation complete` and `recordLoopHealth(success:true)` — the user-visible signals. Verified pre-fix with 5/5 failing tests showing the exact error from the issue; post-fix with 5/5 green, full learning-consolidation suite (36 tests) green, and `tsc --noEmit` clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1d1190ca7e |
test(release-gate): add A23 daemon-liveness assertion (#491)
Issue #491 exposed a structural gap in the pre-publish init gate: it runs `aqe init` against 4 pinned real repos and asserts file existence, KG entry counts, double-init etc — 22 separate assertions — but it NEVER starts the daemon. Every one of the four bugs in #491 lives in the daemon-runtime seam, not the init seam: the kernel→worker wiring opens during `fleet_init`, which only happens at daemon startup. So a fully-broken learning loop sailed through the gate as "OK". This commit adds A23 to run_one(): - opt-in per fixture via the new `expectDaemonLiveness` manifest flag (default false, so existing fixtures keep their ~5s budget) - configurable wait via `daemonLivenessWaitSec` (default 20s) - spawns the cleanroom's start-daemon.cjs, waits, greps stderr and daemon.log for the failure shapes from the #491 chain: "domain not available", "Domain not available", "not available for" - asserts zero matches; lists offending lines + summary marker on fail - cleans up the daemon process (SIGTERM, 2s grace, SIGKILL) Pilot fixture: enabled on `tiny-ts` with a 15s wait window. tiny-ts is the smallest fixture (~17 files, ~6s init), so adding 15s of daemon observation puts the whole fixture at ~22s — well within the 60s timeout and the 20-min job budget. If the pilot is stable for two releases, we extend the flag to mid-ts and p-queue. The larger multi-lang-real fixture stays opt-out until a separate weekly workflow takes long-running daemon scenarios out of the release path. Manifest schemaVersion bumped 3→4 with a changelog entry documenting the new fields. Run-gate header comments updated to document A23 alongside A1-A22. The same script is invoked by post-publish-canary.yml against the published npm version, so A23 doubles as a post-publish smoke for any registry/CDN divergence that affects daemon-runtime — a class of regression neither the build-job bundle smoke nor `test:unit:fast` could detect. This is the structural complement to the npm-publish.yml change in the preceding commit (integration-tests-on-tag-sha runs the SOURCE seam tests; A23 runs the INSTALLED PACKAGE seam check). Together they close the daemon-runtime gap that let #491 ship. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dd22dbddda |
test(workers): generalize #491 Bug 1 protection to all 11 workers
The issue #491 fix wired the daemon WorkerManager to the kernel and shipped one targeted integration test for the learning worker. The same StubWorkerDomainAccess failure mode applies to every other worker that declares targetDomains — test-health, flaky-detector, coverage-tracker, defect-predictor, etc. — and the worker-level unit tests all explicitly punt on this with comments along the lines of "complex dependencies require integration tests for full coverage. Unit tests focus on instantiation." That punt is what let Bug 1 ship in the first place. This test iterates EVERY registered production worker (enumerated dynamically from `daemon.getWorkerManager().list()`), builds the exact WorkerContext the scheduler hands to doExecute, and asserts that each of the worker's declared targetDomains resolves to a real domain API. If the kernel→worker wiring ever breaks again — for any of the 11 workers, not just learning-consolidation — this test fails before the daemon ticks for users. Three assertions: 1. Every registered worker declares ≥1 targetDomain (catches the "shipped a worker with no domains" misconfig). 2. For every (worker, targetDomain) pair, `getDomainAPI(domain)` returns a defined value (the structural Bug 1 contract). Failure list is formatted as a multi-line message so the maintainer sees exactly which seam broke. 3. The worker manager's `context.memory` exposes `vectorSearch` (only HybridMemoryBackend has it, InMemoryWorkerMemory does not) — a cheap structural check for the Bug 4b memory swap. Verified red→green: with the setKernel/setMemory wiring removed from handleFleetInit, assertions 2 and 3 fail, listing every (worker, domain) pair that lost its API. With the wiring restored, 3/3 pass in ~170ms. The test deliberately does NOT call `daemon.runWorker(id)` to exercise each worker's full body. Most workers raise legitimate "no data yet" errors on fresh installs (test-health, flaky-detector etc.) and asserting completion would couple this test to each worker's empty-state behaviour. The contract is structural — the seam to the kernel is alive — not behavioural. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
358990cc01 |
fix(kernel,learning): namespace-aware reads + timestamp rehydration (#491 Bug 2+3)
Issue #491 — even with the worker→kernel wiring restored (Bug 1/4b), `mineExperiences` mined 0 for every domain on installs with hundreds of indexed experiences. Two stacked bugs in the learning read path: Bug 2 — namespace mismatch (the keystone) ========================================= LearningCoordinator writes every experience with `namespace: 'learning-optimization'`, but the MemoryBackend interface only let writers specify a namespace; reads had no such option. HybridBackend.get/search/has/delete therefore hard-coded `defaultNamespace='default'`. The coordinator could not read back its own data — `learning:experience:index:domain:<d>:*` returned 0 keys even when the DB held 81+ matching rows in the `learning-optimization` namespace. Fix: - Add `RetrieveOptions { namespace? }` to kernel/interfaces.ts (symmetric with the existing `StoreOptions.namespace`). - Extend MemoryBackend.get/has/delete/search to accept the option (backward-compatible — undefined still routes to defaultNamespace). - HybridBackend honors the option in all four methods. - InMemoryBackend likewise; its `search` now also scopes the scan by namespace prefix when one is supplied, so unit-test fakes behave the same as the production hybrid backend. - All 15 read sites in LearningCoordinator pass `LEARNING_NS = { namespace: 'learning-optimization' }`, matching the writes that already specified it. One pre-existing unit test (memory-backend.test.ts) was calling `backend.get(key, 'test-ns')` against the undocumented positional namespace param InMemoryBackend used to accept. Migrated it to the options form — the positional shape was never part of the MemoryBackend interface. Bug 3 — Date vs ISO-string in time-window filter ================================================ `recordExperience` stores `timestamp: new Date()`. JSON.stringify serializes it to an ISO string and JSON.parse does not rehydrate. `TimeRange.contains` does `date >= start`; in `string >= Date`, the string coerces to NaN and every comparison is false — so even with Bug 2 fixed, the window filter would still silently drop every row. Fix: in `getExperiencesByDomainAndTime`, coerce `experience.timestamp` to a real `Date` at the kv-read boundary before calling `timeRange.contains`. Downstream callers receive a hydrated Experience with a proper Date. We deliberately fix this at the boundary rather than making `TimeRange.contains` tolerant of strings — the bug is "kv round-trip drops the Date type", not "the value object is wrong", and a tolerant comparator would mask future type drift everywhere else. Round-trip integration test: tests/integration/learning/coordinator-roundtrip.test.ts pins three assertions: 1. recordExperience → mineExperiences finds the experience back (this returned 0 before either fix). 2. The mined experience's timestamp is a real Date instance, not a string (proves Bug 3 fix is at the boundary). 3. A time window deliberately in the past still excludes recent writes (proves we didn't fix Bug 3 by making the filter a no-op). Verified red→green: with Bugs 2+3 reverted (interface kept), tests 1 and 2 fail with `experienceCount=0` and `rows.length=0` — exactly the symptoms from the issue. With both fixes in place, 3/3 pass in ~30ms. 466/466 kernel unit tests and 335/335 adjacent suites stay green; the only test that needed migration is documented above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7f13f129dd |
fix(workers): always emit loop-health from learning worker (#491 Bug 4a)
Issue #491 — `aqe learning loop-health` permanently showed `LearningConsolidationWorker ○ never-ran` even when the worker executed every cycle. Root cause: in `doExecute`, the `recordLoopHealth(success:true)` ping sat *after* `collectPatterns()`, which throws on installs with nothing to consolidate (the common case for fresh users). The error propagated up before liveness was ever recorded, so the dashboard had no idea the worker was alive. Fix: wrap the worker body in try/catch/finally — the same shape CapturedExperienceBridge.drainSafe already uses (which is why the bridge correctly shows `live` and the worker did not). The `finally` emits `recordLoopHealth` with `success:true` on a clean tick and `success:false + error` on a throw, then the catch rethrows so BaseWorker's retry/error tracking sees the failure unchanged. The liveness write is itself wrapped in try/catch so a memory-backend hiccup can't shadow the worker's real error. Liveness contract test: tests/unit/workers/workers/learning-consolidation-liveness.test.ts pins three behaviors: 1. Throw inside collectPatterns → recordLoopHealth still called with `{success:false, error}` (THIS is what regressed before). 2. Successful tick → recordLoopHealth called with `{success:true}`. 3. recordLoopHealth itself throwing must not shadow the original worker error. Verified red→green: with the try/finally reverted, assertion (1) fails with "expected number of calls: 1, received: 0" — exactly the "never-ran" symptom from the issue. With the fix in place, 3/3 pass in ~19ms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eca8d86b37 |
fix(mcp,workers): wire daemon to kernel after fleet_init (#491 Bug 1+4b)
Issue #491 (Jordi Izquierdo) — on a vanilla v3.9.31 install, every domain-dependent worker tick fails with "<domain> not available" and `aqe learning loop-health` shows `learningWorker: never-ran`. Root cause: the MCP-hosted daemon constructs its WorkerManager in `src/mcp/entry.ts` *before* the kernel exists, so it starts with StubWorkerDomainAccess (getDomainAPI returns undefined for every domain) and a private InMemoryWorkerMemory that no dashboard ever reads from. The kernel is built inside `handleFleetInit` later in the same process, but the daemon is never told about it. Fix: - Promote `setKernel`/`setMemory` from concrete-class-only methods to the public WorkerManager interface so consumers can find them. - Add `setMemory(memory: WorkerMemory)` to WorkerManagerImpl (mirrors the existing `setKernel`). Broadens the internal `memory` field from InMemoryWorkerMemory to the WorkerMemory interface so a shared HybridMemoryBackend is type-correct. - Hoist `KernelReference` to interfaces.ts as `WorkerKernelReference` so the interface signature is properly typed without re-introducing the kernel↔worker circular dependency the local declaration was avoiding. - In `handleFleetInit`, after `pluginLoader.loadAll()`, call `getDaemon().getWorkerManager().setKernel(state.kernel)` and `setMemory(state.kernel.memory)`. Wrapped in try/catch so a workless smoke install (no daemon module bundled) still gets a working kernel + queen. Why an interface change, not the cast Bolivar used: the public WorkerManager interface didn't expose setKernel/setMemory at all, so the call site looked like it was reaching for a private method — exactly the kind of seam that previous releases dropped on the assumption that the bridge superseded it. Making it part of the public contract means the next maintainer can't accidentally remove the wiring without breaking a typed interface. Integration test: tests/integration/mcp/fleet-init-wires-daemon.test.ts pins three assertions: 1. Before fleet_init, the daemon's WorkerManager returns undefined for `getDomainAPI('learning-optimization')` (the Bug 1 failure mode). 2. After fleet_init, it returns a real plugin API. 3. The WorkerManager's memory is the same instance as the kernel's memory (Bug 4b), so worker writes reach the dashboard's reads. Verified red→green: with the wiring reverted, all three fail with "expected undefined to be defined"; with the wiring in place, all three pass in ~210ms. Note: Bugs 2, 3, and 4a are addressed in follow-up commits in this series. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
43ebcaa326 |
fix(routing,kernel,cli): audit follow-ups — CRITICAL #1 + HIGH #1/#2 + smoke
Addresses three findings from the brutal-honesty audit of the Phase 1-4 session (commits 66553141..70154c1b). Real integration smoke included. CRITICAL #1 — mincut safety gate inverted on empty graph ======================================================== Before this fix, `QEReasoningBank.routeTask` consulted `getSharedMinCutMonitor().isCritical()` unconditionally. The mincut calculator returns 0 on an empty graph; `isCritical()` is defined as `minCutValue < warningThreshold` — so an empty graph reported as critical and ε was dampened to 0.2x. CLI hook routing is the ONLY place ADR-095 exploration matters in practice, and the SwarmGraph is empty there (Queen coordinator activity is what populates it). So exploration was permanently dampened in the exact deployment case ADR-095 was meant to help. Fix: - new src/learning/routing-topology-gate.ts with `resolveTopologyCriticalFromSharedMincut()` — returns true ONLY when graph has actual vertices AND `isCritical()` returns true. Empty graph = no signal = full ε rate. - qe-reasoning-bank.ts routeTask now uses the helper. - 4 regression tests in tests/unit/learning/routing-mincut-safety-gate.test.ts cover: never-initialized, initialized-but-empty (regression case), populated-with-critical (legit dampening), populated-with-healthy. HIGH #2 — dream-scheduler-wiring test passed by way of failure ============================================================== The previous test used `memoryBackend: 'memory'` which made the DreamEngine.initialize() fail ("UnifiedMemoryManager not initialized") in every run. The kernel's try/catch caught the failure and the test asserted the kernel survived — NOT that the scheduler actually started. The happy-path was never exercised. Rewrite uses a real tmpdir SQLite backend AND resets BOTH singletons between tests (UnifiedMemoryManager + UnifiedPersistenceManager — the latter was missing). Adds an assertion that `_dreamScheduler !== undefined` after initialize() so we know the scheduler ACTUALLY STARTED. The previous "failure tolerance" test was removed: with proper singleton hygiene there's no clean way to force scheduler init to fail without invasive mocking. The behavior is enforced by the try/catch in kernel.ts and visible by inspection. Also added an `await scheduler.dispose()` in the kernel's catch path so a partially-constructed scheduler doesn't leak. HIGH #1 — Real integration smoke against a tmpdir ================================================== Ran (and documented): 1. `aqe init --auto` in /tmp/aqe-smoke-* — fresh shop 2. Inspected generated .agentic-qe/workers/start-daemon.cjs: ✓ contains `candidates.push(require.resolve('agentic-qe/dist/mcp/bundle.js'))` ✓ contains `WARNING: using npx fallback` message (B.1 verified in actual artifact, not just source template) 3. Confirmed routing_outcomes schema in the fresh DB: ✓ exploration INTEGER, criticality REAL, q_weight REAL all present (ADR-095 migration applies at CREATE TABLE time for new installs) 4. Hand-seeded 12 exploit + 3 explore rows; ran `aqe learning loop-health`: ✓ JSON path surfaces routingDiversification with correct counts, avg quality per bucket, criticality, q_weight ✓ Text path now surfaces the section even when loop-health kv is absent (bug fix below) CLI-bundle smoke for `aqe hooks route` and full daemon start OOMs in this 8GB codespace even at 4GB heap — that's an environment limitation documented under qe-reasoning-bank.test.ts; CI has the headroom. Bug found during smoke: aqe learning loop-health (non-JSON) returned early when no loop-health kv record existed, never reaching the routing-diversification render. Operators in CLI-only deployments (routing happens but daemon-side workers never tick) would see an empty dashboard. Fixed by removing the early return. Honest framing on qe-reasoning-bank.test.ts "flaky" claim ========================================================== It isn't flaky — it's resource-heavy. The project already excludes it from `test:unit:fast` (package.json) and runs it in `test:ci` where heap headroom exists. 58 tests × full transformer-embeddings init + HNSW indexing exceeds an 8GB codespace's default heap. I framed it as "flaky" in the audit. That was wrong. Added an extended header comment to the test file documenting the project convention so future readers don't repeat the same mistake. Test status =========== 638/638 tests pass across the 47 impacted unit suites. The +4 net additions (vs 634) are the new routing-mincut-safety-gate tests; dream-scheduler-wiring stayed at 5 tests (one removed, four expanded). Build clean. Type-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
70154c1b72 |
feat(routing): three-signal routing — static + Q-value blend + mincut-gated ε-greedy (ADR-095, #488 Phase 4)
Closes the self-learning loop: the Q-value Bellman updates that #487 populates now have a routing-side reader; structurally-aware exploration (via the mincut infrastructure from ADR-047 / ADR-068) gathers Q-table signal beyond the deterministic greedy winner; the catalog diversifies without sacrificing safety on bottleneck decisions. Three signals blended at the agent-routing decision point: 1. Static score (existing): domain match + capability match + performance prior + language boost + pattern similarity. Computed by `calculateAgentScores` in src/learning/agent-routing.ts. 2. Q-value blend (NEW — reads from rl_q_values): effectiveScore = staticScore * (1 - qWeight) + sigmoid(qValue) * qWeight qWeight ramps from 0 (no Q-data) to MAX_Q_WEIGHT=0.4 over QWEIGHT_RAMP_VISITS=20 visits per (state_key, action_key). Cold-start behavior is identical to pre-ADR-095 static-only routing; the learned signal contributes meaningfully only after the Q-table has matured for that state. 3. Mincut-gated ε-greedy exploration (NEW): baseEpsilon = AQE_ROUTER_EXPLORATION_RATE env or 0.05 default safetyMultiplier = getSharedMinCutMonitor().isCritical() ? 0.2 : 1.0 epsilon = baseEpsilon * safetyMultiplier With probability ε, swap the greedy winner with a uniformly-random pick from the top-3 alternatives. Uses crypto.randomInt (NOT Math.random) to match the codebase convention (randomUUID is the established crypto entry-point in the same modules). Maintainer decisions captured in ADR-095 v2: 1. Default ON (Option A): mincut-modulated exploration ships enabled. AQE_ROUTER_EXPLORATION_RATE=0 is the documented escape hatch. 2. Telemetry surface: extended `aqe learning loop-health` with a new "Routing diversification" section showing 7-day exploit/explore counts, avg quality_score per bucket, avg criticality, avg q_weight. 3. crypto.randomInt over Math.random — codebase convention preserved. 4. Q-value ramp constants are best-guess (MAX_Q_WEIGHT=0.4, QWEIGHT_RAMP_VISITS=20); tune post-deploy from telemetry. Source-level findings worth noting: - `rl_q_values` was populated by `updateHookRouterQValue` since #487 but NEVER read during routing. Verified via grep across src/learning/, src/routing/, src/cli/commands/hooks-handlers/. This commit adds the reader; closes that loop. - Mincut graph is populated by Queen coordinator swarm activity, NOT per-task. So the "criticality" signal is graph-level safety (the `isCritical()` boolean from MinCutHealthMonitor) rather than per-task structural weight. Honest framing reflected in the ADR. What landed: src/kernel/unified-memory-schemas.ts - routing_outcomes gains exploration (INTEGER NOT NULL DEFAULT 0), criticality (REAL), q_weight (REAL) columns + an exploration index for the loop-health bucket queries src/routing/routing-outcomes-migration.ts (NEW) - ensureRoutingOutcomesAdr095Columns(db) — idempotent ALTER TABLE for pre-existing databases. Process-local flag avoids repeat work. src/routing/routing-feedback.ts - hooks the migration helper into the existing schema-migration block src/learning/agent-routing.ts - new helpers: blendStaticAndQValue, resolveExplorationRate, applyExplorationPolicy (uses crypto.randomInt), sigmoid normalization - new helpers: deriveTaskType, buildRoutingStateKey, deriveComplexityBucket — moved from task-hooks.ts so the routing path and the post-task Q-update path share the same state_key derivation - calculateAgentScores accepts optional qValueLookup; backward-compat when undefined (identical pre-ADR-095 behavior) - ScoredAgent gains optional telemetry fields: staticScore, qWeight, qValue, qVisits, exploration src/learning/qe-reasoning-bank-types.ts - QERoutingResult gains optional exploration / criticality / qWeight src/learning/qe-reasoning-bank.ts - routeTask builds the Q-value lookup from rl_q_values keyed by stateKey = deriveTaskType(task) | normal | domain | complexityBucket - applies exploration policy after sort; reports avgQWeight + the mincut safetyMultiplier in the QERoutingResult - new private buildQValueLookup helper with table-exists guard so the method is a graceful no-op when the schema isn't applied yet (tests, fresh-init scenarios) src/cli/commands/hooks-handlers/task-hooks.ts - private deriveTaskType removed (DRY: now imported from agent-routing) - pre-task sentinel INSERT writes the three new columns - migration helper invoked before write src/cli/commands/hooks-handlers/routing-hooks.ts - JSON output passes exploration / criticality / qWeight through - INSERT writes the three new columns; migration helper invoked src/cli/commands/learning.ts - `aqe learning loop-health` queries routing_outcomes for 7-day rolling exploration stats and avg quality_score per bucket. Skipped silently when the columns don't exist (pre-ADR-095 databases). Tests: - tests/unit/learning/agent-routing-exploration.test.ts: 35 tests cover sigmoid blend math, qWeight ramp, env override priority, mincut safety multiplier, exploration rate statistical tolerance, only-top-3 alternative constraint, deriveTaskType regex coverage, stateKey format, complexityBucket scaling, end-to-end Q-blend - tests/unit/routing/routing-outcomes-migration.test.ts: 5 tests cover fresh-DB migration, idempotent re-run, missing-table tolerance, exploration index creation, column type/default checks 634/634 tests pass across CLI commands, workers, bridge, learning, init/phases, architecture, kernel, and routing suites; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
874ae39c6a |
feat(learning,bridge): dream_insights retention + bridge cursor guard (#488 Phase 3)
Two small defensive changes that close the remaining cleanup-and-race gaps from #488. No new ADRs needed — both are additive within already-documented subsystem boundaries. C.2 — dream_insights retention pruner ===================================== The dream_insights schema has no expires_at column and no foreign-key cascade fires on regular cycle delete (cycles aren't routinely purged either). Without pruning the table grows linearly with hook activity — V0.03 evidence in #488 documents 120+ rows in 30h, most with applied=0. Over months `getPendingInsights` queries get slower and vector embeddings (if attached) grow unbounded. src/learning/dream/dream-insights-pruner.ts (NEW) - pruneStaleDreamInsights(db, { retentionDays = 30 }): DELETE FROM dream_insights WHERE applied = 0 AND created_at < datetime('now', '-N days') - Applied insights stay forever (audit trail). - Recently-created unapplied insights stay regardless of applied state — they might still be picked up by a future applyInsight. - Guards against the table not existing (fresh init before the dream schema is applied). Idempotent. src/workers/workers/learning-consolidation.ts - new Step 8 in runContinuousLearningLoop after the mineExperiences sweep: prune insights, emit a finding if pruned > 0, surface `dreamInsightsPruned` in ConsolidationResult and domainMetrics. - Failures isolated per-try-block (worker continues with the rest of its lifecycle work). C.3 — bridge cursor monotonic guard =================================== CapturedExperienceBridge.drain wrote `this.cursor` to the kv unconditionally. Cross-process race: two daemons (possible before #488 B.1 fixed the pidfile, still possible if user manually spawns extras) drain overlapping rows, then the "slower" process overwrites the kv with its older value. Next drain re-publishes already-drained rows; downstream plugins without event-ID dedup see duplicates. src/bridge/captured-experience-bridge.ts - Before persisting the cursor, read back what's stored and write Math.max(this.cursor, persisted). The cursor never regresses. - Read-then-max-write is a TOCTOU but it collapses the regression window to milliseconds — sufficient for the duplicate-daemon edge case. Stronger guarantees (SQL UPDATE WHERE, advisory lock) would bypass the MemoryBackend abstraction without a real-world benefit post-B.1. - Cursor persistence is now best-effort (catch and warn). On failure the next bridge boot re-reads from kv and resumes — a missed write just means we re-publish a small batch, idempotent for ID-dedup'd consumers. Tests ===== tests/unit/learning/dream/dream-insights-pruner.test.ts (NEW) - 8 tests: deletes stale unapplied, keeps applied regardless of age, keeps recent unapplied, honors custom retention, no-op on empty table, no-op when table doesn't exist, default 30-day retention, idempotent re-run tests/unit/bridge/captured-experience-bridge.test.ts (extended) - 3 new tests in describe('cursor monotonic guard'): - does not regress when another writer is ahead - advances normally when this bridge is ahead - sensible fallback when no persisted value exists 594/594 tests pass across CLI, workers, bridge, learning, init/phases, architecture, and kernel suites; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6236a2cdc7 |
feat(kernel): kernel-side dream cycles + hooks-as-producers boundary (ADR-094, #488 Phase 2)
Moves dream cycles out of hook subprocesses into the long-lived kernel
process via the DreamScheduler that already existed but was never wired.
Single cutover, no feature flag — per maintainer decision on ADR-094.
Why this matters
================
Before this commit, every ~20 Edit/Write tool uses triggered a dream cycle
inside a short-lived `npx aqe hooks post-edit` subprocess. The cycle held
a SQLite write transaction for up to 10 seconds while the kernel-owning
MCP server, other hook subprocesses, and user MCP tool calls queued
behind it. stderr from the cycle was consumed by Claude Code's hook
reader and lost from operator visibility. Cold DreamEngine init paid
every ~20 edits.
Now the kernel's `QEKernelImpl.initialize()` boots a `DreamScheduler`
that runs cycles on its own cadence inside the long-lived process. Hook
subprocesses keep only `incrementDreamExperience` (a counter bump that
returns in ~50ms). Operators see `aqe learning loop-health` report the
`dreamScheduler` component as live within one tick.
What landed
===========
src/kernel/kernel.ts
- new private `_dreamScheduler?: DreamScheduler`
- new config flag `enableDreamScheduler` (default `true`, mirrors
`enableExperienceBridge`)
- initialize() constructs DreamEngine + DreamScheduler after the
bridge starts, wraps in try/catch (best-effort), failure logs a
warning and continues (kernel boot never blocks on this)
- dispose() stops the scheduler BEFORE bus/memory cleanup so a dream
in-progress doesn't write to a disposed backend
src/learning/dream/dream-scheduler.ts
- executeDream() now records loop-health on every tick (success or
failure) via the helper from #488 Phase 1 B.2. Operators see
`learning:loop-health.components.dreamScheduler.successesSinceBoot`
grow on each cycle.
src/cli/commands/hooks-handlers/editing-hooks.ts
src/cli/commands/hooks-handlers/task-hooks.ts
- dropped `checkAndTriggerDream` import + call
- kept `incrementDreamExperience` (the counter is the producer-side
signal the kernel scheduler reads)
- JSON output now emits `dreamTriggered: false, dreamReason:
'deferred-to-kernel'` so operator scripts grepping the field see
*where* the cycle actually runs
src/cli/commands/hooks-handlers/hooks-shared.ts
- removed the `checkAndTriggerDream` re-export. The function itself
is still defined in hooks-dream-learning.ts and exported there for
direct test access and the documented rollback path.
tests/unit/architecture/hooks-boundary.test.ts (NEW)
- greps `src/cli/commands/hooks-handlers/` for forbidden patterns:
imports from `src/learning/dream/` and references to
`checkAndTriggerDream`. Skips comment lines so explanatory mentions
don't false-positive. `hooks-dream-learning.ts` is in `allowedFiles`
since it DEFINES the function.
- This is the enforcement mechanism for the boundary contract. A
contributor accidentally re-adding heavy hook-side work gets a
clear CI failure pointing them at ADR-094.
tests/unit/kernel/dream-scheduler-wiring.test.ts (NEW)
- asserts `enableDreamScheduler` default, opt-out path, init+dispose
cleanliness with and without the scheduler, idempotent dispose
tests/unit/cli/commands/post-edit-dream-trigger.test.ts (REWRITTEN)
- was asserting checkAndTriggerDream was called from post-edit (the
#480 fix). That contract is now wrong — the kernel is the only
place that triggers dreams.
- now asserts: counter is bumped (the kernel reads it), no inline
cycle trigger, JSON emits `deferred-to-kernel`, hook stays robust
when the memory backend throws.
docs/implementation/adrs/ADR-094-kernel-side-dream-cycles.md
- status: Implemented
- migration section replaced with the single-cutover rationale and
a documented 4-line rollback path
583/583 tests pass across CLI, workers, bridge, learning, init/phases,
architecture, and kernel suites. Build clean.
Boundary contract (now enforced in CI)
======================================
Hook subprocesses MUST NOT do work that exceeds ~100ms. Acceptable
hook-side work: producer SQLite writes, counter increments, in-memory
routing lookups. Unacceptable: DreamEngine init, multi-second SQLite
transactions, anything that depends on long-lived state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4c0bdfafb9 |
feat(observability): daemon pidfile fix + learning loop-health (#488 Phase 1)
Phase 1 of the #488 production-readiness work, scoped to additive changes that don't need new ADRs. Two parts: B.1 — Daemon pidfile points at the npx wrapper, not the MCP server ================================================================= When neither `node_modules/.bin/aqe-mcp` nor the local `node_modules/agentic-qe/dist/mcp/bundle.js` exists, the generated `.agentic-qe/workers/start-daemon.cjs` falls back to `npx --yes agentic-qe mcp`. `npx` exits as soon as it has forked the real bundle, so `child.pid` is the wrapper PID and the pidfile becomes stale within seconds. `aqe daemon status` then misreports liveness, and the idempotency check at the top of the script spawns duplicate daemons — the reporter hit this twice in two days. Adds `require.resolve('agentic-qe/dist/mcp/bundle.js')` as a 3rd candidate (try/catch-guarded) before the npx fallback. Globally-installed shops (`npm install -g agentic-qe` — the typical case the npx fallback was silently covering) now spawn the real bundle directly; `child.pid` is the long-lived MCP server. The npx fallback remains as a last resort for environments where neither local nor global install is reachable. When taken, it now logs a WARNING to daemon.log so operators can see the degraded pidfile contract. B.2 — `learning:loop-health` observability ========================================== Operators previously had no way to answer "is the self-learning loop closing end-to-end?" without DB-mining `dream_cycles`, `kv_store`, and `routing_outcomes` separately. Cumulative counters in `aqe learning stats` grow forever and can't distinguish "healthy loop" from "loop wedged 3 hours ago with stale totals". Adds a single `learning:loop-health` kv key with per-component liveness: { overallLastSuccess, bootedAt, components: { bridge?: { lastSuccessAt, ticksSinceBoot, ... }, learningWorker?: { lastSuccessAt, ticksSinceBoot, ... }, dreamScheduler?: { lastSuccessAt, ticksSinceBoot, ... }, } } Writers (best-effort — observability MUST NOT crash the observed component): - CapturedExperienceBridge.drainSafe — every poll, success or failure - LearningConsolidationWorker.doExecute — end of every tick - DreamScheduler reserved for #488 Phase 2 (the field stays undefined until Phase 2 wires the scheduler in-kernel) Surface: - new `aqe learning loop-health [--json]` CLI subcommand with component verdicts (live/stale/never-ran) based on per-component staleness thresholds (bridge: 30s; worker/scheduler: 2h) - distinct from the existing `aqe learning health` (per-domain regret analysis); the new command is pipeline liveness, not agent outcome quality Tests: - 12 tests in tests/unit/learning/loop-health.test.ts for the utility: fresh record creation, tick counters, error capture + clear-on-success, multi-component isolation, overallLastSuccess max-computation, throw-safety when the memory backend is broken, staleness verdicts - 4 tests in tests/unit/init/phases/10-workers-daemon-pidfile.test.ts asserting the require.resolve probe is present, try/catch-guarded, ordered before the npx fallback, and that the warning string is in place 541/541 tests pass across CLI commands, workers, bridge, learning, and init/phases suites; build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e266bae929 |
refactor(learning): single writer for pattern usage — qe_pattern_usage + qe_patterns columns (#486 Gap B)
Before this commit two parallel writers bumped pattern usage stats:
- SQLitePatternStore.recordUsage — did BOTH qe_pattern_usage INSERT
and qe_patterns UPDATE inside one transaction (correct).
- hooks-dream-learning.ts inline UPDATE — bumped qe_patterns columns
but SKIPPED the qe_pattern_usage INSERT, so the audit table stayed
EMPTY in production even as qe_patterns.usage_count climbed during
normal hook activity. V0.03 evidence: 0 rows in qe_pattern_usage
despite 33 hits on the dominant cli-hook-code-intelligence pattern.
Extracts the shared accounting into a new helper
src/learning/pattern-usage-recorder.ts with a single transaction:
- INSERT INTO qe_pattern_usage (audit trail per use)
- UPDATE qe_patterns.{usage_count, successful_uses, success_rate,
quality_score, last_used_at, updated_at}
Both writes are atomic — either both land or neither does.
Quality formula matches the canonical pattern-lifecycle.ts shape:
quality_score = confidence*0.3 + min(usage_count/100, 1)*0.2 + success_rate*0.5
Returns updated=false (no throw) when patternId is unknown, because hook
subprocesses iterate selectedPatternIds that may contain stale UUIDs
from a routing kv that drifted from qe_patterns. Callers wanting strict
semantics (SQLitePatternStore.recordUsage) check the flag and throw.
Refactors:
- SQLitePatternStore.recordUsage → thin wrapper around the helper.
Preserves the throw-on-missing contract.
- hooks-dream-learning.ts per-pattern inline UPDATE → replaced with
recordPatternUsage() call. Now passes meaningful metrics
(tokens_saved, source, experience_id) and feedback to the audit row.
Keeps the #455 inline promotion check (separate concern, fires
AFTER the helper has updated the qe_patterns columns).
Invariant established: COUNT(*) FROM qe_pattern_usage WHERE pattern_id=X
equals qe_patterns.usage_count for X. Before this commit that invariant
was systematically violated by the hook path.
Test infrastructure:
- 7 new tests in tests/unit/learning/pattern-usage-recorder.test.ts
cover atomicity, the missing-pattern no-throw contract, the formula
boundary at usage_count=100, null metrics/feedback handling, and
the audit/column count invariant.
- tests/unit/cli/commands/post-task-promotion.test.ts schema seeds
now include qe_pattern_usage (the test fixture was missing the
table that the production schema creates at boot).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
be32ff091e |
feat(workers): auto-trigger mineExperiences per-domain in learning-consolidation (#486 Gap A)
The producer side of the learning-optimization domain pipeline had no
auto-trigger for `mineExperiences`. After v3.9.29's CapturedExperienceBridge
landed, `learning:experience:*` kv populates correctly — but the chain
stopped there because `mineExperiences` was only callable via four manual
MCP tools or an explicit `task_submit`. Nothing scheduled it.
Verified consumer survey before choosing the fix shape:
- `learning:pattern:*` kv writers/readers are all scoped to the
learning-optimization domain + coordination layer
- `qe_patterns` table is the qualified multi-domain catalog with
distinct schema (tier, quality_score, embedding, deprecated_at,
token tracking, successful_uses) — NOT a duplicate of the kv
- The two stores represent sequentially different lifecycle phases:
kv = candidate working memory, table = qualified catalog
- Deprecating the kv would have wide cross-domain blast radius;
auto-triggering `mineExperiences` keeps the boundary intact
Implementation:
- new private `mineExperiencesPerDomain` step in
`runContinuousLearningLoop` (called as Step 7 after lifecycle stats)
- iterates ALL_DOMAINS, reads per-domain cursor from
`learning:consolidation-cursor:{domain}` kv, calls the canonical
`LearningCoordinatorService.mineExperiences(domain, timeRange)`
via `LearningOptimizationExtendedAPI.getLearningService()`
- advances cursor ONLY on `experienceCount > 0` — failures and
empty windows leave the cursor untouched so the next 30-min tick
retries the same window with new experiences
- clamps future/corrupted cursors to the default 1-day lookback
- per-domain try/catch isolates failures (one bad domain doesn't
block the others)
- graceful no-op when the learning-optimization API is unavailable
Why direct service call instead of `runLearningCycle`:
`runLearningCycle` also spawns a learning agent, runs strategy
optimization, and creates knowledge. Any of those can fail and block
mining. The direct service call is the minimum sufficient producer.
Why cursor at all:
`learnPattern` uses `uuidv4()` for pattern IDs and `Date.now()` for
names, so re-mining the same experiences would create duplicate
`learning:pattern:*` rows. The cursor is essential for idempotency
across worker ticks.
7 new tests cover: per-domain invocation, cursor advance on success,
no-advance on failure, throw isolation, cursor reuse as next window
start, future-cursor clamping, and graceful no-op when the API is
unavailable.
463/463 unit tests pass across workers + CLI suites; build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6655314120 |
fix(hooks): wire post-edit dream trigger (#480) + drop pre-task bridge pattern-count gate (#487)
post-edit was initializing `dreamTriggered = false` and only calling
`incrementDreamExperience` — `checkAndTriggerDream` was never invoked.
Since post-edit fires far more often than post-task during Claude Code
sessions, the hook-driven dream loop was starved: `experienceCount`
grew monotonically but `lastDreamTime` stayed null and
`dream_insights.applied` stayed at 0.
Mirrors task-hooks.ts: call `checkAndTriggerDream` after bumping the
counter and surface the full {triggered, reason, insightsGenerated,
insightsApplied} shape in JSON output so operators can distinguish
"not triggered because of state" from "not triggered because we
never tried."
The pre-task bridge was gated on `selectedPatternIds.length > 0`,
which silently dropped the `task-bridge:*` kv write for low-confidence
prompts. post-task's `if (outcome.bridge)` then skipped
`updateHookRouterQValue`, so `rl_q_values` never populated. The
Bellman update's state_key uses (taskType|priority|domain|
complexityBucket) and action_key uses `routing.recommendedAgent` —
neither requires non-empty patterns. The downstream per-pattern
fan-out at hooks-dream-learning.ts:428 already guards
`bridge.selectedPatternIds.length > 0` before iterating, so writing
the bridge with `[]` is structurally safe.
Tests:
- post-edit-dream-trigger.test.ts: 3 cases covering invocation,
full JSON shape on trigger, and dreamReason surfaced when
conditions are not met
- pre-task-bridge-empty-patterns.test.ts: parametrized over
patterns=[] and patterns=[p1,p2], asserts bridge row exists in
both; separate no-op test for missing --description
125/125 hook-handler unit tests pass; build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a664b679e0 |
chore(release): bump version to v3.9.30
Six user-reported MCP fixes (see CHANGELOG and docs/releases/v3.9.30.md): - #467 __dirname ESM polyfill across 7 source files - #469 real all-MiniLM-L6-v2 embeddings (was random-vector mock) - #470 NaN guard in coherence_collapse risk categorization - #472 default test phases when MCP wrapper omits the list - #473 wire SessionOperationCache into MCP tools/call dispatch - #474 validate language and auto-derive framework in test_generate Also fixes a pre-existing field-name typo in tests/integrations/agentic-flow/e2e-integration.test.ts (.similarity → .score) that was vacuously passing because the previous mock embedding generator rarely cleared the threshold to exercise the assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |