Skip to content

feat(growth): turn suggestion backend - #106432

Draft
rafaeelaudibert wants to merge 26 commits into
masterfrom
rafa/ts-5-turn-suggestions-backend
Draft

rafaeelaudibert wants to merge 26 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. 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: the ai-gateway answers with posthog/hogference/jevk5-fp8-0.2 where it is configured, through the client from feat(llm): add a system one client that prefers the ai-gateway #106735. The judge opts into the TypeSafe fallback, so TypeSafe answers with jev-1.13.0 elsewhere.
  • Thresholds: the show threshold is tuned on jev-1.13.0. An earlier benchmark run of the gateway model at the same threshold is in chore(growth): turn suggestion judge benchmark #106433.
  • 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 System One server (AI_GATEWAY_URL with AI_GATEWAY_API_KEY, or TYPESAFE_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.
  • Duplicate reports: three reporters can report the same turn. A five-second Redis SET NX 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.

🤖 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: 68.82 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.57 MiB · 22 files no change █████████░ 85.1% 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.57 MiB · 628 files no change █████████░ 88.6% of 4.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.27 MiB · 2,301 files no change █████████░ 87.2% of 8.34 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/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
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts 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
🟢 [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/index.tsx
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
854 B src/scenes/ChunkLoadErrorBoundary.tsx
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
301.8 KiB ../node_modules/.pnpm/posthog-js@1.434.13_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
267.6 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
100.4 KiB src/lib/api.ts
84.9 KiB src/products.tsx
69.1 KiB src/lib/lemon-ui/icons/icons.tsx
63.9 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
28.3 KiB src/scenes/scenes.ts
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
301.8 KiB ../node_modules/.pnpm/posthog-js@1.434.13_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
271.7 KiB src/taxonomy/core-filter-definitions-by-group.json
267.6 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
100.4 KiB src/lib/api.ts
98.5 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
84.9 KiB src/products.tsx

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.37 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.37 MiB · 19 files no change ████░░░░░░ 41.4% of 5.72 MiB
Deferred (lazy) 2.10 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
790.2 KiB dist/toolbar/toolbar-app-ZNXS4H6W.css
650.4 KiB dist/toolbar/chunk-chunk-DPTOFHTZ.js
483.6 KiB dist/toolbar/chunk-chunk-DPLA2GAY.js
138.3 KiB dist/toolbar/chunk-chunk-2EHPTPPA.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
75.2 KiB dist/toolbar/toolbar-app-FOEGQ3V3.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-U5CQIMDB.js
21.0 KiB dist/toolbar/chunk-chunk-OYNHVBHM.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: 943.53 MiB · 🔺 +7.7 KiB (+0.0%)

ℹ️ MCP UI apps size — 33 app(s), 17628.2 KB JS

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

App JS CSS
debug 597.8 KB 196.2 KB
action 454.1 KB 196.2 KB
action-list 564.1 KB 196.2 KB
cohort 453.1 KB 196.2 KB
cohort-list 563.1 KB 196.2 KB
email-template 452.9 KB 196.2 KB
error-details 469.1 KB 196.2 KB
error-issue 453.8 KB 196.2 KB
error-issue-list 564.0 KB 196.2 KB
experiment 561.2 KB 196.2 KB
experiment-list 564.9 KB 196.2 KB
experiment-results 566.2 KB 196.2 KB
feature-flag 566.7 KB 196.2 KB
feature-flag-list 570.5 KB 196.2 KB
feature-flag-testing 457.3 KB 196.2 KB
inline-scan 453.6 KB 196.2 KB
insight-actors 562.3 KB 196.2 KB
invite-email-preview 452.3 KB 196.2 KB
llm-costs 559.3 KB 196.2 KB
session-recording 454.9 KB 196.2 KB
survey 454.7 KB 196.2 KB
survey-global-stats 561.8 KB 196.2 KB
survey-list 564.8 KB 196.2 KB
survey-stats 561.8 KB 196.2 KB
trace-span 453.5 KB 196.2 KB
trace-span-list 564.0 KB 196.2 KB
vision-observation-list 563.2 KB 196.2 KB
workflow 453.4 KB 196.2 KB
workflow-list 563.5 KB 196.2 KB
loops-review 457.8 KB 196.2 KB
query-results 774.0 KB 196.2 KB
render-ui 856.5 KB 196.2 KB
visual-review-snapshots 457.9 KB 196.2 KB
✅ Hobby preview — passed

Hobby deployment smoke test passed successfully.


Run 36153201920

@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 is not safe to merge: Luna drafts fail, several valid turns cannot produce cards, and the judge sends customer-project text across an unapproved data boundary.

Reviews (1) · Last reviewed commit: "feat(growth): turn suggestion backend"

Comment thread products/posthog_ai/backend/turn_suggestions/judgment.py
Comment thread products/posthog_ai/backend/turn_suggestions/drafter.py Outdated
Comment thread products/posthog_ai/backend/turn_suggestions/transcript.py Outdated
Comment thread products/posthog_ai/backend/turn_suggestions/transcript.py
Comment thread products/posthog_ai/backend/turn_suggestions/service.py Outdated
Comment thread products/posthog_ai/backend/turn_suggestions/service.py
Comment thread products/posthog_ai/backend/turn_suggestions/service.py
Comment thread products/posthog_ai/backend/turn_suggestions/dispatch.py Outdated
Comment thread products/posthog_ai/backend/turn_suggestions/dispatch.py
Comment thread products/posthog_ai/backend/turn_suggestions/drafter.py Outdated
@trunk-io

trunk-io Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 is complete and follows the required structure. It explains the problem, user-visible changes, architecture, testing coverage, release status, documentation status, and agent context. …
✨ 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
@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.

Comment thread products/posthog_ai/backend/turn_suggestions/drafter.py Outdated
@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 added this pull request to stack #106737 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.


| Caller | Source | Lane | Feature flag |
| ---------------------------------------------------------------------------------------- | ----------------------------- | ------- | ----------------------------- |
| PostHog AI turn suggestions (`products/posthog_ai/backend/turn_suggestions/judgment.py`) | `posthog_ai_turn_suggestions` | `BATCH` | `posthog-ai-turn-suggestions` |

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.

🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '45,65p' posthog/egress/typesafe/README.md
sed -n '210,340p' products/posthog_ai/backend/turn_suggestions/judgment.py
sed -n '155,215p' products/posthog_ai/backend/turn_suggestions/service.py
rg -n 'def build_system_one_client|class TypeSafeFallback|approved for|customer data' posthog/llm/system_one.py posthog/egress/typesafe/README.md

Repository: PostHog/posthog

Length of output: 11630


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- system_one definitions and callers ---'
rg -n -A70 -B20 'def build_system_one_client|class TypeSafeFallback|TypeSafeFallback|system_one_configured|JUDGE_TYPESAFE_FALLBACK|JUDGE_MODEL|AI_GATEWAY_URL|AI_GATEWAY_API_KEY' posthog/llm products/posthog_ai/backend/turn_suggestions posthog/egress/typesafe
printf '%s\n' '--- TypeSafe policy and configuration ---'
sed -n '1,45p' posthog/egress/typesafe/README.md
rg -n -A25 -B15 'is_ai_data_processing_approved|posthog-ai-turn-suggestions|turn_suggestions_enabled|def _turn_suggestions_enabled|generate_turn_suggestion' products/posthog_ai/backend/turn_suggestions
printf '%s\n' '--- PR diff for relevant changed files ---'
git diff --unified=25 71a423bdd4e9b3315fa1953f1e63c18b254590db 742e8b8e7e2f794a545ce5ddf76fdeccb76f4af2 -- products/posthog_ai/backend/turn_suggestions/judgment.py products/posthog_ai/backend/turn_suggestions/service.py posthog/llm posthog/egress/typesafe/README.md

Repository: PostHog/posthog

Length of output: 43305


Sensitive Data Exposure

Reachability: External
Exploitability: Difficult
CWE: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor

Sensitive Data Exposure

Reachability: External
Exploitability: Difficult
CWE: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor

Exclude customer turn text from the TypeSafe fallback. The judgment state includes raw user questions, and the TypeSafe fallback sends that state to TypeSafe when the gateway is unavailable. The general is_ai_data_processing_approved check and feature flag do not establish the TypeSafe-specific customer opt-in and leadership sign-off required by the TypeSafe policy. Keep customer text out of the TypeSafe request, or add the required authorization before enabling this fallback.

View in Security blast radius

rafaeelaudibert and others added 26 commits September 25, 2026 12:12
The not-configured banner opened Slack's OAuth in the same tab, so any
form the user was filling in was lost. It now opens a new tab, polls
integrations until the new workspace appears, and hands it to the
caller through onConnected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A workspace another project member adds while the banner waits is no
longer picked up, and a click before the first integrations load takes
the next successful load as the baseline instead of an empty list.

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

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

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

The three turn-completed reporters share one fan-out that sends a
task_run_turn_completed signal, so other products react to a finished
turn without the tasks product importing them. The facade gains a
single-lookup conversation history read, task state helpers on a new
Task.mutate_state_atomic, and a server notification publish that goes
through TaskRun so it gets presence gating and the ACP envelope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An unstamped stream at the length cap may be a trimmed tail, so the
history read still loads the run's log for it. A persisted server
notification sits in both stores without an event id, and the merge
now keeps one copy. Task.mutate_state_atomic hands the mutator a deep
copy, so an in-place edit of a nested value is saved.

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

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
parse_exec_command, normalize_tool_name and INFO_SYNTHETIC_PREFIX move to
products/posthog_ai/backend/exec_commands.py so production code can read
single-exec tool calls without importing eval tooling. The eval harness
and scorers import them from the new module.

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

can_create_scout holds the create endpoint's skill editor check, and the new
scout_creation_available facade read uses it after the enrollment check, so
a caller can decide whether to offer scout creation with the same rule the
endpoint enforces.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scout_creation_available takes a team id and a user id and resolves the
models inside Signals, so no ORM instance crosses the facade.

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

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
build_system_one_client returns a client for the Go ai-gateway's
/v1/systemone route where AI_GATEWAY_URL is set, and for TypeSafe
elsewhere. Callers name one model per server, because the two serve
different models.

The System One request and answer types move out of the TypeSafe egress
domain into posthog/llm/system_one.py, so both servers share them.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
@rafaeelaudibert
rafaeelaudibert force-pushed the rafa/ts-5-turn-suggestions-backend branch from 742e8b8 to ec3c45b Compare September 25, 2026 15:17
Base automatically changed from rafa/ts-4b-system-one-client to master September 25, 2026 17:39
@trunk-io

trunk-io Bot commented Sep 25, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

This branch has not been deployed

No deployments
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.

1 participant