fix(team): filter internal control messages
This commit is contained in:
parent
4013c47332
commit
2a41010610
19 changed files with 496 additions and 32 deletions
|
|
@ -90,6 +90,7 @@ import {
|
||||||
} from '@shared/constants';
|
} from '@shared/constants';
|
||||||
import { shouldSuppressDesktopNotificationForInboxText } from '@shared/utils/idleNotificationSemantics';
|
import { shouldSuppressDesktopNotificationForInboxText } from '@shared/utils/idleNotificationSemantics';
|
||||||
import { parseInboxJson } from '@shared/utils/inboxNoise';
|
import { parseInboxJson } from '@shared/utils/inboxNoise';
|
||||||
|
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
|
|
@ -470,6 +471,9 @@ async function notifyNewInboxMessages(teamName: string, detail: string): Promise
|
||||||
const msg = newMessages[i];
|
const msg = newMessages[i];
|
||||||
// Skip messages sent from our own UI
|
// Skip messages sent from our own UI
|
||||||
if (msg.source && suppressedSources.has(msg.source)) continue;
|
if (msg.source && suppressedSources.has(msg.source)) continue;
|
||||||
|
// Skip app-owned private bootstrap/control prompts. They are durable runtime proof inputs,
|
||||||
|
// not user-visible conversation messages.
|
||||||
|
if (isTeamInternalControlMessageText(msg.text)) continue;
|
||||||
// Skip internal coordination noise (idle_notification, shutdown_*, etc.)
|
// Skip internal coordination noise (idle_notification, shutdown_*, etc.)
|
||||||
if (shouldSuppressDesktopNotificationForInboxText(msg.text)) continue;
|
if (shouldSuppressDesktopNotificationForInboxText(msg.text)) continue;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics';
|
import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
|
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
|
||||||
|
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
import { getEffectiveInboxMessageId } from './inboxMessageIdentity';
|
import { getEffectiveInboxMessageId } from './inboxMessageIdentity';
|
||||||
|
|
@ -138,6 +139,10 @@ function buildSyntheticOpenCodeBootstrapMessages(config: TeamConfig): InboxMessa
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVisibleTeamMessage(message: InboxMessage): boolean {
|
||||||
|
return !isTeamInternalControlMessageText(message.text);
|
||||||
|
}
|
||||||
|
|
||||||
function annotateSlashCommandResponses(messages: InboxMessage[]): void {
|
function annotateSlashCommandResponses(messages: InboxMessage[]): void {
|
||||||
let pendingSlash = null as InboxMessage['slashCommand'] | null;
|
let pendingSlash = null as InboxMessage['slashCommand'] | null;
|
||||||
|
|
||||||
|
|
@ -499,7 +504,9 @@ export class TeamMessageFeedService {
|
||||||
|
|
||||||
const normalizeStartedAt = Date.now();
|
const normalizeStartedAt = Date.now();
|
||||||
const syntheticMessages = buildSyntheticOpenCodeBootstrapMessages(config);
|
const syntheticMessages = buildSyntheticOpenCodeBootstrapMessages(config);
|
||||||
let messages = [...inboxMessages, ...leadTexts, ...sentMessages, ...syntheticMessages];
|
let messages = [...inboxMessages, ...leadTexts, ...sentMessages, ...syntheticMessages].filter(
|
||||||
|
isVisibleTeamMessage
|
||||||
|
);
|
||||||
messages = dedupeLeadProcessCopies(messages, leadTexts);
|
messages = dedupeLeadProcessCopies(messages, leadTexts);
|
||||||
messages = ensureEffectiveMessageIds(messages);
|
messages = ensureEffectiveMessageIds(messages);
|
||||||
messages = dedupeByMessageId(messages);
|
messages = dedupeByMessageId(messages);
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ import {
|
||||||
parseAllTeammateMessages,
|
parseAllTeammateMessages,
|
||||||
type ParsedTeammateContent,
|
type ParsedTeammateContent,
|
||||||
} from '@shared/utils/teammateMessageParser';
|
} from '@shared/utils/teammateMessageParser';
|
||||||
|
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
||||||
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
||||||
import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName';
|
import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName';
|
||||||
import {
|
import {
|
||||||
|
|
@ -19400,24 +19401,28 @@ export class TeamProvisioningService {
|
||||||
// that is not meant for the human user.
|
// that is not meant for the human user.
|
||||||
const cleanReply = replyText ? stripAgentBlocks(replyText) : null;
|
const cleanReply = replyText ? stripAgentBlocks(replyText) : null;
|
||||||
if (cleanReply) {
|
if (cleanReply) {
|
||||||
const relayMsg: InboxMessage = {
|
if (isTeamInternalControlMessageText(cleanReply)) {
|
||||||
from: leadName,
|
logger.debug(`[${teamName}] Suppressed internal lead relay echo`);
|
||||||
to: 'user',
|
} else {
|
||||||
text: cleanReply,
|
const relayMsg: InboxMessage = {
|
||||||
timestamp: nowIso(),
|
from: leadName,
|
||||||
read: true,
|
to: 'user',
|
||||||
summary: cleanReply.length > 60 ? cleanReply.slice(0, 57) + '...' : cleanReply,
|
text: cleanReply,
|
||||||
messageId: `lead-process-${runId}-${Date.now()}`,
|
timestamp: nowIso(),
|
||||||
source: 'lead_process',
|
read: true,
|
||||||
};
|
summary: cleanReply.length > 60 ? cleanReply.slice(0, 57) + '...' : cleanReply,
|
||||||
this.pushLiveLeadProcessMessage(teamName, relayMsg);
|
messageId: `lead-process-${runId}-${Date.now()}`,
|
||||||
// Persist to disk so relayed replies survive app restart and trigger FileWatcher
|
source: 'lead_process',
|
||||||
this.persistSentMessage(teamName, relayMsg);
|
};
|
||||||
this.teamChangeEmitter?.({
|
this.pushLiveLeadProcessMessage(teamName, relayMsg);
|
||||||
type: 'inbox',
|
// Persist to disk so relayed replies survive app restart and trigger FileWatcher
|
||||||
teamName,
|
this.persistSentMessage(teamName, relayMsg);
|
||||||
detail: 'lead-process-reply',
|
this.teamChangeEmitter?.({
|
||||||
});
|
type: 'inbox',
|
||||||
|
teamName,
|
||||||
|
detail: 'lead-process-reply',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return batch.length;
|
return batch.length;
|
||||||
|
|
@ -25426,7 +25431,7 @@ export class TeamProvisioningService {
|
||||||
!hasCapturedVisibleSendMessage
|
!hasCapturedVisibleSendMessage
|
||||||
) {
|
) {
|
||||||
const cleanText = stripAgentBlocks(text).trim();
|
const cleanText = stripAgentBlocks(text).trim();
|
||||||
if (cleanText.length > 0) {
|
if (cleanText.length > 0 && !isTeamInternalControlMessageText(cleanText)) {
|
||||||
this.pushLiveLeadTextMessage(
|
this.pushLiveLeadTextMessage(
|
||||||
run,
|
run,
|
||||||
cleanText,
|
cleanText,
|
||||||
|
|
@ -25440,7 +25445,7 @@ export class TeamProvisioningService {
|
||||||
// into the live cache so Messages/Activity can show the earliest assistant output.
|
// into the live cache so Messages/Activity can show the earliest assistant output.
|
||||||
if (!run.silentUserDmForward && !hasCapturedVisibleSendMessage) {
|
if (!run.silentUserDmForward && !hasCapturedVisibleSendMessage) {
|
||||||
const cleanText = stripAgentBlocks(text).trim();
|
const cleanText = stripAgentBlocks(text).trim();
|
||||||
if (cleanText.length > 0) {
|
if (cleanText.length > 0 && !isTeamInternalControlMessageText(cleanText)) {
|
||||||
this.pushLiveLeadTextMessage(
|
this.pushLiveLeadTextMessage(
|
||||||
run,
|
run,
|
||||||
cleanText,
|
cleanText,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
||||||
import { isApiErrorMessage } from '@shared/utils/apiErrorDetector';
|
import { isApiErrorMessage } from '@shared/utils/apiErrorDetector';
|
||||||
import { isThoughtProtocolNoise } from '@shared/utils/inboxNoise';
|
import { isThoughtProtocolNoise } from '@shared/utils/inboxNoise';
|
||||||
import { extractMarkdownPlainText } from '@shared/utils/markdownTextSearch';
|
import { extractMarkdownPlainText } from '@shared/utils/markdownTextSearch';
|
||||||
|
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
||||||
import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary';
|
import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary';
|
||||||
import { ChevronDown, ChevronRight, ChevronUp, Maximize2 } from 'lucide-react';
|
import { ChevronDown, ChevronRight, ChevronUp, Maximize2 } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -73,6 +74,7 @@ export function isLeadThought(msg: InboxMessage): boolean {
|
||||||
if (msg.messageKind === 'slash_command_result') return false;
|
if (msg.messageKind === 'slash_command_result') return false;
|
||||||
// Protocol noise (JSON coordination signals, raw teammate-message XML) should be hidden
|
// Protocol noise (JSON coordination signals, raw teammate-message XML) should be hidden
|
||||||
if (isThoughtProtocolNoise(msg.text)) return false;
|
if (isThoughtProtocolNoise(msg.text)) return false;
|
||||||
|
if (isTeamInternalControlMessageText(msg.text)) return false;
|
||||||
if (msg.source === 'lead_session') return true;
|
if (msg.source === 'lead_session') return true;
|
||||||
if (msg.source === 'lead_process') return true;
|
if (msg.source === 'lead_process') return true;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -90,7 +92,7 @@ export function isLeadThought(msg: InboxMessage): boolean {
|
||||||
function isLeadSessionNoise(msg: InboxMessage): boolean {
|
function isLeadSessionNoise(msg: InboxMessage): boolean {
|
||||||
if (msg.source !== 'lead_session' && msg.source !== 'lead_process') return false;
|
if (msg.source !== 'lead_session' && msg.source !== 'lead_process') return false;
|
||||||
if (typeof msg.to === 'string' && msg.to.trim().length > 0) return false;
|
if (typeof msg.to === 'string' && msg.to.trim().length > 0) return false;
|
||||||
return isThoughtProtocolNoise(msg.text);
|
return isThoughtProtocolNoise(msg.text) || isTeamInternalControlMessageText(msg.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TimelineItem =
|
export type TimelineItem =
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,41 @@ const baseTask: TeamTaskWithKanban = {
|
||||||
|
|
||||||
const noop = (): void => undefined;
|
const noop = (): void => undefined;
|
||||||
|
|
||||||
|
async function renderTaskCard(
|
||||||
|
props: Partial<React.ComponentProps<typeof KanbanTaskCard>> = {}
|
||||||
|
): Promise<{ host: HTMLDivElement; root: ReturnType<typeof createRoot> }> {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(KanbanTaskCard, {
|
||||||
|
task: baseTask,
|
||||||
|
teamName: 'my-team',
|
||||||
|
columnId: 'in_progress',
|
||||||
|
hasReviewers: true,
|
||||||
|
compact: false,
|
||||||
|
taskMap: new Map(),
|
||||||
|
memberColorMap: new Map([['alice', 'blue']]),
|
||||||
|
onRequestReview: noop,
|
||||||
|
onApprove: noop,
|
||||||
|
onRequestChanges: noop,
|
||||||
|
onMoveBackToDone: noop,
|
||||||
|
onStartTask: noop,
|
||||||
|
onCompleteTask: noop,
|
||||||
|
onCancelTask: noop,
|
||||||
|
onViewChanges: noop,
|
||||||
|
...props,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
return { host, root };
|
||||||
|
}
|
||||||
|
|
||||||
describe('KanbanTaskCard change badge', () => {
|
describe('KanbanTaskCard change badge', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
|
|
@ -197,3 +232,45 @@ describe('KanbanTaskCard change badge', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('KanbanTaskCard blocked border', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('highlights blocked tasks outside final columns', async () => {
|
||||||
|
const { host, root } = await renderTaskCard({
|
||||||
|
task: { ...baseTask, blockedBy: ['task-2'] },
|
||||||
|
columnId: 'in_progress',
|
||||||
|
});
|
||||||
|
|
||||||
|
const card = host.querySelector('[data-task-id="task-1"]');
|
||||||
|
expect(card?.className).toContain('kanban-task-card');
|
||||||
|
expect(card?.className).toContain('border-yellow-500/30');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['done', 'approved'] as const)(
|
||||||
|
'does not highlight blocked tasks in %s',
|
||||||
|
async (columnId) => {
|
||||||
|
const { host, root } = await renderTaskCard({
|
||||||
|
task: { ...baseTask, blockedBy: ['task-2'] },
|
||||||
|
columnId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const card = host.querySelector('[data-task-id="task-1"]');
|
||||||
|
expect(card?.className).not.toContain('border-yellow-500/30');
|
||||||
|
expect(card?.className).toContain('border-[var(--color-border)]');
|
||||||
|
expect(host.textContent).toContain('Blocked by');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,7 @@ export const KanbanTaskCard = memo(
|
||||||
const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? [];
|
const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? [];
|
||||||
const hasBlockedBy = blockedByIds.length > 0;
|
const hasBlockedBy = blockedByIds.length > 0;
|
||||||
const hasBlocks = blocksIds.length > 0;
|
const hasBlocks = blocksIds.length > 0;
|
||||||
|
const shouldHighlightBlocked = hasBlockedBy && columnId !== 'done' && columnId !== 'approved';
|
||||||
const cardSurfaceClass = isLight ? 'bg-white' : 'bg-[var(--color-surface-raised)]';
|
const cardSurfaceClass = isLight ? 'bg-white' : 'bg-[var(--color-surface-raised)]';
|
||||||
|
|
||||||
const taskChangeRequestOptions = useMemo(() => buildTaskChangeRequestOptions(task), [task]);
|
const taskChangeRequestOptions = useMemo(() => buildTaskChangeRequestOptions(task), [task]);
|
||||||
|
|
@ -288,8 +289,8 @@ export const KanbanTaskCard = memo(
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-task-id={task.id}
|
data-task-id={task.id}
|
||||||
className={`relative cursor-pointer rounded-md border px-1.5 py-3 transition-colors hover:border-[var(--color-border-emphasis)] ${
|
className={`kanban-task-card relative cursor-pointer rounded-md border px-1.5 py-3 hover:border-[var(--color-border-emphasis)] ${
|
||||||
hasBlockedBy
|
shouldHighlightBlocked
|
||||||
? `border-yellow-500/30 ${cardSurfaceClass}`
|
? `border-yellow-500/30 ${cardSurfaceClass}`
|
||||||
: `border-[var(--color-border)] ${cardSurfaceClass}`
|
: `border-[var(--color-border)] ${cardSurfaceClass}`
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@
|
||||||
/* Subtle borders */
|
/* Subtle borders */
|
||||||
--color-border-emphasis: rgba(148, 163, 184, 0.12);
|
--color-border-emphasis: rgba(148, 163, 184, 0.12);
|
||||||
/* Emphasis borders */
|
/* Emphasis borders */
|
||||||
|
--kanban-task-card-hover-shadow:
|
||||||
|
0 0 0 1px rgba(129, 140, 248, 0.28), 0 10px 30px rgba(37, 99, 235, 0.24),
|
||||||
|
0 0 22px rgba(129, 140, 248, 0.16);
|
||||||
--color-text: #f1f5f9;
|
--color-text: #f1f5f9;
|
||||||
--color-text-secondary: #94a3b8;
|
--color-text-secondary: #94a3b8;
|
||||||
--color-text-muted: #64748b;
|
--color-text-muted: #64748b;
|
||||||
|
|
@ -269,6 +272,19 @@
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kanban-task-card {
|
||||||
|
box-shadow: none;
|
||||||
|
transition:
|
||||||
|
border-color 140ms ease,
|
||||||
|
box-shadow 140ms ease,
|
||||||
|
background-color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-task-card:hover,
|
||||||
|
.kanban-task-card:focus-visible {
|
||||||
|
box-shadow: var(--kanban-task-card-hover-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.kanban-grid-item-wrapper {
|
.kanban-grid-item-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -466,6 +482,9 @@
|
||||||
/* Warm subtle border */
|
/* Warm subtle border */
|
||||||
--color-border-emphasis: #a8a5a0;
|
--color-border-emphasis: #a8a5a0;
|
||||||
/* Warm emphasis border */
|
/* Warm emphasis border */
|
||||||
|
--kanban-task-card-hover-shadow:
|
||||||
|
0 0 0 1px rgba(37, 99, 235, 0.28), 0 10px 26px rgba(37, 99, 235, 0.18),
|
||||||
|
0 0 18px rgba(79, 70, 229, 0.12);
|
||||||
--color-text: #1c1b19;
|
--color-text: #1c1b19;
|
||||||
/* Warm near-black text */
|
/* Warm near-black text */
|
||||||
--color-text-secondary: #4d4b46;
|
--color-text-secondary: #4d4b46;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import {
|
||||||
getTeamModelLabel,
|
getTeamModelLabel,
|
||||||
getTeamProviderLabel,
|
getTeamProviderLabel,
|
||||||
} from '@renderer/utils/teamModelCatalog';
|
} from '@renderer/utils/teamModelCatalog';
|
||||||
|
import {
|
||||||
|
isNativeAppManagedBootstrapCheckText,
|
||||||
|
isTeamInternalControlMessageText,
|
||||||
|
} from '@shared/utils/teamInternalControlMessages';
|
||||||
|
|
||||||
import type { InboxMessage, TeamProviderId } from '@shared/types';
|
import type { InboxMessage, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -125,6 +129,29 @@ export interface BootstrapAcknowledgementDisplay {
|
||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InternalControlMessageDisplay {
|
||||||
|
summary: string;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInternalControlMessageDisplay(
|
||||||
|
message: Pick<InboxMessage, 'text'>
|
||||||
|
): InternalControlMessageDisplay | null {
|
||||||
|
if (isNativeAppManagedBootstrapCheckText(message.text)) {
|
||||||
|
return {
|
||||||
|
summary: 'Internal bootstrap check',
|
||||||
|
body: 'Internal bootstrap check hidden in the UI.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!isTeamInternalControlMessageText(message.text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
summary: 'Internal control message',
|
||||||
|
body: 'Internal control message hidden in the UI.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getBootstrapPromptDisplay(
|
export function getBootstrapPromptDisplay(
|
||||||
message: Pick<InboxMessage, 'text' | 'to'>
|
message: Pick<InboxMessage, 'text' | 'to'>
|
||||||
): BootstrapPromptDisplay | null {
|
): BootstrapPromptDisplay | null {
|
||||||
|
|
@ -211,6 +238,7 @@ export function getBootstrapAcknowledgementDisplay(
|
||||||
|
|
||||||
export function getSanitizedInboxMessageText(message: Pick<InboxMessage, 'text' | 'to'>): string {
|
export function getSanitizedInboxMessageText(message: Pick<InboxMessage, 'text' | 'to'>): string {
|
||||||
return (
|
return (
|
||||||
|
getInternalControlMessageDisplay(message)?.body ??
|
||||||
getBootstrapPromptDisplay(message)?.body ??
|
getBootstrapPromptDisplay(message)?.body ??
|
||||||
getBootstrapAcknowledgementDisplay(message as Pick<InboxMessage, 'text' | 'from'>)?.body ??
|
getBootstrapAcknowledgementDisplay(message as Pick<InboxMessage, 'text' | 'from'>)?.body ??
|
||||||
message.text ??
|
message.text ??
|
||||||
|
|
@ -222,6 +250,7 @@ export function getSanitizedInboxMessageSummary(
|
||||||
message: Pick<InboxMessage, 'text' | 'to' | 'from' | 'summary'>
|
message: Pick<InboxMessage, 'text' | 'to' | 'from' | 'summary'>
|
||||||
): string {
|
): string {
|
||||||
return (
|
return (
|
||||||
|
getInternalControlMessageDisplay(message)?.summary ??
|
||||||
getBootstrapPromptDisplay(message)?.summary ??
|
getBootstrapPromptDisplay(message)?.summary ??
|
||||||
getBootstrapAcknowledgementDisplay(message)?.summary ??
|
getBootstrapAcknowledgementDisplay(message)?.summary ??
|
||||||
message.summary ??
|
message.summary ??
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
} from '@renderer/utils/bootstrapPromptSanitizer';
|
} from '@renderer/utils/bootstrapPromptSanitizer';
|
||||||
import { shouldKeepIdleMessageInActivityWhenNoiseHidden } from '@renderer/utils/idleNotificationSemantics';
|
import { shouldKeepIdleMessageInActivityWhenNoiseHidden } from '@renderer/utils/idleNotificationSemantics';
|
||||||
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
||||||
|
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
||||||
|
|
||||||
import type { InboxMessage } from '@shared/types';
|
import type { InboxMessage } from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -125,7 +126,10 @@ export function filterTeamMessages(
|
||||||
} = options;
|
} = options;
|
||||||
const leadNames = normalizeLeadNames(rawLeadNames);
|
const leadNames = normalizeLeadNames(rawLeadNames);
|
||||||
|
|
||||||
let list = messages.filter((m) => m.messageKind !== 'task_comment_notification');
|
let list = messages.filter(
|
||||||
|
(m) =>
|
||||||
|
m.messageKind !== 'task_comment_notification' && !isTeamInternalControlMessageText(m.text)
|
||||||
|
);
|
||||||
if (timeWindow) {
|
if (timeWindow) {
|
||||||
list = list.filter((m) => {
|
list = list.filter((m) => {
|
||||||
const ts = new Date(m.timestamp).getTime();
|
const ts = new Date(m.timestamp).getTime();
|
||||||
|
|
|
||||||
47
src/shared/utils/teamInternalControlMessages.ts
Normal file
47
src/shared/utils/teamInternalControlMessages.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
const NATIVE_APP_MANAGED_BOOTSTRAP_CHECK_OPEN = '<agent_teams_native_app_managed_bootstrap_check>';
|
||||||
|
const LEAD_INBOX_RELAY_PROMPT_OPEN = 'You have new inbox messages addressed to you (team lead ';
|
||||||
|
const TEAMMATE_MESSAGE_OPEN_RE = /^<teammate-message\s/i;
|
||||||
|
|
||||||
|
function stripTranscriptSpeakerPrefix(value: string): string {
|
||||||
|
let normalized = value.trim();
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
const next = normalized.replace(/^(?:Human|User):\s*/i, '').trimStart();
|
||||||
|
if (next === normalized) break;
|
||||||
|
normalized = next;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNativeAppManagedBootstrapCheckText(value: unknown): boolean {
|
||||||
|
return (
|
||||||
|
typeof value === 'string' &&
|
||||||
|
stripTranscriptSpeakerPrefix(value).includes(NATIVE_APP_MANAGED_BOOTSTRAP_CHECK_OPEN)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLeadInboxRelayControlPromptText(value: unknown): boolean {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const text = stripTranscriptSpeakerPrefix(value);
|
||||||
|
return (
|
||||||
|
text.startsWith(LEAD_INBOX_RELAY_PROMPT_OPEN) &&
|
||||||
|
text.includes('Process them in order (oldest first).') &&
|
||||||
|
text.includes('\nMessages:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTeammateProtocolControlText(value: unknown): boolean {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return TEAMMATE_MESSAGE_OPEN_RE.test(stripTranscriptSpeakerPrefix(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTeamInternalControlMessageText(value: unknown): boolean {
|
||||||
|
return (
|
||||||
|
isNativeAppManagedBootstrapCheckText(value) ||
|
||||||
|
isLeadInboxRelayControlPromptText(value) ||
|
||||||
|
isTeammateProtocolControlText(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -83,8 +83,8 @@ describe('OpenCode production prompt artifacts safe e2e', () => {
|
||||||
for (const member of launchCommand?.members ?? []) {
|
for (const member of launchCommand?.members ?? []) {
|
||||||
expect(member.prompt).toContain(`You are ${member.name}`);
|
expect(member.prompt).toContain(`You are ${member.name}`);
|
||||||
expect(member.prompt).toContain('Team launch context:');
|
expect(member.prompt).toContain('Team launch context:');
|
||||||
expect(member.prompt).toContain('agent-teams_member_briefing');
|
expect(member.prompt).toContain('agent_teams_app_managed_bootstrap_briefing');
|
||||||
expect(member.prompt).toContain('"runtimeProvider": "opencode"');
|
expect(member.prompt).toContain('AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1');
|
||||||
expect(member.prompt).toContain('agent-teams_message_send');
|
expect(member.prompt).toContain('agent-teams_message_send');
|
||||||
expect(member.prompt).toContain('Launch bootstrap is a silent attach');
|
expect(member.prompt).toContain('Launch bootstrap is a silent attach');
|
||||||
expect(member.prompt).toContain('stay idle silently');
|
expect(member.prompt).toContain('stay idle silently');
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import * as path from 'path';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
||||||
createOpenCodeBridgeHandshakeIdentityHash,
|
createOpenCodeBridgeHandshakeIdentityHash,
|
||||||
type OpenCodeBridgeCommandName,
|
type OpenCodeBridgeCommandName,
|
||||||
type OpenCodeBridgeHandshake,
|
type OpenCodeBridgeHandshake,
|
||||||
|
|
@ -272,6 +273,8 @@ function peerIdentity(
|
||||||
'opencode.launchTeam',
|
'opencode.launchTeam',
|
||||||
'opencode.stopTeam',
|
'opencode.stopTeam',
|
||||||
],
|
],
|
||||||
|
opencodeAppManagedBootstrapContractVersion:
|
||||||
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
|
|
|
||||||
|
|
@ -5052,7 +5052,6 @@ describe('Team agent launch matrix safe e2e', () => {
|
||||||
trackLiveRun(svc, run);
|
trackLiveRun(svc, run);
|
||||||
|
|
||||||
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
||||||
await waitForCondition(() => adapter.launchInputs.length === 2);
|
|
||||||
await waitForCondition(() =>
|
await waitForCondition(() =>
|
||||||
run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished')
|
run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished')
|
||||||
);
|
);
|
||||||
|
|
@ -5094,7 +5093,6 @@ describe('Team agent launch matrix safe e2e', () => {
|
||||||
trackLiveRun(svc, run);
|
trackLiveRun(svc, run);
|
||||||
|
|
||||||
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
||||||
await waitForCondition(() => adapter.launchInputs.length === 2);
|
|
||||||
await waitForCondition(() =>
|
await waitForCondition(() =>
|
||||||
run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished')
|
run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished')
|
||||||
);
|
);
|
||||||
|
|
@ -5146,13 +5144,17 @@ describe('Team agent launch matrix safe e2e', () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter]));
|
svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter]));
|
||||||
const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' });
|
const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' });
|
||||||
|
removeMixedOpenCodeLaneForTest(run, 'bob');
|
||||||
trackLiveRun(svc, run);
|
trackLiveRun(svc, run);
|
||||||
|
|
||||||
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
||||||
await waitForCondition(() => adapter.launchInputs.length === 2);
|
await waitForCondition(() => adapter.launchInputs.length === 1);
|
||||||
await waitForCondition(() =>
|
await waitForCondition(() =>
|
||||||
run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished')
|
run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished')
|
||||||
);
|
);
|
||||||
|
expect(adapter.launchInputs.map((input) => input.expectedMembers.map((member) => member.name))).toEqual([
|
||||||
|
['tom'],
|
||||||
|
]);
|
||||||
|
|
||||||
const statuses = await svc.getMemberSpawnStatuses(teamName);
|
const statuses = await svc.getMemberSpawnStatuses(teamName);
|
||||||
|
|
||||||
|
|
@ -5215,15 +5217,19 @@ describe('Team agent launch matrix safe e2e', () => {
|
||||||
model: 'opencode/nemotron-3-super-free',
|
model: 'opencode/nemotron-3-super-free',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' });
|
const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' });
|
||||||
|
removeMixedOpenCodeLaneForTest(run, 'bob');
|
||||||
trackLiveRun(svc, run);
|
trackLiveRun(svc, run);
|
||||||
|
|
||||||
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
||||||
await waitForCondition(() => adapter.launchInputs.length === 2);
|
await waitForCondition(() => adapter.launchInputs.length === 1);
|
||||||
await waitForCondition(() =>
|
await waitForCondition(() =>
|
||||||
run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished')
|
run.mixedSecondaryLanes.every((lane: { state: string }) => lane.state === 'finished')
|
||||||
);
|
);
|
||||||
|
expect(adapter.launchInputs.map((input) => input.expectedMembers.map((member) => member.name))).toEqual([
|
||||||
|
['tom'],
|
||||||
|
]);
|
||||||
|
|
||||||
const statuses = await svc.getMemberSpawnStatuses(teamName);
|
const statuses = await svc.getMemberSpawnStatuses(teamName);
|
||||||
|
|
||||||
|
|
@ -17443,6 +17449,15 @@ function markMixedOpenCodeLaneConfirmedForTest(run: any, memberName: string): vo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeMixedOpenCodeLaneForTest(run: any, memberName: string): void {
|
||||||
|
run.allEffectiveMembers = (run.allEffectiveMembers ?? []).filter(
|
||||||
|
(member: { name?: string }) => member.name !== memberName
|
||||||
|
);
|
||||||
|
run.mixedSecondaryLanes = (run.mixedSecondaryLanes ?? []).filter(
|
||||||
|
(lane: { member?: { name?: string } }) => lane.member?.name !== memberName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function addGeminiPrimaryToMixedRun(run: any): void {
|
function addGeminiPrimaryToMixedRun(run: any): void {
|
||||||
const now = '2026-04-23T10:00:00.000Z';
|
const now = '2026-04-23T10:00:00.000Z';
|
||||||
const reviewer = {
|
const reviewer = {
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,29 @@ describe('TeamMessageFeedService', () => {
|
||||||
expect(second.messages).toHaveLength(1);
|
expect(second.messages).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides native app-managed bootstrap private control messages from the feed', async () => {
|
||||||
|
const service = new TeamMessageFeedService({
|
||||||
|
getConfig: vi.fn(async () => config),
|
||||||
|
getInboxMessages: vi.fn(async () => [
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'native-bootstrap-private-check',
|
||||||
|
source: 'system_notification',
|
||||||
|
text: '<agent_teams_native_app_managed_bootstrap_check>\nprivate\n</agent_teams_native_app_managed_bootstrap_check>',
|
||||||
|
}),
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'visible-user-message',
|
||||||
|
text: 'Visible message',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
getLeadSessionMessages: vi.fn(async () => []),
|
||||||
|
getSentMessages: vi.fn(async () => []),
|
||||||
|
});
|
||||||
|
|
||||||
|
const feed = await service.getFeed('signal-ops-4');
|
||||||
|
|
||||||
|
expect(feed.messages.map((message) => message.messageId)).toEqual(['visible-user-message']);
|
||||||
|
});
|
||||||
|
|
||||||
it('refreshes the durable feed after cache expiry even when the dirty signal was missed', async () => {
|
it('refreshes the durable feed after cache expiry even when the dirty signal was missed', async () => {
|
||||||
let inboxMessages: InboxMessage[] = [makeMessage()];
|
let inboxMessages: InboxMessage[] = [makeMessage()];
|
||||||
const getInboxMessages = vi.fn(async () => inboxMessages);
|
const getInboxMessages = vi.fn(async () => inboxMessages);
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,42 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
|
||||||
expect(service.getLiveLeadProcessMessages(teamName)).toHaveLength(1);
|
expect(service.getLiveLeadProcessMessages(teamName)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not persist echoed lead relay prompts as user-visible replies', async () => {
|
||||||
|
const service = new TeamProvisioningService();
|
||||||
|
const teamName = 'my-team';
|
||||||
|
seedConfig(teamName);
|
||||||
|
seedLeadInbox(teamName, [
|
||||||
|
{
|
||||||
|
from: 'tom',
|
||||||
|
text: '#f8d7235a done.',
|
||||||
|
timestamp: '2026-02-23T10:00:00.000Z',
|
||||||
|
read: false,
|
||||||
|
summary: '#f8d7235a done',
|
||||||
|
messageId: 'm-1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { writeSpy } = attachAliveRun(service, teamName);
|
||||||
|
const relayPromise = service.relayLeadInboxMessages(teamName);
|
||||||
|
const run = await waitForCapture(service);
|
||||||
|
const payload = JSON.parse(String(writeSpy.mock.calls[0]?.[0] ?? '{}')) as {
|
||||||
|
message?: { content?: Array<{ text?: string }> };
|
||||||
|
};
|
||||||
|
const relayedPrompt = payload.message?.content?.[0]?.text ?? '';
|
||||||
|
|
||||||
|
expect(relayedPrompt).toContain('You have new inbox messages addressed to you');
|
||||||
|
|
||||||
|
(service as any).handleStreamJsonMessage(run, {
|
||||||
|
type: 'assistant',
|
||||||
|
content: [{ type: 'text', text: `Human: ${relayedPrompt}` }],
|
||||||
|
});
|
||||||
|
(service as any).handleStreamJsonMessage(run, { type: 'result', subtype: 'success' });
|
||||||
|
|
||||||
|
await expect(relayPromise).resolves.toBe(1);
|
||||||
|
expect(service.getLiveLeadProcessMessages(teamName)).toHaveLength(0);
|
||||||
|
expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('treats member work sync nudges as actionable in lead relay prompt', async () => {
|
it('treats member work sync nudges as actionable in lead relay prompt', async () => {
|
||||||
const service = new TeamProvisioningService();
|
const service = new TeamProvisioningService();
|
||||||
const teamName = 'my-team';
|
const teamName = 'my-team';
|
||||||
|
|
@ -436,6 +472,37 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not show internal control echoes as late lead thoughts', () => {
|
||||||
|
const service = new TeamProvisioningService();
|
||||||
|
const teamName = 'my-team';
|
||||||
|
seedConfig(teamName);
|
||||||
|
attachAliveRun(service, teamName);
|
||||||
|
|
||||||
|
const run = (service as unknown as { runs: Map<string, unknown> }).runs.get('run-1') as {
|
||||||
|
leadRelayCapture: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
(service as any).handleStreamJsonMessage(run, {
|
||||||
|
type: 'assistant',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Human: You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
Process them in order (oldest first).
|
||||||
|
If action is required, delegate via task creation or SendMessage, and keep responses minimal.
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
1) From: tom
|
||||||
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
|
Text:
|
||||||
|
#f8d7235a done.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.getLiveLeadProcessMessages(teamName)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('adds substantive-only task comment guidance for lead relay prompts', async () => {
|
it('adds substantive-only task comment guidance for lead relay prompts', async () => {
|
||||||
const service = new TeamProvisioningService();
|
const service = new TeamProvisioningService();
|
||||||
const teamName = 'my-team';
|
const teamName = 'my-team';
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,25 @@ describe('LeadThoughtsGroup', () => {
|
||||||
expect(groupTimelineItems([noise])).toEqual([]);
|
expect(groupTimelineItems([noise])).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('excludes Human-prefixed internal control echoes from timeline', () => {
|
||||||
|
const leadRelayEcho = makeLeadSessionMsg(`Human: You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
Process them in order (oldest first).
|
||||||
|
If action is required, delegate via task creation or SendMessage, and keep responses minimal.
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
1) From: tom
|
||||||
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
|
Text:
|
||||||
|
#f8d7235a done.`);
|
||||||
|
const teammateEcho = makeLeadSessionMsg(
|
||||||
|
'Human: <teammate-message teammate_id="alice">{"type":"idle_notification"}</teammate-message>'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isLeadThought(leadRelayEcho)).toBe(false);
|
||||||
|
expect(isLeadThought(teammateEcho)).toBe(false);
|
||||||
|
expect(groupTimelineItems([leadRelayEcho, teammateEcho])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not exclude noise messages with a recipient (captured SendMessage)', () => {
|
it('does not exclude noise messages with a recipient (captured SendMessage)', () => {
|
||||||
const sendMsg = makeLeadSessionMsg(
|
const sendMsg = makeLeadSessionMsg(
|
||||||
'{"type":"idle_notification","from":"tom","idleReason":"available"}',
|
'{"type":"idle_notification","from":"tom","idleReason":"available"}',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getInternalControlMessageDisplay,
|
||||||
getBootstrapPromptDisplay,
|
getBootstrapPromptDisplay,
|
||||||
getSanitizedInboxMessageText,
|
getSanitizedInboxMessageText,
|
||||||
} from '@renderer/utils/bootstrapPromptSanitizer';
|
} from '@renderer/utils/bootstrapPromptSanitizer';
|
||||||
|
|
@ -64,4 +65,28 @@ Do NOT send acknowledgement-only messages such as "ready" or "online".`);
|
||||||
|
|
||||||
expect(display?.runtime).toBe('GPT-5.4 Mini');
|
expect(display?.runtime).toBe('GPT-5.4 Mini');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sanitizes native app-managed bootstrap private control prompts defensively', () => {
|
||||||
|
const message = makeMessage(`<agent_teams_native_app_managed_bootstrap_check>
|
||||||
|
Your Agent Teams startup context was already loaded by the app.
|
||||||
|
</agent_teams_native_app_managed_bootstrap_check>`);
|
||||||
|
|
||||||
|
expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal bootstrap check');
|
||||||
|
expect(getSanitizedInboxMessageText(message)).toBe('Internal bootstrap check hidden in the UI.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sanitizes leaked lead inbox relay prompts defensively', () => {
|
||||||
|
const message = makeMessage(`Human: You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
Process them in order (oldest first).
|
||||||
|
If action is required, delegate via task creation or SendMessage, and keep responses minimal.
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
1) From: tom
|
||||||
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
|
Text:
|
||||||
|
#f8d7235a done.`);
|
||||||
|
|
||||||
|
expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal control message');
|
||||||
|
expect(getSanitizedInboxMessageText(message)).toBe('Internal control message hidden in the UI.');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,81 @@ describe('filterTeamMessages', () => {
|
||||||
expect(result[0].source).toBe('lead_process');
|
expect(result[0].source).toBe('lead_process');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides native app-managed bootstrap private control messages', () => {
|
||||||
|
const messages = [
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'native-bootstrap-private-check',
|
||||||
|
source: 'system_notification',
|
||||||
|
text: '<agent_teams_native_app_managed_bootstrap_check>\nprivate\n</agent_teams_native_app_managed_bootstrap_check>',
|
||||||
|
}),
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'visible-message',
|
||||||
|
text: 'Visible message',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = filterTeamMessages(messages, {
|
||||||
|
timeWindow: null,
|
||||||
|
filter: { from: new Set(), to: new Set(), showNoise: true },
|
||||||
|
searchQuery: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.map((message) => message.messageId)).toEqual(['visible-message']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides leaked lead inbox relay prompt echoes', () => {
|
||||||
|
const messages = [
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'lead-relay-echo',
|
||||||
|
source: 'lead_process',
|
||||||
|
to: 'user',
|
||||||
|
text: `Human: You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
Process them in order (oldest first).
|
||||||
|
If action is required, delegate via task creation or SendMessage, and keep responses minimal.
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
1) From: tom
|
||||||
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
|
Text:
|
||||||
|
#f8d7235a done.`,
|
||||||
|
}),
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'visible-message',
|
||||||
|
text: 'Visible message',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = filterTeamMessages(messages, {
|
||||||
|
timeWindow: null,
|
||||||
|
filter: { from: new Set(), to: new Set(), showNoise: true },
|
||||||
|
searchQuery: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.map((message) => message.messageId)).toEqual(['visible-message']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides Human-prefixed teammate protocol echoes', () => {
|
||||||
|
const messages = [
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'teammate-protocol-echo',
|
||||||
|
source: 'lead_process',
|
||||||
|
text: 'Human: <teammate-message teammate_id="alice">{"type":"idle_notification"}</teammate-message>',
|
||||||
|
}),
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'visible-message',
|
||||||
|
text: 'Visible message',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = filterTeamMessages(messages, {
|
||||||
|
timeWindow: null,
|
||||||
|
filter: { from: new Set(), to: new Set(), showNoise: true },
|
||||||
|
searchQuery: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.map((message) => message.messageId)).toEqual(['visible-message']);
|
||||||
|
});
|
||||||
|
|
||||||
it('hides relay bridge copies when the original message is visible', () => {
|
it('hides relay bridge copies when the original message is visible', () => {
|
||||||
const messages = [
|
const messages = [
|
||||||
makeMessage({
|
makeMessage({
|
||||||
|
|
|
||||||
42
test/shared/utils/teamInternalControlMessages.test.ts
Normal file
42
test/shared/utils/teamInternalControlMessages.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
isLeadInboxRelayControlPromptText,
|
||||||
|
isTeamInternalControlMessageText,
|
||||||
|
isTeammateProtocolControlText,
|
||||||
|
} from '@shared/utils/teamInternalControlMessages';
|
||||||
|
|
||||||
|
const leadRelayPrompt = `You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
Process them in order (oldest first).
|
||||||
|
If action is required, delegate via task creation or SendMessage, and keep responses minimal.
|
||||||
|
IMPORTANT: Your text response here is shown to the user.
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
1) From: tom
|
||||||
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
|
Text:
|
||||||
|
#f8d7235a done.`;
|
||||||
|
|
||||||
|
describe('teamInternalControlMessages', () => {
|
||||||
|
it('detects lead inbox relay prompts and Human-prefixed echoes', () => {
|
||||||
|
expect(isLeadInboxRelayControlPromptText(leadRelayPrompt)).toBe(true);
|
||||||
|
expect(isLeadInboxRelayControlPromptText(`Human: ${leadRelayPrompt}`)).toBe(true);
|
||||||
|
expect(isTeamInternalControlMessageText(`Human: ${leadRelayPrompt}`)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not hide ordinary visible lead replies', () => {
|
||||||
|
expect(
|
||||||
|
isLeadInboxRelayControlPromptText(
|
||||||
|
'I delegated #f8d7235a to tom and asked alice to review when blockers clear.'
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects Human-prefixed teammate protocol blocks', () => {
|
||||||
|
const text =
|
||||||
|
'Human: <teammate-message teammate_id="alice">\n{"type":"idle_notification"}\n</teammate-message>';
|
||||||
|
|
||||||
|
expect(isTeammateProtocolControlText(text)).toBe(true);
|
||||||
|
expect(isTeamInternalControlMessageText(text)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue