diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts
index bdd6e7910..151db462f 100644
--- a/packages/server/src/server/agent/agent-manager.ts
+++ b/packages/server/src/server/agent/agent-manager.ts
@@ -5,6 +5,7 @@ import {
type AgentLifecycleStatus,
} from "../../shared/agent-lifecycle.js";
import type { Logger } from "pino";
+import { getSelfIdentificationInstructions } from "./self-identification-instructions.js";
import type {
AgentCapabilityFlags,
@@ -1106,6 +1107,15 @@ export class AgentManager {
normalized.agentControlMcp = this.agentControlMcp;
}
+ if (
+ normalized.paseoPromptInstructions === undefined &&
+ normalized.agentControlMcp
+ ) {
+ normalized.paseoPromptInstructions = getSelfIdentificationInstructions({
+ cwd: normalized.cwd,
+ });
+ }
+
return normalized;
}
diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts
index ecd21fa9c..f92996763 100644
--- a/packages/server/src/server/agent/agent-sdk-types.ts
+++ b/packages/server/src/server/agent/agent-sdk-types.ts
@@ -202,6 +202,14 @@ export type AgentSessionConfig = {
webSearch?: boolean;
reasoningEffort?: string;
agentControlMcp?: AgentControlMcpConfig;
+ /**
+ * Paseo-owned instructions injected into the first user prompt via
+ * ....
+ *
+ * These MUST NOT be sent via provider system/developer instructions (those are
+ * reserved for provider/session behaviors like resuming).
+ */
+ paseoPromptInstructions?: string;
extra?: {
codex?: AgentMetadata;
claude?: Partial;
diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts
index 1cecfc1de..c580e5618 100644
--- a/packages/server/src/server/agent/mcp-server.ts
+++ b/packages/server/src/server/agent/mcp-server.ts
@@ -32,6 +32,7 @@ import {
} from "../../utils/worktree.js";
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
import { NotGitRepoError, renameCurrentBranch } from "../../utils/checkout-git.js";
+import { injectLeadingPaseoInstructionTag } from "./paseo-instructions-tag.js";
export interface AgentMcpServerOptions {
agentManager: AgentManager;
@@ -456,8 +457,12 @@ export async function createAgentMcpServer(
});
if (initialPrompt) {
+ const initialPromptWithInstructions = injectLeadingPaseoInstructionTag(
+ initialPrompt,
+ snapshot.config.paseoPromptInstructions
+ );
try {
- agentManager.recordUserMessage(snapshot.id, initialPrompt);
+ agentManager.recordUserMessage(snapshot.id, initialPromptWithInstructions);
} catch (error) {
childLogger.error(
{ err: error, agentId: snapshot.id },
@@ -466,7 +471,7 @@ export async function createAgentMcpServer(
}
try {
- startAgentRun(agentManager, snapshot.id, initialPrompt, childLogger);
+ startAgentRun(agentManager, snapshot.id, initialPromptWithInstructions, childLogger);
// If not running in background, wait for completion
if (!background) {
diff --git a/packages/server/src/server/agent/paseo-instructions-tag.test.ts b/packages/server/src/server/agent/paseo-instructions-tag.test.ts
new file mode 100644
index 000000000..09512de3a
--- /dev/null
+++ b/packages/server/src/server/agent/paseo-instructions-tag.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, test } from "vitest";
+
+import {
+ formatPaseoInstructionTag,
+ hasLeadingPaseoInstructionTag,
+ injectLeadingPaseoInstructionTag,
+ stripLeadingPaseoInstructionTag,
+} from "./paseo-instructions-tag.js";
+
+describe("paseo instruction tags", () => {
+ test("formatPaseoInstructionTag wraps content", () => {
+ expect(formatPaseoInstructionTag("hello")).toBe(
+ "\nhello\n"
+ );
+ });
+
+ test("hasLeadingPaseoInstructionTag detects leading tag", () => {
+ expect(hasLeadingPaseoInstructionTag("\nX\n")).toBe(
+ true
+ );
+ expect(hasLeadingPaseoInstructionTag("nope ")).toBe(false);
+ });
+
+ test("stripLeadingPaseoInstructionTag strips leading tag content", () => {
+ const input = [
+ "",
+ "do the thing",
+ "",
+ "",
+ "Hello world",
+ ].join("\n");
+ expect(stripLeadingPaseoInstructionTag(input)).toBe("Hello world");
+ });
+
+ test("injectLeadingPaseoInstructionTag prepends instructions when missing", () => {
+ expect(injectLeadingPaseoInstructionTag("Hello", "do the thing")).toBe(
+ "\ndo the thing\n\n\nHello"
+ );
+ });
+
+ test("injectLeadingPaseoInstructionTag is idempotent when tag already present", () => {
+ const input = "\nX\n\n\nHello";
+ expect(injectLeadingPaseoInstructionTag(input, "do the thing")).toBe(input);
+ });
+});
diff --git a/packages/server/src/server/agent/paseo-instructions-tag.ts b/packages/server/src/server/agent/paseo-instructions-tag.ts
new file mode 100644
index 000000000..548d27b4c
--- /dev/null
+++ b/packages/server/src/server/agent/paseo-instructions-tag.ts
@@ -0,0 +1,48 @@
+const OPEN_TAG = "";
+const CLOSE_TAG = "";
+
+export function formatPaseoInstructionTag(instructions: string): string {
+ return `${OPEN_TAG}\n${instructions}\n${CLOSE_TAG}`;
+}
+
+export function hasLeadingPaseoInstructionTag(text: string): boolean {
+ return /^\s*/.test(text);
+}
+
+/**
+ * Prepend paseo instructions to a prompt exactly once (idempotent by content).
+ * This is intended for agent creation / initial prompt only.
+ */
+export function injectLeadingPaseoInstructionTag(
+ prompt: string,
+ instructions: string | null | undefined
+): string {
+ const normalizedInstructions = instructions?.trim() ?? "";
+ if (!normalizedInstructions) {
+ return prompt;
+ }
+ if (hasLeadingPaseoInstructionTag(prompt)) {
+ return prompt;
+ }
+ return `${formatPaseoInstructionTag(normalizedInstructions)}\n\n${prompt}`;
+}
+
+/**
+ * Remove a leading ... block, if present.
+ * The content is treated as internal metadata and is discarded.
+ */
+export function stripLeadingPaseoInstructionTag(text: string): string {
+ const leadingMatch = text.match(/^\s*/);
+ if (!leadingMatch || leadingMatch.index !== 0) {
+ return text;
+ }
+
+ const openEnd = leadingMatch[0].length;
+ const closeStart = text.indexOf(CLOSE_TAG, openEnd);
+ if (closeStart === -1) {
+ return text;
+ }
+
+ const closeEnd = closeStart + CLOSE_TAG.length;
+ return text.slice(closeEnd).trimStart();
+}
diff --git a/packages/server/src/server/agent/providers/claude-agent-commands.test.ts b/packages/server/src/server/agent/providers/claude-agent-commands.test.ts
index cfac70697..4a5cb6348 100644
--- a/packages/server/src/server/agent/providers/claude-agent-commands.test.ts
+++ b/packages/server/src/server/agent/providers/claude-agent-commands.test.ts
@@ -14,7 +14,10 @@ import { ClaudeAgentClient } from "./claude-agent.js";
import type { AgentSession, AgentSessionConfig, AgentSlashCommand } from "../agent-sdk-types.js";
import { createTestLogger } from "../../../test-utils/test-logger.js";
-describe("ClaudeAgentSession Commands", () => {
+const hasClaudeCredentials =
+ !!process.env.CLAUDE_SESSION_TOKEN || !!process.env.ANTHROPIC_API_KEY;
+
+(hasClaudeCredentials ? describe : describe.skip)("ClaudeAgentSession Commands", () => {
let client: ClaudeAgentClient;
let session: AgentSession;
diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts
index 9a36f6248..29baf0fbf 100644
--- a/packages/server/src/server/agent/providers/claude-agent.ts
+++ b/packages/server/src/server/agent/providers/claude-agent.ts
@@ -48,7 +48,6 @@ import type {
PersistedAgentDescriptor,
} from "../agent-sdk-types.js";
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
-import { getSelfIdentificationInstructions } from "../self-identification-instructions.js";
const fsPromises = promises;
@@ -802,12 +801,7 @@ class ClaudeAgentSession implements AgentSession {
systemPrompt: {
type: "preset",
preset: "claude_code",
- append: [
- getOrchestratorModeInstructions(),
- this.currentMode === "plan" ? "" : getSelfIdentificationInstructions({ cwd: this.config.cwd }),
- ]
- .filter(Boolean)
- .join("\n"),
+ append: [getOrchestratorModeInstructions()].filter(Boolean).join("\n"),
},
settingSources: ["user", "project"],
stderr: (data: string) => {
diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.paseo-instructions.test.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.paseo-instructions.test.ts
new file mode 100644
index 000000000..fb0bd8cfb
--- /dev/null
+++ b/packages/server/src/server/agent/providers/codex-mcp-agent.paseo-instructions.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, test } from "vitest";
+import { mkdtempSync, writeFileSync } from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+import { __test__ } from "./codex-mcp-agent.js";
+import type { AgentSessionConfig } from "../agent-sdk-types.js";
+
+describe("codex developer-instructions vs paseo prompt instructions", () => {
+ test("does not inject Paseo self-identification into developer-instructions", () => {
+ const dir = mkdtempSync(path.join(os.tmpdir(), "codex-rollout-"));
+ const rolloutPath = path.join(dir, "rollout.jsonl");
+
+ const entry = {
+ type: "response_item",
+ payload: {
+ type: "message",
+ role: "user",
+ content: [{ input_text: "hello from history" }],
+ },
+ };
+
+ writeFileSync(rolloutPath, JSON.stringify(entry) + "\n", "utf8");
+
+ const config: AgentSessionConfig = {
+ provider: "codex",
+ cwd: dir,
+ modeId: "auto",
+ };
+
+ const payload = __test__.buildCodexMcpConfig(
+ config,
+ "Hello world",
+ "auto",
+ undefined,
+ rolloutPath
+ );
+
+ const dev = payload["developer-instructions"] ?? "";
+ expect(dev).toContain("");
+ expect(dev).toContain("hello from history");
+ expect(dev.toLowerCase()).not.toContain("set_title");
+ expect(dev.toLowerCase()).not.toContain("set_branch");
+ expect(dev.toLowerCase()).not.toContain("you are running under paseo");
+ });
+});
+
diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts
index a43fda3fa..79a1278c4 100644
--- a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts
+++ b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts
@@ -1133,7 +1133,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
model: CODEX_TEST_MODEL,
reasoningEffort: CODEX_TEST_REASONING_EFFORT,
cwd,
- modeId: "auto",
+ modeId: "read-only",
approvalPolicy: "on-request",
} satisfies AgentSessionConfig;
const filePath = path.join(cwd, "permission.txt");
diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.ts
index 9a8a70628..e5d86e74f 100644
--- a/packages/server/src/server/agent/providers/codex-mcp-agent.ts
+++ b/packages/server/src/server/agent/providers/codex-mcp-agent.ts
@@ -39,7 +39,6 @@ import type {
ListPersistedAgentsOptions,
PersistedAgentDescriptor,
} from "../agent-sdk-types.js";
-import { getSelfIdentificationInstructions } from "../self-identification-instructions.js";
import { curateAgentActivity } from "../activity-curator.js";
type CodexMcpAgentConfig = AgentSessionConfig & { provider: "codex" };
@@ -2759,7 +2758,6 @@ function buildCodexMcpConfig(
developerInstructions = history;
}
}
- const selfIdentificationInstructions = getSelfIdentificationInstructions({ cwd: config.cwd });
// Build MCP servers configuration
const mcpServers: Record = {};
@@ -2833,7 +2831,7 @@ function buildCodexMcpConfig(
}
- const combinedDeveloperInstructions = [developerInstructions, selfIdentificationInstructions]
+ const combinedDeveloperInstructions = [developerInstructions]
.filter(Boolean)
.join("\n\n");
@@ -3227,7 +3225,7 @@ class CodexMcpAgentSession implements AgentSession {
const abortController = new AbortController();
this.currentAbortController = abortController;
- const promptText = await toPromptText(prompt, this.logger);
+ let promptText = await toPromptText(prompt, this.logger);
// NOTE: user_message is NOT emitted here because the agent-manager's
// recordUserMessage() already handles emitting the user message timeline
// event before calling stream(). Emitting here would cause duplicates.
@@ -4652,6 +4650,7 @@ export const __test__ = {
tokenizeCommandArgs,
parseFrontMatter,
expandCodexCustomPrompt,
+ buildCodexMcpConfig,
isMissingConversationIdError,
isMissingConversationIdResponse,
};
diff --git a/packages/server/src/server/daemon-client-v2.e2e.test.ts b/packages/server/src/server/daemon-client-v2.e2e.test.ts
index 1545a935c..b343a1e66 100644
--- a/packages/server/src/server/daemon-client-v2.e2e.test.ts
+++ b/packages/server/src/server/daemon-client-v2.e2e.test.ts
@@ -401,64 +401,64 @@ describe("daemon client v2 E2E", () => {
});
const permissionRequestPromise = waitForSignal(60000, (resolve) => {
- const unsubscribe = ctx.client.on(
- "agent_permission_request",
- (message) => {
- if (message.type !== "agent_permission_request") {
- return;
- }
- if (message.payload.agentId !== agent.id) {
- return;
- }
- resolve(message);
+ const unsubscribe = ctx.client.on("agent_permission_request", (message) => {
+ if (message.type !== "agent_permission_request") {
+ return;
}
- );
+ if (message.payload.agentId !== agent.id) {
+ return;
+ }
+ resolve(message);
+ });
return unsubscribe;
});
- await ctx.client.sendMessage(
- agent.id,
- "Request approval to run the command `printf \"ok\" > permission.txt`."
- );
-
- const permission = await ctx.client.waitForPermission(agent.id, 60000);
- expect(permission).toBeTruthy();
- expect(permission.id).toBeTruthy();
-
- const permissionRequest = await permissionRequestPromise;
- expect(permissionRequest.payload.agentId).toBe(agent.id);
-
const permissionResolvedPromise = waitForSignal(60000, (resolve) => {
- const unsubscribe = ctx.client.on(
- "agent_permission_resolved",
- (message) => {
- if (message.type !== "agent_permission_resolved") {
- return;
- }
- if (message.payload.agentId !== agent.id) {
- return;
- }
- if (message.payload.requestId !== permission.id) {
- return;
- }
- resolve(message);
+ const unsubscribe = ctx.client.on("agent_permission_resolved", (message) => {
+ if (message.type !== "agent_permission_resolved") {
+ return;
}
- );
+ if (message.payload.agentId !== agent.id) {
+ return;
+ }
+ resolve(message);
+ });
return unsubscribe;
});
- await ctx.client.respondToPermission(agent.id, permission.id, {
- behavior: "allow",
- });
- const permissionResolved = await permissionResolvedPromise;
- expect(permissionResolved.payload.requestId).toBe(permission.id);
+ try {
+ await ctx.client.sendMessage(
+ agent.id,
+ [
+ "Use your shell tool to run: `printf \"ok\" > permission.txt`.",
+ "This will require approval. Request permission and wait for approval before continuing.",
+ ].join("\n")
+ );
- const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000);
- expect(finalState.status).toBe("idle");
- expect(existsSync(filePath)).toBe(true);
+ const permission = await ctx.client.waitForPermission(agent.id, 60000);
+ expect(permission).toBeTruthy();
+ expect(permission.id).toBeTruthy();
- await ctx.client.deleteAgent(agent.id);
- rmSync(cwd, { recursive: true, force: true });
+ const permissionRequest = await permissionRequestPromise;
+ expect(permissionRequest.payload.agentId).toBe(agent.id);
+
+ await ctx.client.respondToPermission(agent.id, permission.id, {
+ behavior: "allow",
+ });
+
+ const permissionResolved = await permissionResolvedPromise;
+ expect(permissionResolved.payload.requestId).toBe(permission.id);
+
+ const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000);
+ expect(finalState.status).toBe("idle");
+ expect(existsSync(filePath)).toBe(true);
+ } finally {
+ // Prevent unhandled rejections if the test fails before promises resolve.
+ await permissionRequestPromise.catch(() => {});
+ await permissionResolvedPromise.catch(() => {});
+ await ctx.client.deleteAgent(agent.id);
+ rmSync(cwd, { recursive: true, force: true });
+ }
},
180000
);
diff --git a/packages/server/src/server/daemon-e2e/checkout-ship.e2e.test.ts b/packages/server/src/server/daemon-e2e/checkout-ship.e2e.test.ts
index 6974e3b92..71073687d 100644
--- a/packages/server/src/server/daemon-e2e/checkout-ship.e2e.test.ts
+++ b/packages/server/src/server/daemon-e2e/checkout-ship.e2e.test.ts
@@ -19,6 +19,17 @@ function tmpCwd(prefix: string): string {
return realpathSync(mkdtempSync(path.join(tmpdir(), prefix)));
}
+function hasGitHubCliAuth(): boolean {
+ try {
+ execSync("gh auth status -h github.com", { stdio: "pipe" });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+const testWithGitHubCliAuth = hasGitHubCliAuth() ? test : test.skip;
+
type McpToolResult = {
structuredContent?: Record;
content?: Array<{ structuredContent?: Record } | Record>;
@@ -137,7 +148,7 @@ describe("daemon checkout ship loop", () => {
await ctx.cleanup();
}, 60000);
- test(
+ testWithGitHubCliAuth(
"runs the full checkout ship loop via checkout RPCs",
async () => {
const repoDir = tmpCwd("checkout-ship-");
diff --git a/packages/server/src/server/daemon-e2e/orchestration.e2e.test.ts b/packages/server/src/server/daemon-e2e/orchestration.e2e.test.ts
index 37f990381..78dfec102 100644
--- a/packages/server/src/server/daemon-e2e/orchestration.e2e.test.ts
+++ b/packages/server/src/server/daemon-e2e/orchestration.e2e.test.ts
@@ -120,11 +120,36 @@ describe("daemon E2E", () => {
createAgentCall.type === "tool_call" &&
createAgentCall.output
) {
- // The output contains the agentId
- const output = createAgentCall.output as { agentId?: string };
- if (output.agentId) {
- childAgentId = output.agentId;
- }
+ const output = createAgentCall.output as unknown;
+ const tryExtract = (value: unknown): string | null => {
+ if (!value) return null;
+ if (typeof value === "string") {
+ try {
+ return tryExtract(JSON.parse(value));
+ } catch {
+ return null;
+ }
+ }
+ if (typeof value !== "object") return null;
+ const asObj = value as Record;
+ const direct = asObj.agentId;
+ if (typeof direct === "string") return direct;
+ const structured = asObj.structuredContent;
+ if (structured && typeof structured === "object") {
+ const nested = (structured as Record).agentId;
+ if (typeof nested === "string") return nested;
+ }
+ if (typeof structured === "string") {
+ try {
+ return tryExtract(JSON.parse(structured));
+ } catch {
+ return null;
+ }
+ }
+ return null;
+ };
+
+ childAgentId = tryExtract(output);
}
// Verify we found the child agent ID
diff --git a/packages/server/src/server/daemon-e2e/permissions-codex.e2e.test.ts b/packages/server/src/server/daemon-e2e/permissions-codex.e2e.test.ts
index cdfdd2799..89d58a550 100644
--- a/packages/server/src/server/daemon-e2e/permissions-codex.e2e.test.ts
+++ b/packages/server/src/server/daemon-e2e/permissions-codex.e2e.test.ts
@@ -40,7 +40,7 @@ describe("daemon E2E", () => {
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
cwd,
title: "Codex Permission Test",
- modeId: "auto",
+ modeId: "read-only",
});
expect(agent.id).toBeTruthy();
@@ -105,7 +105,7 @@ describe("daemon E2E", () => {
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
cwd,
title: "Codex Permission Deny Test",
- modeId: "auto",
+ modeId: "read-only",
});
expect(agent.id).toBeTruthy();
diff --git a/packages/server/src/server/daemon-e2e/streaming.e2e.test.ts b/packages/server/src/server/daemon-e2e/streaming.e2e.test.ts
index 0ed05d292..9d3617a87 100644
--- a/packages/server/src/server/daemon-e2e/streaming.e2e.test.ts
+++ b/packages/server/src/server/daemon-e2e/streaming.e2e.test.ts
@@ -17,7 +17,12 @@ function tmpCwd(): string {
const CODEX_TEST_MODEL = "gpt-5.1-codex-mini";
const CODEX_TEST_REASONING_EFFORT = "low";
-describe("daemon E2E", () => {
+const hasClaudeCredentials =
+ !!process.env.CLAUDE_SESSION_TOKEN || !!process.env.ANTHROPIC_API_KEY;
+
+const describeWithClaude = hasClaudeCredentials ? describe : describe.skip;
+
+describeWithClaude("daemon E2E", () => {
let ctx: DaemonTestContext;
beforeEach(async () => {
@@ -389,43 +394,26 @@ describe("daemon E2E", () => {
const queue = ctx.client.getMessageQueue();
const assistantChunks: string[] = [];
- // Debug: dump all events from queue
-
- for (let i = 0; i < queue.length; i++) {
- const m = queue[i];
- if (m.type === "agent_stream" && m.payload.agentId === agent.id) {
- const event = m.payload.event;
- if (event.type === "timeline") {
- const item = event.item;
-
- } else {
-
- }
- } else if (m.type === "agent_state" && m.payload.id === agent.id) {
-
- }
- }
-
- // Find the user_message for message 2 to mark the boundary
- let foundMsg2UserMessage = false;
+ // We only care about Turn 2 ("Hello world ..."), but event ordering can be noisy when
+ // Turn 1 is still streaming. Anchor on the assistant response content itself.
+ let startedCollecting = false;
for (let i = msg2StartPosition; i < queue.length; i++) {
const m = queue[i];
-
- // Look for our user message to mark the start of message 2 context
if (
m.type === "agent_stream" &&
m.payload.agentId === agent.id &&
m.payload.event.type === "timeline"
) {
const item = m.payload.event.item;
- if (item.type === "user_message" && (item.text as string)?.includes("Hello world")) {
- foundMsg2UserMessage = true;
-
- }
- // Collect assistant messages after we found the user message
- if (foundMsg2UserMessage && item.type === "assistant_message" && item.text) {
- assistantChunks.push(item.text);
+ if (item.type === "assistant_message" && item.text) {
+ const text = String(item.text);
+ if (!startedCollecting && text.includes("Hello")) {
+ startedCollecting = true;
+ }
+ if (startedCollecting) {
+ assistantChunks.push(text);
+ }
}
}
}
diff --git a/packages/server/src/server/messages.test.ts b/packages/server/src/server/messages.test.ts
new file mode 100644
index 000000000..3f0ce9025
--- /dev/null
+++ b/packages/server/src/server/messages.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, test } from "vitest";
+
+import { serializeAgentStreamEvent } from "./messages.js";
+
+describe("serializeAgentStreamEvent", () => {
+ test("strips leading paseo-instructions from user_message timeline items", () => {
+ const event = {
+ type: "timeline",
+ provider: "claude",
+ item: {
+ type: "user_message",
+ text: "\nX\n\n\nHello",
+ messageId: "m1",
+ },
+ } as any;
+
+ const serialized = serializeAgentStreamEvent(event) as any;
+ expect(serialized.item.text).toBe("Hello");
+ expect(serialized.item.messageId).toBe("m1");
+ });
+
+ test("does not strip non-leading tags", () => {
+ const event = {
+ type: "timeline",
+ provider: "claude",
+ item: {
+ type: "user_message",
+ text: "Hello \nX\n",
+ },
+ } as any;
+
+ const serialized = serializeAgentStreamEvent(event) as any;
+ expect(serialized.item.text).toBe(event.item.text);
+ });
+});
+
diff --git a/packages/server/src/server/messages.ts b/packages/server/src/server/messages.ts
index a0b594066..5d11e4111 100644
--- a/packages/server/src/server/messages.ts
+++ b/packages/server/src/server/messages.ts
@@ -1,6 +1,7 @@
import type { ManagedAgent } from "./agent/agent-manager.js";
import { toAgentPayload } from "./agent/agent-projections.js";
import type { AgentStreamEvent } from "./agent/agent-sdk-types.js";
+import { stripLeadingPaseoInstructionTag } from "./agent/paseo-instructions-tag.js";
import type {
AgentSnapshotPayload,
AgentStreamEventPayload,
@@ -18,5 +19,21 @@ export function serializeAgentSnapshot(
export function serializeAgentStreamEvent(
event: AgentStreamEvent
): AgentStreamEventPayload {
- return event as AgentStreamEventPayload;
+ if (event.type !== "timeline") {
+ return event as AgentStreamEventPayload;
+ }
+ if (event.item.type !== "user_message") {
+ return event as AgentStreamEventPayload;
+ }
+ const stripped = stripLeadingPaseoInstructionTag(event.item.text);
+ if (stripped === event.item.text) {
+ return event as AgentStreamEventPayload;
+ }
+ return {
+ ...event,
+ item: {
+ ...event.item,
+ text: stripped,
+ },
+ } as AgentStreamEventPayload;
}
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index 99fd89e59..8e7b9013e 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -45,6 +45,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
import { buildProviderRegistry } from "./agent/provider-registry.js";
import { AgentManager } from "./agent/agent-manager.js";
import type { ManagedAgent } from "./agent/agent-manager.js";
+import { injectLeadingPaseoInstructionTag } from "./agent/paseo-instructions-tag.js";
import { toAgentPayload } from "./agent/agent-projections.js";
import {
StructuredAgentResponseError,
@@ -1322,9 +1323,13 @@ export class Session {
const trimmedPrompt = initialPrompt?.trim();
if (trimmedPrompt) {
try {
+ const initialPromptWithInstructions = injectLeadingPaseoInstructionTag(
+ trimmedPrompt,
+ snapshot.config.paseoPromptInstructions
+ );
await this.handleSendAgentMessage(
snapshot.id,
- trimmedPrompt,
+ initialPromptWithInstructions,
uuidv4(),
images
);
diff --git a/packages/server/src/server/voice-conversations.e2e.test.ts b/packages/server/src/server/voice-conversations.e2e.test.ts
index 4654e13d9..8557315b7 100644
--- a/packages/server/src/server/voice-conversations.e2e.test.ts
+++ b/packages/server/src/server/voice-conversations.e2e.test.ts
@@ -20,6 +20,30 @@ async function waitForFile(filepath: string, timeoutMs = 5000): Promise {
}
}
+async function waitForJsonFile(
+ filepath: string,
+ timeoutMs = 5000
+): Promise {
+ const start = Date.now();
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (existsSync(filepath)) {
+ try {
+ const raw = readFileSync(filepath, "utf8");
+ if (raw.trim().length > 0) {
+ return JSON.parse(raw) as T;
+ }
+ } catch {
+ // File may exist but still be mid-write; retry.
+ }
+ }
+ if (Date.now() - start > timeoutMs) {
+ throw new Error(`Timed out waiting for valid JSON: ${filepath}`);
+ }
+ await new Promise((r) => setTimeout(r, 50));
+ }
+}
+
describe("voice conversations - daemon E2E", () => {
test(
"two concurrent clients persist independently under paseoHome/voice-conversations",
@@ -57,16 +81,16 @@ describe("voice conversations - daemon E2E", () => {
await waitForFile(fileA);
await waitForFile(fileB);
- const dataA = JSON.parse(readFileSync(fileA, "utf8")) as {
+ const dataA = await waitForJsonFile<{
voiceConversationId: string;
messageCount: number;
messages: unknown[];
- };
- const dataB = JSON.parse(readFileSync(fileB, "utf8")) as {
+ }>(fileA);
+ const dataB = await waitForJsonFile<{
voiceConversationId: string;
messageCount: number;
messages: unknown[];
- };
+ }>(fileB);
expect(dataA.voiceConversationId).toBe(voiceConversationIdA);
expect(dataB.voiceConversationId).toBe(voiceConversationIdB);
@@ -121,4 +145,3 @@ describe("voice conversations - daemon E2E", () => {
30000
);
});
-