Skip to content

feat(tasks): post agent thread updates for canvas creation and turn completion#70371

Merged
adamleithp merged 7 commits into
feat/channel-feed-messagesfrom
posthog-code/agent-thread-updates
Jul 13, 2026
Merged

feat(tasks): post agent thread updates for canvas creation and turn completion#70371
adamleithp merged 7 commits into
feat/channel-feed-messagesfrom
posthog-code/agent-thread-updates

Conversation

@k11kirky

@k11kirky k11kirky commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Problem

When an agent builds a canvas from a channel's canvas mode, the task's thread stays empty. Teammates following the thread don't see that the canvas exists or that the agent finished. Canvas generation runs as a cloud task, so these updates have to come from the server side. Any client-side implementation only fires while the kicking-off laptop is open.

Note

Stacked on #70320 (channel feed messages) and aligned with its conventions: TaskThreadMessage gains author_kind / event / payload (same shape as ChannelFeedMessage), so agent rows are explicit (author_kind: "agent") instead of implied by a null author, and structured. turn_complete rows carry payload.run_id — the dedupe key that lets a client already rendering a run's live session-derived agent turns suppress the durable row (or vice versa), fixing the double-display overlap with the live thread panel work. Human messages keep author_kind: "human" defaults; content remains the rendered text so older clients degrade cleanly. Additive migration 0057 (no external writers of the table).

Changes

Two authorless (agent) thread messages, both server-side, both gated on the project-bluebird flag evaluated for the task creator:

  • Canvas created, once. publish_canvas (the endpoint behind the desktop-file-system-canvas-partial-update MCP tool) detects a first publish (no prior versions/code) and posts [Canvas name](canvas link) has been created into the publishing task's thread. The task is attributed via the X-PostHog-Task-Id header the sandbox already stamps on every MCP call, validated against the team (same trust model as the signals product's task attribution). The link is the /code/canvas/<channel>/<id> web interstitial, using meta.channelId with a parent-folder fallback.
  • Turn complete, every turn. The sandbox event relay (relay_sandbox_events, which already detects end-of-turn server-side) posts @[Creator](email) <the agent's final turn message> into the thread for background runs of channel tasks. The relay accumulates the agent's message chunks and resets the buffer on each new tool call or user message, so what's posted is the closing prose after the last tool call (capped in length, with a plain "Turn complete." fallback when no text was captured). Interactive runs are excluded (they already dispatch awaiting-input pushes, and chat turns would spam the thread). A short Redis cooldown per run absorbs duplicate end-of-turn events from SSE reconnects, mirroring the push dispatcher.

Supporting pieces: a create agent thread message write path in the tasks facade (author=None, mentions still indexed so the creator gets a mentions-feed entry), and format_mention_token in mentions.py as the serializer counterpart of the existing parser.

Note

TaskThreadMessage.author was already nullable and the serializers already handle a null author, but nothing wrote authorless messages until now. The PostHog Code client renders these as agent messages (PostHog/code#3380).

How did you test this code?

Automated tests only, I couldn't run them in this environment (no Postgres/Redis available), so CI is the arbiter:

  • products/tasks/backend/tests/test_thread_updates.py: turn-complete writes an authorless message with the exact mention token and indexes the creator's mention (catches breaking the token format or author attribution, which the client parses); the cooldown collapses duplicate end-of-turn events (catches removing the guard that prevents double posts on relay reconnect); flag-off / channel-less / creator-less tasks stay silent (catches thread spam for non-flagged users); canvas message content incl. bracket sanitization that would break the link token.
  • posthog/api/file_system/test/test_canvas_publish.py: first publish with the task header announces once and a second publish doesn't (catches losing the first-publish detection); the parent-folder link fallback; a publish without task attribution stays silent (catches announcing human/app saves).

ruff check and ruff format pass on all touched files.

Automatic notifications

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

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Authored by Claude (PostHog Code cloud task) at Peter Kirkham's direction; companion client PR: PostHog/code#3380.
  • Skills invoked: /improving-drf-endpoints, /writing-tests.
  • Considered a client-side implementation first (posting from the app when it observes generation finishing) and rejected it because the poster's laptop may be closed while the cloud run finishes. Also considered agent-side hooks inside the sandbox; the cloud runner has no hook mechanism, and the server already has deterministic detection points (the canvas publish endpoint, the relay's end-of-turn), so the posts live there.
  • The facade functions are best-effort by design: a failed thread post must never fail a canvas publish or disturb the event relay.

