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 { AgentManager, type ManagedAgent } from "./agent-manager.js";
import { AgentStorage } from "./agent-storage.js"; import { AgentStorage } from "./agent-storage.js";
import { PARENT_AGENT_ID_LABEL } from "../../shared/agent-labels.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 { StoredAgentRecord } from "./agent-storage.js";
import type { import type {
AgentClient, 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(); const logger = createTestLogger();
test("normalizeConfig injects the provider default model when omitted", async () => { 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(codexClient.calls).toBe(0);
expect(result.map((d) => d.provider)).toEqual(["claude"]); 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 { getAgentProviderDefinition } from "./provider-manifest.js";
import { IMPORTABLE_PROVIDERS } from "./provider-registry.js"; import { IMPORTABLE_PROVIDERS } from "./provider-registry.js";
import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js"; import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
import { isSystemInjectedEnvelope } from "./agent-prompt.js";
const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000; const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000;
const INTERRUPT_SESSION_TIMEOUT_MS = 2_000; const INTERRUPT_SESSION_TIMEOUT_MS = 2_000;
@@ -2622,6 +2623,9 @@ export class AgentManager {
const historyEvents: Extract<AgentStreamEvent, { type: "timeline" }>[] = []; const historyEvents: Extract<AgentStreamEvent, { type: "timeline" }>[] = [];
for await (const event of agent.session.streamHistory()) { for await (const event of agent.session.streamHistory()) {
if (event.type === "timeline") { if (event.type === "timeline") {
if (event.item.type === "user_message" && isSystemInjectedEnvelope(event.item.text)) {
continue;
}
historyEvents.push(event); historyEvents.push(event);
} }
} }
@@ -2657,6 +2661,9 @@ export class AgentManager {
if (event.type !== "timeline") { if (event.type !== "timeline") {
continue; continue;
} }
if (event.item.type === "user_message" && isSystemInjectedEnvelope(event.item.text)) {
continue;
}
this.recordTimeline( this.recordTimeline(
agent.id, agent.id,
event.item, event.item,
@@ -2904,6 +2911,12 @@ export class AgentManager {
}): Promise<void> { }): Promise<void> {
const { agent, event, options, flags } = params; 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) { if (options?.fromHistory) {
this.recordTimeline( this.recordTimeline(
agent.id, 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 { createTestLogger } from "../../test-utils/test-logger.js";
import { AgentManager } from "./agent-manager.js"; import { AgentManager } from "./agent-manager.js";
import { AgentStorage } from "./agent-storage.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"; 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 () => { it("does not notify archived callers", async () => {
let subscriber: ((event: AgentManagerEvent) => void) | null = null; 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>`; 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 { export interface SendPromptToAgentParams {
agentManager: AgentManager; agentManager: AgentManager;
agentStorage: AgentStorage; agentStorage: AgentStorage;
agentId: string; agentId: string;
/** Prompt to dispatch to the provider (may include image blocks or wrapped text). */ /** Prompt to dispatch to the provider (may include image blocks or wrapped text). */
prompt: AgentPromptInput; prompt: AgentPromptInput;
/** Raw user text. Required when userMessageVisible is true. */
userMessageText?: string;
messageId?: string; messageId?: string;
runOptions?: AgentRunOptions; runOptions?: AgentRunOptions;
/** Optional mode to set on the agent before the run starts. */ /** Optional mode to set on the agent before the run starts. */
@@ -130,12 +134,6 @@ export interface SendPromptToAgentParams {
* schedule fires, notify-on-finish). * schedule fires, notify-on-finish).
*/ */
unarchive?: boolean; 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; logger: Logger;
} }
@@ -154,11 +152,6 @@ export async function sendPromptToAgent(
params: SendPromptToAgentParams, params: SendPromptToAgentParams,
): Promise<{ outOfBand: boolean }> { ): Promise<{ outOfBand: boolean }> {
const unarchive = params.unarchive ?? true; 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); const record = await params.agentStorage.get(params.agentId);
if (record?.archivedAt) { if (record?.archivedAt) {
@@ -215,7 +208,6 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
agentId: callerAgentId, agentId: callerAgentId,
prompt: formatSystemNotificationPrompt(body), prompt: formatSystemNotificationPrompt(body),
unarchive: false, unarchive: false,
userMessageVisible: false,
logger, logger,
}); });
} }
@@ -1268,7 +1268,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
agentManager, agentManager,
agentStorage, agentStorage,
agentId, agentId,
userMessageText: prompt,
prompt, prompt,
sessionMode, sessionMode,
logger: childLogger, logger: childLogger,
-3
View File
@@ -3093,7 +3093,6 @@ export class Session {
agentManager: this.agentManager, agentManager: this.agentManager,
agentStorage: this.agentStorage, agentStorage: this.agentStorage,
agentId, agentId,
userMessageText: text,
prompt, prompt,
messageId, messageId,
runOptions, runOptions,
@@ -7783,7 +7782,6 @@ export class Session {
agentManager: this.agentManager, agentManager: this.agentManager,
agentStorage: this.agentStorage, agentStorage: this.agentStorage,
agentId, agentId,
userMessageText: msg.text,
prompt, prompt,
messageId: msg.messageId, messageId: msg.messageId,
logger: this.sessionLogger, logger: this.sessionLogger,
@@ -8830,7 +8828,6 @@ export class Session {
agentId, agentId,
prompt: formatSystemNotificationPrompt(text), prompt: formatSystemNotificationPrompt(text),
unarchive: false, unarchive: false,
userMessageVisible: false,
logger: this.sessionLogger, logger: this.sessionLogger,
}); });
}, },