Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ DEFAULT_COMPUTE_PROVIDER=docker
# Set to true to prevent curated integrations from being configured or used.
# Existing connections remain stored and become available again once unset.
# R_CURATED_INTEGRATIONS_DISABLED=true
# Set to true to let users default supported communications messages to fast mode.
# Currently applies to Slack messages only.
# R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED=true
# SLACK_APP_ID=
# R_SLACK_SIGNING_SECRET=
# R_TELEGRAM_BOT_TOKEN=
Expand Down
41 changes: 41 additions & 0 deletions apps/api/src/handlers/slack/__tests__/fast-agent.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions apps/api/src/handlers/slack/events/fast-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ export function isBareFastCommandInvocation(text: string): boolean {
return /^!fast(?:\s|$)/i.test(text.trimStart());
}

type FastAgentEntryMode = 'explicit' | 'default';

export function resolveFastAgentEntryMode(params: {
explicitInvocation: boolean;
deploymentSettingEnabled: boolean;
userDefaultEnabled: boolean;
}): FastAgentEntryMode | null {
if (params.explicitInvocation) {
return 'explicit';
}

return params.deploymentSettingEnabled && params.userDefaultEnabled
? 'default'
: null;
}

export function extractFastQuestion(
mentionStrippedText: string,
continuation = false,
Expand Down
34 changes: 28 additions & 6 deletions apps/api/src/handlers/slack/events/message-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
isBareFastCommandInvocation,
isFastCommandInvocation,
processFastAgentMessage,
resolveFastAgentEntryMode,
} from './fast-agent.js';
import { createFastAgentTaskLauncher } from './fast-agent-task-launcher.js';
import { processSnapshotResume } from './snapshot-resume.js';
Expand Down Expand Up @@ -1223,11 +1224,21 @@ async function maybeHandleChannelAutoStart(params: {

const { ackEmoji } = await resolveSlackReactionNames();

if (
userMapping &&
typeof channelAutoStartEvent.user === 'string' &&
isBareFastCommandInvocation(channelAutoStartEvent.text)
) {
const fastAgentEntryMode =
userMapping && typeof channelAutoStartEvent.user === 'string'
? resolveFastAgentEntryMode({
explicitInvocation: isBareFastCommandInvocation(
channelAutoStartEvent.text,
),
deploymentSettingEnabled:
Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED === true,
userDefaultEnabled:
userMapping.communicationsFastModeDefault &&
!isRemovedEvalCommandInvocation(channelAutoStartEvent.text),
})
: null;

if (fastAgentEntryMode && userMapping) {
startFastAgentResponse({
event: { ...channelAutoStartEvent, user: channelAutoStartEvent.user },
slackInstallation: context.slackInstallation,
Expand All @@ -1236,6 +1247,7 @@ async function maybeHandleChannelAutoStart(params: {
userId: userMapping.userId,
teamId: context.teamId,
usageText: 'Use `!fast <question>` in this channel.',
continuation: fastAgentEntryMode === 'default',
processingReactionName: ackEmoji,
errorLogPrefix: `❌ Background fast-agent response failed for auto-start thread ${channelAutoStartEvent.ts}:`,
});
Expand Down Expand Up @@ -1687,7 +1699,16 @@ async function handleSlackEntryEvent(params: {
activeTaskId: activeRun?.taskId,
});

if (isFastCommandInvocation(event.text)) {
const fastAgentEntryMode = resolveFastAgentEntryMode({
explicitInvocation: isFastCommandInvocation(event.text),
deploymentSettingEnabled:
Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED === true,
userDefaultEnabled:
userMapping.communicationsFastModeDefault &&
!isRemovedEvalCommandInvocation(event.text),
});

if (fastAgentEntryMode) {
startFastAgentResponse({
event,
slackInstallation,
Expand All @@ -1696,6 +1717,7 @@ async function handleSlackEntryEvent(params: {
userId: userMapping.userId,
teamId,
activeTaskId: activeRun?.taskId ?? null,
continuation: fastAgentEntryMode === 'default',
processingReactionName: ackEmoji,
errorLogPrefix: `❌ Background fast-agent response failed for thread ${threadId}:`,
});
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/handlers/slack/helpers/user-mapping.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion apps/api/src/handlers/slack/helpers/user-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
} from '@roomote/db/server';

type SlackUserMappingLookup = {
activeMapping: SlackUserMapping | null;
activeMapping:
| (SlackUserMapping & { communicationsFastModeDefault: boolean })
| null;
hasInactiveMapping: boolean;
};

Expand All @@ -26,6 +28,7 @@ export async function lookupSlackUserMapping(params: {
updatedAt: slackUserMappings.updatedAt,
matchedUserId: users.id,
userDeletedAt: users.deletedAt,
userMetadata: users.metadata,
})
.from(slackUserMappings)
.leftJoin(users, eq(users.id, slackUserMappings.userId))
Expand Down Expand Up @@ -59,6 +62,12 @@ export async function lookupSlackUserMapping(params: {
userId: row.userId,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
communicationsFastModeDefault:
typeof row.userMetadata === 'object' &&
row.userMetadata !== null &&
!Array.isArray(row.userMetadata) &&
(row.userMetadata as Record<string, unknown>)
.communications_fast_mode_default === true,
},
hasInactiveMapping: false,
};
Expand Down
1 change: 1 addition & 0 deletions apps/docs/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ as per-task auth tokens or workspace paths.
| `ROOMOTE_FORCE_TELEMETRY` | Development only | Force-enables telemetry in development or preview environments when a Ping endpoint is explicitly configured. |
| `R_CLOUD_ENABLED` | Roomote Cloud only | Deployment-managed switch for Roomote Cloud behavior, including required anonymous analytics and Cloud support integrations. Do not set this for self-hosted deployments. |
| `R_CURATED_INTEGRATIONS_DISABLED` | Optional | Operator policy for the curated **Settings > Integrations** catalog, which is enabled by default. Set to `true` and restart Roomote to prevent those integrations from being configured or used. Existing connections remain stored while disabled and become available again once the value is unset. Communications, source-control, inference, sandbox providers, and environment-defined MCP servers are unaffected. |
| `R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED` | Optional | Set to `true` and restart Roomote to expose a personal setting that defaults a user's supported communications messages to fast mode. The setting currently applies to Slack messages, where it removes the need for `!fast`; it is hidden and unavailable when this flag is unset. |
| `R_GITHUB_APP_SLUG` | GitHub setup | GitHub App slug used by server-rendered setup and GitHub integration flows. |
| `SETUP_TOKEN` | Required (non-local) | One-time bootstrap token that admits the first admin at `/setup`. Required on every non-local deployment — tokenless bootstrap is allowed only when `NODE_ENV` is not `production` and `R_APP_ENV` is `development`, so anything running with `NODE_ENV=production` needs it. Without it, first-admin bootstrap stays closed so nobody can claim the founding-admin slot by reaching the URL first. Optional only in local development. |
| `DASHBOARD_PASSWORD` | Production | Local fallback/admin password value used by the deployment. Generate a strong secret. |
Expand Down
Loading
Loading