Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/local-server/src/codex-session-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.",
}
: {}),
};
}

Expand Down
4 changes: 3 additions & 1 deletion apps/local-server/src/preview-manager-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ async function optionalRead(path: string): Promise<string | null> {
}

async function syncFile(path: string): Promise<void> {
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 {
Expand Down Expand Up @@ -551,6 +552,7 @@ export class PreviewManagerService {
{
cwd: worktreePath,
shell: false,
windowsHide: true,
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
env: processEnvironment(port),
Expand Down
1 change: 1 addition & 0 deletions apps/local-server/src/repository-git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function workspaceCommand(
): Promise<string> {
try {
const { stdout } = await execute(executable, [...args], {
windowsHide: true,
cwd,
encoding: "utf8",
timeout: 60_000,
Expand Down
1 change: 1 addition & 0 deletions apps/local-server/src/workstream-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ function contained(parent: string, child: string): boolean {

async function git(worktree: string, args: readonly string[]): Promise<string> {
const { stdout } = await executeFile("git", [...args], {
windowsHide: true,
cwd: worktree,
encoding: "utf8",
timeout: 10_000,
Expand Down
1 change: 1 addition & 0 deletions apps/local-server/src/worktree-authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,7 @@ export class WorktreeAuthority {
);
return await new Promise<GitResult>((resolveResult, reject) => {
const child = spawn("git", [...args], {
windowsHide: true,
shell: false,
stdio: ["ignore", "pipe", "pipe"],
env: {
Expand Down
67 changes: 66 additions & 1 deletion apps/local-server/test/phase19-codex-session-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,8 +56,32 @@ type Fixture = {
descendantPid(): Promise<number>;
};

const codexLaunches = vi.hoisted(() => [] as Array<{ windowsHide?: boolean }>);
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawn: (...args: Parameters<typeof actual.spawn>) => {
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) => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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:
Expand Down
80 changes: 80 additions & 0 deletions apps/local-server/test/preview-manager-persistence.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import("node:fs/promises")>();
return {
...actual,
open: async (...args: Parameters<typeof actual.open>) => {
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 });
}
});
24 changes: 24 additions & 0 deletions apps/local-server/test/preview-manager-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import("node:child_process")>();
return {
...actual,
spawn: (...args: Parameters<typeof actual.spawn>) => {
spawnOptions.push({ windowsHide: args[2]?.windowsHide });
return actual.spawn(...args);
},
};
});

const roots: string[] = [];
const services: PreviewManagerService[] = [];

Expand Down Expand Up @@ -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());
Expand Down
36 changes: 35 additions & 1 deletion apps/local-server/test/repository-git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import("node:child_process")>();
const { promisify: promisifyFunction } = await import("node:util");
const run = promisifyFunction(actual.execFile);
const wrapped = Object.assign(
(...args: Parameters<typeof actual.execFile>) => 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<typeof createLocalServer>[] = [];
Expand All @@ -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);
Expand Down
Loading
Loading