mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-15 09:22:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fe392087c | ||
|
|
c4dff546e9 | ||
|
|
ca041a75be | ||
|
|
386d35b475 | ||
|
|
e49a31d2a6 | ||
|
|
3a9e12ada9 | ||
|
|
b0c784978f | ||
|
|
7001252000 | ||
|
|
0cf9a0f215 |
@@ -1,16 +0,0 @@
|
||||
# Line-ending policy.
|
||||
#
|
||||
# Shell scripts MUST be checked out with LF endings on every platform.
|
||||
# Git for Windows installs with `core.autocrlf=true` by default, which
|
||||
# rewrites LF -> CRLF on checkout. A strict bash (WSL, Linux CI, macOS)
|
||||
# then chokes on the trailing CR:
|
||||
#
|
||||
# scripts/run-unit-parallel.sh: line 23: $'\r': command not found
|
||||
# scripts/run-unit-parallel.sh: line 24: set: pipefail : invalid option name
|
||||
# scripts/run-unit-parallel.sh: line 32: syntax error near unexpected token `$'{\r''
|
||||
#
|
||||
# That silently disabled `bun run test`, `bun run verify`, `bun run ci:local`
|
||||
# and `bun run test:e2e` for Windows contributors, since all four dispatch
|
||||
# through bash. `eol=lf` pins the checkout regardless of the user's
|
||||
# core.autocrlf setting.
|
||||
*.sh text eol=lf
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Actionlint
|
||||
|
||||
# Lints the GitHub Actions workflow YAML on every change so a malformed
|
||||
# workflow / bad action ref / missing-permission bug is caught before it ships
|
||||
# a broken pipeline. gbrain edits .github/workflows/* often (sharding, cache,
|
||||
# timeouts); this is the cheap guard that keeps those edits honest.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: rhysd/actionlint@393031adb9afb225ee52ae2ccd7a5af5525e03e8 # v1.7.11
|
||||
@@ -12,61 +12,10 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Cancel a superseded run when a newer commit lands on the same PR/branch.
|
||||
# PR number for pull_request events (fork-safe), github.ref fallback for
|
||||
# push/scheduled runs.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
jsonb-parity:
|
||||
# Dedicated required guard for the JSONB double-encode bug-class (#2339).
|
||||
# PGLite parses a double-encoded jsonb string silently, so this assertion can
|
||||
# ONLY be made on real Postgres — a normal gated e2e file would skip without
|
||||
# DATABASE_URL and let the bug ship green (as #2339 did). This job provisions
|
||||
# Postgres and HARD-FAILS if DATABASE_URL is missing, so the guard can never
|
||||
# silently skip.
|
||||
name: JSONB parity (#2339 regression guard)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- run: bun install
|
||||
- name: Require DATABASE_URL (no silent skip)
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
|
||||
run: |
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "::error::DATABASE_URL must be set for the jsonb-parity job — the #2339 guard would silently skip (the exact failure PGLite hides). Failing the job." >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Run JSONB double-encode parity tests on real Postgres
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
|
||||
run: bun test test/e2e/op-checkpoint-jsonb-parity.test.ts test/e2e/jsonb-roundtrip.test.ts
|
||||
|
||||
tier1:
|
||||
name: Tier 1 (Mechanical)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -82,7 +31,7 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
@@ -100,7 +49,6 @@ jobs:
|
||||
# from repo/org secrets. Nightly + manual triggers still supported via
|
||||
# the workflow-level `on:` list.
|
||||
needs: tier1
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -116,28 +64,13 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- run: bun install
|
||||
- name: Install OpenClaw
|
||||
# Bound + retry the install: a transient npm/registry stall here used to
|
||||
# hang unbounded and (since the v0.42.50.0 job timeout) burn the entire
|
||||
# 30m Tier 2 budget before failing — even though the install normally
|
||||
# finishes in well under a minute. `timeout` kills a hung attempt fast;
|
||||
# up to 3 attempts ride out a flaky registry. Step cap is a backstop.
|
||||
timeout-minutes: 8
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
if timeout 120 npm install -g openclaw@2026.4.9; then
|
||||
exit 0
|
||||
fi
|
||||
echo "::warning::openclaw install attempt $attempt failed or timed out; retrying in 10s" >&2
|
||||
sleep 10
|
||||
done
|
||||
echo "::error::openclaw install failed after 3 attempts" >&2
|
||||
exit 1
|
||||
run: npm install -g openclaw@2026.4.9
|
||||
- name: Configure OpenClaw MCP
|
||||
run: |
|
||||
mkdir -p ~/.openclaw
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
name: OSV-Scanner
|
||||
|
||||
# Dependency vulnerability scan (#2182) via Google's official reusable
|
||||
# workflow. Runs weekly and on any PR that touches the dependency manifests.
|
||||
# Tokenless: needs zero secrets. Findings are reported in the job log and as
|
||||
# a SARIF artifact on the run; code-scanning upload is deliberately disabled
|
||||
# so the workflow stays read-only (no security-events: write).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'bun.lock'
|
||||
- 'package.json'
|
||||
schedule:
|
||||
- cron: '30 6 * * 1' # weekly, Monday 06:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
osv-scan:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
# Required by the reusable workflow's own top-level permissions block —
|
||||
# GitHub validates the caller grants a superset AT STARTUP, even with
|
||||
# upload-sarif: false (nothing is actually uploaded; see #2117 upstream).
|
||||
security-events: write
|
||||
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8
|
||||
with:
|
||||
upload-sarif: false
|
||||
@@ -19,23 +19,14 @@ jobs:
|
||||
target: bun-linux-x64
|
||||
artifact: gbrain-linux-x64
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # for attest-build-provenance (Sigstore OIDC)
|
||||
attestations: write # for attest-build-provenance
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- run: bun install
|
||||
- run: bun test
|
||||
- run: bun run verify
|
||||
- run: bun build --compile --target=${{ matrix.target }} --outfile bin/${{ matrix.artifact }} src/cli.ts
|
||||
- name: Attest build provenance
|
||||
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
||||
with:
|
||||
subject-path: bin/${{ matrix.artifact }}
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
@@ -49,7 +40,7 @@ jobs:
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
with:
|
||||
files: |
|
||||
artifacts/gbrain-darwin-arm64/gbrain-darwin-arm64
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Semgrep
|
||||
|
||||
# Static analysis (SAST) with Semgrep Community Edition (#2272). Tokenless:
|
||||
# uses the public registry rulesets, needs zero secrets. Findings print in
|
||||
# the job log; no code-scanning/SARIF upload by design (keeps permissions
|
||||
# read-only, no security-events: write).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '30 7 * * 1' # weekly, Monday 07:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
container:
|
||||
image: semgrep/semgrep:1.170.0@sha256:c98f8829eea377274ee4b10656458b078b88232469b2ff913f091c2317347c9d
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
# Non-blocking initially (continue-on-error): the first runs establish a
|
||||
# baseline without failing unrelated PRs. Graduation path: once the
|
||||
# baseline findings are triaged (fixed or `# nosemgrep`'d), remove
|
||||
# continue-on-error so new findings block PRs.
|
||||
- name: Semgrep scan (report-only)
|
||||
run: semgrep scan --config p/default --config p/typescript --error
|
||||
continue-on-error: true
|
||||
@@ -14,15 +14,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Cancel a superseded run when a newer commit lands on the same PR/branch.
|
||||
# Keyed on the PR number for pull_request events (unique per PR, so two PRs
|
||||
# from forks sharing a branch name don't cancel each other) and falls back to
|
||||
# github.ref for push/scheduled runs. Mirrors heavy-tests.yml; frees runners
|
||||
# and stops a stale-SHA run from reporting a flaky failure on an obsolete commit.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# cache-check: runs first, computes the content hash of every tracked
|
||||
@@ -38,12 +29,11 @@ jobs:
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
cache-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
hit: ${{ steps.lookup.outputs.cache-hit }}
|
||||
hash: ${{ steps.compute.outputs.hash }}
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Compute content hash
|
||||
id: compute
|
||||
run: |
|
||||
@@ -82,9 +72,8 @@ jobs:
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2
|
||||
@@ -101,9 +90,8 @@ jobs:
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 12
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
@@ -122,9 +110,8 @@ jobs:
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
@@ -147,9 +134,8 @@ jobs:
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 12
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
@@ -170,9 +156,8 @@ jobs:
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 12
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
@@ -206,17 +191,12 @@ jobs:
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# 22, not 15: under parallel PR load the PGLite WASM cold-starts stretch a
|
||||
# shard past 15 min while every test is still passing — the timeout then
|
||||
# cancels the job and the test-status gate reads it as a failure. 13 runs
|
||||
# died this way on 2026-07-21/22 alone.
|
||||
timeout-minutes: 22
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
@@ -240,7 +220,6 @@ jobs:
|
||||
needs: [cache-check, gitleaks, verify, serial-tests, slow-eval-longmemeval, slow-entity-resolve-perf, test]
|
||||
if: success() && needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Create cache marker
|
||||
run: |
|
||||
@@ -263,7 +242,6 @@ jobs:
|
||||
needs: [cache-check, gitleaks, verify, serial-tests, slow-eval-longmemeval, slow-entity-resolve-perf, test]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Aggregate result
|
||||
run: |
|
||||
|
||||
+2
-10
@@ -1,7 +1,4 @@
|
||||
# No trailing slash: a bare `node_modules/` pattern matches directories only,
|
||||
# so a *symlink* named node_modules slips past it and can be committed
|
||||
# (that's how the /tmp-pointing symlink in faf5cdba got in). Match any type.
|
||||
node_modules
|
||||
node_modules/
|
||||
bin/
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -18,7 +15,7 @@ supabase/.temp/
|
||||
# self-contained binaries (the bun --compile path embeds it via
|
||||
# `import path from 'admin/dist/index.html' with { type: 'file' }`).
|
||||
# Build via: cd admin && bun install && bun run build.
|
||||
admin/node_modules
|
||||
admin/node_modules/
|
||||
.idea
|
||||
eval/reports/
|
||||
eval/data/world-v1/world.html
|
||||
@@ -38,11 +35,6 @@ export/
|
||||
# .context/test-shards/. Workspace-local by design — never committed.
|
||||
.context/
|
||||
|
||||
# Local agent instruction overrides (CLAUDE.local.md / AGENTS.local.md) — personal,
|
||||
# per-clone, loaded after the committed CLAUDE.md/AGENTS.md. Never committed.
|
||||
CLAUDE.local.md
|
||||
AGENTS.local.md
|
||||
|
||||
# Tier 3 PGLite snapshot fixture (built on demand by build:pglite-snapshot)
|
||||
test/fixtures/pglite-snapshot.tar
|
||||
test/fixtures/pglite-snapshot.version
|
||||
|
||||
@@ -32,12 +32,8 @@ start here.
|
||||
## Read this order
|
||||
|
||||
1. `./AGENTS.md` (this file) — install + operating protocol.
|
||||
2. [`./CLAUDE.md`](./CLAUDE.md) — orientation + resolver: architecture, cross-cutting
|
||||
invariants, the reference map, inline ship rules. It routes to on-demand detail docs:
|
||||
[`./docs/architecture/KEY_FILES.md`](./docs/architecture/KEY_FILES.md) (per-file index —
|
||||
read a file's entry before editing it), [`./docs/TESTING.md`](./docs/TESTING.md) (test
|
||||
tiers + isolation lint + E2E lifecycle), and
|
||||
[`./docs/architecture/thin-client.md`](./docs/architecture/thin-client.md) (remote-MCP seam).
|
||||
2. [`./CLAUDE.md`](./CLAUDE.md) — architecture reference, key files, trust boundaries,
|
||||
test layout.
|
||||
3. [`./docs/architecture/brains-and-sources.md`](./docs/architecture/brains-and-sources.md)
|
||||
— the two-axis mental model (brain = which DB, source = which repo in the DB). Every
|
||||
query routes on both axes. Read before writing anything that touches brain ops.
|
||||
@@ -104,18 +100,15 @@ writing or reviewing an operation, consult `src/core/operations.ts` for the cont
|
||||
## Before shipping
|
||||
|
||||
Easiest path: `bun run ci:local` runs the full CI gate inside Docker (gitleaks,
|
||||
guards + typecheck, then 4-shard parallel unit + E2E against four pgvector
|
||||
containers plus a transaction-mode PgBouncer; unit phase keeps `DATABASE_URL`
|
||||
unset) and tears down. Use `bun run ci:local:diff` for the
|
||||
unit tests with `DATABASE_URL` unset, then all 29 E2E files sequentially against a
|
||||
fresh pgvector container) and tears down. Use `bun run ci:local:diff` for the
|
||||
diff-aware subset during fast iteration on a focused branch. Requires Docker
|
||||
(Docker Desktop / OrbStack / Colima) and `gitleaks` (`brew install gitleaks`).
|
||||
|
||||
Manual path: `bun test` plus the E2E lifecycle described in `./CLAUDE.md` (spin
|
||||
up the test Postgres container, run `bun run test:e2e`, tear it down).
|
||||
|
||||
Ship via the `/ship` skill, not by hand. The full release + contributor process
|
||||
(CHANGELOG voice, version-locations sync, PR conventions, community-PR-wave) lives in
|
||||
[`./docs/RELEASING.md`](./docs/RELEASING.md); read it before shipping.
|
||||
Ship via the `/ship` skill, not by hand.
|
||||
|
||||
## Privacy
|
||||
|
||||
|
||||
+4
-2375
File diff suppressed because it is too large
Load Diff
@@ -11,28 +11,6 @@ bun test
|
||||
|
||||
Requires Bun 1.0+.
|
||||
|
||||
### Windows
|
||||
|
||||
`bun run test`, `verify`, `ci:local` and `test:e2e` all dispatch through bash, so
|
||||
the shell scripts under `scripts/` must be checked out with Unix line endings.
|
||||
The root `.gitattributes` pins `*.sh text eol=lf`, which overrides the
|
||||
`core.autocrlf=true` that Git for Windows installs by default. A fresh clone is
|
||||
correct with no extra steps.
|
||||
|
||||
If you cloned before that pin existed, your working copy still has the old
|
||||
Windows line endings and bash will fail with `$'\r': command not found`. Refresh
|
||||
it once, from the repository root:
|
||||
|
||||
```bash
|
||||
git rm --cached -r . -q
|
||||
git reset --hard
|
||||
bash -n scripts/run-unit-parallel.sh # silence means bash can read the scripts
|
||||
```
|
||||
|
||||
Every `check:*` entry in `package.json` invokes its script as `bash scripts/<name>.sh`
|
||||
rather than relying on the shebang, because bun on Windows cannot exec a `.sh`
|
||||
directly. Keep that prefix when you add a new shell-script check.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
@@ -185,14 +163,6 @@ host port with `GBRAIN_CI_PG_PORT=5435 bun run ci:local` if 5434 collides.
|
||||
Fail-closed selector: an unmapped `src/` change runs all 29 E2E files. Hand-tune
|
||||
narrower mappings via `scripts/e2e-test-map.ts`.
|
||||
|
||||
### PR-side security checks
|
||||
|
||||
Besides the test gate, PRs may trigger three security workflows: Semgrep CE
|
||||
SAST (every PR — **advisory/non-blocking** while the baseline is tuned, so a
|
||||
Semgrep finding won't fail your PR), OSV-Scanner (only when `package.json` or
|
||||
`bun.lock` change), and actionlint (only when `.github/workflows/**` change).
|
||||
See `SECURITY.md` → "Automated security scanning" for details.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
|
||||
@@ -161,29 +161,6 @@ After this step:
|
||||
If a user has a very large brain (>10K pages), `extract --source db` is idempotent
|
||||
and supports `--since YYYY-MM-DD` for incremental runs.
|
||||
|
||||
### Obsidian-style bare wikilinks (opt-in)
|
||||
|
||||
If the user imported an Obsidian or Notion vault that uses **bare** `[[note-name]]`
|
||||
wikilinks — where `[[struktura]]` written in one folder means the page that lives
|
||||
at `projects/struktura.md` in another — GBrain does NOT connect those by default.
|
||||
Out of the box it only resolves path-qualified refs like `[[projects/struktura]]`,
|
||||
so a vault full of bare links shows up as a thin, broken graph. Turn on basename
|
||||
resolution so the cross-folder links connect:
|
||||
|
||||
```bash
|
||||
gbrain config set link_resolution.global_basename true
|
||||
gbrain extract links --source db # re-run so the new edges land
|
||||
```
|
||||
|
||||
`gbrain doctor` surfaces a `link_resolution_opportunity` hint with the exact count
|
||||
("47 of 60 bare wikilinks would resolve") so you know whether it's worth enabling
|
||||
before you flip it. When a bare name matches more than one page (`[[struktura]]` →
|
||||
both `projects/struktura` and `archive/struktura`), GBrain emits one edge to each
|
||||
rather than guessing a winner — review and prune the duplicates with
|
||||
`gbrain graph-query <slug>`. The mode is also honored on the filesystem-walk path
|
||||
(`gbrain extract links` with no `--source db`) and by auto-link on every future
|
||||
`put_page`.
|
||||
|
||||
## Step 5: Load Skills
|
||||
|
||||
If you're running an agent platform (OpenClaw, Hermes, or any repo with a workspace),
|
||||
|
||||
@@ -71,8 +71,8 @@ GBrain is designed to be installed and operated by an AI agent. The fastest path
|
||||
|
||||
If you don't already have an AI agent platform running, start with one of these. Both are designed to read GBrain's install protocol and execute it:
|
||||
|
||||
- **[OpenClaw](https://github.com/openclaw/openclaw)** — deploy [AlphaClaw on Render](https://render.com/deploy?repo=https://github.com/chrysb/alphaclaw) (one click, 8GB+ RAM)
|
||||
- **[Hermes](https://github.com/NousResearch/hermes-agent)** — deploy on [Railway](https://github.com/praveen-ks-2001/hermes-agent-template) (one click)
|
||||
- **[OpenClaw](https://github.com/openclawagents/openclaw)** — deploy [AlphaClaw on Render](https://render.com/deploy?repo=https://github.com/chrysb/alphaclaw) (one click, 8GB+ RAM)
|
||||
- **[Hermes](https://github.com/openclawagents/hermes)** — deploy on [Railway](https://github.com/praveen-ks-2001/hermes-agent-template) (one click)
|
||||
|
||||
Then paste this into your agent:
|
||||
|
||||
@@ -254,28 +254,10 @@ The whole loop is described in [`docs/architecture/topologies.md`](docs/architec
|
||||
|
||||
**Hybrid search.** Vector (HNSW on pgvector) + BM25 keyword + reciprocal-rank fusion + source-tier boost + intent-aware query rewriting. Three named search modes (`conservative`, `balanced`, `tokenmax`) bundle the cost/quality knobs into a single config key. Live cost/recall comparisons in [`docs/eval/SEARCH_MODE_METHODOLOGY.md`](docs/eval/SEARCH_MODE_METHODOLOGY.md). Default: `balanced` with ZeroEntropy reranker on. Per-query graph signals notice when a top result is a hub for THAT query (adjacency boost), is corroborated across team brains (cross-source boost), or is being crowded out by weak chunks from a chatty session (session demote). Run `gbrain search "<query>" --explain` to see per-stage attribution: base score, every boost that fired, what it multiplied. `gbrain doctor` ships a `graph_signals_coverage` check; `gbrain search stats` shows fire counts and failure breakdowns. Vector retrieval pools the best chunk per page, so a page surfaces on its strongest evidence instead of losing to a neighbor on one weak chunk. Queries that match a page's title phrase or a declared free-text alias (`gbrain reindex --aliases` backfills existing pages) get boosted to the page they name. Every result carries an `evidence` tag (why it matched) and a `create_safety` hint (`exists` / `probable` / `unknown`) so an agent decides whether a page already exists instead of guessing from a raw score. `gbrain search diagnose "<query>" --target <slug>` traces which retrieval layer surfaces (or misses) a page.
|
||||
|
||||
**Self-wiring knowledge graph.** Every `put_page` extracts entity refs from markdown/wikilinks/typed-link syntax and writes edges with zero LLM calls. Typed edges (`attended`, `works_at`, `invested_in`, `founded`, `advises`, `mentions`, …). Multi-hop traversal via `gbrain graph-query`. The graph is what produces the +31.4 P@5 lift over vector-only RAG. **Obsidian-style vaults:** bare `[[note-name]]` wikilinks that point across folders — you wrote `[[struktura]]` but the page lives at `projects/struktura.md` — resolve by basename once you opt in with `gbrain config set link_resolution.global_basename true`. Off by default; `gbrain doctor` tells you how many edges you'd gain before you flip it. See [migrating an Obsidian vault](INSTALL_FOR_AGENTS.md#step-45-wire-the-knowledge-graph).
|
||||
**Self-wiring knowledge graph.** Every `put_page` extracts entity refs from markdown/wikilinks/typed-link syntax and writes edges with zero LLM calls. Typed edges (`attended`, `works_at`, `invested_in`, `founded`, `advises`, `mentions`, …). Multi-hop traversal via `gbrain graph-query`. The graph is what produces the +31.4 P@5 lift over vector-only RAG.
|
||||
|
||||
**Job queue (Minions).** BullMQ-shaped, Postgres-native job queue. Durable subagents (LLM tool loops that survive crashes via two-phase pending→done persistence), shell jobs with audit, child jobs with cascading timeouts, rate leases for outbound providers, attachments via S3/Supabase storage. Replaces "spawn subagent as fire-and-forget Promise" with something that recovers from anything.
|
||||
|
||||
**Non-English brains (FTS language config).** The Postgres full-text search tokenizer is configurable via `GBRAIN_FTS_LANGUAGE`. Defaults to `english`. Set it to any text-search configuration that exists in your Postgres instance:
|
||||
|
||||
```bash
|
||||
export GBRAIN_FTS_LANGUAGE=portuguese # uses built-in portuguese stemmer
|
||||
export GBRAIN_FTS_LANGUAGE=spanish # built-in spanish stemmer
|
||||
export GBRAIN_FTS_LANGUAGE=pt_br # custom config (e.g. unaccent + portuguese)
|
||||
```
|
||||
|
||||
List available configs: `psql -c "SELECT cfgname FROM pg_ts_config"`. Both the **query side** (`websearch_to_tsquery`) and the **write side** (the trigger functions that populate `pages.search_vector` and `content_chunks.search_vector`) honor `GBRAIN_FTS_LANGUAGE`. On first install (or upgrade), the `configurable_fts_language` schema migration reads the env var and creates trigger functions in the configured language; subsequent inserts/updates tokenize using that setting. To change language on a brain that has already run the migration, use the dedicated CLI command:
|
||||
|
||||
```bash
|
||||
export GBRAIN_FTS_LANGUAGE=portuguese
|
||||
gbrain reindex-search-vector --dry-run # preview row counts
|
||||
gbrain reindex-search-vector --yes # recreate triggers + backfill
|
||||
```
|
||||
|
||||
The command is idempotent (re-running with the same language is a no-op for vector content) and uses the same recreate-and-backfill primitives as the migration. For accent-insensitive Portuguese (`pt_br`), see [docs/guides/multi-language-fts.md](docs/guides/multi-language-fts.md) for the `unaccent` + portuguese stemmer recipe.
|
||||
|
||||
**43 curated skills.** Routing lives in [`skills/RESOLVER.md`](skills/RESOLVER.md). Covers signal capture, ingest (idea / media / meeting), enrichment, querying, brain ops, citation fixing, daily task management, cron scheduling, reports, voice, soul audit, skill creation, eval framework, and migrations. Skills are markdown files (tool-agnostic), packaged as a single skillpack the installer drops into your agent workspace.
|
||||
|
||||
**Eval framework.** `gbrain eval longmemeval` runs the public [LongMemEval](https://huggingface.co/datasets/xiaowu0162/longmemeval) benchmark against your hybrid retrieval. `gbrain eval export` + `gbrain eval replay` capture real queries and replay them against code changes (set `GBRAIN_CONTRIBUTOR_MODE=1`). `gbrain eval cross-modal` cross-checks an output against the task using three different-provider frontier models. `gbrain eval retrieval-quality` runs NamedThingBench, which hard-gates the named-thing retrieval families (title-substring, alias-synonym, generic-to-named, multi-chunk-dilution) so a regression in "find the page this query names" fails CI loudly. Full methodology in [`docs/eval/SEARCH_MODE_METHODOLOGY.md`](docs/eval/SEARCH_MODE_METHODOLOGY.md).
|
||||
@@ -307,8 +289,6 @@ Data flowing into the brain. Each integration is a recipe — markdown + setup h
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**`gbrain init --pglite` crashes on macOS 26.x (Tahoe)?** PGLite's embedded WASM engine is incompatible with macOS 26.x on Apple Silicon. The fix is to use native Homebrew PostgreSQL + pgvector instead. Full step-by-step setup in [`docs/INSTALL.md` — Troubleshooting: PGLite crashes on macOS 26.x](docs/INSTALL.md#pglite-crashes-on-macos-26x-tahoe).
|
||||
|
||||
**`gbrain import` fails with `expected N dimensions, not M`?** Run `gbrain doctor`. It will print the exact `gbrain config set ...` or `gbrain retrieval-upgrade` command to repair the mismatch. You should not need to delete `~/.gbrain`. Fresh `gbrain init --pglite` auto-detects your embedding provider from API keys in your environment: set `OPENAI_API_KEY` (or `ZEROENTROPY_API_KEY` / `VOYAGE_API_KEY`) before running init, or pass `--embedding-model <provider>:<model>` explicitly. With multiple keys set, init fires an interactive picker. In non-TTY contexts (CI, Docker) with no keys, init exits 1 with a paste-ready setup hint; pass `--no-embedding` to defer setup until runtime. See [`docs/integrations/embedding-providers.md`](docs/integrations/embedding-providers.md) for the full provider matrix and [`docs/operations/headless-install.md`](docs/operations/headless-install.md) for Docker/CI sequencing.
|
||||
|
||||
**Hourly cron sync keeps timing out on a federated brain?** v0.41.13.0 ships
|
||||
|
||||
-48
@@ -8,30 +8,6 @@ on GitHub.
|
||||
|
||||
Do not open a public issue for security vulnerabilities.
|
||||
|
||||
## Automated security scanning
|
||||
|
||||
CI runs three automated security checks alongside secret scanning (Gitleaks):
|
||||
|
||||
- **Dependency vulnerabilities** — OSV-Scanner
|
||||
(`.github/workflows/osv-scanner.yml`) runs weekly and on any PR that touches
|
||||
`package.json` or `bun.lock`.
|
||||
- **Static analysis (SAST)** — Semgrep CE (`.github/workflows/semgrep.yml`)
|
||||
runs on every PR and weekly. It is currently **advisory (non-blocking)**
|
||||
while the finding baseline is tuned; the graduation path to a blocking check
|
||||
is documented in the workflow file.
|
||||
- **Release binary provenance** — release builds
|
||||
(`.github/workflows/release.yml`) attest each compiled binary with
|
||||
[GitHub artifact attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations).
|
||||
Verify a downloaded release binary with:
|
||||
|
||||
```bash
|
||||
gh attestation verify ./gbrain-darwin-arm64 -R garrytan/gbrain
|
||||
gh attestation verify ./gbrain-linux-x64 -R garrytan/gbrain
|
||||
```
|
||||
|
||||
All security workflows use SHA-pinned actions and least-privilege permissions,
|
||||
enforced structurally by actionlint on every workflow change.
|
||||
|
||||
## Remote MCP Security
|
||||
|
||||
### ⚠️ Do NOT use open OAuth client registration for remote MCP
|
||||
@@ -111,18 +87,6 @@ and the DCR `POST /register` path. Pre-v0.41.3 the CLI hard-coded
|
||||
operators to UPDATE `oauth_clients` rows by hand to make claude.ai work
|
||||
without `--enable-dcr`. That footgun is gone.
|
||||
|
||||
### DCR consent default (v0.42.55+)
|
||||
|
||||
The "disable `client_credentials`, only allow `authorization_code`" guidance
|
||||
above is now the built-in default for the DCR path, not just advice for custom
|
||||
wrappers. With `--enable-dcr` on, a self-registered client defaults to the
|
||||
`authorization_code` (browser-approval) grant, and an explicit
|
||||
`client_credentials` request is rejected with `invalid_client_metadata`.
|
||||
Operators who genuinely need the machine-to-machine grant on the registration
|
||||
endpoint opt in with `--enable-dcr-insecure` (which implies `--enable-dcr`); a
|
||||
startup WARNING prints whenever DCR is enabled, and a second when the insecure
|
||||
grant is allowed. Pre-registering clients via the CLI / admin API is unchanged.
|
||||
|
||||
### Token Management
|
||||
|
||||
```bash
|
||||
@@ -159,18 +123,6 @@ the PGLite schema. Local agents continue to use stdio (`gbrain serve`).
|
||||
Running `--http` against a PGLite-backed install fails fast with a clear
|
||||
error message at startup.
|
||||
|
||||
### Docker network isolation (self-hosted Postgres)
|
||||
|
||||
OAuth and source scoping enforce isolation on the `serve --http` path only.
|
||||
Raw Postgres reachability bypasses both: a container that shares Docker's
|
||||
default `bridge` network with the brain's Postgres can open a direct DB
|
||||
session without any token and read every source. Put the brain's Postgres on
|
||||
a user-defined Docker network with nothing untrusted on it, publish its port
|
||||
loopback-only (if at all), and never put `DATABASE_URL` or a Postgres
|
||||
password in untrusted agent containers — those should reach the brain
|
||||
exclusively via OAuth against `serve --http`. Full operator checklist:
|
||||
[docs/mcp/DEPLOY.md — Co-located Docker workloads](docs/mcp/DEPLOY.md#co-located-docker-workloads-self-hosted-postgres).
|
||||
|
||||
### CORS
|
||||
|
||||
Default-deny: no `Access-Control-Allow-Origin` header is sent unless an
|
||||
|
||||
+20
-52
@@ -13,52 +13,48 @@
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.4.3",
|
||||
"vite": "^6.3.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core": "^7.29.6",
|
||||
"postcss": "^8.5.10",
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
@@ -224,7 +220,7 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||
|
||||
@@ -232,7 +228,7 @@
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="],
|
||||
"postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
|
||||
|
||||
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
|
||||
|
||||
@@ -254,36 +250,8 @@
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="],
|
||||
"vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@types/babel__core/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@types/babel__core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@types/babel__generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@types/babel__template/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@types/babel__template/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@types/babel__core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@types/babel__generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@types/babel__template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-56
File diff suppressed because one or more lines are too long
Vendored
+56
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<script type="module" crossorigin src="/admin/assets/index-CviJXT-1.js"></script>
|
||||
<script type="module" crossorigin src="/admin/assets/index-DqP-zmqH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-GxkWX7v3.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+1
-5
@@ -15,11 +15,7 @@
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"vite": "^6.4.3",
|
||||
"vite": "^6.3.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core": "^7.29.6",
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-12
@@ -39,21 +39,11 @@ export const api = {
|
||||
stats: () => apiFetch('/admin/api/stats'),
|
||||
health: () => apiFetch('/admin/api/health-indicators'),
|
||||
agents: () => apiFetch('/admin/api/agents'),
|
||||
sources: () => apiFetch('/admin/api/sources'),
|
||||
requests: (page = 1, qs = '') => apiFetch(`/admin/api/requests?page=${page}${qs}`),
|
||||
apiKeys: () => apiFetch('/admin/api/api-keys'),
|
||||
createApiKey(keyName: string) {
|
||||
return apiFetch('/admin/api/api-keys', { method: 'POST', body: JSON.stringify({ name: keyName }) });
|
||||
},
|
||||
revokeApiKey(keyName: string) {
|
||||
return apiFetch('/admin/api/api-keys/revoke', { method: 'POST', body: JSON.stringify({ name: keyName }) });
|
||||
},
|
||||
createApiKey: (name: string) => apiFetch('/admin/api/api-keys', { method: 'POST', body: JSON.stringify({ name }) }),
|
||||
revokeApiKey: (name: string) => apiFetch('/admin/api/api-keys/revoke', { method: 'POST', body: JSON.stringify({ name }) }),
|
||||
updateClientTtl: (clientId: string, tokenTtl: number | null) => apiFetch('/admin/api/update-client-ttl', { method: 'POST', body: JSON.stringify({ clientId, tokenTtl }) }),
|
||||
rescopeClient: (clientId: string, sourceId: string, federatedRead: string[]) =>
|
||||
apiFetch('/admin/api/rescope-client', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ clientId, sourceId, federatedRead }),
|
||||
}),
|
||||
revokeClient: (clientId: string) => apiFetch('/admin/api/revoke-client', { method: 'POST', body: JSON.stringify({ clientId }) }),
|
||||
// v0.36.1.0 (T15 / E6) — calibration endpoints.
|
||||
calibrationProfile: (holder?: string) =>
|
||||
|
||||
+4
-169
@@ -18,8 +18,6 @@ interface Agent {
|
||||
client_name?: string; // compat
|
||||
grant_types: string[];
|
||||
scope: string;
|
||||
source_id: string | null;
|
||||
federated_read: string[];
|
||||
created_at: string;
|
||||
last_used_at: string | null;
|
||||
total_requests: number;
|
||||
@@ -28,12 +26,6 @@ interface Agent {
|
||||
status: 'active' | 'revoked';
|
||||
}
|
||||
|
||||
interface Source {
|
||||
id: string;
|
||||
name: string;
|
||||
federated: boolean;
|
||||
}
|
||||
|
||||
interface ApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -44,7 +36,6 @@ interface ApiKey {
|
||||
|
||||
export function AgentsPage() {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [sources, setSources] = useState<Source[]>([]);
|
||||
const [hideRevoked, setHideRevoked] = useState(true);
|
||||
const [showRegister, setShowRegister] = useState(false);
|
||||
const [showCredentials, setShowCredentials] = useState<{ clientId: string; clientSecret: string; name: string } | null>(null);
|
||||
@@ -52,10 +43,7 @@ export function AgentsPage() {
|
||||
const [showApiKeyToken, setShowApiKeyToken] = useState<{ name: string; token: string } | null>(null);
|
||||
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadAgents();
|
||||
api.sources().then(setSources).catch(() => {});
|
||||
}, []);
|
||||
useEffect(() => { loadAgents(); }, []);
|
||||
|
||||
const loadAgents = () => { api.agents().then(setAgents).catch(() => {}); };
|
||||
|
||||
@@ -100,7 +88,6 @@ export function AgentsPage() {
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Scopes</th>
|
||||
<th>Sources</th>
|
||||
<th>Status</th>
|
||||
<th>Requests</th>
|
||||
<th>Last Used</th>
|
||||
@@ -121,11 +108,6 @@ export function AgentsPage() {
|
||||
<span key={s} className={`badge badge-${s}`} style={{ marginRight: 4 }}>{s}</span>
|
||||
))}
|
||||
</td>
|
||||
<td style={{ color: 'var(--text-secondary)', fontSize: 12 }}>
|
||||
{a.auth_type === 'oauth'
|
||||
? `${a.source_id || 'none'} · ${(a.federated_read || []).length} readable`
|
||||
: 'Unscoped'}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${a.status === 'active' ? 'badge-success' : 'badge-danger'}`}>{a.status}</span>
|
||||
</td>
|
||||
@@ -162,21 +144,7 @@ export function AgentsPage() {
|
||||
)}
|
||||
|
||||
{selectedAgent && (
|
||||
<AgentDrawer
|
||||
key={selectedAgent.id}
|
||||
agent={selectedAgent}
|
||||
sources={sources}
|
||||
onClose={() => setSelectedAgent(null)}
|
||||
onRevoked={loadAgents}
|
||||
onRescoped={({ sourceId, federatedRead }) => {
|
||||
setSelectedAgent(current => current ? {
|
||||
...current,
|
||||
source_id: sourceId,
|
||||
federated_read: federatedRead,
|
||||
} : current);
|
||||
loadAgents();
|
||||
}}
|
||||
/>
|
||||
<AgentDrawer agent={selectedAgent} onClose={() => setSelectedAgent(null)} onRevoked={loadAgents} />
|
||||
)}
|
||||
|
||||
{showApiKeyCreate && (
|
||||
@@ -413,127 +381,7 @@ function CredentialsModal({ credentials, onClose }: {
|
||||
);
|
||||
}
|
||||
|
||||
function SourceAccessEditor({ clientId, agent, sources, onRescoped }: {
|
||||
clientId: string;
|
||||
agent: Agent;
|
||||
sources: Source[];
|
||||
onRescoped: (scope: { sourceId: string; federatedRead: string[] }) => void;
|
||||
}) {
|
||||
const [writeSource, setWriteSource] = useState(agent.source_id || 'default');
|
||||
const [readSources, setReadSources] = useState<string[]>(agent.federated_read || []);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [saved, setSaved] = useState(false);
|
||||
const readableSet = new Set(readSources);
|
||||
const activeSourceIds = new Set(sources.map(source => source.id));
|
||||
const unavailableReadSources = readSources.filter(sourceId => !activeSourceIds.has(sourceId));
|
||||
const primaryUnavailable = !activeSourceIds.has(writeSource);
|
||||
|
||||
const save = async () => {
|
||||
if (readSources.length === 0) {
|
||||
setError('Select at least one readable source.');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setError('');
|
||||
setSaved(false);
|
||||
try {
|
||||
const result = await api.rescopeClient(clientId, writeSource, readSources) as {
|
||||
sourceId: string;
|
||||
federatedRead: string[];
|
||||
};
|
||||
setWriteSource(result.sourceId);
|
||||
setReadSources(result.federatedRead);
|
||||
setSaved(true);
|
||||
onRescoped(result);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to save source access');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-title">Source Access</div>
|
||||
<div style={{ color: 'var(--text-secondary)', fontSize: 12, lineHeight: 1.5, marginBottom: 12 }}>
|
||||
The primary source is the write destination. Read access is an explicit allowlist and does not widen automatically.
|
||||
</div>
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<label htmlFor="agent-write-source">Primary / write source</label>
|
||||
<select
|
||||
id="agent-write-source"
|
||||
value={writeSource}
|
||||
onChange={e => { setWriteSource(e.target.value); setSaved(false); }}
|
||||
style={{ width: '100%', background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)', borderRadius: 6, padding: '6px 10px', fontSize: 14 }}
|
||||
>
|
||||
{primaryUnavailable && (
|
||||
<option value={writeSource} disabled>{writeSource} · unavailable</option>
|
||||
)}
|
||||
{sources.map(source => (
|
||||
<option key={source.id} value={source.id}>{source.name} ({source.id})</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<fieldset style={{ border: 0, padding: 0, margin: '0 0 14px' }}>
|
||||
<legend>Readable sources</legend>
|
||||
<div className="checkbox-group" style={{ marginTop: 6 }}>
|
||||
{sources.map(source => (
|
||||
<label key={source.id} className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={readableSet.has(source.id)}
|
||||
onChange={e => {
|
||||
setSaved(false);
|
||||
setReadSources(current => e.target.checked
|
||||
? [...current, source.id]
|
||||
: current.filter(id => id !== source.id));
|
||||
}}
|
||||
/>
|
||||
{source.name} ({source.id}){source.federated ? ' · federated' : ' · private'}
|
||||
</label>
|
||||
))}
|
||||
{unavailableReadSources.map(sourceId => (
|
||||
<label key={sourceId} className="checkbox-label" style={{ color: 'var(--warning)' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked
|
||||
onChange={() => {
|
||||
setSaved(false);
|
||||
setReadSources(current => current.filter(id => id !== sourceId));
|
||||
}}
|
||||
/>
|
||||
{sourceId} · unavailable (clear to remove grant)
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
{(primaryUnavailable || unavailableReadSources.length > 0) && (
|
||||
<div style={{ color: 'var(--warning)', fontSize: 13, marginBottom: 10 }}>
|
||||
This client references unavailable or archived sources. Choose an active primary source and clear unavailable read grants before saving.
|
||||
</div>
|
||||
)}
|
||||
{error && <div style={{ color: 'var(--error)', fontSize: 13, marginBottom: 10 }}>{error}</div>}
|
||||
{saved && <div style={{ color: 'var(--success)', fontSize: 13, marginBottom: 10 }}>Source access saved.</div>}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={saving || readSources.length === 0 || sources.length === 0 || primaryUnavailable || unavailableReadSources.length > 0}
|
||||
onClick={save}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Source Access'}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentDrawer({ agent, sources, onClose, onRevoked, onRescoped }: {
|
||||
agent: Agent;
|
||||
sources: Source[];
|
||||
onClose: () => void;
|
||||
onRevoked: () => void;
|
||||
onRescoped: (scope: { sourceId: string; federatedRead: string[] }) => void;
|
||||
}) {
|
||||
function AgentDrawer({ agent, onClose, onRevoked }: { agent: Agent; onClose: () => void; onRevoked: () => void }) {
|
||||
const [tab, setTab] = useState<'claude-code' | 'chatgpt' | 'claude-cowork' | 'perplexity' | 'cursor' | 'json'>('claude-code');
|
||||
const copy = (text: string) => navigator.clipboard.writeText(text);
|
||||
const serverUrl = window.location.origin;
|
||||
@@ -705,15 +553,6 @@ function AgentDrawer({ agent, sources, onClose, onRevoked, onRescoped }: {
|
||||
<span>{agent.token_ttl ? (agent.token_ttl >= 31536000 ? 'No expiry' : agent.token_ttl >= 86400 ? `${Math.floor(agent.token_ttl / 86400)}d` : agent.token_ttl >= 3600 ? `${Math.floor(agent.token_ttl / 3600)}h` : `${agent.token_ttl}s`) : '1h (default)'}</span>
|
||||
</div>
|
||||
|
||||
{isOAuth && (
|
||||
<SourceAccessEditor
|
||||
clientId={cid}
|
||||
agent={agent}
|
||||
sources={sources}
|
||||
onRescoped={onRescoped}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Config Export visible for both auth_type=oauth AND auth_type=api_key.
|
||||
Claude Code + Cursor + JSON tabs render real snippets regardless
|
||||
@@ -740,11 +579,7 @@ function AgentDrawer({ agent, sources, onClose, onRevoked, onRescoped }: {
|
||||
{(() => {
|
||||
const oauthOnlyTabs = new Set(['chatgpt', 'claude-cowork', 'perplexity']);
|
||||
if (!isOAuth && oauthOnlyTabs.has(tab)) {
|
||||
const clientName = tab === 'chatgpt'
|
||||
? 'ChatGPT'
|
||||
: tab === 'claude-cowork'
|
||||
? 'Claude.ai'
|
||||
: 'Perplexity';
|
||||
const clientName = { chatgpt: 'ChatGPT', 'claude-cowork': 'Claude.ai', perplexity: 'Perplexity' }[tab] || tab;
|
||||
return (
|
||||
<div style={{
|
||||
background: 'rgba(255, 200, 100, 0.08)',
|
||||
|
||||
@@ -21,7 +21,7 @@ export function DashboardPage() {
|
||||
api.stats().then(setStats).catch(() => {});
|
||||
api.health().then(setHealth).catch(() => {});
|
||||
|
||||
const es = new EventSource('/admin/events', { withCredentials: true });
|
||||
const es = new EventSource('/admin/events');
|
||||
eventSourceRef.current = es;
|
||||
es.onopen = () => setSseStatus('connected');
|
||||
es.onmessage = (e) => {
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"heic-decode": "^2.1.0",
|
||||
"js-yaml": "^3.15.0",
|
||||
"marked": "^18.0.2",
|
||||
"js-yaml": "^3.14.2",
|
||||
"marked": "^18.0.0",
|
||||
"openai": "^4.0.0",
|
||||
"pgvector": "^0.2.0",
|
||||
"postgres": "^3.4.0",
|
||||
@@ -50,18 +50,6 @@
|
||||
"trustedDependencies": [
|
||||
"@electric-sql/pglite",
|
||||
],
|
||||
"overrides": {
|
||||
"@hono/node-server": "^2.0.5",
|
||||
"body-parser": "^2.3.0",
|
||||
"fast-uri": "^3.1.4",
|
||||
"fast-xml-builder": "^1.1.7",
|
||||
"fast-xml-parser": "^5.7.0",
|
||||
"form-data": "^4.0.6",
|
||||
"hono": "^4.12.25",
|
||||
"ip-address": "^10.1.1",
|
||||
"js-yaml": "^3.15.0",
|
||||
"qs": "^6.15.2",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.74", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Xew9rfz9WWhDSyF8rNhjT/XWOWelNfJrMlmG0Ahw210hStisRpQZ1s+7VeI9JTJOZ5y5tXqBi5kfPwYnCfyRTA=="],
|
||||
|
||||
@@ -163,7 +151,7 @@
|
||||
|
||||
"@electric-sql/pglite": ["@electric-sql/pglite@0.4.3", "", {}, "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@2.0.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-bjD221KPLoJTWUwso1J6fGKiTXEUFedG/s0visavY4zakFPkeGURMRNly+FhBHs7T8Dz4qHaZIMX9ZoJHSJtKA=="],
|
||||
"@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
|
||||
|
||||
"@jsquash/avif": ["@jsquash/avif@2.1.1", "", { "dependencies": { "wasm-feature-detect": "^1.2.11" } }, "sha512-LMRxd0fMgfCLtobDh0/sFYJMMiRJTNYSEEWvRDKXlAeZ08t3gI5V+1thIT0XjXJ+SVG7Zug9B0XPyx0Ti5VRNA=="],
|
||||
|
||||
@@ -171,8 +159,6 @@
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="],
|
||||
@@ -321,13 +307,11 @@
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="],
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
|
||||
|
||||
@@ -401,15 +385,15 @@
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="],
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="],
|
||||
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="],
|
||||
"fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||
|
||||
@@ -433,11 +417,11 @@
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"heic-decode": ["heic-decode@2.1.0", "", { "dependencies": { "libheif-js": "^1.19.8" } }, "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A=="],
|
||||
|
||||
"hono": ["hono@4.12.30", "", {}, "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog=="],
|
||||
"hono": ["hono@4.12.10", "", {}, "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
@@ -447,7 +431,7 @@
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
@@ -455,13 +439,11 @@
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"is-unsafe": ["is-unsafe@2.0.0", "", {}, "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@3.15.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog=="],
|
||||
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
@@ -473,7 +455,7 @@
|
||||
|
||||
"libheif-js": ["libheif-js@1.19.8", "", {}, "sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ=="],
|
||||
|
||||
"marked": ["marked@18.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w=="],
|
||||
"marked": ["marked@18.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
@@ -505,7 +487,7 @@
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="],
|
||||
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
@@ -521,7 +503,7 @@
|
||||
|
||||
"pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
|
||||
|
||||
"qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
@@ -547,9 +529,9 @@
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
@@ -561,7 +543,7 @@
|
||||
|
||||
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||
|
||||
"strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
|
||||
"strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
@@ -595,8 +577,6 @@
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
@@ -615,20 +595,12 @@
|
||||
|
||||
"@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||
|
||||
"body-parser/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
||||
|
||||
"es-set-tostringtag/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"eventsource/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"get-intrinsic/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
+1
-6
@@ -13,9 +13,4 @@ timeout = 60_000
|
||||
# fixtures still match the schema. v0.37's production default is ZE/1280;
|
||||
# tests that want the new default call configureGateway() explicitly in
|
||||
# their own beforeAll.
|
||||
#
|
||||
# #2823: redirect GBRAIN_AUDIT_DIR to a per-run scratch dir BEFORE any test
|
||||
# runs, so audit-emitting code paths (content-sanity, shell-audit, etc.)
|
||||
# can't leak fixture events into the operator's real ~/.gbrain/audit/. See
|
||||
# test/helpers/audit-dir-preload.ts for the full rationale.
|
||||
preload = ["./test/helpers/legacy-embedding-preload.ts", "./test/helpers/audit-dir-preload.ts"]
|
||||
preload = ["./test/helpers/legacy-embedding-preload.ts"]
|
||||
|
||||
@@ -85,40 +85,6 @@ services:
|
||||
volumes:
|
||||
- gbrain-ci-pg-data-4:/var/lib/postgresql/data
|
||||
|
||||
# v0.43 (#2084 / eng-review TD1): PgBouncer in TRANSACTION pooling mode
|
||||
# fronting postgres-1 — the production topology (Supabase direct :5432 +
|
||||
# pooled :6543) behind three consecutive pooler-teardown waves
|
||||
# (#1972 → #2015 → #2084) that CI could never reproduce.
|
||||
# test/e2e/pgbouncer-teardown.test.ts uses a DEDICATED database
|
||||
# (gbrain_pgbouncer) on postgres-1 so it never races shard 1's
|
||||
# TRUNCATE-based fixtures; pgbouncer's wildcard [databases] section
|
||||
# forwards any dbname to DB_HOST.
|
||||
pgbouncer:
|
||||
image: edoburu/pgbouncer:latest
|
||||
environment:
|
||||
DB_HOST: postgres-1
|
||||
DB_PORT: "5432"
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
POOL_MODE: transaction
|
||||
# plain (CI-only): pg16 stores SCRAM verifiers, and pgbouncer can only
|
||||
# answer the server's SCRAM challenge when its userlist holds the
|
||||
# PLAINTEXT password — an md5-hashed userlist fails with
|
||||
# "server login failed: wrong password type".
|
||||
AUTH_TYPE: plain
|
||||
MAX_CLIENT_CONN: "200"
|
||||
DEFAULT_POOL_SIZE: "10"
|
||||
# gbrain's client sets statement_timeout + idle_in_transaction_session_timeout
|
||||
# as startup parameters (db.ts buildConnectionParams); the Supabase pooler
|
||||
# whitelists them, so this pooler must too or every connection is refused
|
||||
# before the teardown path is even reached.
|
||||
IGNORE_STARTUP_PARAMETERS: extra_float_digits,statement_timeout,idle_in_transaction_session_timeout,search_path
|
||||
ports:
|
||||
- "${GBRAIN_CI_PGBOUNCER_PORT:-6543}:5432"
|
||||
depends_on:
|
||||
postgres-1:
|
||||
condition: service_healthy
|
||||
|
||||
runner:
|
||||
image: oven/bun:1
|
||||
working_dir: /app
|
||||
@@ -131,8 +97,6 @@ services:
|
||||
condition: service_healthy
|
||||
postgres-4:
|
||||
condition: service_healthy
|
||||
pgbouncer:
|
||||
condition: service_started
|
||||
# No global DATABASE_URL — scripts/ci-local.sh sets per-shard URL via -e.
|
||||
# Unit phase explicitly unsets DATABASE_URL so test/e2e/* gracefully skip.
|
||||
volumes:
|
||||
|
||||
+1
-79
@@ -94,7 +94,7 @@ export interface BrainEngine {
|
||||
|
||||
**Slug-based API, not ID-based.** Every method takes slugs, not numeric IDs. The engine resolves slugs to IDs internally. This keeps the interface portable... slugs are strings, IDs are database-specific.
|
||||
|
||||
**Embedding is NOT in the engine.** The engine stores embeddings and searches by vector, but it doesn't generate embeddings. `src/core/embedding.ts` handles that (a thin delegation to the provider-agnostic AI gateway in `src/core/ai/gateway.ts`). This is intentional: embedding is an external API call (OpenAI, Voyage, a local Ollama — whichever provider you configured), not a storage concern. All engines share the same embedding service.
|
||||
**Embedding is NOT in the engine.** The engine stores embeddings and searches by vector, but it doesn't generate embeddings. `src/core/embedding.ts` handles that. This is intentional: embedding is an external API call (OpenAI), not a storage concern. All engines share the same embedding service.
|
||||
|
||||
**Chunking is NOT in the engine.** Same logic. `src/core/chunkers/` handles chunking. The engine stores and retrieves chunks. All engines share the same chunkers.
|
||||
|
||||
@@ -148,51 +148,6 @@ RRF fusion, multi-query expansion, and 4-layer dedup are engine-agnostic. They o
|
||||
|
||||
**Why not self-hosted for v0:** The brain should be infrastructure agents use, not something you maintain. Self-hosted Postgres with Docker is a welcome community PR, but v0 optimizes for zero ops.
|
||||
|
||||
### Opt-in RLS source-scope binding (`GBRAIN_RLS_SCOPE_BINDING`)
|
||||
|
||||
Defense-in-depth layer for Postgres deployments that want the database itself
|
||||
to enforce source isolation, in addition to the mandatory app-layer filters
|
||||
(`sourceScopeOpts` — layer 1, always on).
|
||||
|
||||
**Mechanism.** With `GBRAIN_RLS_SCOPE_BINDING=1` (or `true`), the engine's
|
||||
source-scoped read methods wrap their queries in a transaction that first runs
|
||||
`SELECT set_config('app.scopes', $1, true)` — the value is a bound parameter
|
||||
(federated `sourceIds` CSV > scalar `sourceId` > `'*'` for unscoped internal
|
||||
reads), transaction-local (equivalent to `SET LOCAL`, which itself can't take
|
||||
bound params). An RLS policy can then filter rows by
|
||||
`current_setting('app.scopes', true)`.
|
||||
|
||||
**Default off.** With the env var unset, reads call through on the shared pool
|
||||
exactly as before — no per-read transaction, no pool-slot hold (the search
|
||||
methods keep the transaction they always had for their `SET LOCAL
|
||||
statement_timeout`). Existing operators see zero behavior change.
|
||||
|
||||
**Enabling it** (operator-managed SQL; gbrain ships no DDL for this):
|
||||
|
||||
```sql
|
||||
ALTER TABLE pages ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY pages_scope_filter ON pages
|
||||
USING (current_setting('app.scopes', true) = '*'
|
||||
OR source_id = ANY(string_to_array(current_setting('app.scopes', true), ',')));
|
||||
|
||||
-- Required: connections that don't run through the scoped read helper
|
||||
-- (admin, autopilot, cycle, writes) must default to unscoped, or they
|
||||
-- see zero rows once the policy exists:
|
||||
ALTER ROLE <runtime-role> SET app.scopes = '*';
|
||||
|
||||
-- If the runtime role OWNS the table, RLS is skipped for it unless forced:
|
||||
ALTER TABLE pages FORCE ROW LEVEL SECURITY;
|
||||
```
|
||||
|
||||
Safe to enable in either order: the env var without a policy is a no-op
|
||||
setting; a policy without the env var is enforced only via the role default.
|
||||
|
||||
**Honest caveat:** only read paths routed through the scoped helper carry a
|
||||
per-request scope binding — unwrapped paths (writes, admin/maintenance reads)
|
||||
run under the role default and are not backstopped per caller. This is layer 2;
|
||||
the app-layer source filters remain layer 1 and stay mandatory. Behavioral pins
|
||||
live in `test/postgres-engine-rls-scope.test.ts`.
|
||||
|
||||
## PGLiteEngine (v0.7, ships)
|
||||
|
||||
**Dependencies:** `@electric-sql/pglite` (v0.4.4+)
|
||||
@@ -221,39 +176,6 @@ live in `test/postgres-engine-rls-scope.test.ts`.
|
||||
|
||||
**Migration:** `gbrain migrate --to supabase` exports everything (pages, chunks, embeddings, links, tags, timeline) and imports into Supabase. `gbrain migrate --to pglite` goes the other direction. Bidirectional, lossless.
|
||||
|
||||
## JSONB writes: never double-encode (the #2339 trap)
|
||||
|
||||
Writing a JS value into a `jsonb` column has exactly two correct forms. Get this
|
||||
wrong and the write succeeds on PGLite but stores a **jsonb string scalar** on
|
||||
real Postgres — `col ->> 'k'` returns NULL, `jsonb_array_elements` throws, and a
|
||||
`jsonb_typeof = 'array'` CHECK rejects the row (this aborted every sync in #2339).
|
||||
|
||||
| Form | Verdict |
|
||||
|---|---|
|
||||
| Template tag: `` sql`... ${sql.json(obj)}` `` (postgres-engine only) | ✅ native jsonb serialization |
|
||||
| Positional raw call, raw object: `executeRawJsonb(engine, sql, scalars, [obj])` | ✅ object reaches the wire as jsonb |
|
||||
| Positional raw call, stringified: `executeRaw(\`... $N::text::jsonb\`, [JSON.stringify(x)])` | ✅ binds as text, the cast parses it |
|
||||
| Positional raw call, BARE cast: `executeRaw(\`... $N::jsonb\`, [JSON.stringify(x)])` | ❌ **double-encodes** under postgres.js `.unsafe()` |
|
||||
| Template literal interpolation: `` `... ${JSON.stringify(x)}::jsonb` `` | ❌ double-encodes |
|
||||
|
||||
**Why:** postgres.js `.unsafe(sql, params)` (the path behind `executeRaw` /
|
||||
`executeRawDirect`) binds a JS **string** as a text param. A bare `$N::jsonb`
|
||||
cast then wraps that already-JSON string into a jsonb scalar string instead of
|
||||
parsing it. Casting through `$N::text::jsonb` forces a text→jsonb parse.
|
||||
**PGLite's `db.query` parses text→jsonb natively, so it hides the bug** — which is
|
||||
why a regression only shows up on Postgres (and why the parity test must run there).
|
||||
|
||||
**Two CI guards enforce this, both wired into `scripts/check-jsonb-pattern.sh`:**
|
||||
- the template-tag grep (`${JSON.stringify(x)}::jsonb`), and
|
||||
- `scripts/check-jsonb-params.mjs`, an AST-lite scanner for the positional
|
||||
`$N::jsonb` + `JSON.stringify` form the grep misses. Sanctioned escapes:
|
||||
`$N::text::jsonb`, `$N::text[]`, `executeRawJsonb`, `sql.json`, or an inline
|
||||
`jsonb-guard-ok` comment.
|
||||
|
||||
The real backstop is `test/e2e/op-checkpoint-jsonb-parity.test.ts` +
|
||||
`test/e2e/jsonb-roundtrip.test.ts`, which round-trip writes through real Postgres
|
||||
and assert `jsonb_typeof` — the assertion PGLite cannot make.
|
||||
|
||||
## Adding a new engine
|
||||
|
||||
1. Create `src/core/<name>-engine.ts` implementing `BrainEngine`
|
||||
|
||||
+9
-11
@@ -88,15 +88,14 @@ find /data/brain -name '*.md' \
|
||||
Some difference is normal (files added since last sync), but if page count is
|
||||
less than half the file count, sync is silently skipping pages.
|
||||
|
||||
**If page count is way too low:** The #1 cause is an unreachable direct
|
||||
connection on an IPv4-only host. GBrain uses the Transaction pooler (port 6543)
|
||||
for reads, but routes migrations, DDL, and sync transactions to a derived direct
|
||||
connection (`db.<ref>.supabase.co:5432`), which is IPv6-only.
|
||||
- On an IPv4-only host, reads work but sync transactions fail and silently skip
|
||||
pages.
|
||||
- Fix: set `GBRAIN_DIRECT_DATABASE_URL` to the **Session pooler** string (port
|
||||
5432 on the `pooler.supabase.com` host, IPv4), or enable Supabase's IPv4
|
||||
add-on. Then run `gbrain sync --full` to reimport everything.
|
||||
**If page count is way too low:** The #1 cause is the connection pooler bug.
|
||||
Check your `DATABASE_URL`:
|
||||
- If it contains `pooler.supabase.com:6543`, verify it's using **Session mode**,
|
||||
not Transaction mode.
|
||||
- Transaction mode breaks `engine.transaction()` and causes `.begin() is not a
|
||||
function` errors.
|
||||
- Fix: switch to Session mode pooler string, then run `gbrain sync --full`
|
||||
to reimport everything.
|
||||
|
||||
### 4b. Embed Check
|
||||
|
||||
@@ -143,8 +142,7 @@ gbrain search "<text from the correction>"
|
||||
- Is `gbrain sync --watch` still alive (if using watch mode)?
|
||||
- Run `gbrain config get sync.last_run` to see when sync last ran.
|
||||
- Run `gbrain sync --repo /data/brain` manually and check for errors.
|
||||
- If sync errors mention an unreachable host or connection timeout, the direct
|
||||
connection isn't reachable on IPv4 (see 4a above).
|
||||
- If you see `.begin() is not a function`, fix the pooler (see 4a above).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-37
@@ -39,11 +39,10 @@ gbrain migrate --to pglite # Postgres → PGLite (rare)
|
||||
|
||||
For shared / large / multi-machine deployments (a team or company brain with multiple users hitting one server over HTTP MCP with OAuth scoping per user), follow the dedicated walkthrough: **[Tutorial: set up GBrain as your company brain](tutorials/company-brain.md)**.
|
||||
|
||||
API keys live in `~/.gbrain/config.json` (file plane) or env vars (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `ZEROENTROPY_API_KEY`, `VOYAGE_API_KEY`, `ANTHROPIC_API_KEY`). Set via CLI:
|
||||
API keys live in `~/.gbrain/config.json` (file plane) or env vars (`OPENAI_API_KEY`, `ZEROENTROPY_API_KEY`, `VOYAGE_API_KEY`, `ANTHROPIC_API_KEY`). Set via CLI:
|
||||
|
||||
```bash
|
||||
gbrain config set zeroentropy_api_key sk-...
|
||||
gbrain config set openrouter_api_key sk-or-...
|
||||
gbrain config set anthropic_api_key sk-ant-...
|
||||
```
|
||||
|
||||
@@ -112,38 +111,3 @@ gbrain models doctor # 1-token probe per configured model
|
||||
```
|
||||
|
||||
If anything's yellow, `gbrain doctor` names the fix command in the message. Most issues are missing API keys or stale schema (`gbrain upgrade --force-schema`).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### PGLite crashes on macOS 26.x (Tahoe)
|
||||
|
||||
PGLite's embedded WASM engine is incompatible with macOS 26.x (Tahoe) on Apple Silicon. If `gbrain init --pglite` crashes during engine initialization, switch to native Homebrew PostgreSQL:
|
||||
|
||||
```bash
|
||||
# Install PostgreSQL + pgvector
|
||||
brew install postgresql@17
|
||||
brew services start postgresql@17
|
||||
createdb gbrain
|
||||
|
||||
# Build pgvector from source (required for vector search)
|
||||
cd /tmp && git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git
|
||||
cd pgvector && make && make install
|
||||
psql gbrain -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||
|
||||
# Point gbrain at your local Postgres
|
||||
cat > ~/.gbrain/config.json << 'EOF'
|
||||
{
|
||||
"engine": "postgres",
|
||||
"database_url": "postgresql://localhost:5432/gbrain",
|
||||
"schema_pack": "gbrain-base-v2"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Run migrations and verify
|
||||
gbrain apply-migrations --yes
|
||||
gbrain doctor
|
||||
```
|
||||
|
||||
All 102 migrations run on first try. Once `gbrain doctor` shows green, the brain works identically to PGLite — same commands, same skills, same data model. The only difference is the storage backend.
|
||||
|
||||
> **Note:** This workaround is temporary. When the upstream WASM runtime fix ships (likely via a Bun update), `--pglite` will work on Tahoe again.
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
# Releasing & contributing (gbrain)
|
||||
|
||||
The full release + contributor process. CLAUDE.md keeps the ship-critical IRON RULES
|
||||
inline (the Version-locations table, branch=workspace, post-ship `/document-release`,
|
||||
the Privacy + Responsible-disclosure rules, PR-title-version-first, never-hand-roll-ship)
|
||||
and points here for everything else. **Before any ship, read this in full. Use `/ship` —
|
||||
never hand-roll a release.**
|
||||
|
||||
## Pre-ship requirements
|
||||
|
||||
Before shipping (/ship) or reviewing (/review), always run the full test suite.
|
||||
Two equivalent paths:
|
||||
|
||||
**Path A — local CI gate (recommended, v0.23.1+):**
|
||||
- `bun run ci:local` runs the entire stack inside Docker: gitleaks (host),
|
||||
guards + typecheck, then 4-shard parallel unit + E2E against four pgvector
|
||||
containers plus a transaction-mode PgBouncer service (unit phase keeps
|
||||
`DATABASE_URL` unset; `--no-shard` for the legacy sequential flow). Stronger
|
||||
than PR CI's 2-file Tier 1 set; closer to what nightly Tier 1 catches. Spins
|
||||
up + tears down postgres automatically via `docker-compose.ci.yml`. Override
|
||||
the host port with `GBRAIN_CI_PG_PORT=5435 bun run ci:local` if 5434 collides.
|
||||
- `bun run ci:local:diff` runs only the E2E files matched by the diff selector
|
||||
(`scripts/select-e2e.ts`), falling back to ALL E2E files on unmapped src/
|
||||
paths or schema/skills/package.json changes. Fast iteration during a focused
|
||||
branch.
|
||||
|
||||
**Path B — manual lifecycle (still supported):**
|
||||
- `bun test` — unit tests (no database required)
|
||||
- Follow the "E2E test DB lifecycle" steps above to spin up the test DB,
|
||||
run `bun run test:e2e`, then tear it down.
|
||||
|
||||
Both must pass. Do not ship with failing E2E tests. Do not skip E2E tests.
|
||||
|
||||
**Always run typecheck before pushing.** `bun test` (the bun runner)
|
||||
skips TypeScript type checking — it only enforces runtime behavior.
|
||||
Three ways to actually gate on types:
|
||||
|
||||
1. `bun run test` (npm script in `package.json`) — includes `bun run typecheck`
|
||||
plus the four shell pre-checks (`check-jsonb-pattern.sh`,
|
||||
`check-progress-to-stdout.sh`, `check-trailing-newline.sh`,
|
||||
`check-wasm-embedded.sh`) before the runner. Use this mid-branch.
|
||||
2. `bun run typecheck` — `tsc --noEmit` standalone. Fast (~5s on this repo).
|
||||
3. `bun run ci:local` — the full local CI gate from Path A.
|
||||
|
||||
The trap is: writing a new test, running `bun test test/foo.test.ts`,
|
||||
seeing it pass, pushing — and CI's separate typecheck stage rejects an
|
||||
invalid type literal that the runner accepted. Caught one of these
|
||||
shipping the v0.23.2 round-trip E2E (`type: 'reflection'` is not a
|
||||
member of `PageType`). Run `bun run typecheck` once before push, even
|
||||
when only test files changed.
|
||||
|
||||
|
||||
## CHANGELOG + VERSION are branch-scoped
|
||||
|
||||
**VERSION and CHANGELOG describe what THIS branch adds vs master, not how we got
|
||||
here.** Every feature branch that ships gets its own version bump and CHANGELOG
|
||||
entry. The entry is product release notes for users; it is not a log of internal
|
||||
decisions, review rounds, or codex findings.
|
||||
|
||||
**Write the CHANGELOG entry at /ship time, not during development.** Mid-branch
|
||||
iterations, review rounds (CEO/Eng/Codex/DX), and implementation detours belong
|
||||
in the plan file at `~/.claude/plans/`, not in the CHANGELOG. One unified entry
|
||||
per branch, covering what the branch added vs the base branch.
|
||||
|
||||
**Never edit a CHANGELOG entry that already landed on master.** If master has
|
||||
v0.18.2 and your branch adds features, bump to the next version (v0.19.0, not
|
||||
editing master's v0.18.2). When merging master into your branch, master may
|
||||
bring new CHANGELOG entries above yours — push your entry above master's
|
||||
latest and verify:
|
||||
|
||||
- Does CHANGELOG have your branch's own entry separate from master's entries?
|
||||
- Is VERSION higher than master's VERSION?
|
||||
- Is your entry the topmost `## [X.Y.Z]` entry?
|
||||
- `grep "^## \[" CHANGELOG.md` shows a contiguous version sequence?
|
||||
|
||||
If any answer is no, fix it before continuing.
|
||||
|
||||
**CHANGELOG is for users, not contributors.** Write like product release notes:
|
||||
|
||||
- Lead with what the user can now **do** that they couldn't before. Sell the capability.
|
||||
- Plain language, not implementation details. "You can now..." not "Refactored the..."
|
||||
- **Never mention internal artifacts**: plan file IDs, decision tags (D-CX-#, F-ENG-#),
|
||||
review rounds, codex findings, subcontractor credits. These are invisible to users.
|
||||
- Put contributor-facing changes in a separate `### For contributors` section at the bottom.
|
||||
- Every entry should make someone think "oh nice, I want to try that."
|
||||
|
||||
**What to omit:**
|
||||
- "Codex caught X that the CEO review missed" — private process detail.
|
||||
- "D-CX-3 split errors/warnings" — tag is meaningless to users; name the feature instead.
|
||||
- "Fix-wave PR #N supersedes #M" — supersede chains belong in PR bodies, not release notes.
|
||||
- "215 new cases, 3 decisions applied, 7 reviews cleared" — these are planning-mode metrics.
|
||||
|
||||
**What to keep:**
|
||||
- The user-facing change: what commands exist now, what flag was added, what behavior fixed.
|
||||
- Numbers that mean something to the user: TTHW, commands that timed out before, detection counts.
|
||||
- Upgrade instructions: `gbrain upgrade` + any manual step if needed.
|
||||
- Credit to external contributors when a community PR was incorporated.
|
||||
|
||||
## CHANGELOG voice + release-summary format
|
||||
|
||||
**IRON RULE: the CHANGELOG describes what the user gets, not how the work
|
||||
happened.** Nobody reading release notes cares that codex caught a bug, that
|
||||
the plan went through CEO + eng review, that the migration was originally
|
||||
numbered v68 and renumbered to v79 during master merge, or that two
|
||||
review rounds caught architectural mistakes. The reader cares what
|
||||
`gbrain brainstorm` does and how to use it. If a fact only exists because
|
||||
of the development process, it does NOT belong in the CHANGELOG.
|
||||
|
||||
**Specifically forbidden in CHANGELOG entries:**
|
||||
|
||||
- Any mention of review processes (CEO review, eng review, codex review,
|
||||
plan-eng-review, outside voice, adversarial review, autoplan, /review).
|
||||
- "What we caught and fixed before merging" sections. Bugs found pre-merge
|
||||
are not changes — they're things that didn't ship.
|
||||
- Plan file references, plan IDs, plan decision tags (D1, D14, D-CDX-3).
|
||||
- Migration version drama ("originally v68", "renumbered to v77", "claimed
|
||||
by parallel waves") — just say "Migration v79 adds X." If the user
|
||||
cares about migration ordering, they read the diff.
|
||||
- Round counts, finding counts, decision counts ("25 findings across 2
|
||||
rounds", "8 architectural decisions", "5/6 expansions accepted").
|
||||
- Names of internal collaborators ("codex caught", "the reviewer flagged",
|
||||
"Claude noticed").
|
||||
- "Plan + reviews" summary bullets. The plan lives in `~/.claude/plans/`;
|
||||
if a future reader wants the backstory they can grep there.
|
||||
- Any wording that frames a shipped feature as a *recovery* from a planning
|
||||
mistake ("the first plan was wrong", "we corrected the approach", "the
|
||||
shipped version supersedes the original design").
|
||||
|
||||
**Smell test:** read the entry as a stranger who has never touched gbrain.
|
||||
If any sentence makes them think "why are you telling me this?", cut it.
|
||||
Every sentence in the release-summary AND in the itemized changes must
|
||||
answer one of three questions: *What can I now do? How do I use it? What
|
||||
should I watch for after I upgrade?*
|
||||
|
||||
Every version entry in `CHANGELOG.md` MUST start with a release-summary section in
|
||||
the GStack/Garry voice — one viewport's worth of prose + tables that lands like a
|
||||
verdict, not marketing. The itemized changelog (subsections, bullets, files) goes
|
||||
BELOW that summary, separated by a `### Itemized changes` header.
|
||||
|
||||
The release-summary section gets read by humans, by the auto-update agent, and by
|
||||
anyone deciding whether to upgrade. The itemized list is for agents that need to
|
||||
know exactly what changed.
|
||||
|
||||
### Release-summary template
|
||||
|
||||
**Iron rule: lead ELI10, get precise after.** The first ~150 words of every entry
|
||||
must be readable by someone who does NOT know gbrain's internals. No file paths,
|
||||
no function names, no internal constants, no acronyms (no "RRF", no "knobsHash",
|
||||
no "MODE_BUNDLES", no "CDX-4"), no jargon that requires reading the codebase to
|
||||
parse. Lead with the user-visible behavior change, in everyday English, like
|
||||
you're explaining it to a smart engineer who has never opened the repo.
|
||||
|
||||
THEN, once the reader knows what shipped and why they'd care, drill into the
|
||||
precise details: real file paths, real function names, real config keys, real
|
||||
numbers. The precision part is required (the entry is also the technical record
|
||||
of what changed), but it lives AFTER the plain-English lead, never before it.
|
||||
|
||||
The shape:
|
||||
|
||||
1. **One-line bold headline.** What changed for the user, in human English. No
|
||||
jargon. No internal terms. Example good: "Your search stops boosting weak
|
||||
pages just because they have a lot of links pointing at them." Example bad:
|
||||
"PostFusionOpts gains floorRatio; KNOBS_HASH_VERSION bumped 2→3."
|
||||
2. **Plain-English opener** (~3-5 sentences). Describe the problem this fixes in
|
||||
everyday terms. Pretend the reader has a brain full of meeting notes and
|
||||
people pages and wants to know if this release helps them. Concrete example
|
||||
beats abstract description.
|
||||
3. **A "How to turn it on" or "How to use it" section** with paste-ready
|
||||
commands. Real flags, real config keys. This is where precision starts.
|
||||
4. **A "What you'd see in a concrete example" or "The X numbers that matter"
|
||||
section** with a table. Use everyday-language column headers ("Page",
|
||||
"Match quality", "Has many backlinks?") even when the underlying mechanism
|
||||
is technical. The table teaches what the feature does without requiring the
|
||||
reader to understand how.
|
||||
5. **A "What's safe to know about" or "Things to watch" section** for caveats,
|
||||
side effects, cache invalidation, mid-deploy notes. Still in plain language.
|
||||
6. **A "What we caught and fixed before merging" section** if the work went
|
||||
through review (CEO/eng/codex/outside-voice). Translate review findings into
|
||||
plain English. "We caught a stale-cache bug" beats "knobsHash() did not
|
||||
include floorRatio in the v=2 hash input."
|
||||
7. **`### Itemized changes`** (precision lives here). File paths, function
|
||||
names, types, constants, line numbers. This section is for engineers who
|
||||
need to know exactly what moved.
|
||||
|
||||
Voice rules (apply throughout):
|
||||
- No em dashes (use commas, periods, "...").
|
||||
- No AI vocabulary (delve, robust, comprehensive, nuanced, fundamental, etc.) or
|
||||
banned phrases ("here's the kicker", "the bottom line", etc.).
|
||||
- Real numbers, real file names, real commands AFTER the ELI10 lead. Not "fast"
|
||||
but "~30s on 30K pages." In the ELI10 lead, "fast enough that you won't
|
||||
notice" or "~30 seconds even on a big brain."
|
||||
- Short paragraphs, mix one-sentence punches with 2-3 sentence runs.
|
||||
- Connect to user outcomes: "the agent does ~3x less reading" beats "improved
|
||||
precision."
|
||||
- Be direct about quality. "Well-designed" or "this is a mess." No dancing.
|
||||
|
||||
**The smell test:** if someone who has never opened gbrain reads the first 150
|
||||
words and walks away knowing what shipped and whether they care, the entry
|
||||
passes. If they need to grep the codebase to follow along, rewrite the lead.
|
||||
|
||||
**Canonical examples in this CHANGELOG:** v0.35.6.0 (floor-ratio gate, written
|
||||
ELI10-lead-first), v0.34.4.0 (embed stale fix wave). Use those shapes when in
|
||||
doubt. Avoid the shape of entries that lead with internal constants or release
|
||||
mechanics; those exist in older history but should not be the model for new
|
||||
work.
|
||||
|
||||
Source material to pull from:
|
||||
- CHANGELOG.md previous entry for prior context
|
||||
- Latest `gbrain-evals/docs/benchmarks/[latest].md` for headline numbers (sibling repo)
|
||||
- Recent commits (`git log <prev-version>..HEAD --oneline`) for what shipped
|
||||
- Don't make up numbers. If a metric isn't in a benchmark or production data, don't
|
||||
include it. Say "no measurement yet" if asked.
|
||||
|
||||
Target length: ~250-350 words for the summary. Should render as one viewport.
|
||||
|
||||
### "To take advantage of v[version]" block (required, v0.13+)
|
||||
|
||||
After the release-summary and BEFORE `### Itemized changes`, every `## [X.Y.Z]`
|
||||
entry MUST include a human-readable self-repair block under the heading
|
||||
`## To take advantage of v[version]`.
|
||||
|
||||
Why: `gbrain upgrade` runs `gbrain post-upgrade` which runs `gbrain apply-migrations`.
|
||||
This chain has a known weak link — `upgrade.ts` catches post-upgrade failures as
|
||||
best-effort (so the binary still works). When that chain silently fails, users end
|
||||
up with half-upgraded brains. The self-repair block gives them a paste-ready
|
||||
recovery path; the v0.13+ `~/.gbrain/upgrade-errors.jsonl` trail + `gbrain doctor`
|
||||
integration close the loop.
|
||||
|
||||
Template (adapt the verify commands per release):
|
||||
|
||||
```markdown
|
||||
## To take advantage of v[version]
|
||||
|
||||
`gbrain upgrade` should do this automatically. If it didn't, or if `gbrain doctor`
|
||||
warns about a partial migration:
|
||||
|
||||
1. **Run the orchestrator manually:**
|
||||
```bash
|
||||
gbrain apply-migrations --yes
|
||||
```
|
||||
2. **Your agent reads `skills/migrations/v[version].md` the next time you interact with it.**
|
||||
[One sentence on whether headless agents need manual action, or whether the
|
||||
orchestrator already handled the mechanical side.]
|
||||
3. **Verify the outcome:**
|
||||
```bash
|
||||
[release-specific verify commands, e.g. `gbrain graph ... --depth 2`]
|
||||
gbrain stats
|
||||
```
|
||||
4. **If any step fails or the numbers look wrong,** please file an issue:
|
||||
https://github.com/garrytan/gbrain/issues with:
|
||||
- output of `gbrain doctor`
|
||||
- contents of `~/.gbrain/upgrade-errors.jsonl` if it exists
|
||||
- which step broke
|
||||
|
||||
This feedback loop is how the gbrain maintainers find fragile upgrade paths. Thank you.
|
||||
```
|
||||
|
||||
**Skip this block** for patches that are pure bug fixes with zero user-facing action
|
||||
(rare). If the release has a schema migration, data backfill, or new feature the
|
||||
user needs to verify, the block is required.
|
||||
|
||||
The v0.13.0 entry in CHANGELOG.md is the canonical example.
|
||||
|
||||
### Itemized changes (the existing rules)
|
||||
|
||||
Below the release summary, write `### Itemized changes` and continue with the
|
||||
detailed subsections (Knowledge Graph Layer, Schema migrations, Security hardening,
|
||||
Tests, etc.). Same rules as before:
|
||||
|
||||
- Lead with what the user can now DO that they couldn't before
|
||||
- Frame as benefits and capabilities, not files changed or code written
|
||||
- Make the user think "hell yeah, I want that"
|
||||
- Bad: "Added GBRAIN_VERIFY.md installation verification runbook"
|
||||
- Good: "Your agent now verifies the entire GBrain installation end-to-end, catching
|
||||
silent sync failures and stale embeddings before they bite you"
|
||||
- Bad: "Setup skill Phase H and Phase I added"
|
||||
- Good: "New installs automatically set up live sync so your brain never falls behind"
|
||||
- **Always credit community contributions.** When a CHANGELOG entry includes work from
|
||||
a community PR, name the contributor with `Contributed by @username`. Contributors
|
||||
did real work. Thank them publicly every time, no exceptions.
|
||||
|
||||
### Reference: v0.12.0 entry as canonical example
|
||||
|
||||
The v0.12.0 entry in CHANGELOG.md is the canonical example of the format. Match its
|
||||
structure for every future version: bold headline, lead paragraph, "numbers that
|
||||
matter" with BrainBench-style before/after table, "what this means" closer, then
|
||||
`### Itemized changes` with the detailed sections below.
|
||||
|
||||
## Version migrations
|
||||
|
||||
Create a migration file at `skills/migrations/v[version].md` when a release
|
||||
includes changes that existing users need to act on. The auto-update agent
|
||||
reads these files post-upgrade (Section 17, Step 4) and executes them.
|
||||
|
||||
**You need a migration file when:**
|
||||
- New setup step that existing installs don't have (e.g., v0.5.0 added live sync,
|
||||
existing users need to set it up, not just new installs)
|
||||
- New SKILLPACK section with a MUST ADD setup requirement
|
||||
- Schema changes that require `gbrain init` or manual SQL
|
||||
- Changed defaults that affect existing behavior
|
||||
- Deprecated commands or flags that need replacement
|
||||
- New verification steps that should run on existing installs
|
||||
- New cron jobs or background processes that should be registered
|
||||
|
||||
**You do NOT need a migration file when:**
|
||||
- Bug fixes with no behavior changes
|
||||
- Documentation-only improvements (the agent re-reads docs automatically)
|
||||
- New optional features that don't affect existing setups
|
||||
- Performance improvements that are transparent
|
||||
|
||||
**The key test:** if an existing user upgrades and does nothing else, will their
|
||||
brain work worse than before? If yes, migration file. If no, skip it.
|
||||
|
||||
Write migration files as agent instructions, not technical notes. Tell the agent
|
||||
what to do, step by step, with exact commands. See `skills/migrations/v0.5.0.md`
|
||||
for the pattern.
|
||||
|
||||
## Migration is canonical, not advisory
|
||||
|
||||
GBrain's job is to deliver a canonical, working setup to every user on upgrade.
|
||||
Anything that looks like a "host-repo change" — AGENTS.md, cron manifests,
|
||||
launchctl units, config files outside `~/.gbrain/` — is a GBrain migration
|
||||
step, not a nudge we leave for the host-repo maintainer. Migrations edit host
|
||||
files (with backups) to make the canonical setup real. Exceptions: changes
|
||||
that require human judgment (content edits, renames that break semantics,
|
||||
host-specific handler registration where shell-exec would be an RCE surface).
|
||||
Everything mechanical ships in the migration.
|
||||
|
||||
**Test:** if shipping a feature requires a sentence that starts with "in
|
||||
your AGENTS.md, add…" or "in your cron/jobs.json, rewrite…", the migration
|
||||
orchestrator should be doing that edit, not the user.
|
||||
|
||||
**The exception is host-specific code.** For custom Minion handlers
|
||||
(host-specific integrations like inbox sweeps or third-party API scanners), shipping them as a
|
||||
data file the worker would exec is an RCE surface. Those get registered in
|
||||
the host's own repo via the plugin contract (`docs/guides/plugin-handlers.md`);
|
||||
the migration orchestrator emits a structured TODO to
|
||||
`~/.gbrain/migrations/pending-host-work.jsonl` + the host agent walks the
|
||||
TODOs using `skills/migrations/v0.11.0.md` — stays host-agnostic, still
|
||||
canonical.
|
||||
|
||||
|
||||
## Schema state tracking
|
||||
|
||||
`~/.gbrain/update-state.json` tracks which recommended schema directories the user
|
||||
adopted, declined, or added custom. The auto-update agent (SKILLPACK Section 17)
|
||||
reads this during upgrades to suggest new schema additions without re-suggesting
|
||||
things the user already declined. The setup skill writes the initial state during
|
||||
Phase C/E. Never modify a user's custom directories or re-suggest declined ones.
|
||||
|
||||
## GitHub Actions SHA maintenance
|
||||
|
||||
All GitHub Actions in `.github/workflows/` are pinned to commit SHAs. Before shipping
|
||||
(`/ship`) or reviewing (`/review`), check for stale pins and update them:
|
||||
|
||||
```bash
|
||||
for action in actions/checkout oven-sh/setup-bun actions/upload-artifact actions/download-artifact softprops/action-gh-release gitleaks/gitleaks-action; do
|
||||
tag=$(grep -r "$action@" .github/workflows/ | head -1 | grep -o '#.*' | tr -d '# ')
|
||||
[ -n "$tag" ] && echo "$action@$tag: $(gh api repos/$action/git/ref/tags/$tag --jq .object.sha 2>/dev/null)"
|
||||
done
|
||||
```
|
||||
|
||||
If any SHA differs from what's in the workflow files, update the pin and version comment.
|
||||
|
||||
|
||||
## PR descriptions cover the whole branch
|
||||
|
||||
Pull request titles and bodies must describe **everything in the PR diff against the
|
||||
base branch**, not just the most recent commit you made. When you open or update a
|
||||
PR, walk the full commit range with `git log --oneline <base>..<head>` and write the
|
||||
body to cover all of it. Group by feature area (schema, code, tests, docs) — not
|
||||
chronologically by commit.
|
||||
|
||||
This matters because reviewers read the PR body to understand what's shipping. If
|
||||
the body only covers your last commit, they miss everything else and can't review
|
||||
properly. A 7-commit PR with a body that describes commit 7 is worse than no body
|
||||
at all — it actively misleads.
|
||||
|
||||
When in doubt, run `gh pr view <N> --json commits --jq '[.commits[].messageHeadline]'`
|
||||
to see what's actually in the PR before writing the body.
|
||||
|
||||
## Community PR wave process
|
||||
|
||||
Never merge external PRs directly into master. Instead, use the "fix wave" workflow:
|
||||
|
||||
1. **Categorize** — group PRs by theme (bug fixes, features, infra, docs)
|
||||
2. **Deduplicate** — if two PRs fix the same thing, pick the one that changes fewer
|
||||
lines. Close the other with a note pointing to the winner.
|
||||
3. **Collector branch** — create a feature branch (e.g. `garrytan/fix-wave-N`), cherry-pick
|
||||
or manually re-implement the best fixes from each PR. Do NOT merge PR branches directly —
|
||||
read the diff, understand the fix, and write it yourself if needed.
|
||||
4. **Test the wave** — verify with `bun test && bun run test:e2e` (full E2E lifecycle).
|
||||
Every fix in the wave must have test coverage.
|
||||
5. **Close with context** — every closed PR gets a comment explaining why and what (if
|
||||
anything) supersedes it. Contributors did real work; respect that with clear communication
|
||||
and thank them.
|
||||
6. **Ship as one PR** — single PR to master with all attributions preserved via
|
||||
`Co-Authored-By:` trailers. Include a summary of what merged and what closed.
|
||||
|
||||
**Community PR guardrails:**
|
||||
- Always AskUserQuestion before accepting commits that touch voice, tone, or
|
||||
promotional material (README intro, CHANGELOG voice, skill templates).
|
||||
- Never auto-merge PRs that remove YC references or "neutralize" the founder perspective.
|
||||
- Preserve contributor attribution in commit messages.
|
||||
|
||||
## Checking out PRs from garrytan-agents
|
||||
|
||||
`garrytan-agents` is the AI-authored PR account and is NOT a collaborator on
|
||||
this repo. Its PRs live in a fork, so GitHub Actions triggered by
|
||||
`pull_request` events on those PRs do not receive base-repo secrets. Any CI
|
||||
job that needs `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or similar will fail
|
||||
with empty-env auth errors, regardless of what's set on the base repo. This
|
||||
is a GitHub security default, not a config bug.
|
||||
|
||||
When the user says "check out <PR link>" and the PR is from `garrytan-agents`
|
||||
(or any other non-collaborator fork), move the branch into the base repo
|
||||
before running CI:
|
||||
|
||||
1. `gh pr checkout <N>` — pull down the fork's branch. Note the PR number and
|
||||
head branch name (`gh pr view <N> --json headRefName --jq .headRefName`).
|
||||
2. `git push origin HEAD:<branch-name>` — push the same branch to the base
|
||||
repo (origin points at `garrytan/gbrain`, not the fork). This is the move
|
||||
that gives CI access to secrets.
|
||||
3. `gh pr close <N> --comment "moving to base-repo branch for secret access"`
|
||||
— close the fork PR so the queue stays clean.
|
||||
4. `gh pr create --base master --head <branch-name>` — open the replacement
|
||||
PR from the base-repo branch. **Preserve the original PR's title and body
|
||||
verbatim** (`gh pr view <N> --json title,body`); contributor attribution
|
||||
moves to a `Co-Authored-By:` trailer if needed.
|
||||
|
||||
Why this over alternatives: adding `garrytan-agents` as a collaborator, or
|
||||
flipping the repo-wide "send secrets to fork PRs" toggle, both broaden
|
||||
secret distribution to every fork PR from that account or any fork. Moving
|
||||
the branch keeps secret scope tight to just the one PR being shipped.
|
||||
|
||||
-333
@@ -1,333 +0,0 @@
|
||||
# Testing (gbrain repo)
|
||||
|
||||
On-demand reference (see CLAUDE.md Reference map). Current behavior + invariants
|
||||
only.
|
||||
|
||||
`test/e2e/serve-http-oauth.test.ts` additionally pins confidential POST/Basic revocation, public-client SDK fallthrough, malformed/mixed authentication rejection, cross-client isolation, unknown-token opacity, metadata auth methods, no-store responses, strict post-revoke `401`, and retryable backend `503` semantics.
|
||||
|
||||
### Test command tiers
|
||||
|
||||
Seven test command tiers, each with a clear scope:
|
||||
|
||||
| Command | What it runs | Wallclock | When to use |
|
||||
|---|---|---|---|
|
||||
| `bun run test` | Parallel unit-test fast loop. 8-shard fan-out via `scripts/run-unit-parallel.sh`, then a serial pass over `*.serial.test.ts`. Excludes `*.slow.test.ts` and `test/e2e/*`. No pre-checks, no typecheck. | ~85s on a Mac dev box (3650+ tests) | Inner edit loop. Default. |
|
||||
| `bun run verify` | CI's authoritative pre-test gate set, fanned out in parallel by `scripts/run-verify-parallel.sh`: the full `check:*` battery (~30 checks — privacy, jsonb, progress, source-id, test-isolation, wasm, …) plus `bun run typecheck`. The `CHECKS` array in that script is the single source of truth — CI literally calls `bun run verify` in a dedicated job. | ~16s (parallel; typecheck dominates) | Before pushing; before `/ship`. |
|
||||
| `bun run test:full` | `verify && bun run test && bun run test:slow && [smart e2e]`. The local equivalent of "everything CI runs." Smart e2e: runs e2e only when `DATABASE_URL` is set; else loud skip notice to stderr. | ~3-5min depending on slow + e2e | Pre-merge sanity, before opening a PR. |
|
||||
| `bun run test:slow` | Just the `*.slow.test.ts` set (intentional cold-path correctness checks). | seconds-to-minutes | When touching slow-path code. |
|
||||
| `bun run test:serial` | Just the `*.serial.test.ts` set (cross-file-contention quarantine; one bun process per file for true module-registry isolation). | ~1s per quarantined file | Debugging a specific quarantined file. |
|
||||
| `bun run test:e2e` | Real Postgres E2E. Requires Docker + `DATABASE_URL`. Sequential. | ~5-10min | Pre-ship; nightly. |
|
||||
| `bun run check:all` | The historical pre-check scripts (22, chained sequentially in package.json). Overlaps `verify` heavily but is NOT a superset — `verify`'s `CHECKS` array in `scripts/run-verify-parallel.sh` (~30 entries incl. typecheck) is the authoritative gate; `check:all` keeps a few local-only extras (trailing-newline, exports-count, no-legacy-getconnection). | ~10s | Local-only sweep for the extras. |
|
||||
|
||||
### Shell dispatch and Windows
|
||||
|
||||
All four of `test`, `verify`, `ci:local` and `test:e2e` hand off to shell scripts
|
||||
under `scripts/`, so every `check:*` entry in `package.json` invokes its script as
|
||||
`bash scripts/<name>.sh` instead of relying on the shebang — bun on Windows cannot
|
||||
exec a `.sh` directly. Add a new shell-script check with that same prefix. The
|
||||
`scripts/*.ts` entries run under bun and take no prefix.
|
||||
|
||||
The scripts must also be on disk with Unix line endings. A strict bash (WSL, Linux
|
||||
CI, macOS) rejects CRLF and dies on the script's first meaningful line; the Cygwin
|
||||
bash that ships with Git for Windows tolerates it, so a green local run is not by
|
||||
itself evidence that a script is CRLF-clean.
|
||||
The root `.gitattributes` pins `*.sh text eol=lf`, which overrides the
|
||||
`core.autocrlf=true` default that Git for Windows installs. Working copies cloned
|
||||
before that pin need a one-time `git rm --cached -r . -q && git reset --hard` to
|
||||
pick it up; see the Windows section of `CONTRIBUTING.md`.
|
||||
|
||||
Wallclock figures in the table above are from a Mac dev box. Windows is
|
||||
substantially slower because each check pays full process-creation cost, and three
|
||||
tree-walking checks (`check:privacy`, `check:test-names`, `check:test-isolation`)
|
||||
plus `typecheck` can exceed the 120s per-check cap in `run-verify-parallel.sh`
|
||||
there even though they pass on Linux and macOS.
|
||||
|
||||
### CI vs local: intentionally divergent file sets
|
||||
|
||||
- **CI matrix** (`.github/workflows/test.yml`) runs `scripts/test-shard.sh` across 10 matrix shards partitioned by weight-aware LPT bin-packing (`scripts/sharding.ts`) and INCLUDES `*.slow.test.ts` (the two outlier slow files run as dedicated jobs alongside the matrix). CI EXCLUDES `*.serial.test.ts` from the shards and runs them in a dedicated job via `bun run test:serial`, one bun process per file — keeping serial files out of the shard processes is what preserves the `mock.module` quarantine (a top-level mock in one file leaks into every other file sharing its process). `bun run verify` gets its own job too. CI is the ground truth for "did everything pass."
|
||||
- **Local fast loop** (`scripts/run-unit-shard.sh` via the parallel wrapper) uses round-robin-by-index sharding and EXCLUDES `*.slow.test.ts` AND `*.serial.test.ts`. Local trades coverage for inner-loop speed; CI catches what local skips.
|
||||
|
||||
This divergence is intentional. Don't try to make them equal — the two scripts deliberately solve different problems. The regression test at `test/scripts/run-unit-shard.test.ts` pins what the local fast loop should and shouldn't include.
|
||||
|
||||
### Failure-first logging
|
||||
|
||||
When `bun run test` finds any failure, the wrapper:
|
||||
|
||||
1. Writes failure blocks (each prefixed with `--- shard N: <test name> ---`) to `.context/test-failures.log` (workspace-local, gitignored). On systems without a writable `.context/`, falls back to `/tmp/gbrain-test-failures.log`.
|
||||
2. Prints a loud stderr banner with the absolute log path, plus the last 30 lines of the failure log inlined. Banner survives `| head` / `| tail` / agent-side log truncation.
|
||||
3. Writes a one-line-per-shard summary to `.context/test-summary.txt` (`shard N/M: pass=X fail=Y skip=Z rc=W`).
|
||||
4. Exits non-zero. Empty failure log + non-zero exit = infrastructure problem (wedged shard, killed child); the banner says so.
|
||||
|
||||
If a shard wedges (per-shard `GBRAIN_TEST_SHARD_TIMEOUT` cap, default 600s), the wrapper writes `--- shard N: WEDGED after ${SHARD_TIMEOUT}s ---` to the failure log, includes the last 50 lines of the shard log, and proceeds with other shards' results.
|
||||
|
||||
### File taxonomy
|
||||
|
||||
- `*.test.ts` → fast loop (parallel 8-shard fan-out).
|
||||
- `*.slow.test.ts` → run via `bun run test:slow` only (intentional cold-path tests; would dominate the fast loop's wallclock).
|
||||
- `*.serial.test.ts` → run via `bun run test:serial` after the parallel pass completes; one bun process per file (`--max-concurrency=1` within a shared process is not enough — the module registry still leaks `mock.module`). Quarantine for tests that share file-wide state and race when run alongside other files in the same `bun test` process. Several dozen files, discovered by the `*.serial.test.ts` glob — no list to maintain. Typical residents: `mock.module(...)` users (top-level mocks leak across files in a shard process, e.g. `test/embed.serial.test.ts`), env-coupled files (e.g. `test/brain-registry.serial.test.ts`), and process-lifecycle suites that assert on `process.exitCode` (e.g. `test/pglite-engine-disconnect.serial.test.ts`). **Do not put the parallelism back on a serial file unless you've fixed the contention root cause** (it just re-introduces the flake).
|
||||
- `test/e2e/*.test.ts` → real-Postgres E2E. Skipped when `DATABASE_URL` is unset.
|
||||
- `tests/heavy/*.sh` → ops-shape shell scripts. Cost minutes per run; NOT in default `bun test`. Run via `bun run test:heavy` or scheduled nightly via `.github/workflows/heavy-tests.yml`. Examples: pg_upgrade matrix (boot legacy brain → walk to head), RSS budget gate (measure peak worker RSS vs committed baseline), read-latency-under-sync (p50/p95/p99 under concurrent writer load), sync lock regression (N concurrent syncs assert 1 winner + N-1 lock-busy + zero leaked `gbrain_cycle_locks` rows). See `tests/heavy/README.md` for when to add a script here vs `*.slow.test.ts`. Files prefixed with `_` (e.g. `tests/heavy/_build_legacy_fixtures.sh`) are helpers/libs invoked by sibling tests — the runner skips them.
|
||||
- `test/fuzz/*.test.ts` → property-based fuzz harness. Pure-validator targets in `pure-validators.test.ts` are guarded by `scripts/check-fuzz-purity.sh` (in `bun run verify`), which `bun build --target=bun` bundles each target and greps the resulting bundle for banned transitive imports (`node:fs`, `node:child_process`, engine modules). Anything that fails the guard moves to `mixed-validators.test.ts` (still property-tested, but no purity guarantee) or `filesystem-validators.test.ts` (fs-backed, uses temp dirs). Fuzz tests run in the default `bun test` loop because they're fast (~3s for ~12 properties × 1000 runs each).
|
||||
|
||||
### Test-isolation lint and helpers
|
||||
|
||||
The cross-file flake class is enforced statically by `scripts/check-test-isolation.sh`, wired into `bun run verify` and `bun run check:all`. Rules (non-serial unit files only; `*.serial.test.ts` and `test/e2e/*` are skipped):
|
||||
|
||||
| Rule | What it bans | Fix |
|
||||
|---|---|---|
|
||||
| **R1** | `process.env.X = ...`, bracket assignment, `delete process.env.X`, `Object.assign(process.env, ...)`, `Reflect.set(process.env, ...)` | Use `withEnv()` from `test/helpers/with-env.ts`, OR rename file to `*.serial.test.ts` |
|
||||
| **R2** | `mock.module(...)` anywhere in the file | Rename file to `*.serial.test.ts` (no DI on production code for testability) |
|
||||
| **R3** | `new PGLiteEngine(` outside ~50 lines after a `beforeAll(` line | Use the canonical block (below) inside `beforeAll(` |
|
||||
| **R4** | Files creating `new PGLiteEngine(` without `engine.disconnect(` inside an `afterAll(` block | Add `afterAll(() => engine.disconnect())` |
|
||||
|
||||
Files that violated these rules at the isolation-lint baseline are listed in `scripts/check-test-isolation.allowlist`. **The allow-list MUST shrink over time** — never add new entries.
|
||||
|
||||
#### Canonical PGLite block (R3 + R4 compliant)
|
||||
|
||||
Every test file that needs a PGLite engine should use this exact pattern:
|
||||
|
||||
```ts
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
|
||||
beforeAll(async () => {
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await engine.disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetPgliteState(engine);
|
||||
});
|
||||
```
|
||||
|
||||
Why this exact shape: `beforeAll` creates a single engine per file (PGLite WASM cold-start + initSchema is ~20s); `beforeEach` truncates user data via `resetPgliteState` ("two orders of magnitude faster" than fresh-engine-per-test); `afterAll` disconnects so the engine doesn't leak across file boundaries within a shard process.
|
||||
|
||||
#### `withEnv` pattern (R1 fix)
|
||||
|
||||
```ts
|
||||
import { withEnv } from './helpers/with-env.ts';
|
||||
|
||||
test('reads OPENAI_API_KEY', async () => {
|
||||
await withEnv({ OPENAI_API_KEY: 'sk-test' }, async () => {
|
||||
expect(loadConfig().openai_key).toBe('sk-test');
|
||||
});
|
||||
});
|
||||
|
||||
// Delete a var (override is undefined):
|
||||
await withEnv({ GBRAIN_HOME: undefined }, fn);
|
||||
|
||||
// Multiple keys:
|
||||
await withEnv({ A: '1', B: '2', C: undefined }, fn);
|
||||
```
|
||||
|
||||
`withEnv` saves the prior value of every key it touches and restores via try/finally — including when the callback throws. **It is cross-test safe but NOT intra-file concurrent-safe.** `process.env` is process-global; two `test.concurrent()` calls in the same file both touching the same key will race. Files using `withEnv` stay outside the `test.concurrent()` codemod's eligibility filter.
|
||||
|
||||
#### When to quarantine instead of fix
|
||||
|
||||
Rename to `*.serial.test.ts` when:
|
||||
- The file uses `mock.module(...)` (R2 — there's no clean fix without changing production code).
|
||||
- The file is genuinely env-coupled (e.g. `gbrain-home-isolation.test.ts`, `claw-test-cli.test.ts`) — module-load env readers + ESM caching defeat dynamic-import-after-env tricks.
|
||||
- The file's tests intentionally share state across `it()` boundaries.
|
||||
|
||||
The quarantine has grown to dozens of files — treat it as debt: every addition needs a reason from the list above, and prefer fixing the contention root cause when one exists.
|
||||
|
||||
### Unit test inventory
|
||||
|
||||
`bun test` runs all tests without a database. E2E tests skip gracefully when `DATABASE_URL` is not set.
|
||||
|
||||
Unit tests and what they cover:
|
||||
|
||||
- `test/markdown.test.ts` — frontmatter parsing; `splitBody` sentinel precedence, horizontal-rule preservation, `inferType` wiki subtypes.
|
||||
- `test/chunkers/recursive.test.ts` — chunking.
|
||||
- `test/parity.test.ts` — operations contract parity.
|
||||
- `test/cli.test.ts` — CLI structure.
|
||||
- `test/cli-finish-teardown.test.ts` — the #2084 teardown contract: `computeTeardownDeadlineMs` formula/floor/live-registry scaling + `GBRAIN_TEARDOWN_DEADLINE_MS` override (garbage/zero/negative values fall back to the formula); `finishCliTeardown` clean path (drain BEFORE disconnect, no exit, no warn), backstop on hung drain or disconnect (honors an errored op's exit code), throwing drain/disconnect warned + swallowed; the gbrain-owned verdict channel is immune to PGLite WASM `process.exitCode` writes; `flushThenExit` unit coverage with mocked streams (exits once after both stream callbacks, non-TTY aliveness grace, blocked-pipe guard, EPIPE-safe, `GBRAIN_FLUSH_GRACE_MS` override).
|
||||
- `test/flush-then-exit-harness.test.ts` — real spawned-Bun pipe semantics for `flushThenExit` (fixture: `test/fixtures/flush-then-exit-harness.ts`): a 4MB piped stdout payload arrives byte-complete with the exit code even with a late reader, small output survives exit with a concurrent reader, and the fence resolves promptly (wall time well under the guard + grace ceiling).
|
||||
- `test/cli-should-force-exit.test.ts` — `shouldForceExitAfterMain` daemon-survival gate: `serve` (stdio and `--http`) never force-exits, including with preceding global flags; op commands / empty / flag-only argv do; the #2084 case that space-separated global-flag VALUES can't fake a command (`--timeout 30s serve` resolves to the `serve` daemon, not a `30s` command).
|
||||
- `test/cli-exit-verdict-pin.test.ts` — #2084 structural class pin: greps `src/` so the NEXT raw `process.exitCode =` write fails CI (a raw write bypasses the gbrain-owned verdict channel and gets silently zeroed by the deliberate flush-exit — the bug that made doctor's FAIL path exit 0). Runtime variants live in `test/cli-finish-teardown.test.ts`; this is the review-time guard.
|
||||
- `test/cli-pipe-truncation.test.ts` — real-CLI pipe completeness (the #1959 incident class), implementation-agnostic: the actual CLI run the way agents run it (piped stdout) produces complete, parseable, byte-stable `--tools-json` output and exits deliberately, well under the teardown backstop. Synthetic flush-mechanism coverage stays in `test/flush-then-exit-harness.test.ts`.
|
||||
- `test/volunteer-context.test.ts` — push-based context core (#2095), hermetic in-memory PGLite: `parseWindow` lenient `user:`/`assistant:` parsing, multi-turn window extraction, confidence-gated volunteering (arm confidences, multi-turn/newest-turn boosts, `min_confidence` gate, max-pages cap), slug-only suppression, privacy (rationales are deterministic templates; synopses pass the takes/facts fence), and the approximate usage-stats join.
|
||||
- `test/watch-command.test.ts` — `gbrain watch` push transport (#2095): streaming loop, rolling window, session dedupe, `--json` JSONL shape, `channel: 'watch'` event logging, clean EOF return. Hermetic PGLite + injected line/write deps (no subprocess, no real stdin).
|
||||
- `test/watch-sigint.serial.test.ts` — `gbrain watch` SIGINT lifecycle against a real spawned CLI subprocess with a tmpdir brain. SERIAL: parallel unit shards flake on concurrent subprocess spawns (same rationale as `apply-migrations-pglite-spawn.serial.test.ts`).
|
||||
- `test/cli-format-volunteer.test.ts` — `formatResult`'s `volunteer_context` human rendering: pointer lines with confidence/arm/rationale, the empty-result message, the approximate stats summary.
|
||||
- `test/config.test.ts` — config redaction.
|
||||
- `test/files.test.ts` — MIME/hash.
|
||||
- `test/import-file.test.ts` — import pipeline.
|
||||
- `test/upgrade.test.ts` — schema migrations.
|
||||
- `test/file-migration.test.ts` — file migration.
|
||||
- `test/file-resolver.test.ts` — file resolution.
|
||||
- `test/import-resume.test.ts` — import checkpoints.
|
||||
- `test/migrate.test.ts` — migration: v8/v9 helper-btree-index SQL structural assertions; 1000-row wall-clock fixtures guarding the O(n²)→O(n log n) fix; v12/v13 SQL shape; `sqlFor` + `transaction:false` runner semantics; the `max_stalled DEFAULT 1` regression guard; v24 `sqlFor.pglite: ''` no-op assertion; v117 `context_volunteer_events` (named + idempotent entry, documented columns + both source-scoped indexes after `initSchema`, insert + 90-day `purgeStaleVolunteerEvents` round-trip).
|
||||
- `test/bootstrap.test.ts` — bootstrap contract: no-op on fresh install, idempotent across two `initSchema()` calls, no-op on modern brain that already has every probed column, full bootstrap path on a simulated legacy brain, fresh-install regression guard, legacy `links` shape coverage.
|
||||
- `test/schema-bootstrap-coverage.test.ts` — CI guard. `REQUIRED_BOOTSTRAP_COVERAGE` lists every forward reference in `PGLITE_SCHEMA_SQL`; the test fails loudly if `applyForwardReferenceBootstrap` skips one (extend both arrays when adding a column-with-index to the embedded schema blob). Also parses `src/core/migrate.ts` source text for every `ALTER TABLE ... ADD COLUMN` (top-level `sql:`, `sqlFor.{postgres,pglite}` overrides, AND handler-body `engine.runMigration(N, \`ALTER TABLE ...\`)`) and asserts each (table, column) pair is covered by the bootstrap OR by the schema blob's CREATE TABLE bodies — catching the column-only forward-reference class (e.g. `sources.archived`, `oauth_clients.source_id`) that a CREATE INDEX parser alone can't see. `parseBaseTableColumns` strips SQL line + block comments before identifying column names so commented-out lines don't hide adjacent columns.
|
||||
- `test/helpers/schema-diff.ts` + `test/helpers/schema-diff.test.ts` + `test/e2e/schema-drift.test.ts` — cross-engine schema parity gate. Helper exports pure `snapshotSchema(query)` / `diffSnapshots(pg, pglite, opts)` / `formatDiffForFailure(diff)` / `isCleanDiff(diff)` over a four-tuple per column (`data_type`, `udt_name`, `is_nullable`, `column_default`). E2E test spins up fresh PGLite + Postgres, runs `engine.initSchema()` on each, snapshots `information_schema.columns`, then diffs. 2-table allowlist (`files`, `file_migration_ledger`) — every other Postgres table must reach PGLite via `PGLITE_SCHEMA_SQL` or a migration's `sqlFor.pglite` branch. Sentinels for `oauth_clients`, `mcp_request_log`, `access_tokens`, `eval_candidates` give tighter blame messages. Skips without `DATABASE_URL`. Wired into `scripts/e2e-test-map.ts` so changes to `src/schema.sql`, `src/core/pglite-schema.ts`, or `src/core/migrate.ts` trigger it. The failure message names every drift with a paste-ready hint pointing at `src/core/pglite-schema.ts`.
|
||||
- `test/setup-branching.test.ts` — setup flow.
|
||||
- `test/slug-validation.test.ts` — slug validation.
|
||||
- `test/storage.test.ts` — storage backends.
|
||||
- `test/supabase-admin.test.ts` — Supabase admin.
|
||||
- `test/yaml-lite.test.ts` — YAML parsing.
|
||||
- `test/check-update.test.ts` — version check + update CLI.
|
||||
- `test/pglite-engine.test.ts` — PGLite engine, all BrainEngine methods including `addLinksBatch` / `addTimelineEntriesBatch` (empty batch, missing optionals, within-batch dedup via ON CONFLICT, missing-slug rows dropped by JOIN, half-existing batch, batch of 100) plus `connect()` error-wrap assertion (original error nested, #223 link in message, lock released).
|
||||
- `test/links-timeline-jsonb-poison.test.ts` — gbrain#1861 PGLite half (always-on, no `DATABASE_URL`). Locks the `jsonb_to_recordset` batch-insert path for links/timeline/takes against free-text "poison" payloads (commas, quotes, backslashes, braces, em-dashes) and asserts NUL is stripped from free-text body fields but rejected in identity fields. gbrain#2011 adds lone-UTF-16-surrogate cases: every free-text field (link context; timeline summary/detail/source; take claim/source) well-forms to U+FFFD across batch + scalar write paths, while a surrogate in an identity field (slug) still fail-closed rejects the batch. The Postgres lane (`test/e2e/jsonb-batch-poison-postgres.test.ts`) is the one that actually reproduced the original crash.
|
||||
- `test/engine-factory.test.ts` — engine factory + dynamic imports.
|
||||
- `test/integrations.test.ts` — recipe parsing, CLI routing, recipe validation.
|
||||
- `test/publish.test.ts` — content stripping, encryption, password generation, HTML output.
|
||||
- `test/backlinks.test.ts` — entity extraction, back-link detection, timeline entry generation.
|
||||
- `test/lint.test.ts` — LLM artifact detection, code fence stripping, frontmatter validation.
|
||||
- `test/report.test.ts` — report format, directory structure.
|
||||
- `test/skills-conformance.test.ts` — skill frontmatter + required sections validation.
|
||||
- `test/resolver.test.ts` — RESOLVER.md coverage, routing validation; round-trip that every quoted RESOLVER.md trigger matches a frontmatter `triggers:` entry in the target skill, and every `name="<word>"` reference in any SKILL.md resolves to a declared op in `src/core/operations.ts` or a Minions handler in `PROTECTED_JOB_NAMES`.
|
||||
- `test/search.test.ts` — RRF normalization, compiled truth boost, cosine similarity, dedup key.
|
||||
- `test/sql-ranking.test.ts` — source-boost helpers: longest-prefix-match in SQL CASE, `detail=high` temporal-bypass, three-meta-char LIKE escape (`%`, `_`, `\`), single-quote SQL-literal doubling, env override parsing for `GBRAIN_SOURCE_BOOST` + `GBRAIN_SEARCH_EXCLUDE`, `resolveBoostMap` / `resolveHardExcludes` merge semantics.
|
||||
- `test/dedup.test.ts` — source-aware dedup, compiled truth guarantee, layer interactions.
|
||||
- `test/intent.test.ts` — query intent classification: entity/temporal/event/general.
|
||||
- `test/eval.test.ts` — retrieval metrics: `precisionAtK`, `recallAtK`, `mrr`, `ndcgAtK`, `parseQrels`.
|
||||
- `test/check-resolvable.test.ts` — resolver reachability, MECE overlap, gap detection, proximity-based DRY detection, `extractDelegationTargets` coverage.
|
||||
- `test/dry-fix.test.ts` — auto-fix: three shape-aware expander pure-function tests; five guards (working-tree-dirty, no-git-backup, inside-code-fence, already-delegated within 40 lines, ambiguous-multi-match, block-is-callout).
|
||||
- `test/doctor-fix.test.ts` — `gbrain doctor --fix` CLI integration: dry-run preview, apply path, JSON output shape.
|
||||
- `test/backoff.test.ts` — load-aware throttling, concurrency limits, active hours.
|
||||
- `test/fail-improve.test.ts` — deterministic/LLM cascade, JSONL logging, test generation, rotation.
|
||||
- `test/transcription.test.ts` — provider detection, format validation, API key errors.
|
||||
- `test/enrichment-service.test.ts` — entity slugification, extraction, tier escalation.
|
||||
- `test/data-research.test.ts` — recipe validation, MRR/ARR extraction, dedup, tracker parsing, HTML stripping.
|
||||
- `test/minions.test.ts` — Minions job queue: CRUD, state machine, backoff, stall detection, dependencies, worker lifecycle, lock management, claim mechanics, depth/child-cap, timeouts, cascade kill, idempotency, `child_done` inbox, attachments, removeOnComplete/Fail, `max_stalled` clamp/default/plumbing coverage.
|
||||
- `test/extract.test.ts` — link extraction, timeline extraction, frontmatter parsing, directory type inference.
|
||||
- `test/extract-db.test.ts` — `gbrain extract --source db`: typed link inference, idempotency, `--type` filter, `--dry-run` JSON output.
|
||||
- `test/extract-fs.test.ts` — `gbrain extract --source fs`: first-run inserts + second-run reports zero, dry-run dedups candidates across files, second-run perf regression guard for the N+1 dedup bug.
|
||||
- `test/link-extraction.test.ts` — canonical `extractEntityRefs` both formats, `extractPageLinks` dedup, `inferLinkType` heuristics, `parseTimelineEntries` date variants, `isAutoLinkEnabled` config.
|
||||
- `test/graph-query.test.ts` — direction in/out/both, type filter, indented tree output.
|
||||
- `test/features.test.ts` — feature scanning, brain_score calculation, CLI routing, persistence.
|
||||
- `test/file-upload-security.test.ts` — symlink traversal, cwd confinement, slug + filename allowlists, remote vs local trust.
|
||||
- `test/query-sanitization.test.ts` — prompt-injection stripping, output sanitization, structural boundary.
|
||||
- `test/search-limit.test.ts` — `clampSearchLimit` default/cap behavior across `list_pages` and `get_ingest_log`.
|
||||
- `test/repair-jsonb.test.ts` — JSONB repair: TARGETS list, idempotency, engine-awareness.
|
||||
- `test/migrations-v0_12_2.test.ts` — JSONB-repair orchestrator phases: schema → repair → verify → record.
|
||||
- `test/orphans.test.ts` — orphans command: detection, pseudo filtering, text/json/count outputs, MCP op.
|
||||
- `test/postgres-engine.test.ts` — `statement_timeout` scoping: `sql.begin` + `SET LOCAL` shape, source-level grep guardrail against a reintroduced bare `SET statement_timeout`.
|
||||
- `test/sync.test.ts` — sync logic + regression guard asserting top-level `engine.transaction` is not called.
|
||||
- `test/sync-pull-failed-anchor.serial.test.ts` — #3068 regression: a failed internal `git pull` (local-path origin vs `protocol.file.allow=never`) with zero imports returns `partial`/`pull_failed` (not `up_to_date`), freezes `last_commit` + `last_sync_at`, recovers after a manual pull; fall-through import of local commits preserved. Serial: pins `GBRAIN_HOME` to a temp dir for the whole file.
|
||||
- `test/sync-concurrency.test.ts` — `autoConcurrency()` thresholds + PGLite-forces-serial + explicit-override clamping; `shouldRunParallel()` explicit-bypasses-floor contract; `parseWorkers()` validation rejecting `'0'`/`'-3'`/`'foo'`/`'1.5'`/trailing chars.
|
||||
- `test/sync-parallel.test.ts` — PGLite-routed coverage of the bookmark gate under concurrency, head-drift gate, vanished-file failure capture, PGLite-stays-serial, and the `gbrain-sync` writer-lock contract.
|
||||
- `test/sync-all-missing-path.test.ts` — `sync --all --missing-path <fail|skip>` pure helpers: `parseMissingPathMode` (default fail, explicit values, loud rejection of bad/dangling values, never swallows a following flag) and `partitionMissingPathSources` (classification driven only by the injected pathExists predicate — no fs; null `local_path` passes through runnable; order preserved).
|
||||
- `test/sync-failures.test.ts` — `classifyErrorCode` regex coverage for all 12 codes against literal production message strings from `markdown.ts` and `import-file.ts`; `summarizeFailuresByCode` sort + pre-classified-honor; `recordSyncFailures` code-field persistence; `acknowledgeSyncFailures` `AcknowledgeResult` shape + backfill on legacy entries.
|
||||
- `test/doctor.test.ts` — doctor command; assertions that `jsonb_integrity` scans the four JSONB write sites and `markdown_body_completeness` is present.
|
||||
- `test/utils.test.ts` — shared SQL utilities + `tryParseEmbedding` null-return and single-warn semantics.
|
||||
- `test/build-llms.test.ts` — `llms.txt`/`llms-full.txt` generator: path resolution, idempotence, spec shape, regen-drift guard, content contract, AGENTS.md install-path mirror, size-budget enforcement.
|
||||
- `test/oauth.test.ts` — OAuth 2.1 provider: register, getClient, `client_credentials` grant exchange, `authorization_code` flow with PKCE challenge/verifier, refresh token rotation, `verifyAccessToken` with both OAuth + legacy `access_tokens` fallback, `revokeToken`, `sweepExpiredTokens`; contract test asserting `scope` + `localOnly` annotations on all operations; `coerceTimestamp` unit cases (null/undefined/string/number/throw-on-NaN); NULL-`expires_at`-as-expired contract for both refresh + access token paths; cascade-delete contract asserting `revoke-client` purges `oauth_tokens` + `oauth_codes` via FK CASCADE; cross-client isolation (wrong-client attempt MUST reject AND rightful owner MUST still succeed atomically afterward); empty-string `redirect_uri` bypass guard; PKCE DCR public-client gate (`token_endpoint_auth_method: "none"` returns no `client_secret`, default `client_secret_post` clients get the one-time-reveal secret, `getClient` NULL→undefined normalization, full PKCE `/authorize` → `/token` round-trip against a public client).
|
||||
- `test/mcp-dispatch-summarize.test.ts` — `summarizeMcpParams` invariants: declared-keys allow-list intersection, attacker-key-name leak guard (unknown keys counted not named), 1KB byte bucketing for size-probe defense, missing op falls through to fully-redacted shape, declared-keys sorted for deterministic output.
|
||||
- `test/trust-boundary-contract.test.ts` — fail-closed trust semantics under cast bypass: `ctx.remote === undefined` treated as remote/untrusted at every flipped call site; `as any` and `Partial<>` spreads can't downgrade trust by accident.
|
||||
- `test/check-resolvable-cli.test.ts` — CLI wrapper: exit codes, JSON envelope shape, AGENTS.md fallback chain.
|
||||
- `test/regression-v0_16_4.test.ts` — `findRepoRoot` regression guard, hermetic startDir parameterization.
|
||||
- `test/repo-root.test.ts` — `findRepoRoot` walk semantics + default-arg parity; the 4-tier `autoDetectSkillsDir` fallback chain (`$OPENCLAW_WORKSPACE` → `~/.openclaw/workspace` → repo-root → `./skills`); RESOLVER.md/AGENTS.md filename precedence; explicit-env-wins-over-repo-root; tier-0 `$GBRAIN_SKILLS_DIR` valid/invalid/precedence-over-`OPENCLAW_WORKSPACE`; the install-path walk in `autoDetectSkillsDirReadOnly`; no-drift on primary success; `AUTO_DETECT_HINT` + `AUTO_DETECT_HINT_READ_ONLY` content; regression guard asserting the shared `autoDetectSkillsDir` MUST NEVER return `'install_path'` source (how the read-path/write-path split stays safe).
|
||||
- `test/resolver-merge.test.ts` — multi-file resolver merge: `findAllResolverFiles` empty / RESOLVER.md-only / AGENTS.md-only / both-present (RESOLVER.md first); `checkResolvable` merge semantics across `skills/RESOLVER.md` + `../AGENTS.md` for the OpenClaw layout where the skillpack ships a thin RESOLVER.md and the real dispatcher lives at the workspace root; dedup by `skillPath` (first occurrence wins); AGENTS.md-at-workspace-root works alone.
|
||||
- `test/filing-audit.test.ts` — filing audit: `writes_pages` / `writes_to` frontmatter, filing-rules JSON validation.
|
||||
- `test/skill-brain-first.test.ts` — shared frontmatter parser; `analyzeSkillBrainFirst` compliance ladder across 9 fixtures under `test/fixtures/brain-first-skills/` (compliant-callout, compliant-phase, compliant-position, exempt-frontmatter, missing-brain-first, multi-pattern, negation-prose, no-external, typo-frontmatter); offset helpers; external-lookup regex shape; audit snapshot+diff transition logic; `FORMERLY_HARDCODED_EXEMPT` regression absorption.
|
||||
- `test/routing-eval.test.ts` — fixture parsing, structural routing, `ambiguous_with`, Haiku tie-break layer.
|
||||
- `test/skill-manifest.test.ts` — skill manifest parser: drift detection, managed-block markers.
|
||||
- `test/skillify-scaffold.test.ts` — `gbrain skillify scaffold` stubs: SKILL.md, script, tests, routing-eval fixtures.
|
||||
- `test/skillpack-install.test.ts` — `gbrain skillpack install` managed-block install / update / no-clobber semantics.
|
||||
- `test/skillpack-sync-guard.test.ts` — sync-guard: bundled skills stay byte-identical to `skills/` source.
|
||||
- `test/http-transport.test.ts` — HTTP transport: bearer auth + missing/no-Bearer/unknown/revoked + `/health` bypass; dispatch.ts round-trip; invalid_params; application/json response shape (not SSE); CORS default-deny + allowlist; body cap on Content-Length AND chunked; two-bucket rate limit (refill, exhaust+Retry-After, LRU eviction, TTL prune, pre-auth IP fires before DB); `mcp_request_log` audit on success + auth_failed.
|
||||
- `test/restart-sweep.test.ts` — `recipes/restart-sweep.md` inlined script: sentinel-anchored fenced-block extraction with salted tmp filenames to bypass ESM cache; constructor-time env reads (proves no module-load snapshot); idempotency layer load/save/atomic-tmp-rename/corrupt-JSON-recovery/30-day-prune; `(sessionKey, lastAlertedAt)` cooldown gate with 6h threshold; AGGRESSIVE-gate two-state tests; execFile argv shape proving shell metachars in `OPENCLAW_TELEGRAM_GROUP` cannot reach `/bin/sh`; real-`\n`-not-literal alert formatting; `GBRAIN_HOME` state path override.
|
||||
- `test/eval-longmemeval.test.ts` — LongMemEval harness, hermetic with no `DATABASE_URL` and no API keys: PGLite create + reset over runtime-enumerated `pg_tables`, infrastructure-table preservation across resets, JSONL question parsing, retrieval-only and answer-gen modes via stubbed `ThinkLLMClient`, `--limit` cutoff, `--keyword-only` vs hybrid, default `--expansion=off` behavior, perf gate (p50 < 30ms / p99 < 50ms warm reset+import+search on Apple Silicon), `--help` works without a configured brain, fixture round-trip via `test/fixtures/longmemeval-mini.jsonl`.
|
||||
- `test/longmemeval-sanitize.test.ts` — sanitization parity pinning that `INJECTION_PATTERNS` from `src/core/think/sanitize.ts` is the single source of truth (adding a pattern there must cover both `<take>` framing and `<chat_session>` framing, no per-surface regex drift).
|
||||
- `test/openai-compat-multimodal.test.ts` — gateway's openai-compatible multimodal path: happy-path single + multi-input embedding, unauthenticated proxy mode, dimension-mismatch guard (throws `AIConfigError` with model id + observed + expected pre-storage), default-dim fallback when recipe declares `default_dims`, HTTP 401 / 400 / malformed-JSON / non-array error paths, regression that the existing Voyage `/multimodalembeddings` recipe still routes through its dedicated path. Hermetic via the `__setEmbedTransportForTests` seam.
|
||||
- `test/serve-stdio-lifecycle.test.ts` — `MCP_STDIO=1` env guard: stdin EOF does NOT trigger shutdown when the env is set, SIGTERM still does (guard scope is correct), unset env preserves the CLI lifecycle. Exercises the `ServeOptions.mcpStdio?: boolean` test seam directly so tests don't mutate `process.env`.
|
||||
|
||||
### E2E test inventory
|
||||
|
||||
E2E tests live in `test/e2e/` and run against real Postgres+pgvector (require `DATABASE_URL`), except where noted as PGLite in-memory (no `DATABASE_URL` needed).
|
||||
|
||||
- `bun run test:e2e` runs Tier 1 (mechanical, all operations, no API keys). Includes dedicated cases for the postgres-engine `addLinksBatch` / `addTimelineEntriesBatch` bind path — postgres-js's JSONB bind (`jsonb_to_recordset(($1::jsonb)->'rows')`) differs from PGLite's and gets its own coverage.
|
||||
- `test/e2e/search-quality.test.ts` — search quality against PGLite (no API keys, in-memory).
|
||||
- `test/e2e/graph-quality.test.ts` — knowledge graph pipeline (auto-link via put_page, reconciliation, traversePaths) against PGLite in-memory.
|
||||
- `test/e2e/jsonb-batch-poison-postgres.test.ts` — gbrain#1861 regression, the engine that actually crashed. Seeds free-text "poison" context (Zoom URL with `?pwd=`, commas, quotes, Windows backslash path, braces, em-dash) and asserts the links/timeline/takes batch writers no longer error with "malformed array literal"; also asserts NUL is stripped from free-text bodies (`context`/`summary`/`detail`/`claim`) and still rejected in identity fields. gbrain#2011 adds the lone-surrogate crash lock: a lone UTF-16 surrogate in free text (the value that aborted `extract --stale` with `22P02` on Supabase) well-forms to U+FFFD across batch + scalar paths (incl. timeline + take `source`), while a surrogate in an identity field still rejects the batch. `DATABASE_URL`-gated.
|
||||
- `test/e2e/postgres-jsonb.test.ts` — round-trips all 5 JSONB write sites (`pages.frontmatter`, `raw_data.data`, `ingest_log.pages_updated`, `files.metadata`, `page_versions.frontmatter`) against real Postgres and asserts `jsonb_typeof='object'` plus `->>'key'` returns the expected scalar. Guards against the double-encode bug.
|
||||
- `test/e2e/integrity-batch.test.ts` — parity for `scanIntegrity`'s batch-load fast path vs sequential. Cases (dedup, hits, validate, topPages) seed a fixture and assert both paths return identical results. Dedup case uses raw SQL via `getConn().unsafe()` to seed a `(test-source-2, people/alice)` row alongside the default-source row, since `engine.putPage` doesn't take a `source_id`. Pins multi-source overcounting; the "multi-source duplicate slugs scan once" case expects both batch + sequential paths to report 2.
|
||||
- `test/e2e/jsonb-roundtrip.test.ts` — companion regression against the 4 doctor-scanned JSONB sites. Assertion-level overlap with `postgres-jsonb.test.ts` is intentional defense-in-depth: if doctor's scan surface drifts from the actual write surface, one of these tests catches it.
|
||||
- `test/e2e/sync.test.ts` — `--skip-failed` failure-loop test alongside happy-path tests: broken file → `performSync` returns `blocked_by_failures` with grouped breakdown → `performSync({skipFailed: true})` advances bookmark and returns `AcknowledgeResult` with code summary → second broken file → second cycle. Saves and restores the user's real `~/.gbrain/sync-failures.jsonl` so the test is hermetic. Asserts bookmark gating, JSONL state, dedup across paths, summary aggregation, and the literal doctor-rendering string format.
|
||||
- `test/e2e/upgrade.test.ts` — check-update against real GitHub API (network required).
|
||||
- `test/e2e/minions-shell-pglite.test.ts` — PGLite `--follow` inline shell-job path (in-memory, no `DATABASE_URL` required) — the path the minion-orchestrator skill documents for dev use.
|
||||
- `test/e2e/pglite-cli-exit.serial.test.ts` — real spawned-CLI exit behavior on PGLite (in-memory, no `DATABASE_URL`): read commands (`search`/`get`/`query`) exit 0 promptly; CLI_ONLY `capture` exits clean and frees the single-writer lock; the `#2084` describes pin every swept disconnect site — a failed op exits 1 with the error on stderr, and the dashboard, read-only-timeout, doctor, and `dream --dry-run` paths all exit with no force-exit banner.
|
||||
- `test/e2e/pgbouncer-teardown.test.ts` — PgBouncer TRANSACTION-mode teardown (#2084 / the #1972→#2015→#2084 class). Pins the bug CLASS, not timings: a CLI op against a txn-mode pooled URL exits 0 with intact stdout and does NOT ride the 10s hard-deadline backstop (the `engine.disconnect() did not return` banner is the smoking gun — pre-#2084 it printed on 100% of query-shaped ops). Gated by `GBRAIN_PGBOUNCER_URL` + `GBRAIN_PGBOUNCER_DIRECT_URL` (NOT `DATABASE_URL`) — set automatically by `bun run ci:local`'s `pgbouncer` compose service; skips gracefully elsewhere. Uses a DEDICATED `gbrain_pgbouncer` database so it never races the `gbrain_test` TRUNCATE fixtures.
|
||||
- `test/e2e/volunteer-context-postgres.test.ts` — `volunteer_context` on REAL Postgres (#2095; engine parity beyond the hermetic PGLite unit suite): resolution arms through the actual op handler, the fire-and-forget volunteer-event sink landing rows, the stats join, and the RLS pin that `context_volunteer_events` has ROW LEVEL SECURITY enabled (keeps the v35 auto-RLS event trigger honest for migration-created tables). `DATABASE_URL`-gated.
|
||||
- `test/e2e/openclaw-reference-compat.test.ts` — `check-resolvable` + `skillpack install` against a minimal AGENTS.md workspace fixture (`test/fixtures/openclaw-reference-minimal/`), regression guard for the OpenClaw deployment shape.
|
||||
- `test/e2e/search-swamp.test.ts` — reproduces the source-swamp case. Seeds a curated `originals/talks/article-outline-fat-code` page against two `<fork>/chat/` pages stuffed with the same multi-word phrase. Asserts the article wins keyword AND vector ranking, that `detail=high` lets the chat swamp re-surface, and that `source_id` passes through the two-stage CTE intact. PGLite in-memory.
|
||||
- `test/e2e/search-exclude.test.ts` — `test/` + `archive/` pages hidden by default, `include_slug_prefixes` opts back in, caller-supplied `exclude_slug_prefixes` adds to defaults. Both keyword and vector search paths.
|
||||
- `test/e2e/engine-parity.test.ts` — Postgres ↔ PGLite top-result and result-set parity for `searchKeyword` + `searchVector` (Postgres ranks pages then picks best chunk while PGLite returns chunks directly, so the source-boost behavior needs parity coverage). Skips without `DATABASE_URL`.
|
||||
- `test/e2e/postgres-bootstrap.test.ts` — exercises `PostgresEngine.initSchema()` directly against a fresh real Postgres database. Asserts the bootstrap path is no-op on fresh installs and that SCHEMA_SQL replays cleanly through the engine path (not via the standalone `db.initSchema` from `src/core/db.ts`).
|
||||
- `test/e2e/http-transport.test.ts` — `gbrain serve --http` end-to-end against real Postgres: bearer auth round-trip, `last_used_at` SQL-level debounce, `mcp_request_log` row insertion on success and auth_failed paths, `/health` DB-down → 503 (DB-probing health check), and the dispatch round-trip with a real operation. Skips without `DATABASE_URL`.
|
||||
- `test/e2e/serve-http-oauth.test.ts` — real-Postgres E2E against `gbrain serve --http` with full OAuth 2.1. Spawns a subprocess server, registers a client via the CLI, mints `client_credentials` tokens, exercises the `/mcp` JSON-RPC pipeline. Real DCR `/register` HTTP-level response-shape test (asserts `typeof body.client_id_issued_at === 'number'` over the wire, RFC 7591 §3.2.1); real CLI subprocess test for `revoke-client` (registers → mints token → revokes via `execSync` → asserts token rejected at `/mcp` → asserts re-run exits 1); server fixture flips on `--enable-dcr` so `/register` is reachable. **bun execSync env-inheritance contract:** bun's `execSync` does NOT inherit env mutations done via `process.env.X = ...`, only OS-level env from before bun started. helpers.ts loads `.env.testing` and sets `DATABASE_URL` via `process.env` mutation, which is invisible to subprocesses unless `env: { ...process.env }` is passed explicitly — every subprocess call in this file passes `env: { ...process.env }`. Reference fix for the same failure mode in sibling sync/cycle/dream/claw-test E2Es. `afterAll` cleanup is guarded on `clientId` (won't throw if `beforeAll` failed before registration); cleanup errors surface to stderr without throwing so real test failures aren't masked. Also covers the trust-boundary fix: an HTTP MCP `submit_job` for `name: "shell"` MUST reject with a permission error (request handler sets `remote: true` and `submit_job`'s protected-name guard fires), and the same guard rejects subagent submission. Skips without `DATABASE_URL`.
|
||||
- `test/e2e/sync-parallel.test.ts` — `DATABASE_URL`-gated. 60-file Postgres sync at concurrency=4 imports all + no connection leak (probes `pg_stat_activity` before/after to confirm worker engines disconnected). 120-file serial-vs-parallel benchmark prints `SYNC_PARALLEL_BENCH N files | serial=Xms | parallel(4)=Yms | speedup=Zx`. Asserts parallel ≤ serial × 1.5 (CI-noise tolerant; not a strict speedup gate).
|
||||
- `test/e2e/multi-source-bug-class.test.ts` — PGLite in-memory regression suite pinning every multi-source bug site: `listAllPageRefs` ordering by `(source_id, slug)`, `getPage` with sourceId picks the right `(source, slug)` row, `extract-takes` processes both overlapping `people/alice` rows independently, `listPages` filters correctly with `PageFilters.sourceId`, `addLinksBatch` with `from/to_source_id` targets the right rows, `validateSourceId` rejects path traversal, reverse-write disk layout uses `brainDir/.sources/<id>/<slug>.md` for non-default sources, `copyMigrationSources` lands source metadata before overlapping-slug pages. No `DATABASE_URL` needed. Wired into `scripts/e2e-test-map.ts` so changes to extract-takes / patterns / synthesize / embed / extract / migrate-engine auto-trigger it.
|
||||
- `test/e2e/migrate-engine-sources-postgres.test.ts` — `DATABASE_URL`-gated companion for `gbrain migrate --to`: migrates a PGLite brain carrying two non-default sources with overlapping slugs into real Postgres and asserts `copyMigrationSources` created every `sources` FK parent (config JSONB intact, not double-encoded) before any page write. Unit-level manifest identity (crash manifest resumes only against the SAME target; legacy engine-only manifests start fresh) is `test/migrate-engine-resume.test.ts`.
|
||||
- `test/e2e/facts-fence-reconcile-postgres.test.ts` — `DATABASE_URL`-gated round-trip for the escape-aware fence parser: renders a `## Facts` fence whose cells carry literal pipes, backslashes (Windows paths), and empty cells via `renderFactsTable`, runs the wipe-and-reinsert reconcile (`runExtractFacts`) on real Postgres, and asserts every cell survives byte-identically with no column shift.
|
||||
- `test/e2e/source-isolation-pglite.test.ts` — PGLite in-memory regression suite pinning the source-isolation seal at two layers. Engine layer: `searchKeyword` / `searchVector` / `searchKeywordChunks` / `listPages` / `getPage` / `traverseGraph` / `traversePaths` apply `sourceId` (scalar fast path) and `sourceIds` (array path) correctly across both engines. Op-handler layer: routes through `sourceScopeOpts(ctx)` so a `read+write`-scoped OAuth client bound to `--source dept-x` cannot see rows from neighboring sources via `search`, `query`, `list_pages`, `get_page`, or `find_experts`. Covers both `ctx.sourceId` (single-source clients) and `ctx.auth.allowedSources` (federated_read clients) precedence; federated array wins over scalar wins over nothing. No `DATABASE_URL` needed.
|
||||
- `test/e2e/think-source-isolation-pglite.test.ts` — PGLite in-memory suite pinning the `think` gather stage's source scope: seeds three sources with cross-source links and embedded takes, then asserts `runGather` under a federated `sourceIds` grant (and under a scalar `sourceId`) keeps every stream — hybrid retrieval, takes keyword + vector (`searchTakes`/`searchTakesVector`), and the `traversePaths` graph walk — inside the grant while still reaching authorized neighboring sources. No `DATABASE_URL` needed.
|
||||
- `test/e2e/skill-brain-first.test.ts` — doctor reports `skill_brain_first` check with structured issues; `--fix --dry-run` previews insertion without writing; `--fix` applies the canonical Convention callout idempotently; `brain_first: exempt` frontmatter resolves the warn; `brain_first_typo` surfaces a paste-ready hint; audit JSONL records `detected` / `resolved` / `fixed` transitions; stable brain emits 0 audit lines/run.
|
||||
- Tier 2 (`test/e2e/skills.test.ts`) requires OpenClaw + API keys, runs nightly in CI.
|
||||
- If `.env.testing` doesn't exist in this directory, check sibling worktrees: `find ../ -maxdepth 2 -name .env.testing -print -quit` and copy it here if found.
|
||||
- **Run E2E tests without asking permission.** When you want to verify behavior, there's a relevant E2E test, or you're shipping anything covered by an E2E suite — spin up the test DB, run the tests, tear down. Don't ask, don't propose it, don't defer. The lifecycle is short (~2-30s startup, sub-minute tests, instant teardown) and the gate value is high. Skipping with "DATABASE_URL unset" is silent regression, not caution.
|
||||
|
||||
### API keys and running ALL tests
|
||||
|
||||
ALWAYS source the user's shell profile before running tests:
|
||||
|
||||
```bash
|
||||
source ~/.zshrc 2>/dev/null || true
|
||||
```
|
||||
|
||||
This loads `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`. Without these, Tier 2 tests
|
||||
skip silently. Do NOT skip Tier 2 tests just because they require API keys — load
|
||||
the keys and run them.
|
||||
|
||||
When asked to "run all E2E tests" or "run tests", that means ALL tiers:
|
||||
- Tier 1: `bun run test:e2e` (mechanical, sync, upgrade — no API keys needed)
|
||||
- Tier 2: `test/e2e/skills.test.ts` (requires OpenAI + Anthropic + openclaw CLI)
|
||||
- Always spin up the test DB, source zshrc, run everything, tear down.
|
||||
|
||||
### E2E test DB lifecycle (ALWAYS follow this)
|
||||
|
||||
You are responsible for spinning up and tearing down the test Postgres container.
|
||||
Do not leave containers running after tests. Do not skip E2E tests, do not ask
|
||||
permission to run them — see the "run without asking" rule above.
|
||||
|
||||
1. **Check for `.env.testing`** — if missing, copy from sibling worktree.
|
||||
Read it to get the DATABASE_URL (it has the port number).
|
||||
2. **Check if the port is free:**
|
||||
`docker ps --filter "publish=PORT"` — if another container is on that port,
|
||||
pick a different port (try 5435, 5436, 5437) and start on that one instead.
|
||||
3. **Start the test DB:**
|
||||
```bash
|
||||
docker run -d --name gbrain-test-pg \
|
||||
-e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_DB=gbrain_test \
|
||||
-p PORT:5432 pgvector/pgvector:pg16
|
||||
```
|
||||
Wait for ready: `docker exec gbrain-test-pg pg_isready -U postgres`
|
||||
4. **Bootstrap the schema** (required — fresh containers have no `oauth_clients`,
|
||||
`mcp_request_log`, `pages` etc.; tests like `serve-http-oauth.test.ts` will fail
|
||||
with `relation "oauth_clients" does not exist` if you skip this):
|
||||
```bash
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:PORT/gbrain_test \
|
||||
bun run src/cli.ts doctor --json > /dev/null 2>&1
|
||||
```
|
||||
`gbrain doctor` triggers `initSchema()` on first connect, which is the canonical
|
||||
way to bring a fresh DB to head. `apply-migrations --yes` alone does NOT seed
|
||||
the base schema — it runs ALTER-style migrations on top of `initSchema`. Tests
|
||||
that bypass the engine (raw `execSync`-spawned `auth register-client`) hit the
|
||||
schema directly and need this step to have run first.
|
||||
5. **Run E2E tests:**
|
||||
`DATABASE_URL=postgresql://postgres:postgres@localhost:PORT/gbrain_test bun run test:e2e`
|
||||
6. **Tear down immediately after tests finish (pass or fail):**
|
||||
`docker stop gbrain-test-pg && docker rm gbrain-test-pg`
|
||||
|
||||
Never leave `gbrain-test-pg` running. If you find a stale one from a previous run,
|
||||
stop and remove it before starting a new one.
|
||||
File diff suppressed because one or more lines are too long
@@ -40,7 +40,7 @@ Every `put_page` runs `extractEntityRefs` on the markdown body. It matches:
|
||||
- Obsidian wikilinks: `[[wiki/people/garry-tan|Garry Tan]]`
|
||||
- Typed-link blockquotes: `> **Convention:** see [path](path).`
|
||||
|
||||
Three regexes, zero LLM tokens, single SQL `addLinksBatch` call with `INSERT ... SELECT FROM jsonb_to_recordset(($1::jsonb)->'rows') JOIN pages ON CONFLICT DO NOTHING RETURNING 1` (free-text-safe; the prior `unnest(${arr}::text[])` form crashed on calendar/Zoom context per gbrain#1861). The graph grows on every write at near-zero cost. On a 17K-page brain, full graph extract completes in seconds.
|
||||
Three regexes, zero LLM tokens, single SQL `addLinksBatch` call with `INSERT ... SELECT FROM unnest(...) JOIN pages ON CONFLICT DO NOTHING RETURNING 1`. The graph grows on every write at near-zero cost. On a 17K-page brain, full graph extract completes in seconds.
|
||||
|
||||
Heuristic link-type inference (`attended`, `works_at`, `invested_in`, `founded`, `advises`) fires from surrounding sentence context — also LLM-free. Power users who want richer types add them via the typed-link blockquote convention.
|
||||
|
||||
@@ -54,9 +54,7 @@ The cost: +150ms p50 latency, ~$0.025/M tokens. Disabled with `gbrain config set
|
||||
|
||||
## Source-aware ranking
|
||||
|
||||
Hybrid search applies a source-factor CASE expression at the SQL layer (lives in `src/core/search/sql-ranking.ts`). Curated content like `originals/`, `concepts/`, `writing/` outranks bulk content like `your-openclaw/chat/`, `daily/`, `media/x/`. Hard-exclude prefixes (`test/`, `attachments/`, `.raw/`) filter at retrieval, not post-rank.
|
||||
|
||||
`archive/` is deliberately NOT hard-excluded (issue #1777): it holds high-signal historical content users expect to find, so it is demoted (`0.5x` in `DEFAULT_SOURCE_BOOSTS`), not hidden. The demote is a prior applied in the outer SQL re-rank; the cross-encoder reranker (balanced/tokenmax modes) can still PROMOTE an archive page that survives the demote into the rerank candidate window — it is not an unconditional suppression. `gbrain doctor`'s `hidden_by_search_policy` check reports how many chunked pages remain hidden by the surviving exclude prefixes.
|
||||
Hybrid search applies a source-factor CASE expression at the SQL layer (lives in `src/core/search/sql-ranking.ts`). Curated content like `originals/`, `concepts/`, `writing/` outranks bulk content like `your-openclaw/chat/`, `daily/`, `media/x/`. Hard-exclude prefixes (`test/`, `archive/`, `attachments/`, `.raw/`) filter at retrieval, not post-rank.
|
||||
|
||||
The boost map is configurable via `GBRAIN_SOURCE_BOOST` env var or per-call `SearchOpts.exclude_slug_prefixes`. Temporal queries (`detail: 'high'`) bypass the boost so chat pages re-surface for time-sensitive lookups.
|
||||
|
||||
@@ -87,15 +85,6 @@ embedding proximity. Four layers, added after the incident in
|
||||
deciding "is this page already here, safe to NOT write a duplicate?" keys off
|
||||
`create_safety`, not a raw blended score.
|
||||
|
||||
**Extraction quarantine lane (issue #160):** pages carrying the unverified
|
||||
auto-extracted markers (frontmatter `provenance: auto-extracted` +
|
||||
`status: unverified`, see `src/core/extraction-review.ts`) rank as ordinary
|
||||
content — they are skipped by the compiled-truth fusion boost and by the
|
||||
`people/`/`companies/` namespace source-boost, and every search result from
|
||||
such a page carries `unverified: true` so agents can label the provenance.
|
||||
Promote or reject them via `gbrain extraction-pending` / `gbrain
|
||||
extraction-review`.
|
||||
|
||||
The `search` MCP/CLI op is **cheap-hybrid** (vector + keyword + RRF + pool +
|
||||
title + alias, expansion off); `query` is the full-control variant. NamedThingBench
|
||||
(`gbrain eval retrieval-quality`) gates these families on every PR. Diagnose a
|
||||
@@ -132,7 +121,6 @@ expansion (if enabled)
|
||||
hybrid search:
|
||||
├── vector (HNSW on chunk embeddings)
|
||||
├── keyword (BM25 via tsvector)
|
||||
├── relational (v0.42.34.0: typed-edge recall arm — relational queries only)
|
||||
├── source-aware re-rank (CASE in SQL)
|
||||
└── RRF fusion → top 30
|
||||
│
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
# Conversation parser patterns
|
||||
|
||||
The conversation parser turns exported chat and meeting transcripts into a
|
||||
common message stream without requiring an LLM call for known formats. This
|
||||
document describes the built-in pattern contract and the checks required when
|
||||
adding or changing a format.
|
||||
|
||||
## Data flow
|
||||
|
||||
`parseConversation` uses this sequence:
|
||||
|
||||
1. Resolve the page date and timezone context.
|
||||
2. Score every enabled built-in and user pattern against the first ten
|
||||
non-blank lines.
|
||||
3. Re-score the full body when the head score is inconclusive, or when a broad
|
||||
pattern explicitly requires full-body scoring.
|
||||
4. Reject the winner when its acceptance score is below the false-positive
|
||||
floor.
|
||||
5. Apply the winning pattern to every line and attach continuation lines to the
|
||||
preceding message.
|
||||
6. Optionally run LLM polish or fallback when those features are enabled.
|
||||
|
||||
Pattern order is only a tie-breaker. A new regex must be structurally distinct
|
||||
from neighboring formats; moving it earlier in the registry is not a valid
|
||||
non-shadowing strategy.
|
||||
|
||||
## Built-in pattern contract
|
||||
|
||||
Every `PatternEntry` in `builtins.ts` declares:
|
||||
|
||||
- A stable, kebab-case `id`.
|
||||
- A hand-vetted line regex and explicit capture-group indexes.
|
||||
- Where the date comes from and how the time is represented.
|
||||
- A timezone policy.
|
||||
- Whether the format supports multi-line message bodies.
|
||||
- Positive and negative samples that run during module initialization.
|
||||
- A documentation pointer describing the source format.
|
||||
|
||||
The registry refuses to load when a positive sample stops matching, a negative
|
||||
sample starts matching, or a capture map becomes invalid. This catches local
|
||||
regex mistakes before extraction can silently produce empty conversations.
|
||||
|
||||
### Date and timezone rules
|
||||
|
||||
Formats with an inline date should capture it from each message. Time-only
|
||||
formats use an explicit caller fallback first, then the page frontmatter date,
|
||||
then the page effective date. If none is available, the parser uses
|
||||
`1970-01-01` so the missing date remains visible instead of inventing a current
|
||||
date.
|
||||
|
||||
Time-only formats normally use `utc_assumed_with_warn`. The parser constructs a
|
||||
UTC timestamp and returns a timezone warning when the page does not provide a
|
||||
timezone. A new pattern should not imply local-time precision that the source
|
||||
format does not contain.
|
||||
|
||||
### Multi-line messages
|
||||
|
||||
An anchor regex identifies the first line of a message. Subsequent non-anchor
|
||||
lines are appended to that message until another anchor appears. Set
|
||||
`multi_line: true` when continuation content is part of the documented format,
|
||||
such as Markdown bullets, blockquotes, or an exported message body on the next
|
||||
line.
|
||||
|
||||
Tests for a multi-line format should assert the complete message text, including
|
||||
newlines. A message-count assertion alone will not detect lost bullets or a
|
||||
continuation attached to the wrong speaker.
|
||||
|
||||
### Scoring and false positives
|
||||
|
||||
The score compares matched anchors with the pattern's relevant candidate lines.
|
||||
The first pass uses the head of the page for speed. Low-confidence pages are
|
||||
re-scored across the full body before the parser accepts a winner.
|
||||
|
||||
Multi-line formats may opt into `score_continuations_as_body` when their anchor
|
||||
grammar is distinctive. Candidate-only scoring activates only after two anchors
|
||||
match, or when the first non-blank line is an anchor. This evidence threshold
|
||||
lets a single long message keep its continuation body without turning one stray
|
||||
anchor in a prose page into a conversation. Candidate anchor lines that fail the
|
||||
full regex still lower the score. Other patterns continue to use all non-blank
|
||||
lines in their density score.
|
||||
|
||||
Use `score_full_body: true` for a broad grammar that also occurs in ordinary
|
||||
prose. For example, `**Label:** text` can be either a transcript line or a bold
|
||||
label in meeting notes. Narrow formats with a timestamp and a distinctive
|
||||
separator generally do not need this override.
|
||||
|
||||
`quick_reject` is a performance hint, not an acceptance rule. It should cheaply
|
||||
exclude obviously unrelated lines while admitting every string accepted by the
|
||||
main regex.
|
||||
|
||||
## Normalized Slack Markdown
|
||||
|
||||
The `bold-time-dash` pattern parses message anchors shaped like:
|
||||
|
||||
```text
|
||||
**Alice Example** 09:15 — first message
|
||||
- supporting detail
|
||||
**Bob Example** 09:18 — second message
|
||||
```
|
||||
|
||||
Its grammar is:
|
||||
|
||||
```text
|
||||
**speaker** H:MM <dash> text
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
- `H:MM` is a valid 24-hour time from `0:00` through `23:59`.
|
||||
- `<dash>` may be an em dash (`—`), en dash (`–`), or ASCII hyphen (`-`).
|
||||
- The date comes from the resolved page date context.
|
||||
- Continuation lines belong to the preceding message.
|
||||
- The captured clock value is emitted with `Z`. Timezone metadata suppresses
|
||||
the missing-timezone warning but is not currently used for IANA conversion.
|
||||
|
||||
The required time and dash distinguish it from all existing bold-speaker
|
||||
formats:
|
||||
|
||||
- `**Speaker** (09:15): text` uses `bold-paren-time`.
|
||||
- `**Speaker** (9:15 AM): text` uses `bold-paren-time-12h`.
|
||||
- `**Speaker:** text` uses `bold-name-no-time`.
|
||||
- `**Speaker** (2026-04-09 9:15 AM): text` uses `imessage-slack`.
|
||||
|
||||
Keeping these examples in both `test_negative` and parser regression tests makes
|
||||
the non-shadowing contract executable.
|
||||
|
||||
## Adding a built-in format
|
||||
|
||||
1. Collect multiple anonymized examples, including separator and timestamp
|
||||
variants that occur in the same export family.
|
||||
2. Choose the narrowest grammar that represents the format. Constrain numeric
|
||||
fields such as hours and minutes when possible.
|
||||
3. Add at least two positive module-load samples and negative samples for every
|
||||
neighboring pattern that could plausibly overlap.
|
||||
4. Add parser tests that verify speakers, timestamps, text, continuation
|
||||
handling, and non-shadowing behavior.
|
||||
5. Add a dedicated JSONL fixture and include the same cases in
|
||||
`test/fixtures/conversation-formats/all.jsonl`.
|
||||
6. Run the focused parser tests and the fixture evaluator.
|
||||
7. Run the repository verification and full test suites before submission.
|
||||
8. Update `docs/architecture/KEY_FILES.md` when the registry count or supported
|
||||
format inventory changes.
|
||||
|
||||
Use generic fixture identities such as `Alice Example`, `Bob Example`, and
|
||||
`Summary Bot`. Never copy real transcript names or private content into source,
|
||||
tests, documentation, commits, or pull-request descriptions.
|
||||
@@ -75,15 +75,6 @@ Meta-pack stacking creator + investor + engineer via the v0.38
|
||||
preserved — this IS the active pack; the registry walks extends +
|
||||
borrow to materialize the merged view.
|
||||
|
||||
**Merge contract (T20 / #1749).** `resolvePack` merges parent → child
|
||||
(child-wins) for the six ingest/query-shaping fields: `page_types`,
|
||||
`link_types`, `frontmatter_links`, `enrichable_types`, `filing_rules`,
|
||||
and `takes_kinds` (unioned — a child cannot narrow it). `phases` and
|
||||
`calibration_domains` are **NOT** inherited: they gate cycle execution,
|
||||
so each pack must declare its own participation explicitly. That is why
|
||||
`gbrain-everything` re-declares all its phases and all 7
|
||||
`calibration_domains` — inheritance does not carry them.
|
||||
|
||||
Activate via `gbrain config set schema_pack gbrain-everything` and
|
||||
calibration_profile produces all 7 domain scorecards in one JSONB.
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ add `GBRAIN_AUDIT_FULL=1` (v0.43+ TODO; not yet wired).
|
||||
- Per-source pack-upgrade (the handler accepts `sourceId` but
|
||||
`findPackSuccessors` doesn't yet pass it through)
|
||||
- Cross-brain federated mounts that disagree on canonical packs
|
||||
- Automatic rollback (today: manual SQL or `gbrain restore`)
|
||||
- Automatic rollback (today: manual SQL or `gbrain pages restore`)
|
||||
- LLM-assisted mapping_rules codegen from production data (`gbrain
|
||||
schema detect-mappings`; deferred to v0.43+)
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ api_version: gbrain-schema-pack-v1
|
||||
name: my-pack
|
||||
version: 0.0.1
|
||||
gbrain_min_version: 0.39.0
|
||||
extends: gbrain-base # inherits base's TYPES (see Merge contract below); add overrides
|
||||
extends: gbrain-base # inherits everything from base; add overrides below
|
||||
description: |
|
||||
My personal pack.
|
||||
|
||||
@@ -170,34 +170,6 @@ enrichable_types: []
|
||||
filing_rules: []
|
||||
```
|
||||
|
||||
## Merge contract (`extends` + `borrow_from`)
|
||||
|
||||
`resolvePack` composes a pack against its `extends` chain (and any
|
||||
`borrow_from` targets) into the `resolved.manifest` every consumer reads
|
||||
(T20 / #1749). The rules:
|
||||
|
||||
- **Six fields inherit, child-wins:** `page_types`, `link_types`,
|
||||
`frontmatter_links`, `enrichable_types`, `filing_rules`, and `takes_kinds`.
|
||||
A child value with the same key (type name, link name, etc.) overrides the
|
||||
parent's; keys the child doesn't declare come through from the parent.
|
||||
- **`page_types` ordering:** overrides of a base type keep the base's declared
|
||||
position (base's `inferType` prefix priority is authoritative); a genuinely
|
||||
new type — from the child, a `borrow_from`, or a middle pack in the chain —
|
||||
is prepended nearest-first, so a more-derived type's `path_prefix` wins
|
||||
regardless of how deep the chain is.
|
||||
- **`takes_kinds` is UNION, not replace** — it carries a Zod default, so an
|
||||
omitted field is indistinguishable from an explicit one. A child can ADD
|
||||
kinds but **cannot narrow** `takes_kinds` below base ∪ parent. If you need a
|
||||
smaller set, don't `extends` a pack that declares the larger one.
|
||||
- **`phases` and `calibration_domains` are NOT inherited** (child-only). They
|
||||
gate real cycle execution, so each pack must declare its own participation
|
||||
explicitly — inheriting them would silently make a child run phases it never
|
||||
requested. This is why `gbrain-everything` re-declares all its phases and
|
||||
calibration domains by hand. See `lens-packs.md` for the worked example.
|
||||
- **`borrow_from` is selective + non-transitive + fail-closed:** it pulls only
|
||||
the named `types`/`link_types` from the target's OWN declarations (omitting a
|
||||
category borrows none of it); a missing target throws `UnknownPackError`.
|
||||
|
||||
## Recovery + revert
|
||||
|
||||
The single-PR cathedral is hard to revert atomically. Per codex finding
|
||||
@@ -214,7 +186,7 @@ gbrain schema downgrade
|
||||
|
||||
1. `git revert <merge-commit>` — restores the code.
|
||||
2. `gbrain schema downgrade --to gbrain-base` — restores config.
|
||||
3. (Optional) `gbrain purge-deleted --older-than 0h` — drops
|
||||
3. (Optional) `gbrain pages purge-deleted --older-than 0h` — drops
|
||||
v0.39-typed pages that no longer have a matching type in the active
|
||||
pack.
|
||||
|
||||
|
||||
@@ -19,13 +19,11 @@ entire DB from scratch.
|
||||
|
||||
This means:
|
||||
|
||||
- **Disaster recovery is a short, boring sequence.** If your DB volume
|
||||
corrupts, if Postgres eats itself, if PGLite's WASM lock wedges — you
|
||||
don't need a backup. You wipe the derived tables (on PGLite,
|
||||
`gbrain reinit-pglite` wipes the whole embedded DB), re-import from
|
||||
your brain repo with `gbrain sync`, and `gbrain extract all`
|
||||
regenerates the derived state. See "Disaster recovery" below for the
|
||||
exact commands.
|
||||
- **Disaster recovery is one command.** If your DB volume corrupts, if
|
||||
Postgres eats itself, if PGLite's WASM lock wedges — you don't need
|
||||
a backup. You wipe the DB, re-import from your brain repo, and the
|
||||
derived state regenerates. v0.32.3 ships `gbrain rebuild
|
||||
--confirm-destructive` as the documented one-liner.
|
||||
- **Multi-machine sync is git.** Your brain is a repo. Push from one
|
||||
machine, pull from another, and the second machine's DB rebuilds on
|
||||
its next sync. No "back up the database" step.
|
||||
@@ -148,9 +146,11 @@ The promise the rule makes:
|
||||
# Snapshot what's there
|
||||
gbrain stats > /tmp/before.txt
|
||||
|
||||
# Wipe and rebuild — delete the derived tables (pages + content_chunks
|
||||
# survive the CASCADE-safe design), then re-derive from the repo.
|
||||
# On PGLite, `gbrain reinit-pglite` wipes the whole embedded DB instead.
|
||||
# Wipe and rebuild
|
||||
gbrain rebuild --confirm-destructive # v0.32.3 — deletes derived tables
|
||||
# (pages + content_chunks survive
|
||||
# the CASCADE-safe design)
|
||||
# OR manually for v0.32.2:
|
||||
psql -c 'DELETE FROM facts; DELETE FROM takes; DELETE FROM links; DELETE FROM timeline_entries;'
|
||||
gbrain sync
|
||||
gbrain extract all
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# Thin-client routing (remote MCP)
|
||||
|
||||
On-demand reference (see CLAUDE.md Reference map). Current behavior + invariants
|
||||
only; release history lives in `CHANGELOG.md` + git.
|
||||
|
||||
`gbrain init --mcp-only` (v0.29.2) sets up a thin-client install: no local
|
||||
brain content, just an OAuth client pointing at a remote `gbrain serve --http`.
|
||||
v0.29.2/v0.30.0 only refused 9 obvious local-only commands; the other ~25
|
||||
silently fell through to `connectEngine()` and opened the empty local PGLite,
|
||||
returning "No results." against a populated remote brain. v0.31.1 fixes the
|
||||
silent-empty-results bug class for every operation surface.
|
||||
|
||||
Key files:
|
||||
|
||||
- `src/cli.ts` — Routing seam INSIDE the existing op-dispatch path (CDX-1: no
|
||||
parallel `src/core/thin-client/` module; routing is a ~80-line conditional
|
||||
in `runThinClientRouted`). Detects `isThinClient(cfg)` BEFORE `connectEngine`
|
||||
so thin-client installs never open the empty PGLite. localOnly ops on
|
||||
thin-client refuse via `refuseThinClient` (with pinpoint hint table
|
||||
`THIN_CLIENT_REFUSE_HINTS`). Banner via `printIdentityBannerBestEffort`
|
||||
before each routed call (suppressed by `--quiet`, `GBRAIN_NO_BANNER=1`,
|
||||
non-TTY default). Exhaustive TS `never` switch on `RemoteMcpError.reason`
|
||||
for canned, actionable error messages. ENG-2 renderer parity: local-engine
|
||||
path runs `JSON.parse(JSON.stringify(result))` so renderers see the same
|
||||
shape on both paths (kills Date/bigint/Buffer drift class).
|
||||
- `src/core/mcp-client.ts` — `callRemoteTool(config, toolName, args, opts)`.
|
||||
Hardened in v0.31.1 (CDX-4): all transport errors normalized to
|
||||
`RemoteMcpError` via the `toRemoteMcpError` funnel. New `CallRemoteToolOptions
|
||||
{timeoutMs, signal}`; `buildAbortController` composes external signal with
|
||||
timeout. New `RemoteMcpErrorReason` stable union, `RemoteMcpErrorDetail.kind`
|
||||
('timeout' | 'aborted' | 'unreachable') sub-tag, `RemoteMcpErrorDetail.code`
|
||||
field carrying server-supplied error codes (e.g. `missing_scope`).
|
||||
`extractToolErrorCode` parses JSON envelopes first, falls back to substring
|
||||
detection for legacy server messages. `unpackToolResult<T>(res)` unchanged
|
||||
(parses tool-call JSON content). `_clearMcpClientTokenCache()` test escape.
|
||||
- `src/core/cli-options.ts` — `parseGlobalFlags` adds `--timeout=Ns` (accepts
|
||||
`30s`, `2m`, `500ms`, plain ms). Default `null` = per-command default (30s
|
||||
for most ops, 180s for `think`). `parseTimeout(s)` exported helper.
|
||||
- `src/core/doctor-remote.ts` — `gbrain remote doctor` adds the
|
||||
`oauth_client_scopes_probe` check (CDX-5). Probes the read tier via
|
||||
`get_brain_identity` and admin tier via `get_health`; reports per-tier
|
||||
status with pinpoint remediation when admin is missing. `buildScopeCheck`
|
||||
+ `ScopeProbeResult` exported for test access. Skippable via
|
||||
`GBRAIN_DOCTOR_SKIP_SCOPE_PROBE=1` for fixtures that mock /mcp at JSON-RPC
|
||||
initialize level only (MCP SDK Client hangs on shape mismatch).
|
||||
- `src/core/ssrf-validate.ts` (v0.36 Commit 0) — DNS-rebinding-defended URL validation. `validateAndResolveUrl(url)` resolves the hostname via `dns.lookup({all: true, family: 0})`, checks EVERY A AND AAAA record against the internal-IP deny list, returns the resolved IP so callers fetch by IP (defeats DNS rebinding: validation IP === fetch IP). `fetchWithSSRFGuard(url, opts)` does redirect-aware fetching with per-hop re-validation, max 3 hops by default. Reusable across all URL-fetching features. Test seam `__setDnsLookupForTests` for hermetic tests.
|
||||
- `src/core/search/query-intent.ts` extension (v0.36 cross-modal wave) — new `suggestedModality: 'text' | 'image' | 'both'` axis on `QuerySuggestions`. Module-scope `CROSS_MODAL_PATTERNS` regex array (compiles once at module load). `isAmbiguousModalityQuery(query)` heuristic gate fires when a visual noun + reference marker combination indicates genuinely ambiguous routing — used by the Commit 4 LLM tie-break to bound LLM calls to <1% of queries.
|
||||
- `src/core/search/mode.ts` extension (v0.36 cross-modal wave) — `ModeBundle` extended with 7 cross-modal knobs: `cross_modal_both_text_weight` / `cross_modal_both_image_weight` (D6 weighted RRF for `'both'` mode, defaults 0.6/0.4), `image_query_text_refinement_weight` / `image_query_image_refinement_weight` (D13 hybrid intersect for `searchByImage` query refinement, defaults 0.4/0.6), `unified_multimodal` + `unified_multimodal_only` (Phase 3 unified column routing flags), `cross_modal_llm_intent` (Commit 4 opt-in escalation). `SEARCH_MODE_CONFIG_KEYS` extended with 7 corresponding config keys. `KNOBS_HASH_VERSION` bumped 2→3 (D2 — closes the silent cache-hit class where a cached text-mode result could leak to an image-mode caller).
|
||||
- `src/core/search/hybrid.ts` extension (v0.36 cross-modal wave) — cross-modal routing branch at the embed step. Resolves `effectiveModality` from per-call `opts.crossModal` (normalized: literal `'auto'` → undefined per D22-1) → `suggestions.suggestedModality` → `'text'` default. Image route: `embedQueryMultimodal` + `searchVector({embeddingColumn: 'embedding_image'})`, skip expansion + keyword (D9 mode-bundle override). 'both' route: parallel text + image vector searches merged via `rrfFusionWeighted` with `effectiveRrfK(baseRrfK, weight)` from the configured cross-modal weights. Phase 3 unified routing fires when `cfg.search.unified_multimodal === true` — bypasses dual-column branching, runs `embedQueryMultimodal` + `searchVector({embeddingColumn: 'embedding_multimodal'})`, D8 fail-open on zero rows + not strict-mode falls through to dual-column. Commit 4 LLM escalation fires only when (no explicit per-call opt) AND (regex returned 'text') AND (`cfg.search.cross_modal.llm_intent` is true) AND (`isAmbiguousModalityQuery` returns true). Fail-open on every error.
|
||||
- `src/core/search/image-loader.ts` (v0.36 Phase 2) — `loadImageInput(input, opts)` accepts local path, `data:` URI, or `http(s)://` URL. Magic-byte sniff for PNG/JPEG/WebP. Hard size cap (default 10 MB, configurable via `search.image_query.max_bytes`). For URLs: routes through `fetchWithSSRFGuard` so DNS rebinding + redirect chains are defeated. Pre-flight Content-Length check + post-fetch size guard for lying servers. `ImageLoadError` with discriminated `code` (INVALID_FORMAT / OVERSIZED / INVALID_URL / FETCH_FAILED / TIMEOUT / SSRF_BLOCKED / NOT_FOUND).
|
||||
- `src/core/search/by-image.ts` (v0.36 Phase 2) — `searchByImage(engine, input, opts)`. Always runs image branch (`embedQueryMultimodalImage` + `searchVector(embedding_image)`). D13 hybrid intersect: when caller provides optional `query`, runs parallel text branch via `embedQueryMultimodal(query)` and merges via `rrfFusionWeighted` with weights from resolved mode. Phase 3 widens to unified column once `search.unified_multimodal=true` (transparently upgrades the retrieval quality post-reindex).
|
||||
- `src/core/spend-log.ts` (v0.36 Phase 2 D23-#6) — per-OAuth-client paid-API spend tracking against the `mcp_spend_log` table (migration v74). `checkBudget(engine, clientId, capCents)` is the pre-flight gate; throws `BudgetExceededError` when today's spend has hit the cap. `recordSpend(engine, entry)` is best-effort post-call. UTC day-aligned aggregation so caps roll over deterministically regardless of server timezone. Local CLI callers (no clientId) bypass the gate. Pre-v0.36 brains without the table fail open to spend=0. `VOYAGE_MULTIMODAL_3_PER_IMAGE_CENTS` = 0.12 cents per image embed.
|
||||
- `src/core/search/llm-intent.ts` (v0.36 Commit 4) — opt-in LLM tie-break. `classifyModalityWithLLM(query, fallback)` routes through `gateway.chat()` with a fixed single-word-output system prompt. 1s timeout via AbortController. `parseModality(raw, fallback)` is the pure parser — tolerates trailing punctuation + casing. Fail-open on every error (gateway unavailable, timeout, parse failure, unrecognized output) — returns fallback so a misbehaving LLM can never break search. Cost-bounded by the ambiguity heuristic in `query-intent.ts` (fires <1% of queries when on).
|
||||
- `src/commands/reindex-multimodal.ts` (v0.36 Phase 3) — `gbrain reindex --multimodal [--limit N] [--dry-run] [--cost-estimate] [--no-embed] [--yes] [--json]`. Walks `content_chunks WHERE embedding_multimodal IS NULL`, batches via `embedMultimodalSafe` (Commit 0 partial-failure-aware), persists. D7 lock acquisition via `tryAcquireDbLock('gbrain-reindex-multimodal', 360min)`. Cost prompt + 10s Ctrl-C grace window in TTY. `GBRAIN_NO_REEMBED=1` bypass. Checkpoint at `~/.gbrain/reindex-multimodal-checkpoint.json` for resume. D23-#2 auto-flip prompt at coverage=100% completion (TTY: interactive; non-TTY: stderr hint with paste-ready command).
|
||||
- `src/core/backfill-registry.ts` extension (v0.36) — new `modality` backfill kind. SQL filter requires `chunk_source='image_asset'` AND `embedding_image IS NOT NULL` AND `(modality IS NULL OR modality != 'image')`. D22-7 defensive guard: never flag a non-image chunk that happens to have `embedding_image` populated. Idempotent — second run finds zero rows.
|
||||
- `src/core/migrate.ts` v74 (`mcp_spend_log`) + v75 (`embedding_multimodal_column`) — Phase 2 spend-log table + Phase 3 unified column ALTER. v75 is column-only (no HNSW index — deferred to post-reindex per pgvector best practice). v74 uses BTREE on `(client_id, created_at)` + `(token_name, created_at)` — `date_trunc('day', TIMESTAMPTZ)` is NOT IMMUTABLE so can't appear in index expressions; range scan on created_at covers the per-day rollup query.
|
||||
- `src/core/operations.ts` — `get_brain_identity` op (read scope, no params,
|
||||
banner-only): cheap counter packet `{version, engine, page_count,
|
||||
chunk_count, last_sync_iso}` for the thin-client identity banner. Reuses
|
||||
`engine.getStats()`; banner's 60s client-side TTL bounds frequency to
|
||||
≤1/60s per CLI process (well below the Fly.io health-check cadence that
|
||||
motivated the original `getStats` cost warning).
|
||||
- `src/commands/{salience,anomalies,graph-query,think}.ts` — Per-command
|
||||
thin-client routing branches. These commands bypass the operation-layer
|
||||
dispatch in cli.ts (call `engine.foo()` directly), so each gets its own
|
||||
`if (isThinClient(cfg)) { callRemoteTool(...) }` branch that maps CLI flags
|
||||
to op params. `think` is a special case: the server's `think` op
|
||||
intentionally disables `--save`/`--take` for remote callers
|
||||
(operations.ts:1103-1135 trust-boundary gate); thin-client `think` warns
|
||||
loudly when those flags are set.
|
||||
@@ -108,8 +108,8 @@ Every primitive ships with a documented rollback:
|
||||
| Operation | Rollback |
|
||||
|-----------|----------|
|
||||
| Retype | `frontmatter.legacy_type = <original>` preserved on every page (D8). One SQL UPDATE restores types: `UPDATE pages SET type = frontmatter->>'legacy_type' WHERE frontmatter ? 'legacy_type'`. |
|
||||
| Page-to-link | Source page soft-deleted with 72h TTL. `gbrain restore <slug>` within 72h. Link row stays harmless if source restored. |
|
||||
| Page-to-alias | Source page soft-deleted with 72h TTL. `gbrain restore <slug>` within 72h. Alias row stays harmless (or `DELETE FROM slug_aliases WHERE alias_slug = <slug>` to clean up). |
|
||||
| Page-to-link | Source page soft-deleted with 72h TTL. `gbrain pages restore <slug>` within 72h. Link row stays harmless if source restored. |
|
||||
| Page-to-alias | Source page soft-deleted with 72h TTL. `gbrain pages restore <slug>` within 72h. Alias row stays harmless (or `DELETE FROM slug_aliases WHERE alias_slug = <slug>` to clean up). |
|
||||
| Active-pack flip | `gbrain schema use gbrain-base` reverses the flip. |
|
||||
|
||||
## What if my brain doesn't fit?
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
# Community Ideas Ledger
|
||||
|
||||
> A diary of the **valuable ideas** surfaced by the community-PR wave, kept so that
|
||||
> good thinking survives even when the PR that carried it is closed. gbrain moves
|
||||
> fast and the maintainer's "cathedral" rewrites supersede most individual PRs —
|
||||
> but the *idea* behind a closed PR is often still worth something.
|
||||
>
|
||||
> **Bar for this file:** an idea only earns a line if it is (a) still live on
|
||||
> master and (b) genuinely valuable to gbrain users. **Graduating an idea to
|
||||
> `TODOS.md` is a higher bar still** — it must serve the North Star (next-Postgres-
|
||||
> for-memory: widest coverage, best-for-the-most-at-the-least) and be worth a
|
||||
> maintainer-owned implementation. Most lines here will never graduate. That's fine.
|
||||
>
|
||||
> Status legend: **OPEN** = PR still open as a real merge candidate · **CLOSED** =
|
||||
> PR closed, idea captured here · **HELD** = strategic, awaiting maintainer call.
|
||||
> Provenance is credited to the contributor; scrub real private-network names per
|
||||
> the repo privacy rule when anything here graduates to a public artifact.
|
||||
|
||||
_Generated from a full triage of the open-PR backlog (436 community PRs), 2026-06-07._
|
||||
|
||||
---
|
||||
|
||||
## 1. Internationalization — non-English brains are second-class
|
||||
|
||||
The single biggest coverage gap for "serve a billion people." Several independent
|
||||
contributors hit the same walls.
|
||||
|
||||
- **Configurable FTS language** (#580/#581/#582, @rafaelreis-r) — **OPEN, high.**
|
||||
Every `to_tsvector`/`tsquery` is hardcoded `'english'` (query side, trigger side,
|
||||
and no reindex path), so non-English brains run every search through the English
|
||||
stemmer. A coherent 3-PR set: `GBRAIN_FTS_LANGUAGE` config → migration recreating
|
||||
triggers with the chosen language → `gbrain reindex-search-vector` to change it
|
||||
post-install. **Strongest i18n candidate to graduate.**
|
||||
- **Full-Unicode slugs** (#782, @tamagodo-fu; #514 zh, @JimmyJiang67) — **HELD, high.**
|
||||
CJK slugs already work (`CJK_SLUG_CHARS`); generalize to all scripts (Cyrillic,
|
||||
Devanagari, Hangul, …) and widen the remaining ASCII-only validators so non-ASCII
|
||||
slugs flow end-to-end instead of being generated then rejected. #514 also carries a
|
||||
corpus-driven `relationships-zh.json` verb dictionary for `inferLinkType` — a
|
||||
reusable artifact for Chinese relationship typing.
|
||||
- **CJK entity extraction** (#1637, @alkalide) — **OPEN, high.** Mention extraction is
|
||||
ASCII-only (`TOKEN_RE`, `MIN_NAME_LENGTH=4`), so 2–3 char Chinese/Japanese/Korean
|
||||
names are invisible to the gazetteer (there's an in-code TODO acknowledging it).
|
||||
CJK detection + lower min-length + single-token pure-CJK titles + substring pass.
|
||||
|
||||
## 2. Reliability — the daily-driver failure modes
|
||||
|
||||
Recurring, production-observed failures. Many are tiny fixes with outsized impact;
|
||||
these are the densest source of real bugs in the whole backlog.
|
||||
|
||||
- **Embedding egress waste** (#347/#460, @notjbg) — **OPEN, high.** `getChunks` does
|
||||
`SELECT cc.*`, shipping the ~6KB pgvector embedding that `rowToChunk` immediately
|
||||
discards — ~19–22 GB/day egress on a busy Supabase brain. Enumerate the columns;
|
||||
add a CI guard. (#460 dup of #347.)
|
||||
- **Body-keyed embedding reuse** (#1424, @defenestrate2) — **OPEN, high.** Markdown
|
||||
import re-embeds byte-identical chunks that merely shifted position, turning a
|
||||
cosmetic edit into ~99K wasted re-embeds. Reuse by chunk-text hash like the code
|
||||
path already does; add `--force` + a no-hash sentinel.
|
||||
- **`embed --stale` full re-pull** (#775, @kyledeanjackson) — **CLOSED (partial on
|
||||
master), high.** Re-pulled all chunks every cycle (~3TB/mo egress); steady-state
|
||||
brains should do near-zero work. Master added a `countStaleChunks` early-exit;
|
||||
verify it fully closes this.
|
||||
- **Config round-trip storm** (#1694, @Omerbahari) — **OPEN, high.** A single query
|
||||
fires ~85 serial single-key config `SELECT`s — invisible on PGLite, ~85 network
|
||||
RTTs on a remote pooler. Batch + cache `getConfig` (`getConfigMany`).
|
||||
- **cgroup-aware worker sizing** (#1244, @tyler3k1) — **OPEN, high.** `defaultWorkers()`
|
||||
sizes from `os.totalmem()` (host RAM), so containerized installs (Railway/Fly/Render/
|
||||
Cloud Run/ECS) oversize the pool and get OOM-killed mid-import. Use
|
||||
`process.constrainedMemory()`.
|
||||
- **Linux memory-pressure throttle** (#556, @chengzehsu) — **OPEN, high.** `os.freemem()`
|
||||
is `MemFree` (excludes reclaimable cache), so healthy containers reject every batch
|
||||
job. Read `MemAvailable` from `/proc/meminfo`.
|
||||
- **propose_takes never caches empties** (#1218 @AdityaRajeshGadgil / #1760 @notjbg) —
|
||||
**OPEN, high.** A valid `[]` extractor result writes no cache row, so unchanged pages
|
||||
re-spend extractor tokens every ~5min cycle (57,885 calls/11 days observed). Sentinel
|
||||
row keyed on `(source_id, page_slug, content_hash, prompt_version)`.
|
||||
- **Prompt-cache opt-in on hot paths** (#1761, @notjbg) — **OPEN, high.** Only ~4.9% of
|
||||
input tokens hit the Anthropic prompt cache because the highest-volume cycle/extraction
|
||||
call sites don't set `cacheSystem:true` despite gateway support. One-line opt-ins.
|
||||
- **Autopilot reliability cluster** (#232 @ianderse, #464/#465 @notjbg, #289 @RyanAlberts,
|
||||
#477 @vinsew, #1935/#1936 @mdcruz88, #1906/#1891 @rayers/@jalagrange) — **OPEN, high.**
|
||||
A family of distinct live bugs: argless `engine.connect()` wipes saved config and
|
||||
crash-loops under launchd; `cwd=/` wrappers miss `brain/.env`; mtime-only lock probing
|
||||
blocks respawn for 10min after OOM; no backoff on the 5-failure suicide cap;
|
||||
disconnect-before-connect `reconnect()` bricks the engine on a transient blip; config
|
||||
accessors lack the retry wrapper. **Pick the best fix per layer and land as a wave.**
|
||||
- **lint `--fix` corrupts mid-doc fences** (#1417 @trinh-macbook, #1597 @chungty) —
|
||||
**OPEN, high.** Detector/fixer regex disagree, so `lint --fix` strips the closing fence
|
||||
of mid-document ```` ```markdown ```` blocks and autopilot re-corrupts the page every
|
||||
cycle. Only unwrap whole-page fences.
|
||||
- **backlinks worker defaults to `fix`** (#1853 @choomz; #1027 @sliday; #495 @23salus) —
|
||||
**OPEN, high.** Empty-payload backlinks jobs default to `action='fix'`, silently
|
||||
rewriting tracked markdown ("Referenced in" bullets) on every sync→embed→backlinks
|
||||
chain (129 files/day in the wild). Default to `check`; require explicit opt-in. Also
|
||||
fixes a duplicate-line accumulation bug.
|
||||
- **`DATABASE_URL` hijack** (#1884, @awilkinson) — **OPEN, high.** A co-located app's
|
||||
generic `DATABASE_URL` silently overrides the configured brain (wrong DB, or
|
||||
auto-migrates it). Fix precedence: `GBRAIN_DATABASE_URL` > config.json > `DATABASE_URL`.
|
||||
- **Engine-switch strips config** (#1088, @samchaudhary) — **OPEN, high.** `migrate --to`
|
||||
rewrites config to just `{engine,url}`, dropping `embedding_model`/`dimensions`/keys;
|
||||
migration "succeeds" but new embeds break.
|
||||
- **Re-init silently corrupts the brain** (#1060, @vincedk-alt) — **OPEN, high.** Flag-less
|
||||
re-init ignores persisted `embedding_model`/`dimensions` and writes a wrong-shape
|
||||
OpenAI-1536 brain before the dim-check catches it.
|
||||
- **IPv6-only direct URL** (#1006, @diazMelgarejo) — **OPEN, high.** `deriveDirectUrl`
|
||||
turns a Session-Pooler URL into an IPv6-only host, ECONNREFUSED on IPv4-only networks
|
||||
(the majority). Return null for pooler URLs.
|
||||
- **HOME-isolation in tests** (#205/#517/#534 @orendi84, #434 @lloydarmbrust) — **OPEN,
|
||||
high.** The E2E suite spawns `gbrain init/import` against the developer's real
|
||||
`~/.gbrain/config.json`, clobbering their live DB URL+keys. Isolate HOME to a tmpdir.
|
||||
*(A footgun that bites contributors of this very repo.)*
|
||||
- **dim-aware embed write target** (#1263, @DmitryBMsk) — **OPEN, high.** `upsertChunks`
|
||||
always writes the legacy `embedding vector(1536)` column, so brains on an alternate
|
||||
column (`embedding_ze halfvec(2560)`) fail with dim-mismatch on every write.
|
||||
- **Oversized chunks silently unembedded** (#1675, @lubos-buracinsky) — **OPEN, high.**
|
||||
The code chunker emits giant literals/template strings whole; the embedder rejects
|
||||
them and they vanish from semantic search. Cap chunk size so they stay embeddable.
|
||||
- **Token-vs-char truncation** (#557 @chengzehsu, #990 @mgunnin, #1180 @kkroo,
|
||||
#1281 @mmekkaoui, #1947 @100menotu001) — **OPEN, high.** The embed path truncates by
|
||||
chars (`MAX_CHARS`) not tokens, so dense pages still exceed the 8192/300K-token ceiling
|
||||
and loop forever on HTTP 400 with `embedded_at` never cleared; `isTokenLimitError`
|
||||
misses OpenAI's real error string; llama-server's 32-input limit isn't capped; and
|
||||
`--catch-up`'s unbounded budget overflows the 32-bit `setTimeout` and aborts after one
|
||||
batch. A "make embedding backfills never silently wedge" cluster.
|
||||
|
||||
## 3. Search & retrieval quality
|
||||
|
||||
- **Keyword search ignores page titles** (#1646, @jeades) — **OPEN, high.** `searchKeyword`
|
||||
ranks only chunk `search_vector`, never `pages.search_vector` (weight-A titles), so an
|
||||
exact-title `gbrain search` returns nothing while `query` finds it. High-impact, tiny.
|
||||
- **`code-def` misses most OO symbols** (#1628, @rayers) — **OPEN, high.** `DEF_TYPES`
|
||||
omits method/constructor/field/struct/protocol, so `code-def` returns 0 for most
|
||||
object-oriented code. Root-cause fix in `normalizeSymbolType` + `DEF_TYPES`.
|
||||
(Prefer over #1701's fallback-only approach.)
|
||||
- **doc-comment column is wired but dead** (#520, @Evode-Manirahari) — **OPEN, high.** FTS
|
||||
weights `content_chunks.doc_comment` above chunk text but the column is never populated.
|
||||
Extract JSDoc/docstrings per symbol via AST and thread through import.
|
||||
- **autocut weak-top collapse** (#1863, @rayers) — **OPEN, high.** The fresh autocut
|
||||
feature (#1682) normalizes the rerank gap by the top score, so a weak top (0.317→1.0)
|
||||
looks like a confident cliff and rare cross-source queries collapse to 1 result. Add a
|
||||
`minTopScore` floor.
|
||||
- **Graph-hop wikilink rerank** (#717, @gwanghoon91) — **HELD, high.** Zero-token
|
||||
score-shapers (graph-hop wikilink rerank + query-token disambiguation) claimed
|
||||
+2.6/+2.8pt P@5/R@5 on BrainBench. Worth re-evaluating against the new retrieval
|
||||
cathedral's ranker rather than merging the old diff.
|
||||
- **Effective-date time filters** (#1706, @mvanhorn) — **OPEN, med.** `since`/`until`
|
||||
filter on `updated_at`, so content dated to the past but edited recently is mis-filtered;
|
||||
filter on `COALESCE(effective_date, updated_at, created_at)`.
|
||||
|
||||
## 4. Extraction & the knowledge graph
|
||||
|
||||
- **Obsidian wikilink → typed graph edges** (#87 @franmaranchello; alias/title/basename
|
||||
fallback #1188 @rwbaker) — **OPEN/HELD, high.** `[[wikilinks]]`/`![[embeds]]` are
|
||||
invisible to the graph. Materialize them as typed edges with alias (frontmatter
|
||||
`aliases:`), first-H1-title, and basename fallback resolution (path-equality-only gives
|
||||
~5.5% edge recall on real vaults). Master shipped global-basename (#1388); the alias/
|
||||
title fallbacks are the still-novel part.
|
||||
- **Schema-pack-aware link extraction** (#1547, @billy-armstrong) — **OPEN, high.** The
|
||||
link extractor's `DIR_PATTERN` is a frozen 16-prefix const that ignores pack-declared
|
||||
`path_prefixes`, so default-pack installs silently lose wikilinks to `person/`,
|
||||
`writing/`, `wiki/*`. Resolve prefixes from the active pack.
|
||||
- **DB-source extraction** (#1539, @afshaker) — **OPEN, high.** The cycle's extract phase
|
||||
only walks the filesystem, so DB-resident pages (imported transcripts, remote-DB brains)
|
||||
never get links/timeline and `brain_score` is capped. Thread `source:'db'`.
|
||||
- **source_id threaded through fs-walk extract** (#1719, @seungsu-kr) — **OPEN, high.**
|
||||
fs-walk extractors omit `source_id`, defaulting to `'default'`, so the `pages` INNER JOIN
|
||||
drops every row on non-default-source brains — silent 0 inserted.
|
||||
- **extract `--stale` permanent-lag loop** (#1791, @Nazim22) — **OPEN, high.** Pages last
|
||||
edited before the link-extractor version bump get stamped below the version threshold and
|
||||
re-flag every run (~97% pages permanently "stale"). Stamp `GREATEST(updated_at, versionTs)`.
|
||||
- **Plain-text NER for auto-link** (#1565, @donogeme) — **HELD, med.** Plain mentions of
|
||||
people (no `[[wikilink]]`) never become edges. The opt-in idea is right; the shipped
|
||||
implementation (capitalized-bigram regex, Western-names-only) is too crude — needs a
|
||||
real NER pass to clear the graph-integrity bar.
|
||||
|
||||
## 5. Providers & the gateway
|
||||
|
||||
The AI-gateway + recipes + `user_provided_models` system already absorbed ~40
|
||||
per-vendor embedding PRs (Ollama, Gemini, Azure, DashScope, DeepSeek, Zhipu, E5,
|
||||
bge-m3, Copilot, Composio, Kimi, LM Studio, Mistral, Hunyuan, MiniMax…). The
|
||||
*residue* worth keeping:
|
||||
|
||||
- **litellm proxy unusable for chat** (#1953 @miroslavb, #1938 @BKF-Gitty) — **OPEN, high.**
|
||||
The `litellm-proxy` recipe declares only an embedding touchpoint (no chat), so
|
||||
`chat_model=litellm:*` fails validation and `think` degrades to a misleading "set
|
||||
ANTHROPIC_API_KEY"; and `build-gateway-config` never folds `litellm/openrouter/together`
|
||||
keys, so configured proxy auth goes out unauthenticated. Plus user-provided custom-dim
|
||||
embeddings are double-false-rejected in preflight. **The general-OpenAI-compat-proxy
|
||||
story.**
|
||||
- **Matryoshka dims threading** (#1072 @mgandal, #1240 @mike7seven) — **OPEN, high.**
|
||||
Qwen3-Embedding returns its native dim (2560/4096) not the requested one because
|
||||
`dimensions:N` isn't threaded for the openai-compat path, hard-failing a 1536-dim brain.
|
||||
- **"Freeze provider at init, clear vectors on dim change"** (#100/#172, @niallobrien/
|
||||
@nbzy1995) — **CLOSED, med.** A safety insight worth keeping even though the provider
|
||||
PRs are superseded: persist+freeze the brain's provider/dim at init so a later env change
|
||||
can't silently corrupt the vector space; clear stale embeddings on an intentional change.
|
||||
- **China-region provider coverage** (#59 @Magicray1217, #1071 @AzeWZ) — **CLOSED, med.**
|
||||
Make DashScope/DeepSeek/Zhipu first-class recipes that honor `provider_base_urls` (the
|
||||
China-region endpoints) and provider batch limits — on-mission for global coverage.
|
||||
- **Amazon Bedrock native** (#1826, @naterchrdsn) / **Jina asymmetric retrieval**
|
||||
(#1930, @Whamp) — **HELD, high/med.** The maintainer pattern prefers the universal
|
||||
litellm-proxy over per-vendor native recipes, but Bedrock (AWS IAM credential chain) and
|
||||
Jina's asymmetric `input_type=document|query` are distinct enough to warrant a call.
|
||||
- **Local-first chat parity** (#1854/#1855/#1858 @starm2010, #1423 @pabloglzg,
|
||||
#1618 @punksterlabs) — **OPEN, high.** `FREE_LOCAL_CHAT_PROVIDERS` doesn't exist (only
|
||||
embed), brainstorm/cycle/takes hardcode `anthropic:claude-sonnet-4-6`, and the
|
||||
openai-compat `generateObject` path silently fails on providers that reject
|
||||
`json_schema`. The "run gbrain fully local" cluster.
|
||||
- **OpenRouter config key** (#1714 @tmchow), **OAuth bearer for AI providers**
|
||||
(#1312 @pabloglzg), **API-key files** (#570 @shawnduggan) — **OPEN, med.** Credential
|
||||
ergonomics: config-file key (not just env), externally-minted bearer tokens, and
|
||||
`OPENAI_API_KEY_FILE` so OAuth harnesses don't inherit a raw key in `process.env`.
|
||||
|
||||
## 6. Auth, federation & access control (security-adjacent)
|
||||
|
||||
These cluster into a real theme: **runtime access control for remote/multi-tenant MCP
|
||||
beyond prompt discipline.** Several are live security gaps (see the security list in the
|
||||
triage report) and should be treated as a coordinated design, not piecemeal merges.
|
||||
|
||||
- **Clamp remote source overrides** (#1372, @jlfetter1) — **OPEN, high, SECURITY.** A
|
||||
remote MCP caller can pass `source_id` (or `__all__`) to `query`/`get_page` to read
|
||||
sources outside their OAuth `allowedSources` — the param bypasses `sourceScopeOpts`
|
||||
(CWE-285). Clamp to token claims, fail-closed. **#1394 (get_page source_id) must land
|
||||
*with* this clamp, not before it.**
|
||||
- **Read-side prefix/federation enforcement** (#1860 @choomz, #1790 @colin-atlas,
|
||||
#470 @AdityaRajeshGadgil, #1508 @tim404x) — **OPEN, high.** `bound_slug_prefixes` is
|
||||
enforced on write but not read; exact `get_page` uses scalar `ctx.sourceId` while fuzzy
|
||||
uses the federation ladder; unqualified search can scan isolated `--no-federated` sources.
|
||||
Unify on one fail-closed visibility predicate across every read surface.
|
||||
- **Per-OIDC-user access tiers** (#789, @0x471) — **HELD, high, SECURITY.** Map verified
|
||||
OIDC end-users to `oauth_clients.access_tier` dispatch gates + shape filters — real
|
||||
runtime access control. Pairs with multi-agent MCP hardening (#1316, @chipoto69, HELD).
|
||||
- **Federated-read management CLI + admin UI** (#1592/#1601 @bitak1, #1558 @flamerged) —
|
||||
**OPEN, high.** No CLI/UI to inspect or change a client's `federated_read` scope (raw
|
||||
SQL only today). Atomic `array_append`/`array_remove` SQL to avoid read-modify-write
|
||||
races, plus an admin Sources tab.
|
||||
- **Pre-registration flow flags** (#894, @panda850819) — **OPEN, high, SECURITY.**
|
||||
`register-client` hardcodes `redirect_uris=[]`, making the SECURITY.md-recommended
|
||||
pre-registration (DCR-off) flow unusable for Claude.ai/ChatGPT connectors.
|
||||
- **RFC 9728 `resource_metadata`** (#1410, @rayers) — **OPEN, high.** HTTP MCP 401s omit
|
||||
the `resource_metadata` param the MCP auth spec + RFC 9728 require, so claude.ai/Cursor
|
||||
can't discover the auth server and never start OAuth.
|
||||
- **Server-enforced memory groups** (#1497, @oldmate99) — **HELD, med.** Audience-based
|
||||
read/write via `memory_groups` + client-to-group assignment — strategic for hosted
|
||||
multi-tenant, but overlaps the existing source-isolation model; a design call.
|
||||
|
||||
## 7. Security hardening (must not be lost)
|
||||
|
||||
- **Command injection in transcription** (#245, @aliceagent) — **OPEN, high, SECURITY.**
|
||||
`transcription.ts` shell-interpolates an agent-controlled `audioPath` into `execSync`
|
||||
ffprobe/ffmpeg/`rm -rf`. **Confirmed still present on master.** Switch to
|
||||
`execFileSync` arg arrays + `fs.rmSync`.
|
||||
- **Dotfile / skills-dir confinement** (#418/#419, @garagon) — **OPEN, high, SECURITY.**
|
||||
`.gbrain-source` walk-up trusts any ancestor dotfile (source hijack on shared hosts);
|
||||
`resolveWorkspaceSkillsDir` never canonicalizes (symlink escape). `lstat` ownership/
|
||||
symlink/world-writable checks + realpath containment.
|
||||
- **Destructive reclone gate** (#1705, @mvanhorn) — **OPEN, high, SECURITY.**
|
||||
`recloneIfMissing` does `rm`+rename over `src.local_path` without verifying it's
|
||||
gbrain-managed, so a re-pointed source can wipe a user's working tree. Gate behind
|
||||
`isManagedRecloneTarget()` + reject `..`. *(The maintainer's own #1960 is the canonical
|
||||
landing for this class — cross-check.)*
|
||||
- **CORS preflight asymmetry** (#983, @yashkot007) — **OPEN, high, SECURITY.** Preflight
|
||||
returns the full method/header surface unconditionally while the actual-request path
|
||||
gates on the allowlist — leaks allowed surface to non-allowlisted origins.
|
||||
- **jsonb double-encode corruption** (#1584 @warkcod, #597 @vinsew) — **OPEN, high,
|
||||
SECURITY/integrity.** Source-config and subagent writers `JSON.stringify` into a
|
||||
`::jsonb` cast — the exact postgres.js trap CLAUDE.md forbids; corrupts source config
|
||||
(freshness/autopilot) and breaks dream synthesize slug-collection on real Postgres.
|
||||
|
||||
## 8. Developer experience & platform reach
|
||||
|
||||
- **Windows / CRLF portability** (#1294 @xwang4-svg, #1149 @samporter-31, #1554 @Sanjays2402,
|
||||
#1396 @xuezhaolan) — **OPEN, high.** CRLF breaks frontmatter + skill-trigger parsing
|
||||
(CI is Ubuntu-only so it never surfaces), `/dev/stdin` doesn't exist, a POSIX postinstall
|
||||
one-liner hard-fails `bun install`, backslash bundle keys. A coordinated "first-class
|
||||
Windows" pass. *(A working Windows binary + CI target #180/#181 is the prerequisite for
|
||||
the full story.)*
|
||||
- **`.gbrainignore` / per-repo exclusion** (#1483 @eepaul; repo-local code filters
|
||||
#1011 @AndrewLauder; `--respect-gitignore` #1159 @jetsetterfl) — **OPEN, high.** Sync
|
||||
indexes every file with no ignore mechanism (`data/`, `*.parquet`, fixtures, vendored
|
||||
trees), bloating DB + embedding cost. gitignore-parity `.gbrainignore` + per-source
|
||||
`excludePatterns`. *(See also the maintainer's walker-prune work; #1942 prunes
|
||||
vendor/dist/build.)*
|
||||
- **Monorepo sub-path sources** (#774, @jeremyknows) — **HELD, high.** `--src-subpath`
|
||||
(split repo into git-root + logical-source axes) + `--exclude` so one repo can hold N
|
||||
sources at subdirs.
|
||||
- **MCP tool filtering** (#747, @joelwp) — **OPEN, high.** MCP advertises all ~51 ops to
|
||||
every consumer (~10K tokens of schemas, tool confusion); `GBRAIN_EXPOSED_TOOLS` filters
|
||||
the advertised surface.
|
||||
- **Install-method detection for upgrade** (#538, @brucek) — **OPEN, high.** The README's
|
||||
own recommended git-clone+bun-link install detects as `unknown`, so `gbrain upgrade`
|
||||
offers three dead ends including a wrong npm package.
|
||||
- **Runtime subagent defs** (#1282, @dcarolan1) — **OPEN, high.** The plugin loader
|
||||
validates `SubagentDefinition[]` at startup but the handler never reads
|
||||
`data.subagent_def`, so the persisted field is dead at runtime — callers must re-embed
|
||||
the full system body in every job.
|
||||
- **macOS Tahoe PGLite workaround** (#1671, @roysaurav) — **HELD, med.** PGLite's WASM
|
||||
engine crashes on macOS 26 (Apple Silicon); document the native Homebrew Postgres+pgvector
|
||||
fallback. Reader-valuable until the WASM crash is fixed upstream.
|
||||
|
||||
## 9. Capabilities & integrations (strategic — maintainer call)
|
||||
|
||||
These are net-new surfaces held for a product decision, not auto-closed.
|
||||
|
||||
- **Alternative engines** — SQLite/`bun:sqlite`+FTS5 single-file backend (#291, @mvanhorn)
|
||||
and Neo4j GraphBrain REST backend (#594, @pkyanam). Both conflict with the two-engine
|
||||
lockstep invariant and the Postgres-for-memory North Star, but the *zero-WASM single-file*
|
||||
install story (SQLite) is strategically interesting. **HELD.**
|
||||
- **Page versioning / soft-delete / read audit** (#573, @cropsgg) — **HELD, high.** Snapshots
|
||||
with provenance, soft-delete tombstones + hard purge, read-path audit treating edits as
|
||||
derivative works. Ambitious cathedral-scope; maintainer-owned territory.
|
||||
- **Configurable embedding dimension** (#1051, @vincedk-alt) — **HELD, high.** `schema.sql`
|
||||
hardcodes `vector(1536)`; read `embedding_dimensions` from config (default 1536). The
|
||||
canonical fix that dozens of local-provider PRs hack around. *(Pairs with #1263.)*
|
||||
- **Transcribe skill** (#1449, @RyanAlberts) — **OPEN, high.** Implements the empty
|
||||
video/audio branch of `media-ingest` (YouTube captions fast path + yt-dlp/whisper
|
||||
fallback), $0 by default. A genuine capability gap.
|
||||
- **iPhone backup importer** (#1733, @H4RR1SON) — **HELD, med.** Local-CLI-only importer
|
||||
for decrypted iPhone backups (contacts→person pages, iMessage→conversation pages); zero
|
||||
network, thin-client refused.
|
||||
- **Compounding dream phase** (#509, @durang) — **HELD, high.** An LLM "7th phase" that
|
||||
*creates* structure (orphan-mention people, knowledge gaps, concept-dup at cosine>0.92,
|
||||
decay, incomplete pages) vs the deterministic phases. Overlaps `enrich --thin`.
|
||||
- **Codex-OAuth for dream** (#977, @barronlroth) / **dream gateway + `migrate-embedding-dim`**
|
||||
(#1013, @cxbitz) — **HELD, high.** OAuth-backed chat for synthesis; a command to resize
|
||||
the vector schema + clear incompatible embeddings.
|
||||
- **Voice-extraction skill** (#300, @harjclaw) — **CLOSED, med.** Mine the user's outbound-
|
||||
email corpus already in the brain to build a queryable writing-voice profile so agents
|
||||
draft in the user's voice. Overlaps soul-audit.
|
||||
- **MCP put_page parity + DB→markdown reconciliation** (#438, @rayzhux) — **HELD, high.**
|
||||
A frontmatter-only safe auto-link mode for remote callers + `GBRAIN_BRAIN_ROOT` to render
|
||||
remote writes back to markdown so MCP writes reach the git source-of-truth. Touches the
|
||||
remote trust boundary — a design proposal, not a merge.
|
||||
- **Recipe discovery convention** (#1279, @ialmeida-jera) — **OPEN, med.** `~/.gbrain/recipes/`
|
||||
auto-discovery + `--external-dir`, loaded untrusted to keep the command-spawn boundary.
|
||||
- **Destructive-op audit trail + audit-factory** (#1069/#1070, @vincedk-alt) — **HELD, med.**
|
||||
Rotating JSONL forensic trail for hard-deletes + a shared `createAuditLogger` factory.
|
||||
|
||||
## 10. Doctor & brain-health observability
|
||||
|
||||
- **Queue dead-job visibility** (#1185, @ethanbeard) — **OPEN, high.** A collector can
|
||||
heartbeat green while all its jobs die in the worker (3561 dead in the wild) and doctor
|
||||
has zero view into the minions queue. Add a cross-cutting `[queue]` dead-jobs check.
|
||||
- **Orphan-metric alignment** (#1107 @colin477, #915 @xaviroblessarries, #1202 @rwbaker) —
|
||||
**OPEN, high.** `get_health` counts ingestion-by-design (`daily/`, briefings), soft-deleted,
|
||||
and hub pages as orphans, distorting `brain_score`; CLI `find_orphans` uses a *different*
|
||||
predicate than `getHealth`. Unify on one islanded predicate with sensible exclusions.
|
||||
- **doctor check-name registry drift** (#1839, @mvanhorn) — **OPEN, med.** Several emitted
|
||||
checks aren't registered in `doctor-categories`, printing `unknown check name` every run;
|
||||
the drift guard only scanned `doctor.ts`, missing `onboard/checks.ts` emitters.
|
||||
- **Honest stale-lock hint** (#1553, @Sanjays2402) — **OPEN, med.** doctor always says
|
||||
`gbrain sync --break-lock`, which silently no-ops on `gbrain-cycle` locks.
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting observations for the maintainer
|
||||
|
||||
- **The same bug was filed many times.** `extract_facts.entity_hints` missing an `items`
|
||||
schema came in ≥5 times (#812/#832/#847/#863/…, already fixed); the Postgres-singleton
|
||||
disconnect class a dozen+ times; sync no-op freshness, slug-casing, and the embedding-
|
||||
preflight false-reject each 5–15 times. A short "already fixed / known" note in the
|
||||
release notes or a CONTRIBUTING "before you file" list would cut the re-file rate.
|
||||
- **The recipe system is working as a pressure valve** — it correctly absorbed ~40 vendor
|
||||
PRs into config rather than code. The remaining provider asks are about *capabilities*
|
||||
the recipe schema doesn't yet express (asymmetric `input_type`, Matryoshka dims, per-item
|
||||
RPM caps, alternative credential groups), not new vendors.
|
||||
- **i18n (§1) and local-first chat (§5) are the two biggest "serve a billion" coverage
|
||||
gaps** the community is repeatedly hitting and the best candidates to graduate to TODOs.
|
||||
@@ -43,8 +43,8 @@ genuinely has to change.
|
||||
Switching dimensions requires:
|
||||
|
||||
1. Dropping the HNSW vector index (pgvector won't survive an `ALTER COLUMN TYPE`).
|
||||
2. Wiping every existing embedding (the old vectors are unusable in the new space — and pgvector refuses to cast them across dimensions, so this must happen before the alter).
|
||||
3. Altering the column type (Postgres only — PGLite cannot do this).
|
||||
2. Altering the column type (Postgres only — PGLite cannot do this).
|
||||
3. Wiping every existing embedding (the old vectors are unusable in the new space).
|
||||
4. Re-embedding the entire corpus (can take hours on a 50K-page brain and costs $1-100 in API calls depending on model).
|
||||
5. Conditionally recreating the index (HNSW supports up to 2000 dimensions per pgvector; above that you must use exact scans).
|
||||
|
||||
@@ -115,17 +115,12 @@ BEGIN;
|
||||
-- 1. Drop the HNSW index. It can't survive the column type change.
|
||||
DROP INDEX IF EXISTS idx_chunks_embedding;
|
||||
|
||||
-- 2. Clear stale embeddings FIRST. This must happen BEFORE the column
|
||||
-- alter: pgvector refuses to cast existing vectors across dimensions
|
||||
-- ("expected <NEW_DIMS> dimensions, not <OLD_DIMS>"), so altering a
|
||||
-- column that still holds old-width vectors aborts the transaction.
|
||||
-- NULLs cast fine. (The old vectors are unusable in the new space
|
||||
-- anyway — this is the wipe step from the rationale above.)
|
||||
UPDATE content_chunks SET embedding = NULL, embedded_at = NULL;
|
||||
|
||||
-- 3. Alter the column type (all rows are NULL now, so the cast succeeds).
|
||||
-- 2. Alter the column type.
|
||||
ALTER TABLE content_chunks ALTER COLUMN embedding TYPE vector(<NEW_DIMS>);
|
||||
|
||||
-- 3. Clear stale embeddings so they don't survive into the new space.
|
||||
UPDATE content_chunks SET embedding = NULL, embedded_at = NULL;
|
||||
|
||||
-- 4. Recreate the HNSW index ONLY IF dims <= 2000. Above that, leave it
|
||||
-- indexless and rely on exact scans (gbrain searchVector handles this
|
||||
-- automatically — search just gets slower, not broken).
|
||||
|
||||
@@ -183,6 +183,6 @@ This also means the best AI agent setups will be open source by default. Closed,
|
||||
|
||||
Software distribution reimagined: the package is a markdown file, the runtime is a sufficiently smart model, the package manager is your AI agent, and the app store is a git repo.
|
||||
|
||||
`gbrain skillpack scaffold voice-agent`
|
||||
`gbrain install voice-agent`
|
||||
|
||||
That's it.
|
||||
|
||||
@@ -160,7 +160,7 @@ The mode-picker prompt at `gbrain init` and the CLAUDE.md `## Search Mode` table
|
||||
- Your agent's system prompt + reasoning tokens add input that gbrain doesn't see.
|
||||
- Compaction reduces input over a long session.
|
||||
- Most agents make 1-5 searches per turn; cost-per-turn is what bills you, not cost-per-query.
|
||||
- The model price column drifts as providers reprice; pin the rate via `src/core/model-pricing.ts` (the canonical chat-pricing table) for a current snapshot.
|
||||
- The model price column drifts as providers reprice; pin the rate via `src/core/anthropic-pricing.ts` for a current snapshot.
|
||||
|
||||
The picker copy + CLAUDE.md table are the canonical user-facing source. Update them in lockstep when the underlying chunker size or default `searchLimit` changes.
|
||||
|
||||
|
||||
@@ -159,8 +159,7 @@ proxy for worker env.
|
||||
If a brain DB ever traverses a trust boundary, secrets stay out.
|
||||
- **Free-form names.** `inherit:` accepts any snake_case config-key on your
|
||||
worker — `database_url`, `anthropic_api_key`, `openai_api_key`,
|
||||
`openrouter_api_key`, `voyage_api_key`, `groq_api_key`,
|
||||
`zeroentropy_api_key`, or any custom
|
||||
`voyage_api_key`, `groq_api_key`, `zeroentropy_api_key`, or any custom
|
||||
field you stuff into `~/.gbrain/config.json`. The agent picks what it
|
||||
needs.
|
||||
- **`env:` still works** for non-secret values, or for cases where you
|
||||
|
||||
@@ -69,7 +69,7 @@ update_brain_page(slug, new_info, source):
|
||||
page = gbrain get {slug}
|
||||
|
||||
// TIMELINE: always APPEND (never edit existing entries)
|
||||
gbrain timeline-add {slug} {
|
||||
gbrain add_timeline_entry {slug} {
|
||||
date: today,
|
||||
summary: new_info.summary,
|
||||
detail: new_info.detail,
|
||||
|
||||
@@ -46,10 +46,10 @@ on user_shares_media(url_or_file):
|
||||
|
||||
# Step 4: Extract and cross-reference entities
|
||||
for person in transcript.mentioned_people:
|
||||
gbrain link <slug> <person_slug>
|
||||
gbrain link <person_slug> <slug>
|
||||
gbrain timeline-add <person_slug> {date} \
|
||||
"Discussed in {video_title}: {what_was_said}" \
|
||||
gbrain add_link <slug> <person_slug>
|
||||
gbrain add_link <person_slug> <slug>
|
||||
gbrain add_timeline_entry <person_slug> \
|
||||
--entry "Discussed in {video_title}: {what_was_said}" \
|
||||
--source "YouTube: {url}"
|
||||
|
||||
# PATTERN 2: Social Media Bundles
|
||||
@@ -80,8 +80,8 @@ on user_shares_media(url_or_file):
|
||||
|
||||
# Extract entities and cross-reference
|
||||
for entity in bundle.mentioned_entities:
|
||||
gbrain link <slug> <entity_slug>
|
||||
gbrain link <entity_slug> <slug>
|
||||
gbrain add_link <slug> <entity_slug>
|
||||
gbrain add_link <entity_slug> <slug>
|
||||
|
||||
# PATTERN 3: PDFs and Documents
|
||||
elif media.type == "pdf" or media.type == "document":
|
||||
@@ -109,8 +109,8 @@ on user_shares_media(url_or_file):
|
||||
"""
|
||||
|
||||
for entity in document.mentioned_entities:
|
||||
gbrain link <slug> <entity_slug>
|
||||
gbrain link <entity_slug> <slug>
|
||||
gbrain add_link <slug> <entity_slug>
|
||||
gbrain add_link <entity_slug> <slug>
|
||||
|
||||
# Always sync after ingestion
|
||||
gbrain sync
|
||||
@@ -127,7 +127,7 @@ on user_shares_media(url_or_file):
|
||||
## How to Verify
|
||||
|
||||
1. Ingest a YouTube video. Run `gbrain get media/youtube/{slug}`. Confirm the page has: the agent's analysis (not just a summary), key quotes with speaker attribution, and the full diarized transcript.
|
||||
2. Run `gbrain call get_links '{"slug": "media/youtube/{slug}"}'`. Confirm back-links exist to brain pages for every person and company mentioned in the video.
|
||||
2. Run `gbrain get_links media/youtube/{slug}`. Confirm back-links exist to brain pages for every person and company mentioned in the video.
|
||||
3. Pick a person mentioned in the video. Run `gbrain get <person_slug>`. Confirm their timeline has a new entry referencing the video with specific context.
|
||||
4. Ingest a tweet. Confirm the brain page includes the thread context, linked article summaries, and entity cross-references -- not just the tweet text.
|
||||
5. Run `gbrain search "{topic_from_video}"`. Confirm the media page appears in search results (verifies the content is indexed and searchable).
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
# Embedding migration — moving a brain to another embedding provider
|
||||
|
||||
`gbrain migrate embeddings` re-embeds an entire brain onto a different
|
||||
embedding provider/model, safely and resumably. It is the forward path off a
|
||||
sunsetting provider (for example ZeroEntropy's hosted API, which shuts down
|
||||
2026-09-04 and is the shipped default for brains that never picked a model) —
|
||||
but it is provider-agnostic: any configured `provider:model` works as a
|
||||
target.
|
||||
|
||||
Also reachable as `gbrain retrieval-upgrade` (the name `doctor` and the
|
||||
README reference).
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Preview the work + cost. Changes nothing.
|
||||
gbrain migrate embeddings --to openai:text-embedding-3-small --dry-run
|
||||
|
||||
# Run it (interactive confirm shows chunk count + $ estimate first).
|
||||
gbrain migrate embeddings --to openai:text-embedding-3-small
|
||||
|
||||
# Non-interactive (cron / scripts): --yes is required, else exit 2.
|
||||
gbrain migrate embeddings --to voyage:voyage-3-large --yes
|
||||
```
|
||||
|
||||
`--dim <N>` overrides the target width; it defaults to the provider recipe's
|
||||
declared width and is required for recipes that don't declare one (litellm,
|
||||
llama-server, and other bring-your-own-model providers).
|
||||
|
||||
## What it does, in order
|
||||
|
||||
1. **Plan.** Counts every chunk not already in the target embedding space —
|
||||
including chunks on pages with **no recorded embedding signature**
|
||||
(pages embedded before the v108 provenance stamp). Prices the re-embed
|
||||
from the pricing table; unknown providers print "estimate unavailable"
|
||||
instead of a fabricated number.
|
||||
2. **Consent gate.** Prints the plan; requires an interactive `y` or `--yes`.
|
||||
Non-TTY without `--yes` refuses with exit 2 (mirrors the `reindex-code`
|
||||
gate in [spend-controls](../operations/spend-controls.md)). Unlike the pure
|
||||
cost gates there, `spend.posture=tokenmax` does **not** bypass this one:
|
||||
posture waives the spend *ceiling*, and this gate also guards a
|
||||
destructive schema rebuild. Under `tokenmax` the dollar figure is marked
|
||||
informational and the confirmation is still asked. `--yes` is the single
|
||||
scripted bypass.
|
||||
3. **Live probe.** One tiny embed against the TARGET provider before any
|
||||
mutation — validates the API key, model id, and dimension support in a
|
||||
single call. A bad key fails here, with nothing changed.
|
||||
4. **Env-override gate.** Refuses when `GBRAIN_EMBEDDING_MODEL` /
|
||||
`GBRAIN_EMBEDDING_DIMENSIONS` would silently defeat the switch at
|
||||
runtime (the same guard `ze-switch` uses). `--ignore-env-override` for
|
||||
people running deliberate experiments.
|
||||
5. **Apply.** When the target width differs from the actual column width,
|
||||
runs the same atomic schema transition `ze-switch` uses, in one
|
||||
transaction. It rebuilds **all three dim-pinned text-embedding-space
|
||||
columns** — `content_chunks.embedding`, `query_cache.embedding`, and
|
||||
`facts.embedding` — at the new width, preserving each column's type
|
||||
(`vector` vs `halfvec`) and recreating its HNSW index. Missing any of the
|
||||
three leaves it silently broken: a narrow `query_cache.embedding` makes
|
||||
every cache write and read fail *by design* (the cache swallows errors so
|
||||
it can never break search) for a permanent 0% hit rate, and a narrow
|
||||
`facts.embedding` fails every per-fact embed write. The image/multimodal
|
||||
columns ARE deliberately untouched — they use separate models whose
|
||||
dimensions are independent of the text embedding model.
|
||||
Writes `embedding_model` + `embedding_dimensions` to BOTH config planes
|
||||
(file plane for the runtime gateway, DB plane for doctor), invalidates
|
||||
every chunk still in the old space — **including NULL-signature pages** —
|
||||
and purges the semantic query cache so stale cached results can't be
|
||||
served across the swap.
|
||||
6. **Re-embed.** The standard embed pipeline (`embed --stale --catch-up`)
|
||||
with per-source single-flight locks, rate-limit backoff, stderr progress,
|
||||
and optional DB-contention pacing (`--pace[=mode]`).
|
||||
|
||||
## What the rebuild deletes
|
||||
|
||||
The dimension change **deletes every stored embedding vector** in the brain —
|
||||
they are in the old model's space and unusable. They are not recoverable:
|
||||
going back to the previous provider means paying for a second full re-embed.
|
||||
`content_chunks` vectors are rebuilt by the re-embed pass, the query cache
|
||||
refills on the next query, and fact embeddings are rewritten on their next
|
||||
write (or a `gbrain extract` pass).
|
||||
|
||||
## Resume after a kill
|
||||
|
||||
The NULL-embedding column is the checkpoint. If the run is killed (or some
|
||||
pages fail to embed), re-run the **same command**: chunks already embedded on
|
||||
the target are never re-embedded, the schema/config steps no-op, and the run
|
||||
continues where it stopped. An in-flight marker (`embedding_migration.state`
|
||||
in DB config) records the target; it is cleared only when the backlog drains
|
||||
to zero.
|
||||
|
||||
A page whose chunks straddle two stale batches is embedded correctly but not
|
||||
stamped by the embed loop (which only stamps all-or-nothing per batch), so the
|
||||
migration runs one reconcile pass after the drain that stamps every
|
||||
fully-embedded page. Without it a large brain would report "incomplete" and the
|
||||
re-run would pay again for those pages. `--batch-size N` tunes the batch
|
||||
(default 2000).
|
||||
|
||||
`--no-embed` applies schema + config + invalidation and stops, so you can run
|
||||
the (potentially long) re-embed later or in the background:
|
||||
|
||||
```bash
|
||||
gbrain migrate embeddings --to openai:text-embedding-3-small --yes --no-embed
|
||||
gbrain embed --stale --catch-up --include-null-signature --background
|
||||
```
|
||||
|
||||
## During the migration
|
||||
|
||||
While the re-embed runs, semantic search returns degraded (lexical-arm-only)
|
||||
results for not-yet-re-embedded content. Pick a quiet window for large
|
||||
brains, or use `--pace` to keep the DB responsive.
|
||||
|
||||
## Pages without an embedding signature (#3391)
|
||||
|
||||
Pages embedded before provenance stamping have `embedding_signature IS NULL`
|
||||
and are grandfathered by the routine stale sweep (so an upgrade never
|
||||
surprise-re-embeds a whole corpus). After a provider swap that grandfather
|
||||
clause would silently leave those pages in the OLD embedding space — mixed
|
||||
vector spaces in one index, degrading retrieval with nothing in the logs.
|
||||
|
||||
- `gbrain migrate embeddings` always includes them.
|
||||
- Plain `gbrain embed --stale` warns when a model swap leaves NULL-signature
|
||||
pages behind, and `gbrain embed --stale --include-null-signature` re-embeds
|
||||
them.
|
||||
|
||||
## Reranker
|
||||
|
||||
Migrating embeddings does not touch the reranker. If
|
||||
`search.reranker.model` points at the outgoing provider, the plan prints a
|
||||
warning; disable it (`gbrain config set search.reranker.enabled false`) or
|
||||
point it at another provider.
|
||||
|
||||
## Self-hosting instead of migrating
|
||||
|
||||
If the outgoing model's weights are available (zembed-1's are Apache-2.0),
|
||||
serving them locally via `llama-server` / `ollama` / a LiteLLM proxy
|
||||
preserves your existing vectors — no re-embed at all. Point
|
||||
`embedding_model` at the local recipe and keep the same dimensions. The
|
||||
migration command is for when you'd rather move to a hosted provider.
|
||||
@@ -49,23 +49,23 @@ on enrich(entity, trigger):
|
||||
data["contacts"] = google_contacts(entity.email) # Contact data
|
||||
|
||||
# Step 5: Store raw data (auditable, re-processable)
|
||||
gbrain call put_raw_data \
|
||||
'{"slug": "<entity_slug>", "data": {"sources": {"crustdata": {"fetched_at": "...", "data": {...}}, ...}}}'
|
||||
gbrain put_raw_data <entity_slug> \
|
||||
--data '{"sources": {"crustdata": {"fetched_at": "...", "data": {...}}, ...}}'
|
||||
# Overwrite on re-enrichment, don't append
|
||||
|
||||
# Step 6: Write to brain page
|
||||
if path == "CREATE":
|
||||
gbrain put <entity_slug> --content "<compiled_truth_from_all_sources>"
|
||||
gbrain timeline-add <entity_slug> {date} "Page created via enrichment"
|
||||
gbrain add_timeline_entry <entity_slug> --entry "Page created via enrichment"
|
||||
elif path == "UPDATE":
|
||||
# Append timeline, update compiled truth ONLY if materially new
|
||||
gbrain timeline-add <entity_slug> {date} "Enriched: {new_signal}"
|
||||
gbrain add_timeline_entry <entity_slug> --entry "Enriched: {new_signal}"
|
||||
# Flag contradictions -- don't silently resolve them
|
||||
|
||||
# Step 7: Cross-reference the graph
|
||||
gbrain link <person_slug> <company_slug> # person -> company
|
||||
gbrain link <company_slug> <person_slug> # company -> person
|
||||
gbrain link <person_slug> <deal_slug> # person -> deal
|
||||
gbrain add_link <person_slug> <company_slug> # person -> company
|
||||
gbrain add_link <company_slug> <person_slug> # company -> person
|
||||
gbrain add_link <person_slug> <deal_slug> # person -> deal
|
||||
# Every entity page links to every other entity page that references it
|
||||
|
||||
# People page sections (not a LinkedIn profile -- a living portrait):
|
||||
@@ -94,8 +94,8 @@ on enrich(entity, trigger):
|
||||
## How to Verify
|
||||
|
||||
1. Enrich a Tier 1 person. Run `gbrain get <slug>` and confirm the page has Executive Summary, State, What They Believe, Contact, and Timeline sections populated from multiple sources.
|
||||
2. Run `gbrain call get_raw_data '{"slug": "<slug>"}'`. Confirm raw API responses are stored with `sources.{provider}.fetched_at` timestamps.
|
||||
3. Run `gbrain call get_links '{"slug": "<slug>"}'`. Confirm cross-reference links exist to the person's company page, deal pages, and related entities.
|
||||
2. Run `gbrain get_raw_data <slug>`. Confirm raw API responses are stored with `sources.{provider}.fetched_at` timestamps.
|
||||
3. Run `gbrain get_links <slug>`. Confirm cross-reference links exist to the person's company page, deal pages, and related entities.
|
||||
4. Check a page that was enriched AND has a user-written Assessment. Confirm the Assessment section was preserved, not overwritten by API data.
|
||||
5. Try to re-enrich the same person. Confirm the system checks the `fetched_at` timestamp and skips if less than a week old.
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ on upcoming_meeting(meeting):
|
||||
"last_interaction": page.timeline[0], # most recent
|
||||
"open_threads": page.open_threads,
|
||||
"relationship_temperature": page.relationship,
|
||||
"relevant_deals": gbrain call get_links '{"slug": "<attendee_slug>"}',
|
||||
"relevant_deals": gbrain get_links <attendee_slug>,
|
||||
}
|
||||
else:
|
||||
briefing[attendee] = "No brain page -- consider enriching"
|
||||
@@ -67,14 +67,14 @@ on inbox_cleared():
|
||||
for email in processed_emails:
|
||||
if email.contained_new_information:
|
||||
# Update the sender's brain page with new signal
|
||||
gbrain timeline-add <sender_slug> {date} \
|
||||
"Email re: {subject}. Key info: {extracted_signal}" \
|
||||
gbrain add_timeline_entry <sender_slug> \
|
||||
--entry "Email re: {subject}. Key info: {extracted_signal}" \
|
||||
--source "email from {sender} re {subject}, {date}"
|
||||
|
||||
# Update any mentioned entity pages too
|
||||
for entity in email.mentioned_entities:
|
||||
gbrain timeline-add <entity_slug> {date} \
|
||||
"{what_was_said_about_them}" \
|
||||
gbrain add_timeline_entry <entity_slug> \
|
||||
--entry "{what_was_said_about_them}" \
|
||||
--source "email from {sender}, {date}"
|
||||
|
||||
# WORKFLOW 4: Scheduling Nudges
|
||||
|
||||
+13
-41
@@ -15,23 +15,17 @@ with the brain repo automatically. You never have to remember to run sync.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Prerequisite: a reachable direct connection
|
||||
### Prerequisite: Session Mode Pooler
|
||||
|
||||
GBrain is tuned for the Supabase **Transaction pooler** (port 6543): it
|
||||
auto-disables prepared statements there and routes `engine.transaction()`
|
||||
(migrations, DDL, sync imports) to a derived **direct** connection
|
||||
(`db.<ref>.supabase.co:5432`). That direct host is IPv6-only, so on an
|
||||
IPv4-only host it is unreachable. When that happens gbrain now falls back to
|
||||
the pooler automatically (one stderr warning, then single-pool mode for the
|
||||
rest of the process) — but the pooler's ~2-min statement timeout can truncate
|
||||
very long migrations or bulk imports.
|
||||
Sync uses `engine.transaction()` on every import. If `DATABASE_URL` points to
|
||||
Supabase's **Transaction mode** pooler, sync will throw `.begin() is not a
|
||||
function` and **silently skip most pages**. This is the number one cause of
|
||||
"sync ran but nothing happened."
|
||||
|
||||
Fix: make the direct connection reachable over IPv4. Either set
|
||||
`GBRAIN_DIRECT_DATABASE_URL` to the **Session pooler** string (port 5432 on the
|
||||
`pooler.supabase.com` host, IPv4), or enable Supabase's IPv4 add-on.
|
||||
`GBRAIN_DISABLE_DIRECT_POOL=1` skips the direct pool (and the fallback warning)
|
||||
entirely. Verify by running `gbrain sync` and checking that the page count in
|
||||
`gbrain stats` matches the syncable file count in the repo.
|
||||
Fix: use the **Session mode** pooler string (port 6543, Session mode) or the
|
||||
direct connection (port 5432, IPv6-only). Verify by running `gbrain sync` and
|
||||
checking that the page count in `gbrain stats` matches the syncable file count
|
||||
in the repo.
|
||||
|
||||
### The Primitives
|
||||
|
||||
@@ -64,9 +58,8 @@ gbrain sync --repo /data/brain && gbrain embed --stale
|
||||
Name: gbrain-auto-sync
|
||||
Schedule: */15 * * * *
|
||||
Prompt: "Run: gbrain sync --repo /data/brain && gbrain embed --stale
|
||||
Log the result. If sync errors mention an unreachable host or timeout,
|
||||
the direct connection isn't reachable over IPv4 (set
|
||||
GBRAIN_DIRECT_DATABASE_URL to the Session pooler, or enable the IPv4 add-on)."
|
||||
Log the result. If sync fails with .begin() is not a function,
|
||||
the DATABASE_URL is using Transaction mode pooler."
|
||||
```
|
||||
|
||||
**Hermes:**
|
||||
@@ -123,27 +116,6 @@ hashes match. If both a cron and `--watch` fire simultaneously, no conflict.
|
||||
server is down when a push happens, that sync is missed. Pair webhooks
|
||||
with a cron fallback that catches anything the webhook missed.
|
||||
|
||||
4. **A single un-parseable file can't wedge all indexing.** When a file fails
|
||||
to import (malformed YAML frontmatter, an unquoted colon, etc.), sync holds
|
||||
the bookmark and tells you exactly which file broke — a *fresh* failure
|
||||
fails closed so nothing is silently dropped. But a file that fails the same
|
||||
way `GBRAIN_SYNC_AUTOSKIP_AFTER` consecutive syncs (default 3, set `0` to
|
||||
disable) is auto-skipped so the rest of the brain keeps indexing past it.
|
||||
Skipped files don't disappear: `gbrain doctor` keeps warning until you fix
|
||||
or delete them, and fixing the file clears it on the next sync. A repository
|
||||
history rewrite still hard-blocks even with `--skip-failed`. Run
|
||||
`gbrain sync --skip-failed` to acknowledge a known-bad set yourself.
|
||||
|
||||
5. **Import checkpoints name the import target, not the caller's CWD.**
|
||||
Interrupted `gbrain import <dir>` runs may leave
|
||||
`~/.gbrain/import-checkpoint.json` so the next import can resume. The
|
||||
checkpoint `dir` is the absolute, resolved import target captured when
|
||||
import starts. It is not a cleanup instruction and it must not be
|
||||
re-derived from the process working directory. Checkpoints written by
|
||||
gbrain include `schema_version: 1`, `owner: "gbrain"`, and
|
||||
`kind: "import"` so downstream tools can validate the contract before
|
||||
deciding whether to resume.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. **Edit a file and search for the change.** Edit a brain markdown file,
|
||||
@@ -153,8 +125,8 @@ hashes match. If both a cron and `--watch` fire simultaneously, no conflict.
|
||||
|
||||
2. **Compare page count to file count.** Run `gbrain stats` and count the
|
||||
syncable markdown files in the brain repo. The page count in the database
|
||||
should match. If they diverge, files are being silently skipped (likely an
|
||||
unreachable direct connection on IPv4 — see the prerequisite above).
|
||||
should match. If they diverge, files are being silently skipped (likely
|
||||
a Transaction mode pooler issue).
|
||||
|
||||
3. **Check embedded chunk count.** In `gbrain stats`, the embedded chunk
|
||||
count should be close to the total chunk count. A large gap means
|
||||
|
||||
@@ -32,15 +32,15 @@ on new_meeting_transcript(meeting):
|
||||
|
||||
# Step 3: Propagate to ALL entity pages (MANDATORY -- most agents skip this)
|
||||
for person in meeting.attendees + meeting.mentioned_people:
|
||||
gbrain timeline-add <person_slug> {date} \
|
||||
"Met in '{meeting.title}' on {date}. Key points: ..." \
|
||||
gbrain add_timeline_entry <person_slug> \
|
||||
--entry "Met in '{meeting.title}' on {date}. Key points: ..." \
|
||||
--source "Meeting notes '{meeting.title}', {date}"
|
||||
# Update their State section if new information surfaced
|
||||
# Update company pages for each person's company if relevant
|
||||
|
||||
for company in meeting.mentioned_companies:
|
||||
gbrain timeline-add <company_slug> {date} \
|
||||
"Discussed in '{meeting.title}': {what_was_said}" \
|
||||
gbrain add_timeline_entry <company_slug> \
|
||||
--entry "Discussed in '{meeting.title}': {what_was_said}" \
|
||||
--source "Meeting notes '{meeting.title}', {date}"
|
||||
|
||||
# Step 4: Extract action items
|
||||
@@ -49,8 +49,8 @@ on new_meeting_transcript(meeting):
|
||||
|
||||
# Step 5: Back-link everything (bidirectional graph)
|
||||
for entity in all_entities_mentioned:
|
||||
gbrain link <slug> <entity_slug> # meeting -> entity
|
||||
gbrain link <entity_slug> <slug> # entity -> meeting
|
||||
gbrain add_link <slug> <entity_slug> # meeting -> entity
|
||||
gbrain add_link <entity_slug> <slug> # entity -> meeting
|
||||
|
||||
# Step 6: Sync so new pages are immediately searchable
|
||||
gbrain sync
|
||||
@@ -73,7 +73,7 @@ on new_meeting_transcript(meeting):
|
||||
1. After ingesting a meeting, run `gbrain get meetings/{date}-{slug}`. Confirm the page has the agent's analysis above the bar and the full diarized transcript below it.
|
||||
2. For each attendee, run `gbrain get <attendee_slug>`. Check that their timeline has a new entry referencing the meeting with specific insights (not just "attended meeting").
|
||||
3. Pick a company mentioned in the meeting. Run `gbrain get <company_slug>`. Confirm a timeline entry exists referencing what was discussed about the company.
|
||||
4. Run `gbrain call get_links '{"slug": "meetings/{date}-{slug}"}'`. Verify back-links exist to all attendee and entity pages.
|
||||
4. Run `gbrain get_links meetings/{date}-{slug}`. Verify back-links exist to all attendee and entity pages.
|
||||
5. Run `gbrain search "{meeting_topic}"`. Confirm the meeting page appears in search results (verifies sync ran).
|
||||
|
||||
---
|
||||
|
||||
@@ -54,33 +54,6 @@ gbrain jobs supervisor stop
|
||||
An agent seeing exit=2 can safely treat it as "one is already running";
|
||||
exit=1 should page a human.
|
||||
|
||||
### Lowering scheduling priority (`--nice`)
|
||||
|
||||
When the worker pool runs at full concurrency on a machine you also use
|
||||
interactively, it can drive the load average high enough to starve your
|
||||
shell. Cutting `--concurrency` throws away throughput. Reach for `--nice`
|
||||
instead — it lowers the job tree's CPU scheduling priority without touching
|
||||
width, so the work runs full-speed when the box is idle and yields when it
|
||||
isn't:
|
||||
|
||||
```bash
|
||||
# Full concurrency, low priority. Propagates to the spawned worker and its
|
||||
# children (shell jobs, subagents) via OS niceness inheritance.
|
||||
gbrain jobs supervisor --concurrency 4 --nice 10
|
||||
|
||||
# Equivalent for a bare worker, or set it durably in the environment.
|
||||
GBRAIN_NICE=10 gbrain jobs work --concurrency 4
|
||||
```
|
||||
|
||||
`--nice` takes a POSIX value from `-20` (highest priority) to `19`
|
||||
(nicest/lowest); positive values need no privilege, negative values need
|
||||
root. `GBRAIN_NICE` is the env equivalent (the flag wins). Confirm the
|
||||
effective value with `gbrain jobs stats`, `gbrain jobs supervisor status
|
||||
--json`, or the `supervisor_niceness` check in `gbrain doctor` — the doctor
|
||||
check warns if what you asked for isn't what's actually running (e.g. a
|
||||
negative value denied without privilege, or an OS `RLIMIT_NICE` clamp). This
|
||||
is distinct from the concurrency / inflight cap and composes with it.
|
||||
|
||||
### Which supervisor when?
|
||||
|
||||
The supervisor solves in-process crash recovery. Platform-level
|
||||
|
||||
@@ -155,7 +155,6 @@ child-spawn time:
|
||||
- `inherit: ["database_url"]` → child env `GBRAIN_DATABASE_URL`
|
||||
- `inherit: ["anthropic_api_key"]` → child env `ANTHROPIC_API_KEY`
|
||||
- `inherit: ["openai_api_key"]` → child env `OPENAI_API_KEY`
|
||||
- `inherit: ["openrouter_api_key"]` → child env `OPENROUTER_API_KEY`
|
||||
- `inherit: ["voyage_api_key"]` → child env `VOYAGE_API_KEY`
|
||||
- `inherit: ["groq_api_key", "zeroentropy_api_key"]` → both injected
|
||||
- Or any arbitrary config-key your worker has (`my_custom_field` →
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# Multi-language full-text search
|
||||
|
||||
GBrain's keyword search arm uses Postgres full-text search (tsvector/tsquery).
|
||||
The tokenizer language is configurable via the `GBRAIN_FTS_LANGUAGE`
|
||||
environment variable. Default: `english`.
|
||||
|
||||
## How it works
|
||||
|
||||
Postgres text-search configurations control stemming and stop-word removal.
|
||||
`GBRAIN_FTS_LANGUAGE` is read by `src/core/fts-language.ts` and applied on
|
||||
both sides of the search:
|
||||
|
||||
- **Query side** — `websearch_to_tsquery('<lang>', $query)` in both engines
|
||||
(Postgres and PGLite).
|
||||
- **Write side** — the `update_page_search_vector` and
|
||||
`update_chunk_search_vector` trigger functions that populate
|
||||
`pages.search_vector` and `content_chunks.search_vector`.
|
||||
|
||||
The value is validated against `/^[a-z][a-z0-9_]*$/` before it is ever
|
||||
interpolated into SQL (tsvector functions don't accept parameterized config
|
||||
names). Invalid values fall back to `english` with a warning.
|
||||
|
||||
## Built-in languages
|
||||
|
||||
Set the env var to any configuration your Postgres instance ships:
|
||||
|
||||
```bash
|
||||
export GBRAIN_FTS_LANGUAGE=portuguese
|
||||
export GBRAIN_FTS_LANGUAGE=spanish
|
||||
export GBRAIN_FTS_LANGUAGE=german
|
||||
```
|
||||
|
||||
List what's available:
|
||||
|
||||
```sql
|
||||
SELECT cfgname FROM pg_ts_config;
|
||||
```
|
||||
|
||||
PGLite (the embedded default engine) ships the same built-in snowball
|
||||
configurations as stock Postgres.
|
||||
|
||||
## First install vs. changing language later
|
||||
|
||||
On first install (or upgrade), the `configurable_fts_language` schema
|
||||
migration reads `GBRAIN_FTS_LANGUAGE` and stamps the trigger functions with
|
||||
that language. After the migration has run, changing the env var alone does
|
||||
NOT retokenize existing rows — the migration shows as applied and is skipped.
|
||||
Use the explicit command:
|
||||
|
||||
```bash
|
||||
export GBRAIN_FTS_LANGUAGE=portuguese
|
||||
gbrain reindex-search-vector --dry-run # preview: language + row counts
|
||||
gbrain reindex-search-vector --yes # recreate triggers + backfill
|
||||
```
|
||||
|
||||
The command recreates both trigger functions under the new language and
|
||||
backfills every existing `pages` and `content_chunks` row in batches,
|
||||
streaming progress to stderr. It is idempotent: re-running with the same
|
||||
language produces identical vectors. `--json` prints a machine-readable
|
||||
result envelope but still requires `--yes` (or an interactive confirm).
|
||||
|
||||
## Recipe: accent-insensitive Portuguese (`pt_br`)
|
||||
|
||||
Brazilian Portuguese content often mixes accented and unaccented spellings
|
||||
("São Paulo" vs "Sao Paulo"). Build a custom config that folds accents via
|
||||
the `unaccent` extension, then stems with the portuguese snowball dictionary:
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS unaccent;
|
||||
|
||||
CREATE TEXT SEARCH CONFIGURATION pt_br (COPY = portuguese);
|
||||
|
||||
ALTER TEXT SEARCH CONFIGURATION pt_br
|
||||
ALTER MAPPING FOR hword, hword_part, word
|
||||
WITH unaccent, portuguese_stem;
|
||||
```
|
||||
|
||||
Then point GBrain at it:
|
||||
|
||||
```bash
|
||||
export GBRAIN_FTS_LANGUAGE=pt_br
|
||||
gbrain reindex-search-vector --yes
|
||||
```
|
||||
|
||||
Note: custom configurations require a real Postgres instance (e.g. the
|
||||
Supabase engine). The config must exist BEFORE the migration or the reindex
|
||||
command runs, or Postgres will reject the trigger recreation with
|
||||
`text search configuration "pt_br" does not exist`.
|
||||
|
||||
## Caveats
|
||||
|
||||
- One language per brain: the setting is global to the database, not
|
||||
per-source. Mixed-language brains should pick the dominant language (the
|
||||
vector-search arm is language-agnostic and covers the rest).
|
||||
- Keep `GBRAIN_FTS_LANGUAGE` set consistently in every environment that
|
||||
writes to the brain (CLI shells, MCP server, cron jobs) — a writer without
|
||||
the env var tokenizes new rows in `english` until the next reindex.
|
||||
@@ -91,7 +91,7 @@ first):
|
||||
6. The seeded `default` source.
|
||||
|
||||
So inside `~/.gstack/plans/` on a brain that pinned `gstack` to
|
||||
`~/.gstack` via `.gbrain-source`, `gbrain put` implicitly writes to
|
||||
`~/.gstack` via `.gbrain-source`, `gbrain put-page` implicitly writes to
|
||||
the `gstack` source. Outside any registered directory with no env/dotfile
|
||||
set, it writes to the default.
|
||||
|
||||
@@ -114,11 +114,8 @@ Flip later with `gbrain sources federate <id>` / `unfederate <id>`.
|
||||
Full subcommand reference:
|
||||
|
||||
```
|
||||
gbrain sources add <id> --path <p> [--name <n>] [--federated|--no-federated] [--force]
|
||||
gbrain sources add <id> --path <p> [--name <n>] [--federated|--no-federated]
|
||||
Register a source. id: [a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?
|
||||
--path must be a git repo (or a subdirectory of one) — see
|
||||
"The git requirement for --path sources" below. --force
|
||||
skips that check to register before git-init exists.
|
||||
gbrain sources list [--json] List all sources with page counts + federation state.
|
||||
gbrain sources remove <id> [--yes] [--dry-run] [--keep-storage]
|
||||
Cascade-delete a source (pages, chunks, timeline).
|
||||
@@ -131,47 +128,6 @@ gbrain sources federate <id>
|
||||
gbrain sources unfederate <id>
|
||||
```
|
||||
|
||||
## The git requirement for --path sources
|
||||
|
||||
Every `--path` source must be a git repository (or live inside one — a
|
||||
subdirectory of a git repo works too) with at least one committed, tracked
|
||||
file under that path. `gbrain sources add` validates this at registration
|
||||
time and refuses a directory that doesn't qualify — no `.git` at all, a
|
||||
`git init` with no commit yet, or a commit made before `git add` — with an
|
||||
actionable error instead of silently registering a source that will fail
|
||||
(or worse, "succeed" while importing nothing) on its first `gbrain sync`.
|
||||
Fix it with:
|
||||
|
||||
```bash
|
||||
git -C <path> init
|
||||
git -C <path> add -A
|
||||
git -C <path> commit -m "initial import"
|
||||
gbrain sources add <id> --path <path>
|
||||
```
|
||||
|
||||
Two details that are easy to miss:
|
||||
|
||||
- **Files must actually be committed, not just present.** The sync walker
|
||||
reads files through git objects, so `git init` alone — even followed by an
|
||||
empty commit (`git commit --allow-empty`) — isn't enough. Registration
|
||||
checks for real tracked content (`git ls-tree HEAD` scoped to the path),
|
||||
not just a resolvable `HEAD`, so this footgun is caught immediately
|
||||
instead of surfacing later as a sync that imports nothing.
|
||||
- **`--force` registers the source anyway**, skipping the check. Use this if
|
||||
you're registering a path before an automated pipeline gets around to
|
||||
`git init`-ing it. GBrain never auto-`git init`s a `--path` source for
|
||||
you — it's your directory, not a gbrain-managed clone (same consent
|
||||
boundary as sync-time self-heal, which also never mutates a `--path`
|
||||
source without an explicit ask).
|
||||
|
||||
**If sync ever reports a problem with the sync anchor** (`last_commit`) —
|
||||
after a force-push, a history rewrite, or a from-scratch `git init` on a
|
||||
directory that was synced before — you do not need to reset anything by
|
||||
hand. `gbrain sync` detects an unreachable or non-ancestor anchor
|
||||
automatically and recovers: either a full reimport (anchor object missing)
|
||||
or a direct tree-to-tree diff against the orphaned bookmark (anchor present
|
||||
but rewritten), advancing the anchor to the new HEAD when it completes.
|
||||
|
||||
## Citation format for agents
|
||||
|
||||
When agents receive multi-source results they MUST cite pages in
|
||||
@@ -188,10 +144,10 @@ citations keep working.
|
||||
|
||||
```bash
|
||||
# Pass --source explicitly
|
||||
gbrain put topics/ai ... --source wiki
|
||||
gbrain put-page topics/ai ... --source wiki
|
||||
|
||||
# Or rely on the dotfile / env / CWD match
|
||||
cd ~/.gstack && gbrain put plans/multi-repo ...
|
||||
cd ~/.gstack && gbrain put-page plans/multi-repo ...
|
||||
# → source auto-resolves to gstack
|
||||
```
|
||||
|
||||
@@ -199,58 +155,6 @@ Reads span federated sources by default. Writes require a resolved
|
||||
source (explicit, inferred, or default). The resolver never picks a
|
||||
source silently when ambiguous — it errors with a clear fix.
|
||||
|
||||
## Durability: keep a brain repo in sync (auto-harden)
|
||||
|
||||
A long-lived agent that writes to a knowledge-wiki git repo needs three
|
||||
things to never lose work: pull before it edits, push every write, and not
|
||||
go stale while it sits idle. `gbrain sources harden` installs all of that,
|
||||
idempotently. The moment you add a brain repo with a token, it runs
|
||||
automatically:
|
||||
|
||||
```bash
|
||||
# Clone + register a GitHub repo, then auto-harden it for durability.
|
||||
# Use a fine-grained PAT scoped to just this repo.
|
||||
gbrain sources add wiki --url https://github.com/you/brain-wiki.git --pat-file ~/.secrets/wiki-pat
|
||||
# → clones, then installs: local auto-push hook, scripts/brain-commit-push.sh,
|
||||
# always-on durability rules in AGENTS.md/RESOLVER.md, a 30-min pull cron,
|
||||
# and a repo-scoped credential. Verifies push works before declaring done.
|
||||
|
||||
# Run the same audit on an existing source any time (idempotent):
|
||||
gbrain sources harden wiki --pat-file ~/.secrets/wiki-pat
|
||||
|
||||
# Pull on demand (the cron calls the --path form, which never opens the DB):
|
||||
gbrain sources pull wiki
|
||||
|
||||
# Remove the durability scaffolding (also runs automatically on `sources remove`):
|
||||
gbrain sources unharden wiki
|
||||
```
|
||||
|
||||
What hardening guarantees:
|
||||
|
||||
- **Pull-first, conflict-safe.** Every pull is a divergence-safe rebase. A
|
||||
dirty working tree is skipped (your in-progress edits are never touched); a
|
||||
rebase conflict is aborted cleanly and flagged for attention, never left
|
||||
half-applied.
|
||||
- **Push is never deferred.** `scripts/brain-commit-push.sh "<msg>" <path>`
|
||||
commits and pushes atomically and refuses to report success without a
|
||||
confirmed push. The post-commit hook is a best-effort background fallback;
|
||||
the helper is the guarantee.
|
||||
- **No silent staleness.** A 30-minute background pull keeps an idle session
|
||||
current. It runs DB-free, so it never contends with a live brain for the
|
||||
PGLite single-writer lock.
|
||||
|
||||
Flags: `--no-cron` skips the scheduled pull, `--no-verify` skips the push
|
||||
probe, `--dry-run` reports what would change, `--json` emits a machine
|
||||
report, `--all` hardens every source with a remote (same-account only).
|
||||
`--no-harden` on `sources add` opts out of auto-harden.
|
||||
|
||||
Security: the push automation is installed locally per machine (never
|
||||
committed into the repo), the token is wired per-repo (an existing
|
||||
credential helper is reused when present), and it never appears in the repo,
|
||||
the remote URL, logs, or the JSON report. For a self-hosted git server
|
||||
reachable only over a filesystem path, set `GBRAIN_GIT_ALLOW_FILE_TRANSPORT=1`
|
||||
(default is HTTPS-only).
|
||||
|
||||
## Upgrading an existing brain
|
||||
|
||||
`gbrain upgrade` runs the v16 + v17 migrations automatically. Your
|
||||
|
||||
@@ -20,8 +20,8 @@ on every_inbound_message(message):
|
||||
for entity in entities:
|
||||
existing = gbrain search "{entity.name}"
|
||||
if existing:
|
||||
gbrain timeline-add <entity_slug> {date} \
|
||||
"{what_was_said}" \
|
||||
gbrain add_timeline_entry <entity_slug> \
|
||||
--entry "{what_was_said}" \
|
||||
--source "User, direct message, {timestamp}"
|
||||
# else: flag for enrichment if important enough
|
||||
|
||||
@@ -64,13 +64,13 @@ on nightly_schedule("02:00"):
|
||||
# The brain COMPOUNDS overnight.
|
||||
|
||||
# 5a: Entity sweep -- find unlinked mentions
|
||||
pages = gbrain list
|
||||
pages = gbrain list_pages
|
||||
for page in pages:
|
||||
mentions = extract_entity_mentions(page.content)
|
||||
existing_links = gbrain call get_links '{"slug": "<page.slug>"}'
|
||||
existing_links = gbrain get_links <page.slug>
|
||||
for mention in mentions:
|
||||
if mention not in existing_links:
|
||||
gbrain link <page.slug> <mention_slug> # fix broken graph
|
||||
gbrain add_link <page.slug> <mention_slug> # fix broken graph
|
||||
|
||||
# 5b: Citation audit -- find facts without sources
|
||||
for page in pages:
|
||||
@@ -80,7 +80,7 @@ on nightly_schedule("02:00"):
|
||||
|
||||
# 5c: Memory consolidation -- update compiled truth from timeline
|
||||
for page in stale_pages(older_than="7d"):
|
||||
timeline = gbrain timeline <page.slug>
|
||||
timeline = gbrain get_timeline <page.slug>
|
||||
if timeline.has_new_entries_since_last_consolidation:
|
||||
# Re-synthesize compiled truth from accumulated timeline
|
||||
updated_truth = consolidate(page.compiled_truth, timeline.new_entries)
|
||||
@@ -110,11 +110,11 @@ on nightly_schedule("02:00"):
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. Send a message mentioning a person with a brain page. Confirm the agent detects the entity and adds a timeline entry to their page (`gbrain timeline <slug>`).
|
||||
1. Send a message mentioning a person with a brain page. Confirm the agent detects the entity and adds a timeline entry to their page (`gbrain get_timeline <slug>`).
|
||||
2. Ask the agent about someone in the brain. Confirm it runs `gbrain search` or `gbrain get` BEFORE reaching for external APIs (check the tool call order).
|
||||
3. Write a new page with `gbrain put`, then immediately run `gbrain search` for it. Confirm it appears in results (verifies sync ran).
|
||||
4. Run `gbrain doctor`. Confirm it returns a health report with database status, page count, and any flagged issues.
|
||||
5. After a dream cycle runs, check a page that had unlinked entity mentions. Confirm new links were added (`gbrain call get_links '{"slug": "<slug>"}'`).
|
||||
5. After a dream cycle runs, check a page that had unlinked entity mentions. Confirm new links were added (`gbrain get_links <slug>`).
|
||||
|
||||
---
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||||
|
||||
@@ -47,8 +47,8 @@ on user_message(message):
|
||||
|
||||
# Step 3: Cross-link to everything that shaped the thinking
|
||||
for entity in idea.influences:
|
||||
gbrain link originals/{slug} <entity_slug>
|
||||
gbrain link <entity_slug> originals/{slug}
|
||||
gbrain add_link originals/{slug} <entity_slug>
|
||||
gbrain add_link <entity_slug> originals/{slug}
|
||||
|
||||
# Step 4: Sync
|
||||
gbrain sync
|
||||
@@ -79,7 +79,7 @@ on user_message(message):
|
||||
|
||||
1. Generate an original idea in conversation (e.g., "I call this the 'ambition debt' problem -- every year you delay going big, the compound interest works against you"). Confirm a new page appears at `brain/originals/ambition-debt` with `gbrain get originals/ambition-debt`.
|
||||
2. Check that the page uses the user's exact phrasing for the title and slug -- not a sanitized version.
|
||||
3. Run `gbrain call get_links '{"slug": "originals/ambition-debt"}'`. Confirm cross-links exist to related people, meetings, or other originals.
|
||||
3. Run `gbrain get_links originals/ambition-debt`. Confirm cross-links exist to related people, meetings, or other originals.
|
||||
4. Express a take on someone else's idea (e.g., "I think Thiel's contrarian question is wrong because..."). Confirm it goes to `originals/` (synthesis is original), not `concepts/`.
|
||||
5. Run `gbrain search "ambition debt"`. Confirm the originals page appears in search results and is discoverable.
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ expect it.
|
||||
| `version` | string | yes | Your plugin's semver. Informational. |
|
||||
| `plugin_version` | string | yes | Contract lock. Must equal `"gbrain-plugin-v1"` for v0.15. |
|
||||
| `subagents` | string | no | Subdir name (default `subagents`). Escape-attempts are rejected. |
|
||||
| `description` | string | no | Shown in a future plugin-listing command. |
|
||||
| `description` | string | no | Shown in future `gbrain plugin list`. |
|
||||
|
||||
## Subagent definition files
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# Push-based context (#2095, v0.42.43.0)
|
||||
|
||||
Retrieval used to be pull-only: the agent had to *know to ask* before the brain
|
||||
contributed anything. Push-based context inverts that — the brain volunteers
|
||||
relevant pages from the recent conversation, confidence-gated so push noise
|
||||
never becomes worse than pull silence.
|
||||
|
||||
Three channels share one zero-LLM core (`src/core/context/volunteer.ts`):
|
||||
|
||||
| Channel | Surface | When to use |
|
||||
|---|---|---|
|
||||
| `reflex` | automatic, inside the context engine | default-on for plugin hosts; nothing to call |
|
||||
| `op` | `gbrain volunteer-context` / MCP `volunteer_context` | agents without the plugin; one call per turn |
|
||||
| `watch` | `gbrain watch` | stream a transcript in, volunteered pages stream out |
|
||||
|
||||
## How it decides
|
||||
|
||||
1. **Extract** entities across the last N turns (capitalized runs, `@handles`),
|
||||
merged with recency / frequency / user-role salience. Assistant-introduced
|
||||
entities and "what did she invest in?" follow-ups whose antecedent was named
|
||||
in the window now resolve.
|
||||
2. **Resolve** through the alias table, exact titles, and slug suffixes — each
|
||||
arm carries an honest confidence: alias 0.9, exact title 0.8, slug-suffix 0.6,
|
||||
+0.05 when mentioned in ≥2 turns or the newest turn.
|
||||
3. **Gate** at `min_confidence` (default 0.7 — slug-suffix matches need an
|
||||
explicit lower gate), suppress pages already surfaced (slug-presence only),
|
||||
cap at 3 pages (hard cap 5).
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
# one-shot: pipe recent turns (oldest → newest)
|
||||
printf 'user: ask alice-example about the deal\nassistant: noted\nuser: what did she say?\n' \
|
||||
| gbrain volunteer-context
|
||||
|
||||
# streaming: volunteered pages print as the transcript flows
|
||||
some-transcript-feed | gbrain watch --json
|
||||
|
||||
# the feedback loop: how often were volunteered pages actually opened?
|
||||
gbrain volunteer-context --stats
|
||||
```
|
||||
|
||||
Stats are **approximate** by design: "used" means `pages.last_retrieved_at >
|
||||
volunteered_at` — the 5-minute last-retrieved throttle causes false negatives
|
||||
and unrelated reads of the same page cause false positives. Use the per-arm
|
||||
precision to tune `min_confidence`, not as an exact metric.
|
||||
|
||||
**PGLite + `gbrain watch`:** PGLite is single-connection, and watch holds its
|
||||
connection for the whole session — a concurrent `gbrain serve` or any write
|
||||
path blocks until watch exits. On a PGLite brain, run watch in bursts (piped
|
||||
input exits at EOF) or use the ambient reflex channel instead, which routes
|
||||
through a running serve's resolve socket rather than taking the lock. Routing
|
||||
watch through that same socket is a filed follow-up (TODOS.md). Postgres
|
||||
brains are unaffected.
|
||||
|
||||
## Config
|
||||
|
||||
| Key | Default | What it does |
|
||||
|---|---|---|
|
||||
| `retrieval_reflex_window_turns` | 4 | turns the ambient reflex extracts from; 1 = legacy current-turn-only (file/env plane: `GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS`) |
|
||||
| `retrieval_reflex` | true | the ambient channel's master switch |
|
||||
| `retrieval_reflex_max_pointers` | 3 | pointer cap per turn |
|
||||
|
||||
Per-call knobs: `max_pages` + `min_confidence` on both the op and `gbrain watch`
|
||||
(`--max-pages` / `--min-confidence`, plus `--window-turns` / `--source` on watch);
|
||||
on the op only: `prior_context` (text whose already-surfaced slugs are suppressed),
|
||||
`session_id` / `turn` attribution params (watch stamps its own per-session id and
|
||||
turn numbers in the feedback log), and `days` to size the `--stats` window.
|
||||
|
||||
## Storage + privacy
|
||||
|
||||
Volunteered pages log to `context_volunteer_events` (migration v117): slug,
|
||||
arm, confidence, channel, optional session/turn — the rationale is a
|
||||
deterministic template string, never raw conversation text. Event writes are
|
||||
best-effort (fire-and-forget, drained at CLI exit) — the log is a tuning signal,
|
||||
not an audit trail. Rows are pruned after 90 days by the dream cycle's purge
|
||||
phase. Synopses always strip the takes/facts fences — the same strip `get_page`
|
||||
applies to untrusted callers, applied unconditionally here so private fence rows
|
||||
never reach a prompt regardless of caller trust.
|
||||
@@ -16,39 +16,6 @@ gbrain doctor --json | jq '.checks[] | select(.name == "queue_health")'
|
||||
- **waiting-depth**: any per-name queue deeper than 10 (override via
|
||||
`GBRAIN_QUEUE_WAITING_THRESHOLD`). Signals a missing `maxWaiting`.
|
||||
|
||||
## The worker is alive but wedged (dead pool)
|
||||
|
||||
The nastiest stall: the worker process is *running* (passes `ps` / `kill -0` /
|
||||
container health), but its DB connection died (common behind a transaction
|
||||
pooler) and never came back, so it claims no jobs and finishes nothing. Jobs
|
||||
pile up with **0 active**. Liveness checks all pass; nothing crashes.
|
||||
|
||||
As of v0.42.22.0 this self-heals — you usually won't have to do anything:
|
||||
|
||||
- **The worker exits on its own dead pool.** Under a supervisor, the worker's
|
||||
DB-liveness probe runs and self-exits (`db_dead`) after ~3 minutes; the
|
||||
supervisor respawns it with a fresh pool.
|
||||
- **The supervisor restarts a worker that stops making progress.** If a queue
|
||||
has claimable work, **0 live-lock active jobs**, and no completions for 15
|
||||
minutes while the child is alive, the supervisor restarts it (covers stuck
|
||||
handlers too, not just dead pools). Tune with `--wedge-restart-minutes` /
|
||||
`--wedge-restart-checks` on `gbrain jobs supervisor` (0 disables).
|
||||
|
||||
The signal is loud now — check either:
|
||||
|
||||
```bash
|
||||
gbrain jobs stats --queue default # prints a WEDGED QUEUE line
|
||||
gbrain doctor --json | jq '.checks[] | select(.name == "wedged_queue")'
|
||||
```
|
||||
|
||||
`wedged_queue` is a per-queue health **error** (0 active_healthy + waiting > 0 +
|
||||
stale completions). Manual fix if you ever need it:
|
||||
|
||||
```bash
|
||||
gbrain jobs supervisor stop && gbrain jobs supervisor start # fresh pool
|
||||
gbrain jobs retry <id> # dead-lettered jobs
|
||||
```
|
||||
|
||||
## Triage commands
|
||||
|
||||
```bash
|
||||
|
||||
@@ -87,9 +87,8 @@ proposal (this lives in v0.42 follow-up; v1 emits the audit event).
|
||||
| `--judge-model MODEL` | tier.reasoning | Scores rollouts |
|
||||
| `--patch \| --rewrite` | patch | Edit ops only vs. full rewrites |
|
||||
| `--dry-run` | off | Cost preview, no LLM calls |
|
||||
| `--no-mutate` | off | Write proposed.md, don't replace SKILL.md (no held-out needed) |
|
||||
| `--allow-mutate-bundled` | off | Required to mutate gbrain-bundled skills in place — ALSO requires `--held-out` (>=5 rows) or the run hard-refuses |
|
||||
| `--held-out <path>` | — | Independent test set (same JSONL shape as the benchmark, task IDs disjoint from it). A candidate that beats the benchmark but regresses on the held-out set is refused. Required for in-place bundled mutation. |
|
||||
| `--no-mutate` | off | Write proposed.md, don't replace SKILL.md |
|
||||
| `--allow-mutate-bundled` | off | Required to mutate gbrain-bundled skills |
|
||||
| `--max-cost-usd N` | 5.00 | Hard cap; preflight refuses if exceeded |
|
||||
| `--max-runtime-min N` | 30 | Wall-clock cap |
|
||||
| `--force` | off | Bypass dirty-working-tree refusal |
|
||||
@@ -124,8 +123,7 @@ refuses to start when the estimate exceeds `--max-cost-usd`.
|
||||
| Validation gate is mandatory | D12 (paper) | Accepting LLM judge noise as improvement |
|
||||
| Frontmatter mutation forbidden | D5 | Routing surface drift (`check-resolvable` regression) |
|
||||
| Per-skill DB lock | D14 | Two concurrent runs corrupting history/versions |
|
||||
| Bundled-skill gate | D16 | Auto-mutating skills shipped with gbrain (in-place mutation requires `--allow-mutate-bundled` + a `--held-out` set of >=5 benchmark-disjoint tasks; else hard-refuse + proposed.md) |
|
||||
| Held-out gate | F11 | Accepting a candidate that overfits its own benchmark — `--held-out` refuses a candidate whose held-out score regresses below baseline |
|
||||
| Bundled-skill gate | D16 | Auto-mutating skills shipped with gbrain |
|
||||
| Bootstrap review sentinel | D15 | Self-referential benchmark gaming |
|
||||
| Read-only tool sandbox in rollouts | D13 | Optimization runs writing junk pages to your brain |
|
||||
| History-intent-first atomic commit | D8 | Half-written SKILL.md on crash |
|
||||
|
||||
@@ -131,9 +131,7 @@ into gbrain so other clients can scaffold it. Default behavior:
|
||||
`~/.gbrain/harvest-private-patterns.txt` plus built-in defaults
|
||||
(canonical private fork name, common email regex, Slack channel pattern). Any
|
||||
match → rollback (delete the harvested files) and exit non-zero.
|
||||
- `openclaw.plugin.json` updated with the new slug, sorted. Harvest must preserve
|
||||
the top-level OpenClaw-native plugin fields (`id`, `configSchema`, `contracts`)
|
||||
because OpenClaw validates those before it can install the package.
|
||||
- `openclaw.plugin.json` updated with the new slug, sorted.
|
||||
- `--no-lint` bypasses the linter (after a manual editorial scrub).
|
||||
|
||||
Use the `skillpack-harvest` skill (its companion editorial workflow)
|
||||
|
||||
@@ -16,34 +16,6 @@ benefit-focused bullets, waits for explicit permission, then runs the full
|
||||
upgrade flow including re-reading skills, running migrations, and syncing
|
||||
schema. The user gets new capabilities automatically.
|
||||
|
||||
## Self-upgrade modes (v0.42)
|
||||
|
||||
gbrain now stays current the way gstack does: it rides invocation frequency. A
|
||||
throttled, cache-read-only check runs at the start of every `gbrain` invocation
|
||||
(CLI and MCP) and emits an `UPGRADE_AVAILABLE <old> <new>` marker on stderr. No
|
||||
host cron required — every agent kind (Claude Code, Codex, OpenClaw, Hermes, the
|
||||
`gbrain serve` host behind a Perplexity thin client) converges to current by
|
||||
construction. The behavior is governed by one file-plane config key,
|
||||
`self_upgrade.mode`:
|
||||
|
||||
| Mode | Behavior | Who it's for |
|
||||
|------|----------|--------------|
|
||||
| `notify` (default) | Emit the marker + a 4-option prompt; never apply without confirmation. | Interactive installs / anyone with a human in the loop. |
|
||||
| `auto` (opt-in) | Apply silently, but ONLY during quiet hours, ONLY when the brain is idle, doctor-gated, and never re-trying a known-bad version. | Headless / always-on installs (autopilot daemon, the `gbrain serve` host). |
|
||||
| `off` | Never check. | Air-gapped / pinned installs. |
|
||||
|
||||
Enable hands-off upgrades on an always-on install with one line:
|
||||
|
||||
```bash
|
||||
gbrain config set self_upgrade.mode auto
|
||||
```
|
||||
|
||||
`auto` is deliberately NOT a default anywhere — it's an explicit autonomy grant,
|
||||
because applying code from GitHub unattended is, by design, remote code
|
||||
execution. The trust model is TLS + GitHub (same as `gbrain upgrade`);
|
||||
signature verification is a tracked follow-up. Apply manually any time with
|
||||
`gbrain self-upgrade`.
|
||||
|
||||
## Implementation
|
||||
|
||||
### The Check (cron-initiated)
|
||||
@@ -94,11 +66,7 @@ what they can DO now that they couldn't before, not what files changed.
|
||||
| daily | Store preference, switch cron back to daily |
|
||||
| stop / unsubscribe / no more | Disable the cron. Tell user how to resume |
|
||||
|
||||
**In `notify` mode (the default), never auto-upgrade — always wait for explicit
|
||||
confirmation.** The `auto` mode (opt-in, see "Self-upgrade modes" above) is the
|
||||
only path that applies without a prompt, and only under its conservative gates
|
||||
(quiet hours + idle + doctor-gate). This per-cron-prompt flow is the `notify`
|
||||
experience.
|
||||
**Never auto-upgrade.** Always wait for explicit confirmation.
|
||||
|
||||
### The Full Upgrade Flow (after user says yes)
|
||||
|
||||
@@ -175,13 +143,10 @@ copy. Set up a weekly cron to check automatically.
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **In `notify` mode, never auto-install.** The upgrade waits for the user's
|
||||
explicit "yes." Even if the check detects an update and the changelog looks
|
||||
great, the agent messages the user and waits. The `auto` mode (opt-in) exists
|
||||
for headless/always-on installs where there's no human to prompt — it applies
|
||||
only during quiet hours, only when idle, doctor-gated, never retrying a
|
||||
known-bad version. Don't enable `auto` on an interactive workstation; the
|
||||
prompt-first `notify` flow is the right default there.
|
||||
1. **Never auto-install.** The upgrade must always wait for the user's explicit
|
||||
"yes." Even if the cron detects an update at 9 AM and the changelog looks
|
||||
great, the agent messages the user and waits. Auto-installing can break
|
||||
workflows, introduce breaking changes, or interrupt work in progress.
|
||||
|
||||
2. **Migration files are agent instructions, not scripts.** They tell the agent
|
||||
what to do step by step in plain language. They are NOT bash scripts to
|
||||
|
||||
@@ -208,10 +208,9 @@ architectural rounds shipped in the budget-cathedral wave that followed:
|
||||
- **P3 (judge chunking):** `runJudge` in `src/core/brainstorm/judges.ts`
|
||||
auto-chunks at 100 ideas/call. Context-window overflow is structurally
|
||||
prevented.
|
||||
- **P4 (unicode sanitization):** `ensureWellFormed` (in `src/core/text-safe.ts`,
|
||||
used by `src/core/brainstorm/orchestrator.ts`) replaces unpaired surrogates
|
||||
with U+FFFD before serialization. (Consolidated from the original hand-rolled
|
||||
`sanitizeUnicode` in v0.42.40.0 / #2011.)
|
||||
- **P4 (unicode sanitization):** `sanitizeUnicode` in
|
||||
`src/core/brainstorm/orchestrator.ts` strips unpaired surrogates before
|
||||
serialization.
|
||||
- **P5 (BudgetTracker at the gateway layer):** new
|
||||
`src/core/budget/budget-tracker.ts` is the canonical primitive. The
|
||||
gateway's `withBudgetTracker(tracker, fn)` composes via
|
||||
|
||||
@@ -34,7 +34,7 @@ The resolved provider + dimensions get persisted to `~/.gbrain/config.json` atom
|
||||
| `zhipu` | `ZHIPUAI_API_KEY` | 1024 | varies | no | no |
|
||||
| `ollama` | (none — runs locally) | 768 | 0 | yes | no |
|
||||
| `llama-server` | (none — runs locally) | user-set | 0 | yes | no |
|
||||
| `litellm` | `LITELLM_API_KEY` (optional) | user-set | varies | yes (proxy) | yes (backend permitting) |
|
||||
| `litellm` | `LITELLM_API_KEY` (optional) | user-set | varies | yes (proxy) | no |
|
||||
| `together` | `TOGETHER_API_KEY` | 768 | varies | no | no |
|
||||
| `anthropic` | (no embedding model — chat only) | — | — | — | — |
|
||||
| `deepseek` | (no embedding model — chat only) | — | — | — | — |
|
||||
@@ -77,8 +77,6 @@ The doctor distinguishes two repair paths:
|
||||
|
||||
Default. Set `OPENAI_API_KEY`. Models: `text-embedding-3-large` (3072 max, 1536 default), `text-embedding-3-small` (1536). Matryoshka via the `dimensions` field — gbrain pins it from `embedding_dimensions` config so existing 1536-dim brains stay aligned across SDK upgrades.
|
||||
|
||||
Optional `OPENAI_BASE_URL` — point the native OpenAI provider at an OpenAI-compatible gateway. A bare host is normalized to carry the `/v1` suffix automatically (so `https://gw.example.com` and `https://gw.example.com/v1` both work); when unset, the SDK's default endpoint is untouched. `ANTHROPIC_BASE_URL` gets the same normalization for Anthropic chat/expansion calls.
|
||||
|
||||
### Voyage AI
|
||||
|
||||
Best-in-class quality on the Voyage 4 family (Jan 2026 release). Set `VOYAGE_API_KEY`. Models: `voyage-4-large`, `voyage-4`, `voyage-4-lite`, `voyage-4-nano`, `voyage-3.5`, `voyage-code-3` (code-tuned), `voyage-finance-2`, `voyage-law-2`, `voyage-multimodal-3` (text + image).
|
||||
@@ -103,7 +101,7 @@ For GCP service-account / Vertex AI auth (production deployments), see the v0.32
|
||||
|
||||
### OpenRouter
|
||||
|
||||
Single OpenAI-compatible API for fan-out to OpenAI, Anthropic, Google, DeepSeek, Meta Llama, Qwen, and dozens of other hosted providers. One key, many models. Set `OPENROUTER_API_KEY` or `openrouter_api_key` in `~/.gbrain/config.json`, then use `openrouter:<provider>/<model>` (e.g. `openrouter:openai/gpt-5.2`, `openrouter:anthropic/claude-sonnet-4.6`).
|
||||
Single OpenAI-compatible API for fan-out to OpenAI, Anthropic, Google, DeepSeek, Meta Llama, Qwen, and dozens of other hosted providers. One key, many models. Set `OPENROUTER_API_KEY` and use `openrouter:<provider>/<model>` (e.g. `openrouter:openai/gpt-5.2`, `openrouter:anthropic/claude-sonnet-4.6`).
|
||||
|
||||
**Embedding**: `openai/text-embedding-3-small` (1536d default, Matryoshka shrink to 512/768/1024). OR's embedding catalog also includes `text-embedding-3-large`, `google/gemini-embedding-2-preview`, `qwen/qwen3-embedding-8b`, `bge-m3` — opt in via `--embedding-model openrouter:<id>`. Pricing matches the upstream provider (OR adds a small markup).
|
||||
|
||||
@@ -143,15 +141,13 @@ Set `ZHIPUAI_API_KEY`. Models: `embedding-3` (current; Matryoshka 256-2048 dims)
|
||||
|
||||
No env required — Ollama runs unauthenticated locally. Optional `OLLAMA_BASE_URL` (default `http://localhost:11434/v1`) and `OLLAMA_API_KEY` (for auth-enabled deployments).
|
||||
|
||||
Recipe ships with `nomic-embed-text` (768d, recommended), `mxbai-embed-large` (1024d), `all-minilm` (384d), plus the larger modern embedders `qwen3-embed-8b` (4096d) and `snowflake-arctic-embed-l-v2` (1024d). `gbrain providers test --model ollama:nomic-embed-text` smoke-tests the local install.
|
||||
|
||||
The recipe default is `nomic-embed-text`'s 768 dims. If you run one of the larger models, declare its native dimension with `--embedding-dimensions <N>` at init — gbrain trusts the value you declare for local recipes instead of rejecting a non-768 width.
|
||||
Recipe ships with `nomic-embed-text` (768d, recommended), `mxbai-embed-large` (1024d), `all-minilm` (384d). `gbrain providers test --model ollama:nomic-embed-text` smoke-tests the local install.
|
||||
|
||||
### llama-server (local, llama.cpp)
|
||||
|
||||
`llama.cpp`'s `llama-server --embeddings` endpoint. No env required. Optional `LLAMA_SERVER_BASE_URL` (default `http://localhost:8080/v1`) and `LLAMA_SERVER_API_KEY`.
|
||||
|
||||
User-driven models: launch llama-server with `--model <gguf-path> --embeddings`, then run `gbrain init --embedding-model llama-server:<your-id> --embedding-dimensions <N>`. gbrain trusts the dimension you declare (you know the GGUF you launched); the recipe refuses the implicit shorthand `--model llama-server` because there's no canonical first model.
|
||||
User-driven models: launch llama-server with `--model <gguf-path> --embeddings`, then run `gbrain init --embedding-model llama-server:<your-id> --embedding-dimensions <N>`. The recipe refuses the implicit shorthand `--model llama-server` because there's no canonical first model.
|
||||
|
||||
### LiteLLM proxy (universal escape hatch)
|
||||
|
||||
@@ -159,8 +155,6 @@ Run [LiteLLM](https://docs.litellm.ai/docs/proxy/quick_start) in front of any pr
|
||||
|
||||
This is the catch-all for "my provider isn't in the list above." Set up LiteLLM, then `gbrain init --embedding-model litellm:<your-model-id> --embedding-dimensions <N>`.
|
||||
|
||||
**Include the `/v1` suffix in `LITELLM_BASE_URL` if your proxy serves the OpenAI route there** (e.g. `http://localhost:4000/v1`). Many LiteLLM deployments expose the OpenAI-compatible API only under `/v1`; pointing gbrain at the bare host 404s or fails authentication with no hint. gbrain trusts the dimension you declare for the proxy-backed model — the proxy's backend, not gbrain, decides the true width — so `--embedding-dimensions <N>` is required and accepted as-is.
|
||||
|
||||
## Choosing dimensions
|
||||
|
||||
Three numbers matter:
|
||||
@@ -189,3 +183,5 @@ The supported paths:
|
||||
- **Postgres (Supabase / self-hosted):** follow the SQL recipe in `docs/embedding-migrations.md` (drop the HNSW index, ALTER COLUMN TYPE, clear stale embeddings, recreate the index conditionally, then `gbrain init --supabase --embedding-model X --embedding-dimensions N` to update the file plane and re-embed).
|
||||
|
||||
`gbrain doctor` 8c "alternative_providers" surfaces unconfigured providers whose env is already set — useful when you've configured OpenAI but also have e.g. `VOYAGE_API_KEY` exported and want to know you can switch without extra setup.
|
||||
|
||||
`gbrain doctor` 8c "alternative_providers" surfaces unconfigured providers whose env is already set — useful when you've configured OpenAI but also have e.g. `VOYAGE_API_KEY` exported and want to know you can switch without extra setup.
|
||||
|
||||
@@ -7,7 +7,7 @@ brain source's repo that runs `gbrain frontmatter validate` against staged
|
||||
|
||||
## What the hook catches
|
||||
|
||||
The same eight validation classes the `frontmatter-guard` skill and
|
||||
The same seven validation classes the `frontmatter-guard` skill and
|
||||
`gbrain doctor`'s `frontmatter_integrity` subcheck report:
|
||||
|
||||
| Code | What it catches |
|
||||
@@ -18,7 +18,6 @@ The same eight validation classes the `frontmatter-guard` skill and
|
||||
| `SLUG_MISMATCH` | `slug:` in frontmatter doesn't match path-derived slug |
|
||||
| `NULL_BYTES` | Binary corruption (`\x00`) anywhere in the content |
|
||||
| `NESTED_QUOTES` | `title: "outer "inner" outer"` shape that breaks YAML |
|
||||
| `NON_STRING_FIELD` | `title`/`type`/`slug` is an unquoted non-string scalar (`title: 123`) |
|
||||
| `EMPTY_FRONTMATTER` | `---` ... `---` with nothing meaningful between |
|
||||
|
||||
## Install
|
||||
|
||||
+2
-46
@@ -74,20 +74,13 @@ to the HTTP server, so no migration is required.
|
||||
gbrain serve --http --port 3131
|
||||
```
|
||||
|
||||
On first start in an interactive terminal, the server prints an **admin
|
||||
bootstrap token** to stderr:
|
||||
On first start, the server prints an **admin bootstrap token** to stderr:
|
||||
|
||||
```
|
||||
Admin bootstrap token: 3a1f9c...
|
||||
Open http://localhost:3131/admin and paste it to log in.
|
||||
```
|
||||
|
||||
On a non-TTY start (systemd, Docker, any piped or captured logs) the generated
|
||||
token is hidden so it never lands in log storage. For headless deploys either
|
||||
set `GBRAIN_ADMIN_BOOTSTRAP_TOKEN` to a value you control before starting, or
|
||||
run `gbrain serve --http --print-admin-token` once on a trusted terminal to
|
||||
force printing.
|
||||
|
||||
Save this token. Open `http://localhost:3131/admin` and paste it to access the
|
||||
dashboard. The dashboard shows live activity, registered clients, request logs,
|
||||
and per-client config export.
|
||||
@@ -250,7 +243,7 @@ All 30 GBrain operations are available remotely, including `sync_brain` and
|
||||
directory where `gbrain serve` was launched. Symlinks, `..` traversal, and absolute
|
||||
paths outside cwd are rejected. Page slugs and filenames are allowlist-validated
|
||||
(alphanumeric + hyphens; no control chars, RTL overrides, or backslashes). Local
|
||||
CLI callers (`gbrain files upload ...`) keep unrestricted filesystem access since
|
||||
CLI callers (`gbrain file upload ...`) keep unrestricted filesystem access since
|
||||
the user owns the machine.
|
||||
|
||||
## Deployment Options
|
||||
@@ -258,43 +251,6 @@ the user owns the machine.
|
||||
See [ALTERNATIVES.md](ALTERNATIVES.md) for a comparison of ngrok, Tailscale
|
||||
Funnel, and cloud hosts (Fly.io, Railway).
|
||||
|
||||
### Co-located Docker workloads (self-hosted Postgres)
|
||||
|
||||
OAuth scopes and source scoping guard the `gbrain serve --http` path. They do
|
||||
NOT guard raw Postgres. If the brain's Postgres runs as a container on the same
|
||||
Docker host as other workloads (agent runtimes, n8n, staging fixtures), any
|
||||
container sharing Docker's default `bridge` network can open a direct DB
|
||||
session — no OAuth token required — and read every source. That silently
|
||||
recreates a privileged path underneath the isolation you configured at the MCP
|
||||
layer.
|
||||
|
||||
Network-zone the host so untrusted containers can never reach Postgres:
|
||||
|
||||
```
|
||||
Docker host
|
||||
├── gbrain-net ← ONLY the brain's Postgres (+ gbrain serve, if containerized)
|
||||
├── agent-<id>-net ← each untrusted agent runtime, isolated
|
||||
└── default bridge ← no secret-bearing databases
|
||||
```
|
||||
|
||||
Operator checklist:
|
||||
|
||||
```text
|
||||
[ ] Postgres is on a user-defined Docker network, not the default bridge
|
||||
(or nothing else runs on that bridge)
|
||||
[ ] If Postgres publishes a host port at all, it binds loopback only
|
||||
(`-p 127.0.0.1:5432:5432`, never `0.0.0.0`)
|
||||
[ ] Untrusted agent containers have no DATABASE_URL or Postgres password
|
||||
[ ] Untrusted agents reach the brain via OAuth/Bearer against serve --http only
|
||||
(host loopback via host.docker.internal / host gateway — never gbrain-net)
|
||||
[ ] OAuth clients are least-privilege: scoped --source / --federated-read,
|
||||
pre-minted short-lived tokens preferred over long-lived client secrets
|
||||
[ ] Isolation verified: a team-scoped client cannot read internal-only sources
|
||||
```
|
||||
|
||||
Optional defense-in-depth: a dedicated Postgres role (or RLS) limited to the
|
||||
allowed `source_id`s, so even a leaked connection string can't read everything.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"missing_auth" error**
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
# Conversation backfill durable outcomes
|
||||
|
||||
`gbrain extract-conversation-facts` stores page-level outcomes in `facts` so
|
||||
bulk runs, autopilot, and `gbrain doctor` can distinguish finished work from
|
||||
retryable work without adding another state table.
|
||||
|
||||
This is completion authority, not ordinary extracted knowledge. The authority
|
||||
is deliberately narrow: a marker is valid only for the exact page or transcript
|
||||
snapshot that was parsed, and only after every required operation succeeded.
|
||||
|
||||
## Outcome protocol
|
||||
|
||||
The current protocol is v2. Its source names are versioned so rows written by
|
||||
older best-effort implementations cannot suppress a corrective replay.
|
||||
|
||||
| Outcome | `facts.source` | Meaning |
|
||||
|---|---|---|
|
||||
| Complete | `cli:extract-conversation-facts:terminal:v2` | Every eligible segment was extracted and inserted successfully, the input remained unchanged, and the terminal write succeeded. |
|
||||
| Scanned, not extractable | `cli:extract-conversation-facts:non-extractable:v2` | A recognized input was scanned successfully but contained no eligible multi-message segment. |
|
||||
| Unfinished | no matching v2 outcome | Work is pending, failed, was not recognized, changed during extraction, or has only a legacy marker. |
|
||||
|
||||
The non-extractable outcome is intentionally separate from completion. It does
|
||||
not claim that knowledge facts were extracted. CLI counters, cycle details, and
|
||||
doctor output preserve that distinction.
|
||||
|
||||
## Snapshot identity
|
||||
|
||||
Every v2 marker binds `source_session` to the parser input snapshot:
|
||||
|
||||
```text
|
||||
<outcome-source>:<page-slug>:<version-token>
|
||||
```
|
||||
|
||||
There are two token forms.
|
||||
|
||||
### Database-backed page body
|
||||
|
||||
For pages parsed from `compiled_truth` and `timeline`, the token is:
|
||||
|
||||
```text
|
||||
page-<pages.content_hash>-<effective-date>
|
||||
```
|
||||
|
||||
`content_hash` covers title, type, compiled truth, timeline, and frontmatter.
|
||||
The effective-date suffix covers the remaining date input used by parsing. This
|
||||
identity does not depend on JavaScript's millisecond timestamp precision, so two
|
||||
writes within one PostgreSQL millisecond still produce different tokens when
|
||||
parser input changes. A legacy page with a null content hash uses a computed
|
||||
SHA-256 fallback and is verified in-process by both extraction and doctor.
|
||||
|
||||
### Raw transcript sidecar
|
||||
|
||||
When frontmatter contains `raw_transcript`, the source text lives outside the
|
||||
page row and may change without changing `pages.updated_at`. Its token is:
|
||||
|
||||
```text
|
||||
sidecar-<SHA-256>
|
||||
```
|
||||
|
||||
The digest covers the exact body given to the parser plus parser-relevant page
|
||||
metadata: title, type, frontmatter, and effective date. Selection recomputes
|
||||
the digest before skipping work. A sidecar-only edit therefore reopens the page.
|
||||
|
||||
`gbrain doctor` cannot read sidecars in its SQL aggregate, so it enumerates those
|
||||
pages in bounded batches and calls the same canonical verifier used by
|
||||
extraction. Doctor and extraction therefore agree after sidecar-only edits.
|
||||
|
||||
## Selection and locking
|
||||
|
||||
Bulk extraction follows this sequence:
|
||||
|
||||
1. Enumerate candidate pages in bounded batches.
|
||||
2. Filter candidates with matching v2 outcomes.
|
||||
3. Apply `--limit` to the remaining pages that actually need work.
|
||||
4. Acquire the source-and-slug advisory lock.
|
||||
5. Re-fetch the page under that lock.
|
||||
6. Recompute and recheck the snapshot-bound outcome.
|
||||
7. Prepare one immutable parser snapshot and process it.
|
||||
8. Re-fetch and recompute the snapshot before writing an outcome.
|
||||
|
||||
The pre-lock check avoids parser, filesystem, and model work for ordinary
|
||||
completed pages. The under-lock refetch prevents a stale enumeration object
|
||||
from becoming the certified input. The final comparison prevents an edit that
|
||||
happens during model or insertion work from receiving a marker for old content.
|
||||
|
||||
An edit can occur after the final comparison and before marker insertion. That
|
||||
is still safe because the marker contains the old version token. Future
|
||||
selection compares the token, not marker creation time, and reopens the page.
|
||||
|
||||
Single-page `--slug` runs use the same under-lock path.
|
||||
|
||||
## Strict extraction success
|
||||
|
||||
The general `extractFactsFromTurn` API remains best-effort for interactive
|
||||
callers. It historically returns an empty array for both a legitimate zero-fact
|
||||
answer and several model failures.
|
||||
|
||||
Conversation backfill instead uses `extractFactsFromTurnWithOutcome`, whose
|
||||
result separates:
|
||||
|
||||
- `{ ok: true, facts: [] }`, a successful extraction with no durable facts;
|
||||
- `{ ok: true, facts: [...] }`, a successful extraction with facts; and
|
||||
- `{ ok: false, reason, error? }`, an unavailable provider, provider error,
|
||||
refusal, content filter, malformed output, or repeated truncation.
|
||||
|
||||
Any failed segment aborts the page attempt. Any `insertFacts` failure also
|
||||
aborts it. The page receives neither a checkpoint advancement nor a terminal
|
||||
outcome. Facts inserted by earlier segments may remain temporarily, but the
|
||||
next claim deletes this command's rows for the page and replays cleanly.
|
||||
|
||||
Bulk workers continue past an individual page failure, but they do not hide it.
|
||||
`pages_failed` counts failed claims, stderr names each page, the CLI exits 1,
|
||||
the autopilot phase reports `warn`, and receipts/rollups classify the run as
|
||||
incomplete. A tolerant pool is therefore observable without sacrificing the
|
||||
rest of a large backfill.
|
||||
|
||||
This distinction is load-bearing. Treating a provider outage as a successful
|
||||
zero-fact response would make a transient failure durable and permanently hide
|
||||
the page from later runs.
|
||||
|
||||
## Non-extractable authority
|
||||
|
||||
A non-extractable marker is written only when all of the following are true:
|
||||
|
||||
- a deterministic or accepted parser format recognized the input;
|
||||
- ordinary segmentation produced no eligible multi-message segment;
|
||||
- the parser phase was not `no_match`;
|
||||
- cleanup of prior command-owned rows succeeded; and
|
||||
- the input snapshot was still current immediately before cleanup and write.
|
||||
|
||||
A `no_match` result stays unfinished so a new parser pattern, optional fallback,
|
||||
or corrected input can recover it. Oversize pages, disappeared pages, lock
|
||||
contention, dry runs, aborts, cleanup errors, provider failures, extraction
|
||||
failures, insertion failures, and outcome-write failures also stay unfinished.
|
||||
|
||||
Cleanup errors are never interpreted as "zero rows deleted." Propagating them
|
||||
prevents a fresh non-extractable marker from coexisting with stale extracted
|
||||
facts that could not be removed.
|
||||
|
||||
## Checkpoints are not authority
|
||||
|
||||
Operation checkpoints are only progress hints. They do not prove which page
|
||||
snapshot was processed, and old checkpoint entries do not include a snapshot
|
||||
token. When a page lacks a matching v2 outcome, the command discards that
|
||||
page's checkpoint entry and performs a delete-first full replay.
|
||||
|
||||
This rule prevents two corruption classes:
|
||||
|
||||
- edited text with timestamps older than the old watermark being skipped; and
|
||||
- command-owned facts being deleted while the checkpoint skips the segments
|
||||
needed to recreate them.
|
||||
|
||||
Deleting `op_checkpoints` does not reopen pages with matching v2 outcomes.
|
||||
Deleting or editing an outcome does not make a checkpoint authoritative.
|
||||
|
||||
## `--limit` semantics
|
||||
|
||||
`--limit N` caps pages that require processing, not completed pages inspected
|
||||
while finding them. Durable filtering happens before clipping a batch. With a
|
||||
completed page first and a pending page second, `--limit 1` processes the
|
||||
pending page rather than consuming the limit on the completed page.
|
||||
|
||||
`pages_considered` may therefore exceed `--limit` because it includes durable
|
||||
outcomes observed during selection. Model-bearing page work does not exceed the
|
||||
limit.
|
||||
|
||||
## `--force`
|
||||
|
||||
`--force` bypasses durable outcome selection and clears the page checkpoint.
|
||||
It still uses delete-first replay, strict extraction outcomes, advisory locks,
|
||||
and snapshot verification. Force means "recompute" rather than "relax safety."
|
||||
|
||||
## Operator signals
|
||||
|
||||
The result exposes separate counters:
|
||||
|
||||
- `pages_skipped_completed`
|
||||
- `pages_skipped_non_extractable`
|
||||
- `pages_marked_non_extractable`
|
||||
- `pages_failed`
|
||||
|
||||
The CLI aggregates these across sources. The autopilot backfill phase includes
|
||||
them in phase details. `gbrain doctor` reports `completed`,
|
||||
`scanned_not_extractable`, and `backlog` independently.
|
||||
|
||||
Run a small canary twice:
|
||||
|
||||
```bash
|
||||
gbrain extract-conversation-facts --source-id default --limit 10 --workers 1 --max-cost-usd 0.25 --yes
|
||||
gbrain extract-conversation-facts --source-id default --limit 10 --workers 1 --max-cost-usd 0.25 --yes
|
||||
gbrain doctor
|
||||
```
|
||||
|
||||
On the second run, unchanged pages should move through durable skip counters.
|
||||
Edit one page or raw transcript sidecar and rerun; that page should process
|
||||
again and receive a marker with a new token.
|
||||
|
||||
## Maintainer contracts
|
||||
|
||||
- Version completion protocols when their success guarantees change.
|
||||
- Require an exact `source`, page slug, and snapshot-bound `source_session`.
|
||||
- Keep completion and non-extractable as different sources and counters.
|
||||
- Re-fetch after acquiring the lock; never certify the enumeration object.
|
||||
- Revalidate the snapshot before writing either durable outcome.
|
||||
- Keep sidecar content in the version identity.
|
||||
- Keep regular-page content hash and effective date in the version identity.
|
||||
- Never turn model, insertion, cleanup, cancellation, or parser failures into
|
||||
successful empty extraction.
|
||||
- Never classify `no_match` or dry-run output as a durable negative.
|
||||
- Do not make operation checkpoints completion authority.
|
||||
- Apply work limits after durable filtering.
|
||||
- Keep doctor source-scoped by both page and fact `source_id`.
|
||||
- Give terminal completion precedence if both current outcome rows exist.
|
||||
- Update CLI and cycle aggregation whenever a result counter changes.
|
||||
|
||||
## Focused verification
|
||||
|
||||
```bash
|
||||
bun test test/extract-conversation-facts.test.ts
|
||||
bun test test/doctor-conversation-facts-backlog.test.ts
|
||||
bun x tsc --noEmit
|
||||
```
|
||||
|
||||
The focused suite covers checkpoint garbage collection, same-timestamp edits,
|
||||
edits during extraction, sidecar-only edits, legacy marker replay, provider and
|
||||
insert failures, cleanup failure, recognized non-extractable scans, retryable
|
||||
parser misses, post-filter limits, force replay, and doctor accounting.
|
||||
@@ -1,240 +0,0 @@
|
||||
# Conversation parser LLM fallback
|
||||
|
||||
The conversation parser has two stages:
|
||||
|
||||
1. A deterministic registry recognizes known transcript formats.
|
||||
2. An optional LLM fallback parses pages that every built-in pattern rejects.
|
||||
|
||||
The second stage is disabled by default. Enabling it is a privacy decision
|
||||
because unmatched transcript text can be sent to the configured utility-tier
|
||||
model provider.
|
||||
|
||||
## Enable or disable the fallback
|
||||
|
||||
Enable it for the current brain:
|
||||
|
||||
```bash
|
||||
gbrain config set conversation_parser.llm_fallback_enabled true
|
||||
```
|
||||
|
||||
Disable it:
|
||||
|
||||
```bash
|
||||
gbrain config set conversation_parser.llm_fallback_enabled false
|
||||
```
|
||||
|
||||
The key is registered explicitly, so neither command needs `--force`.
|
||||
Values other than the exact string `true` leave the fallback disabled.
|
||||
|
||||
The setting affects conversation fact extraction. It does not make the
|
||||
synchronous `conversation-parser scan` command call a model, and it does not
|
||||
enable the separate LLM polish scaffold.
|
||||
|
||||
## Select the utility model and run a canary
|
||||
|
||||
Inspect the model routing before enabling a production run:
|
||||
|
||||
```bash
|
||||
gbrain models
|
||||
```
|
||||
|
||||
The fallback uses the resolved `utility` tier. Override that tier when the
|
||||
brain should use a different configured provider or model:
|
||||
|
||||
```bash
|
||||
gbrain config set models.tier.utility <provider:model>
|
||||
```
|
||||
|
||||
Start with one known unmatched page and an explicit cost cap:
|
||||
|
||||
```bash
|
||||
gbrain extract-conversation-facts \
|
||||
--source-id <source-id> \
|
||||
--slug <conversation-slug> \
|
||||
--max-cost-usd 1
|
||||
```
|
||||
|
||||
Do not add `--dry-run` to this canary. Dry runs deliberately stop before the
|
||||
fallback boundary, so they cannot prove provider routing or model output.
|
||||
Success emits the per-page fallback log described under
|
||||
[Operator visibility](#operator-visibility). After the canary, remove `--slug`
|
||||
to process the source normally.
|
||||
|
||||
## When the fallback runs
|
||||
|
||||
For each eligible conversation page, extraction:
|
||||
|
||||
1. Reads the same body used by the deterministic parser, including a configured
|
||||
raw transcript sidecar for meeting pages.
|
||||
2. Calls `parseConversation(body, { page })`.
|
||||
3. Uses the deterministic messages when any built-in pattern succeeds.
|
||||
4. Calls the LLM fallback only when the parse phase is exactly `no_match`, the
|
||||
message list is empty, the opt-in key is `true`, and this is not a dry run.
|
||||
5. Splits accepted fallback messages into the normal extraction segments.
|
||||
|
||||
The fallback never replaces, edits, or polishes a successful deterministic
|
||||
parse. Adding a built-in pattern therefore removes model use for that format
|
||||
without changing configuration.
|
||||
|
||||
Dry runs remain local and cost-free. They report deterministic segmentation
|
||||
only and never send unmatched content to a provider.
|
||||
|
||||
## Data sent to the model
|
||||
|
||||
The full unmatched body is processed in overlapping windows of at most 100
|
||||
non-empty lines, with up to 20 lines of preceding context. Blank lines are
|
||||
omitted. Every model request receives:
|
||||
|
||||
- an instruction to treat the transcript as untrusted data;
|
||||
- an authoritative page date when one can be derived;
|
||||
- the sampled transcript inside an explicit chat-log envelope.
|
||||
|
||||
The system prompt tells the model not to follow commands or instructions found
|
||||
inside transcript content. It asks for message extraction only.
|
||||
|
||||
Each window is cached independently. Overlap results with the same normalized
|
||||
speaker and timestamp are deduplicated; when one body contains the other, the
|
||||
longer body wins. This preserves common multi-line messages that straddle a
|
||||
window boundary. If any later window has an ordinary provider or parse failure,
|
||||
the fallback returns no page result and extraction does not advance the
|
||||
checkpoint. Successful earlier windows stay cached for the retry.
|
||||
|
||||
Fallback calls allow up to 8,000 output tokens. Any non-terminal model stop,
|
||||
including length truncation, refusal, content filtering, tool use, or an
|
||||
unrecognized provider stop, is rejected before parsing and caching. A
|
||||
syntactically valid partial JSON array therefore cannot advance a checkpoint.
|
||||
|
||||
The utility model is resolved once per source run through the normal model
|
||||
configuration chain. The default fallback is the utility-tier Anthropic model.
|
||||
|
||||
## Date and timestamp behavior
|
||||
|
||||
The fallback uses the deterministic parser's date precedence:
|
||||
|
||||
1. an explicit caller date;
|
||||
2. `frontmatter.date`;
|
||||
3. the page effective date;
|
||||
4. `1970-01-01` when no date is known.
|
||||
|
||||
A real page date is included in both the prompt and the content-hash cache key.
|
||||
Two pages with identical time-only transcript text but different dates cannot
|
||||
share a cached parse.
|
||||
|
||||
Returned timestamps must be strict RFC3339 date-times with seconds and an
|
||||
explicit `Z` or numeric timezone offset. Calendar fields are validated before
|
||||
parsing. Accepted timestamps are normalized to whole-second UTC form:
|
||||
|
||||
```text
|
||||
YYYY-MM-DDTHH:MM:SSZ
|
||||
```
|
||||
|
||||
Date-only values, timezone-less values, impossible calendar dates, timestamps
|
||||
more than 24 hours in the future, blank speakers, and blank message bodies are
|
||||
discarded. Valid messages are stable-sorted by timestamp before segmentation.
|
||||
Canonical chronological UTC output keeps segment filtering and durable
|
||||
checkpoint comparisons stable and prevents future checkpoint poisoning.
|
||||
|
||||
If no page date is known, the prompt retains the historical epoch fallback.
|
||||
Full timestamps present in the transcript can still be extracted normally.
|
||||
|
||||
## Non-chat and failure behavior
|
||||
|
||||
The model is instructed to return an empty JSON array for non-chat content.
|
||||
An empty response, malformed JSON, unavailable provider, or transport failure
|
||||
leaves the page with no messages. Extraction skips that page and continues.
|
||||
|
||||
The fallback is fail-open with respect to parser availability. It does not turn
|
||||
a model outage into a deterministic-parser outage.
|
||||
|
||||
Cancellation and `BudgetExhausted` are control-flow signals, not provider
|
||||
failures. The extraction caller explicitly propagates them through the
|
||||
fail-open boundary so aborts stay prompt and hard cost caps remain effective.
|
||||
An `AbortError` from a provider timeout still fails open while the caller's own
|
||||
abort signal remains live.
|
||||
|
||||
The gateway can discover an underestimated budget overage only after the final
|
||||
provider result. Extraction checks tracker spend against its cap after the run,
|
||||
so an overage remains visible even when there is no next model reservation.
|
||||
|
||||
## Cache and repeat runs
|
||||
|
||||
Successful fallback results use the shared conversation-parser cache:
|
||||
|
||||
- an in-process map for repeat calls during one process;
|
||||
- the `conversation_parser_llm_cache` table for repeat calls across processes.
|
||||
|
||||
Each chunk's cache key includes the call shape, resolved model, page date
|
||||
metadata, and chunk content hash. A cached response is still validated before
|
||||
it originally enters the cache.
|
||||
|
||||
Once fallback messages produce extractable segments, the ordinary per-page
|
||||
checkpoint advances to the newest segment timestamp. A later run can read the
|
||||
cached parse, apply the checkpoint watermark, and skip already completed
|
||||
segments without another provider call.
|
||||
|
||||
## Operator visibility
|
||||
|
||||
`ExtractConversationFactsResult.pages_llm_fallback` counts pages for which the
|
||||
fallback returned at least one valid message. The command also logs:
|
||||
|
||||
```text
|
||||
[extract-conversation-facts] LLM fallback parsed N message(s) for <slug>
|
||||
```
|
||||
|
||||
The multi-source CLI summary reports the total number of fallback-parsed pages.
|
||||
A zero count means either the fallback was disabled, deterministic patterns
|
||||
handled every page, or fallback attempts returned no valid messages.
|
||||
|
||||
## Maintainer contracts
|
||||
|
||||
Keep these boundaries intact when changing the fallback:
|
||||
|
||||
- Default off. Page text must not reach the fallback without the exact opt-in.
|
||||
- Never call the provider during `--dry-run`.
|
||||
- Deterministic first. Invoke it only for phase `no_match`.
|
||||
- One model resolution per source run, not per page.
|
||||
- Use `deriveDateContext({ page })` so regex and LLM timestamps share metadata.
|
||||
- Put date metadata in the hashed request content to prevent cross-date cache
|
||||
collisions.
|
||||
- Process every non-empty line in bounded cached overlapping windows. Preserve
|
||||
common cross-boundary continuations through overlap and deterministic
|
||||
deduplication. Never checkpoint a partial page after a later window fails or
|
||||
returns a non-terminal stop reason.
|
||||
- Validate and canonicalize all model-produced fields before segmentation.
|
||||
- Stable-sort accepted messages before segmenting or checkpointing them.
|
||||
- Keep the exact config key in `KNOWN_CONFIG_KEYS`. Do not register the whole
|
||||
`conversation_parser.*` namespace while other scaffolded keys remain unwired.
|
||||
- Preserve `[]` and `null` as skip-page outcomes.
|
||||
- Propagate cancellation and budget-stop errors selected by the extraction
|
||||
caller; fail open only for ordinary provider and parse failures.
|
||||
- Never persist inferred regexes or promote model guesses into the built-in
|
||||
registry.
|
||||
|
||||
## Test coverage
|
||||
|
||||
The focused tests cover:
|
||||
|
||||
- default-off behavior with zero fallback calls;
|
||||
- enabled dry-run behavior with zero provider calls;
|
||||
- exact config-key registration;
|
||||
- a successful production-path fallback;
|
||||
- page-date prompt and cache-key separation;
|
||||
- durable checkpoint advancement and cache reuse;
|
||||
- complete processing beyond the first 100 non-empty lines;
|
||||
- cross-boundary continuation preservation and overlap deduplication;
|
||||
- rejection of truncated, refused, and content-filtered model results;
|
||||
- all-or-nothing page results when a later chunk fails;
|
||||
- non-chat empty arrays and malformed output;
|
||||
- strict timestamp normalization, ordering, and invalid-item filtering;
|
||||
- provider-unavailable and transport-failure behavior;
|
||||
- provider-timeout versus caller-cancellation behavior;
|
||||
- thrown and post-record budget-stop reporting.
|
||||
|
||||
Run the focused surface with:
|
||||
|
||||
```bash
|
||||
bun test test/conversation-parser/llm-base.test.ts \
|
||||
test/conversation-parser/llm-fallback.test.ts \
|
||||
test/extract-conversation-facts.test.ts \
|
||||
test/config-set.test.ts
|
||||
```
|
||||
@@ -1,112 +0,0 @@
|
||||
# Spend controls
|
||||
|
||||
GBrain's embedding-spend gates in one place: every gate, its config key, default,
|
||||
whether it blocks or just informs, how to widen or disable it, and how the
|
||||
`spend.posture` switch governs all of them.
|
||||
|
||||
The orienting idea: **GBrain itself is rounding error; the spend that matters is
|
||||
downstream embedding.** These gates exist so a routine sync or enrich can't run up
|
||||
an unexpected embedding bill, while never wedging an unattended cron.
|
||||
|
||||
## `spend.posture` — one switch for "cost is not my constraint"
|
||||
|
||||
```bash
|
||||
gbrain config set spend.posture tokenmax # all cost gates become informational
|
||||
gbrain config set spend.posture gated # default — gates enforce
|
||||
```
|
||||
|
||||
| Value | Effect |
|
||||
|-------|--------|
|
||||
| `gated` (default) | Every cost gate enforces its limit as documented below. |
|
||||
| `tokenmax` | Every cost gate prints its estimate and **proceeds** — informational only. Spend is still recorded to the ledger; posture removes the *ceiling*, not the *accounting*. |
|
||||
|
||||
`spend.posture` is deliberately separate from `search.mode=tokenmax` (which governs
|
||||
retrieval payload size, not embedding spend). When a gate fires and
|
||||
`search.mode=tokenmax` but `spend.posture` is unset, the gate prints a one-line hint
|
||||
pointing at this switch.
|
||||
|
||||
**Precedence:** an explicit per-call cap (`--max-usd N`, `--max-cost N`) always wins
|
||||
over posture. `tokenmax` only governs the default/absent case — it never overrides a
|
||||
number you typed on the command line.
|
||||
|
||||
## Off switches (`off` / `unlimited` / `none`)
|
||||
|
||||
The USD-limit knobs accept `off`, `unlimited`, or `none` (case-insensitive) to mean
|
||||
"no limit" — no more setting sentinel values like `100000`.
|
||||
|
||||
- `0` is **not** "off". On `sync.cost_gate_min_usd`, `0` means "block on any nonzero
|
||||
spend" (a real choice). On the backfill caps, `0` falls back to the default.
|
||||
- Internally "no limit" is the string `unlimited` in any printed/JSON output and "no
|
||||
cap" inside the budget tracker — never a raw `Infinity` (which would serialize to
|
||||
`null` in ledger rows).
|
||||
|
||||
## The gates
|
||||
|
||||
| Gate | Config key | Default | Blocks? | Off switch | tokenmax |
|
||||
|------|-----------|---------|---------|-----------|----------|
|
||||
| Sync inline-embed cost gate | `sync.cost_gate_min_usd` | `0.50` | TTY prompt / non-TTY auto-defer | `off` (or `0` = block-on-any) | informational |
|
||||
| Backfill 24h per-source spend cap | `embed.backfill_max_usd_per_source_24h` | `25` | refuses submission | `off` (`0` → default) | bypassed (still ledgered) |
|
||||
| Backfill per-job budget | `embed.backfill_max_usd` | `10` | caps the job's tracker | `off` (`0` → default) | uncapped (still ledgered) |
|
||||
| Backfill cooldown | `embed.backfill_cooldown_min` | `10` | skips re-submission inside window | — (latency knob, not spend) | **not** bypassed |
|
||||
| `reindex-code` cost gate | — (preview before re-embed) | — | TTY prompt / non-TTY refuse + exit 2 | `--max-cost off` | informational |
|
||||
| `migrate embeddings` consent gate | — (plan + estimate before provider migration) | — | TTY y/N prompt / non-TTY refuse + exit 2 | `--yes` | estimate marked informational, but **still prompts** (guards a destructive schema rebuild, not just spend) |
|
||||
| `enrich` / `onboard --auto` | `--max-usd` (per-call) | — | refuse without a cap (non-TTY) | `--max-usd off` | runs uncapped (still ledgered) |
|
||||
|
||||
### Sync inline-embed cost gate
|
||||
|
||||
Fires only when sync embeds **inline** (federated_v2 off, or `--serial` without
|
||||
`--no-embed`). Under federated_v2 + parallel, embedding is deferred to capped backfill
|
||||
jobs and the gate is informational. The estimate prices the **delta** — the files this
|
||||
sync will actually import (fetched-first, so it sees commits the run is about to pull) —
|
||||
not the whole tree. A busy brain with a dirty working tree but caught-up commits
|
||||
estimates `$0`, because an attached-HEAD sync imports only the committed diff.
|
||||
|
||||
Behavior above the floor:
|
||||
- **TTY:** prompts `[y/N]`.
|
||||
- **Non-interactive (cron/agent):** **auto-defers** embeds to capped backfill jobs and
|
||||
exits 0 — it never wedges the pipeline. The backlog drains via the jobs worker or
|
||||
`gbrain embed --stale`. Pass `--yes` to embed inline instead.
|
||||
|
||||
Output format splits on the explicit `--json` flag: `--json` emits a structured
|
||||
envelope; otherwise human text. Every gate message carries paste-ready knobs.
|
||||
|
||||
`--full` re-embeds the stale backlog inline (full sync sweeps it), so a `--full`
|
||||
estimate is `delta + stale backlog`, labeled as such.
|
||||
|
||||
### Estimate labels
|
||||
|
||||
- `~N tokens (delta: changed files since last sync)` — the precise estimate.
|
||||
- `<=N tokens (full-tree ceiling for K source(s): <reasons> …)` — a conservative
|
||||
over-count used only when a precise delta can't be computed: a first sync, a chunker
|
||||
version drift (forces a full re-chunk), or git being unavailable. Unchanged files
|
||||
still skip via `content_hash` at execution, so the ceiling over-states real spend.
|
||||
|
||||
## Notes & limits
|
||||
|
||||
- **Pre-pull window:** the gate fetches before estimating, so it prices what the run
|
||||
will pull. If a fetch fails (offline), it estimates against local HEAD and labels the
|
||||
result; the bounded residual is priced on the next run.
|
||||
- **Single-source `gbrain sync`** carries the same gate as `sync --all` (it previously
|
||||
embedded inline with no preview).
|
||||
- **Recovery under parallel:** `--skip-failed` / `--retry-failed` work under parallel
|
||||
sync (the failure ledger is per-source and lock-serialized) — you no longer have to
|
||||
drop to `--serial`, which is what used to arm the inline gate.
|
||||
|
||||
## Escape hatches at a glance
|
||||
|
||||
```bash
|
||||
# Never gate this brain on cost:
|
||||
gbrain config set spend.posture tokenmax
|
||||
|
||||
# Widen the sync inline floor to $5:
|
||||
gbrain config set sync.cost_gate_min_usd 5
|
||||
|
||||
# Disable the sync inline floor entirely:
|
||||
gbrain config set sync.cost_gate_min_usd off
|
||||
|
||||
# Lift the backfill 24h spend cap:
|
||||
gbrain config set embed.backfill_max_usd_per_source_24h off
|
||||
|
||||
# Run enrich uncapped non-interactively:
|
||||
gbrain enrich --max-usd off # or: gbrain config set spend.posture tokenmax
|
||||
```
|
||||
@@ -1,211 +0,0 @@
|
||||
---
|
||||
title: "feat: Add idea-lineage thinking skill"
|
||||
type: feat
|
||||
status: completed
|
||||
date: 2026-06-03
|
||||
---
|
||||
|
||||
# feat: Add idea-lineage thinking skill
|
||||
|
||||
## Summary
|
||||
|
||||
Add an `idea-lineage` thinking skill that traces how one idea has evolved through a user's brain: first mention, best articulation, related concepts, reversals, contradictions, abandoned branches, and the current live version. The contribution should start as a read-only skill with routing and conformance coverage, not as a new CLI or MCP operation.
|
||||
|
||||
## Problem Frame
|
||||
|
||||
GBrain already has two adjacent capabilities that are easy to conflate with this feature:
|
||||
|
||||
- `skills/concept-synthesis/SKILL.md` is a mutating, batch-oriented concept map builder. It deduplicates many concept stubs, tiers them, writes concept pages, and creates an intellectual universe.
|
||||
- `find_trajectory` and `gbrain eval trajectory` are structured entity trajectories over typed facts and events. They work best for questions like metric history, founder consistency, role/status changes, and event timelines.
|
||||
|
||||
`idea-lineage` should occupy the narrow space between them: a query-time, single-idea, citation-backed synthesis of conceptual evolution. It should help a user ask "how has my thinking about this idea changed?" without running a global concept-synthesis job or forcing the idea into an entity/metric trajectory model.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Behavior**
|
||||
|
||||
- R1. The skill accepts a single idea, topic, concept phrase, or nearby concept page and produces a focused lineage for that idea only.
|
||||
- R2. The output identifies first mention, best articulation, related concepts, reversals, contradictions, abandoned branches, and current live version when evidence supports each category.
|
||||
- R3. Every lineage claim is grounded in existing brain evidence: page links, dates, verbatim snippets, timeline entries, takes, contradiction findings, or trajectory points when applicable.
|
||||
- R4. The skill distinguishes evidence strength. Missing or weak evidence should be reported as a gap, not filled with plausible narrative.
|
||||
- R5. The default workflow is read-only and does not write or mutate brain pages.
|
||||
|
||||
**Routing**
|
||||
|
||||
- R6. Routing should prefer `idea-lineage` for single-idea evolution requests such as "how has my thinking about X changed?".
|
||||
- R7. Routing should keep broad corpus/map requests on `concept-synthesis`.
|
||||
- R8. Routing should keep structured entity metric/status questions on `find_trajectory`, `gbrain eval trajectory`, or `gbrain think` trajectory injection.
|
||||
|
||||
**Privacy and portability**
|
||||
|
||||
- R9. The skill and fixtures must use public, generic examples only.
|
||||
- R10. The plan and implementation must avoid private fork names, real people, real companies, funds, or host-specific filesystem paths in public artifacts.
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
### In Scope
|
||||
|
||||
- A new bundled skill under `skills/idea-lineage/`.
|
||||
- Resolver, manifest, and plugin-bundle wiring.
|
||||
- Routing fixtures that prove the new intent is reachable and does not swallow `concept-synthesis` or trajectory-shaped prompts.
|
||||
- Documentation inside the skill body that explains when to use `search`, `query`, `get_page`, `list_pages`, `takes_search`, `find_contradictions`, and optionally `find_trajectory`.
|
||||
- Focused conformance, resolver, and routing verification.
|
||||
|
||||
### Deferred to Follow-Up Work
|
||||
|
||||
- A first-class `idea_lineage` MCP operation.
|
||||
- A `gbrain idea lineage <query>` CLI.
|
||||
- Persisting lineage reports back into the brain.
|
||||
- New database tables, schema-pack fields, or concept lineage graph primitives.
|
||||
- Automated contradiction-probe reruns. The skill should read cached contradiction findings if available, not trigger expensive probes.
|
||||
|
||||
### Outside This Contribution
|
||||
|
||||
- Replacing `concept-synthesis`.
|
||||
- Changing the facts/takes epistemology model.
|
||||
- Changing `find_trajectory`'s entity-slug contract.
|
||||
- Implementing the broader taxonomy redesign tracked by issue #1668.
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
- **Start as a markdown skill:** GBrain's architecture treats skills as fat markdown workflows. This feature can be useful by orchestrating existing read operations, so a CLI/MCP surface would add contract weight before the behavior is proven.
|
||||
- **Make the skill non-mutating by default:** The user intent is investigative. Writing lineage pages should remain a later explicit mode after routing and output quality are established.
|
||||
- **Use evidence buckets rather than a single narrative pass:** The output should force the agent to separately evaluate first mention, articulation, current version, reversals, contradictions, and abandoned branches. That reduces the risk of smoothing over conflict.
|
||||
- **Keep `find_trajectory` as an optional side-channel:** It is valuable when an idea query resolves to an entity attribute or status history, but `idea-lineage` should not depend on typed facts being present.
|
||||
- **Avoid the existing "trace idea evolution" trigger phrase:** That phrase already routes to `concept-synthesis`; adding it to the new skill would create avoidable resolver ambiguity.
|
||||
|
||||
## High-Level Technical Design
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A["User asks about one idea"] --> B{"Intent shape"}
|
||||
B -->|"whole corpus / map"| C["concept-synthesis"]
|
||||
B -->|"entity metric / status over time"| D["trajectory surfaces"]
|
||||
B -->|"single conceptual idea"| E["idea-lineage skill"]
|
||||
E --> F["Resolve idea candidates"]
|
||||
F --> G["Gather evidence via search/query/pages/takes"]
|
||||
G --> H["Classify lineage moments"]
|
||||
H --> I["Synthesize cited answer with confidence gaps"]
|
||||
```
|
||||
|
||||
## Implementation Units
|
||||
|
||||
### U1. Add the `idea-lineage` Skill
|
||||
|
||||
- **Goal:** Create the read-only skill contract and workflow.
|
||||
- **Requirements:** R1, R2, R3, R4, R5, R9, R10
|
||||
- **Dependencies:** None
|
||||
- **Files:**
|
||||
- `skills/idea-lineage/SKILL.md`
|
||||
- `test/skills-conformance.test.ts`
|
||||
- **Approach:** Create a new skill with required frontmatter and conformance sections. The skill should define its workflow in phases: clarify the target idea, resolve likely concept/page anchors, collect evidence, classify lineage moments, produce a cited synthesis, and state gaps. Frontmatter should set `mutating: false` and list read operations only.
|
||||
- **Patterns to follow:**
|
||||
- `skills/strategic-reading/SKILL.md` for a read-only thinking-skill shape with related-skill boundaries.
|
||||
- `skills/query/SKILL.md` for search/query/get-page guidance.
|
||||
- `skills/concept-synthesis/SKILL.md` for contrast, not for behavior reuse.
|
||||
- **Test scenarios:**
|
||||
- A new `SKILL.md` with frontmatter, `## Contract`, `## Output Format`, and `## Anti-Patterns` passes conformance.
|
||||
- The frontmatter declares a unique `name: idea-lineage`.
|
||||
- The skill body references only portable, synthetic examples.
|
||||
- **Verification:** `bun test test/skills-conformance.test.ts` passes.
|
||||
|
||||
### U2. Wire Resolver, Manifest, and Bundle Metadata
|
||||
|
||||
- **Goal:** Make the skill discoverable by bundled skill users and resolvable by agents.
|
||||
- **Requirements:** R6, R7, R8, R9, R10
|
||||
- **Dependencies:** U1
|
||||
- **Files:**
|
||||
- `skills/RESOLVER.md`
|
||||
- `skills/manifest.json`
|
||||
- `openclaw.plugin.json`
|
||||
- `test/resolver.test.ts`
|
||||
- `test/skillpack-reference.test.ts`
|
||||
- **Approach:** Add `idea-lineage` to the skill manifest and plugin skill list. Add a resolver row in the thinking or uncategorized section with narrow user phrases such as "how has my thinking about", "trace the lineage of this idea", "what is my current version of", and "show reversals in my thinking about". Keep broad concept-map phrases routed to `concept-synthesis`.
|
||||
- **Patterns to follow:**
|
||||
- `skills/RESOLVER.md` rows for `strategic-reading`, `concept-synthesis`, and `perplexity-research`.
|
||||
- Existing sorted `openclaw.plugin.json` skill list.
|
||||
- **Test scenarios:**
|
||||
- Every quoted resolver trigger fuzzy-matches a frontmatter trigger in `skills/idea-lineage/SKILL.md`.
|
||||
- `idea-lineage` is listed in `skills/manifest.json`.
|
||||
- `idea-lineage` is listed in `openclaw.plugin.json` if the contribution ships as part of the bundled OpenClaw skillpack.
|
||||
- Existing skills remain reachable.
|
||||
- **Verification:** `bun test test/resolver.test.ts` passes.
|
||||
|
||||
### U3. Add Routing Eval Fixtures
|
||||
|
||||
- **Goal:** Prove the new routing boundary against adjacent skills.
|
||||
- **Requirements:** R6, R7, R8
|
||||
- **Dependencies:** U1, U2
|
||||
- **Files:**
|
||||
- `skills/idea-lineage/routing-eval.jsonl`
|
||||
- `skills/concept-synthesis/routing-eval.jsonl`
|
||||
- `src/core/routing-eval.ts`
|
||||
- **Approach:** Add positive fixtures for single-idea lineage prompts and negative or ambiguity-declared fixtures around adjacent surfaces. The fixture text should paraphrase triggers rather than copy them exactly, because the routing fixture linter rejects tautological trigger copies.
|
||||
- **Test scenarios:**
|
||||
- "Show how my thinking about founder-led sales changed over time" routes to `idea-lineage`.
|
||||
- "What is my current version of the compounding trust idea?" routes to `idea-lineage`.
|
||||
- "Synthesize my concepts into a tiered intellectual map" stays on `concept-synthesis`.
|
||||
- "How has acme-example MRR trended since January?" does not route to `idea-lineage`.
|
||||
- Negative fixtures avoid false positives for generic "publish this report" or "what is this concept?" prompts.
|
||||
- **Verification:** `gbrain routing-eval --json` reports no new misses, false positives, or unapproved ambiguity for the added fixtures.
|
||||
|
||||
### U4. Add Output Contract and Citation Discipline
|
||||
|
||||
- **Goal:** Make the skill's user-facing answer shape predictable and reviewable.
|
||||
- **Requirements:** R2, R3, R4, R5
|
||||
- **Dependencies:** U1
|
||||
- **Files:**
|
||||
- `skills/idea-lineage/SKILL.md`
|
||||
- `skills/conventions/quality.md`
|
||||
- `skills/brain-ops/SKILL.md`
|
||||
- **Approach:** Define the output format directly in the skill body. The recommended shape should include a compact current answer, evidence timeline, lineage buckets, contradictions/reversals, abandoned branches, related concepts, and confidence gaps. Require page/date/snippet evidence for each non-gap claim. Preserve quote fidelity and avoid hallucinated dates.
|
||||
- **Patterns to follow:**
|
||||
- `skills/conventions/quality.md` for citation and quote-fidelity expectations.
|
||||
- `skills/brain-ops/SKILL.md` for source attribution and source-id formatting.
|
||||
- `docs/takes-vs-facts.md` for not conflating holder-attributed takes with the brain owner's facts.
|
||||
- **Test scenarios:**
|
||||
- Test expectation: none beyond conformance for the markdown-only contract; routing and conformance tests cover the machine-checkable surface.
|
||||
- **Verification:** Manual review confirms the skill body tells the agent how to cite, label gaps, and separate facts/takes/trajectory evidence.
|
||||
|
||||
### U5. Refresh Generated Documentation If Required
|
||||
|
||||
- **Goal:** Keep generated LLM-facing docs consistent if the test suite requires it.
|
||||
- **Requirements:** R9, R10
|
||||
- **Dependencies:** U1, U2, U3
|
||||
- **Files:**
|
||||
- `llms.txt`
|
||||
- `llms-full.txt`
|
||||
- `test/build-llms.test.ts`
|
||||
- **Approach:** Run the build-llms test after adding the skill. If it fails because committed docs are stale, regenerate with the existing generator and include the generated diff. If it passes without regeneration, leave these files unchanged.
|
||||
- **Patterns to follow:**
|
||||
- `package.json` script `build:llms`.
|
||||
- `test/build-llms.test.ts` failure message.
|
||||
- **Test scenarios:**
|
||||
- Committed `llms.txt` and `llms-full.txt` match generator output.
|
||||
- `llms-full.txt` remains within the size budget.
|
||||
- **Verification:** `bun test test/build-llms.test.ts` passes.
|
||||
|
||||
## Acceptance Examples
|
||||
|
||||
- AE1. When the user asks "How has my thinking about founder-led sales changed over time?", the agent routes to `idea-lineage`, searches for evidence, and returns a cited lineage rather than running `concept-synthesis`.
|
||||
- AE2. When the user asks "Run concept synthesis across my notes", the agent routes to `concept-synthesis`, not `idea-lineage`.
|
||||
- AE3. When the user asks "How did acme-example's MRR trend?", the agent uses trajectory surfaces rather than `idea-lineage`.
|
||||
- AE4. When the evidence does not support an "abandoned branch" claim, the output includes a gap instead of inventing one.
|
||||
|
||||
## Risks & Dependencies
|
||||
|
||||
- **Resolver overlap risk:** `concept-synthesis` already uses "trace idea evolution". Mitigate by avoiding that exact trigger and adding routing fixtures around the boundary.
|
||||
- **Narrative overreach risk:** The feature invites story-making. Mitigate by requiring dates, snippets, links, and explicit gaps for unsupported categories.
|
||||
- **Privacy risk:** Skill examples can easily drift into real-brain language. Use synthetic examples only and rely on existing privacy checks.
|
||||
- **Generated-doc churn risk:** Adding a bundled skill may require `llms.txt` and `llms-full.txt` regeneration. Treat generated-doc changes as mechanical and separate from the skill design during review.
|
||||
- **Future taxonomy dependency:** Issue #1668 may eventually change concept filing and identity. This plan avoids new schema assumptions so the contribution remains compatible with the current repo.
|
||||
|
||||
## Sources & Research
|
||||
|
||||
- `skills/concept-synthesis/SKILL.md` defines the existing batch, mutating, concept-map surface.
|
||||
- `skills/RESOLVER.md` and `skills/manifest.json` define current skill reachability and bundle metadata.
|
||||
- `docs/architecture/lens-packs.md` shows that atoms and concepts are already part of the lens-pack/dream-cycle substrate.
|
||||
- `docs/proposals/temporal-contradiction-probe.md` and `docs/takes-vs-facts.md` define the temporal and epistemic boundaries this skill must not blur.
|
||||
- `src/core/operations.ts`, `src/core/trajectory.ts`, `src/commands/eval-trajectory.ts`, and `test/operations-find-trajectory.test.ts` define the current `find_trajectory` contract.
|
||||
- Pull requests #1131, #1296, and #1364 provide the recent trajectory, think-routing, and lens-pack context.
|
||||
- Issue #1668 is related future taxonomy work, but not a prerequisite for this contribution.
|
||||
@@ -140,9 +140,6 @@ Stable phase names shipped in v0.15.2:
|
||||
- `import.files`
|
||||
- `sync.deletes`, `sync.renames`, `sync.imports`
|
||||
- `migrate.copy_pages`, `migrate.copy_links`
|
||||
- `migrate.reembed` (the re-embed pass of `gbrain migrate embeddings`; total is the
|
||||
stale-chunk backlog at the start of the pass, so it can grow slightly if a
|
||||
writer adds chunks mid-run)
|
||||
- `repair_jsonb.run`, `repair_jsonb.<table>.<column>`
|
||||
- `backlinks.scan`
|
||||
- `lint.pages`
|
||||
|
||||
@@ -91,18 +91,3 @@ First full takes extraction run on a ~100K-page brain:
|
||||
4. **Self-reported ≠ verified.** "Reports 7 figures" → holder=person, weight=0.75, NOT world/1.0
|
||||
5. **No false precision.** Use 0.05 increments (0.35, 0.55, 0.75), not 0.74 or 0.82
|
||||
6. **"So what" test.** Skip Twitter handles, follower counts, obvious metadata
|
||||
|
||||
## Owner-holder canonicalization
|
||||
|
||||
"The brain owner" is, by convention, the holder string **`self`** — the value the
|
||||
dream `consolidate` phase stamps when it promotes the owner's hot facts into cold
|
||||
takes. Calibration, `think`, and the `doctor` calibration check resolve the owner
|
||||
holder through `resolveOwnerHolder` (`src/core/owner-holder.ts`): explicit override
|
||||
> `emotional_weight.user_holder` config > `self`.
|
||||
|
||||
Known limitation (tracked in garrytan/gbrain#2465): the owner can also
|
||||
appear under `brain` (a take the owner asserts, via `propose_takes`) and
|
||||
`people/<owner>` (extraction that names the owner). The resolver selects the
|
||||
*default* canonical owner string for reads; it does not merge those other
|
||||
strings. Per-take attribution for other people (e.g. `people/george`) is
|
||||
unaffected and correct.
|
||||
|
||||
@@ -13,7 +13,7 @@ Step-by-step walkthroughs that take you from zero to a working outcome. Concrete
|
||||
|
||||
These are the next tutorials on the roadmap. Open an issue if one of them is the one you need most; that's how we'll prioritize.
|
||||
|
||||
- **Set up GBrain for VC dealflow** — the operator's recipe. People pages for founders, companies with typed Facts fence carrying ARR / team-size / runway across dates, meetings auto-ingested, deal pages linking everything. Shows `gbrain whoknows`, `gbrain find-trajectory`, and `gbrain founder scorecard` on real workflows.
|
||||
- **Set up GBrain for VC dealflow** — the operator's recipe. People pages for founders, companies with typed Facts fence carrying ARR / team-size / runway across dates, meetings auto-ingested, deal pages linking everything. Shows `gbrain whoknows`, `gbrain find_trajectory`, and `gbrain founder scorecard` on real workflows.
|
||||
|
||||
- **Migrate your existing vault into GBrain** — for Notion / Obsidian / Roam users with a vault that doesn't match GBrain's default layout. Walks through `gbrain schema detect` → `suggest` → `review-candidates` so the brain learns your shape instead of forcing you to learn its.
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ gbrain serve --http --port 3131 --bind 0.0.0.0
|
||||
|
||||
The `--bind 0.0.0.0` is important. By default the server binds to localhost only, which is correct for a personal install but blocks remote teammates. Setting `0.0.0.0` accepts connections from any interface.
|
||||
|
||||
The server prints an admin bootstrap token to stderr on first start when run in an interactive terminal. Save it. You'll use it once for the admin dashboard. On a non-TTY start (systemd, Docker, piped logs) the token is hidden from logs — set `GBRAIN_ADMIN_BOOTSTRAP_TOKEN` yourself or pass `--print-admin-token` on a trusted terminal instead.
|
||||
The server prints an admin bootstrap token to stderr on first start. Save it. You'll use it once for the admin dashboard.
|
||||
|
||||
For development, tunnel the local server out via ngrok:
|
||||
|
||||
@@ -484,10 +484,6 @@ Returns a per-source dashboard: when each source last synced, how many pages, ho
|
||||
|
||||
The admin dashboard at `https://brain.acme-co.com/admin` shows live request volume, registered OAuth clients, recent activity, and brain stats. Use the admin bootstrap token from Part 4 to log in the first time, then register additional admin users from inside the dashboard.
|
||||
|
||||
### If agents run as containers on the same Docker host
|
||||
|
||||
OAuth source scoping only guards the HTTP MCP path. If the brain's Postgres and your teammates' agent runtimes are containers on the same Docker host, make sure the agents can't reach Postgres directly over Docker's default bridge network — a direct DB session skips OAuth entirely. Put Postgres on its own user-defined network, publish it loopback-only if at all, and never hand agent containers a `DATABASE_URL`. The copy-paste operator checklist lives in [docs/mcp/DEPLOY.md — Co-located Docker workloads](../mcp/DEPLOY.md#co-located-docker-workloads-self-hosted-postgres).
|
||||
|
||||
---
|
||||
|
||||
## Part 13: Cost and speed expectations
|
||||
@@ -554,7 +550,7 @@ What to do next:
|
||||
|
||||
- **Wire ingestion** from external systems (Granola, Linear, Slack) using the [ingestion source contract](../skillpack-anatomy.md). Most companies want their meetings auto-ingested so the brain stays current without anyone typing notes.
|
||||
- **Set up team-specific dashboards** through the admin UI. Each team lead can have their own view of brain health and activity.
|
||||
- **Explore the rest of the brain layer.** `gbrain whoknows` (find the expert on a topic), `gbrain find-trajectory` (how a metric changed over time), `gbrain founder scorecard` (especially useful for VC and ops teams), the contradiction-detection cycle that surfaces conflicts between different people's notes.
|
||||
- **Explore the rest of the brain layer.** `gbrain whoknows` (find the expert on a topic), `gbrain find_trajectory` (how a metric changed over time), `gbrain founder scorecard` (especially useful for VC and ops teams), the contradiction-detection cycle that surfaces conflicts between different people's notes.
|
||||
|
||||
If you're building in this space (which YC has flagged as the [company-brain category in its Request for Startups](https://www.ycombinator.com/rfs#company-brain)), you might as well build on this. Everything described above is open source, MIT licensed, and what I run in production behind my own AI agents.
|
||||
|
||||
|
||||
@@ -233,30 +233,20 @@ keep it or `git checkout` to throw it away. Nothing is committed for you.
|
||||
|
||||
**For a skill that ships with gbrain** (anything under the gbrain repo's own
|
||||
`skills/`): SkillOpt refuses to overwrite it by default and writes the winner to
|
||||
`skills/<name>/skillopt/proposed.md` instead (while keeping `best.md` as the
|
||||
optimizer's current-best pointer), so an optimization pass can never silently
|
||||
mutate a skill other people depend on. Two ways to handle that:
|
||||
`skills/<name>/skillopt/best.md` instead, so an optimization pass can never
|
||||
silently mutate a skill other people depend on. Two ways to handle that:
|
||||
|
||||
```bash
|
||||
# See the proposed improvement without touching SKILL.md (works for ANY skill):
|
||||
gbrain skillopt meeting-prep --split 1:1:1 --no-mutate
|
||||
# → writes skills/meeting-prep/skillopt/proposed.md, updates best.md, and prints the proposal path.
|
||||
# → writes skills/meeting-prep/skillopt/best.md, prints its path. Copy what you want.
|
||||
|
||||
# Actually rewrite a bundled skill (explicit opt-in + an independent held-out set):
|
||||
gbrain skillopt brain-ops --split 1:1:1 --allow-mutate-bundled \
|
||||
--held-out skills/brain-ops/held-out.jsonl
|
||||
# Actually rewrite a bundled skill (explicit opt-in):
|
||||
gbrain skillopt brain-ops --split 1:1:1 --allow-mutate-bundled
|
||||
```
|
||||
|
||||
Rewriting a bundled skill in place now requires BOTH `--allow-mutate-bundled` AND
|
||||
`--held-out <path>` (a JSONL with the same shape as your benchmark, but at least 5
|
||||
tasks whose IDs don't appear in the benchmark). The held-out set is how the run
|
||||
proves the edit didn't just learn the benchmark: a candidate that climbs the
|
||||
benchmark but slips on the held-out tasks is refused. Drop `--held-out` and the
|
||||
run hard-refuses and points you at `proposed.md` instead.
|
||||
|
||||
Rule of thumb: `--no-mutate` when you want to read the diff before trusting it
|
||||
(no held-out needed); `--allow-mutate-bundled --held-out` only when you intend to
|
||||
commit a proven change to a shared skill.
|
||||
Rule of thumb: `--no-mutate` when you want to read the diff before trusting it;
|
||||
`--allow-mutate-bundled` only when you intend to commit a change to a shared skill.
|
||||
|
||||
## Step 6: Iterate
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ Save this token. You'll need it for the AlphaClaw setup.
|
||||
|
||||
AlphaClaw is the setup harness that manages OpenClaw deployment.
|
||||
|
||||
1. Go to [alphaclaw.md](https://alphaclaw.md)
|
||||
1. Go to [alphaclaw.com](https://alphaclaw.com)
|
||||
2. Enter your **workspace repo** (not the brain repo): `your-org/myagent`
|
||||
3. Select "Use existing" if the repo already exists
|
||||
4. Enter your GitHub PAT from Step 2
|
||||
@@ -115,21 +115,21 @@ You can use the same keys across multiple agents.
|
||||
|
||||
## Step 6: Install GBrain
|
||||
|
||||
Once OpenClaw is running, installation is two commands — one in the brain repo, one in the agent workspace:
|
||||
Once OpenClaw is running:
|
||||
|
||||
```bash
|
||||
# In the BRAIN repo (the git repo that holds your markdown pages):
|
||||
gbrain init --supabase
|
||||
|
||||
# In the AGENT WORKSPACE repo (where OpenClaw runs):
|
||||
gbrain skillpack scaffold --all
|
||||
gbrain install
|
||||
```
|
||||
|
||||
`gbrain init --supabase` walks a short wizard that asks for your Supabase connection string and creates the schema. You'll get that connection string in Step 7 — read 7a and 7b first so you paste the right one (the transaction pooler, not the direct connection). If you'd rather try things locally before paying for a database, `gbrain init --pglite` gives you a zero-config embedded engine instead; you can migrate to Supabase later with `gbrain migrate --to supabase`.
|
||||
This installs:
|
||||
|
||||
`gbrain skillpack scaffold --all` copies the ~43 bundled skills into your agent workspace as first-class files you can edit freely. (The old managed-install model was retired in v0.36.0.0; see `docs/INSTALL.md` if you're upgrading from an older release.)
|
||||
- About 60 skills
|
||||
- About 9 skill packs
|
||||
- Default brain structure
|
||||
- MCP server configuration
|
||||
- Supabase connection (for embeddings and search)
|
||||
|
||||
From this point, the agent has working memory and access to every skill.
|
||||
GBrain populates the brain repo with its default directory structure, skill files, and configuration. From this point, the agent has working memory and access to every skill.
|
||||
|
||||
---
|
||||
|
||||
@@ -145,15 +145,14 @@ GBrain uses Supabase for vector embeddings and full-text search at scale. There
|
||||
|
||||
Skip this and every embed write fails with "type vector does not exist" the moment GBrain tries to create its schema. pgvector is what stores the embeddings; the schema migrations refuse to run without it. Five seconds in the UI; an hour of debugging if you forget.
|
||||
|
||||
### 7b. Get the TRANSACTION POOLER connection string, not the direct one
|
||||
### 7b. Get the CONNECTION POOLER connection string, not the direct one
|
||||
|
||||
In the Supabase dashboard, click **Connect** in the top navigation bar, then **Connection String**. Supabase shows three options. They look almost identical. Use the right one.
|
||||
In **Project Settings → Database → Connection string**, Supabase shows you two options. They look almost identical. Use the right one.
|
||||
|
||||
- **Direct connection** (port 5432, host `db.YOUR-PROJECT.supabase.co`). Talks straight to the Postgres instance. IPv6-only. Will fail if your Render host doesn't have IPv6 outbound (most don't by default).
|
||||
- **Transaction pooler** (port 6543, host `aws-0-...pooler.supabase.com`). Talks through Supabase's pooler (Supavisor) in transaction mode. Works over IPv4. Survives connection storms from parallel workers. GBrain is tuned for this one: it auto-disables prepared statements on port 6543 and routes migrations, DDL, and worker locks to a separate direct connection (see 7c).
|
||||
- **Session pooler** (port 5432, host `aws-0-...pooler.supabase.com`). Also works over IPv4, with full session features. You don't need it as your main URL, but it's the free way to fix the IPv4 gotcha in 7c.
|
||||
- **Direct connection** (port 5432). Talks straight to the Postgres instance. IPv6-only. Will fail if your Render host doesn't have IPv6 outbound (most don't by default).
|
||||
- **Connection pooler** (port 6543, hostname starts with `aws-0-...pooler.supabase.com`). Talks through Supabase's pgbouncer. Works over IPv4. Survives connection storms from parallel workers.
|
||||
|
||||
You want the **Transaction pooler** string. Format looks like:
|
||||
You want the **connection pooler** string. Format looks like:
|
||||
|
||||
```
|
||||
postgresql://postgres.YOUR-PROJECT:YOUR-PASSWORD@aws-0-us-west-1.pooler.supabase.com:6543/postgres
|
||||
@@ -165,23 +164,11 @@ Configure it via:
|
||||
gbrain config set database_url "postgresql://postgres.YOUR-PROJECT:YOUR-PASSWORD@aws-0-us-west-1.pooler.supabase.com:6543/postgres"
|
||||
```
|
||||
|
||||
### 7c. Fix the IPv4 gotcha for migrations, DDL, and worker locks
|
||||
### 7c. Buy the IPv4 add-on if your host is IPv4-only
|
||||
|
||||
The transaction pooler (7b) carries your normal reads and writes over IPv4. But GBrain runs schema migrations, DDL, and background-worker locks on a *direct* connection, which it derives from your pooler URL by swapping the host to `db.YOUR-PROJECT.supabase.co:5432`. That direct host is **IPv6-only**. On an IPv4-only host (most Render plans), reads work but migrations hang and worker locks orphan, often silently.
|
||||
Even with the pooler, some Supabase regions and some Render plans hit IPv6 resolution snags. If your `gbrain doctor` shows connection failures and the error mentions "network unreachable" or hangs forever on connect, you need Supabase's **IPv4 add-on**.
|
||||
|
||||
Two ways to fix it. The free one first:
|
||||
|
||||
**Free: point GBrain's direct connection at the Session pooler.** The session pooler is the same Supavisor host on port 5432, and it's IPv4. Copy the **Session pooler** string from the same **Connect → Connection String** panel and set it as the direct-connection override:
|
||||
|
||||
```bash
|
||||
export GBRAIN_DIRECT_DATABASE_URL="postgresql://postgres.YOUR-PROJECT:YOUR-PASSWORD@aws-0-us-west-1.pooler.supabase.com:5432/postgres"
|
||||
```
|
||||
|
||||
Now both pools — reads on the transaction pooler (6543), DDL and locks on the session pooler (5432) — run over IPv4 at zero extra cost.
|
||||
|
||||
**Paid: buy Supabase's IPv4 add-on.** About $4 a month, Pro tier or higher. It makes the direct `db.*.supabase.co` host reachable over IPv4, so the derived direct connection just works with no extra config. In the Supabase dashboard, **Project Settings → Add-ons → IPv4 address**. Toggle on, wait a minute, retry.
|
||||
|
||||
Either fixes it. If `gbrain doctor` still shows connection failures that mention "network unreachable" or hangs forever on connect, you haven't done one of these yet.
|
||||
In the Supabase dashboard, **Project Settings → Add-ons → IPv4 address**. About $4 a month. Toggle on, wait a minute, retry the connection. This bit me on multiple installs before I learned to just buy it up front.
|
||||
|
||||
### 7d. Verify the connection
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ gbrain schema sync --apply
|
||||
The sync backfills `page.type = 'meeting'` on all 4000 pages in 1000-row batches. Now:
|
||||
|
||||
- `gbrain whoknows "Q3 roadmap discussion"` routes through the meeting type, ranking by `expert_routing` signal (attendees, recency, salience) instead of raw text.
|
||||
- The `extract_facts` cycle runs on every meeting page automatically (because `extractable: true`), pulling typed facts like `attended_by=alice-example`, `date=2026-05-23`.
|
||||
- `gbrain extract-facts` runs on every meeting page automatically (because `extractable: true`), pulling typed facts like `attended_by=alice-example`, `date=2026-05-23`.
|
||||
- The downstream `think` skill can now answer "what did we decide about pricing in the last three roadmap meetings" by querying the meeting graph instead of grep'ing 4000 files.
|
||||
|
||||
One command. 4000 pages went from invisible to queryable. The content didn't change. The structure did.
|
||||
@@ -62,7 +62,7 @@ gbrain schema add-link-type led-by --page-type deal --target-type inves
|
||||
gbrain schema sync --apply
|
||||
```
|
||||
|
||||
Now `gbrain whoknows "Series A SaaS"` routes through `investor` and `portco` types specifically, not the noisy general type set. `gbrain graph-query alice-example --type intro-from --depth 2` walks two hops of intros to surface "Alice introduced you to Bob who introduced you to Charlie." The `extract_facts` cycle starts producing typed claims from the fence in your deal pages: `(deals/acme-seed, raise=2000000, valuation=15000000, lead=widget-vc, closed_at=2026-05-23)`.
|
||||
Now `gbrain whoknows "Series A SaaS"` routes through `investor` and `portco` types specifically, not the noisy general type set. `gbrain graph-query alice-example --type intro-from --depth 2` walks two hops of intros to surface "Alice introduced you to Bob who introduced you to Charlie." `gbrain extract-facts` starts producing typed claims from the fence in your deal pages: `(deals/acme-seed, raise=2000000, valuation=15000000, lead=widget-vc, closed_at=2026-05-23)`.
|
||||
|
||||
The CRM you've been promising yourself you'll set up next quarter? You just shipped it in 4 commands. It's downstream of your notes, not parallel to them.
|
||||
|
||||
@@ -143,7 +143,7 @@ Re-run the same `whoknows` query. Top-3 should shift, because the new type is no
|
||||
|
||||
Three things gbrain does that generic note systems can't:
|
||||
|
||||
**1. The brain knows the difference between a person and an idea.** Page-type matters at query time. `gbrain whoknows` only considers `expert_routing: true` types. The `extract_facts` cycle only runs on `extractable: true` types. `gbrain graph-query` walks declared link verbs. None of that works on a flat tag system because tags don't have semantics — they're labels. Types are first-class citizens with rules attached.
|
||||
**1. The brain knows the difference between a person and an idea.** Page-type matters at query time. `gbrain whoknows` only considers `expert_routing: true` types. `gbrain extract-facts` only runs on `extractable: true` types. `gbrain graph-query` walks declared link verbs. None of that works on a flat tag system because tags don't have semantics — they're labels. Types are first-class citizens with rules attached.
|
||||
|
||||
**2. Untyped content is invisible content.** If your meetings are typed as `note`, expert routing skips them, facts extraction ignores them, link inference doesn't fire. They exist on disk and they're indexed for text search, but the structural surfaces (whoknows, find_experts, recall, think) treat them as second-class. Adding a type isn't cosmetic; it's structural promotion.
|
||||
|
||||
|
||||
@@ -415,7 +415,6 @@ export async function main(argv: string[]): Promise<number> {
|
||||
chat_model: config?.chat_model ?? modelFull,
|
||||
chat_fallback_chain: config?.chat_fallback_chain,
|
||||
base_urls: config?.provider_base_urls,
|
||||
provider_chat_options: config?.provider_chat_options,
|
||||
env: { ...process.env } as Record<string, string>,
|
||||
});
|
||||
|
||||
|
||||
+1471
-529
File diff suppressed because one or more lines are too long
@@ -7,9 +7,7 @@ Repo: https://github.com/garrytan/gbrain
|
||||
## Core entry points
|
||||
|
||||
- [AGENTS.md](https://raw.githubusercontent.com/garrytan/gbrain/master/AGENTS.md): Start here if you are not Claude Code. Install order, trust boundary, skill resolver, config/debug/migration pointers.
|
||||
- [CLAUDE.md](https://raw.githubusercontent.com/garrytan/gbrain/master/CLAUDE.md): Orientation + resolver. North Star, two axes, architecture + cross-cutting invariants, the reference map pointing at on-demand docs, and the inline ship IRON RULES.
|
||||
- [docs/architecture/KEY_FILES.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/architecture/KEY_FILES.md): Per-file index for the gbrain repo: what each src/ file does + its load-bearing invariants. The on-demand detail CLAUDE.md's reference map routes to.
|
||||
- [docs/architecture/thin-client.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/architecture/thin-client.md): The thin-client / remote-MCP / cross-modal routing seam: isThinClient detection, callRemoteTool, SSRF-hardened URL validation, per-command routing.
|
||||
- [CLAUDE.md](https://raw.githubusercontent.com/garrytan/gbrain/master/CLAUDE.md): Architecture reference. Key files, trust boundaries, engine factory, test layout.
|
||||
- [INSTALL_FOR_AGENTS.md](https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md): 9-step agent installation.
|
||||
- [skills/RESOLVER.md](https://raw.githubusercontent.com/garrytan/gbrain/master/skills/RESOLVER.md): Skill dispatcher. Read first for any task.
|
||||
- [README.md](https://raw.githubusercontent.com/garrytan/gbrain/master/README.md): Project overview, benchmarks, 30-minute setup.
|
||||
@@ -25,7 +23,6 @@ Repo: https://github.com/garrytan/gbrain
|
||||
- [docs/guides/minions-deployment.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/minions-deployment.md): Deploying the gbrain jobs worker: crontab + watchdog, inline --follow, systemd/Procfile/fly.toml, upgrade checklist.
|
||||
- [docs/guides/quiet-hours.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/quiet-hours.md): Notification hold + timezone-aware delivery.
|
||||
- [docs/guides/scaling-skills.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/scaling-skills.md): Three-tier architecture for agents with 300+ skills: always-loaded, resolver-routed, and dormant. Per-turn token math, the v0.41.7.0 compact list-format resolver, and the `gbrain doctor` safety net. 306 skills, ~21K tokens freed per turn, zero capability loss.
|
||||
- [docs/guides/push-context.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/push-context.md): Push-based context: the brain volunteers confidence-gated pages from the rolling conversation window. Three channels (ambient reflex, volunteer_context op, gbrain watch), config knobs, and the volunteered-vs-used feedback loop.
|
||||
- [docs/mcp/DEPLOY.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY.md): MCP server deployment.
|
||||
|
||||
## AI providers
|
||||
@@ -45,11 +42,6 @@ Repo: https://github.com/garrytan/gbrain
|
||||
- [skills/migrations/](https://raw.githubusercontent.com/garrytan/gbrain/master/skills/migrations/): Per-version (v0.5.0 - v0.14.1) agent-executable migration instructions.
|
||||
- [CHANGELOG.md](https://raw.githubusercontent.com/garrytan/gbrain/master/CHANGELOG.md): Release-summary voice + itemized changes + self-repair block per version.
|
||||
|
||||
## Contributing
|
||||
|
||||
- [docs/TESTING.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/TESTING.md): Test command tiers, the test-isolation lint (R1-R4), the canonical PGLite block, withEnv, the E2E DB lifecycle, and the file taxonomy. Maintainer-facing.
|
||||
- [docs/RELEASING.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/RELEASING.md): Full release + contributor process: pre-ship test requirements, the CHANGELOG voice + release-summary template, the 'To take advantage of vX' block, version migrations, GitHub Actions SHA refresh, PR conventions, community-PR-wave. (Ship IRON RULES stay inline in CLAUDE.md.)
|
||||
|
||||
## Philosophy
|
||||
|
||||
- [docs/ethos/THIN_HARNESS_FAT_SKILLS.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ethos/THIN_HARNESS_FAT_SKILLS.md): Why skills live in markdown.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"id": "gbrain-context-engine",
|
||||
"name": "gbrain",
|
||||
"version": "0.32.3.0",
|
||||
"description": "Personal knowledge brain with Postgres + pgvector hybrid search",
|
||||
@@ -47,9 +46,7 @@
|
||||
"skills/data-research",
|
||||
"skills/enrich",
|
||||
"skills/functional-area-resolver",
|
||||
"skills/gbrain-advisor",
|
||||
"skills/idea-ingest",
|
||||
"skills/idea-lineage",
|
||||
"skills/ingest",
|
||||
"skills/maintain",
|
||||
"skills/media-ingest",
|
||||
|
||||
+34
-50
@@ -23,7 +23,6 @@
|
||||
"./backoff": "./src/core/backoff.ts",
|
||||
"./search/hybrid": "./src/core/search/hybrid.ts",
|
||||
"./search/expansion": "./src/core/search/expansion.ts",
|
||||
"./think": "./src/core/think/index.ts",
|
||||
"./ai/gateway": "./src/core/ai/gateway.ts",
|
||||
"./extract": "./src/commands/extract.ts",
|
||||
"./ingestion": "./src/core/ingestion/index.ts",
|
||||
@@ -42,20 +41,19 @@
|
||||
"eval:autocut": "bun test test/search/autocut-eval.test.ts",
|
||||
"test:full": "bun run verify && bash scripts/run-unit-parallel.sh && bun run test:slow && ([ -n \"$DATABASE_URL\" ] && bash scripts/run-e2e.sh || echo '[test:full] skipped E2E (no DATABASE_URL); run docker-compose -f docker-compose.ci.yml up + bun run test:e2e to include' 1>&2)",
|
||||
"verify": "bash scripts/run-verify-parallel.sh",
|
||||
"check:source-config-leak": "bash scripts/check-source-config-leak.sh",
|
||||
"check:no-pii-agent-voice": "bash scripts/check-no-pii-in-agent-voice.sh",
|
||||
"check:synthetic-corpus-privacy": "bash scripts/check-synthetic-corpus-privacy.sh",
|
||||
"check:system-of-record": "bash scripts/check-system-of-record.sh",
|
||||
"check:admin-scope-drift": "bash scripts/check-admin-scope-drift.sh",
|
||||
"check:cli-exec": "bash scripts/check-cli-executable.sh",
|
||||
"check:all": "bash scripts/check-privacy.sh && bash scripts/check-proposal-pii.sh && bash scripts/check-test-real-names.sh && bash scripts/check-jsonb-pattern.sh && bash scripts/check-source-id-projection.sh && bash scripts/check-source-config-leak.sh && bash scripts/check-progress-to-stdout.sh && bash scripts/check-no-tracked-symlinks.sh && bash scripts/check-no-legacy-getconnection.sh && bash scripts/check-test-isolation.sh && bash scripts/check-trailing-newline.sh && bash scripts/check-wasm-embedded.sh && bash scripts/check-exports-count.sh && bash scripts/check-admin-build.sh && bash scripts/check-admin-scope-drift.sh && bash scripts/check-cli-executable.sh && bash scripts/check-skill-brain-first.sh && bash scripts/check-operations-filter-bypass.sh && bash scripts/check-gateway-routed-no-direct-anthropic.sh && bash scripts/check-worker-pool-atomicity.sh && bash scripts/check-key-files-current-state.sh && bash scripts/check-no-double-retry.sh && bash scripts/check-batch-audit-site.sh",
|
||||
"check:gateway-routed": "bash scripts/check-gateway-routed-no-direct-anthropic.sh",
|
||||
"check:worker-pool-atomicity": "bash scripts/check-worker-pool-atomicity.sh",
|
||||
"check:doc-history": "bash scripts/check-key-files-current-state.sh",
|
||||
"check:source-config-leak": "scripts/check-source-config-leak.sh",
|
||||
"check:no-pii-agent-voice": "scripts/check-no-pii-in-agent-voice.sh",
|
||||
"check:synthetic-corpus-privacy": "scripts/check-synthetic-corpus-privacy.sh",
|
||||
"check:system-of-record": "scripts/check-system-of-record.sh",
|
||||
"check:admin-scope-drift": "scripts/check-admin-scope-drift.sh",
|
||||
"check:cli-exec": "scripts/check-cli-executable.sh",
|
||||
"check:all": "scripts/check-privacy.sh && scripts/check-proposal-pii.sh && scripts/check-test-real-names.sh && scripts/check-jsonb-pattern.sh && scripts/check-source-id-projection.sh && scripts/check-source-config-leak.sh && scripts/check-progress-to-stdout.sh && scripts/check-no-legacy-getconnection.sh && scripts/check-test-isolation.sh && scripts/check-trailing-newline.sh && scripts/check-wasm-embedded.sh && scripts/check-exports-count.sh && scripts/check-admin-build.sh && scripts/check-admin-scope-drift.sh && scripts/check-cli-executable.sh && scripts/check-skill-brain-first.sh && scripts/check-operations-filter-bypass.sh && scripts/check-gateway-routed-no-direct-anthropic.sh && scripts/check-worker-pool-atomicity.sh && scripts/check-no-double-retry.sh && scripts/check-batch-audit-site.sh",
|
||||
"check:gateway-routed": "scripts/check-gateway-routed-no-direct-anthropic.sh",
|
||||
"check:worker-pool-atomicity": "scripts/check-worker-pool-atomicity.sh",
|
||||
"check:resolver": "bun src/cli.ts check-resolvable --strict --skills-dir skills/",
|
||||
"check:skill-brain-first": "bash scripts/check-skill-brain-first.sh",
|
||||
"check:wasm": "bash scripts/check-wasm-embedded.sh",
|
||||
"check:newlines": "bash scripts/check-trailing-newline.sh",
|
||||
"check:skill-brain-first": "scripts/check-skill-brain-first.sh",
|
||||
"check:wasm": "scripts/check-wasm-embedded.sh",
|
||||
"check:newlines": "scripts/check-trailing-newline.sh",
|
||||
"test:e2e": "bash scripts/run-e2e.sh",
|
||||
"test:slow": "bash scripts/run-slow-tests.sh",
|
||||
"test:heavy": "bash scripts/run-heavy.sh",
|
||||
@@ -65,28 +63,26 @@
|
||||
"ci:local:diff": "bash scripts/ci-local.sh --diff",
|
||||
"ci:select-e2e": "bun run scripts/select-e2e.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check:jsonb": "bash scripts/check-jsonb-pattern.sh",
|
||||
"check:search-path": "bash scripts/check-search-path.sh",
|
||||
"check:no-double-retry": "bash scripts/check-no-double-retry.sh",
|
||||
"check:batch-audit-site": "bash scripts/check-batch-audit-site.sh",
|
||||
"check:worker-lock-renewal-shape": "bash scripts/check-worker-lock-renewal-shape.sh",
|
||||
"check:source-id-projection": "bash scripts/check-source-id-projection.sh",
|
||||
"check:privacy": "bash scripts/check-privacy.sh",
|
||||
"check:proposal-pii": "bash scripts/check-proposal-pii.sh",
|
||||
"check:eval-glossary": "bash scripts/check-eval-glossary-fresh.sh",
|
||||
"check:test-names": "bash scripts/check-test-real-names.sh",
|
||||
"check:progress": "bash scripts/check-progress-to-stdout.sh",
|
||||
"check:no-tracked-symlinks": "bash scripts/check-no-tracked-symlinks.sh",
|
||||
"check:exports-count": "bash scripts/check-exports-count.sh",
|
||||
"check:admin-build": "bash scripts/check-admin-build.sh",
|
||||
"check:admin-embedded": "bash scripts/check-admin-embedded.sh",
|
||||
"check:test-isolation": "bash scripts/check-test-isolation.sh",
|
||||
"check:fuzz-purity": "bash scripts/check-fuzz-purity.sh",
|
||||
"check:operations-filter-bypass": "bash scripts/check-operations-filter-bypass.sh",
|
||||
"check:fixture-privacy": "bash scripts/check-fixture-privacy.sh",
|
||||
"check:jsonb": "scripts/check-jsonb-pattern.sh",
|
||||
"check:no-double-retry": "scripts/check-no-double-retry.sh",
|
||||
"check:batch-audit-site": "scripts/check-batch-audit-site.sh",
|
||||
"check:worker-lock-renewal-shape": "scripts/check-worker-lock-renewal-shape.sh",
|
||||
"check:source-id-projection": "scripts/check-source-id-projection.sh",
|
||||
"check:privacy": "scripts/check-privacy.sh",
|
||||
"check:proposal-pii": "scripts/check-proposal-pii.sh",
|
||||
"check:eval-glossary": "scripts/check-eval-glossary-fresh.sh",
|
||||
"check:test-names": "scripts/check-test-real-names.sh",
|
||||
"check:progress": "scripts/check-progress-to-stdout.sh",
|
||||
"check:exports-count": "scripts/check-exports-count.sh",
|
||||
"check:admin-build": "scripts/check-admin-build.sh",
|
||||
"check:admin-embedded": "scripts/check-admin-embedded.sh",
|
||||
"check:test-isolation": "scripts/check-test-isolation.sh",
|
||||
"check:fuzz-purity": "scripts/check-fuzz-purity.sh",
|
||||
"check:operations-filter-bypass": "scripts/check-operations-filter-bypass.sh",
|
||||
"check:fixture-privacy": "scripts/check-fixture-privacy.sh",
|
||||
"check:conversation-parser": "bun src/cli.ts eval conversation-parser test/fixtures/conversation-formats/all.jsonl --no-llm",
|
||||
"check:source-scope-onboard": "bash scripts/check-source-scope-onboard.sh",
|
||||
"postinstall": "bun run scripts/postinstall.ts",
|
||||
"check:source-scope-onboard": "scripts/check-source-scope-onboard.sh",
|
||||
"postinstall": "command -v gbrain >/dev/null 2>&1 && gbrain apply-migrations --yes --non-interactive || echo '[gbrain] postinstall skipped. If installed via bun install -g github:...: run `gbrain doctor` and `gbrain apply-migrations --yes` manually. See https://github.com/garrytan/gbrain/issues/218' 1>&2",
|
||||
"prepublish:clawhub": "bun run build:all",
|
||||
"publish:clawhub": "clawhub package publish . --family bundle-plugin"
|
||||
},
|
||||
@@ -120,8 +116,8 @@
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"heic-decode": "^2.1.0",
|
||||
"js-yaml": "^3.15.0",
|
||||
"marked": "^18.0.2",
|
||||
"js-yaml": "^3.14.2",
|
||||
"marked": "^18.0.0",
|
||||
"openai": "^4.0.0",
|
||||
"pgvector": "^0.2.0",
|
||||
"postgres": "^3.4.0",
|
||||
@@ -146,17 +142,5 @@
|
||||
"bun": ">=1.3.10"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "0.42.67.0",
|
||||
"overrides": {
|
||||
"@hono/node-server": "^2.0.5",
|
||||
"fast-uri": "^3.1.4",
|
||||
"body-parser": "^2.3.0",
|
||||
"fast-xml-builder": "^1.1.7",
|
||||
"fast-xml-parser": "^5.7.0",
|
||||
"form-data": "^4.0.6",
|
||||
"hono": "^4.12.25",
|
||||
"ip-address": "^10.1.1",
|
||||
"qs": "^6.15.2",
|
||||
"js-yaml": "^3.15.0"
|
||||
}
|
||||
"version": "0.42.3.0"
|
||||
}
|
||||
|
||||
@@ -24,23 +24,10 @@
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join, resolve, sep } from 'node:path';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const MAX_CHARS = 2500;
|
||||
|
||||
// #1851: a topic id is the ONLY thing that crosses the wire from a call link
|
||||
// (never the topic content itself — that would be prompt injection + a leak via
|
||||
// URLs/logs). The id indexes `$BRAIN_ROOT/topics/<topicId>.md` server-side, so
|
||||
// it must be a strict slug: lowercase alnum + dashes, no dots/slashes. This
|
||||
// regex alone rejects `../../SOUL` (no dots, no slashes); the resolve-under-dir
|
||||
// check below is defense-in-depth.
|
||||
const TOPIC_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
/** True iff `topicId` is a safe slug (see TOPIC_ID_RE). */
|
||||
export function isValidTopicId(topicId) {
|
||||
return typeof topicId === 'string' && topicId.length <= 128 && TOPIC_ID_RE.test(topicId);
|
||||
}
|
||||
|
||||
// Emotion-word filter. Content-agnostic — catches what's loaded in the
|
||||
// operator's OWN words without hardcoding names of people in their life.
|
||||
// Add words to this list if your brain uses domain-specific vocabulary.
|
||||
@@ -165,50 +152,6 @@ export async function buildMarsContext({ brainRoot, timezone } = {}) {
|
||||
return cap(scrub(ctx));
|
||||
}
|
||||
|
||||
/**
|
||||
* #1851 — Build TOPIC context: the recent conversation in the topic the agent
|
||||
* was summoned into, so calling Mars/Venus from inside a thread boots them
|
||||
* already knowing what you were just discussing.
|
||||
*
|
||||
* The server resolves this from `topicId` at connect time (the id is the only
|
||||
* thing the call link carries). Reads `$BRAIN_ROOT/topics/<topicId>.md`. The
|
||||
* operator's brain owns what lands in that file (recent turns + a 2-3 line
|
||||
* synthesized summary is the intended shape — not a raw dump).
|
||||
*
|
||||
* Persona-agnostic: the SAME topic block is injected for Mars or Venus; only
|
||||
* the persona identity (section 1 of the prompt) differs. Returns '' when
|
||||
* there's no topic, the id is unsafe, or the file is missing — falling back to
|
||||
* the generic per-persona live context (current behavior).
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.brainRoot
|
||||
* @param {string} opts.topicId — strict slug; see {@link isValidTopicId}
|
||||
* @returns {Promise<string>} ≤2500 chars, PII-scrubbed, or '' to degrade.
|
||||
*/
|
||||
export async function buildTopicContext({ brainRoot, topicId } = {}) {
|
||||
if (!brainRoot || !topicId || !isValidTopicId(topicId)) return '';
|
||||
|
||||
// Defense-in-depth: confine the resolved path under <brainRoot>/topics even
|
||||
// though the slug regex already forbids traversal characters.
|
||||
const topicsDir = resolve(join(brainRoot, 'topics'));
|
||||
const path = resolve(join(topicsDir, `${topicId}.md`));
|
||||
if (path !== join(topicsDir, `${topicId}.md`) || !path.startsWith(topicsDir + sep)) {
|
||||
return '';
|
||||
}
|
||||
if (!existsSync(path)) return '';
|
||||
|
||||
try {
|
||||
const raw = readFileSync(path, 'utf8').trim();
|
||||
if (!raw) return '';
|
||||
let ctx = 'RECENT CONVERSATION IN THE TOPIC YOU WERE SUMMONED INTO.\n';
|
||||
ctx += "Use this so you already know what was just being discussed. Don't recite it; let it inform you.\n\n";
|
||||
ctx += raw;
|
||||
return cap(scrub(ctx));
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build logistics-salient context for Venus.
|
||||
*
|
||||
|
||||
@@ -50,32 +50,6 @@ export async function buildMarsContext(opts);
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function buildVenusContext(opts);
|
||||
|
||||
/**
|
||||
* #1851 — Build TOPIC context: the recent conversation in the topic the agent
|
||||
* was summoned into (persona-agnostic; the same block is used for Mars or
|
||||
* Venus). Lets a caller drop a persona into whatever thread they were already
|
||||
* discussing without re-explaining.
|
||||
*
|
||||
* The server resolves this from `topicId` at connect time. `topicId` is the
|
||||
* ONLY topic field accepted over the wire (a call link carries it). NEVER
|
||||
* accept topic CONTENT as a parameter — that's prompt injection + a leak into
|
||||
* URLs, browser history, referrers, and access logs.
|
||||
*
|
||||
* `topicId` MUST be a strict slug (^[a-z0-9][a-z0-9-]*$, ≤128 chars); the
|
||||
* shipped example reads `$BRAIN_ROOT/topics/<topicId>.md` and confines the
|
||||
* resolved path under `topics/` (defense-in-depth against traversal).
|
||||
*
|
||||
* Required: PII scrubbed. Required: ≤ 2500 chars. Returns '' when there is no
|
||||
* topic, the id is unsafe, or the file is missing → the persona falls back to
|
||||
* its generic live context (current behavior).
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.brainRoot
|
||||
* @param {string} opts.topicId — strict slug; indexes topics/<topicId>.md
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function buildTopicContext(opts);
|
||||
```
|
||||
|
||||
## Brain layout expected by the shipped example
|
||||
|
||||
@@ -25,17 +25,13 @@ import { VENUS } from './venus.mjs';
|
||||
|
||||
// ── Shared preamble (tools, rules, time) ─────────────────
|
||||
export function buildSharedContext(opts = {}) {
|
||||
const { authenticated = false, identity = '', dateTime = '', topicName = '' } = opts;
|
||||
const { authenticated = false, identity = '', dateTime = '' } = opts;
|
||||
|
||||
let ctx = '';
|
||||
if (dateTime) ctx += `CURRENT DATE/TIME: ${dateTime}\n\n`;
|
||||
if (authenticated && identity) {
|
||||
ctx += `The caller is verified as ${identity}. All allow-listed tools are available.\n\n`;
|
||||
}
|
||||
// #1851: when summoned from a specific topic, name it up top so the persona
|
||||
// knows the frame of the call. The recent-conversation detail is injected
|
||||
// separately as the `# Topic Context` block (see prompt.mjs).
|
||||
if (topicName) ctx += `CURRENT TOPIC: ${topicName}\n\n`;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import { getPersona, buildSharedContext } from './lib/personas/personas.mjs';
|
||||
import { getEffectiveAllowlist } from './tools.mjs';
|
||||
import { buildMarsContext, buildVenusContext, buildTopicContext } from './lib/context-builder.example.mjs';
|
||||
import { buildMarsContext, buildVenusContext } from './lib/context-builder.example.mjs';
|
||||
|
||||
/**
|
||||
* Build the system prompt for a session.
|
||||
@@ -33,11 +33,6 @@ import { buildMarsContext, buildVenusContext, buildTopicContext } from './lib/co
|
||||
* @param {string} [opts.dateTime] — ISO timestamp; defaults to now
|
||||
* @param {string} [opts.brainRoot] — absolute path to operator's brain repo
|
||||
* @param {string} [opts.timezone]
|
||||
* @param {string} [opts.topicId] — #1851: topic the agent was summoned into.
|
||||
* The ONLY topic field accepted over the wire; the server resolves the
|
||||
* recent-conversation context from the brain (never pass topic CONTENT in —
|
||||
* that's prompt injection + a URL/log leak).
|
||||
* @param {string} [opts.topicName] — human label for the topic (display only).
|
||||
* @returns {Promise<string>} sanitized system prompt
|
||||
*/
|
||||
export async function buildSystemPrompt(opts = {}) {
|
||||
@@ -48,13 +43,12 @@ export async function buildSystemPrompt(opts = {}) {
|
||||
let prompt = `# You ARE ${persona.name}\n`;
|
||||
prompt += `You are ${persona.name}, a voice AI. You are NOT a generic assistant. You are NOT Claude. You are NOT GPT. You are ${persona.name} with the personality below.\n\n`;
|
||||
|
||||
// 2. Shared context (date/time + identity if authed + topic name if summoned).
|
||||
// 2. Shared context (date/time + identity if authed).
|
||||
const dateTime = opts.dateTime || new Date().toISOString();
|
||||
prompt += buildSharedContext({
|
||||
authenticated: !!opts.authenticated,
|
||||
identity: opts.identity || '',
|
||||
dateTime,
|
||||
topicName: opts.topicName || '',
|
||||
});
|
||||
|
||||
// 3. Persona body.
|
||||
@@ -75,20 +69,6 @@ export async function buildSystemPrompt(opts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// 4b. #1851 Topic context — the recent conversation in the topic the agent
|
||||
// was summoned into. Resolved server-side from topicId (the only topic field
|
||||
// that crosses the wire). Injected AFTER the persona body + live context so
|
||||
// the identity-first ordering still wins; the topic only adds background.
|
||||
// No topicId → omitted → generic behavior (acceptance criterion).
|
||||
if (opts.brainRoot && opts.topicId) {
|
||||
try {
|
||||
const tctx = await buildTopicContext({ brainRoot: opts.brainRoot, topicId: opts.topicId });
|
||||
if (tctx) prompt += `# Topic Context\n${tctx}\n\n`;
|
||||
} catch (err) {
|
||||
console.warn(`[prompt] topic-context builder threw: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Tool list — only the allow-list, never the denylist.
|
||||
const allowed = getEffectiveAllowlist();
|
||||
if (allowed.length > 0) {
|
||||
|
||||
@@ -94,11 +94,6 @@
|
||||
const params = new URLSearchParams(location.search);
|
||||
const persona = (params.get('persona') || 'venus').toLowerCase();
|
||||
const TEST_MODE = params.get('test') === '1';
|
||||
// #1851: a per-topic call link carries topicId (+ optional topicName). We
|
||||
// forward ONLY these to /session — the server resolves the topic's recent
|
||||
// conversation from the brain. Topic content never travels in a URL.
|
||||
const topicId = params.get('topicId') || '';
|
||||
const topicName = params.get('topicName') || '';
|
||||
|
||||
document.getElementById('personaBadge').textContent = `persona: ${persona}`;
|
||||
if (TEST_MODE) document.getElementById('testBadge').style.display = '';
|
||||
@@ -237,9 +232,7 @@
|
||||
await pc.setLocalDescription(offer);
|
||||
|
||||
setStatus('sending SDP offer to /session...');
|
||||
let sessionUrl = `/session?persona=${encodeURIComponent(persona)}`;
|
||||
if (topicId) sessionUrl += `&topicId=${encodeURIComponent(topicId)}`;
|
||||
if (topicName) sessionUrl += `&topicName=${encodeURIComponent(topicName)}`;
|
||||
const sessionUrl = `/session?persona=${encodeURIComponent(persona)}`;
|
||||
const res = await fetch(sessionUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/sdp' },
|
||||
|
||||
@@ -124,21 +124,12 @@ async function handleSession(req, res) {
|
||||
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const persona = (url.searchParams.get('persona') || DEFAULT_PERSONA).toLowerCase();
|
||||
// #1851: a call link minted from a Telegram topic carries topicId (+ an
|
||||
// optional display topicName). The id is the ONLY topic data we accept over
|
||||
// the wire — buildSystemPrompt resolves the recent-conversation context from
|
||||
// the brain server-side. We never accept topic CONTENT as a param (that would
|
||||
// be prompt injection + a leak into URLs/referrers/access logs).
|
||||
const topicId = url.searchParams.get('topicId') || undefined;
|
||||
const topicName = url.searchParams.get('topicName') || undefined;
|
||||
|
||||
// Build the persona-aware system prompt at session start.
|
||||
const systemPrompt = await buildSystemPrompt({
|
||||
persona,
|
||||
brainRoot: process.env.BRAIN_ROOT,
|
||||
timezone: process.env.TIMEZONE,
|
||||
topicId,
|
||||
topicName,
|
||||
});
|
||||
|
||||
// Session config for OpenAI Realtime /v1/realtime/calls.
|
||||
|
||||
@@ -32,16 +32,6 @@ The depth of the conversation is the signal. If it's surface-level scheduling, r
|
||||
|
||||
This skill is invoked by the host agent's resolver when the operator's voice or text input matches the triggers above. The voice agent (`services/voice-agent/code/server.mjs`) consumes the persona key (`mars`) at session start via `?persona=mars` on the WebRTC `/session` endpoint, OR via the `DEFAULT_PERSONA=mars` env var if Mars is the operator's default.
|
||||
|
||||
### Summoning Mars into a topic (#1851)
|
||||
|
||||
To call Mars *from inside* a specific conversation topic, mint a per-topic call link by adding `topicId` (a strict slug, `^[a-z0-9][a-z0-9-]*$`) and an optional `topicName`:
|
||||
|
||||
```
|
||||
/call?persona=mars&topicId=real-estate&topicName=Real%20Estate
|
||||
```
|
||||
|
||||
Mars boots already knowing the topic's recent conversation. Only the `topicId` crosses the wire — the server resolves the recent-conversation context from `$BRAIN_ROOT/topics/<topicId>.md`. **Never put topic content in the URL** (prompt injection + a leak into history/referrers/logs). No `topicId` → Mars uses his generic live context (unchanged behavior).
|
||||
|
||||
## Mode detection (inside the persona)
|
||||
|
||||
Mars detects mode from conversational signals:
|
||||
|
||||
@@ -33,16 +33,6 @@ If a question requires multi-paragraph thinking, Venus tees it up briefly and ro
|
||||
|
||||
This skill is invoked by the host agent's resolver when the operator's voice or text input matches the triggers above. The voice agent (`services/voice-agent/code/server.mjs`) reads the persona key (`venus`) at session start via `?persona=venus` on the WebRTC `/session` endpoint, OR via the `DEFAULT_PERSONA=venus` env var (the default).
|
||||
|
||||
### Summoning Venus into a topic (#1851)
|
||||
|
||||
Mint a per-topic call link by adding `topicId` (a strict slug, `^[a-z0-9][a-z0-9-]*$`) and an optional `topicName`:
|
||||
|
||||
```
|
||||
/call?persona=venus&topicId=q3-planning&topicName=Q3%20Planning
|
||||
```
|
||||
|
||||
Venus boots already knowing the topic's recent conversation. Only the `topicId` crosses the wire — the server resolves context from `$BRAIN_ROOT/topics/<topicId>.md`. **Never put topic content in the URL** (prompt injection + a history/referrer/log leak). No `topicId` → Venus uses her generic today-at-a-glance context (unchanged behavior).
|
||||
|
||||
## Tool posture
|
||||
|
||||
Venus uses the read-only allow-list from `services/voice-agent/code/tools.mjs`:
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
* topic-context.test.mjs — #1851 topic-aware voice personas.
|
||||
*
|
||||
* Pins the security + behavior contract for summoning Mars/Venus into a topic:
|
||||
* - topicId path-traversal is rejected (only the brain-owned topics/<id>.md)
|
||||
* - the topic block is injected when a topic is provided
|
||||
* - no topic → generic behavior (no topic block), persona identity unchanged
|
||||
* - topic X vs topic Y produce different context
|
||||
* - the topic block can NOT override persona identity / hard rules
|
||||
* - PII in a topic file is scrubbed
|
||||
* - topic CONTENT is never accepted over the wire (only topicId)
|
||||
*/
|
||||
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { buildTopicContext, isValidTopicId } from '../../code/lib/context-builder.example.mjs';
|
||||
import { buildSystemPrompt } from '../../code/prompt.mjs';
|
||||
|
||||
let brainRoot;
|
||||
|
||||
// Build PII-shaped strings at runtime so the literal phone/email shapes never
|
||||
// appear in this source file (the agent-voice PII guard greps the recipe tree
|
||||
// for those shapes). The runtime values still exercise the scrubber.
|
||||
const FAKE_PHONE = ['415', '555', '0100'].join('-');
|
||||
const FAKE_EMAIL = ['someone', 'example.test'].join('@');
|
||||
|
||||
beforeEach(() => {
|
||||
brainRoot = mkdtempSync(join(tmpdir(), 'agent-voice-topic-'));
|
||||
mkdirSync(join(brainRoot, 'topics'), { recursive: true });
|
||||
writeFileSync(join(brainRoot, 'topics', 'real-estate.md'), 'We were discussing the warehouse-lease offer and the inspection timeline.');
|
||||
writeFileSync(join(brainRoot, 'topics', 'yc-batch.md'), 'Talking through the W26 batch interview schedule.');
|
||||
// A file with PII to verify scrubbing (shapes built at runtime, see above).
|
||||
writeFileSync(join(brainRoot, 'topics', 'with-pii.md'), `Call me at ${FAKE_PHONE} or ${FAKE_EMAIL} about the deal.`);
|
||||
// A secret OUTSIDE the topics dir that traversal must not reach.
|
||||
writeFileSync(join(brainRoot, 'SOUL.md'), 'TOP SECRET SOUL CONTENT');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { rmSync(brainRoot, { recursive: true, force: true }); } catch { /* noop */ }
|
||||
});
|
||||
|
||||
describe('isValidTopicId', () => {
|
||||
it('accepts strict slugs', () => {
|
||||
expect(isValidTopicId('real-estate')).toBe(true);
|
||||
expect(isValidTopicId('yc-batch-2026')).toBe(true);
|
||||
});
|
||||
it('rejects traversal and unsafe ids', () => {
|
||||
expect(isValidTopicId('../../SOUL')).toBe(false);
|
||||
expect(isValidTopicId('foo/bar')).toBe(false);
|
||||
expect(isValidTopicId('foo.md')).toBe(false);
|
||||
expect(isValidTopicId('UPPER')).toBe(false);
|
||||
expect(isValidTopicId('')).toBe(false);
|
||||
expect(isValidTopicId(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTopicContext', () => {
|
||||
it('returns the topic conversation for a valid id', async () => {
|
||||
const ctx = await buildTopicContext({ brainRoot, topicId: 'real-estate' });
|
||||
expect(ctx).toContain('warehouse-lease');
|
||||
});
|
||||
|
||||
it('topic X and topic Y differ', async () => {
|
||||
const x = await buildTopicContext({ brainRoot, topicId: 'real-estate' });
|
||||
const y = await buildTopicContext({ brainRoot, topicId: 'yc-batch' });
|
||||
expect(x).toContain('warehouse-lease');
|
||||
expect(y).toContain('W26 batch');
|
||||
expect(x).not.toEqual(y);
|
||||
});
|
||||
|
||||
it('rejects path traversal — cannot read SOUL.md outside topics/', async () => {
|
||||
const ctx = await buildTopicContext({ brainRoot, topicId: '../../SOUL' });
|
||||
expect(ctx).toBe('');
|
||||
expect(ctx).not.toContain('TOP SECRET');
|
||||
});
|
||||
|
||||
it('scrubs PII in the topic file', async () => {
|
||||
const ctx = await buildTopicContext({ brainRoot, topicId: 'with-pii' });
|
||||
expect(ctx).not.toContain(FAKE_PHONE);
|
||||
expect(ctx).not.toContain(FAKE_EMAIL);
|
||||
});
|
||||
|
||||
it('missing topic file → empty (generic fallback)', async () => {
|
||||
expect(await buildTopicContext({ brainRoot, topicId: 'does-not-exist' })).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildSystemPrompt topic-awareness', () => {
|
||||
it('injects a # Topic Context block when topicId is provided', async () => {
|
||||
const prompt = await buildSystemPrompt({ persona: 'mars', brainRoot, topicId: 'real-estate', topicName: 'Real Estate' });
|
||||
expect(prompt).toContain('# Topic Context');
|
||||
expect(prompt).toContain('warehouse-lease');
|
||||
expect(prompt).toContain('CURRENT TOPIC: Real Estate');
|
||||
});
|
||||
|
||||
it('no topicId → no topic block (generic behavior unchanged)', async () => {
|
||||
const prompt = await buildSystemPrompt({ persona: 'mars', brainRoot });
|
||||
expect(prompt).not.toContain('# Topic Context');
|
||||
expect(prompt).not.toContain('CURRENT TOPIC:');
|
||||
});
|
||||
|
||||
it('persona identity stays first; topic context cannot override it', async () => {
|
||||
const prompt = await buildSystemPrompt({ persona: 'mars', brainRoot, topicId: 'real-estate', topicName: 'Real Estate' });
|
||||
// Identity-first: the "You ARE Mars" line precedes the topic block.
|
||||
expect(prompt.indexOf('# You ARE Mars')).toBeLessThan(prompt.indexOf('# Topic Context'));
|
||||
// Hard rules survive after the topic block.
|
||||
expect(prompt).toContain('# Hard Rules');
|
||||
expect(prompt.indexOf('# Topic Context')).toBeLessThan(prompt.indexOf('# Hard Rules'));
|
||||
});
|
||||
|
||||
it('a traversal topicId yields the generic prompt (no block, no leak)', async () => {
|
||||
const prompt = await buildSystemPrompt({ persona: 'venus', brainRoot, topicId: '../../SOUL' });
|
||||
expect(prompt).not.toContain('# Topic Context');
|
||||
expect(prompt).not.toContain('TOP SECRET');
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: retrieval-reflex
|
||||
name: Retrieval Reflex
|
||||
version: 0.1.0
|
||||
description: Teaches the host agent WHEN to look something up and WHAT to pull. Ships a policy skill (trigger + retrieval spec) into the host resolver; pairs with the deterministic pointer layer in the context engine.
|
||||
category: reflex
|
||||
install_kind: copy-into-host-repo
|
||||
requires: []
|
||||
secrets: []
|
||||
health_checks:
|
||||
- type: command
|
||||
argv: [gbrain, doctor, --json]
|
||||
label: Retrieval reflex wiring (see retrieval_reflex_health)
|
||||
setup_time: 2 min
|
||||
cost_estimate: "$0 — zero-LLM deterministic layer + a prose policy skill"
|
||||
---
|
||||
|
||||
# Retrieval Reflex: teach the agent *when* and *what* to retrieve
|
||||
|
||||
gbrain is great at **storing** knowledge and at **injecting deterministic
|
||||
context** every turn. It does not, by itself, teach the host agent the *policy*
|
||||
of retrieval: **when** to look something up and **what** to pull. Without it,
|
||||
the agent can discuss a person who has a rich brain page for several messages
|
||||
without ever opening it — then answer generically about facts the brain already
|
||||
knew.
|
||||
|
||||
This reflex has two halves:
|
||||
|
||||
1. **Deterministic pointer layer (automatic, on by default).** The
|
||||
`gbrain-context` engine scans each turn's user message for salient,
|
||||
resolvable entities and injects a compact pointer (name → slug → one-line
|
||||
summary) so the agent *knows the page exists*. Zero-LLM, fail-open. Nothing
|
||||
to install — it's on unless `retrieval_reflex` is disabled in
|
||||
`~/.gbrain/config.json` or `GBRAIN_RETRIEVAL_REFLEX=false`.
|
||||
|
||||
2. **Policy skill (this recipe installs it).** A SKILL fragment in the host
|
||||
resolver that encodes the trigger policy and retrieval spec the agent
|
||||
follows when a pointer appears or an entity becomes the subject.
|
||||
|
||||
## IMPORTANT: Instructions for the Agent
|
||||
|
||||
**You are the installer.** Run these steps on behalf of the user.
|
||||
|
||||
1. Confirm the deterministic layer isn't disabled:
|
||||
`gbrain doctor --json | jq '.checks[] | select(.name=="retrieval_reflex_health")'`
|
||||
2. Install the policy skill into the host repo (the OpenClaw/agent repo that
|
||||
holds `skills/RESOLVER.md` or `AGENTS.md`):
|
||||
`gbrain integrations install retrieval-reflex --target <host-repo>`
|
||||
3. Verify: re-run `gbrain doctor` and confirm `retrieval_reflex_health` is `ok`.
|
||||
|
||||
The deterministic layer needs no install. On a PGLite brain it resolves through
|
||||
the running `gbrain serve` (or a host-provided capability); if neither is
|
||||
available it stays disabled and this policy skill carries the behavior — the
|
||||
doctor check reports which.
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"recipe": "retrieval-reflex",
|
||||
"version": "0.1.0",
|
||||
"install_kind": "copy-into-host-repo",
|
||||
"description": "src → target mapping consumed by `gbrain integrations install retrieval-reflex`. Policy-only recipe: ships one SKILL.md into the host resolver and appends a resolver row. The deterministic pointer layer lives in the gbrain context engine and needs no install.",
|
||||
"target_root_relative_to_host_repo": "skills/retrieval-reflex",
|
||||
"skills_target_root_relative_to_host_repo": "skills",
|
||||
"files": [],
|
||||
"skills": [
|
||||
{ "src": "skills/retrieval-reflex/SKILL.md", "target": "skills/retrieval-reflex/SKILL.md", "mode": "0644" }
|
||||
],
|
||||
"resolver_rows_to_append": [
|
||||
"retrieval-reflex | a named person/company/project/place becomes the subject; a brain-page pointer appears in context; \"who is\", \"what do we know about\", \"tell me about\"; about to assert a non-trivial detail about a named entity"
|
||||
]
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
name: retrieval-reflex
|
||||
version: 0.1.0
|
||||
description: When/what to retrieve — open the brain page for a salient entity before answering from memory.
|
||||
triggers:
|
||||
- "who is"
|
||||
- "what do we know about"
|
||||
- "tell me about"
|
||||
mutating: false
|
||||
writes_pages: false
|
||||
writes_to: []
|
||||
tools: [get_page, query, graph, backlinks]
|
||||
---
|
||||
|
||||
# Retrieval Reflex — retrieve on demand, when an entity is salient
|
||||
|
||||
A person doesn't bulk-load their whole address book into working memory. They
|
||||
retrieve **on demand**, when an entity becomes **salient**, use it, and drop it.
|
||||
Encode that reflex. The brain probably has the data — if a name is salient and
|
||||
you haven't opened its page, open it before you answer.
|
||||
|
||||
## Trigger policy — WHEN to retrieve
|
||||
|
||||
Retrieve when ANY of these holds AND the page isn't already loaded in context:
|
||||
|
||||
- An entity (person / company / project / deal / place) is the **subject** of
|
||||
the message, or a decision/judgment about it is being made, or the exchange is
|
||||
substantive / relational / emotional about it.
|
||||
- A **brain-page pointer** appeared in context this turn (the deterministic
|
||||
layer told you the page exists) — open it before relying on details.
|
||||
- A name or term appears that you **don't recognize** and that looks notable →
|
||||
do a quick resolve (the human reflex).
|
||||
- You're about to **assert a non-trivial detail** about an entity (attribution,
|
||||
status, history) → verify against the brain first. Say "let me check", not a guess.
|
||||
|
||||
**Skip** trivial passing mentions, logistics pings, and anything already loaded.
|
||||
Judgment first — retrieve when it changes the quality of the reply, not reflexively.
|
||||
|
||||
## Retrieval spec — WHAT to pull, and when to stop
|
||||
|
||||
Escalate only as far as the task needs:
|
||||
|
||||
1. **Pointer / metadata.** If a pointer is already in context (slug + one-line
|
||||
summary), and the task only needs identity, stop there.
|
||||
2. **Full page.** When the entity is the subject or details matter, open it:
|
||||
`get_page <slug>` (MCP) — read the page before relying on specifics.
|
||||
3. **Linked neighbors.** Only when relationship context is needed, pull
|
||||
`graph` / `backlinks` for the slug.
|
||||
|
||||
**Resolve only the name(s) the current task needs, use them, drop them.** No
|
||||
bulk-loading the inner circle.
|
||||
|
||||
## The failure this prevents
|
||||
|
||||
If you've discussed a named person for more than a message without opening their
|
||||
page, open it now. The write side captures everything; the read side only helps
|
||||
if you actually look.
|
||||
|
||||
See also: `skills/query/SKILL.md` (search the brain), `skills/brain-ops/SKILL.md`.
|
||||
+8
-13
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: x-to-brain
|
||||
name: X-to-Brain
|
||||
version: 0.8.2
|
||||
version: 0.8.1
|
||||
description: Twitter timeline, mentions, and keyword monitoring flow into brain pages. Tracks deletions, engagement velocity, OCR on images, and real-time alerts.
|
||||
category: sense
|
||||
requires: []
|
||||
@@ -9,12 +9,9 @@ secrets:
|
||||
- name: X_BEARER_TOKEN
|
||||
description: X API v2 Bearer token (Basic tier minimum, $200/mo for full archive search)
|
||||
where: https://developer.x.com/en/portal/dashboard — create a project + app, copy the Bearer Token from "Keys and tokens"
|
||||
- name: X_HANDLE
|
||||
description: Your X username without the @ (used for the app-only health check — /users/me requires user-context OAuth, which app-only bearer tokens don't have)
|
||||
where: Your X profile — the handle in your profile URL, e.g. x.com/yourhandle → yourhandle
|
||||
health_checks:
|
||||
- type: http
|
||||
url: "https://api.x.com/2/users/by/username/$X_HANDLE"
|
||||
url: "https://api.x.com/2/users/me"
|
||||
auth: bearer
|
||||
auth_token: "$X_BEARER_TOKEN"
|
||||
label: "X API"
|
||||
@@ -113,17 +110,15 @@ Tell the user:
|
||||
4. Inside the project, create a new App
|
||||
5. Go to the app's 'Keys and tokens' tab
|
||||
6. Under 'Bearer Token', click 'Generate' (or 'Regenerate')
|
||||
7. Copy the Bearer Token and paste it to me, along with your X handle (without the @)
|
||||
7. Copy the Bearer Token and paste it to me
|
||||
|
||||
Note: Free tier gives read-only access with low limits. Basic tier ($200/mo)
|
||||
gives search/recent endpoint and higher limits. Pro tier gets full archive search."
|
||||
|
||||
Set both `X_BEARER_TOKEN` and `X_HANDLE` in the environment. Validate immediately
|
||||
(app-only bearer tokens cannot call `/users/me` — that endpoint requires
|
||||
user-context OAuth — so validation uses the by-username lookup):
|
||||
Validate immediately:
|
||||
```bash
|
||||
curl -sf -H "Authorization: Bearer $X_BEARER_TOKEN" \
|
||||
"https://api.x.com/2/users/by/username/$X_HANDLE" \
|
||||
"https://api.x.com/2/users/me" \
|
||||
&& echo "PASS: X API connected" \
|
||||
|| echo "FAIL: X API token invalid"
|
||||
```
|
||||
@@ -139,10 +134,10 @@ starting with 'AAA...', (3) if you just created the app, the token is valid imme
|
||||
```bash
|
||||
# Look up the user's X user ID from their handle
|
||||
curl -sf -H "Authorization: Bearer $X_BEARER_TOKEN" \
|
||||
"https://api.x.com/2/users/by/username/$X_HANDLE" | grep -o '"id":"[^"]*"'
|
||||
"https://api.x.com/2/users/by/username/USERNAME" | grep -o '"id":"[^"]*"'
|
||||
```
|
||||
|
||||
Look up the user ID from the handle collected in Step 1.
|
||||
Ask the user for their X handle (e.g., @yourhandle). Look up their user ID.
|
||||
Save it — the collector needs the numeric ID, not the handle.
|
||||
|
||||
### Step 3: Configure the Collector
|
||||
@@ -210,7 +205,7 @@ The agent should review collected data 2-3x daily and run enrichment.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gbrain/integrations/x-to-brain
|
||||
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.8.2","status":"ok","details":{"user_id":"X_USER_ID"}}' >> ~/.gbrain/integrations/x-to-brain/heartbeat.jsonl
|
||||
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.8.1","status":"ok","details":{"user_id":"X_USER_ID"}}' >> ~/.gbrain/integrations/x-to-brain/heartbeat.jsonl
|
||||
```
|
||||
|
||||
## Production Patterns (v0.8.1)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user