Skip to content
2 changes: 2 additions & 0 deletions posthog/temporal/ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from posthog.temporal.ai.slack_app.posthog_code_slack_mention import PostHogCodeSlackMentionWorkflow
from posthog.temporal.ai.slack_app.posthog_code_slack_mention_command import PostHogCodeSlackMentionCommandWorkflow
from posthog.temporal.ai.slack_app.posthog_slack_inbox_onboarding import PostHogSlackInboxOnboardingWorkflow
from posthog.temporal.ai.slack_app.slack_app_mention import SlackAppMentionWorkflow

from .llm_traces_summaries.summarize_traces import (
SummarizeLLMTracesInputs,
Expand All @@ -35,6 +36,7 @@
# workflows.
POSTHOG_CODE_SLACK_WORKFLOWS = [
PostHogCodeSlackMentionWorkflow,
SlackAppMentionWorkflow,
PostHogCodeSlackMentionCommandWorkflow,
PostHogCodeSlackTerminateTaskWorkflow,
PostHogSlackInboxOnboardingWorkflow,
Expand Down
11 changes: 11 additions & 0 deletions posthog/temporal/ai/slack_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
forward_posthog_code_followup_activity,
handle_posthog_code_rules_command_activity,
handle_posthog_code_slack_mention_command_activity,
mark_slack_app_message_processing_activity,
post_posthog_code_authorship_timeout_activity,
post_posthog_code_internal_error_activity,
post_posthog_code_no_repos_activity,
Expand All @@ -34,12 +35,16 @@
run_posthog_slack_inbox_onboarding_activity,
)
from posthog.temporal.ai.slack_app.types import (
SLACK_APP_PROCESSING_REACTION,
SLACK_APP_QUEUED_REACTION,
MarkSlackAppMessageProcessingInput,
PostHogCodeRepoCascadeOutcome,
PostHogCodeRulesCommandResult,
PostHogCodeSlackMentionCommandResult,
PostHogCodeSlackMentionCommandWorkflowInputs,
PostHogCodeSlackMentionWorkflowInputs,
PostHogSlackInboxOnboardingInputs,
SlackAppMentionWorkflowInputs,
SlackRepoSelectionOutcome,
)

Expand All @@ -64,20 +69,25 @@
handle_posthog_code_rules_command_activity,
create_posthog_code_routing_rule_activity,
handle_posthog_code_slack_mention_command_activity,
mark_slack_app_message_processing_activity,
run_posthog_slack_inbox_onboarding_activity,
]

