Skip to content

feat(tasks): route slack follow-ups through the workflow follow-up queue - #70806

Merged
VojtechBartos merged 1 commit into
masterfrom
vojtab/slack-followup-queue
Jul 20, 2026
Merged

feat(tasks): route slack follow-ups through the workflow follow-up queue#70806
VojtechBartos merged 1 commit into
masterfrom
vojtab/slack-followup-queue

Conversation

@VojtechBartos

@VojtechBartos VojtechBartos commented Jul 14, 2026

Copy link
Copy Markdown
Member

Problem

Slack thread follow-ups were delivered straight to the sandbox, bypassing the task workflow's follow-up queue — so Slack and web messages had no ordering against each other, and a busy agent needed a 504 special case.

Changes

The Slack forward signals send_followup_message like the web UI does. The workflow delivers one message at a time and each delivery blocks until the turn completes, so messages execute strictly in order. The deterministic Slack message_id rides in the signal, keeping redelivery idempotent. Delivery failures surface through the run's error path; the forward still replies inline when a message can't be queued. The facade's direct send_user_message wrapper loses its last caller and is removed.

How did you test this code?

All tested locally in a two-user Slack thread (rapid alternating commands execute strictly in order), plus the rewritten forwarding tests, delivery tests, and web API call-shape tests — ~360 green.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

Internal behavior only.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code (Claude Fable 5).

VojtechBartos commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

@trunk-io

trunk-io Bot commented Jul 15, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@VojtechBartos
VojtechBartos force-pushed the vojtab/slack-followup-queue branch from 07d817a to a1f105c Compare July 16, 2026 12:07
@VojtechBartos
VojtechBartos marked this pull request as ready for review July 16, 2026 12:39
@VojtechBartos
VojtechBartos requested a review from a team July 16, 2026 12:39
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 16, 2026 12:40
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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Bind queued Slack follow-ups to their authenticated actor

The queued signal carries content, artifacts, and an idempotency key, but not actor_user. The delivery activity instead reads the mutable slack_actor_user_id from run state to mint the sandbox JWT and refresh the MCP OAuth token. Since the next Slack follow-up updates that state before its queued predecessor is dispatched, a low-privilege teammate can send a prompt immediately before a privileged participant's follow-up; their queued prompt is then executed with the privileged participant's sandbox and OAuth credentials. The prior direct path minted the token from this invocation's actor_user, so it did not have this cross-message credential race.

Prompt To Fix With AI
Carry 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

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Security Review

Queued follow-ups do not preserve per-message actor identity. Concurrent replies from different authorized Slack users can cause one user's message to run with another user's sandbox credential context.

Reviews (1): Last reviewed commit: "feat(tasks): route slack follow-ups thro..." | Re-trigger Greptile

Comment on lines +1049 to +1056
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),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Queued Messages Lose Actor Identity

The signal carries the message but not its resolved actor_user, so delivery later derives credentials from the run-wide slack_actor_user_id. If two authorized Slack users reply quickly, the second forward can overwrite that state before the first message is delivered, causing the first user's message to run with the second user's sandbox credentials.

Rule Used: When implementing new features, ensure that owners... (source)

Learned From
PostHog/posthog#31236

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]))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Signal Shape Breaks Rolling Deploys

This always sends three positional arguments, while workers running the previous workflow code accept only message and artifact_ids. During a rolling deployment, a new caller can signal an in-flight workflow still assigned to an old worker, where the extra argument makes signal handling fail and the Slack follow-up is never queued.

