Skip to content
Closed
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
8 changes: 4 additions & 4 deletions apps/code/src/main/services/auth/port-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ import {

@injectable()
export class TokenCipherPortAdapter implements IAuthTokenCipher {
encrypt(plaintext: string): string {
return encrypt(plaintext);
encrypt(plaintext: string): Promise<string> {
return Promise.resolve(encrypt(plaintext));
}

decrypt(encrypted: string): string | null {
return decrypt(encrypted);
decrypt(encrypted: string): Promise<string | null> {
return Promise.resolve(decrypt(encrypted));
}
}

Expand Down
14 changes: 14 additions & 0 deletions packages/agent/src/acp-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,22 @@ export const POSTHOG_NOTIFICATIONS = {

/** RTK output-compression token savings tallied at the end of a run */
RTK_SAVINGS: "_posthog/rtk_savings",

/** Latest native Codex goal state, persisted so cold cloud resumes can restore it. */
CODEX_GOAL: "_posthog/codex_goal",
} as const;

export type NativeGoalState = {
objective: string;
status:
| "active"
| "paused"
| "blocked"
| "usageLimited"
| "budgetLimited"
| "complete";
};

/**
* Custom request methods for PostHog-specific operations that need a response
* (request/response, not fire-and-forget). Used with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { sandboxPolicyFor } from "./session-config";

// Required-field invariants the native codex app-server enforces on each request.
const REQUIRED_FIELDS: Record<string, string[]> = {
"thread/goal/clear": ["threadId"],
"thread/goal/get": ["threadId"],
"thread/goal/set": ["threadId"],
"turn/interrupt": ["threadId", "turnId"],
"turn/steer": ["threadId", "input", "expectedTurnId"],
};
Expand Down Expand Up @@ -43,7 +46,12 @@ function makeStubRpc(responses: Record<string, unknown>) {
message: `Invalid request: missing field \`${missing}\``,
};
}
return (responses[method] ?? {}) as T;
const response = responses[method];
return (
typeof response === "function"
? await response(params)
: (response ?? {})
) as T;
},
notify() {},
async close() {},
Expand Down Expand Up @@ -295,6 +303,274 @@ describe("CodexAppServerAgent", () => {
).toHaveLength(1);
});

it.each([
{
label: "reads an empty goal",
prompt: "/goal",
method: "thread/goal/get",
response: { goal: null },
expectedParams: { threadId: "thr_1" },
expectedText: "No goal set. Usage: `/goal <objective>`",
expectedGoal: undefined,
},
{
label: "reads an active goal",
prompt: "/goal",
method: "thread/goal/get",
response: { goal: { objective: "Ship the fix", status: "active" } },
expectedParams: { threadId: "thr_1" },
expectedText: "Goal active: Ship the fix",
expectedGoal: undefined,
},
{
label: "sets a goal",
prompt: "/goal Ship the fix",
method: "thread/goal/set",
response: { goal: { objective: "Ship the fix", status: "active" } },
expectedParams: { threadId: "thr_1", objective: "Ship the fix" },
expectedText: "Goal set: Ship the fix",
expectedGoal: { objective: "Ship the fix", status: "active" },
},
{
label: "clears a goal",
prompt: "/goal clear",
method: "thread/goal/clear",
response: { cleared: true },
expectedParams: { threadId: "thr_1" },
expectedText: "Goal cleared.",
expectedGoal: null,
},
{
label: "pauses a goal",
prompt: "/goal pause",
method: "thread/goal/set",
response: { goal: { objective: "Ship the fix", status: "paused" } },
expectedParams: { threadId: "thr_1", status: "paused" },
expectedText: "Goal paused: Ship the fix",
expectedGoal: { objective: "Ship the fix", status: "paused" },
},
{
label: "resumes a goal",
prompt: "/goal resume",
method: "thread/goal/set",
response: { goal: { objective: "Ship the fix", status: "active" } },
expectedParams: { threadId: "thr_1", status: "active" },
expectedText: "Goal resumed: Ship the fix",
expectedGoal: { objective: "Ship the fix", status: "active" },
},
])("$label without starting a model turn", async (testCase) => {
const stub = makeStubRpc({
"thread/start": { thread: { id: "thr_1" } },
[testCase.method]: testCase.response,
});
const { client, sessionUpdates, extNotifications } = makeFakeClient();
const agent = new CodexAppServerAgent(client, {
processOptions: { binaryPath: "/bundle/codex" },
rpcFactory: stub.factory,
});

await agent.newSession({ cwd: "/repo" } as unknown as NewSessionRequest);
const result = await agent.prompt({
sessionId: "thr_1",
prompt: [{ type: "text", text: testCase.prompt }],
} as unknown as PromptRequest);

expect(result.stopReason).toBe("end_turn");
expect(stub.requests).toContainEqual({
method: testCase.method,
params: testCase.expectedParams,
});
expect(
stub.requests.some((request) => request.method === "turn/start"),
).toBe(false);
expect(sessionUpdates).toContainEqual({
sessionId: "thr_1",
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: testCase.expectedText },
},
});
if (testCase.expectedGoal !== undefined) {
expect(extNotifications).toContainEqual({
method: "_posthog/codex_goal",
params: { goal: testCase.expectedGoal },
});
}
});

it("handles a goal command wrapped in hidden cold-resume context", async () => {
const stub = makeStubRpc({
"thread/start": { thread: { id: "thr_1" } },
"thread/goal/get": {
goal: { objective: "Ship the fix", status: "paused" },
},
});
const { client, sessionUpdates } = makeFakeClient();
const agent = new CodexAppServerAgent(client, {
processOptions: { binaryPath: "/bundle/codex" },
rpcFactory: stub.factory,
});
await agent.newSession({ cwd: "/repo" } as unknown as NewSessionRequest);

await agent.prompt({
sessionId: "thr_1",
prompt: [
{
type: "text",
text: "Previous conversation context",
_meta: { ui: { hidden: true } },
},
{ type: "text", text: "/goal" },
{
type: "text",
text: "Respond to the user above",
_meta: { ui: { hidden: true } },
},
],
} as unknown as PromptRequest);

expect(stub.requests).toContainEqual({
method: "thread/goal/get",
params: { threadId: "thr_1" },
});
expect(
stub.requests.some((request) => request.method === "turn/start"),
).toBe(false);
expect(sessionUpdates).toContainEqual({
sessionId: "thr_1",
update: {
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "/goal" },
},
});
});

it("restores a persisted goal when starting a replacement thread", async () => {
const restoredGoal = { objective: "Ship the fix", status: "paused" };
const stub = makeStubRpc({
"thread/start": { thread: { id: "thr_1" } },
"thread/goal/set": { goal: restoredGoal },
});
const { client, extNotifications } = makeFakeClient();
const agent = new CodexAppServerAgent(client, {
processOptions: { binaryPath: "/bundle/codex" },
rpcFactory: stub.factory,
});

await agent.newSession({
cwd: "/repo",
_meta: { nativeGoal: restoredGoal },
} as unknown as NewSessionRequest);

expect(stub.requests).toContainEqual({
method: "thread/goal/set",
params: { threadId: "thr_1", ...restoredGoal },
});
expect(extNotifications).toContainEqual({
method: "_posthog/codex_goal",
params: { goal: restoredGoal },
});
});

it("interrupts a native goal turn that was already queued when paused", async () => {
const stub = makeStubRpc({
"thread/start": { thread: { id: "thr_1" } },
"thread/goal/set": {
goal: { objective: "Ship the fix", status: "paused" },
},
});
const { client } = makeFakeClient();
const agent = new CodexAppServerAgent(client, {
processOptions: { binaryPath: "/bundle/codex" },
rpcFactory: stub.factory,
});
await agent.newSession({ cwd: "/repo" } as unknown as NewSessionRequest);

await agent.prompt({
sessionId: "thr_1",
prompt: [{ type: "text", text: "/goal pause" }],
} as unknown as PromptRequest);
stub.emit("turn/started", { turn: { id: "goal_tick_1" } });
await Promise.resolve();

expect(stub.requests).toContainEqual({
method: "turn/interrupt",
params: { threadId: "thr_1", turnId: "goal_tick_1" },
});
});

it("interrupts a native goal turn that started before it was paused", async () => {
const stub = makeStubRpc({
"thread/start": { thread: { id: "thr_1" } },
"thread/goal/set": {
goal: { objective: "Ship the fix", status: "paused" },
},
});
const { client } = makeFakeClient();
const agent = new CodexAppServerAgent(client, {
processOptions: { binaryPath: "/bundle/codex" },
rpcFactory: stub.factory,
});
await agent.newSession({ cwd: "/repo" } as unknown as NewSessionRequest);
stub.emit("turn/started", { turn: { id: "goal_tick_1" } });

await agent.prompt({
sessionId: "thr_1",
prompt: [{ type: "text", text: "/goal pause" }],
} as unknown as PromptRequest);

expect(stub.requests).toContainEqual({
method: "turn/interrupt",
params: { threadId: "thr_1", turnId: "goal_tick_1" },
});
});

it("retries queued goal cancellation after an interrupt failure", async () => {
let interruptAttempts = 0;
const stub = makeStubRpc({
"thread/start": { thread: { id: "thr_1" } },
"thread/goal/set": {
goal: { objective: "Ship the fix", status: "paused" },
},
"turn/interrupt": () => {
interruptAttempts++;
if (interruptAttempts === 1) {
throw new Error("interrupt failed");
}
return {};
},
});
const { client } = makeFakeClient();
const agent = new CodexAppServerAgent(client, {
processOptions: { binaryPath: "/bundle/codex" },
rpcFactory: stub.factory,
});
await agent.newSession({ cwd: "/repo" } as unknown as NewSessionRequest);

await agent.prompt({
sessionId: "thr_1",
prompt: [{ type: "text", text: "/goal pause" }],
} as unknown as PromptRequest);
stub.emit("turn/started", { turn: { id: "goal_tick_1" } });
await Promise.resolve();
await Promise.resolve();
stub.emit("turn/started", { turn: { id: "goal_tick_2" } });
await Promise.resolve();

expect(
stub.requests.filter((request) => request.method === "turn/interrupt"),
).toEqual([
{
method: "turn/interrupt",
params: { threadId: "thr_1", turnId: "goal_tick_1" },
},
{
method: "turn/interrupt",
params: { threadId: "thr_1", turnId: "goal_tick_2" },
},
]);
});

it("includes buffered command output when completion omits aggregatedOutput", async () => {
const stub = makeStubRpc({
initialize: {},
Expand Down Expand Up @@ -1713,6 +1989,7 @@ describe("CodexAppServerAgent", () => {
{
skills: [
{ name: "deploy", description: "Deploy", enabled: true },
{ name: "goal", description: "Duplicate", enabled: true },
{ name: "danger", description: "Disabled", enabled: false },
],
},
Expand All @@ -1731,7 +2008,14 @@ describe("CodexAppServerAgent", () => {
(u: any) => u.update?.sessionUpdate === "available_commands_update",
) as any
)?.update?.availableCommands;
expect(cmds.map((c: { name: string }) => c.name)).toEqual(["deploy"]);
expect(cmds).toEqual([
{
name: "goal",
description: "Set or view the goal for a long-running task",
input: { hint: "[<objective>|clear|pause|resume]" },
},
{ name: "deploy", description: "Deploy" },
]);
});

it("emits _posthog/sdk_session when a taskRunId is present", async () => {
Expand Down
Loading
Loading