Created with PostHog Code

@k11kirky k11kirky self-assigned this Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hey @k11kirky! 👋

It looks like your git author email on this PR isn't your @posthog.com address (k11kirky@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

k11kirky added a commit to PostHog/code that referenced this pull request Jul 13, 2026
The canvas-created and turn-complete thread posts now come from the PostHog backend (PostHog/posthog#70371) so they land even when no client is open. This PR keeps only the rendering side: markdown [label](url) links in thread messages, and authorless messages shown as "Agent" with a robot avatar.

Generated-By: PostHog Code
Task-Id: ea09a661-1f6d-4bd8-a046-24be60909d28
@trunk-io

trunk-io Bot commented Jul 13, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

k11kirky added 3 commits July 13, 2026 12:47
…ompletion

Canvas generation runs in the cloud, so thread announcements must come from the server, not a client that may be closed. On a canvas's first publish (attributed via the X-PostHog-Task-Id header the sandbox stamps on MCP calls), the generating task's thread gets an authorless "[name](link) has been created" message. On every end-of-turn the sandbox relay observes for a background channel-task run, the thread gets a turn-complete note @-mentioning the task creator. Both are gated on the project-bluebird flag and are best-effort.

Generated-By: PostHog Code
Task-Id: ea09a661-1f6d-4bd8-a046-24be60909d28
…ests

The endpoint test lives outside the isolated tasks product, so it now loads Task/TaskThreadMessage via apps.get_model (the pattern test_folder_context_generation uses) instead of importing product internals. Renamed self.run to self.task_run since it shadowed TestCase.run.

Generated-By: PostHog Code
Task-Id: ea09a661-1f6d-4bd8-a046-24be60909d28
The turn-complete thread post now carries the agent's closing message instead of a static "Turn complete." The relay accumulates agent_message_chunk text and resets on a new tool call or user message, so end-of-turn holds the prose after the last tool call. Falls back to "Turn complete." when no text was captured (e.g. after a reconnect), and the facade caps oversized messages.

Generated-By: PostHog Code
Task-Id: ea09a661-1f6d-4bd8-a046-24be60909d28
@k11kirky k11kirky force-pushed the posthog-code/agent-thread-updates branch from c958286 to dab7dc9 Compare July 13, 2026 11:48
@k11kirky k11kirky changed the base branch from master to feat/channel-feed-messages July 13, 2026 11:48
…tions

Stacked on the ChannelFeedMessage PR: TaskThreadMessage gains author_kind/event/payload mirroring its shape. Agent rows are now author_kind=agent with a stable event key (canvas_created, turn_complete) and structured payload; turn_complete carries run_id so clients rendering live session-derived agent turns can dedupe against the durable row. Content keeps the rendered text so older clients degrade cleanly. Additive migration 0057 on posthog_task_thread_message (no external writers).

Generated-By: PostHog Code
Task-Id: ea09a661-1f6d-4bd8-a046-24be60909d28
k11kirky added a commit to PostHog/code that referenced this pull request Jul 13, 2026
Types the new TaskThreadMessage fields from PostHog/posthog#70371 (stacked on #70320) and keys agent rendering on author_kind, keeping the authorless fallback for older backends. payload.run_id on turn_complete rows is the dedupe key for live session-derived agent turn views.

Generated-By: PostHog Code
Task-Id: ea09a661-1f6d-4bd8-a046-24be60909d28
@k11kirky k11kirky marked this pull request as ready for review July 13, 2026 12:03
@assign-reviewers-posthog assign-reviewers-posthog Bot requested a review from a team July 13, 2026 12:03
@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Security Review

The canvas publish path can post into any same-team task thread named by the request header, so the task ownership check needs to be tightened.

Reviews (1): Last reviewed commit: "feat(tasks): align agent thread rows wit..." | Re-trigger Greptile

Comment thread posthog/api/file_system/file_system.py Outdated
k11kirky added 2 commits July 13, 2026 13:06
The IDOR model-coverage repo check requires every team-scoped model to appear in the semgrep rule list; ChannelFeedMessage (introduced on the base branch this PR stacks on) was missing, failing repo checks and cancelling the rest of backend CI.

Generated-By: PostHog Code
Task-Id: ea09a661-1f6d-4bd8-a046-24be60909d28
…ntity

The X-PostHog-Task-Id header only proved same-team membership, so a caller able to publish one canvas could name any same-team task and plant an agent-authored canvas_created message in its thread (flagged by review). The sandbox publishes with the task creator's credentials, so the facade now requires the attributed task to be created by the requesting user and drops the announcement otherwise.

Generated-By: PostHog Code
Task-Id: ea09a661-1f6d-4bd8-a046-24be60909d28
@tests-posthog

tests-posthog Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

⏭️ Skipped snapshot commit because branch advanced to ca835ef while workflow was testing e5c17ff.

The new commit will trigger its own snapshot update workflow.

If you expected this workflow to succeed: This can happen due to concurrent commits. To get a fresh workflow run, either:

  • Merge master into your branch, or
  • Push an empty commit: git commit --allow-empty -m 'trigger CI' && git push

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit 5f38e68 · box box-b0f20d8a97a8 · ready in 521s (push → usable) · build log · rebuilds on every push, torn down on close

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — no change

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 63.04 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 budget

How 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 import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.21 MiB · 22 files no change ███░░░░░░░ 28.2% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.09 MiB · 2,969 files no change █████████░ 87.5% of 9.25 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
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.2/node_modules/posthog-js/dist/rrweb.js
266.9 KiB ../node_modules/.pnpm/@posthog+icons@0.37.4_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
221.5 KiB ../node_modules/.pnpm/posthog-js@1.399.2/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.5 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1267.34 MiB · 🔺 +10.5 KiB (+0.0%)

ℹ️ MCP UI apps size — 31 app(s), 15651.9 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 577.8 KB 177.4 KB
action 429.2 KB 177.4 KB
action-list 542.6 KB 177.4 KB
cohort 428.2 KB 177.4 KB
cohort-list 541.6 KB 177.4 KB
email-template 428.1 KB 177.4 KB
error-details 443.9 KB 177.4 KB
error-issue 428.9 KB 177.4 KB
error-issue-list 542.5 KB 177.4 KB
experiment 539.9 KB 177.4 KB
experiment-list 543.4 KB 177.4 KB
experiment-results 541.6 KB 177.4 KB
feature-flag 545.4 KB 177.4 KB
feature-flag-list 549.1 KB 177.4 KB
feature-flag-testing 432.1 KB 177.4 KB
insight-actors 540.6 KB 177.4 KB
invite-email-preview 427.5 KB 177.4 KB
llm-costs 537.9 KB 177.4 KB
session-recording 429.9 KB 177.4 KB
session-summary 435.3 KB 177.4 KB
survey 429.7 KB 177.4 KB
survey-global-stats 540.7 KB 177.4 KB
survey-list 543.3 KB 177.4 KB
survey-stats 540.6 KB 177.4 KB
trace-span 428.6 KB 177.4 KB
trace-span-list 542.6 KB 177.4 KB
workflow 428.6 KB 177.4 KB
workflow-list 542.0 KB 177.4 KB
query-results 723.5 KB 177.4 KB
render-ui 613.8 KB 177.4 KB
visual-review-snapshots 433.0 KB 177.4 KB
Playwright — all passed

All tests passed.

View test results →

@adamleithp adamleithp merged commit 25490dd into feat/channel-feed-messages Jul 13, 2026
159 checks passed
@adamleithp adamleithp deleted the posthog-code/agent-thread-updates branch July 13, 2026 12:47
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()

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.

Medium: Agent announcements can be forged

X-PostHog-Task-Id is an ordinary caller-controlled header. Checking that the authenticated user created the task prevents writes to another user's task, but any team member can create a public-channel task and directly publish a canvas with this header, causing the server to insert an author_kind=agent announcement without any agent involvement. Require sandbox-authenticated context, such as a signed task claim or dedicated internal credential, before creating an agent-authored row.

@veria-ai

veria-ai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR overview

This PR adds task-related agent thread updates, including posting announcements when a canvas is created and when a turn completes. The touched file-system publishing path now participates in creating these agent-authored task thread messages.

There is one open security issue: the agent announcement path can currently be triggered with a caller-controlled task header, allowing a team member to forge an agent-authored canvas announcement in a task they created. This is an integrity and trust-boundary problem with a limited blast radius, since it affects task/thread messaging rather than direct data access or code execution. No issues have been addressed yet, so this should be tightened before merge by requiring a server-trusted agent or sandbox credential for agent-authored rows.

Open issues (1)

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

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.

2 participants