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:
@@ -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
|
||||
|
||||
@@ -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("&");
|
||||
});
|
||||
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user