feat(tasks): add task-centric activity feed endpoint - #72793
Conversation
Adds `GET /api/projects/{team}/task_activity/`, a one-row-per-task feed of every
task the requester is involved in — created, @-mentioned in, or authored a thread
message on — ordered by most recent relevant activity. This backs the Channels
(project-bluebird) Activity view, which previously showed only raw @-mentions.
Each row's activity_kind names the winning signal (awaiting_input / message /
mention / created). "Awaiting your input" is derived from the durable
event="turn_complete" thread message, which only exists for channel-filed tasks;
a newer reply the user authored outranks an older turn-complete so the row reads
"message" instead. Aggregation is a fixed set of grouped queries gated through
`_visible_task_qs`, plus two supporting indexes on TaskThreadMessage.
Why: users want the Activity view to surface all activity involving them —
especially tasks an agent has updated and is waiting on them for — not just
mentions.
Generated-By: PostHog Code
Task-Id: c10b01e4-4645-4c82-98b2-610b533f7f7c
Generated-By: PostHog Code Task-Id: c4f5025f-8edf-40d4-a163-7174899045d1
7e171cc to
3d36de5
Compare
The feed endpoint could not return a row: list_task_activity built a TaskActivityDTO with an `id` the dataclass did not declare, so every request raised TypeError. WizardCloudRunDTO had picked up the mirror-image problem — a required `id` its only construction site never passes. Both are fixed and the serializer field lists now line up with their dataclasses. `awaiting_input` was read off an `event="turn_complete"` thread message, which nothing writes any more (list_thread_messages already filters those rows out as legacy). It is now projected from notify_task_run_awaiting_input, so every path that decides a run is waiting — stream ingest, agent proxy callback, sandbox relay — feeds the same row, independent of the mobile push flag and cooldown. Read state is per task rather than a feed-wide sweep. mark_read takes task ids, and loading a task's thread clears that task's row, so reaching a task from the sidebar counts the same as clicking it in the Activity list. Activity a user caused themselves (their own task, their own reply) lands already-read, so the badge only counts things actually waiting on them. Also: the projection upsert is a single statement with a newest-wins conflict clause, replacing a get_or_create read-modify-write that could lose a row under concurrent messages and could drag activity_at backwards on a retried write. The superseded read-time aggregation is deleted. The migration is renumbered onto current master, drops its backfill (the feed is forward-only), and adds a partial index for the unread count. Generated-By: PostHog Code Task-Id: 35a47457-b411-4d4b-80a5-ad06e2e6b1e5
The repo-check IDOR sweep requires every team-scoped model to appear in the semgrep rule set. Without this the new model fails the check and cancels the rest of Backend CI before the Django suite runs. Generated-By: PostHog Code Task-Id: 35a47457-b411-4d4b-80a5-ad06e2e6b1e5
🤖 CI report✅ Bundle size — 🟢 -112 B (-0.0%)Uncompressed size of every built Total: 64.45 MiB · 🟢 -112 B (-0.0%) 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 |
| 762 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 281.5 KiB | ../node_modules/.pnpm/posthog-js@1.407.3/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 |
| 236.0 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 226.4 KiB | ../node_modules/.pnpm/posthog-js@1.407.3/node_modules/posthog-js/dist/module.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 |
| 106.2 KiB | src/lib/api.ts |
| 94.0 KiB | ../packages/quill/packages/quill/dist/index.js |
| 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
✅ Toolbar bundle — eager 2.18 MiB within budget
What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.
| Metric | Size | Δ vs base | Budget |
|---|---|---|---|
| Eager (shipped) entry + static imports |
2.18 MiB · 17 files | no change | ████░░░░░░ 38.2% of 5.72 MiB |
| Deferred (lazy) | 2.07 MiB · 33 files | no change | n/a — loads on demand |
Loader dist/toolbar.js |
1.1 KiB | no change | █░░░░░░░░░ 5.8% of 19.5 KiB |
Largest eagerly-shipped chunks
| Size | File |
|---|---|
| 716.9 KiB | dist/toolbar/toolbar-app-PSC4FBCN.css |
| 545.7 KiB | dist/toolbar/chunk-chunk-RPF3UQEA.js |
| 484.3 KiB | dist/toolbar/chunk-chunk-33S7B7JC.js |
| 133.6 KiB | dist/toolbar/chunk-chunk-TVLD7Q5U.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-T5KY5WYR.js |
| 71.0 KiB | dist/toolbar/toolbar-app-4IZ7WDYG.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-27JL52RE.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-BVRVRZS6.js |
| 20.9 KiB | dist/toolbar/chunk-chunk-Z72TCCZX.js |
| 12.2 KiB | dist/toolbar/chunk-chunk-PIK3PADE.js |
Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile
✅ Dist folder size — 🔺 +15.3 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1355.81 MiB · 🔺 +15.3 KiB (+0.0%)
ℹ️ MCP UI apps size — 32 app(s), 17066.0 KB JS
Built size of each MCP UI app (main.js + styles.css).
| App | JS | CSS |
|---|---|---|
| debug | 599.5 KB | 187.7 KB |
| action | 457.8 KB | 187.7 KB |
| action-list | 564.3 KB | 187.7 KB |
| cohort | 456.8 KB | 187.7 KB |
| cohort-list | 563.3 KB | 187.7 KB |
| email-template | 456.6 KB | 187.7 KB |
| error-details | 472.4 KB | 187.7 KB |
| error-issue | 457.5 KB | 187.7 KB |
| error-issue-list | 564.2 KB | 187.7 KB |
| experiment | 561.5 KB | 187.7 KB |
| experiment-list | 565.1 KB | 187.7 KB |
| experiment-results | 563.2 KB | 187.7 KB |
| feature-flag | 567.1 KB | 187.7 KB |
| feature-flag-list | 570.9 KB | 187.7 KB |
| feature-flag-testing | 461.0 KB | 187.7 KB |
| insight-actors | 562.1 KB | 187.7 KB |
| invite-email-preview | 456.0 KB | 187.7 KB |
| llm-costs | 559.5 KB | 187.7 KB |
| session-recording | 458.6 KB | 187.7 KB |
| session-summary | 463.9 KB | 187.7 KB |
| survey | 458.4 KB | 187.7 KB |
| survey-global-stats | 562.2 KB | 187.7 KB |
| survey-list | 565.0 KB | 187.7 KB |
| survey-stats | 562.2 KB | 187.7 KB |
| trace-span | 457.2 KB | 187.7 KB |
| trace-span-list | 564.2 KB | 187.7 KB |
| workflow | 457.1 KB | 187.7 KB |
| workflow-list | 563.7 KB | 187.7 KB |
| loops-review | 461.2 KB | 187.7 KB |
| query-results | 745.8 KB | 187.7 KB |
| render-ui | 826.4 KB | 187.7 KB |
| visual-review-snapshots | 461.6 KB | 187.7 KB |
⚠️ Backend coverage — 95.0% of changed backend lines covered — 18 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ███████████████████░ 95.0% (344 / 362)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/tasks/backend/temporal/process_task/activities/relay_sandbox_events.py |
25.0% | 405, 760, 762 |
products/tasks/backend/push_dispatcher.py |
77.8% | 101–102, 112–113 |
products/tasks/backend/presentation/views/channels_api.py |
83.9% | 218, 221–224 |
products/tasks/backend/logic/stream/event_ingest.py |
85.7% | 422 |
products/tasks/backend/facade/api.py |
91.3% | 5260, 5295, 5313, 5347 |
products/tasks/backend/presentation/serializers.py |
97.0% | 1500 |
🤖 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 30269703496 -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,261 |
demo |
███████████░░░░░░░░░ 56.2% |
1,497 / 2,663 |
warehouse_sources_queue |
████████████░░░░░░░░ 59.2% |
148 / 250 |
tasks |
██████████████░░░░░░ 69.6% |
30,976 / 44,536 |
data_tools |
██████████████░░░░░░ 70.0% |
63 / 90 |
ai_gateway |
███████████████░░░░░ 75.0% |
9 / 12 |
signals |
████████████████░░░░ 80.5% |
22,797 / 28,327 |
cdp |
████████████████░░░░ 81.0% |
3,146 / 3,883 |
data_modeling |
█████████████████░░░ 84.2% |
6,549 / 7,781 |
wizard |
█████████████████░░░ 84.8% |
935 / 1,102 |
notebooks |
█████████████████░░░ 85.6% |
7,517 / 8,777 |
agent_platform |
█████████████████░░░ 86.4% |
3,807 / 4,405 |
actions |
█████████████████░░░ 86.6% |
717 / 828 |
cohorts |
██████████████████░░ 87.8% |
4,488 / 5,114 |
product_tours |
██████████████████░░ 87.9% |
1,303 / 1,482 |
data_warehouse |
██████████████████░░ 88.3% |
12,086 / 13,692 |
exports |
██████████████████░░ 88.4% |
6,950 / 7,862 |
business_knowledge |
██████████████████░░ 89.0% |
4,391 / 4,936 |
conversations |
██████████████████░░ 89.3% |
16,953 / 18,978 |
engineering_analytics |
██████████████████░░ 89.4% |
6,386 / 7,145 |
dashboards |
██████████████████░░ 89.4% |
5,983 / 6,693 |
visual_review |
██████████████████░░ 89.5% |
5,837 / 6,522 |
error_tracking |
██████████████████░░ 89.7% |
10,174 / 11,343 |
alerts |
██████████████████░░ 90.0% |
4,056 / 4,508 |
mcp_analytics |
██████████████████░░ 90.2% |
2,879 / 3,193 |
streamlit_apps |
██████████████████░░ 90.4% |
2,501 / 2,767 |
links |
██████████████████░░ 90.6% |
183 / 202 |
slack_app |
██████████████████░░ 91.0% |
9,075 / 9,978 |
marketing_analytics |
██████████████████░░ 91.1% |
11,903 / 13,072 |
stamphog |
██████████████████░░ 91.1% |
4,056 / 4,450 |
mcp_store |
██████████████████░░ 91.9% |
4,257 / 4,634 |
product_analytics |
███████████████████░ 92.5% |
5,848 / 6,319 |
notifications |
███████████████████░ 92.6% |
1,012 / 1,093 |
early_access_features |
███████████████████░ 92.6% |
1,287 / 1,390 |
ai_observability |
███████████████████░ 92.8% |
15,228 / 16,409 |
surveys |
███████████████████░ 93.0% |
5,769 / 6,201 |
managed_migrations |
███████████████████░ 93.1% |
1,429 / 1,535 |
web_analytics |
███████████████████░ 93.2% |
14,826 / 15,906 |
posthog_ai |
███████████████████░ 93.2% |
1,326 / 1,422 |
approvals |
███████████████████░ 93.3% |
3,437 / 3,682 |
reminders |
███████████████████░ 93.4% |
468 / 501 |
workflows |
███████████████████░ 93.6% |
6,545 / 6,992 |
legal_documents |
███████████████████░ 94.1% |
1,568 / 1,667 |
endpoints |
███████████████████░ 94.1% |
8,640 / 9,177 |
tracing |
███████████████████░ 94.5% |
2,670 / 2,826 |
messaging |
███████████████████░ 94.5% |
2,811 / 2,974 |
review_hog |
███████████████████░ 94.6% |
6,898 / 7,289 |
skills |
███████████████████░ 94.6% |
3,158 / 3,337 |
logs |
███████████████████░ 95.4% |
10,018 / 10,504 |
experiments |
███████████████████░ 95.8% |
25,365 / 26,485 |
growth |
███████████████████░ 96.1% |
3,245 / 3,376 |
annotations |
███████████████████░ 96.2% |
732 / 761 |
replay_vision |
███████████████████░ 96.3% |
15,595 / 16,199 |
revenue_analytics |
███████████████████░ 96.3% |
1,887 / 1,960 |
feature_flags |
███████████████████░ 96.4% |
17,372 / 18,025 |
user_interviews |
███████████████████░ 96.5% |
2,638 / 2,734 |
access_control |
███████████████████░ 96.9% |
870 / 898 |
warehouse_sources |
███████████████████░ 97.2% |
340,242 / 350,127 |
customer_analytics |
███████████████████░ 97.2% |
9,757 / 10,038 |
data_catalog |
████████████████████ 97.6% |
2,469 / 2,530 |
analytics_platform |
████████████████████ 98.0% |
2,153 / 2,197 |
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.
⚠️ Django migration SQL — 1 new migration to review
We've detected new migrations on this PR. Review the SQL output for each migration:
products/tasks/backend/migrations/0073_task_activity.py
BEGIN;
--
-- Create model TaskActivity
--
CREATE TABLE "posthog_task_activity" ("id" uuid NOT NULL PRIMARY KEY, "kind" varchar(32) NOT NULL, "activity_at" timestamp with time zone NOT NULL, "read_at" timestamp with time zone NULL, "message_id" uuid NULL, "task_id" uuid NOT NULL, "team_id" integer NOT NULL, "user_id" integer NOT NULL);
--
-- Create constraint task_activity_user_task_unique on model taskactivity
--
ALTER TABLE "posthog_task_activity" ADD CONSTRAINT "task_activity_user_task_unique" UNIQUE ("team_id", "user_id", "task_id");
--
-- Create index task_activity_feed_idx on field(s) team, user, activity_at, id of model taskactivity
--
CREATE INDEX "task_activity_feed_idx" ON "posthog_task_activity" ("team_id", "user_id", "activity_at", "id");
--
-- Create index task_activity_unread_idx on field(s) team, user of model taskactivity
--
CREATE INDEX "task_activity_unread_idx" ON "posthog_task_activity" ("team_id", "user_id") WHERE "read_at" IS NULL;
ALTER TABLE "posthog_task_activity" ADD CONSTRAINT "posthog_task_activit_message_id_2d258364_fk_posthog_t" FOREIGN KEY ("message_id") REFERENCES "posthog_task_thread_message" ("id") DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE "posthog_task_activity" ADD CONSTRAINT "posthog_task_activity_task_id_c4d245b1_fk_posthog_task_id" FOREIGN KEY ("task_id") REFERENCES "posthog_task" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "posthog_task_activity_message_id_2d258364" ON "posthog_task_activity" ("message_id");
CREATE INDEX "posthog_task_activity_task_id_c4d245b1" ON "posthog_task_activity" ("task_id");
CREATE INDEX "posthog_task_activity_team_id_605c66cc" ON "posthog_task_activity" ("team_id");
CREATE INDEX "posthog_task_activity_user_id_9c37f739" ON "posthog_task_activity" ("user_id");
COMMIT;Last updated: 2026-07-27 13:22 UTC (c4abdd2)
✅ Django migration risk — migration analysis complete
We've analyzed your migrations for potential risks.
Summary: 1 Safe | 0 Needs Review | 0 Blocked
✅ Safe
Brief or no lock, backwards compatible
tasks.0073_task_activity
└─ #1 ✅ CreateModel
Creating new table is safe
model: TaskActivity
│
└──> ℹ️ INFO:
ℹ️ Skipped operations on newly created tables (empty tables
don't cause lock contention).
Last updated: 2026-07-27 13:22 UTC (c4abdd2)
The generated client typed taskActivityList as returning a paginated envelope, because drf-spectacular assumes the viewset's default pagination. The endpoint sends one page object carrying its own unread total, so any consumer reading `results` off the top level would have found the wrong shape. pagination_class = None on the viewset. Also switches the TaskActivity primary key to uuid7, which satisfies prefer-uuid7-django-pk without a suppression. The sibling task models carry nosemgrep waivers because they predate the rule; a new table has no such constraint, and a time-ordered key suits this one — rows are insert-heavy and read newest-first, so index appends stay local and the id becomes a meaningful tiebreak when two rows share an activity_at. Generated-By: PostHog Code Task-Id: 35a47457-b411-4d4b-80a5-ad06e2e6b1e5
Dropping pagination stopped drf-spectacular wrapping the response in a paginated envelope, but its list-view heuristic still typed the operation as an array of page objects. `list` returns exactly one envelope (results + unread_count), so force the heuristic off with an AutoSchema subclass, following the same pattern review_hog uses for its page envelope — including pinning the operationId back to `*_list`, since disabling the heuristic otherwise renames it to `*_retrieve`. Also records the task-activity post_save receiver in setup_receivers_baseline.txt. test_setup_receivers_match_baseline fails on any receiver connecting during django.setup() that isn't listed; this one lives in the same already-imported module as track_task_run_completion, so it adds no startup import weight. Generated-By: PostHog Code Task-Id: 35a47457-b411-4d4b-80a5-ad06e2e6b1e5
Generated-By: PostHog Code Task-Id: 35a47457-b411-4d4b-80a5-ad06e2e6b1e5
🦔 Hogbox preview · ✅ ready▶ Open the preview
commit |
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
products/tasks/backend/facade/api.py:5268
**Replayed waits become fresh activity**
When an awaiting-input notification is replayed after the user reads the task or newer activity arrives, assigning `django_timezone.now()` makes the old event win the newest-wins upsert and resets `read_at` to null, causing a stale awaiting-input row and unread badge.
### Issue 2 of 2
products/tasks/backend/facade/api.py:5142
**Read state races message retrieval**
When another message lands between this update and the following message query, the response includes that message while its activity remains unread, causing the user to see the message while the sidebar still marks its task unread. A query failure after the committed update instead clears activity without returning the thread.
Reviews (1): Last reviewed commit: "chore(tasks): annotate the activity quer..." | Re-trigger Greptile |
| user_id=creator_id, | ||
| task_id=task_run.task_id, | ||
| kind=TaskActivity.Kind.AWAITING_INPUT, | ||
| activity_at=django_timezone.now(), |
There was a problem hiding this comment.
Replayed waits become fresh activity
When an awaiting-input notification is replayed after the user reads the task or newer activity arrives, assigning django_timezone.now() makes the old event win the newest-wins upsert and resets read_at to null, causing a stale awaiting-input row and unread badge.
Prompt To Fix With AI
This is a comment left during a code review.
Path: products/tasks/backend/facade/api.py
Line: 5268
Comment:
**Replayed waits become fresh activity**
When an awaiting-input notification is replayed after the user reads the task or newer activity arrives, assigning `django_timezone.now()` makes the old event win the newest-wins upsert and resets `read_at` to null, causing a stale awaiting-input row and unread badge.
How can I resolve this? If you propose a fix, please make it concise.Generated-By: PostHog Code Task-Id: 744f81ec-6b8e-420c-960c-541b1932b46c
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
Generated-By: PostHog Code Task-Id: 744f81ec-6b8e-420c-960c-541b1932b46c
Generated-By: PostHog Code Task-Id: 744f81ec-6b8e-420c-960c-541b1932b46c
Generated-By: PostHog Code Task-Id: 744f81ec-6b8e-420c-960c-541b1932b46c
Generated-By: PostHog Code Task-Id: 744f81ec-6b8e-420c-960c-541b1932b46c
| def notify_task_run_turn_completed(task_run: TaskRun) -> None: | ||
| _project_completed_activity(task_run) | ||
| _enqueue(task_run, kind="awaiting", body=f'"{_task_title(task_run)}" finished') | ||
|
|
There was a problem hiding this comment.
Inconsistency between activity projection and push notification kind. The function projects a COMPLETED activity via _project_completed_activity(task_run) but enqueues a push notification with kind="awaiting". This mismatch could cause:
- The activity feed showing "completed" while push notifications show "awaiting"
- Client code expecting these to match may break
- User confusion from inconsistent messaging
Fix:
def notify_task_run_turn_completed(task_run: TaskRun) -> None:
_project_completed_activity(task_run)
_enqueue(task_run, kind="completed", body=f'"{_task_title(task_run)}" finished')Or if "awaiting" is intentional, the activity projection should use _project_awaiting_input_activity instead.
| def notify_task_run_turn_completed(task_run: TaskRun) -> None: | |
| _project_completed_activity(task_run) | |
| _enqueue(task_run, kind="awaiting", body=f'"{_task_title(task_run)}" finished') | |
| def notify_task_run_turn_completed(task_run: TaskRun) -> None: | |
| _project_completed_activity(task_run) | |
| _enqueue(task_run, kind="completed", body=f'"{_task_title(task_run)}" finished') | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
Generated-By: PostHog Code Task-Id: 744f81ec-6b8e-420c-960c-541b1932b46c
Generated-By: PostHog Code Task-Id: 744f81ec-6b8e-420c-960c-541b1932b46c
Problem
Activity in Channels needs to show task-level updates that require attention, including agent completion and permission requests. Read state must remain correct when new events arrive concurrently, and large feeds must not hide unread tasks behind a fixed first page.
Changes
Adds a task-centric activity API backed by a per-user, per-task projection.
mark_readaction accepts per-task activity timestamps so an older client cannot clear newer activity.The companion desktop implementation is PostHog/code #3671.
How did you test this code?
Regression coverage includes:
Validated with Ruff, formatting, repository-wide mypy, migration checks, OpenAPI and MCP generation, strict preflight, and the full backend CI matrix. Database-backed tests could not run in the agent workspace because Docker and Postgres were unavailable; they ran in CI.
Automatic notifications
Docs update
No standalone documentation change is needed. The generated OpenAPI and MCP contracts document the endpoint and request fields.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
PostHog Code using Codex implemented and validated the final changes after a human review and manual product testing. Skills used:
/django-migrations,/improving-drf-endpoints,/implementing-mcp-tools,/writing-tests,/writing-code-comments, and/running-ci-preflight.The main design decisions were to keep reads side-effect-free, version explicit read mutations with the activity being acknowledged, and project completion independently of mobile push delivery. The branch was updated from
masterwith a signed merge after an unrelated MCP integration failure.Created with PostHog Code