mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
* refactor(context): extract pure gateVolunteeredPointers (no behavior change) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(context): volunteer-shaped resolve IPC — channel/cwd/volunteer fields, handler-result widening, server-side gate + validated source + delivery-point logging; listener on both engines Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(cli): gbrain volunteer-hook — one-shot harness hook entry (IPC-first; Postgres-only fallback; startup-hook-exempt) T0 evidence: a UserPromptSubmit hook's additionalContext is recorded in the session transcript as a structured hook_additional_context attachment and transcript_path is stable across turns — cross-turn dedupe reads ONLY those attachments (never raw tail substrings). Live smoke: seeded brain + running serve → pointer injected via IPC at 0.43s wall, turn 2 suppressed, one event banked channel=claude-code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(recipes): claude-code-reflex + codex-reflex (experimental) hook adapters Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(doctor): volunteer_channels per-adapter visibility Engine-aware sibling of retrieval_reflex_health: groups context_volunteer_events by channel (7d) so operators can see which push adapters fire; message separates installed-but-unregistered from registered-but-quiet; pre-v117 tolerant; info-only status. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(context): hook-lane feedback loop + cross-turn dedupe — converge cathedral-3 onto the #3975 harness hooks The #3975 hook lane volunteered pages invisibly (nothing logged to context_volunteer_events) and re-volunteered the same page every turn an entity was mentioned (no priorContextText). This convergence ports the cathedral-3 branch's two gap-fills into the blessed lane and retires its parallel surface: - transcripts: parseTranscript extracts injectedContextBlocks (structured hook_additional_context attachments — the T0-verified recorded shape of our own injections; never raw-turn substring matching) - hook user-prompt: feeds those blocks as priorContextText (volunteer once per session, not once per mention) + sends a feedback-loop channel (--harness <claude-code|codex>, default claude-code); hook joins STARTUP_HOOK_SKIP_COMMANDS (per-prompt check-update spawn class, grep-pinned) - turn-context: result exposes post-trim volunteered survivors - resolve-ipc: additive TurnContextRequest.channel + onTurnContextDelivered seam (fires post-write only — abandoned blocks are never counted) - serve: delivery callback logs volunteered pages + pointers per channel - doctor: volunteer_channels check (per-channel 7d visibility; separates installed-but-unregistered from registered-but-quiet; pre-v117 tolerant) Retired (never released): gbrain volunteer-hook, hook-adapters.ts, claude-code-reflex + codex-reflex recipes, the volunteer-shaped resolve wire extension. Kept: gateVolunteeredPointers extraction, VolunteerChannel claude-code/codex values, the real hook-transcript fixture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: regenerate llms bundles for the hook-lane doc updates Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: coverage for the hook-lane delivery wiring Extract logTurnContextDeliveryFireAndForget into volunteer-events.ts so the shipped serve callback is unit-testable (channel fallback, sessionId clamp, pointer+page rows in ONE synchronous registration — the double dynamic import could register the pending write too late to survive process exit). Direct units for isVolunteerChannel, explicit-channel logDeliveredReflexPointers, and the pure gate (idempotency pin; stale parity claim in its doc corrected). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes — 7-reviewer batch Multi-confirmed: volunteer_channels now runs on the LOCAL doctor path too (buildChecks; docs pointed there); transient DB errors no longer misreported as pre-v117; priorContextText deduped + 32KB-capped hook-side. Red team: delivery counts reconciled against the hook heartbeat (CAUTION when mostly degraded — write-accept isn't injection); IPC clamp drops advisory priorContextText BEFORE any window turn; foreign hook_additional_context blocks (no gbrain marker) excluded from dedupe input; engine-aware quiet guidance (Postgres brains aren't told to chase hook registration); 2MB dedupe horizon documented. Security: wire channel claims restricted to harness channels. DRY: VOLUNTEER_CHANNELS single source, shared reflexPointerRationale, SESSION_ID_MAX_LEN shared with the op clamp; dead channel param dropped from logDeliveredReflexPointers. Tests: budget-trim volunteered invariant under real trim pressure, poll/ordering-based delivery-seam tests (no fixed sleeps), doctor branch + engine-parity-row tests, wiring source pins, cap pins; fixture pruned to load-bearing lines (environment inventory removed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial-review fixes — cross-model P1 + Claude adversarial batch P1 (codex structured review + Claude adversarial, cross-model): the remote run_doctor path now threads the caller's source scope into volunteer_channels — a source-bound token can no longer see other sources' activity counts/timestamps (must-never-violate source-isolation rule); local trusted doctor stays brain-wide. Claude adversarial: IPC connection handler gains a one-request re-entrancy guard (trailing bytes mid-await double-processed the same line → duplicate handler work + duplicate delivery logging); partial client-side trims now heartbeat as reason 'trimmed' (delivery-count drift visibility); heartbeat reconciliation uses the same 7-day window as the counts + a 5-sample floor; one oversized dedupe block no longer evicts all smaller ones (continue, not break); quiet-channel guidance no longer claims old serves attribute hook traffic to reflex (they log nothing); 'transient' claim softened to evidence; marker-filter + unknown-channel-default comments state their honest limits. Pins: scoped-query SQL/params, re-entrancy single-fire, updated wiring grep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v0.45.1.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: restamp bootstrap runbook + regenerate vendored template repo for v0.45.1.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v0.45.1.0 Post-ship /document-release pass: fold the post-doc review fixes into the reference docs (KEY_FILES: source-scoped remote volunteer_channels + heartbeat reconciliation, 32KB-capped dedupe input, marker filter, IPC one-request guard + clamp order), fix BRAINBENCH's now-stale memoryless-hook rationale, and mark the doctor-check half of the push-context TODO shipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: cross-model doc-review fixes for v0.45.1.0 Codex doc review against the shipped diff: push-context channel table gains the harness-hook row (three-channels claim was stale), the guide states the PGLite-socket topology limit and the bounded dedupe horizon, delivery-point claims are hedged to match the code (write-accept isn't proof of injection; the doctor reconciles against the hook heartbeat, which can't inspect registration), BRAINBENCH's future-tense hooks-PR sentence updated now the hook has landed, and the adapter-flip + resolver-channel TODOS reflect the current lane count. llms-full.txt regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(hook): document --harness in the user-prompt usage text Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
381 lines
16 KiB
TypeScript
381 lines
16 KiB
TypeScript
/**
|
|
* Retrieval Reflex — resolver core (issue #1981, Layer 1).
|
|
*
|
|
* Given salient candidate surface-forms (from entity-salience.ts) and a real
|
|
* BrainEngine, resolve the ones that map to an EXISTING brain page and return a
|
|
* compact POINTER block (name → slug → safe one-line synopsis) to inject into
|
|
* the system prompt. Detect + point, NEVER auto-dump the body — the agent makes
|
|
* the deliberate get_page call when the entity is actually the subject.
|
|
*
|
|
* This is the NARROW operation behind the resolve capability: it takes
|
|
* candidates and returns pointers. The IPC (serve) and host (ctx.brainQuery)
|
|
* paths run THIS SAME function server-side so no raw SQL ever crosses a trust
|
|
* boundary — the wire only carries candidates in and pointers out.
|
|
*
|
|
* Deterministic, zero-LLM. Precision-biased resolution (no trgm-fuzzy):
|
|
* 1. alias-first — page_aliases exact (unambiguous single-slug only)
|
|
* 2. title + slug-suffix — lower(title) exact OR slug suffix match
|
|
* (real slugs are namespaced people/alice-example; bare slugify misses).
|
|
*
|
|
* Privacy (eng-review D5): the synopsis is taken from a SAFE source —
|
|
* frontmatter `summary` if present, else the page body with takes/private-fact
|
|
* fences STRIPPED (same boundary get_page applies to untrusted readers). Raw
|
|
* compiled_truth is never injected.
|
|
*/
|
|
|
|
import type { BrainEngine } from '../engine.ts';
|
|
import { normalizeAlias } from '../search/alias-normalize.ts';
|
|
import { slugify } from '../entities/resolve.ts';
|
|
import { stripTakesFence } from '../takes-fence.ts';
|
|
import { stripFactsFence } from '../facts-fence.ts';
|
|
import type { EntityCandidate } from './entity-salience.ts';
|
|
|
|
/** Default cap on pointers injected per turn (config: retrieval_reflex_max_pointers). */
|
|
export const DEFAULT_MAX_POINTERS = 3;
|
|
const SYNOPSIS_MAX = 160;
|
|
|
|
/** Which resolution arm produced a pointer (provenance → honest confidence). */
|
|
export type ResolveArm = 'alias' | 'title' | 'slug-suffix';
|
|
|
|
/**
|
|
* v0.43 (#2095) — arm → confidence. Lives HERE, next to the arm definitions,
|
|
* so arm identity and its score can't drift apart (eng-review note). The
|
|
* volunteer layer imports these; small deterministic boosts (multi-turn /
|
|
* newest-turn mention) are added on top there.
|
|
*/
|
|
export const ARM_CONFIDENCE: Record<ResolveArm, number> = {
|
|
alias: 0.9,
|
|
title: 0.8,
|
|
'slug-suffix': 0.6,
|
|
};
|
|
|
|
export interface ReflexPointer {
|
|
display: string;
|
|
slug: string;
|
|
/** Which brain source the page lives in (federated callers need it for dedup). */
|
|
source_id: string;
|
|
synopsis: string;
|
|
/** Resolution provenance (v0.43 #2095). */
|
|
arm: ResolveArm;
|
|
/** Base arm confidence (ARM_CONFIDENCE[arm]); callers may boost. */
|
|
confidence: number;
|
|
/**
|
|
* normalizeAlias form of the CANDIDATE that resolved this pointer (v0.43
|
|
* #2095) — lets the volunteer layer join pointers back to window-salience
|
|
* metadata without guessing from the display label (which falls back to the
|
|
* page title when the candidate surface differs, e.g. alias "Swami" →
|
|
* title "Swami X"). Absent only when suffix classification couldn't
|
|
* recover the source candidate.
|
|
*/
|
|
matchedNorm?: string;
|
|
}
|
|
|
|
export interface PointerBlock {
|
|
pointers: ReflexPointer[];
|
|
/** Pre-rendered markdown for systemPromptAddition. */
|
|
text: string;
|
|
}
|
|
|
|
export interface ResolvePointersOpts {
|
|
maxPointers?: number;
|
|
/**
|
|
* Joined text of PRIOR turns + already-loaded page bodies (NOT the current
|
|
* user message). Pointers whose slug/title already appear here are suppressed
|
|
* — the agent has seen them. MUST exclude the current turn, or the triggering
|
|
* message's own mention would suppress every pointer (eng-review/Codex fix).
|
|
*/
|
|
priorContextText?: string;
|
|
/**
|
|
* v0.43 (#2095, codex D7) — suppression mode.
|
|
* 'slug-and-title' (default, window=1 legacy): suppress when the slug OR
|
|
* the page title appears whole-word in prior context.
|
|
* 'slug-only' (REQUIRED under multi-turn windowing): suppress on slug
|
|
* presence only. Slugs only enter context when a pointer block or page
|
|
* body was actually injected; a bare mention of "Alice Example" in a
|
|
* prior turn never contains `people/alice-example`. The title rule
|
|
* would suppress every entity merely MENTIONED in a prior window turn
|
|
* — breaking window extraction by construction.
|
|
*/
|
|
suppression?: 'slug-and-title' | 'slug-only';
|
|
/**
|
|
* v0.43 (#2095) — federated scope: resolve across these sources instead of
|
|
* the single positional sourceId. Precedence mirrors sourceScopeOpts
|
|
* (federated array > scalar). Alias arm loops per source; the title/slug
|
|
* arm uses source_id = ANY(...) in one query.
|
|
*/
|
|
sourceIds?: string[];
|
|
}
|
|
|
|
export interface PageRow {
|
|
slug: string;
|
|
source_id: string;
|
|
title: string;
|
|
type: string | null;
|
|
frontmatter: Record<string, unknown> | null;
|
|
compiled_truth: string | null;
|
|
}
|
|
|
|
/**
|
|
* Resolve candidates to a pointer block. Returns null when nothing resolves
|
|
* (so the caller injects nothing). Never throws for data reasons — each arm is
|
|
* independently guarded so a pre-v110 brain (no page_aliases) still gets the
|
|
* title/slug arm.
|
|
*/
|
|
export async function resolveEntitiesToPointers(
|
|
engine: BrainEngine,
|
|
sourceId: string,
|
|
candidates: EntityCandidate[],
|
|
opts: ResolvePointersOpts = {},
|
|
): Promise<PointerBlock | null> {
|
|
if (!candidates.length) return null;
|
|
const maxPointers = opts.maxPointers ?? DEFAULT_MAX_POINTERS;
|
|
const priorLc = (opts.priorContextText ?? '').toLowerCase();
|
|
|
|
// display lookup keyed by normalized query, so resolved slugs can recover a
|
|
// human surface form for the pointer label.
|
|
const displayByNorm = new Map<string, string>();
|
|
const aliasNorms: string[] = [];
|
|
const titlesLc: string[] = [];
|
|
const exactSlugs: string[] = [];
|
|
const slugSuffixes: string[] = [];
|
|
// Reverse maps for arm-2 provenance (which candidate produced a row) —
|
|
// populated in this same pass so the derivations happen exactly once.
|
|
const titleToNorm = new Map<string, string>();
|
|
const slugToNorm = new Map<string, string>();
|
|
for (const c of candidates) {
|
|
const norm = normalizeAlias(c.query);
|
|
if (!norm) continue;
|
|
if (!displayByNorm.has(norm)) displayByNorm.set(norm, c.display);
|
|
aliasNorms.push(norm);
|
|
const tl = c.query.toLowerCase();
|
|
titlesLc.push(tl);
|
|
if (!titleToNorm.has(tl)) titleToNorm.set(tl, norm);
|
|
const s = slugify(c.query);
|
|
if (s) {
|
|
exactSlugs.push(s);
|
|
slugSuffixes.push(`%/${s}`);
|
|
if (!slugToNorm.has(s)) slugToNorm.set(s, norm);
|
|
}
|
|
}
|
|
if (!aliasNorms.length) return null;
|
|
|
|
// Federated scope (v0.43 #2095): explicit sourceIds win over the scalar.
|
|
const sourceIds = opts.sourceIds?.length ? opts.sourceIds : [sourceId];
|
|
|
|
// Ordered set of resolved (source, slug) pairs with arm provenance —
|
|
// alias hits pushed first → higher confidence.
|
|
const resolved: Array<{ slug: string; source_id: string; arm: ResolveArm; matchedNorm?: string }> = [];
|
|
const seen = new Set<string>();
|
|
// Neither source ids nor slugs contain spaces, so a space separator is safe.
|
|
const keyOf = (src: string, slug: string) => `${src} ${slug}`;
|
|
const push = (slug: string, src: string, arm: ResolveArm, matchedNorm?: string) => {
|
|
if (!slug) return;
|
|
const k = keyOf(src, slug);
|
|
if (!seen.has(k)) {
|
|
seen.add(k);
|
|
resolved.push({ slug, source_id: src, arm, matchedNorm });
|
|
}
|
|
};
|
|
// Arm 1 — alias-first. Unambiguous single-slug hits only, per source (no
|
|
// engine-interface change for federation). Guarded: pre-v110 brains throw
|
|
// "relation page_aliases does not exist" — swallow and continue.
|
|
// Per-source lookups are independent — run them concurrently so a
|
|
// federated caller (M granted sources) pays one RTT, not M sequential
|
|
// ones (~71ms each cross-region; the reflex runs under a 1.5s budget).
|
|
// Results are folded back in sourceIds order so pointer ordering stays
|
|
// deterministic. Per-source failures degrade independently (pre-v110
|
|
// brains have no page_aliases table).
|
|
const aliasResults = await Promise.allSettled(
|
|
sourceIds.map((src) => engine.resolveAliases(aliasNorms, { sourceId: src })),
|
|
);
|
|
for (let i = 0; i < sourceIds.length; i++) {
|
|
const r = aliasResults[i];
|
|
if (r.status !== 'fulfilled') continue;
|
|
for (const norm of aliasNorms) {
|
|
const hits = r.value.get(norm);
|
|
if (hits && hits.length === 1) push(hits[0].slug, sourceIds[i], 'alias', norm);
|
|
}
|
|
}
|
|
|
|
// Arm 2 — exact title OR slug-suffix. This is the recall fix: a bare "Alice
|
|
// Example" slugifies to alice-example, but the real page is people/alice-example,
|
|
// so a plain slug = ANY() misses. Match lower(title) exactly or the slug suffix.
|
|
let rows: PageRow[] = [];
|
|
try {
|
|
rows = await engine.executeRaw<PageRow>(
|
|
`SELECT slug, source_id, title, type, frontmatter, compiled_truth
|
|
FROM pages
|
|
WHERE deleted_at IS NULL
|
|
AND source_id = ANY($1::text[])
|
|
AND ( lower(title) = ANY($2::text[])
|
|
OR slug = ANY($3::text[])
|
|
OR slug LIKE ANY($4::text[]) )`,
|
|
[sourceIds, titlesLc, exactSlugs, slugSuffixes],
|
|
);
|
|
} catch {
|
|
rows = [];
|
|
}
|
|
// Hydrate alias-resolved pages too (their bodies for the synopsis) if not in rows.
|
|
const rowByKey = new Map<string, PageRow>();
|
|
for (const r of rows) rowByKey.set(keyOf(r.source_id, r.slug), r);
|
|
const aliasOnly = resolved.filter((p) => !rowByKey.has(keyOf(p.source_id, p.slug)));
|
|
if (aliasOnly.length) {
|
|
try {
|
|
const extra = await engine.executeRaw<PageRow>(
|
|
`SELECT slug, source_id, title, type, frontmatter, compiled_truth
|
|
FROM pages
|
|
WHERE deleted_at IS NULL AND source_id = ANY($1::text[]) AND slug = ANY($2::text[])`,
|
|
[sourceIds, aliasOnly.map((p) => p.slug)],
|
|
);
|
|
for (const r of extra) rowByKey.set(keyOf(r.source_id, r.slug), r);
|
|
} catch {
|
|
/* ignore — alias slug may be stale */
|
|
}
|
|
}
|
|
// Title/slug matches that weren't alias hits, appended after alias hits.
|
|
// Arm provenance per row is classified in JS (codex D8) — the combined OR
|
|
// can't report which predicate matched: an exact lower(title) hit is the
|
|
// 'title' arm; anything else got in via slug / slug-suffix.
|
|
const titleSet = new Set(titlesLc);
|
|
for (const r of rows) {
|
|
const titleLc = (r.title ?? '').toLowerCase();
|
|
if (titleSet.has(titleLc)) {
|
|
push(r.slug, r.source_id, 'title', titleToNorm.get(titleLc));
|
|
} else {
|
|
// Slug arm: exact slugified-candidate match, else suffix scan.
|
|
const tail = r.slug.includes('/') ? r.slug.slice(r.slug.lastIndexOf('/') + 1) : r.slug;
|
|
push(r.slug, r.source_id, 'slug-suffix', slugToNorm.get(r.slug) ?? slugToNorm.get(tail));
|
|
}
|
|
}
|
|
|
|
// Build pointers in confidence order, applying suppression + cap.
|
|
const suppression = opts.suppression ?? 'slug-and-title';
|
|
const pointers: ReflexPointer[] = [];
|
|
for (const { slug, source_id, arm, matchedNorm } of resolved) {
|
|
const row = rowByKey.get(keyOf(source_id, slug));
|
|
if (!row) continue;
|
|
// Suppression: already present in PRIOR context. The current turn is
|
|
// deliberately excluded from priorContextText. Under windowing
|
|
// ('slug-only', codex D7) only the slug counts — a slug appears in prior
|
|
// context only when a pointer/page was actually surfaced there, while a
|
|
// title appears on any bare mention.
|
|
if (priorLc) {
|
|
if (priorLc.includes(slug.toLowerCase())) continue;
|
|
if (suppression === 'slug-and-title') {
|
|
const titleLc = (row.title ?? '').toLowerCase();
|
|
if (titleLc && wholeWordIncludes(priorLc, titleLc)) continue;
|
|
}
|
|
}
|
|
const display = displayForRow(row, displayByNorm);
|
|
const synopsis = safeSynopsis(row);
|
|
pointers.push({ display, slug, source_id, synopsis, arm, confidence: ARM_CONFIDENCE[arm], matchedNorm });
|
|
if (pointers.length >= maxPointers) break;
|
|
}
|
|
|
|
if (!pointers.length) return null;
|
|
return { pointers, text: renderPointerBlock(pointers) };
|
|
}
|
|
|
|
/** Recover a display label: prefer the matched candidate surface, else the page title. */
|
|
function displayForRow(row: PageRow, displayByNorm: Map<string, string>): string {
|
|
const byTitle = displayByNorm.get(normalizeAlias(row.title ?? ''));
|
|
if (byTitle) return byTitle;
|
|
// try the slug tail (people/alice-example → alice-example)
|
|
const tail = row.slug.includes('/') ? row.slug.slice(row.slug.lastIndexOf('/') + 1) : row.slug;
|
|
return row.title || tail;
|
|
}
|
|
|
|
/**
|
|
* Privacy-safe synopsis (eng-review D5). Prefer a curated frontmatter `summary`;
|
|
* otherwise strip takes/private-fact fences from the body (the same boundary
|
|
* get_page applies to untrusted readers) and take the first sentence. Never
|
|
* returns raw compiled_truth.
|
|
*
|
|
* Exported for the MEMORY_VERBS v1 entity card (verbs/entity-card.ts) — the
|
|
* card's `summary` field runs through THIS boundary, not a parallel one.
|
|
*/
|
|
export function safeSynopsis(row: PageRow): string {
|
|
const fmSummary = row.frontmatter?.summary;
|
|
if (typeof fmSummary === 'string' && fmSummary.trim()) {
|
|
return clip(collapse(fmSummary), SYNOPSIS_MAX);
|
|
}
|
|
const body = row.compiled_truth ?? '';
|
|
if (!body) return '';
|
|
const stripped = stripFactsFence(stripTakesFence(body), { keepVisibility: ['world'] });
|
|
// Drop frontmatter block, markdown headings, and blank lines; first real prose line.
|
|
const firstProse = stripped
|
|
.replace(/^---[\s\S]*?---\s*/m, '')
|
|
.split('\n')
|
|
.map((l) => l.trim())
|
|
.find((l) => l && !l.startsWith('#') && !l.startsWith('<!--'));
|
|
if (!firstProse) return '';
|
|
// first sentence-ish
|
|
const sentence = firstProse.split(/(?<=[.!?])\s/)[0];
|
|
return clip(collapse(sentence), SYNOPSIS_MAX);
|
|
}
|
|
|
|
function collapse(s: string): string {
|
|
return s.replace(/\s+/g, ' ').trim();
|
|
}
|
|
function clip(s: string, n: number): string {
|
|
return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s;
|
|
}
|
|
|
|
/** Whole-word containment so "ab" doesn't match inside "fabric". */
|
|
function wholeWordIncludes(haystack: string, needle: string): boolean {
|
|
if (!needle) return false;
|
|
const esc = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
return new RegExp(`(^|\\W)${esc}(\\W|$)`).test(haystack);
|
|
}
|
|
|
|
export function renderPointerBlock(pointers: ReflexPointer[]): string {
|
|
const lines = [
|
|
'## Brain pages mentioned this turn',
|
|
'You referenced entities with existing brain pages. Open the page before relying on',
|
|
'details — do not answer from memory.',
|
|
'',
|
|
];
|
|
for (const p of pointers) {
|
|
const syn = p.synopsis ? ` — ${p.synopsis}` : '';
|
|
lines.push(`- **${p.display}** → \`${p.slug}\`${syn} (use get_page before relying on details)`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* v0.43 (#2095, codex D11 + red-team) — ambient-channel feedback logging,
|
|
* ACCEPT-SIDE ONLY. Called by the delivery points (the serve IPC server after
|
|
* a successful write; the direct-Postgres reflex rung after its per-turn
|
|
* timeout admitted the block) — never inside the resolver itself, because a
|
|
* pointer block that timed out client-side was NEVER injected into a prompt,
|
|
* and logging it would inflate "volunteered" counts and drag the measured
|
|
* precision toward zero (corrupting the exact stats users tune
|
|
* min_confidence with).
|
|
*/
|
|
/**
|
|
* Canonical rationale template for a delivered reflex pointer — shared by the
|
|
* ambient-channel logger below AND the hook lane's delivery logger
|
|
* (volunteer-events.ts:logTurnContextDeliveryFireAndForget) so the two
|
|
* channels' rationale strings can never drift.
|
|
*/
|
|
export function reflexPointerRationale(p: ReflexPointer): string {
|
|
return `${p.arm} match "${p.display}"`;
|
|
}
|
|
|
|
export function logDeliveredReflexPointers(engine: BrainEngine, pointers: ReflexPointer[]): void {
|
|
if (!pointers.length) return;
|
|
void import('./volunteer-events.ts')
|
|
.then(({ logVolunteerEventsFireAndForget, volunteerEventRowsFrom }) => {
|
|
logVolunteerEventsFireAndForget(
|
|
engine,
|
|
volunteerEventRowsFrom(
|
|
pointers.map((p) => ({ ...p, rationale: reflexPointerRationale(p) })),
|
|
{ channel: 'reflex' },
|
|
),
|
|
);
|
|
})
|
|
.catch(() => {
|
|
/* telemetry only */
|
|
});
|
|
}
|