Skip to content

feat(growth): turn suggestion backend - #106432

Merged
trunk-io[bot] merged 11 commits into
masterfrom
rafa/ts-5-turn-suggestions-backend
Oct 6, 2026
Merged

trunk-io[bot] merged 11 commits into
masterfrom
rafa/ts-5-turn-suggestions-backend

Conversation

@rafaeelaudibert

@rafaeelaudibert rafaeelaudibert commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Problem

  • Someone who asks PostHog AI a question worth tracking gets one answer, and no way to keep getting it.
  • A scout, a notebook, an alert, a subscription or an error alert could carry that answer forward, but the user has to know they exist and build one by hand.
  • Layer 6 of 9 in the stack that replaces feat(growth): suggest a scout or notebook after a PostHog AI turn #101991. Layers 1 to 5 are merged. This layer decides which follow-up to offer. The cards arrive in layers 8 and 9.

Note

This layer needs a worker consuming the new posthog_ai Celery queue. https://github.com/PostHog/charts/pull/16307 adds it and merged on 2026-09-25. Confirm the worker runs in each region before this layer merges.

Changes

  • Offer: after a PostHog AI turn completes, the server can publish one suggestion frame to the conversation: a scout, a notebook, an insight alert, a subscription or an error alert.
  • Judge: Jev, a System One model, judges the turn. It sees the question, the tool names and a masked answer, never tool outputs or ids.
  • Judge server: only the ai-gateway answers, with posthog/hogference/jevk5-fp8-0.2, through the client from feat(llm): add a system one client that prefers the ai-gateway #106735.
  • No TypeSafe: the judge has no TypeSafe fallback. Master blocks TypeSafe on Cloud (fix(typesafe): block external requests on cloud #108333), and the judge state carries the user's question.
  • Thresholds: production and the benchmark in chore(growth): turn suggestion judge benchmark #106433 share the same policy function and the same 0.5 show threshold. At 0.5, the gateway model scored 95% precision and 95% recall.
  • Drafter: Luna writes text only for a scout prompt or a notebook outline. Every other card uses templated copy.
  • Limits: a conversation gets at most two cards, never on two turns in a row. A dismissed card mutes the conversation.
  • Stored outcome: the offer ledger in Task.state is the only stored outcome of a card. The new GET turn_suggestions/state/ endpoint serves it to a reloaded thread.
  • Resolve: POST turn_suggestions/resolve/ records a dismiss or an accept. It tells other open tabs through a frame on the offer's run and on the task's latest run.
  • Gates: the posthog-ai-turn-suggestions flag, AI data processing approval, a conversation started in the web app, and a configured ai-gateway (AI_GATEWAY_URL with AI_GATEWAY_API_KEY).
  • Queue: the work runs on a new posthog_ai Celery queue, two seconds after the turn, so the last frames of the answer have landed. A job that has not started 32 seconds after the report expires, so a stalled worker drains no late cards.
  • Completed turns only: the job judges the latest turn only once its _posthog/turn_complete frame has landed. A follow-up still streaming is noted, not judged, and its own completion classifies it.
  • Observability: every job outcome increments posthog_ai_turn_suggestion_outcomes_total{status, reason} and logs one line. A missing judge configuration logs a warning.
  • Duplicate reports: three reporters can report the same turn. A Redis SET NX held for the two-second settle wait drops the duplicates before they reach the queue.
  • Turn index: the server counts user messages with the same rules as the frontend, so both sides agree on which turn a card belongs to.
  • Generated API types and the MCP tool config are regenerated.
flowchart LR
    A[Turn completes] --> D[Redis dedup]
    D --> E[posthog_ai queue]
    E --> L[Offer ledger gates]
    L --> G{{Jev judges the masked turn}}
    G -->|scout or notebook| R{{Luna drafts the text}}
    G -->|alert, subscription, error alert| H[Suggestion frame]
    R --> H
    classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff;
    classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000;
    classDef phGray fill:#e5e7eb,stroke:#c7ccd1,color:#000;
    class A phYellow;
    class G,R phBlue;
    class D,E,L,H phGray;
Loading

How did you test this code?

  • test_turn_suggestions.py covers the transcript fold, the masked Jev request, the offer policy, the ledger rules and races, and the gates.
  • A case there catches a regression where runs with only session/prompt frames merge every earlier answer into the last turn.
  • test_turn_suggestions_api.py covers the state and resolve endpoints.
  • Not run: Luna against the real gateway, or an end-to-end conversation. The local machine has no AI stack.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

None: the feature is behind a flag that is off by default.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Fable 5.1 wrote the original #101991. Claude Opus 5.5 switched the judge to Jev, split the stack and fixed the review findings.

  • Skills invoked: /stacking-prs, /typesafe-ai, /routing-outbound-api-calls, /improving-drf-endpoints, /writing-dataclasses, /writing-tests, /writing-code-comments, /security-audit, /code-review, /writing-pr-descriptions.
  • CodeRabbit CLI pass skipped by the user's standing choice.
  • Compared to feat(growth): suggest a scout or notebook after a PostHog AI turn #101991: a dedicated Celery queue, the Redis dedup, and the ledger as the only stored outcome. feat(growth): suggest a scout or notebook after a PostHog AI turn #101991 also wrote a resolution frame to S3.
  • Billing: the Luna drafter calls PostHog/ai-gateway, so PostHog pays for drafts. The Python gateway fallback uses the unbilled growth route, never the customer's AI credits.
  • The judge no longer reads TYPESAFE_API_KEY, so this stack does not use the secret from https://github.com/PostHog/charts/pull/16183.
  • On 2026-10-03, Claude Opus 5.5 rebased the stack on master. It dropped the TypeSafe fallback and moved team attribution to build_system_one_client(team_id=...), to match master's System One rules.
  • All fixture data is invented.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

✅ Complexity (TypeScript) — clean

Cyclomatic complexity above the limit in changed typescript files (10 for production files, 15 for test files). Warn only: worth simplifying when you next touch these functions.

✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Bundle size — no change

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

Total: 76.00 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 — 2 over 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.64 MiB · 23 files no change █████████░ 88.9% of 1.84 MiB
logged-out boot: index + App + bootApp (preloaded by every page, including /login)
src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
3.72 MiB · 663 files no change █████████░ 92.3% of 4.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.62 MiB · 2,415 files no change █████████░ 91.4% of 8.34 MiB
🟡 dashboard scene
src/scenes/dashboard/Dashboard.tsx
13.90 MiB · 4,917 files no change ██████████ 103.2% of 13.48 MiB
today home path
src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
7.64 MiB · 2,427 files no change █████████░ 89.0% of 8.58 MiB
🟡 events scene
src/scenes/activity/explore/EventsScene.tsx
13.63 MiB · 4,789 files no change ██████████ 107.9% of 12.64 MiB
replay detail scene
src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
12.13 MiB · 4,142 files no change ████████░░ 77.2% of 15.72 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/svg/ stays out of src/index.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/components/ stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/dashboards/frontend/widgets/previews/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 zod/v4/locales/de.js stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/svg/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/components/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 src/scenes/project-homepage/today/TodayReportPage.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 src/queries/Query/Query.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.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/index.tsx
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
839 B src/scenes/ChunkLoadErrorBoundary.tsx
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
220.3 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
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
99.7 KiB src/lib/api.ts
90.6 KiB src/products.tsx
68.7 KiB src/lib/lemon-ui/icons/icons.tsx
40.7 KiB src/lib/utils/eventUsageLogic.ts
38.7 KiB ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js
33.9 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
29.0 KiB ../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v4/core/schemas.js
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.3 KiB src/taxonomy/core-filter-definitions-by-group.json
220.3 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
153.7 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
112.3 KiB ../packages/quill/packages/quill/dist/index.js
99.7 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
90.6 KiB src/products.tsx
Largest files eagerly shipped from src/scenes/dashboard/Dashboard.tsx
Size File
315.5 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.3 KiB src/taxonomy/core-filter-definitions-by-group.json
220.3 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
181.8 KiB src/queries/validators.js
153.7 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
112.3 KiB ../packages/quill/packages/quill/dist/index.js
99.7 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
Size File
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.3 KiB src/taxonomy/core-filter-definitions-by-group.json
220.3 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
153.7 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
112.3 KiB ../packages/quill/packages/quill/dist/index.js
99.7 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
90.6 KiB src/products.tsx
Largest files eagerly shipped from src/scenes/activity/explore/EventsScene.tsx
Size File
315.5 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.3 KiB src/taxonomy/core-filter-definitions-by-group.json
220.3 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
181.8 KiB src/queries/validators.js
153.7 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
112.3 KiB ../packages/quill/packages/quill/dist/index.js
99.7 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
Size File
315.5 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
280.3 KiB src/taxonomy/core-filter-definitions-by-group.json
220.3 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
181.8 KiB src/queries/validators.js
153.7 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
112.3 KiB ../packages/quill/packages/quill/dist/index.js
99.7 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/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.21 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.21 MiB · 19 files no change ████░░░░░░ 38.7% of 5.72 MiB
Deferred (lazy) 2.12 MiB · 44 files no change n/a — loads on demand
Loader dist/toolbar.js 1.2 KiB no change █░░░░░░░░░ 6.0% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
850.6 KiB dist/toolbar/toolbar-app-5BH4NJL4.css
657.4 KiB dist/toolbar/chunk-chunk-R75Z5TYV.js
259.4 KiB dist/toolbar/chunk-chunk-6EL3FW4B.js
138.2 KiB dist/toolbar/chunk-chunk-2GNJAOKU.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
75.2 KiB dist/toolbar/toolbar-app-2PA223GW.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-46UEXYQR.js
21.0 KiB dist/toolbar/chunk-chunk-Q5QJDMUL.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

✅ Dist folder size — 🔺 +7.7 KiB (+0.0%)

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

Total: 998.62 MiB · 🔺 +7.7 KiB (+0.0%)

ℹ️ MCP UI apps size — 32 app(s), 17182.9 KB JS

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

App JS CSS
debug 597.9 KB 202.7 KB
action 454.2 KB 202.7 KB
action-list 564.3 KB 202.7 KB
cohort 453.2 KB 202.7 KB
cohort-list 563.2 KB 202.7 KB
email-template 453.0 KB 202.7 KB
error-details 469.7 KB 202.7 KB
error-issue 454.5 KB 202.7 KB
error-issue-list 564.9 KB 202.7 KB
experiment 561.4 KB 202.7 KB
experiment-list 565.0 KB 202.7 KB
experiment-results 566.4 KB 202.7 KB
feature-flag 566.8 KB 202.7 KB
feature-flag-list 570.6 KB 202.7 KB
feature-flag-testing 457.3 KB 202.7 KB
inline-scan 453.6 KB 202.7 KB
insight-actors 562.4 KB 202.7 KB
llm-costs 559.4 KB 202.7 KB
session-recording 455.4 KB 202.7 KB
survey 454.8 KB 202.7 KB
survey-global-stats 561.9 KB 202.7 KB
survey-list 565.0 KB 202.7 KB
survey-stats 561.9 KB 202.7 KB
trace-span 453.5 KB 202.7 KB
trace-span-list 564.2 KB 202.7 KB
vision-observation-list 563.3 KB 202.7 KB
workflow 453.5 KB 202.7 KB
workflow-list 563.6 KB 202.7 KB
loops-review 457.9 KB 202.7 KB
query-results 774.3 KB 202.7 KB
render-ui 857.7 KB 202.7 KB
visual-review-snapshots 458.0 KB 202.7 KB
✅ Playwright — all passed

All tests passed.

View test results →

✅ ClickHouse migration SQL — none

No ClickHouse migrations in the latest push.

✅ Hogbox preview — ready, open the preview

▶ 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 12f5841 · box box-c48b6aa63329 · ready in 735s (push → usable) · build log · rebuilds on every push, torn down on close

✅ Hobby preview — passed

Hobby deployment smoke test passed successfully.


Run 37408797445

@rafaeelaudibert
rafaeelaudibert requested review from a team, MattBro and fercgomes and removed request for a team September 25, 2026 04:32
@greptile-apps

greptile-apps Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[High risk] Adds a new background job queue and task processing system.

The PR should not merge until the judge can distinguish eligible insights without exposing sensitive names.

Reviews (3) · Last reviewed commit: "fix(growth): mask insight names and keep..."

@trunk-io

trunk-io Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

Failed Test Failure Summary Logs
compareTopLevelSections() reports a modifiers change when the current query overrides the team default A TypeError occurred because the code attempted to access the 'add' property of an undefined object. Logs ↗︎

View Full Report ↗︎ ⋅ Docs

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 8b672de9-2847-4a3f-b512-7bf1f82082ca

📥 Commits

Reviewing files that changed from the base of the PR and between 742e8b8 and ec3c45b.

⛔ Files ignored due to path filters (3)
  • products/posthog_ai/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/posthog_ai/frontend/generated/api.ts is excluded by !**/generated/**
  • products/posthog_ai/frontend/generated/api.zod.ts is excluded by !**/generated/**
📒 Files selected for processing (22)
  • bin/celery-queues.env
  • posthog/celery_queues.py
  • posthog/egress/typesafe/README.md
  • products/posthog_ai/backend/api/__init__.py
  • products/posthog_ai/backend/api/turn_suggestions.py
  • products/posthog_ai/backend/receivers.py
  • products/posthog_ai/backend/routes.py
  • products/posthog_ai/backend/tasks.py
  • products/posthog_ai/backend/tests/test_turn_suggestions.py
  • products/posthog_ai/backend/tests/test_turn_suggestions_api.py
  • products/posthog_ai/backend/turn_suggestions/__init__.py
  • products/posthog_ai/backend/turn_suggestions/classifier.py
  • products/posthog_ai/backend/turn_suggestions/dispatch.py
  • products/posthog_ai/backend/turn_suggestions/drafter.py
  • products/posthog_ai/backend/turn_suggestions/judgment.py
  • products/posthog_ai/backend/turn_suggestions/offer_ledger.py
  • products/posthog_ai/backend/turn_suggestions/service.py
  • products/posthog_ai/backend/turn_suggestions/transcript.py
  • products/posthog_ai/backend/turn_suggestions/verdict.py
  • products/posthog_ai/mcp/tools.yaml
  • services/mcp/src/api/generated.ts
  • tach.toml
💤 Files with no reviewable changes (1)
  • products/posthog_ai/backend/turn_suggestions/init.py

Included review availability: Your plan provides up to 12 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The change adds PostHog AI turn suggestions. It extracts and redacts conversation transcripts, judges turns, selects and drafts offers, and records and publishes suggestions. Completed task runs can trigger delayed generation through a new Celery queue. Task-scoped API endpoints return suggestion state and record resolutions. MCP tool definitions, generated API types, and tests are also added.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to ec3c4

This adds turn suggestion processing for PostHog AI behind a flag that is off by default. Earlier concerns about a crash on large turns and a type-check failure appear to be addressed, but neither fix has been confirmed at the current head. Before enabling the feature, confirm that CI passes, that the posthog_ai worker runs in each region, and that the TypeSafe fallback does not send customer text.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to ec3c4

Access checks and consent checks are present, but an interrupted background operation can leave a suggestion recorded without displaying it. The new endpoints and worker path also depend on access enforcement and regional deployment conditions that are not fully verified here.

Retained concerns

  • Medium · reliability · inferred: An interruption after recording an offer but before publishing its frame can leave a durable offer that the conversation cannot replay. The normal publish-failure withdrawal does not cover interruption, and the recorded offer affects subsequent suggestion eligibility.
Security review details

Security Blast Radius

  • inferred — A request supplies a task ID, but observed state access is constrained by the route's team context and task-visibility check; resolution additionally requires control visibility. No demonstrated cross-team mutation path was found. Independently, approved conversation content can reach external AI processing during generation.

Security Findings and Attack Paths

  • inferred — No verified Security finding or established attacker path is present. The deferred documentation candidate identifies a TypeSafe caller entry, not evidence that this change exposes sensitive data; inherited route authentication and organization binding remain unverified.

Trust Boundaries and Controls

  • observed — State reads check task visibility before accessing the ledger; resolution checks the narrower control predicate before mutation. The underlying state mutation is team-scoped, but the user-authorization check and mutation are separate operations.

Resilience and Maintainability Implications

  • inferred — The row lock limits duplicate and conflicting ledger transitions, but it does not make ledger recording and stream publication atomic. That gap can consume suggestion capacity without delivering the corresponding card.

Hardening Proposals

  • proposed — Make recorded-but-unpublished offers recoverable or reconcilable after worker interruption, and verify route organization binding and regional worker availability before enabling the feature.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the user problem, behavior, safeguards, tests, release status, and agent involvement. It is mostly complete, but it uses checkboxes for release status, omits the requi…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
products/posthog_ai/backend/turn_suggestions/judgment.py-205-214 (1)

205-214: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cap the insight and issue options. Otherwise a large turn crashes the task.

_issue_options and _insight_options number every ref in the transcript. _error_issues in transcript.py collects every row from every query-error-tracking-issues-list result in the turn. For example, a turn with a few 100-row list calls can yield more than 254 distinct issues. With the added none option, ChoiceQuestion.__post_init__ then raises ValueError because the count exceeds MAX_CHOICE_OPTIONS.

build_judge_questions runs inside the try in judge_turn, but the except clauses catch only the TypeSafe errors and OSError. The ValueError therefore propagates out of generate_turn_suggestion_task. claim_turn has already run at that point, so the turn is consumed without a recorded outcome or analytics event.

Cap the refs in one place. build_judge_state and read_judgment use the same helpers, so the state, the questions and the answer lookup stay in sync.

Proposed fix
+# One option stays free for `_NO_MATCH`; see MAX_CHOICE_OPTIONS in posthog.egress.typesafe.
+_MAX_REF_OPTIONS = 50
+
 def _numbered[T](prefix: str, refs: Sequence[T]) -> dict[str, T]:
-    return {f"{prefix}_{index}": ref for index, ref in enumerate(refs, start=1)}
+    return {f"{prefix}_{index}": ref for index, ref in enumerate(refs[:_MAX_REF_OPTIONS], start=1)}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 3f5b4fd9-99ea-4812-8e43-0c9d4db24213

📥 Commits

Reviewing files that changed from the base of the PR and between ec508ef and b01e9e0.

⛔ Files ignored due to path filters (3)
  • products/posthog_ai/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/posthog_ai/frontend/generated/api.ts is excluded by !**/generated/**
  • products/posthog_ai/frontend/generated/api.zod.ts is excluded by !**/generated/**
📒 Files selected for processing (22)
  • bin/celery-queues.env
  • posthog/celery_queues.py
  • posthog/egress/typesafe/README.md
  • products/posthog_ai/backend/api/__init__.py
  • products/posthog_ai/backend/api/turn_suggestions.py
  • products/posthog_ai/backend/receivers.py
  • products/posthog_ai/backend/routes.py
  • products/posthog_ai/backend/tasks.py
  • products/posthog_ai/backend/tests/test_turn_suggestions.py
  • products/posthog_ai/backend/tests/test_turn_suggestions_api.py
  • products/posthog_ai/backend/turn_suggestions/__init__.py
  • products/posthog_ai/backend/turn_suggestions/classifier.py
  • products/posthog_ai/backend/turn_suggestions/dispatch.py
  • products/posthog_ai/backend/turn_suggestions/drafter.py
  • products/posthog_ai/backend/turn_suggestions/judgment.py
  • products/posthog_ai/backend/turn_suggestions/offer_ledger.py
  • products/posthog_ai/backend/turn_suggestions/service.py
  • products/posthog_ai/backend/turn_suggestions/transcript.py
  • products/posthog_ai/backend/turn_suggestions/verdict.py
  • products/posthog_ai/mcp/tools.yaml
  • services/mcp/src/api/generated.ts
  • tach.toml

Included review availability: Your plan provides up to 12 included reviews per hour; 5 remain after this review.

Comment thread products/posthog_ai/backend/routes.py Resolved
@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/ts-5-turn-suggestions-backend branch from b01e9e0 to 24076a6 Compare September 25, 2026 05:20
@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/ts-5-turn-suggestions-backend branch from 24076a6 to bae2470 Compare September 25, 2026 12:56
@rafaeelaudibert

Copy link
Copy Markdown
Member Author

@coderabbitai review-body comment:

  • Cap the insight and issue options: fixed by bae2470

@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
git show --stat --oneline bae2470ae09
git show --format=fuller --no-ext-diff bae2470ae09 -- products/posthog_ai/backend/turn_suggestions/judgment.py products/posthog_ai/backend/turn_suggestions/transcript.py
ast-grep outline products/posthog_ai/backend/turn_suggestions/judgment.py --match 'build_judge_state|build_judge_questions|read_judgment'
rg -n -C 4 'saved_insights|error_issues|MAX_.*OPTIONS|insight_options|issue_options' products/posthog_ai/backend/turn_suggestions/judgment.py products/posthog_ai/backend/turn_suggestions/transcript.py

Length of output: 23649


@rafaeelaudibert The commit caps insight and issue options at 20 each. The judge state, choice questions, and answer lookup use the same capped list. No further change is needed for this finding.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
products/posthog_ai/backend/turn_suggestions/transcript.py-254-262 (1)

254-262: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assemble streamed user_message_chunk frames before starting a turn.

ACP permits one user message to arrive as multiple user_message_chunk updates with the same messageId. The backend currently calls start_turn for every chunk, and exact-text deduplication cannot match a full _posthog/user_message against an individual chunk. The frontend parser also emits one user item per chunk without assembling messageId groups.

Accumulate chunks by messageId before calling start_turn, then apply the existing deduplication to the assembled message.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 4aef9b4b-d343-4e6f-b973-1d6b14722fc4

📥 Commits

Reviewing files that changed from the base of the PR and between b01e9e0 and 4bfafe9.

⛔ Files ignored due to path filters (3)
  • products/posthog_ai/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/posthog_ai/frontend/generated/api.ts is excluded by !**/generated/**
  • products/posthog_ai/frontend/generated/api.zod.ts is excluded by !**/generated/**
📒 Files selected for processing (22)
  • bin/celery-queues.env
  • posthog/celery_queues.py
  • posthog/egress/typesafe/README.md
  • products/posthog_ai/backend/api/__init__.py
  • products/posthog_ai/backend/api/turn_suggestions.py
  • products/posthog_ai/backend/receivers.py
  • products/posthog_ai/backend/routes.py
  • products/posthog_ai/backend/tasks.py
  • products/posthog_ai/backend/tests/test_turn_suggestions.py
  • products/posthog_ai/backend/tests/test_turn_suggestions_api.py
  • products/posthog_ai/backend/turn_suggestions/__init__.py
  • products/posthog_ai/backend/turn_suggestions/classifier.py
  • products/posthog_ai/backend/turn_suggestions/dispatch.py
  • products/posthog_ai/backend/turn_suggestions/drafter.py
  • products/posthog_ai/backend/turn_suggestions/judgment.py
  • products/posthog_ai/backend/turn_suggestions/offer_ledger.py
  • products/posthog_ai/backend/turn_suggestions/service.py
  • products/posthog_ai/backend/turn_suggestions/transcript.py
  • products/posthog_ai/backend/turn_suggestions/verdict.py
  • products/posthog_ai/mcp/tools.yaml
  • services/mcp/src/api/generated.ts
  • tach.toml
💤 Files with no reviewable changes (1)
  • products/posthog_ai/backend/turn_suggestions/init.py

Included review availability: Your plan provides up to 12 included reviews per hour; 4 remain after this review.

@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/ts-5-turn-suggestions-backend branch from 4bfafe9 to 662d586 Compare September 25, 2026 13:43
@rafaeelaudibert
rafaeelaudibert removed this pull request from stack #106431 September 25, 2026 13:45
@rafaeelaudibert
rafaeelaudibert changed the base branch from rafa/ts-4-scout-creation-check to rafa/ts-4b-system-one-client September 25, 2026 13:45
@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/ts-5-turn-suggestions-backend branch from 662d586 to db6d16b Compare September 25, 2026 13:54
@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/ts-5-turn-suggestions-backend branch from db6d16b to 742e8b8 Compare September 25, 2026 14:24
@rafaeelaudibert

Copy link
Copy Markdown
Member Author

@coderabbitai review-body comment:

  • Assemble streamed user_message_chunk frames before starting a turn: not changing it. The thread renders one human message per chunk too, and the turn index has to match the thread's count. Assembling chunks on one side only would move every later card onto the wrong turn.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 49f1298d-483b-4aa2-b1b7-9e9c72370935

📥 Commits

Reviewing files that changed from the base of the PR and between 4bfafe9 and 742e8b8.

⛔ Files ignored due to path filters (3)
  • products/posthog_ai/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/posthog_ai/frontend/generated/api.ts is excluded by !**/generated/**
  • products/posthog_ai/frontend/generated/api.zod.ts is excluded by !**/generated/**
📒 Files selected for processing (22)
  • bin/celery-queues.env
  • posthog/celery_queues.py
  • posthog/egress/typesafe/README.md
  • products/posthog_ai/backend/api/__init__.py
  • products/posthog_ai/backend/api/turn_suggestions.py
  • products/posthog_ai/backend/receivers.py
  • products/posthog_ai/backend/routes.py
  • products/posthog_ai/backend/tasks.py
  • products/posthog_ai/backend/tests/test_turn_suggestions.py
  • products/posthog_ai/backend/tests/test_turn_suggestions_api.py
  • products/posthog_ai/backend/turn_suggestions/__init__.py
  • products/posthog_ai/backend/turn_suggestions/classifier.py
  • products/posthog_ai/backend/turn_suggestions/dispatch.py
  • products/posthog_ai/backend/turn_suggestions/drafter.py
  • products/posthog_ai/backend/turn_suggestions/judgment.py
  • products/posthog_ai/backend/turn_suggestions/offer_ledger.py
  • products/posthog_ai/backend/turn_suggestions/service.py
  • products/posthog_ai/backend/turn_suggestions/transcript.py
  • products/posthog_ai/backend/turn_suggestions/verdict.py
  • products/posthog_ai/mcp/tools.yaml
  • services/mcp/src/api/generated.ts
  • tach.toml
💤 Files with no reviewable changes (1)
  • products/posthog_ai/backend/turn_suggestions/init.py

Included review availability: Your plan provides up to 12 included reviews per hour; 6 remain after this review.

Comment thread posthog/egress/typesafe/README.md Resolved
rafaeelaudibert and others added 11 commits October 6, 2026 04:20
Classify each completed PostHog AI turn and publish an end-of-turn
suggestion frame. The task runs on a new posthog_ai Celery queue, a
short Redis lock drops duplicate turn reports before they are queued,
and the offer ledger in the task state is the only stored outcome of a
card: a reload reads it from the new state endpoint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…blishing

- read insight and issue refs from the MCP app-data envelope, and resolve
  tool identity from the adapter's _meta.posthog stamp
- note a follow-up that is still running instead of claiming its turn
- skip conversations past the resume chain depth, where turn indexes slide
- count a card only when it reached the run log
- cap the insight and issue options Jev sees
- release the enqueue dedup key when queueing fails, and keep it only for
  the settle wait
- send the Luna draft cap as max_completion_tokens

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…g's cost

The Luna drafter now uses build_openai_client, so it reaches the Go
ai-gateway where AI_GATEWAY_URL is set and bills the wallet of the team
that owns that key. The Python fallback moves from the posthog_ai route,
which bills the customer's AI credits, to the unbilled growth route.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The judge now asks posthog/hogference/jevk5-fp8-0.2 through the
ai-gateway where it is configured, and jev-1.13.0 through TypeSafe
elsewhere. Ref options cap at 15, so a choice fits the gateway model's 16
options. The classified event records the model the configured server
was asked for.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Master now blocks TypeSafe on PostHog Cloud and its README says a Cloud
feature must not pass a TypeSafeFallback. The judge state carries the
user's question, so the judge drops the jev-1.13.0 TypeSafe fallback and
asks only the PostHog-hosted Jev build on the ai-gateway.

Team attribution uses the new team_id argument of build_system_one_client
instead of setting the distinct id and team_id property by hand. A
TypeSafe key on its own no longer configures the judge, and the failure
test checks that no request leaves in that case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion cap

A failed enqueue now releases only its own dedup reservation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- skip a latest turn that has no turn_complete frame yet, so a follow-up
  still streaming is noted instead of judged and claimed
- expire the celery job shortly after the settle wait, so a stalled
  posthog_ai worker drains no backlog of late cards
- count and log every job outcome, and warn when the judge is not configured
- mark the insight or issue the answer names, so masked labels that read
  the same stay distinguishable for jev

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Oct 6, 2026

Copy link
Copy Markdown

This pull request is queued for merge as part of 106435, which will merge 106432, 106433, 106434, 106435.

@trunk-io

trunk-io Bot commented Oct 6, 2026

Copy link
Copy Markdown

Stacked PR 106435 failed testing in the merge queue. Please investigate the failure and re-submit the stack.

@trunk-io

trunk-io Bot commented Oct 6, 2026

Copy link
Copy Markdown

This pull request is queued for merge as part of 106435, which will merge 106432, 106433, 106434, 106435.

@trunk-io
trunk-io Bot merged commit c55efaa into master Oct 6, 2026
@trunk-io
trunk-io Bot deleted the rafa/ts-5-turn-suggestions-backend branch October 6, 2026 09:23
@trunk-io

trunk-io Bot commented Oct 6, 2026

Copy link
Copy Markdown

This pull request was merged into master as part of stacked PR 106435.

@deployment-status-posthog

deployment-status-posthog Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-10-06 09:50 UTC Run
prod-us ✅ Deployed 2026-10-06 10:13 UTC Run
prod-eu ✅ Deployed 2026-10-06 10:14 UTC Run

This branch was successfully deployed

1 active deployment
preview-pr-106432 — 12f58417 Deployed Oct 6, 2026 by github-actions[bot]
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