From 6f16900b8a232107cca0b58803e5d02dc5cf48b8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 26 May 2026 11:28:22 +0700 Subject: [PATCH] Hide paseo-system prompts from the agent timeline System-injected prompts (finish notifications, schedule fires) are wrapped in a envelope and dispatched as regular user messages to the provider. The app was showing them as user-message turns. Detect the envelope at the manager boundary and skip them during live runs and history replay so only real user input renders. --- .../src/server/agent/agent-manager.test.ts | 117 ++++++++++++++++++ .../server/src/server/agent/agent-manager.ts | 13 ++ .../src/server/agent/agent-prompt.test.ts | 13 +- .../server/src/server/agent/agent-prompt.ts | 20 +-- .../server/src/server/agent/mcp-server.ts | 1 - packages/server/src/server/session.ts | 3 - 6 files changed, 147 insertions(+), 20 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 1b1f7a793..711fb9110 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -9,6 +9,7 @@ import { createTestLogger } from "../../test-utils/test-logger.js"; import { AgentManager, type ManagedAgent } from "./agent-manager.js"; import { AgentStorage } from "./agent-storage.js"; import { PARENT_AGENT_ID_LABEL } from "../../shared/agent-labels.js"; +import { formatSystemNotificationPrompt } from "./agent-prompt.js"; import type { StoredAgentRecord } from "./agent-storage.js"; import type { AgentClient, @@ -390,6 +391,50 @@ class StreamingAssistantClient implements AgentClient { } } +interface FakeCodexEmitterArgs { + turnItems?: AgentTimelineItem[]; + historyItems?: AgentTimelineItem[]; +} + +function fakeCodexEmitting(args: FakeCodexEmitterArgs): AgentClient { + const turnItems = args.turnItems ?? []; + const historyItems = args.historyItems ?? []; + + class FakeCodexSession extends TestAgentSession { + override async startTurn(): Promise<{ turnId: string }> { + const turnId = "turn-fake-codex"; + setTimeout(() => { + this.pushEvent({ type: "turn_started", provider: this.provider, turnId }); + for (const item of turnItems) { + this.pushEvent({ type: "timeline", provider: this.provider, item, turnId }); + } + this.pushEvent({ type: "turn_completed", provider: this.provider, turnId }); + }, 0); + return { turnId }; + } + + override async *streamHistory(): AsyncGenerator { + for (const item of historyItems) { + yield { type: "timeline", provider: this.provider, item }; + } + } + } + + return { + provider: "codex", + capabilities: TEST_CAPABILITIES, + async isAvailable() { + return true; + }, + async createSession(config: AgentSessionConfig) { + return new FakeCodexSession(config); + }, + async resumeSession() { + throw new Error("unused"); + }, + }; +} + const logger = createTestLogger(); test("normalizeConfig injects the provider default model when omitted", async () => { @@ -5419,3 +5464,75 @@ test("listImportablePersistedAgents narrows to the providerFilter when supplied" expect(codexClient.calls).toBe(0); expect(result.map((d) => d.provider)).toEqual(["claude"]); }); + +test("user_message events wrapping a paseo-system envelope are not added to the timeline", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-envelope-live-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + + const codex = fakeCodexEmitting({ + turnItems: [ + { + type: "user_message", + text: formatSystemNotificationPrompt("child finished"), + }, + { type: "user_message", text: "plain user message" }, + ], + }); + + const manager = new AgentManager({ + clients: { codex }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-0000000005a1", + }); + + const snapshot = await manager.createAgent({ provider: "codex", cwd: workdir }); + + await manager.runAgent(snapshot.id, { text: "do something" }); + + const timeline = manager.getTimeline(snapshot.id); + const userMessages = timeline.filter((item) => item.type === "user_message"); + + expect(userMessages).toHaveLength(1); + expect(userMessages[0].text).toBe("plain user message"); +}); + +test("user_message events wrapping a paseo-system envelope are not restored during history replay", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-envelope-history-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + + const codex = fakeCodexEmitting({ + historyItems: [ + { + type: "user_message", + text: formatSystemNotificationPrompt("schedule fired"), + messageId: "msg_history_envelope", + }, + { + type: "user_message", + text: "real user message", + messageId: "msg_history_real", + }, + { type: "assistant_message", text: "reply" }, + ], + }); + + const manager = new AgentManager({ + clients: { codex }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-0000000005a2", + }); + + const snapshot = await manager.createAgent({ provider: "codex", cwd: workdir }); + + await manager.hydrateTimelineFromProvider(snapshot.id); + + const timeline = manager.getTimeline(snapshot.id); + const userMessages = timeline.filter((item) => item.type === "user_message"); + + expect(userMessages).toHaveLength(1); + expect(userMessages[0].text).toBe("real user message"); +}); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 6cf589f2a..ab4d98dda 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -56,6 +56,7 @@ import { ForegroundRunState, type ForegroundTurnWaiter } from "./foreground-run- import { getAgentProviderDefinition } from "./provider-manifest.js"; import { IMPORTABLE_PROVIDERS } from "./provider-registry.js"; import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js"; +import { isSystemInjectedEnvelope } from "./agent-prompt.js"; const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000; const INTERRUPT_SESSION_TIMEOUT_MS = 2_000; @@ -2622,6 +2623,9 @@ export class AgentManager { const historyEvents: Extract[] = []; for await (const event of agent.session.streamHistory()) { if (event.type === "timeline") { + if (event.item.type === "user_message" && isSystemInjectedEnvelope(event.item.text)) { + continue; + } historyEvents.push(event); } } @@ -2657,6 +2661,9 @@ export class AgentManager { if (event.type !== "timeline") { continue; } + if (event.item.type === "user_message" && isSystemInjectedEnvelope(event.item.text)) { + continue; + } this.recordTimeline( agent.id, event.item, @@ -2904,6 +2911,12 @@ export class AgentManager { }): Promise { const { agent, event, options, flags } = params; + if (event.item.type === "user_message" && isSystemInjectedEnvelope(event.item.text)) { + flags.shouldDispatchEvent = false; + flags.shouldNotifyWaiters = false; + return; + } + if (options?.fromHistory) { this.recordTimeline( agent.id, diff --git a/packages/server/src/server/agent/agent-prompt.test.ts b/packages/server/src/server/agent/agent-prompt.test.ts index 9223ed36d..d8eb8eb2a 100644 --- a/packages/server/src/server/agent/agent-prompt.test.ts +++ b/packages/server/src/server/agent/agent-prompt.test.ts @@ -1,11 +1,20 @@ -import { expect, it, vi } from "vitest"; +import { expect, it, test, vi } from "vitest"; import { createTestLogger } from "../../test-utils/test-logger.js"; import { AgentManager } from "./agent-manager.js"; import { AgentStorage } from "./agent-storage.js"; -import { setupFinishNotification } from "./agent-prompt.js"; +import { + formatSystemNotificationPrompt, + isSystemInjectedEnvelope, + setupFinishNotification, +} from "./agent-prompt.js"; import type { AgentManagerEvent, ManagedAgent } from "./agent-manager.js"; +test("isSystemInjectedEnvelope matches the envelope formatSystemNotificationPrompt produces", () => { + expect(isSystemInjectedEnvelope(formatSystemNotificationPrompt("child finished"))).toBe(true); + expect(isSystemInjectedEnvelope("hello world")).toBe(false); +}); + it("does not notify archived callers", async () => { let subscriber: ((event: AgentManagerEvent) => void) | null = null; diff --git a/packages/server/src/server/agent/agent-prompt.ts b/packages/server/src/server/agent/agent-prompt.ts index dc07948e2..97d4afc52 100644 --- a/packages/server/src/server/agent/agent-prompt.ts +++ b/packages/server/src/server/agent/agent-prompt.ts @@ -112,14 +112,18 @@ export function formatSystemNotificationPrompt(reason: string): string { return `\n${reason}\n`; } +const SYSTEM_ENVELOPE_PATTERN = /^\n[\s\S]*\n<\/paseo-system>$/; + +export function isSystemInjectedEnvelope(text: string): boolean { + return SYSTEM_ENVELOPE_PATTERN.test(text); +} + export interface SendPromptToAgentParams { agentManager: AgentManager; agentStorage: AgentStorage; agentId: string; /** Prompt to dispatch to the provider (may include image blocks or wrapped text). */ prompt: AgentPromptInput; - /** Raw user text. Required when userMessageVisible is true. */ - userMessageText?: string; messageId?: string; runOptions?: AgentRunOptions; /** Optional mode to set on the agent before the run starts. */ @@ -130,12 +134,6 @@ export interface SendPromptToAgentParams { * schedule fires, notify-on-finish). */ unarchive?: boolean; - /** - * Default true. When false, the prompt is dispatched to the provider as an - * invisible/system prompt — the app shows no user-message turn. - * Use false for system-injected prompts. - */ - userMessageVisible?: boolean; logger: Logger; } @@ -154,11 +152,6 @@ export async function sendPromptToAgent( params: SendPromptToAgentParams, ): Promise<{ outOfBand: boolean }> { const unarchive = params.unarchive ?? true; - const userMessageVisible = params.userMessageVisible ?? true; - - if (userMessageVisible && params.userMessageText === undefined) { - throw new Error("userMessageText is required when userMessageVisible is true"); - } const record = await params.agentStorage.get(params.agentId); if (record?.archivedAt) { @@ -215,7 +208,6 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): agentId: callerAgentId, prompt: formatSystemNotificationPrompt(body), unarchive: false, - userMessageVisible: false, logger, }); } diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 1ffab6819..aab4bd8f0 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -1268,7 +1268,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom agentManager, agentStorage, agentId, - userMessageText: prompt, prompt, sessionMode, logger: childLogger, diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 2ee7cf8c1..e4c392cde 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -3093,7 +3093,6 @@ export class Session { agentManager: this.agentManager, agentStorage: this.agentStorage, agentId, - userMessageText: text, prompt, messageId, runOptions, @@ -7783,7 +7782,6 @@ export class Session { agentManager: this.agentManager, agentStorage: this.agentStorage, agentId, - userMessageText: msg.text, prompt, messageId: msg.messageId, logger: this.sessionLogger, @@ -8830,7 +8828,6 @@ export class Session { agentId, prompt: formatSystemNotificationPrompt(text), unarchive: false, - userMessageVisible: false, logger: this.sessionLogger, }); },