mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-18 19:02:18 +02:00
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:
co-authored by
Sina Matian
Claude Fable 5
parent
225e6e4ccd
commit
329d8d6921
@@ -34,9 +34,9 @@
|
|||||||
* gstack-brain-context-load --quiet
|
* gstack-brain-context-load --quiet
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readFileSync, statSync, readdirSync } from "fs";
|
import { existsSync, readFileSync, statSync, readdirSync, accessSync, constants } from "fs";
|
||||||
import { join, dirname, basename, resolve } from "path";
|
import { join, dirname, basename, resolve, delimiter } from "path";
|
||||||
import { execFileSync, spawnSync } from "child_process";
|
import { spawnSync } from "child_process";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
|
|
||||||
import { parseSkillManifest, type GbrainManifest, type GbrainManifestQuery, withErrorContext } from "../lib/gstack-memory-helpers";
|
import { parseSkillManifest, type GbrainManifest, type GbrainManifestQuery, withErrorContext } from "../lib/gstack-memory-helpers";
|
||||||
@@ -68,7 +68,9 @@ interface QueryResult {
|
|||||||
|
|
||||||
const HOME = homedir();
|
const HOME = homedir();
|
||||||
const GSTACK_HOME = process.env.GSTACK_HOME || join(HOME, ".gstack");
|
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
|
const PAGE_SIZE_CAP = 10 * 1024; // 10KB per query result before truncation
|
||||||
|
|
||||||
// ── CLI ────────────────────────────────────────────────────────────────────
|
// ── CLI ────────────────────────────────────────────────────────────────────
|
||||||
@@ -190,16 +192,28 @@ function resolveSkillFile(args: CliArgs): string | null {
|
|||||||
|
|
||||||
// ── Dispatchers ────────────────────────────────────────────────────────────
|
// ── Dispatchers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let gbrainOnPath: boolean | null = null;
|
||||||
|
|
||||||
function gbrainAvailable(): boolean {
|
function gbrainAvailable(): boolean {
|
||||||
try {
|
// Stat-based PATH scan, memoized. Spawning `gbrain --version` under the
|
||||||
execFileSync("gbrain", ["--version"], {
|
// 500ms budget misreported gbrain as missing whenever a cold process spawn
|
||||||
stdio: "ignore",
|
// exceeded the timeout (loaded machine, node-based CLI cold start), and
|
||||||
timeout: MCP_TIMEOUT_MS,
|
// re-probing per query burned 3x the budget before any real work.
|
||||||
});
|
if (gbrainOnPath !== null) return gbrainOnPath;
|
||||||
return true;
|
const exts = process.platform === "win32"
|
||||||
} catch {
|
? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";")
|
||||||
return false;
|
: [""];
|
||||||
}
|
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 {
|
function dispatchVector(q: GbrainManifestQuery, args: CliArgs): QueryResult {
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ fi
|
|||||||
function prependPath(binDir: string): Record<string, string> {
|
function prependPath(binDir: string): Record<string, string> {
|
||||||
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") || "PATH";
|
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") || "PATH";
|
||||||
const currentPath = process.env[pathKey] || "";
|
const currentPath = process.env[pathKey] || "";
|
||||||
return { [pathKey]: `${binDir}${delimiter}${currentPath}` };
|
return {
|
||||||
|
[pathKey]: `${binDir}${delimiter}${currentPath}`,
|
||||||
|
// Cold process spawns on a loaded machine can exceed the 500ms default
|
||||||
|
// budget; the fake gbrain is instant once spawned, so give it headroom.
|
||||||
|
GSTACK_BRAIN_TIMEOUT_MS: "10000",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("gstack-brain-context-load CLI", () => {
|
describe("gstack-brain-context-load CLI", () => {
|
||||||
@@ -252,6 +257,44 @@ describe("gstack-brain-context-load — graceful gbrain absence", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("manifest filter: blocks reach gbrain as --filter args with template vars resolved (#1687)", () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-"));
|
||||||
|
const binDir = join(dir, "bin");
|
||||||
|
mkdirSync(binDir);
|
||||||
|
writeFakeGbrain(binDir);
|
||||||
|
const skillFile = join(dir, "SKILL.md");
|
||||||
|
writeFileSync(
|
||||||
|
skillFile,
|
||||||
|
`---
|
||||||
|
name: x
|
||||||
|
gbrain:
|
||||||
|
schema: 1
|
||||||
|
context_queries:
|
||||||
|
- id: prior-sessions
|
||||||
|
kind: list
|
||||||
|
filter:
|
||||||
|
type: ceo-plan
|
||||||
|
tags_contains: "repo:{repo_slug}"
|
||||||
|
sort: updated_at_desc
|
||||||
|
limit: 5
|
||||||
|
render_as: "## Prior sessions"
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = runScript(["--skill-file", skillFile, "--repo", "my-test-repo"], prependPath(binDir));
|
||||||
|
expect(r.exitCode).toBe(0);
|
||||||
|
expect(r.stdout).toContain("fake gbrain list_pages");
|
||||||
|
expect(r.stdout).toContain("--filter type=ceo-plan");
|
||||||
|
expect(r.stdout).toContain("--filter tags_contains=repo:my-test-repo");
|
||||||
|
expect(r.stdout).toContain("--sort updated_at_desc");
|
||||||
|
} finally {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("vector + list queries still complete (with SKIP) when gbrain CLI is missing", () => {
|
it("vector + list queries still complete (with SKIP) when gbrain CLI is missing", () => {
|
||||||
// We can't easily un-install gbrain; rely on the helper's own missing-binary
|
// We can't easily un-install gbrain; rely on the helper's own missing-binary
|
||||||
// detection. The default manifest uses kind: list which calls gbrain. If
|
// detection. The default manifest uses kind: list which calls gbrain. If
|
||||||
|
|||||||
Reference in New Issue
Block a user