* fix(integrations): parameterize resolver-row fence by recipe id The install fence was hardcoded gbrain:agent-voice:resolver-rows, so any second copy-into-host-repo recipe wrote a mislabeled block (and refresh/ uninstall keyed on recipe id would miss it). Derive it from manifest.recipe. * feat(context): Retrieval Reflex — teach the agent when/what to retrieve (#1981) Deterministic per-turn pointer layer in the context engine: a zero-LLM, precision-biased scan resolves salient entities (names, @handles) to existing brain pages and injects compact pointers (name → slug → safe synopsis). Detect + point, never auto-dump. Fail-open, capped, suppression on prior context only. Engine-aware resolver ladder (no second DB connection): host ctx.brainQuery → PGLite serve resolve IPC (unix socket) → Postgres cached direct → disabled. Synopsis runs through get_page's privacy strip. Plus the retrieval-reflex recipe + policy skill, the retrieval_reflex_health doctor check, config gate, and the init next-step hint. * chore: bump version and changelog (v0.42.38.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(architecture): KEY_FILES entries for Retrieval Reflex surface (#1981) Document the new src/core/context/ modules, the context-engine resolver ladder, the serve resolve IPC, the retrieval_reflex_health doctor check, and the recipe-id-keyed install fence. Current-state only. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.7 KiB
TypeScript
74 lines
2.7 KiB
TypeScript
/**
|
|
* Retrieval Reflex resolve IPC round-trip tests (#1981, T3/T5).
|
|
*/
|
|
import { describe, test, expect, afterEach } from 'bun:test';
|
|
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import {
|
|
resolveSocketPath,
|
|
startResolveIpcServer,
|
|
resolveViaIpc,
|
|
IPC_UNAVAILABLE,
|
|
} from '../../src/core/context/resolve-ipc.ts';
|
|
import type { PointerBlock } from '../../src/core/context/retrieval-reflex.ts';
|
|
|
|
const servers: Array<{ close: () => void }> = [];
|
|
afterEach(() => {
|
|
for (const s of servers.splice(0)) { try { s.close(); } catch { /* noop */ } }
|
|
});
|
|
|
|
function tmpDir(): string {
|
|
return mkdtempSync(join(tmpdir(), 'rr-ipc-'));
|
|
}
|
|
|
|
describe('resolve IPC', () => {
|
|
test('round-trip: client gets the pointer block the server returns', async () => {
|
|
const dir = tmpDir();
|
|
const sock = resolveSocketPath(dir);
|
|
const block: PointerBlock = { pointers: [{ display: 'Alice', slug: 'people/alice', synopsis: 'x' }], text: 'BLOCK' };
|
|
const server = await startResolveIpcServer(sock, async (req) => {
|
|
expect(req.candidates[0].query).toBe('Alice');
|
|
return block;
|
|
});
|
|
expect(server).not.toBeNull();
|
|
servers.push(server!);
|
|
|
|
const got = await resolveViaIpc(sock, { candidates: [{ display: 'Alice', query: 'Alice' }] });
|
|
expect(got).not.toBe(IPC_UNAVAILABLE);
|
|
expect((got as PointerBlock).text).toBe('BLOCK');
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('absent socket → IPC_UNAVAILABLE (caller falls through ladder)', async () => {
|
|
const dir = tmpDir();
|
|
const got = await resolveViaIpc(resolveSocketPath(dir), { candidates: [{ display: 'A', query: 'A' }] });
|
|
expect(got).toBe(IPC_UNAVAILABLE);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('server returning null relays as null (resolved, nothing found)', async () => {
|
|
const dir = tmpDir();
|
|
const sock = resolveSocketPath(dir);
|
|
const server = await startResolveIpcServer(sock, async () => null);
|
|
servers.push(server!);
|
|
const got = await resolveViaIpc(sock, { candidates: [{ display: 'A', query: 'A' }] });
|
|
expect(got).toBeNull();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('stale socket file is cleaned up so a fresh server can bind', async () => {
|
|
const dir = tmpDir();
|
|
const sock = resolveSocketPath(dir);
|
|
const s1 = await startResolveIpcServer(sock, async () => null);
|
|
servers.push(s1!);
|
|
s1!.close();
|
|
// bind again at the same path — startResolveIpcServer must unlink the stale file
|
|
const s2 = await startResolveIpcServer(sock, async () => null);
|
|
expect(s2).not.toBeNull();
|
|
servers.push(s2!);
|
|
expect(existsSync(sock)).toBe(true);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
});
|