Hide paseo-system prompts from the agent timeline

System-injected prompts (finish notifications, schedule fires) are wrapped
in a <paseo-system> 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.
This commit is contained in:
Mohamed Boudra
2026-05-26 11:28:22 +07:00
parent c31ab074e9
commit 6f16900b8a
6 changed files with 147 additions and 20 deletions
@@ -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<AgentStreamEvent> {
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");
});
@@ -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<AgentStreamEvent, { type: "timeline" }>[] = [];
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<void> {
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,
@@ -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;
@@ -112,14 +112,18 @@ export function formatSystemNotificationPrompt(reason: string): string {
return `<paseo-system>\n${reason}\n</paseo-system>`;
}
const SYSTEM_ENVELOPE_PATTERN = /^<paseo-system>\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,
});
}
@@ -1268,7 +1268,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
agentManager,
agentStorage,
agentId,
userMessageText: prompt,
prompt,
sessionMode,
logger: childLogger,
-3
View File
@@ -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,
});
},