fix(collab): treated guest prompts as user interjections

Converted user-attributed collab prompt frames to prioritized user messages only on the model-facing path, preserving guest details in persisted transcript frames.

Added regression coverage for the provider role, steering envelope, and retained guest attribution.

Fixes #7288
This commit is contained in:
roboomp
2026-08-01 17:04:00 +00:00
parent 09a7c86563
commit 352c0fdce2
3 changed files with 57 additions and 7 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed collab guest prompts being sent to models as unframed developer context, so guest messages now retain their transcript attribution while reaching the model as prioritized user interjections ([#7288](https://github.com/can1357/oh-my-pi/issues/7288)).
## [17.2.3] - 2026-08-01
### Changed
+27 -7
View File
@@ -24,6 +24,7 @@ import type {
} from "@oh-my-pi/pi-ai";
import * as AIError from "@oh-my-pi/pi-ai/error";
import { isRecord, logger, prompt } from "@oh-my-pi/pi-utils";
import { COLLAB_PROMPT_MESSAGE_TYPE } from "@oh-my-pi/pi-wire";
import userInterjectionTemplate from "../prompts/steering/user-interjection.md" with { type: "text" };
import { formatTitleConversationContext, type TitleConversationTurn } from "../tiny/message-preproc";
@@ -614,8 +615,18 @@ export function normalizeCustomMessagePayload<T = unknown>(
};
}
function isSteeringUserMessage(message: AgentMessage | undefined): message is UserMessage & { steering: true } {
return message?.role === "user" && message.steering === true;
type SteeringUserMessage =
| (UserMessage & { steering: true })
| (CustomMessage & {
customType: typeof COLLAB_PROMPT_MESSAGE_TYPE;
attribution: "user";
});
function isSteeringUserMessage(message: AgentMessage | undefined): message is SteeringUserMessage {
if (message?.role === "user") return message.steering === true;
return (
message?.role === "custom" && message.customType === COLLAB_PROMPT_MESSAGE_TYPE && message.attribution === "user"
);
}
function userMessageWithoutSteering(message: UserMessage): UserMessage {
@@ -655,17 +666,26 @@ function getArrayContentImages(content: (TextContent | ImageContent)[]): ImageCo
return images ?? [];
}
function wrapSteeringUserMessage(message: UserMessage): UserMessage {
function wrapSteeringUserMessage(message: SteeringUserMessage): UserMessage {
const userMessage: UserMessage =
message.role === "user"
? userMessageWithoutSteering(message)
: {
role: "user",
content: message.content,
attribution: "user",
timestamp: message.timestamp,
};
if (typeof message.content === "string") {
if (message.content.length === 0) return message;
return { ...userMessageWithoutSteering(message), content: renderSteeringEnvelope(message.content) };
if (message.content.length === 0) return message.role === "user" ? message : userMessage;
return { ...userMessage, content: renderSteeringEnvelope(message.content) };
}
const text = getArrayContentText(message.content);
if (text.length === 0) return message;
if (text.length === 0) return message.role === "user" ? message : userMessage;
const content: (TextContent | ImageContent)[] = [{ type: "text", text: renderSteeringEnvelope(text) }];
content.push(...getArrayContentImages(message.content));
return { ...userMessageWithoutSteering(message), content };
return { ...userMessage, content };
}
export function wrapSteeringForModel(messages: AgentMessage[]): AgentMessage[] {
@@ -7,6 +7,7 @@ import {
SKILL_PROMPT_MESSAGE_TYPE,
wrapSteeringForModel,
} from "@oh-my-pi/pi-coding-agent/session/messages";
import { COLLAB_PROMPT_MESSAGE_TYPE } from "@oh-my-pi/pi-wire";
function expectAttribution(message: Message | undefined, expected: "user" | "agent" | undefined): void {
expect(message).toBeDefined();
@@ -382,6 +383,31 @@ describe("wrapSteeringForModel", () => {
expect(wrappedText).not.toContain("&amp;");
});
it("presents user-attributed collab prompts as wrapped user turns", () => {
const message: AgentMessage = {
role: "custom",
customType: COLLAB_PROMPT_MESSAGE_TYPE,
content: "Reply with exactly PONG",
display: true,
details: { from: "guest" },
attribution: "user",
timestamp: 1,
};
const wrapped = wrapSteeringForModel([message]);
const providerMessages = convertToLlm(wrapped);
expect(wrapped[0]?.role).toBe("user");
expect(getUserText(wrapped[0])).toContain("<system-notice>");
expect(getUserText(wrapped[0])).toContain("Reply with exactly PONG");
expect(providerMessages).toHaveLength(1);
expect(providerMessages[0]?.role).toBe("user");
expect(message).toMatchObject({
role: "custom",
details: { from: "guest" },
});
});
it("wraps buried steering messages too so wire bytes stay stable across turns", () => {
const buried: AgentMessage = {
role: "user",