__all__ = [
"CLASSIFIER_THREAD_HISTORY_MESSAGES",
"MarkSlackAppMessageProcessingInput",
"POSTHOG_CODE_SLACK_MENTION_PICKER_GUIDANCE",
"POSTHOG_CODE_SLACK_RULES_ADD_PICKER_GUIDANCE",
"SLACK_APP_PROCESSING_REACTION",
"SLACK_APP_QUEUED_REACTION",
"PostHogCodeRepoCascadeOutcome",
"PostHogCodeRulesCommandResult",
"PostHogCodeSlackMentionCommandResult",
"PostHogCodeSlackMentionCommandWorkflowInputs",
"PostHogCodeSlackMentionWorkflowInputs",
"PostHogSlackInboxOnboardingInputs",
"SLACK_APP_ACTIVITIES",
"SlackAppMentionWorkflowInputs",
"SlackRepoSelectionOutcome",
"block_posthog_code_task_if_no_personal_github_activity",
"cascade_posthog_code_repository_activity",
Expand All @@ -93,6 +103,7 @@
"forward_posthog_code_followup_activity",
"handle_posthog_code_rules_command_activity",
"handle_posthog_code_slack_mention_command_activity",
"mark_slack_app_message_processing_activity",
"post_posthog_code_authorship_timeout_activity",
"post_posthog_code_internal_error_activity",
"post_posthog_code_no_repos_activity",
Expand Down
2 changes: 2 additions & 0 deletions posthog/temporal/ai/slack_app/activities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
POSTHOG_CODE_SLACK_MENTION_PICKER_GUIDANCE,
POSTHOG_CODE_SLACK_RULES_ADD_PICKER_GUIDANCE,
block_posthog_code_task_if_no_personal_github_activity,
mark_slack_app_message_processing_activity,
post_posthog_code_authorship_timeout_activity,
post_posthog_code_internal_error_activity,
post_posthog_code_no_repos_activity,
Expand Down Expand Up @@ -60,6 +61,7 @@
"forward_posthog_code_followup_activity",
"handle_posthog_code_rules_command_activity",
"handle_posthog_code_slack_mention_command_activity",
"mark_slack_app_message_processing_activity",
"post_posthog_code_authorship_timeout_activity",
"post_posthog_code_internal_error_activity",
"post_posthog_code_no_repos_activity",
Expand Down
38 changes: 37 additions & 1 deletion posthog/temporal/ai/slack_app/activities/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import structlog
from temporalio import activity

from posthog.temporal.ai.slack_app.types import PostHogCodeSlackMentionWorkflowInputs, coerce_mention_workflow_inputs
from posthog.temporal.ai.slack_app.types import (
MarkSlackAppMessageProcessingInput,
PostHogCodeSlackMentionWorkflowInputs,
coerce_mention_workflow_inputs,
)
from posthog.temporal.common.utils import close_db_connections

if TYPE_CHECKING:
Expand Down Expand Up @@ -428,3 +432,35 @@ def post_posthog_code_internal_error_activity(
thread_ts=thread_ts,
text="Sorry, I hit an internal error while processing that request. Please try again.",
)


@activity.defn
@close_db_connections
def mark_slack_app_message_processing_activity(input: MarkSlackAppMessageProcessingInput) -> None:
"""Swap the queued :hourglass: reaction for :eyes: when the conversation
queue starts processing a message.

Purely cosmetic UX feedback: never raises, so a Slack hiccup can't stall
the conversation queue behind retries of a reaction.
"""
from posthog.models.integration import Integration, SlackIntegration
from posthog.temporal.ai.slack_app.helpers import swap_reaction
from posthog.temporal.ai.slack_app.types import SLACK_APP_PROCESSING_REACTION, SLACK_APP_QUEUED_REACTION

try:
integration = Integration.objects.get(
id=input.integration_id,
kind="slack",
integration_id=input.slack_team_id,
)
slack = SlackIntegration(integration)
swap_reaction(
slack.client, input.channel, input.message_ts, SLACK_APP_QUEUED_REACTION, SLACK_APP_PROCESSING_REACTION
)
except Exception as e:
logger.warning(
"slack_app_processing_reaction_failed",
channel=input.channel,
message_ts=input.message_ts,
error=str(e),
)
45 changes: 36 additions & 9 deletions posthog/temporal/ai/slack_app/activities/task_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,12 +563,14 @@ def forward_posthog_code_followup_activity(
slack = SlackIntegration(integration)

followup_user_text_prefix: str | None = None
actor_user = mapping.task.created_by
if slack_user_id != mapping.mentioning_slack_user_id:
# The follow-up is from a different Slack user than the one who started the
# thread. Try to resolve them to a PostHog user with access to the same team
# — if so, let them participate; the message is still relayed in the original
# author's name (their sandbox token, their identity to the agent), with the
# actual sender's name prefixed onto the text so the agent sees who spoke.
# Follow-up from someone other than the original mentioner. Resolve them to a
# PostHog user with access to the same team; if they qualify, they participate
# under their own identity — the sandbox JWT and the PostHog MCP OAuth token
# are both rebound to them via send_refresh_session below, so their actions
# (insights, dashboards, etc.) attribute to them rather than the task creator.
# The actor's name is still prefixed onto the text so the agent sees who spoke.
resolved = resolve_slack_user(slack, integration, slack_user_id, channel, thread_ts)
if not resolved:
logger.info(
Expand All @@ -584,6 +586,7 @@ def forward_posthog_code_followup_activity(
# into the LLM-forwarded prefix when both name and slack_email are absent.
actor_name = resolved.user.get_full_name() or resolved.slack_email or resolved.user.email
followup_user_text_prefix = f"{actor_name}: "
actor_user = resolved.user
logger.info(
"posthog_code_followup_cross_user_authorized",
channel=channel,
Expand Down Expand Up @@ -682,14 +685,38 @@ def forward_posthog_code_followup_activity(
if user_message_ts:
safe_react(slack.client, channel, user_message_ts, "eyes")

# Per-message identity applies only to queue-dispatched messages
# (slack-app-queue-workflow flag); the legacy per-message workflow keeps
# every credential bound to the task creator, exactly as before.
identity_user = actor_user if inputs.per_message_identity else mapping.task.created_by

auth_token = None
created_by = mapping.task.created_by
if created_by and created_by.id:
distinct_id = created_by.distinct_id or f"user_{created_by.id}"
if identity_user and identity_user.id:
distinct_id = identity_user.distinct_id or f"user_{identity_user.id}"
auth_token = tasks_facade.create_sandbox_connection_token(
task_run.id, user_id=created_by.id, distinct_id=distinct_id
task_run.id, user_id=identity_user.id, distinct_id=distinct_id
)

# Rebind the sandbox to the message's actor *before* sending it, so
# this turn's actions attribute to whoever actually spoke, not
# whoever spoke last. The tasks layer owns which credentials that
# covers, tracks the sandbox's current identity, bypasses its refresh
# rate limits on any transition (including switching *back* to the
# task creator), and is best-effort by contract — a rebind failure is
# logged there and never blocks the message. The belt-and-braces
# except covers facade-level surprises for the same reason: deliver
# the message under the previous identity rather than dropping it.
if inputs.per_message_identity:
try:
tasks_facade.rebind_sandbox_identity_for_user(task_run.id, identity_user.id, auth_token=auth_token)
except Exception:
logger.exception(
"slack_app_followup_identity_rebind_failed",
channel=channel,
thread_ts=thread_ts,
actor_user_id=identity_user.id,
)

result = tasks_facade.send_user_message(task_run.id, user_text, auth_token=auth_token, timeout=90)
if not result.success and result.retryable and result.status_code != 504:
result = tasks_facade.send_user_message(task_run.id, user_text, auth_token=auth_token, timeout=90)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ def safe_react(client: Any, channel: str, timestamp: str, name: str) -> None:
pass
else:
raise


def swap_reaction(client: Any, channel: str, timestamp: str, remove: str, add: str) -> None:
"""Replace one reaction with another; a missing old reaction is a no-op."""
try:
client.reactions_remove(channel=channel, timestamp=timestamp, name=remove)
except Exception:
pass
safe_react(client, channel, timestamp, add)
Loading
Loading