From e2a32f7818f243db20a998760d1aebdfb934f34c Mon Sep 17 00:00:00 2001 From: MelaBuilt AI <282087817+MelaBuilt-AI@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:09:27 -0400 Subject: [PATCH] fix(windows): preserve preview persistence and hide native tool consoles --- .../local-server/src/codex-session-adapter.ts | 13 +++ .../src/preview-manager-service.ts | 4 +- apps/local-server/src/repository-git.ts | 1 + apps/local-server/src/workstream-service.ts | 1 + apps/local-server/src/worktree-authority.ts | 1 + .../phase19-codex-session-adapter.test.ts | 67 +++++++++++++++- .../test/preview-manager-persistence.test.ts | 80 +++++++++++++++++++ .../test/preview-manager-service.test.ts | 24 ++++++ apps/local-server/test/repository-git.test.ts | 36 ++++++++- .../test/workstream-feature-loop.test.ts | 69 ++++++++++++++-- 10 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 apps/local-server/test/preview-manager-persistence.test.ts diff --git a/apps/local-server/src/codex-session-adapter.ts b/apps/local-server/src/codex-session-adapter.ts index ec38ea8..677ac25 100644 --- a/apps/local-server/src/codex-session-adapter.ts +++ b/apps/local-server/src/codex-session-adapter.ts @@ -335,6 +335,7 @@ export class CodexSessionAdapter implements AgentAdapter { child = bridged ?? spawn(installation.executablePath, args, { + windowsHide: true, cwd: options.workingDirectory ?? installation.nativeSessionRoot, env: processEnvironment(options.runtimeHome), detached: true, @@ -558,6 +559,8 @@ export class CodexSessionAdapter implements AgentAdapter { >(); let eventDispatch = Promise.resolve(); let toolEventCount = 0; + let toolFailed = false; + let toolCompleted = false; const emit = (event: AdapterTurnEvent) => { // Raw bytes and text bytes bound fragments; count only retained tool activity. if ( @@ -656,6 +659,10 @@ export class CodexSessionAdapter implements AgentAdapter { item.status === "failed" || (typeof item.exit_code === "number" && item.exit_code !== 0) || (item.error !== undefined && item.error !== null); + if (event.type === "item.completed") { + if (failed) toolFailed = true; + else toolCompleted = true; + } const activityId = typeof item.id === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(item.id) @@ -697,6 +704,12 @@ export class CodexSessionAdapter implements AgentAdapter { finalText: finalText as string, deltas, sessionRef: seenSessionId as string, + ...(toolFailed && !toolCompleted + ? { + blockedReason: + "Codex tool execution failed; no tool completed successfully. Review the agent response before retrying.", + } + : {}), }; } diff --git a/apps/local-server/src/preview-manager-service.ts b/apps/local-server/src/preview-manager-service.ts index 320d7d3..50ed908 100644 --- a/apps/local-server/src/preview-manager-service.ts +++ b/apps/local-server/src/preview-manager-service.ts @@ -268,7 +268,8 @@ async function optionalRead(path: string): Promise { } async function syncFile(path: string): Promise { - const handle = await open(path, "r"); + // Windows requires write access to flush; r+ preserves the copied backup. + const handle = await open(path, "r+"); try { await handle.sync(); } finally { @@ -551,6 +552,7 @@ export class PreviewManagerService { { cwd: worktreePath, shell: false, + windowsHide: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], env: processEnvironment(port), diff --git a/apps/local-server/src/repository-git.ts b/apps/local-server/src/repository-git.ts index f7eb29e..a210409 100644 --- a/apps/local-server/src/repository-git.ts +++ b/apps/local-server/src/repository-git.ts @@ -15,6 +15,7 @@ export async function workspaceCommand( ): Promise { try { const { stdout } = await execute(executable, [...args], { + windowsHide: true, cwd, encoding: "utf8", timeout: 60_000, diff --git a/apps/local-server/src/workstream-service.ts b/apps/local-server/src/workstream-service.ts index ec14d58..d4ad203 100644 --- a/apps/local-server/src/workstream-service.ts +++ b/apps/local-server/src/workstream-service.ts @@ -454,6 +454,7 @@ function contained(parent: string, child: string): boolean { async function git(worktree: string, args: readonly string[]): Promise { const { stdout } = await executeFile("git", [...args], { + windowsHide: true, cwd: worktree, encoding: "utf8", timeout: 10_000, diff --git a/apps/local-server/src/worktree-authority.ts b/apps/local-server/src/worktree-authority.ts index fcb7d24..b6813a4 100644 --- a/apps/local-server/src/worktree-authority.ts +++ b/apps/local-server/src/worktree-authority.ts @@ -946,6 +946,7 @@ export class WorktreeAuthority { ); return await new Promise((resolveResult, reject) => { const child = spawn("git", [...args], { + windowsHide: true, shell: false, stdio: ["ignore", "pipe", "pipe"], env: { diff --git a/apps/local-server/test/phase19-codex-session-adapter.test.ts b/apps/local-server/test/phase19-codex-session-adapter.test.ts index 1fe96cc..cf9606b 100644 --- a/apps/local-server/test/phase19-codex-session-adapter.test.ts +++ b/apps/local-server/test/phase19-codex-session-adapter.test.ts @@ -21,6 +21,8 @@ import { type FixtureControl = { readonly fragmented?: boolean; readonly toolFlood?: boolean; + readonly toolsFail?: boolean; + readonly toolRecovery?: boolean; readonly version?: string; readonly invalidHelp?: boolean; readonly attestHang?: boolean; @@ -54,8 +56,32 @@ type Fixture = { descendantPid(): Promise; }; +const codexLaunches = vi.hoisted(() => [] as Array<{ windowsHide?: boolean }>); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: (...args: Parameters) => { + codexLaunches.push(args[2] as { windowsHide?: boolean }); + return actual.spawn(...args); + }, + }; +}); + const temporaryRoots: string[] = []; +it("hides direct native Codex attestation and turn windows", async () => { + const fixture = await fixtureExecutable(); + const native = adapter(fixture); + codexLaunches.length = 0; + await native.attest(); + await native.createWorldSession("hidden-console"); + expect(codexLaunches.length).toBeGreaterThan(0); + expect(codexLaunches.every((options) => options.windowsHide === true)).toBe( + true, + ); +}); + it.each([false, true])( "enables live web research on direct, relay and coding resumes without opening shell networking (native profile: %s)", async (nativeProfile) => { @@ -93,6 +119,33 @@ it.each([false, true])( }, ); +it.each([false, true])( + "reports all-tool failure without quarantining the conversation (recovery: %s)", + async (recovery) => { + const fixture = await fixtureExecutable({ + toolsFail: true, + toolRecovery: recovery, + }); + const native = adapter(fixture); + const session = await native.createWorldSession("blocked-work"); + const result = await native.sendText(session.id, "create homepage", { + mode: "collaborate", + rootSessionRef: session.id, + workingDirectory: fixture.nativeSessionRoot, + }); + expect(result.finalText).toBe("fixture complete"); + if (recovery) expect(result.blockedReason).toBeUndefined(); + else + expect(result.blockedReason).toBe( + "Codex tool execution failed; no tool completed successfully. Review the agent response before retrying.", + ); + expect(JSON.stringify(result)).not.toContain("PRIVATE_CANARY"); + await expect( + native.attach(session.id, { worldInstanceId: "blocked-work" }), + ).resolves.toMatchObject({ id: session.id }); + }, +); + it("still bounds normalized tool activity independently of text fragments", async () => { const fixture = await fixtureExecutable({ toolFlood: true }); const native = adapter(fixture); @@ -208,7 +261,19 @@ if (isResume) { threadId = "11111111-1111-4111-8111-" + String(counter).padStart(12, "0"); } -const emit = (event) => process.stdout.write(JSON.stringify(event) + "\\n"); +const emit = (event) => { + if (control.toolsFail && event.type === "item.completed" && event.item?.type !== "agent_message") { + event.item.status = "failed"; + event.item.error = { message: "helper_unknown_error: setup refresh had errors PRIVATE_CANARY" }; + if (event.item.type === "command_execution") event.item.exit_code = 1; + if (control.toolRecovery && event.item.id === "item-compound") { + event.item.status = "completed"; + event.item.exit_code = 0; + delete event.item.error; + } + } + process.stdout.write(JSON.stringify(event) + "\\n"); +}; emit({ type: "thread.started", thread_id: diff --git a/apps/local-server/test/preview-manager-persistence.test.ts b/apps/local-server/test/preview-manager-persistence.test.ts new file mode 100644 index 0000000..d8ec6ea --- /dev/null +++ b/apps/local-server/test/preview-manager-persistence.test.ts @@ -0,0 +1,80 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { expect, it, vi } from "vitest"; + +import { PreviewManagerService } from "../src/preview-manager-service.js"; + +vi.mock("node:fs/promises", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + open: async (...args: Parameters) => { + const handle = await actual.open(...args); + if (args[1] === "r") { + // Windows rejects flushing a handle without write access. + handle.sync = async () => { + throw Object.assign( + new Error("EPERM: operation not permitted, fsync"), + { + code: "EPERM", + syscall: "fsync", + }, + ); + }; + } + return handle; + }, + }; +}); + +it("durably updates approved recipes with Windows flush semantics without truncating the backup", async () => { + const directory = await mkdtemp(join(tmpdir(), "aiw-preview-persistence-")); + const service = new PreviewManagerService({ + directory, + resolveWorkstream: () => { + throw new Error("Recipe persistence must not dispatch a Workstream"); + }, + }); + const recipe = { + requestId: "approve-one", + correlationId: "approve-one", + recipeId: "static-site", + expectedRevision: null, + repositoryId: "repository-one", + label: "Static website", + executable: process.execPath, + args: ["preview.mjs", "{port}"], + readinessPath: "/", + browserPath: "/", + }; + try { + await service.approveRecipe(recipe); + const original = await readFile( + join(directory, "preview-manager.current.json"), + "utf8", + ); + await service.approveRecipe({ + ...recipe, + requestId: "approve-two", + correlationId: "approve-two", + expectedRevision: 1, + label: "Updated static website", + }); + expect( + await readFile(join(directory, "preview-manager.previous.json"), "utf8"), + ).toBe(original); + const saved = JSON.parse( + await readFile(join(directory, "preview-manager.current.json"), "utf8"), + ); + expect(saved.generation).toBe(2); + expect(saved.payload.recipes[0]).toMatchObject({ + revision: 2, + label: "Updated static website", + }); + } finally { + // No preview process is launched by recipe-only persistence. + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/apps/local-server/test/preview-manager-service.test.ts b/apps/local-server/test/preview-manager-service.test.ts index e11cffa..bfb2a59 100644 --- a/apps/local-server/test/preview-manager-service.test.ts +++ b/apps/local-server/test/preview-manager-service.test.ts @@ -9,6 +9,18 @@ import { type PreviewWorkstreamBinding, } from "../src/preview-manager-service.js"; +const spawnOptions = vi.hoisted(() => [] as Array<{ windowsHide?: boolean }>); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + spawn: (...args: Parameters) => { + spawnOptions.push({ windowsHide: args[2]?.windowsHide }); + return actual.spawn(...args); + }, + }; +}); + const roots: string[] = []; const services: PreviewManagerService[] = []; @@ -117,6 +129,18 @@ afterEach(async () => { }); describe("PreviewManagerService", () => { + it("hides the preview console while serving the owned worktree", async () => { + const value = await fixture(); + await value.service.approveRecipe(recipe()); + spawnOptions.length = 0; + const launched = (await value.service.start(start())).preview; + expect(launched.state).toBe("ready"); + expect(await (await fetch(launched.url!)).text()).toContain( + "exact worktree preview", + ); + expect(spawnOptions).toEqual([{ windowsHide: true }]); + }); + it("relaunches a saved approved preview after shutdown, but not an explicitly stopped preview", async () => { const fixtureState = await fixture(); await fixtureState.service.approveRecipe(recipe()); diff --git a/apps/local-server/test/repository-git.test.ts b/apps/local-server/test/repository-git.test.ts index d54d5cc..7e760d9 100644 --- a/apps/local-server/test/repository-git.test.ts +++ b/apps/local-server/test/repository-git.test.ts @@ -3,13 +3,34 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; -import { afterEach, expect, it } from "vitest"; +import { afterEach, expect, it, vi } from "vitest"; import { RepositoryIntakeService } from "../src/repository-intake.js"; import { createLocalServer } from "../src/server.js"; import { RepositoryGitService, workspaceCommand, } from "../src/repository-git.js"; +const commandOptions = vi.hoisted(() => [] as Array<{ windowsHide?: boolean }>); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + const { promisify: promisifyFunction } = await import("node:util"); + const run = promisifyFunction(actual.execFile); + const wrapped = Object.assign( + (...args: Parameters) => actual.execFile(...args), + actual.execFile, + ); + Object.defineProperty(wrapped, promisifyFunction.custom, { + value: async ( + file: string, + args: string[], + options: { windowsHide?: boolean }, + ) => { + commandOptions.push({ windowsHide: options?.windowsHide }); + return run(file, args, options); + }, + }); + return { ...actual, execFile: wrapped }; +}); const exec = promisify(execFile); const roots: string[] = []; const servers: ReturnType[] = []; @@ -18,6 +39,19 @@ afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); }); +it("hides the Workbench Git initialization process", async () => { + const root = await mkdtemp(join(tmpdir(), "aiw-git-hidden-")); + roots.push(root); + commandOptions.length = 0; + await workspaceCommand(root, "git", ["init", "--initial-branch=main"]); + expect(commandOptions).toEqual([ + expect.objectContaining({ windowsHide: true }), + ]); + expect(await readFile(join(root, ".git/HEAD"), "utf8")).toContain( + "refs/heads/main", + ); +}); + async function fixture() { const root = await mkdtemp(join(tmpdir(), "aiw-repo-git-")); roots.push(root); diff --git a/apps/local-server/test/workstream-feature-loop.test.ts b/apps/local-server/test/workstream-feature-loop.test.ts index 1055c85..24a22be 100644 --- a/apps/local-server/test/workstream-feature-loop.test.ts +++ b/apps/local-server/test/workstream-feature-loop.test.ts @@ -21,6 +21,38 @@ import { type WorkstreamAgentReference, } from "../src/workstream-service.js"; +const backgroundLaunches = vi.hoisted( + () => [] as Array<{ windowsHide?: boolean }>, +); +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + const { promisify: promisifyFunction } = await import("node:util"); + const run = promisifyFunction(actual.execFile); + const wrapped = Object.assign( + (...args: Parameters) => actual.execFile(...args), + actual.execFile, + ); + Object.defineProperty(wrapped, promisifyFunction.custom, { + value: async ( + file: string, + args: string[], + options: { windowsHide?: boolean }, + ) => { + if (file === "git") backgroundLaunches.push(options); + return run(file, args, options); + }, + }); + return { + ...actual, + execFile: wrapped, + spawn: (...args: Parameters) => { + if (args[0] === "git") + backgroundLaunches.push(args[2] as { windowsHide?: boolean }); + return actual.spawn(...args); + }, + }; +}); + const executeFile = promisify(execFile); const roots: string[] = []; const services: WorkstreamService[] = []; @@ -185,19 +217,40 @@ afterEach(async () => { }); describe("Workstream feature loop", () => { - it("keeps a completed but permission-blocked dispatch blocked", async () => { + it("hides background Git windows while projecting a Workstream", async () => { const value = await fixture(); - vi.mocked(value.port.dispatch).mockResolvedValueOnce({ - blockedReason: "Claude Code denied a required tool permission.", - }); + backgroundLaunches.length = 0; await value.service.create(createRequest()); await vi.waitFor(async () => - expect((await value.service.current())?.status).toBe("blocked"), - ); - expect((await value.service.current())?.events.at(-1)?.summary).toContain( - "denied a required tool permission", + expect((await value.service.current())?.status).toBe("ready-for-review"), ); + await value.service.current(); + expect(backgroundLaunches.length).toBeGreaterThan(0); + expect( + backgroundLaunches.every((options) => options.windowsHide === true), + ).toBe(true); }); + it.each([ + "Claude Code denied a required tool permission.", + "Codex tool execution failed; no tool completed successfully. Review the agent response before retrying.", + ])( + "keeps a completed but blocked dispatch blocked: %s", + async (blockedReason) => { + const value = await fixture(); + vi.mocked(value.port.dispatch).mockResolvedValueOnce({ blockedReason }); + await value.service.create(createRequest()); + await vi.waitFor(async () => + expect((await value.service.current())?.status).toBe("blocked"), + ); + expect((await value.service.current())?.events.at(-1)?.summary).toContain( + blockedReason, + ); + const persisted = JSON.parse( + await readFile(join(value.store, "workstream.current.json"), "utf8"), + ); + expect(persisted.payload.workstream.status).toBe("blocked"); + }, + ); it("refuses stale, busy and foreign continuation sources before allocating or dispatching", async () => { const value = await fixture();