diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 199b476c1..fbed3a507 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -64,7 +64,10 @@ function createConfig(overrides: Partial = {}): AgentSession }; } -function createSession(configOverrides: Partial = {}): CodexTestSession { +function createSession( + configOverrides: Partial = {}, + options: { goalsEnabled?: boolean } = {}, +): CodexTestSession { const session = new __codexAppServerInternals.CodexAppServerAgentSession( createConfig(configOverrides), null, @@ -72,6 +75,9 @@ function createSession(configOverrides: Partial = {}): Codex () => { throw new Error("Test session cannot spawn Codex app-server"); }, + {}, + false, + options.goalsEnabled === true, ) as CodexTestSession; session.connected = true; session.currentThreadId = "test-thread"; @@ -887,6 +893,45 @@ describe("Codex app-server provider", () => { ]); }); + test("appends blank-line spacing to /goal status messages", async () => { + const requests: Array<{ method: string; params: unknown }> = []; + const session = createSession({}, { goalsEnabled: true }); + session.client = { + request: vi.fn(async (method: string, params: unknown) => { + requests.push({ method, params }); + if (method === "thread/loaded/list") { + return { data: ["test-thread"] }; + } + return {}; + }), + }; + + const handler = session.tryHandleOutOfBand?.("/goal ship feature"); + expect(handler).not.toBeNull(); + + const events: AgentStreamEvent[] = []; + await handler?.run({ emit: (event) => events.push(event) }); + + expect(requests).toContainEqual({ + method: "thread/goal/set", + params: { + threadId: "test-thread", + objective: "ship feature", + status: "active", + }, + }); + expect(events).toEqual([ + { + type: "timeline", + provider: "codex", + item: { + type: "assistant_message", + text: "Goal set: ship feature\n\n", + }, + }, + ]); + }); + test("maps question responses from headers back to question ids and completes the tool call", async () => { const session = createSession(); const events: AgentStreamEvent[] = []; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 9fa49eea2..89644968c 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -141,6 +141,10 @@ function parseGoalSubcommand(args: string | undefined): GoalSubcommand { return { kind: "set", objective: trimmed }; } +function formatOutOfBandStatusMessage(text: string): string { + return `${text.replace(/\n+$/u, "")}\n\n`; +} + const CODEX_APP_SERVER_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: true, @@ -3609,7 +3613,7 @@ class CodexAppServerAgentSession implements AgentSession { const subcommand = parseGoalSubcommand(parsed.args); return { run: async ({ emit }) => { - const text = await this.executeGoalSubcommand(subcommand); + const text = formatOutOfBandStatusMessage(await this.executeGoalSubcommand(subcommand)); emit({ type: "timeline", provider: CODEX_PROVIDER,