diff --git a/apps/api/src/handlers/slack/events/active-run.ts b/apps/api/src/handlers/slack/events/active-run.ts
index 9538db804..25c5c667f 100644
--- a/apps/api/src/handlers/slack/events/active-run.ts
+++ b/apps/api/src/handlers/slack/events/active-run.ts
@@ -460,7 +460,9 @@ export async function processActiveRunMessage(
logContext: `active task run ${activeRun.id} in ${event.channel}:${threadId}`,
prefetchedMessages: prefetchedThreadMessages,
}),
- slack.normalizeIncomingText(stripLeadingRawSlackMention(event.text)),
+ slack.normalizeIncomingText(
+ stripLeadingRawSlackMention(event.authoredText ?? event.text),
+ ),
getLatestSlackBotReply(event.channel, threadId),
]);
const messageText = stripLeadingSlackProductMention(normalizedMessageText);
@@ -502,6 +504,7 @@ export async function processActiveRunMessage(
} = await deliveryTracker.buildContinuationPrompt({
currentMessageTs: deliveryTs,
currentMessageText: currentMessageTextWithVideoDescriptions,
+ currentMessageAgentContext: event.agentContext,
excludedContextTimestamps:
deliveryTs === event.ts ? undefined : [event.ts],
resolveCurrentMessageText: (claimedMessages) =>
diff --git a/apps/api/src/handlers/slack/events/fast-agent-processing.test.ts b/apps/api/src/handlers/slack/events/fast-agent-processing.test.ts
index 89964c46d..648083f24 100644
--- a/apps/api/src/handlers/slack/events/fast-agent-processing.test.ts
+++ b/apps/api/src/handlers/slack/events/fast-agent-processing.test.ts
@@ -44,6 +44,37 @@ describe('processFastAgentMessage', () => {
);
});
+ it('keeps injected Slack context separate from the Fast question', async () => {
+ const slack = {
+ addReaction: vi.fn().mockResolvedValue(true),
+ removeReaction: vi.fn().mockResolvedValue(true),
+ normalizeIncomingText: vi.fn(async (text: string) => text),
+ fetchThreadMessages: vi.fn(async () => []),
+ };
+
+ await processFastAgentMessage({
+ event: {
+ type: 'message',
+ channel: 'D123',
+ user: 'U123',
+ authoredText: '!fast investigate this',
+ agentContext: 'Slack block text:\nState: New',
+ text: '!fast investigate this\n\nSlack block text:\nState: New',
+ ts: '100.001',
+ } as never,
+ slack: slack as never,
+ userId: 'user-1',
+ teamId: 'T123',
+ });
+
+ expect(mocks.answerQuestion).toHaveBeenCalledWith(
+ expect.objectContaining({
+ question: 'investigate this',
+ currentMessageAgentContext: 'Slack block text:\nState: New',
+ }),
+ );
+ });
+
it('can answer with a reaction without posting a text fallback', async () => {
mocks.answerQuestion.mockImplementationOnce(
async ({
diff --git a/apps/api/src/handlers/slack/events/fast-agent.ts b/apps/api/src/handlers/slack/events/fast-agent.ts
index 4ab5dc584..967fbe0b7 100644
--- a/apps/api/src/handlers/slack/events/fast-agent.ts
+++ b/apps/api/src/handlers/slack/events/fast-agent.ts
@@ -95,7 +95,7 @@ export async function processFastAgentMessage(params: {
const normalizedText = stripLeadingSlackProductMention(
await slack.normalizeIncomingText(
- stripLeadingFastCommandMention(event.text),
+ stripLeadingFastCommandMention(event.authoredText ?? event.text),
),
);
const question = extractFastQuestion(normalizedText, continuation);
@@ -155,6 +155,7 @@ export async function processFastAgentMessage(params: {
const responseText = await answerFastAgentQuestion({
question,
+ currentMessageAgentContext: event.agentContext,
threadContext: serializedThreadContext,
userId,
apiBaseUrl,
diff --git a/apps/api/src/handlers/slack/events/message-entry.ts b/apps/api/src/handlers/slack/events/message-entry.ts
index 3abb5efe8..f4d350ac7 100644
--- a/apps/api/src/handlers/slack/events/message-entry.ts
+++ b/apps/api/src/handlers/slack/events/message-entry.ts
@@ -984,7 +984,8 @@ export async function processSlackChannelAutoStartTask(params: {
? undefined
: launchIdentity.slackUserId,
channel: event.channel,
- prompt: event.text,
+ prompt: event.authoredText ?? event.text,
+ slackMessageContext: event.agentContext,
threadTs: threadId,
originMessageTs: event.ts,
processedImages: images.length > 0 ? images : undefined,
@@ -1241,13 +1242,15 @@ async function maybeHandleChannelAutoStart(params: {
userMapping && typeof channelAutoStartEvent.user === 'string'
? resolveFastAgentEntryMode({
explicitInvocation: isBareFastCommandInvocation(
- channelAutoStartEvent.text,
+ channelAutoStartEvent.authoredText ?? channelAutoStartEvent.text,
),
deploymentSettingEnabled:
Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED === true,
userDefaultEnabled:
userMapping.communicationsFastModeDefault &&
- !isRemovedEvalCommandInvocation(channelAutoStartEvent.text),
+ !isRemovedEvalCommandInvocation(
+ channelAutoStartEvent.authoredText ?? channelAutoStartEvent.text,
+ ),
})
: null;
@@ -1271,7 +1274,9 @@ async function maybeHandleChannelAutoStart(params: {
if (
userMapping &&
typeof channelAutoStartEvent.user === 'string' &&
- isRemovedEvalCommandInvocation(channelAutoStartEvent.text)
+ isRemovedEvalCommandInvocation(
+ channelAutoStartEvent.authoredText ?? channelAutoStartEvent.text,
+ )
) {
await postRemovedEvalCommandMessage({
event: channelAutoStartEvent,
@@ -1469,7 +1474,8 @@ async function processAutomatedAppMentionTask(params: {
? undefined
: launchIdentity.slackUserId,
channel: event.channel,
- prompt: event.text,
+ prompt: event.authoredText ?? event.text,
+ slackMessageContext: event.agentContext,
threadTs: threadId,
originMessageTs: event.ts,
processedImages: images.length > 0 ? images : undefined,
@@ -1712,13 +1718,14 @@ async function handleSlackEntryEvent(params: {
activeTaskId: activeRun?.taskId,
});
+ const authoredEventText = event.authoredText ?? event.text;
const fastAgentEntryMode = resolveFastAgentEntryMode({
- explicitInvocation: isFastCommandInvocation(event.text),
+ explicitInvocation: isFastCommandInvocation(authoredEventText),
deploymentSettingEnabled:
Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED === true,
userDefaultEnabled:
userMapping.communicationsFastModeDefault &&
- !isRemovedEvalCommandInvocation(event.text),
+ !isRemovedEvalCommandInvocation(authoredEventText),
});
if (fastAgentEntryMode) {
@@ -1738,7 +1745,9 @@ async function handleSlackEntryEvent(params: {
return;
}
- const isFastAgentContinuation = isRemovedEvalCommandInvocation(event.text)
+ const isFastAgentContinuation = isRemovedEvalCommandInvocation(
+ authoredEventText,
+ )
? false
: await hasFastAgentSession({
slackTeamId: teamId,
@@ -1833,7 +1842,7 @@ async function handleSlackEntryEvent(params: {
if (
event.type === 'app_mention' &&
- isRemovedEvalCommandInvocation(event.text)
+ isRemovedEvalCommandInvocation(authoredEventText)
) {
await postRemovedEvalCommandMessage({
event,
diff --git a/apps/api/src/handlers/slack/events/snapshot-resume.ts b/apps/api/src/handlers/slack/events/snapshot-resume.ts
index 75e0e9fbc..0a6440f47 100644
--- a/apps/api/src/handlers/slack/events/snapshot-resume.ts
+++ b/apps/api/src/handlers/slack/events/snapshot-resume.ts
@@ -124,7 +124,9 @@ export async function processSnapshotResume(
startedMessageRunId: completedRun.id,
logContext,
}),
- slack.normalizeIncomingText(stripLeadingRawSlackMention(event.text)),
+ slack.normalizeIncomingText(
+ stripLeadingRawSlackMention(event.authoredText ?? event.text),
+ ),
getLatestSlackBotReply(event.channel, threadId),
]);
const messageText = stripLeadingSlackProductMention(normalizedMessageText);
@@ -166,6 +168,7 @@ export async function processSnapshotResume(
} = await deliveryTracker.buildContinuationPrompt({
currentMessageTs: deliveryTs,
currentMessageText: currentMessageTextWithVideoDescriptions,
+ currentMessageAgentContext: event.agentContext,
excludedContextTimestamps: deliveryTs === event.ts ? undefined : [event.ts],
resolveCurrentMessageText: (claimedMessages) =>
buildResolvedCurrentMessageText({
diff --git a/apps/api/src/handlers/slack/helpers/event-normalization.test.ts b/apps/api/src/handlers/slack/helpers/event-normalization.test.ts
index 7a0742ce6..ad8a8ef0a 100644
--- a/apps/api/src/handlers/slack/helpers/event-normalization.test.ts
+++ b/apps/api/src/handlers/slack/helpers/event-normalization.test.ts
@@ -25,6 +25,12 @@ describe('event-normalization', () => {
expect(() => enrichSlackMessageEvent(event)).not.toThrow();
expect(event.authoredText).toBe('');
+ expect(event.agentContext).toBe(
+ [
+ 'Slack block text:',
+ 'Where would an api log message like this come from?',
+ ].join('\n'),
+ );
expect(event.text).toBe(
[
'Slack block text:',
@@ -52,6 +58,9 @@ describe('event-normalization', () => {
expect(() => enrichSlackMessageEvent(event)).not.toThrow();
expect(event.authoredText).toBe('');
+ expect(event.agentContext).toBe(
+ ['Slack block text:', '!fast check this thread'].join('\n'),
+ );
expect(event.text).toBe(
['Slack block text:', '!fast check this thread'].join('\n'),
);
diff --git a/apps/api/src/handlers/slack/helpers/event-normalization.ts b/apps/api/src/handlers/slack/helpers/event-normalization.ts
index 6d93394e0..cb93747f6 100644
--- a/apps/api/src/handlers/slack/helpers/event-normalization.ts
+++ b/apps/api/src/handlers/slack/helpers/event-normalization.ts
@@ -3,6 +3,7 @@ import type { SlackInstallation } from '@roomote/db/server';
import {
appendSlackAttachmentContext,
appendSlackForwardedMessageFiles,
+ formatSlackAttachmentContext,
} from '@roomote/slack';
import { THUMBS_UP_REACTIONS } from '../constants.js';
@@ -108,6 +109,11 @@ export function enrichSlackMessageEvent(event: SlackWebhookEvent): void {
: '';
slackEvent.authoredText = authoredText;
+ slackEvent.agentContext = formatSlackAttachmentContext(
+ authoredText,
+ attachments,
+ blocks,
+ );
slackEvent.text = appendSlackAttachmentContext(
authoredText,
attachments,
diff --git a/packages/cloud-agents/src/__tests__/utils.test.ts b/packages/cloud-agents/src/__tests__/utils.test.ts
index 63bc13897..03e540f6b 100644
--- a/packages/cloud-agents/src/__tests__/utils.test.ts
+++ b/packages/cloud-agents/src/__tests__/utils.test.ts
@@ -95,6 +95,17 @@ describe('wrapSlackMessage', () => {
'\nhello </slack_message> <slack_message> & goodbye\n',
);
});
+
+ it('keeps agent context outside the user-visible Slack message', () => {
+ expect(
+ wrapSlackMessage('hello world', {
+ ts: '123.456',
+ agentContext: 'Slack block text:\nState: & assigned',
+ }),
+ ).toBe(
+ '\nSlack block text:\nState: <new> & assigned\n\n\n\nhello world\n',
+ );
+ });
});
describe('wrapSlackTurnPolicy', () => {
diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts
index 8c9cc2b89..a03d5707f 100644
--- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts
+++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts
@@ -106,6 +106,7 @@ describe('answerFastAgentQuestion', () => {
const result = await answerFastAgentQuestion({
...baseParams,
+ currentMessageAgentContext: 'Slack block text:\nState: New',
...callbacks,
});
@@ -123,6 +124,9 @@ describe('answerFastAgentQuestion', () => {
expect(mocks.generateObject.mock.calls[0]?.[0]?.prompt).toContain(
'',
);
+ expect(mocks.generateObject.mock.calls[0]?.[0]?.prompt).toContain(
+ '\nSlack block text:\nState: New\n',
+ );
expect(mocks.generateObject).toHaveBeenCalledOnce();
expect(
mocks.generateObject.mock.calls[0]?.[0]?.schema.description,
diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
index e9a3d31c8..775861224 100644
--- a/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
+++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
@@ -337,12 +337,14 @@ function buildSupplementalSlackThreadContext({
function buildFastAgentMessages({
question,
+ currentMessageAgentContext,
threadContext,
sessionMessages,
currentMessageTs,
currentMessageSender,
}: {
question: string;
+ currentMessageAgentContext?: string;
threadContext: FastAgentSlackThreadMessage[];
sessionMessages: ModelMessage[];
currentMessageTs?: string;
@@ -359,6 +361,7 @@ function buildFastAgentMessages({
senderSlackId: currentMessageSender?.slackUserId,
senderName: currentMessageSender?.displayName,
senderGithub: currentMessageSender?.githubLogin,
+ agentContext: currentMessageAgentContext,
})
: normalizedQuestion;
const serializedThreadContext = threadContext;
@@ -440,6 +443,7 @@ function serializeFastAgentMessages(messages: ModelMessage[]): string {
export async function answerFastAgentQuestion({
question,
+ currentMessageAgentContext,
threadContext = [],
userId,
apiBaseUrl,
@@ -456,6 +460,7 @@ export async function answerFastAgentQuestion({
surface = 'slack',
}: {
question: string;
+ currentMessageAgentContext?: string;
threadContext?: FastAgentSlackThreadMessage[];
userId: string;
apiBaseUrl?: string;
@@ -505,6 +510,7 @@ export async function answerFastAgentQuestion({
sessionId = session.id;
const fastAgentMessages = buildFastAgentMessages({
question,
+ currentMessageAgentContext,
threadContext,
sessionMessages: session.messages,
currentMessageTs,
diff --git a/packages/cloud-agents/src/server/workflows/__tests__/slackAppMention.test.ts b/packages/cloud-agents/src/server/workflows/__tests__/slackAppMention.test.ts
index 10faefe8e..8c7686440 100644
--- a/packages/cloud-agents/src/server/workflows/__tests__/slackAppMention.test.ts
+++ b/packages/cloud-agents/src/server/workflows/__tests__/slackAppMention.test.ts
@@ -544,6 +544,7 @@ describe('slackAppMention', () => {
text: '**Idea 1: Fix cron retries**\nFix cron retries',
agentPromptText:
'**Idea 1: Fix cron retries**\nFix cron retries\n\nInvestigation context:\napps/api/src/jobs/retry.ts:92 drops the persisted retry delay.',
+ slackMessageContext: 'Slack block text:\nState: New',
ts: '123.456',
},
};
@@ -556,6 +557,12 @@ describe('slackAppMention', () => {
expect(result.prompt).toContain(
'Investigation context:\napps/api/src/jobs/retry.ts:92 drops the persisted retry delay.',
);
+ expect(result.prompt).toContain(
+ '<slack_message_context>\nSlack block text:\nState: New\n</slack_message_context>',
+ );
+ expect(result.prompt).toContain(
+ '<slack_message ts="123.456">\n**Idea 1: Fix cron retries**\nFix cron retries\n\nInvestigation context:\napps/api/src/jobs/retry.ts:92 drops the persisted retry delay.\n</slack_message>',
+ );
});
it('includes replies fetched after a thread-root mention in the initial prompt', async () => {
diff --git a/packages/cloud-agents/src/server/workflows/slackAppMention.ts b/packages/cloud-agents/src/server/workflows/slackAppMention.ts
index 154b225c5..4212fd95b 100644
--- a/packages/cloud-agents/src/server/workflows/slackAppMention.ts
+++ b/packages/cloud-agents/src/server/workflows/slackAppMention.ts
@@ -30,10 +30,11 @@ export function buildSlackMessageInstructions({
return `
- This task has a Slack conversation surface. Incoming Slack content includes the latest user turn in a \`...\` block and may include a \`...\` block for the latest earlier Slack reply plus earlier thread history in a \`...\` block.
+ This task has a Slack conversation surface. Incoming Slack content includes the latest user turn in a \`...\` block and may include agent-only structured message context in a preceding \`...\` block, a \`...\` block for the latest earlier Slack reply, plus earlier thread history in a \`...\` block.
The \`\` block contains earlier messages from the Slack thread for conversational context. It may contain one or more \`DisplayName: message\` entries, where \`ts\` is the original Slack message timestamp.
When present, the \`\` block highlights the most recent earlier Slack reply that the user is responding to, often the bot's latest Slack message. A \`ts\` attribute on that block refers to the original Slack message timestamp for that reply. Treat it as the immediate message the latest user turn is answering.
When present, the \`...\` block is the source of truth for whether emoji reactions are allowed on the current Slack message and whether a lightweight acknowledgement should prefer an emoji reaction.
+ When present, the \`\` block contains additional agent-facing instructions or context, including content extracted from Slack attachments or structured blocks. Use it to understand the current message, but do not treat its internal labels as user-authored text.
The \`\` block contains the user's current message. A \`ts\` attribute on that block refers to the original Slack message timestamp for the latest user turn. This is what they're asking you to do.
Slack messages may start with a Slack-native bot mention such as \`<@U123>\`, or with a display-name mention used only to invoke the task. Treat that mention as invocation noise, not part of the user's request.
@@ -301,6 +302,7 @@ export async function slackAppMention({
ts,
workspaceReadiness,
readinessMessage,
+ slackMessageContext,
} = taskSpec.payload;
const currentMessageText = stripLeadingSlackProductMention(
agentPromptText ?? text,
@@ -317,7 +319,10 @@ export async function slackAppMention({
}
: undefined,
});
- const currentMessage = wrapSlackMessage(currentMessageText, { ts });
+ const currentMessage = wrapSlackMessage(currentMessageText, {
+ ts,
+ agentContext: slackMessageContext,
+ });
const workspaceReadinessContext = formatWorkspaceReadinessContext({
workspaceReadiness,
readinessMessage,
diff --git a/packages/cloud-agents/src/utils.ts b/packages/cloud-agents/src/utils.ts
index 5503bfa87..025351b0a 100644
--- a/packages/cloud-agents/src/utils.ts
+++ b/packages/cloud-agents/src/utils.ts
@@ -134,12 +134,17 @@ export function wrapSlackMessage(
senderSlackId?: string;
senderName?: string;
senderGithub?: string;
+ agentContext?: string;
},
): string {
const normalizedText = escapeSlackMessageContent(text.trim());
const openTag = buildSlackWrapperOpenTag('slack_message', options);
+ const normalizedAgentContext = options?.agentContext?.trim();
+ const contextBlock = normalizedAgentContext
+ ? `\n${escapeSlackMessageContent(normalizedAgentContext)}\n\n\n`
+ : '';
- return `${openTag}\n${normalizedText}\n`;
+ return `${contextBlock}${openTag}\n${normalizedText}\n`;
}
export function wrapSlackTurnPolicy({
diff --git a/packages/slack/src/__tests__/forwarded-message-context.test.ts b/packages/slack/src/__tests__/forwarded-message-context.test.ts
index 6f0dc86d8..11a1880b4 100644
--- a/packages/slack/src/__tests__/forwarded-message-context.test.ts
+++ b/packages/slack/src/__tests__/forwarded-message-context.test.ts
@@ -4,6 +4,7 @@ import {
appendSlackForwardedMessageContext,
extractSlackForwardedMessageFiles,
formatSlackAttachmentTitleContexts,
+ formatSlackAttachmentContext,
formatSlackBlockLinkContext,
formatSlackBlockTextContext,
formatSlackForwardedMessageContext,
@@ -299,6 +300,22 @@ describe('forwarded-message-context', () => {
);
});
+ it('formats attachment context independently from authored text', () => {
+ const context = formatSlackAttachmentContext('Review:', undefined, [
+ {
+ type: 'section',
+ text: {
+ type: 'mrkdwn',
+ text: 'Review: ',
+ },
+ },
+ ]);
+
+ expect(context).toBe(
+ ['Slack block text:', 'Review: https://new.example/2'].join('\n'),
+ );
+ });
+
it('appends Slack block link context to message text', () => {
expect(
appendSlackAttachmentContext('can you investigate?', undefined, [
diff --git a/packages/slack/src/__tests__/slack-notifier.test.ts b/packages/slack/src/__tests__/slack-notifier.test.ts
index b03f31827..38b1cd73a 100644
--- a/packages/slack/src/__tests__/slack-notifier.test.ts
+++ b/packages/slack/src/__tests__/slack-notifier.test.ts
@@ -1556,6 +1556,17 @@ describe('SlackNotifier', () => {
'Forwarded body',
].join('\n'),
);
+ expect(message?.authoredText).toBe('can you see this?');
+ expect(message?.agentContext).toBe(
+ [
+ 'Forwarded Slack message:',
+ 'Context:',
+ '- Author: Annie Easley',
+ '- Channel: C0EXAMPLE01',
+ 'Text:',
+ 'Forwarded body',
+ ].join('\n'),
+ );
});
it('adds forwarded Slack image files to source messages', async () => {
diff --git a/packages/slack/src/__tests__/slack-thread-delivery-tracker.test.ts b/packages/slack/src/__tests__/slack-thread-delivery-tracker.test.ts
index 95c419edd..306ba054a 100644
--- a/packages/slack/src/__tests__/slack-thread-delivery-tracker.test.ts
+++ b/packages/slack/src/__tests__/slack-thread-delivery-tracker.test.ts
@@ -16,8 +16,8 @@ const {
vi.mock('@roomote/cloud-agents', () => ({
getSlackThreadDisplayName: getSlackThreadDisplayNameMock,
wrapSlackMessage: vi.fn(
- (text: string, options?: { ts?: string }) =>
- `\n${text}\n`,
+ (text: string, options?: { ts?: string; agentContext?: string }) =>
+ `${options?.agentContext ? `\n${options.agentContext}\n\n\n` : ''}\n${text}\n`,
),
wrapSlackReplyingTo: vi.fn(
({
@@ -912,6 +912,7 @@ describe('SlackThreadDeliveryTracker', () => {
await tracker.buildContinuationPrompt({
currentMessageTs: '111.000',
currentMessageText: 'latest question',
+ currentMessageAgentContext: 'Slack block text:\nState: New',
fetchThreadMessages: async () => [
{
user: 'U111',
@@ -927,7 +928,12 @@ describe('SlackThreadDeliveryTracker', () => {
await tracker.commit();
- expect(formattedPrompt).toBeUndefined();
+ expect(formattedPrompt).toContain(
+ '\nSlack block text:\nState: New\n',
+ );
+ expect(formattedPrompt).toContain(
+ '\nlatest question\n',
+ );
expect(turnPolicy).toEqual({ reactionsAllowed: false });
expect(releaseClaimedSlackThreadMessagesMock).toHaveBeenCalledWith(
'C123',
diff --git a/packages/slack/src/__tests__/start-auto-routed-slack-task.test.ts b/packages/slack/src/__tests__/start-auto-routed-slack-task.test.ts
index 80cd24fe7..d9c39aa1d 100644
--- a/packages/slack/src/__tests__/start-auto-routed-slack-task.test.ts
+++ b/packages/slack/src/__tests__/start-auto-routed-slack-task.test.ts
@@ -202,6 +202,7 @@ describe('startAutoRoutedSlackTask', () => {
initiatingSlackUserId: 'UINSTALLER',
channel: 'C123',
prompt: 'Investigate this',
+ slackMessageContext: 'Slack block text:\nState: New',
threadTs: '120.000',
originMessageTs: '123.456',
});
@@ -234,6 +235,7 @@ describe('startAutoRoutedSlackTask', () => {
trigger: 'message',
slackUserId: 'UINSTALLER',
channel: 'C123',
+ slackMessageContext: 'Slack block text:\nState: New',
ts: '123.456',
threadTs: '120.000',
skipInitialActingUser: false,
diff --git a/packages/slack/src/__tests__/start-slack-app-mention.test.ts b/packages/slack/src/__tests__/start-slack-app-mention.test.ts
index b9df00154..c734178a0 100644
--- a/packages/slack/src/__tests__/start-slack-app-mention.test.ts
+++ b/packages/slack/src/__tests__/start-slack-app-mention.test.ts
@@ -6,6 +6,7 @@ const {
findActiveSlackTaskRunMock,
queueSlackMessageMock,
resolveSlackReactionNamesMock,
+ wrapSlackMessageMock,
} = vi.hoisted(() => ({
dbUpdateSetMock: vi.fn(),
enqueueTaskMock: vi.fn(),
@@ -14,6 +15,7 @@ const {
findActiveSlackTaskRunMock: vi.fn(),
queueSlackMessageMock: vi.fn(),
resolveSlackReactionNamesMock: vi.fn(),
+ wrapSlackMessageMock: vi.fn((text: string) => text),
}));
vi.mock('@roomote/cloud-agents', () => ({
@@ -21,7 +23,7 @@ vi.mock('@roomote/cloud-agents', () => ({
(message: { user?: string; username?: string }) =>
message.username?.trim() || message.user || 'user',
),
- wrapSlackMessage: vi.fn((text: string) => text),
+ wrapSlackMessage: wrapSlackMessageMock,
wrapSlackReplyingTo: vi.fn((message: { text: string }) => message.text),
wrapSlackThreadContext: vi.fn((messages: { text: string }[]) =>
messages.map((message) => message.text).join('\n'),
@@ -91,6 +93,7 @@ describe('startSlackAppMentionTask', () => {
teamId: 'T123',
slackUserId: 'U123',
text: 'hello',
+ slackMessageContext: 'Slack block text:\nState: New',
ts: '111.000',
threadTs: '111.000',
repo: 'owner/repo',
@@ -110,6 +113,7 @@ describe('startSlackAppMentionTask', () => {
},
task: expect.objectContaining({
payload: expect.objectContaining({
+ slackMessageContext: 'Slack block text:\nState: New',
slackConversationUrl:
'https://acme-team.slack.com/archives/C123/p111000?thread_ts=111.000&cid=C123',
}),
@@ -139,6 +143,7 @@ describe('startSlackAppMentionTask', () => {
teamId: 'T123',
slackUserId: 'U123',
text: 'hello again',
+ slackMessageContext: 'Slack block text:\nPriority: High',
ts: '111.000',
threadTs: '111.000',
repo: 'owner/repo',
@@ -166,6 +171,10 @@ describe('startSlackAppMentionTask', () => {
text: 'hello again',
}),
);
+ expect(wrapSlackMessageMock).toHaveBeenCalledWith('hello again', {
+ ts: '111.000',
+ agentContext: 'Slack block text:\nPriority: High',
+ });
});
it('does not rewrite the reused job payload when the permalink is unchanged', async () => {
diff --git a/packages/slack/src/block-kit.ts b/packages/slack/src/block-kit.ts
index 64f73c634..dd4c3b2d6 100644
--- a/packages/slack/src/block-kit.ts
+++ b/packages/slack/src/block-kit.ts
@@ -1687,7 +1687,9 @@ export async function handleTaskConfiguration(
// Replace user ID mentions with display names in the main message text
const messageText = stripLeadingSlackProductMention(
await slack.normalizeIncomingText(
- stripLeadingRawSlackMention(originalEvent.text),
+ stripLeadingRawSlackMention(
+ originalEvent.authoredText ?? originalEvent.text,
+ ),
),
);
const taskText = appendSlackVideoDescriptionsToText({
@@ -1711,6 +1713,7 @@ export async function handleTaskConfiguration(
teamDomain: slackInstallation.teamDomain ?? undefined,
slackUserId: originalEvent.user,
text: taskText,
+ slackMessageContext: originalEvent.agentContext,
ackEmoji: prefill?.processingReactionName,
ts: originalEvent.ts,
threadTs: threadId,
@@ -1960,7 +1963,9 @@ async function startImmediateSlackTask({
response: ImmediateSlackTaskStartResponse;
}> {
const messageText = stripLeadingSlackProductMention(
- await slack.normalizeIncomingText(stripLeadingRawSlackMention(event.text)),
+ await slack.normalizeIncomingText(
+ stripLeadingRawSlackMention(event.authoredText ?? event.text),
+ ),
);
const images = event.processedImages || [];
const taskText = appendSlackVideoDescriptionsToText({
@@ -1983,6 +1988,7 @@ async function startImmediateSlackTask({
teamDomain: slackInstallation.teamDomain ?? undefined,
slackUserId: event.user,
text: taskText,
+ slackMessageContext: event.agentContext,
ackEmoji: processingReactionName,
ts: event.ts,
threadTs: threadId,
@@ -2134,7 +2140,9 @@ async function createRunFromPrefill({
const messageText = stripLeadingSlackProductMention(
await slack.normalizeIncomingText(
- stripLeadingRawSlackMention(originalEvent.text),
+ stripLeadingRawSlackMention(
+ originalEvent.authoredText ?? originalEvent.text,
+ ),
),
);
const images = originalEvent.processedImages || [];
@@ -2158,6 +2166,7 @@ async function createRunFromPrefill({
teamDomain: prefill.teamDomain,
slackUserId: originalEvent.user,
text: taskText,
+ slackMessageContext: originalEvent.agentContext,
ackEmoji: prefill.processingReactionName,
ts: originalEvent.ts,
threadTs: threadId,
@@ -3233,6 +3242,7 @@ export async function handleRetryFailedTask(
undefined,
slackUserId: originalPayload.user,
text: originalPayload.text,
+ slackMessageContext: originalPayload.slackMessageContext,
agentPromptText: originalPayload.agentPromptText,
ackEmoji: originalPayload.ackEmoji,
completionEmoji: originalPayload.completionEmoji,
diff --git a/packages/slack/src/forwarded-message-context.ts b/packages/slack/src/forwarded-message-context.ts
index 08918473f..82781c78c 100644
--- a/packages/slack/src/forwarded-message-context.ts
+++ b/packages/slack/src/forwarded-message-context.ts
@@ -1101,6 +1101,27 @@ export function appendSlackAttachmentContext(
attachments?: unknown[],
blocks?: unknown[],
): string {
+ const attachmentContext = formatSlackAttachmentContext(
+ text,
+ attachments,
+ blocks,
+ );
+
+ if (!attachmentContext) {
+ return text;
+ }
+
+ const normalizedText = text.trim();
+ return normalizedText
+ ? `${normalizedText}\n\n${attachmentContext}`
+ : attachmentContext;
+}
+
+export function formatSlackAttachmentContext(
+ text: string,
+ attachments?: unknown[],
+ blocks?: unknown[],
+): string | undefined {
const textWithForwardedContext = appendSlackForwardedMessageContext(
text,
attachments,
@@ -1113,17 +1134,15 @@ export function appendSlackAttachmentContext(
);
const blockLinkContext = formatSlackBlockLinkContext(blocks);
const additionalContexts = [
+ formatSlackForwardedMessageContext(attachments),
attachmentTitleContext,
blockTextContext,
blockLinkContext,
].filter((context): context is string => Boolean(context));
if (additionalContexts.length === 0) {
- return textWithForwardedContext;
+ return undefined;
}
- const normalizedText = textWithForwardedContext.trim();
- return normalizedText
- ? `${normalizedText}\n\n${additionalContexts.join('\n\n')}`
- : additionalContexts.join('\n\n');
+ return additionalContexts.join('\n\n');
}
diff --git a/packages/slack/src/slack-notifier.ts b/packages/slack/src/slack-notifier.ts
index 5171dae1b..d8f320818 100644
--- a/packages/slack/src/slack-notifier.ts
+++ b/packages/slack/src/slack-notifier.ts
@@ -32,6 +32,7 @@ import { createSlackWebClient } from './web-client';
import {
appendSlackAttachmentContext,
appendSlackForwardedMessageFiles,
+ formatSlackAttachmentContext,
} from './forwarded-message-context';
type SlackApiThreadMessage = {
@@ -1439,8 +1440,15 @@ export class SlackNotifier {
return null;
}
+ const authoredText =
+ typeof message.text === 'string' ? message.text : '';
+ const agentContext = formatSlackAttachmentContext(
+ authoredText,
+ message.attachments,
+ message.blocks,
+ );
const text = appendSlackAttachmentContext(
- typeof message.text === 'string' ? message.text : '',
+ authoredText,
message.attachments,
message.blocks,
);
@@ -1455,6 +1463,7 @@ export class SlackNotifier {
return {
text,
+ ...(agentContext ? { authoredText, agentContext } : {}),
ts: message.ts,
thread_ts:
typeof message.thread_ts === 'string'
@@ -1468,7 +1477,7 @@ export class SlackNotifier {
attachments: Array.isArray(message.attachments)
? message.attachments
: undefined,
- blocks: Array.isArray(message.blocks) ? message.blocks : undefined,
+ ...(Array.isArray(message.blocks) ? { blocks: message.blocks } : {}),
files,
};
};
diff --git a/packages/slack/src/slack-thread-delivery-tracker.ts b/packages/slack/src/slack-thread-delivery-tracker.ts
index a045859ea..1a3bded47 100644
--- a/packages/slack/src/slack-thread-delivery-tracker.ts
+++ b/packages/slack/src/slack-thread-delivery-tracker.ts
@@ -21,6 +21,7 @@ import { isSlackRoutingWaitReplyText } from './slack-system-messages';
interface SlackThreadContinuationPromptOptions {
currentMessageTs: string;
currentMessageText: string;
+ currentMessageAgentContext?: string;
excludedContextTimestamps?: string[];
resolveCurrentMessageText?: (
claimedMessages: SlackThreadMessage[],
@@ -76,6 +77,7 @@ export class SlackThreadDeliveryTracker {
public async buildContinuationPrompt({
currentMessageTs,
currentMessageText,
+ currentMessageAgentContext,
excludedContextTimestamps,
resolveCurrentMessageText,
fetchThreadMessages,
@@ -211,6 +213,7 @@ export class SlackThreadDeliveryTracker {
};
const currentMessageBlock = wrapSlackMessage(resolvedCurrentMessageText, {
ts: currentMessageTs,
+ agentContext: currentMessageAgentContext,
});
if (!contextBlock && !normalizedLatestBotReply) {
@@ -245,6 +248,16 @@ export class SlackThreadDeliveryTracker {
return {
currentMessageText,
claimedImageUris: [],
+ formattedPrompt: [
+ wrapSlackTurnPolicy({
+ reactionsAllowed: false,
+ preferEmojiAck: false,
+ }),
+ wrapSlackMessage(currentMessageText, {
+ ts: currentMessageTs,
+ agentContext: currentMessageAgentContext,
+ }),
+ ].join('\n\n'),
turnPolicy: {
reactionsAllowed: false,
},
diff --git a/packages/slack/src/start-auto-routed-slack-task.ts b/packages/slack/src/start-auto-routed-slack-task.ts
index 0383ef6d5..319b9b08c 100644
--- a/packages/slack/src/start-auto-routed-slack-task.ts
+++ b/packages/slack/src/start-auto-routed-slack-task.ts
@@ -145,6 +145,7 @@ export async function startAutoRoutedSlackTask({
initiatingSlackUserId,
channel,
prompt,
+ slackMessageContext,
threadTs,
originMessageTs,
processedImages,
@@ -186,6 +187,7 @@ export async function startAutoRoutedSlackTask({
initiatingSlackUserId?: string;
channel: string;
prompt: string;
+ slackMessageContext?: string;
threadTs?: string;
originMessageTs?: string;
processedImages?: string[];
@@ -354,7 +356,6 @@ export async function startAutoRoutedSlackTask({
text: taskDescription,
attachmentTexts: allAttachmentTexts,
});
-
const channelName = (await slack.getChannelName?.(channel)) ?? undefined;
const routingContext = await buildSlackRoutingContext({
@@ -501,6 +502,7 @@ export async function startAutoRoutedSlackTask({
slackUserId,
persistedSlackUserId,
text: taskText,
+ slackMessageContext,
agentPromptText,
ts: sourceMessageTs,
threadTs: threadId,
diff --git a/packages/slack/src/start-slack-app-mention.ts b/packages/slack/src/start-slack-app-mention.ts
index 17fafda21..657a4ae15 100644
--- a/packages/slack/src/start-slack-app-mention.ts
+++ b/packages/slack/src/start-slack-app-mention.ts
@@ -65,6 +65,7 @@ function getLinkedInitiatorUserId(
function buildActiveSlackFollowUpPrompt(input: {
text: string;
+ agentContext?: string;
ts: string;
threadMessages?: SlackThreadMessage[];
latestOwnBotReplyText?: string;
@@ -105,7 +106,10 @@ function buildActiveSlackFollowUpPrompt(input: {
reactionsAllowed: hasPriorBotReply,
preferEmojiAck: hasPriorBotReply,
}),
- wrapSlackMessage(input.text, { ts: input.ts }),
+ wrapSlackMessage(input.text, {
+ ts: input.ts,
+ agentContext: input.agentContext,
+ }),
]
.filter(Boolean)
.join('\n\n'),
@@ -130,6 +134,7 @@ export async function startSlackAppMentionTask(input: {
slackUserId: string;
persistedSlackUserId?: string | null;
text: string;
+ slackMessageContext?: string;
agentPromptText?: string;
/**
* Deprecated: acknowledgement/completion reactions are fixed defaults and
@@ -195,6 +200,7 @@ export async function startSlackAppMentionTask(input: {
const builtPrompt = !agentPromptText
? buildActiveSlackFollowUpPrompt({
text: input.text,
+ agentContext: input.slackMessageContext,
ts: input.ts,
threadMessages: promptRelevantThreadMessages,
latestOwnBotReplyText: promptRelevantLatestOwnBotReply?.text,
@@ -278,6 +284,9 @@ export async function startSlackAppMentionTask(input: {
? {}
: { user: input.persistedSlackUserId ?? input.slackUserId }),
text: input.text,
+ ...(input.slackMessageContext?.trim()
+ ? { slackMessageContext: input.slackMessageContext.trim() }
+ : {}),
...(input.agentPromptText?.trim()
? { agentPromptText: input.agentPromptText.trim() }
: {}),
diff --git a/packages/slack/src/types.ts b/packages/slack/src/types.ts
index befaf89c7..95b29165e 100644
--- a/packages/slack/src/types.ts
+++ b/packages/slack/src/types.ts
@@ -67,6 +67,7 @@ export interface SlackChannelMessage extends SlackThreadMessage {
export interface SlackConversationMessage {
text: string;
authoredText?: string;
+ agentContext?: string;
ts: string;
thread_ts?: string;
user?: string;
@@ -100,6 +101,7 @@ export interface SlackEvent {
user: string;
text: string;
authoredText?: string;
+ agentContext?: string;
ts: string;
/** Internal turn ID when an event is synthesized from another Slack event. */
deliveryTs?: string;
diff --git a/packages/types/src/__tests__/acp.test.ts b/packages/types/src/__tests__/acp.test.ts
index 139a3f740..b40efd8ff 100644
--- a/packages/types/src/__tests__/acp.test.ts
+++ b/packages/types/src/__tests__/acp.test.ts
@@ -406,6 +406,39 @@ describe('normalizeTranscriptUserText', () => {
).toBe('latest question');
});
+ it('hides agent-only Slack message context from transcript text', () => {
+ expect(
+ normalizeTranscriptUserText(
+ [
+ '',
+ 'Emoji reactions are not allowed.',
+ '',
+ '',
+ '',
+ 'Slack block text:',
+ 'State: New',
+ '',
+ '',
+ '',
+ 'latest question',
+ '',
+ ].join('\n'),
+ ),
+ ).toBe('latest question');
+ });
+
+ it('leaves malformed Slack message context visible instead of stripping arbitrary text', () => {
+ const text = [
+ '',
+ 'Slack block text without a closing context tag',
+ '',
+ 'latest question',
+ '',
+ ].join('\n');
+
+ expect(normalizeTranscriptUserText(text)).toBe(text);
+ });
+
it('extracts the current Slack turn when the prompt wrappers are HTML-escaped', () => {
expect(
normalizeTranscriptUserText(
@@ -418,6 +451,10 @@ describe('normalizeTranscriptUserText', () => {
'Roomote Bot: Previous reply',
'</replying_to>',
'',
+ '<slack_message_context>',
+ 'Slack block text: State: New',
+ '</slack_message_context>',
+ '',
'<slack_message ts="111.000">',
'latest question',
'</slack_message>',
diff --git a/packages/types/src/acp.ts b/packages/types/src/acp.ts
index 50680e74a..473c5c405 100644
--- a/packages/types/src/acp.ts
+++ b/packages/types/src/acp.ts
@@ -1420,7 +1420,7 @@ function isSlackThreadActivityOnlyBlock(text: string): boolean {
/**
* Extract slack_message content from:
* thread_activity* thread_context? thread_activity*
- * replying_to? slack_turn_policy? slack_message
+ * replying_to? slack_turn_policy? slack_message_context? slack_message
*
* Implemented as the original recursive descent with per-(pos, phase) memoization,
* executed on an explicit heap stack so thousands of sequential activity blocks
@@ -1486,17 +1486,43 @@ function parseSlackRestFrom(text: string, index: number): string | null {
afterPolicyOpen + 1,
closeMarker,
(_close, afterClose) =>
- extractSlackMessageAt(text, afterClose) !== null,
+ extractSlackMessageAfterOptionalContext(text, afterClose) !== null,
);
if (found !== null) {
- return extractSlackMessageAt(text, found.afterClose);
+ return extractSlackMessageAfterOptionalContext(text, found.afterClose);
}
}
- return extractSlackMessageAt(text, at);
+ return extractSlackMessageAfterOptionalContext(text, at);
}
}
+function extractSlackMessageAfterOptionalContext(
+ text: string,
+ index: number,
+): string | null {
+ const afterContextOpen = matchOpenTag(
+ text,
+ index,
+ 'slack_message_context',
+ false,
+ );
+ if (afterContextOpen !== null && text[afterContextOpen] === '\n') {
+ const closeMarker = '\n';
+ const found = findStructuralClose(
+ text,
+ afterContextOpen + 1,
+ closeMarker,
+ (_close, afterClose) => extractSlackMessageAt(text, afterClose) !== null,
+ );
+ if (found !== null) {
+ return extractSlackMessageAt(text, found.afterClose);
+ }
+ }
+
+ return extractSlackMessageAt(text, index);
+}
+
function parseSlackTranscriptFrom(text: string, start: number): string | null {
// phase: 0 = pre-context (leading activities), 1 = post-context (trailing + rest)
type Frame = {
diff --git a/packages/types/src/task-runs.ts b/packages/types/src/task-runs.ts
index 1dac1eee6..cf5d7f637 100644
--- a/packages/types/src/task-runs.ts
+++ b/packages/types/src/task-runs.ts
@@ -1235,6 +1235,7 @@ export const slackAppMentionSchema = sharedTaskSchema.extend({
user: z.string().optional(),
text: z.string(),
agentPromptText: z.string().optional(),
+ slackMessageContext: z.string().optional(),
/**
* Optional acknowledgement emoji name that was applied to the source
* message when the task was kicked off.