A7: run only the resolve_symbol_edges dream phase, detected first

/sync-gbrain --dream and the --full auto-build ran gbrain's full dream
cycle (~35 minutes, LLM phases) when the call graph needs only the
resolve_symbol_edges phase. The stage now runs
gbrain dream [--source <id>] --phase resolve_symbol_edges when the
installed gbrain accepts that phase (detected by behavior: gbrain
validates --phase before printing --help). Otherwise it skips the forced
dream and says the full cycle costs about 35 minutes and how to run it,
instead of starting it.

Co-authored-by: liutiming <timingliu@outlook.com>
This commit is contained in:
garrytanandliutiming committed 2026-10-03 21:26:00 +00:00
1 parent ce651c2089
commit 76bf72d89f
4 files changed
+106 -8

No files matched your search

+30 -3
View File
@@ -1690,6 +1690,16 @@ export function shouldRunDream(args: CliArgs, cycle: CycleStatus | null): boolea
* binary / malformed env) — a broken install must be visible, not disguised as
* optional maintenance.
*/
/**
* A7: detected, not assumed. gbrain validates --phase values while parsing,
* before --help prints, so a gbrain without the phase exits non-zero and one
* without --phase prints help that never mentions it.
*/
export function gbrainSupportsSymbolEdgePhase(env: NodeJS.ProcessEnv = process.env): boolean {
const r = spawnGbrain(["dream", "--phase", "resolve_symbol_edges", "--help"], { baseEnv: env, timeout: 15_000 });
return r.status === 0 && /--phase\b/.test(`${r.stdout || ""}${r.stderr || ""}`);
}
export async function runDream(args: CliArgs): Promise<StageResult> {
const t0 = Date.now();
@@ -1702,8 +1712,8 @@ export async function runDream(args: CliArgs): Promise<StageResult> {
ok: true,
duration_ms: 0,
summary: sourceId
? `would: gbrain dream --source ${sourceId} (build this source's call graph)`
: "would: gbrain dream (call-graph build)",
? `would: gbrain dream --source ${sourceId} --phase resolve_symbol_edges (build this source's call graph; skipped if gbrain cannot scope the phase)`
: "would: gbrain dream --phase resolve_symbol_edges (call-graph build; skipped if gbrain cannot scope the phase)",
};
}
@@ -1744,7 +1754,24 @@ export async function runDream(args: CliArgs): Promise<StageResult> {
// only when we can't derive the source id (not in a git repo).
const root = repoRoot();
const sourceId = root ? resolveCodeSourceId(root, gbrainEnv) : null;
const dreamArgs = sourceId ? ["dream", "--source", sourceId] : ["dream"];
// A7 (#2783): the call graph needs only the resolve_symbol_edges phase. A
// full `gbrain dream` runs every maintenance phase, LLM ones included
// (~35 min). Scope it when the installed gbrain can; never fall back to
// the full cycle on its own.
if (!gbrainSupportsSymbolEdgePhase()) {
return {
name: "dream",
ran: false,
ok: true,
duration_ms: Date.now() - t0,
summary:
"skipped — the installed gbrain cannot run only the resolve_symbol_edges phase, and the full dream cycle " +
`costs about 35 minutes (LLM phases). Upgrade with gstack-gbrain-install, or run it yourself: gbrain dream${sourceId ? ` --source ${sourceId}` : ""}`,
};
}
const dreamArgs = sourceId
? ["dream", "--source", sourceId, "--phase", "resolve_symbol_edges"]
: ["dream", "--phase", "resolve_symbol_edges"];
// spawnGbrain seeds DATABASE_URL from gbrain's config via buildGbrainEnv.
//
+19
View File
@@ -453,6 +453,25 @@ with `gbrain sources remove <id>` only if you are sure.
---
## `dream: skipped — the installed gbrain cannot run only the resolve_symbol_edges phase, and the full dream cycle costs about 35 minutes (LLM phases).`
**Problem.** `/sync-gbrain --dream` (or `--full`) did not build the call
graph.
**Cause.** The call graph needs only gbrain's `resolve_symbol_edges` phase.
The installed gbrain cannot run that phase alone (no `dream --phase`, or no
such phase), and gstack no longer starts gbrain's full maintenance cycle on
its own: it takes about 35 minutes and runs LLM phases.
**Fix.**
```bash
gstack-gbrain-install # upgrade gbrain, then:
/sync-gbrain --dream
```
Or run the full cycle yourself when you have the time: `gbrain dream --source <id>`.
---
## Nothing is syncing but I expect it to
**Not an error, but a common gotcha.** Check in order:
+5 -4
View File
@@ -47,8 +47,8 @@ When the user types `/sync-gbrain`, run this skill. Argument modes (parsed by
the skill itself, not a dispatcher binary):
- `/sync-gbrain` — incremental sync (default; mtime fast-path; ~50ms steady-state)
- `/sync-gbrain --full` — full code reindex via `gbrain reindex-code` (~25-35 min on a big repo). Auto-builds the call graph (`gbrain dream`) **only when it was never built**.
- `/sync-gbrain --dream` — build this source's call graph (`gbrain code-callers`/`code-callees`) via a source-scoped `gbrain dream --source <id>` cycle; ~minutes; runs lock-free after the sync stages. Always forces, even if already built. Only produces a graph on a code-aware schema pack; otherwise the run reports a WARN explaining why the graph is still empty.
- `/sync-gbrain --full` — full code reindex via `gbrain reindex-code` (~25-35 min on a big repo). Auto-builds the call graph (`gbrain dream --phase resolve_symbol_edges`) **only when it was never built**.
- `/sync-gbrain --dream` — build this source's call graph (`gbrain code-callers`/`code-callees`) via `gbrain dream --source <id> --phase resolve_symbol_edges`; ~minutes; runs lock-free after the sync stages. Always forces, even if already built. Runs only that phase, never gbrain's full maintenance cycle (about 35 minutes with LLM phases); if the installed gbrain cannot scope the phase, the dream row says so and nothing runs. Only produces a graph on a code-aware schema pack; otherwise the run reports a WARN explaining why the graph is still empty.
- `/sync-gbrain --no-dream` — skip the dream cycle that `--full` would otherwise auto-run.
- `/sync-gbrain --code-only` — only run the code stage; skip memory + brain-sync
- `/sync-gbrain --dry-run` — preview what would sync; no writes anywhere
@@ -346,8 +346,9 @@ If `CYCLE == never` AND the user did NOT pass `--dream`/`--full` AND Step 3
>
> ELI10: `gbrain code-callers`/`code-callees` (who calls this function / what it
> calls) return nothing until the `resolve_symbol_edges` phase runs for this
> source. `gbrain dream --source <this source>` runs it (scoped to this
> worktree's code, takes a few minutes). It only produces a graph if this
> source. `gbrain dream --source <this source> --phase resolve_symbol_edges`
> runs only that phase (scoped to this worktree's code, takes a few minutes,
> not the full ~35-minute maintenance cycle). It only produces a graph if this
> source's schema pack extracts code symbols; if it doesn't, the run completes
> but the graph stays empty and the dream row will say so.
>
+52 -1
View File
@@ -14,7 +14,7 @@
*/
import { describe, it, expect, afterEach } from "bun:test";
import { mkdtempSync, existsSync, writeFileSync, utimesSync, rmSync } from "fs";
import { mkdtempSync, existsSync, writeFileSync, utimesSync, rmSync, mkdirSync, readFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { usePrivateStateRoot } from "./helpers/private-state-root";
@@ -279,3 +279,54 @@ describe("formatStage — WARN render", () => {
expect(formatStage({ ...base, ran: false, ok: true })).toContain("SKIP");
});
});
// A7 (#2783): the forced dream runs only the resolve_symbol_edges phase, and
// only when the installed gbrain can scope it (detected, not assumed).
describe("dream stage scopes gbrain dream to resolve_symbol_edges (A7)", () => {
function sandbox(phaseSupport: "phase" | "no-phase-flag" | "unknown-phase") {
const home = mkdtempSync(join(tmpdir(), "gstack-dream-phase-"));
const bin = join(home, "bin");
const cwd = join(home, "not-a-repo");
mkdirSync(join(home, ".gbrain"), { recursive: true });
mkdirSync(bin, { recursive: true });
mkdirSync(cwd, { recursive: true });
writeFileSync(join(home, ".gbrain", "config.json"), JSON.stringify({ engine: "pglite", database_path: join(home, ".gbrain", "brain.pglite") }));
const help = phaseSupport === "no-phase-flag" ? "Usage: gbrain dream [--source <id>]" : " --phase <name> Run only the named phase(s).";
writeFileSync(join(bin, "gbrain"), `#!/bin/sh
echo "$*" >> "${home}/calls.log"
case "$1" in
--version) echo "gbrain 0.60.37.0" ;;
sources) echo '{"sources":[]}' ;;
dream)
if [ "${phaseSupport}" = "unknown-phase" ]; then echo 'Unknown phase "resolve_symbol_edges".' >&2; exit 1; fi
case " $* " in *" --help "*) echo "${help}"; exit 0 ;; esac
echo "[cycle] resolve_symbol_edges: resolved 4 edges" ;;
*) exit 1 ;;
esac
`, { mode: 0o755 });
const r = spawnSync(process.execPath, [SCRIPT, "--dream", "--no-code", "--no-memory", "--no-brain-sync"], {
cwd,
encoding: "utf-8",
timeout: 60_000,
env: { ...process.env, HOME: home, GSTACK_HOME: join(home, ".gstack"), GBRAIN_HOME: "", PATH: `${bin}:/usr/bin:/bin` },
});
const calls = readFileSync(join(home, "calls.log"), "utf-8").trim().split("\n").filter((l) => l.startsWith("dream"));
rmSync(home, { recursive: true, force: true });
return { out: (r.stdout || "") + (r.stderr || ""), calls };
}
it("runs `gbrain dream --phase resolve_symbol_edges` when gbrain supports it, never the full cycle", () => {
const { calls } = sandbox("phase");
const cycles = calls.filter((c) => !c.includes("--help"));
expect(cycles).toEqual(["dream --phase resolve_symbol_edges"]);
});
for (const kind of ["no-phase-flag", "unknown-phase"] as const) {
it(`skips the forced dream and names the 35-minute full cycle when gbrain cannot scope it (${kind})`, () => {
const { out, calls } = sandbox(kind);
expect(calls.every((c) => c.includes("--help"))).toBe(true);
expect(out).toContain("full dream cycle costs about 35 minutes");
expect(out).toContain("gstack-gbrain-install");
});
}
});