mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:49:14 +00:00
* feat(migrate): provider-agnostic embedding migration service — the path off ZeroEntropy (#3390) - gbrain migrate embeddings --to <provider:model> (alias: retrieval-upgrade): plan + cost preflight, consent gate (--yes / TTY confirm / non-TTY exit 2), live probe against the target provider before any mutation, env-override gate, schema dimension transition via the shared runSchemaTransition, dual-plane config write, NULL-signature-inclusive invalidation, query-cache purge, resumable re-embed through the standard embed pipeline (single-flight locks, backoff, pacing, stderr progress). Killed runs resume by re-running the same command; the NULL-embedding column is the checkpoint. - #3391 root-cause fix (both engines): countStaleChunks / sumStaleChunkChars / invalidateStaleSignatureEmbeddings accept includeNullSignature to lift the v108 grandfather clause; embed --stale warns loudly when a model swap leaves NULL-signature pages in the old embedding space, and --include-null-signature re-embeds them. Default sweep behavior unchanged. - knobs_hash v=12 → v=13 (prov=default legacy callers must not be served pre-migration cache rows). - migrate_embeddings op: scope admin, localOnly, hidden cliHints, hard remote refusal, needs_confirmation without yes=true. - One-shot post-upgrade ZE-sunset banner (ze_sunset_notice_shown) for brains resolving to a zeroentropyai:* embedding model or reranker. - doctor's dimension-mismatch repair hint now names the real command. - Docs: docs/guides/embedding-migration.md, KEY_FILES entries, spend-controls gate row. Tests: PGLite unit + full-lifecycle flow (interrupted-run resume), real-Postgres e2e (pgvector DDL path + #3391 predicate parity). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): satisfy check:test-isolation + bump the remaining knobs_hash pins - test/migrate-embeddings-flow.test.ts → .serial.test.ts: the file holds a temp GBRAIN_HOME + an installed fake embed transport for its whole lifecycle (beforeAll → afterAll), which withEnv() can't wrap. This also fixes the CI shard-pollution failure in test/ai/recipes-existing-regression.test.ts (that file passes solo on both master and this branch; the flow test's configureGateway + provider-key deletion was leaking into it inside the same shard process). - test/embedding-migration.test.ts: env-override case now uses withEnv(). - Bump the three remaining KNOBS_HASH_VERSION pins to 13 (cross-modal-phase1, search-alias-resolved-boost, search/knobs-hash-reranker). - Docs + llms bundles follow the test rename. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(test): wire the new Postgres e2e into the smart e2e selector map Changes to embed.ts / embedding-migration.ts / retrieval-upgrade-planner.ts / postgres-engine.ts now trigger test/e2e/migrate-embeddings-postgres.test.ts — the #3391 stale predicates and runSchemaTransition's DDL path behave differently on real pgvector than on PGLite, so the smart selector has to know. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(migrate): consult spend.posture in the embedding-migration consent gate The brief asked the gate to honor spend.posture; it previously didn't read it at all. Now it does — but deliberately does NOT bypass on tokenmax: posture waives the spend CEILING, and this gate also guards a destructive schema rebuild (existing vectors dropped, retrieval degraded until the re-embed finishes). Under tokenmax the dollar figure is marked informational on stderr and the confirmation is still asked; --yes stays the single scripted bypass. Pinned by a new case in the flow test so a later refactor can't quietly turn posture into a bypass. Guide + spend-controls table updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * wip: blocker fixes --------- Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
195 lines
6.8 KiB
Markdown
195 lines
6.8 KiB
Markdown
# Progress events
|
|
|
|
Canonical reference for the JSONL progress stream that `gbrain` writes to
|
|
`stderr` when a bulk command runs with `--progress-json`. Stable from
|
|
v0.15.2. Additive changes only; no renames or removals without a major
|
|
version bump.
|
|
|
|
Most humans won't read this page. Agents parsing progress will.
|
|
|
|
## When do I get these events?
|
|
|
|
Any of these commands stream events when `--progress-json` is set:
|
|
|
|
- `gbrain doctor` (DB checks, JSONB integrity, markdown body completeness,
|
|
integrity sample)
|
|
- `gbrain orphans`
|
|
- `gbrain embed`
|
|
- `gbrain files sync`
|
|
- `gbrain export`
|
|
- `gbrain extract [links|timeline|all]` (fs or db source)
|
|
- `gbrain import`
|
|
- `gbrain sync`
|
|
- `gbrain migrate --to …`
|
|
- `gbrain repair-jsonb`
|
|
- `gbrain check-backlinks`
|
|
- `gbrain lint`
|
|
- `gbrain integrity auto`
|
|
- `gbrain eval`
|
|
- `gbrain apply-migrations` (the orchestrator + every child command)
|
|
|
|
Non-bulk commands (`stats`, `graph-query`, `get`, `put`, etc.) don't emit
|
|
events — they return in under a second.
|
|
|
|
## Channel
|
|
|
|
- Progress events: **`stderr`**, one JSON object per line, `\n`-terminated.
|
|
- Data results (`--json` payloads from each command): **`stdout`**.
|
|
- Final human summaries: **`stdout`**.
|
|
|
|
Agents can safely capture stdout for their result parsing and read stderr
|
|
separately for progress.
|
|
|
|
## Flags
|
|
|
|
| Flag | Behavior |
|
|
|---|---|
|
|
| *(none)* | Auto. TTY: `\r`-rewriting single line. Non-TTY: plain line-per-event on stderr. |
|
|
| `--progress-json` | Force JSON-lines mode on stderr (this doc). |
|
|
| `--quiet` | Suppress progress entirely. Warnings and final output still print. |
|
|
| `--progress-interval=<ms>` | Override the minimum interval between tick emits (default 1000). |
|
|
|
|
Global flags: parsed by `src/core/cli-options.ts` before command dispatch,
|
|
so `gbrain --progress-json doctor` works the same as
|
|
`gbrain doctor --progress-json` (the latter also works — per-command
|
|
parsers see the flag via the shared `CliOptions` singleton).
|
|
|
|
## Event types
|
|
|
|
Every event is a single-line JSON object with these common fields:
|
|
|
|
| Field | Type | Notes |
|
|
|---|---|---|
|
|
| `event` | string | One of: `start`, `tick`, `heartbeat`, `finish`, `abort`. |
|
|
| `phase` | string | Machine-stable snake_case, dot-separated. See "Phase names" below. |
|
|
| `ts` | ISO 8601 UTC string | Event emission time. |
|
|
| `elapsed_ms` | number | Ms since the phase started. Present on `tick`/`heartbeat`/`finish`/`abort`. |
|
|
|
|
### `start`
|
|
|
|
Emitted when a phase begins.
|
|
|
|
```json
|
|
{"event":"start","phase":"doctor.db_checks","ts":"2026-04-20T12:34:56.789Z"}
|
|
{"event":"start","phase":"import.files","total":52000,"ts":"2026-04-20T12:34:56.789Z"}
|
|
```
|
|
|
|
Optional fields:
|
|
|
|
- `total` — the total item count if known at start.
|
|
|
|
### `tick`
|
|
|
|
Emitted periodically during iteration. Time- and item-gated: the reporter
|
|
won't emit more often than `minIntervalMs` (default 1000) and
|
|
`minItems` (default `max(10, ceil(total/100))`).
|
|
|
|
```json
|
|
{"event":"tick","phase":"orphans.scan","done":15000,"total":52000,"pct":28.8,"elapsed_ms":4200,"eta_ms":10300,"ts":"..."}
|
|
```
|
|
|
|
Fields:
|
|
|
|
- `done` — items completed in this phase.
|
|
- `total` — total items, if known. Omitted when the scan doesn't have a
|
|
total up front (e.g. a streaming iterator).
|
|
- `pct` — `done/total * 100`, one decimal. Omitted when `total` is unknown.
|
|
- `eta_ms` — projected ms until `done === total`, from the observed rate.
|
|
Omitted when `total` is unknown.
|
|
- `note` — optional string with the current item (e.g. a slug or filename).
|
|
|
|
### `heartbeat`
|
|
|
|
Emitted for long-running single operations that don't iterate
|
|
(e.g. `SELECT` against a 50K-row table). No `done`, no `total` — just a
|
|
signal that work is still happening.
|
|
|
|
```json
|
|
{"event":"heartbeat","phase":"doctor.markdown_body_completeness","note":"scanning pages for truncation…","elapsed_ms":1000,"ts":"..."}
|
|
```
|
|
|
|
### `finish`
|
|
|
|
Emitted when a phase completes normally.
|
|
|
|
```json
|
|
{"event":"finish","phase":"import.files","done":52000,"total":52000,"elapsed_ms":187000,"ts":"..."}
|
|
```
|
|
|
|
### `abort`
|
|
|
|
Emitted by a single process-level SIGINT/SIGTERM handler that tracks every
|
|
live phase. After `abort`, no further events emit for that phase.
|
|
|
|
```json
|
|
{"event":"abort","phase":"doctor.markdown_body_completeness","reason":"SIGINT","elapsed_ms":5300,"ts":"..."}
|
|
```
|
|
|
|
## Phase names
|
|
|
|
Phases use `snake_case.dot.path` naming. A fresh reporter starts at the
|
|
root; `child()` composition appends to the parent's current phase, so a
|
|
sync that calls import emits `sync.import.<file>`, not `import.<file>`.
|
|
|
|
Stable phase names shipped in v0.15.2:
|
|
|
|
- `doctor.db_checks` (umbrella for all DB-side doctor checks)
|
|
- `orphans.scan`
|
|
- `embed.pages`
|
|
- `extract.links_fs`, `extract.timeline_fs`, `extract.links_db`, `extract.timeline_db`
|
|
- `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`
|
|
- `integrity.auto`
|
|
- `eval.single`, `eval.ab`
|
|
- `export.pages`
|
|
- `files.sync`
|
|
|
|
Sub-phases exposed via `child()`:
|
|
|
|
- `sync.import.files` — nested inside a sync
|
|
- `apply_migrations.v0_12_2.jsonb_repair` — nested inside the orchestrator
|
|
|
|
## Subprocess inheritance
|
|
|
|
When a parent CLI spawns `gbrain …` child processes (mostly in
|
|
`src/commands/migrations/*`), global flags (`--quiet`, `--progress-json`,
|
|
`--progress-interval`) are propagated to the child's argv via the
|
|
`childGlobalFlags()` helper in `src/core/cli-options.ts`. Child stderr
|
|
passes straight through `stdio: 'inherit'` so the event stream is one
|
|
merged JSONL feed on the parent's stderr.
|
|
|
|
One exception: the orchestrator phase in `migrations/v0_12_2.ts` that
|
|
captures child stdout (`repair-jsonb --dry-run --json` for verification)
|
|
does not pass `--progress-json` to avoid any risk of stdout pollution
|
|
breaking the orchestrator's `JSON.parse`. Its stdio is explicit:
|
|
`['ignore', 'pipe', 'inherit']` so stderr still flows through.
|
|
|
|
## Minion jobs
|
|
|
|
`gbrain jobs work` (the Minion worker daemon) keeps progress in the DB,
|
|
not on stderr. Each Minion handler that runs a bulk core (embed, sync,
|
|
extract, import, backlinks) calls `job.updateProgress({done, total,
|
|
…})` per iteration. Agents read per-job progress via the
|
|
`get_job_progress` MCP operation or `gbrain jobs get <id>`.
|
|
|
|
The `jobs work` daemon itself emits coarse one-line-per-job stderr output
|
|
for liveness only. Per-page detail lives in the DB.
|
|
|
|
## Compatibility
|
|
|
|
- **Added**: only. A new event type, a new field, a new phase name — all
|
|
safe. Agents must ignore unknown fields and unknown event types.
|
|
- **Removed/renamed**: never without a major version bump.
|
|
- **Schema changes**: announced in `CHANGELOG.md` and in
|
|
`skills/migrations/v<next>.md`.
|
|
|
|
If your agent depends on this schema and something surprises you, open
|
|
an issue with the event you received and what you expected.
|