fix(brain-context): cold-start probe latency permanently disabled gbrain context

gbrainAvailable() spawned gbrain --version under a 500ms budget; a cold CLI
start on a loaded machine blew the timeout, misclassified gbrain as missing,
and every skill session silently ran brainless — plus the per-query re-probe
burned 3x the budget before any real work. Replaced with a memoized
stat-based PATH scan (PATHEXT-aware on Windows) and made the query timeout
overridable via GSTACK_BRAIN_TIMEOUT_MS for loaded CI environments.

Also picks up the fork's manifest-filter coverage (#1687 shape) against the
fake-gbrain harness — passes against our existing filter support.

Ported from time-attack/gstack (GStack 2).

Co-authored-by: Sina Matian <sina@time-attack.dev>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-14 11:55:23 -07:00
co-authored by Sina Matian Claude Fable 5
parent 225e6e4ccd
commit 329d8d6921
2 changed files with 71 additions and 14 deletions
+27 -13
View File
@@ -34,9 +34,9 @@
* gstack-brain-context-load --quiet
*/
import { existsSync, readFileSync, statSync, readdirSync } from "fs";
import { join, dirname, basename, resolve } from "path";
import { execFileSync, spawnSync } from "child_process";
import { existsSync, readFileSync, statSync, readdirSync, accessSync, constants } from "fs";
import { join, dirname, basename, resolve, delimiter } from "path";
import { spawnSync } from "child_process";
import { homedir } from "os";
import { parseSkillManifest, type GbrainManifest, type GbrainManifestQuery, withErrorContext } from "../lib/gstack-memory-helpers";
@@ -68,7 +68,9 @@ interface QueryResult {
const HOME = homedir();
const GSTACK_HOME = process.env.GSTACK_HOME || join(HOME, ".gstack");
const MCP_TIMEOUT_MS = 500;
// 500ms hard cap per Section 1C; overridable for slow/loaded environments
// (test harnesses under CI load, cold CLI starts).
const MCP_TIMEOUT_MS = Math.max(1, parseInt(process.env.GSTACK_BRAIN_TIMEOUT_MS || "", 10) || 500);
const PAGE_SIZE_CAP = 10 * 1024; // 10KB per query result before truncation
// ── CLI ────────────────────────────────────────────────────────────────────
@@ -190,16 +192,28 @@ function resolveSkillFile(args: CliArgs): string | null {
// ── Dispatchers ────────────────────────────────────────────────────────────
let gbrainOnPath: boolean | null = null;
function gbrainAvailable(): boolean {
try {
execFileSync("gbrain", ["--version"], {
stdio: "ignore",
timeout: MCP_TIMEOUT_MS,
});
return true;
} catch {
return false;
}
// Stat-based PATH scan, memoized. Spawning `gbrain --version` under the
// 500ms budget misreported gbrain as missing whenever a cold process spawn
// exceeded the timeout (loaded machine, node-based CLI cold start), and
// re-probing per query burned 3x the budget before any real work.
if (gbrainOnPath !== null) return gbrainOnPath;
const exts = process.platform === "win32"
? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";")
: [""];
gbrainOnPath = (process.env.PATH || "").split(delimiter).some((dir) =>
dir !== "" && exts.some((ext) => {
try {
accessSync(join(dir, `gbrain${ext}`), constants.X_OK);
return true;
} catch {
return false;
}
})
);
return gbrainOnPath;
}
function dispatchVector(q: GbrainManifestQuery, args: CliArgs): QueryResult {