The #2339 parity guard's beforeAll runs setupDB (full migration chain)
under bun's default 5s hook timeout, which flaked on a slow CI runner
(setupDB hit 5001ms). Other e2e suites already pass explicit hook
timeouts; bring this file in line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On machines with neither gtimeout nor timeout on PATH, run-verify-parallel.sh
and run-unit-parallel.sh fall back to a bg-pid + sleep-watchdog cap. Both
read $? only after tearing the watchdog down (kill + wait on cap_pid), so the
sentinel .exit files recorded the killed watchdog's status — 143 — instead of
the check/shard's own exit code. Every run reported total failure (verify:
pass=0 fail=31; unit: rc=143 per shard) while every per-check/shard log
showed success.
Capture rc immediately after `wait $pid` in both scripts, and reap the
watchdog's sleep child (pkill -P, children-first — the same orphan quirk the
heartbeat cleanup documents) so the fallback stops leaking one sleep per
check/shard.
Regression tests force the fallback branch hermetically on any host via a
curated PATH with no timeout binaries: the verify dispatcher runs from a
tempdir copy with a stubbed `bun`, pinning exit 0 + all-zero sentinels when
checks pass and the check's own rc (not 143) when one fails; the unit wrapper
runs real two-shard fixture passes, pinning rc=0 sentinels and a real
failure's rc=1.
Co-authored-by: paul-0320 <paul@ymyd.co.kr>
Co-authored-by: YMYD <53603073+OJ-OnJourney@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(embed): stamp gateway-resolved model in content_chunks.model, not compiled default (#2846)
upsertChunks fell back to the compile-time DEFAULT_EMBEDDING_MODEL
('zeroentropyai:zembed-1') when a ChunkInput carried no explicit `model`.
The embed pipeline (src/commands/embed.ts) builds ChunkInputs without a
`model` field, so rows whose vectors were produced by the config-resolved
model (e.g. openai:text-embedding-3-large) were mislabeled with the
hardcoded default — corrupting the provenance that signature-drift
staleness and dimension-migration logic depend on.
Both engines now resolve the gateway's runtime embedding model once per
upsert and use it as the fallback, mirroring the existing resolve-then-
default pattern used for schema sizing. Regression test added (pglite);
verified via negative control that it fails against the old fallback.
This is a write-path change (upsertChunks), not a search-path change, so
retrieval eval replay is not applicable.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* test: Lane A.7 pins gateway-resolved chunk model, not compiled default
#2846 changed upsertChunks' fallback from DEFAULT_EMBEDDING_MODEL to the
gateway-resolved runtime model. Lane A.7 still pinned the old fallback,
and the test preload (test/helpers/legacy-embedding-preload.ts) pins the
gateway to openai:text-embedding-3-large for every test process — so the
original #2846 landing failed this test deterministically and got batch-
reverted. The test now asserts the resolved model (the intended #2846
semantics) while keeping the CDX2-4 bare-literal regression guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: SailorJoe6 <SailorJoe6@Gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Garry Tan <garrytan@gmail.com>
DashScope's OpenAI-compatible rerank endpoint lives at
{base}/compatible-api/v1/reranks — PLURAL leaf, different base path from
the embedding surface (compatible-mode). Reusing llama-server-reranker
against DashScope forces users to hand-patch the recipe's '/rerank' leaf
in node_modules, which every upgrade silently reverts (and llama.cpp
genuinely serves singular /rerank, so changing that recipe would break
real llama.cpp users).
New dedicated recipe rides the v0.40.6.1 recipe-pluggable reranker path:
- id dashscope-rerank, base_url_default compatible-api/v1 (intl), ZE wire
- path '/reranks', default_timeout_ms 30s, 5MB payload ceiling
- models: only qwen3-rerank (live-verified 200; gte-rerank-v2 is rejected
by the compat surface with 'Unsupported model for OpenAI compatibility
mode', so it is deliberately not listed)
- separate recipe (not a reranker touchpoint on dashscope) because
provider_base_urls is keyed by recipe id and the two capabilities need
different prefixes — same topology as llama-server vs
llama-server-reranker
Tests: recipe shape smoke mirroring recipe-llama-server-reranker.test.ts
(path/timeout/payload pins, /v1/v1 concat guard, auth resolve, sibling
recipe isolation). bun test test/ai/: 322 pass / 0 fail.
Co-authored-by: Yicon <charlieyiconghuang@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The four `hybridSearch — reranker enabled (reorder)` cases stub the gateway
at 1536 dims (DIMS). Since v0.36.3.0 hybridSearch resolves the embedding
column via loadConfig(), whose precedence is
cfg.embedding_dimensions > gateway dims > default. On any machine whose
~/.gbrain/config.json sets embedding_dimensions to something other than 1536
(e.g. text-embedding-3-small at 1280), the real config outranks the stub: the
1536-d stub vector fails the gateway dim check, the error is swallowed, search
falls back to keyword-only, and the reranker never runs (rerankerFn gets 0
docs, rerank_score undefined). Green in CI only because a fresh runner has no
config file — deterministic red on a contributor's machine.
Fix (test-only): isolate GBRAIN_HOME to an empty tmpdir in beforeAll so
loadConfig() returns null and the stub's dims win, then restore it and clean
up in afterAll. Same idiom as emptyHome() in
test/ai/gateway-probe-chat-model.test.ts.
Verified with a planted ~/.gbrain/config.json at 1280 dims: 2 pass / 4 fail
before, 6 pass / 0 fail after; still green with no config file. typecheck clean.
Fixes#1527
Co-authored-by: Willisbest <132954469+Willisbest@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Reland of #2639, reverted with its batch in 68e4cebd. getHealth's
entity_pages CTE and the top-linked-pages query only match the legacy
'person' and 'company' types, so brains using the gbrain-base-v2 pack's
'entity' type report 0% entity link/timeline coverage in `gbrain health`.
Add 'entity' to both queries in both engines (PGLite + Postgres, in
lockstep per the engine-parity rule).
Reland fix (the batch-red root cause): the original PR's test expected
the entities/project-x page to appear in orphan_pages, but #3023's shared
orphan-reporting policy (landed before #2639 merged) excludes the
'entities' first segment from orphan reporting, so the test failed on
master. Orphan expectations now account for the policy exclusion.
Co-authored-by: Tyler Robinson <tylr.rob@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Replace the strictly sequential per-chunk synopsis loop with a bounded
sliding worker pool (existing runSlidingPool helper). Results land in
chunk order via index-addressed writes; code chunks still bypass the
wrapper; embedding remains one page-level batch after all synopses.
New knob GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY, default 4, clamped to
[1,16]; 1 reproduces the prior sequential behavior exactly. Each chunk
task still acquires/releases the global synopsis rate-lease, which
remains the cross-worker governor; the lease id now travels from
acquire to release instead of shared mutable state, and lease waits
are abort-responsive.
At 20-45s per synopsis call, a 120-chunk transcript page previously
needed 60-90+ min wall time and routinely outlived job timeouts.
Co-authored-by: spiky02plateau <155588579+spiky02plateau@users.noreply.github.com>
statusForVersion short-circuited on any 'complete' entry before checking
the trailing 'retry' marker, so --force-retry appended an inert row and a
version marked complete with zero work done could never be re-run without
hand-editing completed.jsonl. Check retry-latest first: an explicit
--force-retry now yields 'pending' even past an earlier 'complete', while
a stray 'partial' after 'complete' still cannot regress the version.
Co-authored-by: spiky02plateau <155588579+spiky02plateau@users.noreply.github.com>
SupabaseStorage.getSignedUrl built the download URL as `${projectUrl}${signedURL}`,
but Supabase's sign API returns `signedURL` relative to the Storage API root
(/object/sign/<bucket>/<path>?token=...), so the generated link dropped /storage/v1
and returned 404. Now prepends `${projectUrl}/storage/v1`, tolerating an
already-absolute URL or a value that already carries the prefix. `gbrain files
signed-url` links resolve again.
Co-authored-by: FloridaStyle <daniel.wiggins@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Reasoning models (MiniMax-M3, DeepSeek-R1, etc.) return <think>...</think>
tags in the content field before the actual JSON output. This caused
parseExtractorOutput to fail in two ways:
1. The fence regex /^\`\`\`(json)?...$/ requires the fence at text start;
<think> preceding it prevents matching, so the raw text (with trailing
fences) hits JSON.parse and throws.
2. When think tags contain [ or { characters, indexOf finds them inside
the reasoning block instead of the actual JSON array.
Changes:
- Strip <think>...</think> tags before any parsing (covers all reasoning models)
- Add JSON.parse fallback: truncate at last ] or } to handle trailing
noise (leftover markdown fences after stripping)
Tests: 28/28 pass (3 new cases for think tags + trailing noise).
Co-authored-by: qaz8545355 <603191978@qq.com>
Co-authored-by: qaz8545355 <junjun@openclaw.local>
The empty-fence guard counted every `row_num IS NULL AND entity_slug IS NOT NULL`
row as a pending v0_32_2 backfill, but the inline facts writer keeps producing
rows of exactly that shape post-migration: when a resolved slug has no fenceable
page (slugify-floor / stub-guard-blocked unprefixed slugs like `wingman`,
`people-jane-doe`), backstop.ts falls through to a DB-only insert with row_num
NULL. Those rows are structurally unfenceable — no page to fence onto, and the
ledger-complete migration won't re-run — so they jammed the phase forever
(~16/day observed) and the warning advised a no-op `apply-migrations --yes`.
Discriminator: a row is a genuine backfill candidate only if its entity_slug
resolves to a LIVE page in the same source (EXISTS in `pages` with deleted_at
NULL) — mirroring the migration's Phase B, which only fences slugs that map to
a writable page. Genuine pre-v0.32.2 rows (their entity page exists) still gate;
inline-writer unfenceable rows no longer do. Warning text updated to name the
"entity page present, not yet fenced" condition.
Regression tests pin both sides: unfenceable rows (no page / soft-deleted page)
do NOT gate and the phase converges; a legacy row WITH a backing page still
gates. Fails pre-fix, passes post-fix.
(#2484)
Reland note: original merge (53c90869) was batch-reverted (4b6cf32c) —
the guard-semantics change broke test/phantom-redirect.test.ts
'round 2 P1: legacy-row guard fires BEFORE phantom-redirect pass',
which seeded a legacy row WITHOUT a backing page and expected the
guard to fire. Under the new (intended) semantics such a row is
structurally unfenceable and must NOT gate. Fixed by seeding a live
backing page for the legacy row, preserving what the test pins
(guard fires before the phantom-redirect pass).
Co-authored-by: Javier Aldape <javieraldape@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Code legitimately contains tiktoken special-token strings (e.g. CLIP/GPT tokenizers embed the literal <|endoftext|>). The default encode() uses disallowed_special='all' and THROWS on those, crashing reindex-code on valid source files. Re-encode treating them as ordinary text (allowed=[], disallowed=[]); heuristic fallback if even that fails. A token COUNT needs no special-token semantics.
Co-authored-by: Jim Tang <jimruitang@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(write-through): guard mkdir against EEXIST on Bun+Windows
* fix(budget): resolve non-Anthropic model pricing via canonical table under --max-cost
* fix(enrich): exclude dream-generated pages from thin candidates
---------
Co-authored-by: nguyenchiviet <40517873+nguyenchiviet@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
OpenRouter (and potentially other proxy providers) expose OpenAI's
text-embedding-3 models with a provider prefix in the model ID, e.g.
`openai/text-embedding-3-large` rather than bare `text-embedding-3-large`.
`dimsProviderOptions()` checks `modelId.startsWith('text-embedding-3')`
which fails for the prefixed form, so the `dimensions` parameter is never
sent. The upstream provider returns its native dimensionality (3072 for
-large) instead of the configured value (e.g. 1536), causing an immediate
"dim mismatch" error on first embed.
The default OpenRouter embedding (`text-embedding-3-small` at 1536d)
masked this because its native size happens to match the default config.
The bug surfaces when using `-large`, or `-small` with a non-1536 dim
(512, 768, 1024 — all listed in the recipe's `dims_options`).
Fix: strip the provider prefix before the `startsWith` check. The full
prefixed ID is preserved in the error message for user clarity.
Co-authored-by: Noetherly <280958447+noetherly@users.noreply.github.com>
* feat(subagent): claude-cli MessagesClient adapter (baseline, no tool use)
Closes#334 (partially — text-only baseline; tool use lands in the next
commit on this branch).
Adds a MessagesClient adapter that shells out to `claude --print
--output-format json --model <model>` instead of the Anthropic SDK. When
`GBRAIN_USE_CLAUDE_CLI=1` is set, the subagent worker registers the adapter
in place of the SDK client; the default path (Anthropic SDK with
ANTHROPIC_API_KEY) is unchanged when the env var is unset or set to
anything else.
The benefit is that Claude Max subscribers can run Minions subagents
against their existing OAuth subscription, no ANTHROPIC_API_KEY needed.
New: src/core/minions/handlers/claude-cli-adapter.ts
- Implements the MessagesClient interface exported from subagent.ts.
- Strips provider prefixes (`anthropic:`, `litellm:`) from the model id
because `claude --print` only accepts CLI-native aliases (`sonnet`,
`opus`, `haiku`, or the bare `claude-*-N-M` form).
- Flattens the Anthropic messages array into a single text prompt for
claude-cli stdin. Tool blocks (tool_use / tool_result) are stringified
as placeholders so multi-turn conversations stay coherent in this
baseline; native tool_use round-tripping is the follow-up commit.
- Spawns claude with stdio piped, captures stdout, parses the
`{type:"result", subtype:"success", result, usage, ...}` JSON envelope,
and returns it as a properly shaped Anthropic.Message with
`stop_reason: 'end_turn'`.
- Token totals propagate from the claude usage block so the subagent
handler's `ctx.updateTokens()` reports usable numbers.
- AbortSignal is wired through to SIGTERM the child so the subagent loop's
cancellation path stays correct.
Modified: src/commands/jobs.ts (worker registration)
- Conditionally constructs a MessagesClient via the new adapter when
GBRAIN_USE_CLAUDE_CLI=1.
- Passes it into makeSubagentHandler({ engine, client: subagentClient }).
- Logs `[minion worker] subagent routing via claude-cli (GBRAIN_USE_CLAUDE_CLI=1)`
on startup so the env var status is operator-visible.
Limitations of this commit (addressed in the follow-up):
- Tool use is not yet supported. Tools in params.tools are ignored; the
adapter returns a single text block with stop_reason='end_turn'.
- Token counts come from claude-cli's reporting and may not match the
Anthropic API's accounting precisely (especially for cache tiers).
Original design from #334; this commit preserves that author's attribution.
The follow-up commits on this branch carry the tool-use implementation.
* feat(subagent): tool use + context isolation + convention rename on top of jarvisdoes baseline
Builds on the previous commit (jarvisdoes's #334 baseline) by adding three
things the upstream issue called out as gaps or that surfaced during review:
1. Tool use support via system-prompt-instructed JSON emission.
2. Context isolation flags so claude-cli does not load operator-level
CLAUDE.md, skills, and local project context into every subagent call.
3. Env var rename from GBRAIN_USE_CLAUDE_CLI=1 to
GBRAIN_SUBAGENT_PROVIDER=claude-cli to match the existing
GBRAIN_<noun>_<role>=<value> convention used by GBRAIN_CHAT_MODEL,
GBRAIN_EMBEDDING_MODEL, GBRAIN_EXPANSION_MODEL.
## Tool use
The MessagesClient interface returns Anthropic.Message objects whose
content array may include tool_use blocks. The subagent handler filters
those blocks and dispatches each tool, so any backend that produces
correctly shaped tool_use blocks gets the same loop behavior as the
Anthropic SDK.
The adapter injects a system-prompt addendum describing the tool registry
plus an emission protocol:
<use_tools>
[{"id": "...", "name": "...", "input": {...}}, ...]
</use_tools>
After the response comes back, extractToolCalls() scans for the block,
parses the JSON (tolerant of optional ```json fencing), and converts each
entry into a tool_use content block. Multiple parallel tool calls in one
turn are supported via the array shape; this is the exact case that
breaks today on the codex-proxy / litellm GPT-5.x bridge where parallel
tool-call response IDs get dropped.
Defensive fallbacks:
- Malformed JSON inside the block: drop to text-only, stop_reason='end_turn'.
- Unterminated <use_tools> (no close tag): drop to text-only.
- Model omits id field: adapter synthesizes a toolu_claude_cli_<rand> id.
- Empty response: still hand the subagent loop a well-formed content
array so the .filter chain does not crash.
## Context isolation
claude-cli auto-discovers CLAUDE.md from cwd upward and injects the
operator's skills + plugins + auto-memory into the default system prompt.
On a real install that is ~42-65k tokens of contamination per subagent
call, with both cost and behavioral consequences (the subagent picks up
the operator's coding conventions, opinions, and preferences).
The maximum suppression that still preserves OAuth / Claude Max
subscription auth is:
- Spawn from a dedicated clean cwd (tmpdir-based) so LOCAL CLAUDE.md
auto-discovery has nothing to find. -13k tokens on a real gbrain
install where CLAUDE.md is substantial.
- --disable-slash-commands so skill resolution does not pull in
/skill-name handlers.
- --system-prompt <gbrain prompt> so the default system prompt is
replaced rather than appended to.
The --bare flag would also strip user-level ~/.claude/CLAUDE.md but it
forces ANTHROPIC_API_KEY auth, defeating the whole point of this adapter.
The remaining ~42k cached tokens from user-level instructions are
accepted as a cost-trivial trade-off because the Max subscription absorbs
the per-call cost. Behavioral contamination is mitigated by gbrain's
strong per-call system prompt overriding any operator-level drift.
## Env var rename
Surveyed all ~140 GBRAIN_* env vars in src/. The codebase uses three
patterns: GBRAIN_NO_<feature> (negative toggles), GBRAIN_<noun>_<role>
=<value> (routing keys), GBRAIN_ALLOW_<feature> (permissive toggles).
GBRAIN_USE_* does not appear anywhere except jarvisdoes's original
commit; it would introduce a fourth pattern.
GBRAIN_SUBAGENT_PROVIDER=claude-cli aligns with the routing-keys family
and is value-extensible — adding codex-cli / meridian-proxy / etc. later
means a new value, not a new env var. The scope ('SUBAGENT_*') is also
unambiguous about which calls the toggle covers; GBRAIN_USE_CLAUDE_CLI
was silent on whether it applied to all gbrain LLM calls or only the
subagent path.
Unknown values are rejected with a fail-fast error message naming the
two valid values rather than silently falling through to the default.
## Tests
New file: test/claude-cli-adapter.test.ts — 12 tests, 33 assertions:
- Text-only round trip (single text block, usage propagation, end_turn).
- Provider prefix stripping ('anthropic:claude-sonnet-4-6' -> 'claude-sonnet-4-6').
- Single tool_use parsing.
- Multiple parallel tool calls in one block (the case that triggered
the codex-proxy regression).
- Fenced JSON inside <use_tools> block.
- Model-omitted id gets synthesized to toolu_claude_cli_<rand>.
- Malformed JSON falls back to text.
- Unterminated block falls back to text.
- AbortSignal SIGTERMs the child.
- Error envelope rejected with informative message.
- Non-JSON output rejected with raw-output excerpt in the error.
- argv + cwd assertion: --disable-slash-commands + --system-prompt are
present and cwd is the dedicated tmpdir.
Tests use a POSIX shell stub at GBRAIN_CLAUDE_CLI_BIN that emits a
scripted --output-format json envelope, so the suite runs without
claude-cli installed and without API credits.
* feat(ai): claude-cli recipe with native gateway integration (supersedes #334 baseline)
Replaces the MessagesClient adapter + GBRAIN_USE_CLAUDE_CLI=1 env-var
gate from the previous commit on this branch with a proper gateway recipe.
The recipe path gives per-call routing as a native capability: a model
string like `claude-cli:claude-sonnet-4-6` lands here while a sibling
`litellm:gpt-5.4` continues through the litellm-proxy / codex-proxy path
in the same worker. No global env-var switch, no agent.use_gateway_loop
bypass, no MessagesClient injection at jobs.ts worker startup.
The previous commit on this branch (jarvisdoes baseline) is preserved
in the history for #334 authorship attribution. Its functional changes
are backed out here because the recipe pattern is gbrain's established
integration seam; introducing a parallel MessagesClient + env-var path
would have created two routing mechanisms competing for the same job.
New: src/core/ai/recipes/claude-cli.ts
- Recipe declaration: id 'claude-cli', tier 'native', implementation
'claude-cli', chat-only (no embedding or expansion touchpoints).
- Models: claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5-20251001.
- supports_tools and supports_subagent_loop both true.
- supports_prompt_cache false because the CLI handles caching internally
and does not surface cache_control via the standard control plane.
- auth_env.required is the empty array because the CLI owns auth (OAuth
session managed by `claude login`).
- Friendly aliases mirror the `anthropic` recipe: `sonnet`, `haiku`,
`opus` and the same legacy-id rewrites for back-compat with stale
config strings.
New: src/core/ai/providers/claude-cli-language-model.ts
- ClaudeCliLanguageModel class implementing the ai-sdk LanguageModelV2
interface.
- doGenerate: renders the ai-sdk prompt array into a system text + user
text, injects the use_tools protocol instructions when tools are
present, spawns `claude --print --output-format json --model <X>
--disable-slash-commands --system-prompt <gbrain prompt>` from a
dedicated tmpdir (contamination suppression: no local CLAUDE.md
auto-discovery), parses the JSON envelope, extracts <use_tools>
blocks, and returns ai-sdk-shaped LanguageModelV2Content (text +
tool-call parts with stringified-JSON input matching the V2 contract).
- Tolerates fenced JSON inside use_tools blocks, malformed JSON
(falls back to text), missing close tag (falls back to text),
model-omitted ids (synthesizes toolu_claude_cli_<rand>).
- Parallel tool calls in one block round-trip cleanly: this is the
case that drops IDs on the litellm + codex-proxy bridge today.
- AbortSignal SIGTERMs the child for proper cancellation.
- doStream throws not-supported (gateway.toolLoop is non-streaming).
Modified: src/core/ai/gateway.ts
- Adds case 'claude-cli' to instantiateChat (returns ClaudeCliLanguageModel).
- Adds case 'claude-cli' to instantiateExpansion (same wrapper, reserved
for a future expansion touchpoint declaration).
- Adds case 'claude-cli' to instantiateEmbedding (throws, no embedding
model, mirrors the native-anthropic path).
- Lazy require() at the call site keeps the gateway module load cheap
for users who never use the claude-cli path.
Modified: src/core/ai/recipes/index.ts
- Registers `claudeCli` in the ALL[] array next to `anthropic`.
Modified: src/core/ai/types.ts
- Adds 'claude-cli' to the Implementation union so the gateway switch
is exhaustive at compile time.
Reverted: src/commands/jobs.ts
- Drops the GBRAIN_USE_CLAUDE_CLI=1 env-var gate the prior commit
added. Routing now happens at the gateway based on the model string.
Deleted: src/core/minions/handlers/claude-cli-adapter.ts
- The MessagesClient adapter is superseded by the recipe + LanguageModelV2
path. Two routing mechanisms competing for the same job would have
forced users to reason about which one wins; the recipe is the single
source of truth.
New file: test/claude-cli-recipe.test.ts (16 tests, 46 assertions):
- Recipe registration: getRecipe returns chat-only Recipe; aliases map
short names (sonnet/haiku/opus) to canonical model ids.
- Text round trip: single text content block, usage propagation, stop
finish reason.
- Provider prefix stripping.
- Single tool-call parsing.
- Multiple parallel tool calls in one block.
- Fenced JSON inside the block.
- Model-omitted id synthesizes toolu_claude_cli_<rand>.
- Malformed JSON falls back to text + stop reason.
- Unterminated block falls back to text + stop reason.
- Tools offered but model declines: returns text-only with stop reason
so the gateway-loop treats it as a final answer rather than wedging
for tool calls that never come.
- AbortSignal SIGTERMs the child.
- is_error envelope rejected.
- Non-JSON output rejected.
- doStream throws.
- argv + cwd assertion: --print, --disable-slash-commands,
--system-prompt are present and cwd is the dedicated tmpdir.
Tests use a POSIX shell stub at GBRAIN_CLAUDE_CLI_BIN so the suite runs
without claude-cli installed and without API credits.
End-to-end smoke verified against a real `claude --print --model haiku`
invocation: model emitted `<use_tools>` block with toolu_add_001 +
{"a":12,"b":30}, adapter parsed back into a `tool-call` content block,
finishReason 'tool-calls'.
* feat(ai/claude-cli): harden subagent isolation, env scrub, verbose + stdin robustness
Four defensive fixes to the claude-cli provider so a subagent call behaves
identically regardless of the host's ambient Claude Code config:
- Agent isolation: pass `--tools ''` and `--strict-mcp-config` so the subprocess
runs as a raw LLM with no built-in tools and no inherited user MCP servers.
Without `--strict-mcp-config`, each call boots the user's MCP servers (including
gbrain's own), causing recursion plus PGLite single-writer lock contention.
- Env scrub: drop ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL
from the child env so the CLI authenticates via its own OAuth subscription
session. An inherited API key silently flips billing to per-token API usage,
the exact setup this recipe exists to replace.
- Verbose-mode compat: with `"verbose": true` in ~/.claude/settings.json,
`--print --output-format json` emits an event array instead of a bare result
object. Tolerate both shapes and select the result event.
- stdin robustness: handle the child stdin 'error' event and wrap write/end so a
missing binary (ENOENT) or early child death (EPIPE) rejects cleanly instead of
crashing the worker with an unhandled error.
Adds unit coverage for the env scrub, the isolation argv, and the verbose event
array. Verified against claude CLI 2.1.x.
* test(ai/claude-cli): cover verbose-array no-result + missing-binary reject paths
Two error branches in the hardened claude-cli provider had no coverage: the
verbose event-array path when no result event is present, and a missing binary
surfacing as a clean spawn-failed rejection. The missing-binary case is the
deterministic form of the stdin/EPIPE robustness; a synchronous stdin-write
throw is not reliably triggerable in a unit test, so the real ENOENT path the
handlers defend is exercised instead. Both reuse the existing shell-stub harness.
---------
Co-authored-by: Brett <brettdavies@users.noreply.github.com>
Co-authored-by: jarvisdoes <258486803+jarvisdoes@users.noreply.github.com>
Co-authored-by: Marco Maldonado <34176133+loweaxerium@users.noreply.github.com>
queue.add() with an idempotency_key returns any existing row regardless
of status. This means dead jobs (exhausted retries from a transient
provider outage) permanently block re-submission of the same work —
even after the underlying issue is fixed.
Fix: when the existing row is dead or cancelled, NULL its
idempotency_key (preserving the row for audit) and fall through to the
INSERT path so a fresh job can be created.
Affects dream synthesize children that died during provider migrations
(429 rate-limit on old Anthropic proxy, tool-results-missing on old
OpenRouter). 45 dead children were blocking re-synthesis of transcripts
in production.
Includes 4 new tests covering dead, cancelled, completed, and active
status interactions with idempotency dedup.
Co-authored-by: Rafael Reis <57492577+rafaelreis-r@users.noreply.github.com>
Co-authored-by: Rafael Reis <rafael.reis@contabilizei.com.br>
`gbrain doctor --remediation-plan` printed two consecutive lines that
contradicted each other when the brain was below target AND the target
was unreachable with autonomous remediation:
Brain score: 45/100 → target 90
Target unreachable: max with autonomous remediation is 70/100.
No remediations needed. Brain is at target.
The second sentence hid the real next step (configure the prereqs that
would lift `max_reachable_score`) and made the brain look healthy when it
was not.
Fix: gate the "Brain is at target" line on `brain_score_current >=
targetScore`. When the plan is empty AND the brain is below target, the
"Target unreachable" line above is already the user-facing explanation;
the `Blocked checks` block below surfaces the manual gap.
Extracted `renderRemediationPlanLines(plan, targetScore): string[]` as a
pure helper alongside `runRemediationPlan` so the regression coverage
asserts on the rendered output directly rather than mocking
`console.log`. `runRemediationPlan` now joins the lines verbatim through
console.log; behavior is byte-identical for every case other than the
fixed contradiction.
Five regression tests cover: unreachable-and-below-target (the bug
case), reachable-and-at-target, exact-target, below-target-with-plan,
unreachable-with-partial-plan. 38 tests across the adjacent doctor test
files stay green; `bun run typecheck` clean.
Co-authored-by: Brett <brettdavies@users.noreply.github.com>
synthesize-concepts.ts's design comment says extract_atoms stamps a
`concepts:` frontmatter field on each atom and :92 consumes ONLY that
field — but the extractor never wrote it, so the atoms → concepts
pipeline was dead end-to-end: every cycle reported "synthesize_concepts:
skipped — no atoms with concept refs" no matter how many atoms
accumulated (696 page-derived atoms / 0 with concepts on our production
brain before an external backfill).
Fix, all on the extractor side (no synthesize change needed):
- EXTRACT_PROMPT asks for `concepts` (1-3 kebab-case TOPIC labels) with
an explicit reuse-over-coinage instruction — labels must cluster,
since synthesize_concepts only materializes groups of >=2.
- parseAtomsResponse validates labels (kebab regex, max 3, drop
invalid; empty -> undefined).
- The putPage frontmatter write stamps `concepts` alongside lesson /
source_quote.
Tests: 4 parse cases + an end-to-end regression that goes extractor ->
real frontmatter -> synthesize_concepts' OWN DB query path -> concept
page. The existing tests fed synthesize via the `_atoms` seam, which is
exactly how this gap survived.
Validated in production ahead of this PR by stamping the same shape
externally: the next synthesize_concepts run wrote 33 concept pages
(T2=7/T3=26) from 60 stamped atoms, zero failures.
Co-authored-by: 陈源泉 <84364275+ChenyqThu@users.noreply.github.com>
Co-authored-by: 陈源泉 <chenyuanquan@chenyuanquandeMac-mini.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(doctor): raw-source persistence guarantee — warn-only v1 (#1978)
Every synthesized/derived page (dream_generated:true or type:synthesis)
must carry a raw trace or an explicit exemption. v1 is warn-only:
- New doctor check `raw_provenance` (brain category) flags synthesized
pages with none of: raw_trace/raw_source/source_uri/raw_trace_exempt
frontmatter, an attached raw_data row, or synthesis_evidence rows.
- Dream synthesize now stamps `raw_source: <transcript path>` into each
written page's frontmatter via the existing #2569 provenance stamp.
- Dream-cycle summary index pages and extract receipts carry an explicit
`raw_trace_exempt: true` + reason (no source document of their own).
No write path is blocked; fail-closed enforcement is the v2 escalation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(doctor): exclude soft-deleted pages from raw_provenance check
Sibling frontmatter checks (quarantined_pages, flagged_pages) filter
deleted_at IS NULL; without it a deleted synthesized page keeps warning
(and its slug keeps being named) through the 72h recovery window with
no way to clear the warn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
dream.drift.enabled has gated an unwired scaffold since v0.28: runPhaseDrift
had zero call sites, the resolved model + BudgetMeter were discarded
(void modelId; void meter), and no operator-readable output existed.
- Wire 'drift' as a CyclePhase (default OFF via dream.drift.enabled),
ordered after the calibration trio and before embed so the report page
gets embedded same-cycle. PHASE_SCOPE=global, cycle-lock coordinated,
--once (--phase drift --once) bypasses the gate for one run.
- Implement the LLM judge: soft-band candidates (weight 0.3-0.85, active,
unresolved, fresh timeline evidence) are judged against their page's
recent timeline entries via gateway chat; BudgetMeter-gated
(dream.drift.budget, default $1), capped by dream.drift.max_per_cycle
(default 20). Judge model resolves models.drift -> reasoning tier ->
sonnet fallback (unchanged from scaffold).
- Report-only v1: judged candidates land on a reports/drift-<date> page.
dream.drift.auto_update mutates NOTHING; the flag state is recorded in
the report for operators.
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): admin-gated rescope surface for DCR clients stuck on default scope (#1914)
DCR clients self-register with source_id='default' + federated_read=['default']
and the registration comment promised 'rescope via the CLI later' — but no
rescope surface existed. Adds:
- GBrainOAuthProvider.rescopeClient(clientId, { sourceId?, federatedRead? }):
single-statement COALESCE update, canonical source-id validation
(assertValidSourceId), FK-backed existence check on the write source,
friendly errors for pre-v60/v61 schemas and unknown clients. Takes effect
for already-issued tokens because verifyAccessToken re-reads oauth_clients.
- gbrain auth rescope-client <client_id> [--source S] [--federated-read a,b]
(trusted local CLI).
- POST /admin/api/rescope-client (requireAdmin), mirroring the existing
register-client / revoke-client admin endpoints.
Deliberately does NOT let clients self-widen scope (options a/b from the
issue) — fail-closed trust invariant.
Fixes#1914
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): rescope-client admin endpoint returns 400 (not 500) for nonexistent write source
The FK-translated 'Source "x" does not exist' error is a client error;
map it to 400 like the sibling validation failures. ('No OAuth client
found' is matched first, so the 404 path is unaffected.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Pages ingested into a config.federated=true source were invisible to
normal reads: get_page/list_pages scoped to the scalar resolved source
('default'), while the fully UNSCOPED resolve_slugs leaked every
source's slugs — the reporter's exact observation matrix.
- federatedSearchScope now backs get_page, list_pages and resolve_slugs
(not just search/query), so the unqualified read surface shares one
visibility set: grant > federated set > scalar source. resolve_slugs
gains the missing sourceScopeOpts-family scoping (leak sealed).
- The widening gate is now field-presence instead of ctx.remote:
localFederatedSourceIds is populated only by server-side transports
(never from caller params), so trust stays fail-closed while the
stdio MCP transport (no GBRAIN_SOURCE) and the legacy HTTP token
path (no operator-set permissions.source_id grant) can opt their
unqualified callers into the operator-configured federated set.
Tokens WITH a grant, per-call source_id, and OAuth allowedSources
all still win and never widen.
- gbrain sync now attributes its ingest-log row to the synced source
instead of the shared 'default' bucket (attribution sub-bug).
No engine SQL changes: getPage/listPages/resolveSlugs already accept
sourceIds[] in both engines (#1393/#876).
Fixes#3242
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Both engines' searchTakes used whole-string trigram similarity
(claim % query), which structurally cannot pass the 0.3 threshold for a
short keyword against a 100-200 char claim — keyword search returned
zero results on real brains. Switch the predicate to word similarity
(query <% claim) and rank by word_similarity(query, claim), in both
postgres-engine and pglite-engine per the engine-parity invariant.
Holder allow-list and source-scope filters unchanged.
Regression test: single-word query must match a long claim containing
it (fails under the old predicate).
Fixes#3267
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
OAuth/source scoping only guards the serve --http path; a container
sharing Docker's default bridge with the brain's Postgres can open a
direct DB session without a token. Adds a 'Co-located Docker workloads'
subsection to docs/mcp/DEPLOY.md with the operator checklist, a
trust-boundary paragraph in SECURITY.md, and an ops note + cross-link
in the company-brain tutorial.
Fixes#3270
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Rebase of #3279 onto current master: the whoami oauth shape gains
source_id (AuthInfo.sourceId, null when absent) and federated_read
(AuthInfo.allowedSources, [] when absent) — read-only self-introspection
that widens no grant. Re-applied against the post-#3091 description
string (stdio transport shape preserved) and merged the grant tests
into the current whoami.test.ts alongside the stdio cases.
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: boundless-forest <boundless-forest@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Agent tools (Claude Code, Codex, …) support a *.local.md counterpart to the
committed CLAUDE.md/AGENTS.md for personal, per-clone instruction overrides that
load after the committed file and are meant to stay uncommitted.
gbrain already ignores other workspace-local agent artifacts (.context/,
.claude/), so this fills the remaining gap for the two root-level override files.
Explicit filenames rather than a *.local.md glob, matching the existing
commented, specific style.
doctor and the get_health CLI surface printed two different timeline
metrics under one ambiguous 'timeline' label: the entity-scoped
timeline_coverage fraction (eligible entity pages with a timeline entry)
and the whole-brain timeline_coverage_score brain-score component (all
pages with a timeline entry, 0-15). Different numerators AND
denominators, indistinguishable in output.
Label-only fix, scoring unchanged:
- graph_coverage check: 'entity timeline coverage N%'
- brain_score breakdown: 'timeline density (all pages) N/15'
- get_health CLI: 'Timeline coverage (entity pages)' plus a new
'Timeline density (all pages): N/15' line when the score is present
Adds test/doctor-timeline-metric-labels-2298.test.ts pinning the
denominator semantics, the rendered doctor messages, and the CLI
guard matrix (fails on master, passes here).
Takeover of #2761.
Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: TurgutKural <TurgutKural@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(thin-client): map --source/GBRAIN_SOURCE/.gbrain-source onto source_id for routed ops (#2098)
The thin-client route short-circuits before makeContext, so the 6-tier
source resolution never ran and `gbrain query --source X` against a
remote brain sent the unknown `source` key verbatim — the server op
ignored it and searched unscoped.
applyThinClientSourceScope now runs the engine-free tiers (flag → env →
dotfile; DB-backed tiers need an engine, and the server's grant scoping
covers the rest) and sets the op's source_id wire param. Ops declaring
their own `source` param are untouched; an explicit --source on an op
with no source_id wire param errors loudly instead of silently dropping;
explicit --source-id/--all-sources on the wire win over ambient tiers.
Fixes#2098
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(thin-client): use withEnv() instead of direct process.env mutation (test-isolation R1)
CI verify failed on check:test-isolation — thin-client-source-scope.test.ts
mutated process.env.GBRAIN_SOURCE via beforeEach/afterEach. Wrapped each
test body in withEnv() from test/helpers/with-env.ts instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(thin-client): keep ambient scope out of get_skill's non-scope source_id param
get_skill's source_id is a mode switch (host catalog vs brain-resident-pack
lookup), not a read-scope filter. Ambient GBRAIN_SOURCE / .gbrain-source
injection would silently reroute 'gbrain skill <name>' on thin clients to
getResidentSkillDetail. Exclude it via NON_SCOPE_SOURCE_ID_OPS; explicit
--source-id still passes through, explicit --source errors with a hint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Garry Tan <garrytan@gmail.com>
* chore(ci): refresh GitHub Actions SHA pins (checkout v4, action-gh-release v2)
Pre-ship pin staleness check per docs/RELEASING.md: both floating major
tags moved upstream; pins updated to the current tag commits.
* v0.42.65.0 chore(release): 92 verified fixes since v0.42.64.0 — changelog + version bump
Aggregates everything merged to master since the v0.42.64.0 bump commit:
community fixes, credited takeovers, batch re-lands, CI hardening, and
maintainer-approved features. Net commit list excludes revert pairs.
No new schema migrations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(deps): clear OSV-flagged transitive dependencies via override floors
Raise the existing security-floor overrides so the lockfile resolves
patched versions of three transitive packages flagged by the OSV scan
(@hono/node-server, fast-uri, body-parser). None are on gbrain's own
runtime path (@hono/node-server is only referenced by the MCP SDK's
optional hono transport, which gbrain does not load); the floors keep
the dependency scan green. MCP/OAuth unit tests pass against the
resolved versions.
* chore(release): fold #3110 into the v0.42.65.0 entry (93 net changes)
---------
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(links): resolve [[wikilink]] + slug-path frontmatter values; keep frontmatter links fresh on the incremental cycle
Takeover/rebase of two community PRs:
PR #1983 — frontmatter link fields never resolved Obsidian-style values:
- makeResolver step 1's strict slug regex rejected digit-leading folders
(90-people/nicolai) and nested paths (a/b/c); broadened to any slug-shaped
value with an EXACT getPage match only (no fuzzy, no false positives).
- extractFrontmatterLinks resolved "[[dir/slug]]" verbatim; new anchored
unwrapWikilink() strips wholly-wrapped [[...]] (and |alias/#heading/^block)
before resolution. Bare values pass through unchanged.
- Same broadened slug-shape applied to the fs-path synthetic resolver in
extractLinksFromFile (exact Set membership guards it), so the fs
frontmatter path resolves PARA-numbered slugs too.
PR #2434 — the cycle's incremental extract (extractForSlugs) extracted body
links only, so externally-edited YAML (sources:/related:) edges drifted
stale. Adds an includeFrontmatter opt (threaded as a param after sourceId,
which master added in #1747/#1503 after the PR was cut), gated by the new
config key autopilot.incremental_extract_include_frontmatter (default off,
preserves body-only behavior).
Tests: unwrapWikilink unit coverage, broadened-resolver + end-to-end
frontmatter cases in test/link-extraction.test.ts; fs-resolver digit-leading
case in test/extract.test.ts; incremental gate off/on cases in
test/extract-incremental.test.ts.
Co-authored-by: spiky02plateau <spiky02plateau@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cycle): honor DB-plane config for incremental_extract_include_frontmatter
The gate read loadConfig() (file/env plane) only, but the documented enable
command — gbrain config set autopilot.incremental_extract_include_frontmatter
true — writes the DB plane via engine.setConfig, so the feature could never be
turned on the documented way (silent no-op, #2120 class). Now the file plane
wins when the key is present there; otherwise the DB plane is consulted,
matching the autopilot.auto_drain.* read pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: spiky02plateau <spiky02plateau@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Garry Tan <garrytan@gmail.com>
Adds a `perplexity` embedding recipe (OpenAI-compatible at
https://api.perplexity.ai/v1, auth via PERPLEXITY_API_KEY only — never an
OPENAI_API_KEY fallback) covering pplx-embed-v1-0.6b and pplx-embed-v1-4b.
Perplexity's /embeddings endpoint diverges from OpenAI's wire shape in two
places that break the AI SDK adapter, handled by a new perplexityCompatFetch
shim (mirrors the Voyage/ZeroEntropy pattern incl. the two-layer OOM caps):
- encoding_format only accepts base64_int8/base64_binary; the SDK's 'float'
default is forced to 'base64_int8' outbound.
- The response embedding is base64-encoded signed int8 components (natively
quantized); decoded to number[] inbound so the SDK's Zod schema validates.
Cosine similarity is scale-invariant, so raw int8 components rank correctly.
Flexible dims (Matryoshka-style 128..native max: 1024 for 0.6b, 2560 for 4b)
validate fail-loud in dims.ts + the init preflight; `dimensions` is
Perplexity's native field so no wire translation is needed. default_dims is
1024 (works on a plain vector column for both models); the 4b model's full
2560 width rides the existing halfvec (>2000 dims) storage/ANN path. Pricing
entries land in embedding-pricing.ts.
Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(schema): make bundled pack inspection truthful (#2029)
Two live bugs:
- parseYamlMini had no block-scalar support, so a 'description: |' swallowed
every following top-level key — the active gbrain-recommended pack loaded
with 0 page types. Add parseBlockScalar for |/|-/|+ and >/>-/>+ in both
mapping and sequence-sibling positions.
- The bundled-pack list was hand-copied in three places (operations.ts had 2
names, mutate.ts had 3, load-active.ts had 7). New single registry
src/core/schema-pack/bundled.ts carries all 7 shipped packs; every
consumer derives from it.
Takeover of #2029, rebased onto master (schema.ts hunks already landed).
Co-authored-by: JiraiyaETH <JiraiyaETH@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(minions): subagent default client resolves config-stored Anthropic key (#2048)
The legacy subagent path constructed a bare new Anthropic() (env-only), so
launchd/MCP workers whose key lives in gbrain config (anthropic_api_key)
failed auth. anthropic-key.ts now exports resolveAnthropicKey() (env first,
then config; hasAnthropicKey delegates) and makeSubagentHandler passes it as
apiKey.
Partial takeover of #2048 — only the auth patch; the path patches were
superseded by the outputRoot mechanism (#2415).
Co-authored-by: JiraiyaETH <JiraiyaETH@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(schema): point bundled-registry test at bundled.ts source of truth
The bundled pack list moved from load-active.ts to bundled.ts in the
truthful-inspection refactor; the T4 registry test still grepped
load-active.ts source. Assert BUNDLED_PACK_NAMES directly instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(schema): block scalars keep '#' as literal content
Inside a YAML block scalar '#' is content, not a comment; parseBlockScalar
was routing lines through stripComment/isBlank, truncating descriptions
like 'see issue #2029' and blanking comment-looking lines. Use the raw
line inside the scalar. Adds a pinning test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: JiraiyaETH <JiraiyaETH@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A serve process that wedges mid-boot (e.g. a boot step blocked on an
unreachable upstream) held the PGLite write lock indefinitely — the
post-#2348 lock discipline never steals from a live holder, so every CLI
consumer timed out until the serve PID was manually killed.
runServe (stdio path) now arms a boot-readiness deadline around
startMcpServer: if the transport hasn't connected within
GBRAIN_SERVE_BOOT_TIMEOUT_SECONDS (default 60, 0 disables), it logs the
condition, awaits engine.disconnect() (raced against the existing
5s cleanup deadline so a wedged WASM close can't trap it either), and
exits non-zero so supervisors restart with backoff. A completed boot
clears the timer; the HTTP path is untouched (own lifecycle).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cycle): propose_takes skips cleanly when the chat provider is unavailable + narrow page projection
Takeover of PR #1979 by @shawnduggan. The original PR gated on a
hardcoded ANTHROPIC_API_KEY heuristic (modelNeedsAnthropicKey defaulting
to true), which master deliberately removed elsewhere — it misclassified
non-Anthropic stacks and fought the tier-config model resolution. This
lands the intent the master-blessed way: probe the RESOLVED chat model
(opts.model ?? getChatModel()) via probeChatModel — same semantics as
patterns.ts / think/index.ts — and skip the phase cheaply when the
provider can't run. Injected extractors are never gated.
Also keeps the PR's uncontested half: load proposal candidates with a
narrow projection (slug, source_id, compiled_truth) instead of
listPages' SELECT p.*, preserving sourceIds > sourceId scope precedence
and updated_desc ordering.
Co-authored-by: shawnduggan <shawnduggan@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(brainstorm): price cost preview against the configured chat model + models.brainstorm.judge config key
Takeover of PR #1855 by @starm2010, shrunk to the brainstorm-only
portion (the cycle-phase hunks are superseded by the resolveModel-in-
phase approach already on master). The cost preview + hard cost ceiling
previously always priced anthropic:claude-sonnet-4-6 even when the
configured chat_model (which the gateway actually runs) was something
else; modelStr now resolves override → config.chat_model → fallback.
The judge phase honors a new models.brainstorm.judge config key when no
--judge-model flag is passed, resolved in the orchestrator so every
caller (brainstorm, lsd, eval-brainstorm) benefits.
Co-authored-by: starm2010 <starm2010@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(test): use withEnv()/emptyHome() in propose-takes no-key tests
check:test-isolation R1 flagged direct process.env mutation in the two
new no-key tests. Swap the hand-rolled save/mutate/restore for the
canonical withEnv() helper (+ emptyHome() for the hermetic GBRAIN_HOME).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(jobs/autopilot): interval persistence, --lock-duration flag, dead-jobs doctor check, deployment-shape docs
Four backlog items in the jobs/autopilot workers, locks & installers area:
- #2794: `gbrain autopilot --install` silently dropped `--interval`. The
installer now parses + validates it, persists it to config
(autopilot.interval), and threads it into the wrapper's exec line; a
later flag-less --install regenerates the wrapper from the persisted
value, and the daemon run path falls back to the same config key.
- #1014: new `--lock-duration MS` flag (env: GBRAIN_LOCK_DURATION) on
`gbrain jobs work` and `gbrain jobs supervisor` to tune the worker
stall-lock window (and so the lockDuration x max_stalled wall-clock
dead-letter cap). Validated like --health-interval (integer >= 1000ms);
the supervisor propagates it to the spawned worker via buildWorkerArgs;
shown in the worker startup banner.
- Takeover of PR #1185 (@ethanbeard): `gbrain integrations doctor` now
surfaces dead minion jobs as a cross-cutting [queue] check. Reworked
from the original: consumes a new machine-readable `gbrain jobs list
--json` surface instead of screen-scraping the human table (long job
names shift the columns), and scopes to a 24h finished_at window so one
ancient dead job can't flag ISSUES forever (parity with main doctor's
queue checks).
- #631: documented the production deployment shape for autopilot vs jobs
supervisor in docs/guides/minions-deployment.md — recommend the
`autopilot --no-worker` + `jobs supervisor` split, warn against running
both worker lanes, and cross-link the --no-worker liveness probe.
Co-authored-by: ethanbeard <ethanbeard@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(autopilot): make #2794 wrapper-script tests hermetic — fake gbrain on PATH
writeWrapperScript calls resolveGbrainCliPath(), which shells out to
`which gbrain` and throws on CI runners where no gbrain binary is
installed. The two new --interval threading tests failed only in CI
(dev machines have gbrain on PATH). Prepend a fake executable to PATH
for the describe block so resolution is deterministic everywhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: ethanbeard <ethanbeard@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(init): explicit --embedding-model overrides the persisted --no-embedding sentinel (#2301)
Pre-fix, once ~/.gbrain/config.json carried embedding_disabled: true (the
--no-embedding deferred-setup sentinel), every re-init silently re-deferred
embedding: resolveAIOptions honored the sentinel BEFORE the explicit
--embedding-model flag and never cleared noEmbedding, and the persistence
merge carried the sentinel forward via ...existingFile. Both recovery paths
were dead ends — `gbrain config set embedding_model` is hard-refused
(schema-sizing field), and re-init hit the sentinel.
Fix:
- resolveAIOptions: an explicit --embedding-model / --model flag clears the
sentinel-derived noEmbedding (explicit --no-embedding on the same
invocation still wins — that branch runs after).
- initPGLite + initPostgres persistence: a resolved (model, dims) tuple
drops the stale embedding_disabled key instead of inheriting it.
- assertEmbeddingEnabled message no longer recommends the refused
`gbrain config set embedding_model` command; the working re-init recipe
leads.
Test: test/e2e/init-reinit-after-deferred.test.ts — deferred init then
re-init with an explicit model recovers (sentinel gone, model persisted);
bare re-init still honors the sentinel.
Fixes#2301
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: scrub remaining hard-refused `config set embedding_model` advice from init deferred-setup messages
The PR fixed the recovery recipe in assertEmbeddingEnabled but the
deferred-setup lines in initPGLite/initPostgres and the fail-loud
defer hint still pointed users at the Lane C.2 hard-refused command.
Point all three at the working re-init recipe instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(dream): stamp path-derived sources so --dir runs land cycle freshness (#1869)
gbrain dream --dir <path> (and the configured sync.repo_path fallback)
never wrote last_source_cycle_at / last_full_cycle_at because runCycle's
stamp gate reads opts.sourceId and dream only set it from --source.
Doctor's cycle_freshness stayed perpetually stale on path-scoped brains.
Fix at the command level: dream derives the source id from the resolved
brain dir via resolveSourceForDir (now exported from cycle.ts) and passes
it as opts.sourceId. runCycle's stamp/lock semantics are untouched, so
legacy global callers (autopilot-global-maintenance runs GLOBAL_PHASES
with a brainDir and no sourceId) cannot falsely stamp per-source
freshness — the flaw that sank the runCycle-wide variant in PR #2549.
A derived match on an archived source is skipped (mirrors the explicit
--source archived guard).
Takeover of #2549.
Co-authored-by: javieraldape <javieraldape@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(autopilot): close the engine on SIGTERM/SIGINT instead of hard-exiting (#1872)
systemctl stop (SIGTERM) previously hard-exited autopilot without ever
closing the engine. On PGLite the cycle steps run INLINE in the autopilot
process, so a mid-write exit kills WASM Postgres with the WAL dirty and
can corrupt the brain.
Now both exit paths close the engine first:
- autopilot's own shutdown() (SIGINT + internal stops like max_crashes /
cycle-failure-cap) aborts the in-flight inline cycle via an
AbortController threaded into runCycle, drains it briefly, and awaits
engine.disconnect() before process.exit(0).
- process-cleanup's SIGTERM handler (installed at cli.ts module load,
exits within its 3s cleanup deadline) reaches the same closeEngine via
a registered 'autopilot-engine-close' cleanup callback.
PGLite's disconnect() drains the pending query and checkpoints before
closing; a second call is a no-op, so both paths firing is safe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(dream): conform dream-dir-source-stamp to canonical PGLite isolation pattern
check:test-isolation R3/R4 flagged the new test file: engine was created
in beforeEach (outside beforeAll) and never disconnected in afterAll.
Switch to the canonical shared-engine pattern (beforeAll create,
beforeEach resetPgliteState, afterAll disconnect) per
test/helpers/reset-pglite.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: javieraldape <javieraldape@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
orphan_ratio's denominator is swamped on auto_chronicle brains by the
machine-generated chronicle events (life/events/<day>-<hash>, written
per eligible event) — no inbound links by design. The shipped policy
already excludes raw/atoms/skills/dreaming/daily and extracts/, but
life/events/ was still counted; on a 1,657-page auto_chronicle brain it
was ~72% of the orphan mass, enough to pin the ratio red.
Add 'life/events/' to DENY_PREFIXES — a scoped prefix, NOT the whole
`life/` first-segment, so human-authored life/diary/ (gbrain capture
--type diary) stays IN the denominator. Same shipped hardcoded-class
mechanism as the existing entries; not the #2215 user-config route
(closed not_planned). Knowledge classes (concepts/people/notes/projects)
also stay in, so genuine graph decay still trips.
Regression in test/orphans-pure-fn.test.ts: life/events/ now excluded
(fails before, passes after); life/diary/ and concepts//notes//projects/
pinned as still-counted. doctor's orphan_ratio uses the same shouldExclude
path (getOrphansData; local + doctor-remote MCP), covered transitively.
zerank-2 is the default reranker under search_mode: tokenmax, but had no
pricing entry — any --max-cost-capped rerank call TX2 hard-failed in
BudgetTracker.reserve() with "no pricing entry".
Adding the entry to EMBEDDING_PRICING alone (the issue's suggested fix)
does not resolve this: lookupPricing()'s rerank branch in
budget-tracker.ts never consulted that table at all, only
ANTHROPIC_PRICING and the FREE_LOCAL_RERANK_PROVIDERS zero-price set.
Verified by reproducing the hard-fail with only the pricing-table entry
added and confirming it still threw.
Fix: add the $0.025/1M-token entry (docs/ai-providers/zeroentropy.md)
and wire the rerank branch to fall back to lookupEmbeddingPrice, reusing
the existing provider:model-keyed table instead of duplicating a third
pricing surface.
Addresses the report in #3223.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>