feat(tasks): channel feed messages — durable, multiplayer system announcements - #70320
Conversation
…uncements
Introduces ChannelFeedMessage: the first durable, team-visible "feed message"
model for task channels. Until now a channel's feed was purely a task list
(GET /tasks/?channel=<uuid>) with no way to record anything that isn't a task —
no lifecycle events, no agent/system rows, nothing.
ChannelFeedMessage adds that surface, deliberately generic:
- author (FK user) + author_kind (human | system | agent) — the first
system/agent-authored rows in a channel feed, not just human tasks
- event (stable key) + payload (JSON) — structured + rename/i18n-safe
- content — freeform escape hatch
- optional client-supplied created_at so a burst of announcements orders
deterministically instead of racing on insert time
Endpoint: GET/POST /api/projects/{id}/task_channels/{channel_id}/feed/
(nested under the channel, personal channels owner-only, public team-visible).
First event wired end-to-end: channel_created — emitted server-side in
resolve_channel the moment a public channel is created, so "Ann created this
context" appears in the feed no matter which client or integration created it.
The desktop app (posthog/code) posts a companion context_md_building row when it
launches the CONTEXT.md planning session.
Layered per the tasks app conventions: model -> facade (frozen DTOs) -> DRF
serializers -> viewset -> route. Full API test coverage for post/list, team
visibility, personal-channel isolation, event validation, and the
server-emitted channel_created (including no-reemit on resolve of an existing
channel).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Reviews (1): Last reviewed commit: "feat(tasks): channel feed messages — dur..." | Re-trigger Greptile |
ReviewContext: First non-task feed row for channels — aligns with the channels architecture plan (code#3328) as a precursor to workstream F (real-time feed). One tension worth tracking: the plan sequences workstream A (unified channel model) first, and this table FKs the task- Correctness — solid:
Findings:
Style: layering exactly matches the tasks app (model → facade DTO → serializer → viewset → route), no inline logic in views, constraint comments justify themselves. Verdict: high quality, nothing blocking. Fixing 1/2/5/6 on the branch now. |
Clamp created_at to ±10min of now (it exists for burst ordering, not backdating), test the ordering it was added for, test the cross-team boundary, and document the team_scope() requirement for future non-request emitters plus the unpaginated-list assumption. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR overviewThis PR adds durable channel feed messages for task-related system announcements, including publishing announcements when filesystem-backed task or canvas activity occurs. The changes support multiplayer visibility for these task announcements in public channels. One issue remains open after one prior issue was addressed. The remaining concern is that ordinary write requests can supply a task header that causes an announcement to be recorded as an agent message, allowing a team member to create trusted-looking system activity in a public channel. The impact is limited to message provenance and teammate-facing announcement integrity, but it is directly attacker-controlled within the app’s normal permissions. Open issues (1)
Fixed/addressed: 1 · PR risk: 5/10 |
…ompletion (#70371) Co-authored-by: tests-posthog[bot] <250237707+tests-posthog[bot]@users.noreply.github.com>
🦔 Hogbox preview · ✅ ready▶ Open the preview
commit |
🤖 CI report✅ Bundle size — no changeUncompressed size of every built Total: 64.43 MiB · no change No file changed by more than 1000 B. Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report ✅ Eager graph — within budgetHow much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy
🟢 Largest files eagerly shipped from
|
| Size | File |
|---|---|
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 24.6 KiB | ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js |
| 6.3 KiB | ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js |
| 4.5 KiB | ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js |
| 3.9 KiB | ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js |
| 1.4 KiB | ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js |
| 1.3 KiB | src/RootErrorBoundary.tsx |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
| 789 B | src/scenes/ChunkLoadErrorBoundary.tsx |
| 668 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 278.6 KiB | ../node_modules/.pnpm/posthog-js@1.399.5/node_modules/posthog-js/dist/rrweb.js |
| 267.7 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 234.9 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 222.1 KiB | ../node_modules/.pnpm/posthog-js@1.399.5/node_modules/posthog-js/dist/module.js |
| 164.0 KiB | src/queries/validators.js |
| 154.3 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 105.9 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
| 90.6 KiB | ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479
⚠️ Dist folder size — 🔺 +10.6 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1284.45 MiB · 🔺 +10.6 KiB (+0.0%)
ℹ️ MCP UI apps size — 31 app(s), 16561.5 KB JS
Built size of each MCP UI app (main.js + styles.css).
| App | JS | CSS |
|---|---|---|
| debug | 598.2 KB | 179.2 KB |
| action | 456.5 KB | 179.2 KB |
| action-list | 563.0 KB | 179.2 KB |
| cohort | 455.4 KB | 179.2 KB |
| cohort-list | 562.0 KB | 179.2 KB |
| email-template | 455.3 KB | 179.2 KB |
| error-details | 471.1 KB | 179.2 KB |
| error-issue | 456.1 KB | 179.2 KB |
| error-issue-list | 562.9 KB | 179.2 KB |
| experiment | 560.1 KB | 179.2 KB |
| experiment-list | 563.8 KB | 179.2 KB |
| experiment-results | 561.8 KB | 179.2 KB |
| feature-flag | 565.8 KB | 179.2 KB |
| feature-flag-list | 569.5 KB | 179.2 KB |
| feature-flag-testing | 459.4 KB | 179.2 KB |
| insight-actors | 560.8 KB | 179.2 KB |
| invite-email-preview | 454.7 KB | 179.2 KB |
| llm-costs | 558.1 KB | 179.2 KB |
| session-recording | 457.2 KB | 179.2 KB |
| session-summary | 462.5 KB | 179.2 KB |
| survey | 457.0 KB | 179.2 KB |
| survey-global-stats | 560.9 KB | 179.2 KB |
| survey-list | 563.7 KB | 179.2 KB |
| survey-stats | 560.8 KB | 179.2 KB |
| trace-span | 455.8 KB | 179.2 KB |
| trace-span-list | 562.9 KB | 179.2 KB |
| workflow | 455.8 KB | 179.2 KB |
| workflow-list | 562.4 KB | 179.2 KB |
| query-results | 743.7 KB | 179.2 KB |
| render-ui | 824.0 KB | 179.2 KB |
| visual-review-snapshots | 460.3 KB | 179.2 KB |
⚠️ Playwright — 1 failed
🎭 Playwright report · View test results →
❌ 1 failed test:
- View persons list, navigate to detail, and browse tabs (chromium)
These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!
⚠️ Backend coverage — 93.0% of changed backend lines covered — 32 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ███████████████████░ 93.0% (482 / 514)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/tasks/backend/temporal/process_task/activities/tests/test_relay_sandbox_events.py |
0.0% | 560 |
products/tasks/backend/temporal/process_task/activities/relay_sandbox_events.py |
50.0% | 334, 406, 412, 421, 428–429, 485, 507, 520, 658 |
products/tasks/backend/facade/api.py |
83.9% | 4760–4761, 4882, 5097–5098, 5104, 5110–5112, 5143–5144, 5160, 5184–5185 |
products/tasks/backend/presentation/views/channels_api.py |
88.2% | 120, 123–124, 162 |
products/tasks/backend/presentation/serializers.py |
96.2% | 1343 |
products/tasks/backend/models.py |
96.3% | 931 |
posthog/api/file_system/file_system.py |
97.1% | 1229 |
🤖 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 29334057965 -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,411 / 21,215 |
demo |
███████████░░░░░░░░░ 55.2% |
1,436 / 2,601 |
warehouse_sources_queue |
████████████░░░░░░░░ 59.2% |
148 / 250 |
tasks |
█████████████░░░░░░░ 67.1% |
25,151 / 37,465 |
data_tools |
██████████████░░░░░░ 70.0% |
63 / 90 |
ai_gateway |
███████████████░░░░░ 75.0% |
9 / 12 |
data_modeling |
████████████████░░░░ 78.4% |
4,696 / 5,987 |
signals |
████████████████░░░░ 78.5% |
18,101 / 23,059 |
cdp |
████████████████░░░░ 80.6% |
3,105 / 3,851 |
wizard |
████████████████░░░░ 82.5% |
772 / 936 |
cohorts |
█████████████████░░░ 82.9% |
3,048 / 3,675 |
notebooks |
█████████████████░░░ 83.8% |
6,086 / 7,259 |
agent_platform |
█████████████████░░░ 84.1% |
3,095 / 3,678 |
actions |
█████████████████░░░ 86.6% |
717 / 828 |
product_tours |
█████████████████░░░ 87.5% |
1,266 / 1,447 |
engineering_analytics |
██████████████████░░ 87.6% |
4,417 / 5,040 |
exports |
██████████████████░░ 88.3% |
6,857 / 7,763 |
visual_review |
██████████████████░░ 88.5% |
5,565 / 6,287 |
business_knowledge |
██████████████████░░ 88.5% |
4,400 / 4,969 |
conversations |
██████████████████░░ 88.9% |
15,924 / 17,921 |
mcp_analytics |
██████████████████░░ 89.1% |
2,485 / 2,790 |
dashboards |
██████████████████░░ 89.1% |
5,648 / 6,337 |
error_tracking |
██████████████████░░ 89.5% |
9,611 / 10,734 |
streamlit_apps |
██████████████████░░ 90.4% |
2,499 / 2,764 |
slack_app |
██████████████████░░ 90.6% |
9,460 / 10,444 |
links |
██████████████████░░ 90.6% |
183 / 202 |
marketing_analytics |
██████████████████░░ 90.7% |
11,476 / 12,646 |
product_analytics |
██████████████████░░ 91.0% |
5,450 / 5,989 |
managed_migrations |
██████████████████░░ 91.9% |
908 / 988 |
workflows |
██████████████████░░ 92.0% |
4,795 / 5,210 |
mcp_store |
██████████████████░░ 92.1% |
3,665 / 3,981 |
data_warehouse |
██████████████████░░ 92.1% |
17,298 / 18,781 |
alerts |
██████████████████░░ 92.1% |
3,389 / 3,678 |
web_analytics |
███████████████████░ 92.7% |
13,702 / 14,787 |
notifications |
███████████████████░ 92.7% |
1,026 / 1,107 |
ai_observability |
███████████████████░ 92.7% |
14,670 / 15,822 |
surveys |
███████████████████░ 92.9% |
5,660 / 6,094 |
posthog_ai |
███████████████████░ 93.2% |
1,311 / 1,407 |
tracing |
███████████████████░ 93.2% |
2,423 / 2,599 |
approvals |
███████████████████░ 93.3% |
3,395 / 3,640 |
reminders |
███████████████████░ 93.4% |
468 / 501 |
early_access_features |
███████████████████░ 93.8% |
848 / 904 |
legal_documents |
███████████████████░ 94.1% |
1,568 / 1,667 |
endpoints |
███████████████████░ 94.1% |
8,606 / 9,143 |
skills |
███████████████████░ 94.4% |
2,827 / 2,995 |
revenue_analytics |
███████████████████░ 94.4% |
3,586 / 3,797 |
messaging |
███████████████████░ 94.5% |
2,530 / 2,677 |
review_hog |
███████████████████░ 94.5% |
6,429 / 6,802 |
growth |
███████████████████░ 94.9% |
2,393 / 2,522 |
logs |
███████████████████░ 95.3% |
9,441 / 9,907 |
experiments |
███████████████████░ 95.6% |
24,017 / 25,124 |
replay_vision |
███████████████████░ 95.6% |
12,690 / 13,272 |
feature_flags |
███████████████████░ 96.0% |
14,600 / 15,203 |
warehouse_sources |
███████████████████░ 96.1% |
213,644 / 222,336 |
annotations |
███████████████████░ 96.2% |
732 / 761 |
user_interviews |
███████████████████░ 96.4% |
2,242 / 2,325 |
access_control |
███████████████████░ 96.8% |
849 / 877 |
data_catalog |
███████████████████░ 97.2% |
1,642 / 1,689 |
customer_analytics |
███████████████████░ 97.3% |
7,396 / 7,600 |
analytics_platform |
████████████████████ 98.2% |
2,098 / 2,137 |
metrics |
████████████████████ 98.3% |
2,363 / 2,403 |
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.
✅ ClickHouse migration SQL — none
No ClickHouse migrations in the latest push.
Migration SQL ChangesHey 👋, we've detected some migrations on this PR. Here's the SQL output for each migration, make sure they make sense:
|
🔍 Migration Risk AnalysisWe've analyzed your migrations for potential risks. Summary: 1 Safe | 1 Needs Review | 0 Blocked
|
| by the requesting user — the header can't point the announcement at someone | ||
| else's task thread. No header (a human or app save) means no announcement. | ||
| """ | ||
| raw_task_id = (request.headers.get("X-PostHog-Task-Id") or "").strip() |
There was a problem hiding this comment.
Medium: Caller-controlled header can forge agent messages
X-PostHog-Task-Id is accepted on ordinary file_system:write requests, but the downstream path stores the announcement with author_kind=AGENT and no author. Any team member can set this header to a public-channel task they created, publish a canvas's first version, and create a trusted-looking agent announcement visible to teammates; matching the user to the task creator does not prove the request came from the sandbox. Require sandbox-specific authenticated provenance for this path, or record requests without it as human-authored.
- write agent thread messages via for_team so temporal-relay callers pass the fail-closed manager (CI TeamScopeError) - client feed posts are marked human-authored; system/agent kinds reserved for server-side writers - cap feed payloads at 8 KB and channels at 500 feed rows; bound the list path to the newest rows - only announce canvas creation for requests bearing a sandbox-app OAuth token, so the X-PostHog-Task-Id header alone can't forge agent messages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…essages # Conflicts: # products/tasks/backend/migrations/max_migration.txt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| if ( | ||
| ChannelFeedMessage.objects.filter(channel_id=channel_id, team_id=team_id, deleted=False).count() | ||
| >= CHANNEL_FEED_MAX_MESSAGES | ||
| ): | ||
| return "full" |
There was a problem hiding this comment.
Race condition allows exceeding the CHANNEL_FEED_MAX_MESSAGES limit. Between checking the count and creating the message, concurrent requests can create additional messages, causing the channel to exceed the cap.
To fix, use a database transaction with SELECT FOR UPDATE or implement row-level locking:
from django.db import transaction
with transaction.atomic():
count = ChannelFeedMessage.objects.filter(
channel_id=channel_id, team_id=team_id, deleted=False
).select_for_update().count()
if count >= CHANNEL_FEED_MAX_MESSAGES:
return "full"
message = ChannelFeedMessage.objects.create(**fields)Alternatively, add a database constraint or use optimistic locking if the soft limit is acceptable.
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
The first of its kind
A task channel's feed has always been just a task list (
GET /tasks/?channel=<uuid>). There has never been a way to put anything into that feed that isn't a task — no lifecycle events, no agent- or system-authored rows, no "X created this channel". This PR introduces the first durable, team-visible, non-task feed entry for channels:ChannelFeedMessage.What it adds
Model (
ChannelFeedMessage) — deliberately generic so it's a foundation, not a one-off:author(FK user) +author_kind(human|system|agent) — the first system/agent-authored rows in a channel feedevent(stable key) +payload(JSON) — structured and rename/i18n-safecontent— freeform escape hatch for ad-hoc textcreated_atso a burst of announcements orders deterministically instead of racing on insert timeEndpoint —
GET/POST /api/projects/{id}/task_channels/{channel_id}/feed/, nested under the channel. Public channels are team-visible; personal (#me) channels are owner-only.First event, wired end-to-end —
channel_created, emitted server-side inresolve_channelthe moment a public channel is created. So "Ann created this context" shows up in the feed regardless of which client or integration made the channel — not something a single client has to remember to post. (The desktop app posts a companioncontext_md_buildingrow when it kicks off the CONTEXT.md planning session.)Shape / layering
Follows the tasks app's existing layering exactly:
model → facade (frozen DTOs) → DRF serializers → viewset → route. Nothing bespoke.Tests
Full API coverage in
test_channels_api.py::ChannelFeedMessageAPITestCase(7 passing): post + list, team visibility, personal-channel isolation, event validation (400), unknown channel (404), and the server-emittedchannel_created— including no re-emit when an existing channel is resolved again.Migration
0056_channelfeedmessage— additiveCreateModel+ one index;makemigrations --checkclean, applied and exercised locally.Client
Consumed by the Code desktop app (
posthog/code) — separate PR — which renders these as "PostHog agent" feed rows alongside task cards.🤖 Generated with Claude Code