# Queue on the workflow so delivery is ordered with the web path and the
# turn runs under this message's actor. The deterministic message id
# keeps redelivery idempotent.
signal_result = tasks_facade.signal_task_run_user_message(

Copy link
Copy Markdown
Contributor

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 mutable slack_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 in PendingFollowup and SendFollowupToSandboxInput, validate that actor still has team access, and mint the token from that immutable per-message identity.

@veria-ai

veria-ai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

PR overview

This PR changes Slack task follow-ups so they are routed through the workflow follow-up queue rather than being handled directly. The touched task creation flow queues pending Slack follow-up messages for later delivery to the sandbox/workflow path.

There is one open security issue, and none have been addressed yet. The remaining issue is significant: queued follow-up messages are not bound to the original Slack actor, so overlapping submissions by authorized participants could cause one user’s follow-up to run with another user’s sandbox and MCP credentials. The PR should bind the actor identity to each queued follow-up and validate access at delivery time before credentials are minted.

Open issues (1)

Fixed/addressed: 0 · PR risk: 7/10

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Backend coverage — 89.0% of changed backend lines covered — 6 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ██████████████████░░ 89.0% (53 / 59)

File Patch Uncovered changed lines
products/tasks/backend/facade/api.py 42.9% 2575–2578
products/tasks/backend/temporal/client.py 50.0% 476
products/tasks/backend/temporal/process_task/workflow.py 66.7% 1839

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29725779251 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.6% 8,414 / 21,225
demo ███████████░░░░░░░░░ 56.2% 1,497 / 2,663
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
tasks █████████████░░░░░░░ 67.0% 25,936 / 38,683
data_tools ██████████████░░░░░░ 70.0% 63 / 90
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 79.4% 19,503 / 24,561
data_modeling ████████████████░░░░ 80.0% 4,834 / 6,045
cdp ████████████████░░░░ 80.7% 3,118 / 3,864
wizard ████████████████░░░░ 82.5% 772 / 936
notebooks █████████████████░░░ 85.1% 7,096 / 8,338
cohorts █████████████████░░░ 86.2% 4,065 / 4,717
agent_platform █████████████████░░░ 86.4% 3,807 / 4,405
actions █████████████████░░░ 86.6% 717 / 828
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
exports ██████████████████░░ 88.4% 6,943 / 7,853
business_knowledge ██████████████████░░ 88.5% 4,400 / 4,969
conversations ██████████████████░░ 89.0% 16,183 / 18,186
mcp_analytics ██████████████████░░ 89.2% 2,514 / 2,819
dashboards ██████████████████░░ 89.4% 5,912 / 6,611
visual_review ██████████████████░░ 89.4% 5,818 / 6,505
engineering_analytics ██████████████████░░ 89.5% 5,507 / 6,154
alerts ██████████████████░░ 89.9% 4,054 / 4,508
early_access_features ██████████████████░░ 90.1% 1,031 / 1,144
error_tracking ██████████████████░░ 90.1% 9,885 / 10,966
streamlit_apps ██████████████████░░ 90.4% 2,501 / 2,767
slack_app ██████████████████░░ 90.6% 8,985 / 9,922
links ██████████████████░░ 90.6% 183 / 202
marketing_analytics ██████████████████░░ 90.8% 11,514 / 12,684
stamphog ██████████████████░░ 91.0% 3,993 / 4,387
product_analytics ██████████████████░░ 91.4% 5,781 / 6,328
mcp_store ██████████████████░░ 91.8% 3,685 / 4,012
data_warehouse ██████████████████░░ 92.4% 18,771 / 20,309
notifications ███████████████████░ 92.7% 1,031 / 1,112
ai_observability ███████████████████░ 92.8% 14,916 / 16,077
workflows ███████████████████░ 92.8% 5,520 / 5,949
web_analytics ███████████████████░ 92.9% 13,853 / 14,913
surveys ███████████████████░ 93.0% 5,724 / 6,157
posthog_ai ███████████████████░ 93.2% 1,325 / 1,421
approvals ███████████████████░ 93.3% 3,395 / 3,640
reminders ███████████████████░ 93.4% 468 / 501
tracing ███████████████████░ 93.4% 2,546 / 2,725
managed_migrations ███████████████████░ 93.8% 1,220 / 1,300
legal_documents ███████████████████░ 94.1% 1,568 / 1,667
endpoints ███████████████████░ 94.1% 8,606 / 9,143
messaging ███████████████████░ 94.2% 2,647 / 2,810
revenue_analytics ███████████████████░ 94.5% 3,598 / 3,809
skills ███████████████████░ 94.5% 2,881 / 3,049
review_hog ███████████████████░ 94.6% 6,806 / 7,191
logs ███████████████████░ 95.3% 9,844 / 10,325
experiments ███████████████████░ 95.7% 24,417 / 25,527
replay_vision ███████████████████░ 95.8% 13,776 / 14,383
growth ███████████████████░ 95.8% 2,837 / 2,960
annotations ███████████████████░ 96.2% 732 / 761
feature_flags ███████████████████░ 96.3% 16,233 / 16,859
user_interviews ███████████████████░ 96.4% 2,242 / 2,325
warehouse_sources ███████████████████░ 96.5% 287,939 / 298,535
access_control ███████████████████░ 96.8% 849 / 877
customer_analytics ███████████████████░ 97.2% 7,482 / 7,700
data_catalog ███████████████████░ 97.4% 2,303 / 2,365
analytics_platform ████████████████████ 98.0% 2,102 / 2,145
metrics ████████████████████ 98.2% 2,491 / 2,536
pulse ████████████████████ 98.4% 2,017 / 2,049
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.



def signal_task_followup_message(workflow_id: str, message: str | None, artifact_ids: list[str]) -> None:
def signal_task_followup_message(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 await handler(*input.args)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 😆

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hahaha, opus 4.8, but still should refund us right? :D

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def! And a beer as well 🍻

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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

@VojtechBartos
VojtechBartos force-pushed the vojtab/slack-followup-queue branch from a1f105c to f917c10 Compare July 16, 2026 14:10
@VojtechBartos
VojtechBartos changed the base branch from master to vojtab/slack-followup-signal-handler July 16, 2026 14:10

@cvolzer3 cvolzer3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fable comments incoming

Comment on lines +1046 to +1056
# Queue on the workflow so delivery is ordered with the web path and the
# turn runs under this message's actor. 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),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message now travels in the ordered queue, but the actor still travels out-of-band through run state: this activity writes slack_actor_user_id (via update_task_run_state above) before signaling, and send_followup_to_sandbox re-reads it at delivery time via get_task_run_credential_user. Under the exact scenario this PR enables (two users queuing rapid messages), Alice's queued message can be delivered after Bob's forward has already overwritten the state, so her turn runs with Bob's connection token, Bob's distinct_id, and Bob's user-scoped MCP servers mounted by the credential refresh. The old path minted the token inline with the correct actor, so this is a new gap, and this comment's claim that "the turn runs under this message's actor" is no longer guaranteed.

Suggestion: carry actor_user_id in the signal payload (send_permission_response already does exactly this) and thread it through PendingFollowup into SendFollowupToSandboxInput, falling back to run state when absent. If that's deferred, this comment should at least be reworded so it doesn't document a guarantee the code no longer provides.

@@ -1212,68 +1190,27 @@ def test_forwarding_failure_posts_error(self, mock_slack_cls, mock_send, mock_to
call_kwargs = mock_slack_instance.client.chat_postMessage.call_args.kwargs
assert "couldn't deliver" in call_kwargs["text"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_forwarding_failure_posts_error is stale now: both patches target functions the activity no longer calls, so the mocked failure return is inert. What actually happens is the real signal_task_run_user_message runs, sync_connect() attempts a live Temporal connection inside a unit test, the signal fails, and the activity posts the error reply, so the assertion passes by accident while doing real network I/O on every run. The scenario is properly covered by test_signal_failure_posts_error_reply below; this one can be deleted.

artifact_ids=_uploaded_attachment_ids(uploaded_attachments),
message_id=_slack_followup_message_id(channel, user_message_ts, thread_ts),
)
if signal_result is not True:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signal_task_run_user_message collapses every exception to False, so a transient Temporal connectivity blip now lands the user on an ❌ and "The sandbox may have stopped. Please try starting a new task." with no retry, where the old path retried retryable errors once. Since this activity is already designed to be safely re-run under Temporal retries (deterministic message_id, upsert-safe attachment upload), consider letting transient signal errors propagate so the activity retries, and reserving this terminal reply for definitive outcomes (run not found, workflow already completed).

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 (_send_followup_to_sandbox's except path sets followup_delivery_failed), whereas the old direct path posted an inline error and left the run alive for an in-thread retry. And in that async failure path nothing swaps this 👀 reaction to ❌ on the user's message; the failure only surfaces through the generic run-failure Slack update. This is consistent with the web path so it's likely intentional, but it deserves an explicit sign-off.

@VojtechBartos
VojtechBartos force-pushed the vojtab/slack-followup-queue branch from f917c10 to 947292e Compare July 17, 2026 09:38
@VojtechBartos
VojtechBartos force-pushed the vojtab/slack-followup-signal-handler branch from 129a602 to da75016 Compare July 17, 2026 10:08
@VojtechBartos
VojtechBartos force-pushed the vojtab/slack-followup-queue branch from 947292e to 844215b Compare July 17, 2026 10:08
Base automatically changed from vojtab/slack-followup-signal-handler to master July 20, 2026 07:36
Slack thread follow-ups delivered straight to the sandbox via the
facade, bypassing the task workflow's follow-up queue: no ordering with
the web path, and a busy agent needed a 504 special case. The forward
now signals send_followup_message like the web UI, so the workflow
delivers one message at a time and each delivery blocks until the turn
completes. The deterministic slack message id travels with the signal,
keeping redelivery idempotent across retries. Delivery failures surface
through the run error path; the forward still replies inline when a
message cannot be queued. The facade send_user_message wrapper loses
its last caller and is removed.
@VojtechBartos
VojtechBartos force-pushed the vojtab/slack-followup-queue branch from 844215b to 4febdf9 Compare July 20, 2026 07:48
@VojtechBartos
VojtechBartos merged commit dab8fb4 into master Jul 20, 2026
247 of 249 checks passed
@VojtechBartos
VojtechBartos deleted the vojtab/slack-followup-queue branch July 20, 2026 08:42
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 20, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-20 09:05 UTC Run
prod-us ✅ Deployed 2026-07-20 09:18 UTC Run
prod-eu ✅ Deployed 2026-07-20 09:18 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants