-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(tasks): route slack follow-ups through the workflow follow-up queue #70806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -948,13 +948,6 @@ def forward_posthog_code_followup_activity( | |
| if user_message_ts: | ||
| safe_react(slack.client, channel, user_message_ts, "eyes") | ||
|
|
||
| auth_token = None | ||
| if actor_user and actor_user.id: | ||
| distinct_id = actor_user.distinct_id or f"user_{actor_user.id}" | ||
| auth_token = tasks_facade.create_sandbox_connection_token( | ||
| task_run.id, user_id=actor_user.id, distinct_id=distinct_id | ||
| ) | ||
|
|
||
| uploaded_attachments, attachment_skips = _upload_prepared_slack_attachments( | ||
| tasks_facade, | ||
| task_run_id=task_run.id, | ||
|
|
@@ -973,42 +966,24 @@ def forward_posthog_code_followup_activity( | |
| or user_text | ||
| ) | ||
|
|
||
| send_kwargs: dict[str, Any] = { | ||
| "auth_token": auth_token, | ||
| "timeout": 90, | ||
| # Deterministic across activity retries: a retry after a partial failure | ||
| # (or the in-line resend below) redelivers with the same id, and the | ||
| # agent-server drops the duplicate instead of applying the message twice. | ||
| "message_id": _slack_followup_message_id(channel, user_message_ts, thread_ts), | ||
| } | ||
| if uploaded_attachments: | ||
| send_kwargs["artifacts"] = uploaded_attachments | ||
|
|
||
| result = tasks_facade.send_user_message(task_run.id, user_text, **send_kwargs) | ||
| if not result.success and result.retryable and result.status_code != 504: | ||
| result = tasks_facade.send_user_message(task_run.id, user_text, **send_kwargs) | ||
|
|
||
| if not result.success: | ||
| # Queue on the workflow so delivery is ordered with the web path. The | ||
| # deterministic message id keeps redelivery idempotent. | ||
| signal_result = tasks_facade.signal_task_run_user_message( | ||
| task_run.id, | ||
| mapping.task_id, | ||
| task_run.team_id, | ||
| content=user_text, | ||
| artifact_ids=_uploaded_attachment_ids(uploaded_attachments), | ||
| message_id=_slack_followup_message_id(channel, user_message_ts, thread_ts), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The queued signal carries content, artifacts, and an idempotency key, but not Prompt To Fix With AICarry the authenticated Slack actor identity (at minimum the validated PostHog user ID, and preferably the Slack user ID for audit attribution) as part of signal_task_run_user_message -> signal_task_followup_message -> PendingFollowup -> SendFollowupToSandboxInput. At dispatch, resolve and validate that specific actor, and mint both the sandbox connection token and refreshed MCP OAuth credentials from it rather than from mutable TaskRun.state. Do not update shared actor state as the source of credentials for queued messages; add a regression test with two queued follow-ups from different users proving each delivery uses its own actor.Severity: high | Confidence: 96% | React with 👍 if useful or 👎 if not |
||
| ) | ||
|
Comment on lines
+971
to
+978
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The signal carries the message but not its resolved Rule Used: When implementing new features, ensure that owners... (source) Learned From |
||
| if signal_result is not True: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| logger.warning( | ||
| "posthog_code_followup_forwarding_failed", | ||
| "slack_app_followup_signal_failed", | ||
| channel=channel, | ||
| thread_ts=thread_ts, | ||
| error=result.error, | ||
| status_code=result.status_code, | ||
| task_run_id=str(task_run.id), | ||
| signal_result=signal_result, | ||
| ) | ||
| if result.retryable and result.status_code == 504: | ||
| # Agent is still processing — leave the :eyes: reaction up so the thread | ||
| # reads as in-progress. relayAgentResponse fires when it finishes, | ||
| # delivering the correct response to Slack. | ||
| _delete_followup_progress( | ||
| integration_id=inputs.integration_id, | ||
| channel=channel, | ||
| thread_ts=thread_ts, | ||
| user_message_ts=user_message_ts, | ||
| mentioning_slack_user_id=mapping.mentioning_slack_user_id, | ||
| ) | ||
| return True | ||
|
|
||
| _set_followup_done_reaction(slack, channel, user_message_ts, "x") | ||
| slack.client.chat_postMessage( | ||
| channel=channel, | ||
|
|
@@ -1017,7 +992,7 @@ def forward_posthog_code_followup_activity( | |
| ) | ||
| return True | ||
|
|
||
| # Message delivered; the agent is now working on it, so leave the :eyes: reaction | ||
| # Message queued; the agent picks it up next, so leave the :eyes: reaction | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Behavior change worth stating in the PR description: once the message is queued, a delivery failure inside the workflow marks the whole run failed ( |
||
| # up. relayAgentResponse posts the agent's response once it finishes. | ||
| _delete_followup_progress( | ||
| integration_id=inputs.integration_id, | ||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -465,10 +465,15 @@ def execute_build_sandbox_image_workflow(image_id: str, team_id: int, *, refresh | |
| ) | ||
|
|
||
|
|
||
| def signal_task_followup_message(workflow_id: str, message: str | None, artifact_ids: list[str]) -> None: | ||
| def signal_task_followup_message( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here we need patching or in-roll workflows will fail, similarly in #70762
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, you are right, interestingly my agent were talling me extra params will be dropped, but looks like TypeError will follow aka
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if that agent was Fable, Anthropic needs to send us some money back 😆
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hahaha, opus 4.8, but still should refund us right? :D
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. def! And a beer as well 🍻
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i have created new base PR0 which just adds defaults to signal handler, we should be good to go #71562 |
||
| workflow_id: str, | ||
| message: str | None, | ||
| artifact_ids: list[str], | ||
| message_id: str | None = None, | ||
| ) -> None: | ||
| client = sync_connect() | ||
| handle = client.get_workflow_handle(workflow_id) | ||
| asyncio.run(handle.signal("send_followup_message", args=[message, artifact_ids])) | ||
| asyncio.run(handle.signal("send_followup_message", args=[message, artifact_ids, message_id])) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This always sends three positional arguments, while workers running the previous workflow code accept only |
||
|
|
||
|
|
||
| def signal_agent_text_delta(workflow_id: str, text: str) -> None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| from unittest.mock import AsyncMock, MagicMock, patch | ||
|
|
||
| from products.tasks.backend.temporal.client import ( | ||
| execute_posthog_code_agent_relay_workflow, | ||
| signal_task_followup_message, | ||
| ) | ||
| from products.tasks.backend.temporal.slack_relay.activities import RelaySlackMessageInput | ||
|
|
||
|
|
||
| @patch("products.tasks.backend.temporal.client.sync_connect") | ||
| def test_relay_enqueue_constructs_workflow_input(mock_connect: MagicMock) -> None: | ||
| # Guards against the client kwargs drifting from the RelaySlackMessageInput | ||
| # fields — that mismatch raises TypeError at enqueue time and every Slack | ||
| # relay surfaces as a 503 while the sandbox swallows the error silently. | ||
| mock_client = MagicMock(start_workflow=AsyncMock()) | ||
| mock_connect.return_value = mock_client | ||
|
|
||
| relay_id = execute_posthog_code_agent_relay_workflow( | ||
| run_id="run-1", text="hello", relay_id="relay-1", user_message_ts="123.456" | ||
| ) | ||
|
|
||
| assert relay_id == "relay-1" | ||
| workflow_input = mock_client.start_workflow.call_args.args[1] | ||
| assert isinstance(workflow_input, RelaySlackMessageInput) | ||
| assert workflow_input.text == "hello" | ||
| assert workflow_input.run_id == "run-1" | ||
|
|
||
|
|
||
| @patch("products.tasks.backend.temporal.client.sync_connect") | ||
| def test_followup_signal_sends_expected_args(mock_connect: MagicMock) -> None: | ||
| handle = MagicMock(signal=AsyncMock()) | ||
| mock_connect.return_value = MagicMock(get_workflow_handle=MagicMock(return_value=handle)) | ||
|
|
||
| signal_task_followup_message("wf-1", "hi", ["artifact-1"], message_id="msg-1") | ||
|
|
||
| handle.signal.assert_awaited_once_with("send_followup_message", args=["hi", ["artifact-1"], "msg-1"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
High: Follow-up identity is not bound to the queued message
This signal does not carry
actor_user.id, while the delivery activity later mints credentials from the run's mutableslack_actor_user_id. If two authorized participants submit overlapping follow-ups, the later state update can make the earlier participant's command execute with the other participant's sandbox and MCP credentials. Include the actor identity inPendingFollowupandSendFollowupToSandboxInput, validate that actor still has team access, and mint the token from that immutable per-message identity.