mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
4c0bdfafb9
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>
74 lines
3.1 KiB
TypeScript
74 lines
3.1 KiB
TypeScript
/**
|
|
* Issue #488 B.1 — daemon pidfile points at the npx wrapper, not the
|
|
* long-lived MCP server.
|
|
*
|
|
* Root cause: `start-daemon.cjs` falls back to `npx --yes agentic-qe mcp`
|
|
* when neither the local `.bin/aqe-mcp` nor the local
|
|
* `node_modules/agentic-qe/dist/mcp/bundle.js` exists. `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` and the
|
|
* idempotency check at the top of the script both misbehave.
|
|
*
|
|
* The fix: add `require.resolve('agentic-qe/dist/mcp/bundle.js')` as a 3rd
|
|
* candidate before the npx fallback. Global installs (`npm install -g`)
|
|
* are now found directly; `child.pid` is the real long-lived process.
|
|
*
|
|
* This test asserts the candidate is present in the generated daemon
|
|
* script template inside `10-workers.ts` and that the npx fallback now
|
|
* emits a warning so operators can spot the degraded case.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const PHASE_FILE = path.resolve(
|
|
__dirname,
|
|
'../../../../src/init/phases/10-workers.ts',
|
|
);
|
|
|
|
describe('start-daemon.cjs generator (#488 B.1)', () => {
|
|
const source = readFileSync(PHASE_FILE, 'utf-8');
|
|
|
|
it('the generated script probes for a global install via require.resolve', () => {
|
|
// Catches the case where someone removes the require.resolve probe —
|
|
// which would silently re-introduce the npx-wrapper-PID bug for users
|
|
// who installed agentic-qe globally.
|
|
expect(source).toContain(
|
|
"require.resolve('agentic-qe/dist/mcp/bundle.js')",
|
|
);
|
|
});
|
|
|
|
it('the require.resolve probe is guarded by try/catch (so missing global install does not crash)', () => {
|
|
// Use a regex anchored on the candidates.push line so we know the
|
|
// try/catch surrounds the actual probe, not some unrelated code.
|
|
const probeBlock =
|
|
/try\s*\{\s*candidates\.push\(require\.resolve\('agentic-qe\/dist\/mcp\/bundle\.js'\)\);\s*\}\s*catch\s*\{[\s\S]*?\}/;
|
|
expect(source).toMatch(probeBlock);
|
|
});
|
|
|
|
it('the npx fallback now warns operators about the pidfile limitation', () => {
|
|
// The npx path remains as a last resort, but operators must be able
|
|
// to spot it in daemon.log so they can fix their install.
|
|
expect(source).toMatch(/WARNING.*npx fallback/);
|
|
expect(source).toMatch(/daemon\.pid will point at the npx wrapper/);
|
|
});
|
|
|
|
it('candidate ordering: bundle.js paths come before npx fallback', () => {
|
|
// The require.resolve probe must run BEFORE the existsSync(c) call
|
|
// that picks the first candidate, otherwise the npx fallback wins
|
|
// even when a global install is reachable.
|
|
const requireResolveIdx = source.indexOf(
|
|
"candidates.push(require.resolve('agentic-qe/dist/mcp/bundle.js'))",
|
|
);
|
|
const binCandidateIdx = source.indexOf(
|
|
'const binCandidate = candidates.find',
|
|
);
|
|
const npxFallbackIdx = source.indexOf("'--yes', 'agentic-qe', 'mcp'");
|
|
|
|
expect(requireResolveIdx).toBeGreaterThan(0);
|
|
expect(binCandidateIdx).toBeGreaterThan(requireResolveIdx);
|
|
expect(npxFallbackIdx).toBeGreaterThan(binCandidateIdx);
|
|
});
|
|
});
|