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>
135 lines
5.9 KiB
TypeScript
135 lines
5.9 KiB
TypeScript
/**
|
|
* The hook lane's delivery logging — the SHIPPED wiring behind serve's
|
|
* onTurnContextDelivered callback (logTurnContextDeliveryFireAndForget) plus
|
|
* the isVolunteerChannel runtime guard. Hermetic: stub engine captures the
|
|
* multi-row INSERT; the fire-and-forget sink is drained per test.
|
|
*/
|
|
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
import {
|
|
isVolunteerChannel,
|
|
logTurnContextDeliveryFireAndForget,
|
|
awaitPendingVolunteerEventWrites,
|
|
_resetPendingVolunteerEventWritesForTests,
|
|
} from '../src/core/context/volunteer-events.ts';
|
|
import { logDeliveredReflexPointers, type ReflexPointer } from '../src/core/context/retrieval-reflex.ts';
|
|
import type { VolunteeredPage } from '../src/core/context/volunteer.ts';
|
|
import type { BrainEngine } from '../src/core/engine.ts';
|
|
|
|
interface CapturedInsert { sql: string; params: unknown[] }
|
|
|
|
function stubEngine(captured: CapturedInsert[], fail = false): BrainEngine {
|
|
return {
|
|
executeRaw: async (sql: string, params: unknown[]) => {
|
|
if (fail) throw new Error('insert exploded');
|
|
captured.push({ sql, params });
|
|
return [];
|
|
},
|
|
} as unknown as BrainEngine;
|
|
}
|
|
|
|
const PAGE: VolunteeredPage = {
|
|
slug: 'companies/acme', source_id: 'default', display: 'Acme', confidence: 0.85,
|
|
arm: 'title', rationale: 'exact title match "Acme"', synopsis: 'a company',
|
|
};
|
|
const POINTER: ReflexPointer = {
|
|
display: 'Alice', slug: 'people/alice', source_id: 'default', synopsis: 'x', arm: 'alias', confidence: 0.9,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
_resetPendingVolunteerEventWritesForTests();
|
|
});
|
|
|
|
describe('isVolunteerChannel', () => {
|
|
test('accepts exactly the five known channels; rejects everything else', () => {
|
|
for (const ok of ['op', 'reflex', 'watch', 'claude-code', 'codex']) {
|
|
expect(isVolunteerChannel(ok)).toBe(true);
|
|
}
|
|
for (const bad of ['vim', '', null, undefined, 42, {}, 'CLAUDE-CODE', 'hook']) {
|
|
expect(isVolunteerChannel(bad)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('logTurnContextDeliveryFireAndForget — the shipped serve wiring', () => {
|
|
test('volunteered pages land under the request channel with the clamped sessionId', async () => {
|
|
const captured: CapturedInsert[] = [];
|
|
const engine = stubEngine(captured);
|
|
logTurnContextDeliveryFireAndForget(
|
|
engine,
|
|
{ volunteered: [PAGE], pointers: [] },
|
|
{ channel: 'codex', sessionId: 's'.repeat(400) },
|
|
);
|
|
await awaitPendingVolunteerEventWrites(2000);
|
|
expect(captured).toHaveLength(1);
|
|
const { params } = captured[0];
|
|
expect(params).toContain('codex');
|
|
expect(params).toContain('companies/acme');
|
|
// Trust-boundary clamp: 400-char sessionId stored at 256.
|
|
const session = (params as string[]).find((p) => typeof p === 'string' && p.startsWith('sss'));
|
|
expect(session!.length).toBe(256);
|
|
});
|
|
|
|
test('absent/unknown channel falls back to claude-code (the only registered harness today)', async () => {
|
|
const captured: CapturedInsert[] = [];
|
|
logTurnContextDeliveryFireAndForget(stubEngine(captured), { volunteered: [PAGE] }, {});
|
|
await awaitPendingVolunteerEventWrites(2000);
|
|
expect(captured[0].params).toContain('claude-code');
|
|
|
|
captured.length = 0;
|
|
_resetPendingVolunteerEventWritesForTests();
|
|
logTurnContextDeliveryFireAndForget(stubEngine(captured), { volunteered: [PAGE] }, { channel: 'not-a-channel' });
|
|
await awaitPendingVolunteerEventWrites(2000);
|
|
expect(captured[0].params).toContain('claude-code');
|
|
});
|
|
|
|
test('pointers log through the reflex-pointer path under the SAME channel', async () => {
|
|
const captured: CapturedInsert[] = [];
|
|
logTurnContextDeliveryFireAndForget(stubEngine(captured), { volunteered: [], pointers: [POINTER] }, { channel: 'claude-code' });
|
|
await awaitPendingVolunteerEventWrites(2000);
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured[0].params).toContain('claude-code');
|
|
expect(captured[0].params).toContain('people/alice');
|
|
});
|
|
|
|
test('empty delivery logs nothing; a failing insert never throws into the caller', async () => {
|
|
const captured: CapturedInsert[] = [];
|
|
logTurnContextDeliveryFireAndForget(stubEngine(captured), { volunteered: [], pointers: [] }, { channel: 'claude-code' });
|
|
await awaitPendingVolunteerEventWrites(2000);
|
|
expect(captured).toHaveLength(0);
|
|
|
|
// Fire-and-forget: the sink swallows the insert failure.
|
|
expect(() =>
|
|
logTurnContextDeliveryFireAndForget(stubEngine([], true), { volunteered: [PAGE] }, { channel: 'op' }),
|
|
).not.toThrow();
|
|
await awaitPendingVolunteerEventWrites(2000);
|
|
});
|
|
});
|
|
|
|
describe('logDeliveredReflexPointers — ambient reflex channel', () => {
|
|
test('logs under the reflex channel with the shared rationale template', async () => {
|
|
const captured: CapturedInsert[] = [];
|
|
logDeliveredReflexPointers(stubEngine(captured), [POINTER]);
|
|
await awaitPendingVolunteerEventWrites(2000);
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured[0].params).toContain('reflex');
|
|
expect(captured[0].params).toContain('alias match "Alice"'); // reflexPointerRationale parity
|
|
});
|
|
});
|
|
|
|
describe('channel guards', () => {
|
|
test('isHarnessChannel accepts only harness channels — internal channels are refused from the wire', async () => {
|
|
const { isHarnessChannel } = await import('../src/core/context/volunteer-events.ts');
|
|
expect(isHarnessChannel('claude-code')).toBe(true);
|
|
expect(isHarnessChannel('codex')).toBe(true);
|
|
for (const internal of ['op', 'reflex', 'watch']) expect(isHarnessChannel(internal)).toBe(false);
|
|
});
|
|
|
|
test('wire delivery claiming an INTERNAL channel falls back to the harness default (stat-pollution guard)', async () => {
|
|
const captured: CapturedInsert[] = [];
|
|
logTurnContextDeliveryFireAndForget(stubEngine(captured), { volunteered: [PAGE] }, { channel: 'reflex' });
|
|
await awaitPendingVolunteerEventWrites(2000);
|
|
expect(captured[0].params).toContain('claude-code');
|
|
expect(captured[0].params).not.toContain('reflex');
|
|
});
|
|
});
|