implement six-skill gstack 2 runtime

This commit is contained in:
Sinabina
2026-07-17 11:08:14 -07:00
parent ce37bd36a9
commit b6572ebbb7
455 changed files with 108945 additions and 2622 deletions
+16 -5
View File
@@ -722,7 +722,7 @@ export class BrowserManager {
this.consecutiveFailures = 0;
}
async close() {
async close(timeoutMs = 5000) {
if (this.browser || (this.connectionMode === 'headed' && this.context)) {
if (this.connectionMode === 'headed') {
// Headed/persistent context mode: close the context (which closes the browser)
@@ -730,14 +730,14 @@ export class BrowserManager {
if (this.browser) this.browser.removeAllListeners('disconnected');
await Promise.race([
this.context ? this.context.close() : Promise.resolve(),
new Promise(resolve => setTimeout(resolve, 5000)),
new Promise(resolve => setTimeout(resolve, timeoutMs)),
]).catch(() => {});
} else {
// Launched mode: close the browser we spawned
this.browser.removeAllListeners('disconnected');
await Promise.race([
this.browser.close(),
new Promise(resolve => setTimeout(resolve, 5000)),
new Promise(resolve => setTimeout(resolve, timeoutMs)),
]).catch(() => {});
}
this.browser = null;
@@ -797,6 +797,11 @@ export class BrowserManager {
const tabId = id ?? this.activeTabId;
const page = this.pages.get(tabId);
if (!page) throw new Error(`Tab ${tabId} not found`);
// Capture before page.close(): Playwright may synchronously deliver the
// close event, whose map cleanup changes activeTabId to 0. The public
// closeTab promise still owns the invariant that closing the active last
// tab leaves one usable blank tab.
const wasActive = tabId === this.activeTabId;
await page.close();
this.pages.delete(tabId);
@@ -804,7 +809,7 @@ export class BrowserManager {
this.tabOwnership.delete(tabId);
// Switch to another tab if we closed the active one
if (tabId === this.activeTabId) {
if (wasActive) {
const remaining = [...this.pages.keys()];
if (remaining.length > 0) {
this.activeTabId = remaining[remaining.length - 1];
@@ -1568,9 +1573,15 @@ export class BrowserManager {
console.log('[browse] Handoff: extension not found — headed mode without side panel');
}
const userDataDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
const userDataDir = resolveChromiumProfile();
fs.mkdirSync(userDataDir, { recursive: true });
// The handoff profile follows the same host-neutral resolution and
// stale-lock cleanup contract as launchHeaded(). The current browser is
// headless and does not own this persistent profile, so cleanup cannot
// disrupt the live rollback path retained below.
cleanSingletonLocks(userDataDir);
// T1: same automation-tell-stripping defaults as launchHeaded().
// The handoff path (headless → headed re-launch) takes the same
// anti-detection posture.
+7 -13
View File
@@ -10,11 +10,15 @@
* 1. Prefer node on PATH + a bundled JS entry at
* browse/dist/security-sidecar.js (built by package.json's
* build:security-sidecar script).
* 2. Dev fallback: node + browse/src/security-sidecar-entry.ts via tsx
* (only available in the source checkout, not the compiled install).
* 3. If Node is missing or no entry resolves, return null. The /pty-inject-scan
* 2. If Node is missing or no compiled entry resolves, return null. The
* /pty-inject-scan
* endpoint then responds with l4 { available: false } and the extension
* degrades to WARN+confirm (D7).
*
* A plain-Node TypeScript fallback is intentionally not offered. It was not
* executable on the supported Node 18 floor and, if partially executed by a
* newer Node, could begin downloading local model weights before failing.
* GStack 2 does not bundle that model runtime or its weights.
*/
import { existsSync } from "fs";
@@ -46,9 +50,6 @@ function browseRoot(): string {
if (existsSync(join(candidate, "browse", "dist", "security-sidecar.js"))) {
return candidate;
}
if (existsSync(join(candidate, "src", "security-sidecar-entry.ts"))) {
return candidate;
}
const next = dirname(candidate);
if (next === candidate) break;
candidate = next;
@@ -67,12 +68,5 @@ export function findSecuritySidecar(): SidecarLocation | null {
return { node, entry: compiled, mode: "compiled" };
}
// Dev fallback. Compiled installs won't have src/ on disk so this only
// resolves when running from the source checkout.
const devEntry = join(root, "src", "security-sidecar-entry.ts");
if (existsSync(devEntry)) {
return { node, entry: devEntry, mode: "dev" };
}
return null;
}
+5 -2
View File
@@ -421,14 +421,17 @@ export async function handleMetaCommand(
}
case 'stop': {
await shutdown();
// Return the acknowledgement before closing the listener. Shutting down
// inline resets the CLI's fetch, which it reasonably interprets as a
// crash and then restarts the daemon it was asked to stop.
setTimeout(() => { void shutdown(); }, 25).unref?.();
return 'Server stopped';
}
case 'restart': {
// Signal that we want a restart — the CLI will detect exit and restart
console.log('[browse] Restart requested. Exiting for CLI to restart.');
await shutdown();
setTimeout(() => { void shutdown(); }, 25).unref?.();
return 'Restarting...';
}