diff --git a/.context/app/planning/development-plan.md b/.context/app/planning/development-plan.md index 8f6637e3..51f1c6cf 100644 --- a/.context/app/planning/development-plan.md +++ b/.context/app/planning/development-plan.md @@ -21,14 +21,14 @@ supersedes: Conversational Questionnaire Phases.md ## Project -| Field | Value | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Name | Conversational Questionnaire | -| Repo | `human-centric-engineering/conquest` (forked from `human-centric-engineering/sunrise` at v0.0.1) | -| Host platform | Sunrise v0.0.1 | -| Lead | Simon Holmes | -| Status | `building` — P0 done (F0.1, PR #10); P1 done (F1.1 ingestion: PR #13/#14/#15); P2.5 done (F2.5.1, #21); P2 done (F2.1 #18/#19, F2.2 #23, F2.3 #24, F2.4 #25); P3 done (F3.1 #26, F3.2 #27, F3.3 #28, F3.4 #29); P4 done (F4.1 #30, F4.2 #32, F4.3 #33, F4.4 #34, F4.5 #35, F4.6 #36 merged); P5 done (F5.1 judge agents #37; F5.2 evaluation run persistence #38; F5.3 suggestion review #39); P6 in progress (F6.1 per-turn orchestrator + streaming done on `feat/f6.1-per-turn-orchestrator` — 6 PRs; F6.2 voice input done on `feat/f6.2-voice-input`) | -| Opened | 2026-05-30 | +| Field | Value | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Name | Conversational Questionnaire | +| Repo | `human-centric-engineering/conquest` (forked from `human-centric-engineering/sunrise` at v0.0.1) | +| Host platform | Sunrise v0.0.1 | +| Lead | Simon Holmes | +| Status | `building` — P0 done (F0.1, PR #10); P1 done (F1.1 ingestion: PR #13/#14/#15); P2.5 done (F2.5.1, #21); P2 done (F2.1 #18/#19, F2.2 #23, F2.3 #24, F2.4 #25); P3 done (F3.1 #26, F3.2 #27, F3.3 #28, F3.4 #29); P4 done (F4.1 #30, F4.2 #32, F4.3 #33, F4.4 #34, F4.5 #35, F4.6 #36); P5 done (F5.1 #37, F5.2 #38, F5.3 #39); P6 done (F6.1 #40, F6.2 #41, F6.3 #42, F6.4 #43); P7 done (F7.1 #44, F7.2 #45, F7.3 #46, F7.4 #47) + admin upload-questionnaire UI (#48). **Next: P8** (analytics, exports, anonymous-mode hardening), then P9. A 2026-06-07 audit found five deferred items whose home feature shipped but which were never delivered — being closed on `feat/close-deferred-gaps` (see decisions log). | +| Opened | 2026-05-30 | --- @@ -142,8 +142,8 @@ The build moves from scaffolding → ingestion → admin manage → demo brandin | **P3** | Configuration, invitations, and cost estimation | done | Per-version config; invitation flow; pre-launch cost estimate. All four features merged: F3.1 config (#26); F3.2 invitation flow (#27); F3.3 cost estimation (#28); F3.4 demo-client invitation branding (#29). | | **P4** | Conversational engine (non-streaming) | in flight | Selection · extraction · contradiction · completion logic, exercised without the streaming surface. F4.1 selection strategies (#30) + F4.2 answer extraction (#32) + F4.3 contradiction detection (#33) + F4.4 answer refinement (#34) + F4.5 completion logic (#35) merged; F4.6 session state machine in flight. | | **P5** | Design-time evaluation (agents-as-judges) | done | Judges score a questionnaire against goal/audience; suggestion review queue. F5.1 judge agents done (app-native dispatch — `evaluate-structure` capability + 7 `kind='judge'` seeds + preview route); F5.2 run persistence done (synchronous run route + app-owned run/finding models + admin run history); F5.3 review queue done (structured `proposedEdit` ops on the findings contract + apply-through-fork engine + read-time staleness + interactive queue UI). | -| **P6** | Conversational session (streaming) | not started | Per-turn orchestrator over streaming chat; voice + attachments. | -| **P7** | User-facing conversational UI | not started | Split-screen chat + answer-slot panel; polish; PDF export. | +| **P6** | Conversational session (streaming) | done | F6.1 per-turn orchestrator + streaming (#40); F6.2 voice input (#41); F6.3 cost-cap enforcement (#42); F6.4 demo session reset (#43). _Attachment input closed by the 2026-06-07 audit gap-fill (Item 5)._ | +| **P7** | User-facing conversational UI | done | F7.1 chat surface (#44); F7.2 answer-slot panel (#45); F7.3 session lifecycle UX (#46); F7.4 PDF export (#47); admin upload-questionnaire UI (#48). _Attachment input + invite claim-via-login closed by the 2026-06-07 audit gap-fills._ | | **P8** | Admin analytics, exports, anonymous mode | not started | Dashboards, CSV/JSON export, anonymous-mode handling. | | **P9** | Hardening + forking docs | not started | Runbook, flag inventory, `forking.md`, concurrent-session sanity. | @@ -306,7 +306,7 @@ The rest of what was P2.5 lands where its dependency is built. Every piece stays | Tenant-scoped user routing (resolve tenant from URL / invite token) | Needs user-facing routes + invite tokens | **F7.1** (P7) | | Theming application points (landing / completion pages) | Needs P7 user pages | **F7.1 / F7.3** (P7) | | Demo session reset (`reset-sessions`) | Needs the full session graph | **F6.4** (P6) | -| Clone-for-client admin utility | Needs tags (F2.2) + config (F3.1) | **P3+**, promote once both exist | +| Clone-for-client admin utility | Needs tags (F2.2) + config (F3.1) | **done 2026-06-07** (deferred-gaps audit, Item 4) | | Demo content seed (`010-demo-content.ts`) | Needs a complete themeable vertical | **F9.4** (P9) | --- @@ -798,6 +798,8 @@ Claude reads this doc for intent and the original [[Conversational Questionnaire Append-only. Newest at the top. Each entry: date, decision, context, link. +- **2026-06-07 — Deferred-item audit + gap-fills (`feat/close-deferred-gaps`).** An audit against the merged code (PRs #10–#48) found the plan header was stale (it said "P6 in progress / P7 not started" when both phases were fully merged) and surfaced **five items that were deferred to a home feature which has since shipped, but were never actually delivered** — the same shape as the admin upload-questionnaire UI (#48), which had fallen between F1.1 (API-only) and the P2/P7 UI work with no owning feature. All five verified against the code, not the trackers. **(1) Invite claim-via-existing-login** — F3.2 returns `409 ACCOUNT_EXISTS`; claim-via-login was deferred to P7 (done) and never built. **(2) Live `refinementHistory` append** — F4.4 built the model; F6.1 deferred wiring `persistRefinement` into the live turn loop (only the F4.4 preview route appends), so real sessions silently drop the history trail P8 will read. **(3) Clone-for-client admin utility** — parked at "promote once F2.2 + F3.1 exist" (done since P3); never built. **(4) Attachment input** — an F7.1 indicative task that shipped out-of-scope, blocked on an F4.2 content-parts extension that was itself deferred in F6.1; voice was wired (F6.2/F7.1) but attachments never were. **(5) `every_n_turns` contradiction cadence** — a cost knob deferred to "F4.6's real loop" (F6.1); detection still runs every turn. **Also delivered with this branch: the demo-client branding preview** — the admin sets four theme fields (CTA/accent colour, logo, welcome copy) but the admin UI never showed them back; a new `DemoClientThemePreview` (reuses `resolveTheme()` + the escaped `--app-logo-url` background) now renders swatches/logo on the demo-clients list, detail page, and live in the edit form. The five gap-fills land as one-PR-each commits on the branch, ordered by priority (correctness 1–2 before P8). Each gets a `planning/features/fX.Y.md` tracker as it ships. (These are app-internal deferrals, not Sunrise-platform gaps, so they're tracked here in the decisions log — not in `upstream-gaps.md`, which is platform-scoped.) + - **2026-06-06 — F7.3 session lifecycle UX (on `feat/F7.3-session-lifecycle-ux`).** The respondent's window onto the session lifecycle: pause/resume, the completion-offer → submit flow, the cost-cap state, and the anonymous-mode indicator. 2 PRs. **The gap it closed:** a live respondent session had **no path to `completed`** — the orchestrator streamed a completion offer ("Would you like to submit?") as prose but nothing recorded acceptance (the only `completed` transition was the admin-only `/complete` preview route); F7.3's respondent `submit` route is now that path. **Decisions (confirmed with the user):** **offer signal = a `GET …/status` read, not a stream frame** — the F6.1 SSE stream carries no offer marker, so rather than reopen that contract (the F7.2 precedent) a new status endpoint returns a respondent-safe `SessionStatusView` (completion kind + coverage + a coarse cost **tier**, never raw spend + `anonymous`) and the UI refetches it on the same `onTurnSettled` that drives the answer panel; the Submit affordance shows when `completion.kind === 'offer'`. **Submit gate is upstream** — `assessCompletion` only returns `offer` once the required-questions gate is clear, so the Submit affordance never appears with a required question blank; the submit route just re-asserts the offer state via the F4.5 `resolveCompletion('accept', …, { run: false })` with **no contradiction sweep** (contradictions surface live via F4.3 during chat), the lone exception being the existing "a question-cap-reached session can always submit" behaviour, honoured as-is. **Pause is signed-in only** — a no-login session's token is client-only (lost on reload), so a deliberate pause has nowhere durable to resume from; the `lifecycle` route refuses anonymous callers `403 PAUSE_NOT_PERMITTED` (they still see system states), and `canPause`/`canResume` mirror that in the UI (a budget-paused `hard`-tier session is non-resumable). **Backend (PR1):** pure `session/status-view.ts` (`buildSessionStatusView` + `canSubmitSession`); `_lib/session-status.ts` reuses `buildTurnContext` so the assessment is byte-identical to the live turn's; three routes `GET status` / `POST lifecycle {pause|resume}` / `POST submit` under `questionnaire-sessions/[id]/`, all reusing `resolveTurnAccess`, gate order flag→load→access→action, illegal transition→409; registry entries. **Frontend (PR2):** stream hook gains `completed` (+ `BLOCKING_STATUSES`) and an imperative `applyStatus`; a new `useSessionLifecycle` hook (status view + pause/resume/submit + derived flags) lifted into `SessionWorkspace` alongside the F7.2 stream+panel hooks (the shared `onTurnSettled` now refetches both panel and status); `lifecycle/` components `SessionLifecycleBar` (anon badge, pause/resume, soft-cost hint — renders nothing when empty), `CompletionOffer` (Submit CTA + dismiss), `SessionComplete` (themed confirmation replacing the workspace on `completed`); authed page SSR-seeds the status view and maps it to the initial chat status (budget-pause→`cost_capped`, respondent pause→resumable `not_active`, completed→confirmation), anon boot fetches on mount. **No migration** (every status + event exists from F4.6); gated entirely by the existing `APP_QUESTIONNAIRES_LIVE_SESSIONS_ENABLED`. **Tests:** pure builder unit; the three routes at integration (gate order, both access modes, anon-pause 403, submit idempotency/409s/happy path, illegal-transition→409); `use-session-lifecycle` + `applyStatus` hook tests; lifecycle component tests. `npm run validate` green. **No CHANGELOG entry** (app surface). Tracker: `planning/features/f7.3.md`. Docs: new `session-lifecycle.md` + updated `per-turn-orchestrator.md` / `answer-slot-panel.md`. - **2026-06-06 — F7.2 answer-slot panel (on `feat/F7.2-answer-slot-panel`).** The live panel beside the F7.1 chat showing answer slots as the conversation fills them in — confidence, provenance, refinement history, per-slot Revisit. 2 PRs. **Decisions (confirmed with the user):** **read endpoint, not a stream event** — the F6.1 SSE stream emits only `start/content/warning/done/error` (answers persist server-side before `done`), so rather than extend that contract the panel reads a new `GET /api/v1/app/questionnaire-sessions/:id/answers` and refetches when each turn settles; the route reuses `resolveTurnAccess` (authed owner OR anonymous `X-Session-Token`) so it serves both respondent kinds, gate order flag→load→access, no status gate (paused/completed still show answers), no extra limiter (read). **Panel scope is an admin config setting** — new per-version `answerSlotPanelScope` (`full_progress | answered_only`, default `full_progress`) added via the F3.1 7-step config path; `answered_only` filters in the **pure builder** (`buildAnswerPanelView`) so pending prompts are never sent to the client; `totalCount` still reflects the whole version so the header reads "N captured" honestly. **Click = inline-expand + a confirm-gated Revisit** that re-asks through the same turn loop (a true scroll-to-turn was rejected — it would force touching `QuestionnaireChat` and has a turn-ordinal alignment hazard). **Rationale shown quietly** in the expanded view. **Live wiring:** a new `SessionWorkspace` client parent lifts the single `useQuestionnaireSessionStream` so chat + panel share it — an additive `onTurnSettled` option (fires on a clean settle to `idle`) drives `panel.refetch`, and the shared stream is what lets the panel's Revisit call `stream.sendMessage(...)`; `QuestionnaireChat` was refactored to **take `stream` as a prop** (hook moved up), render otherwise unchanged. **Layout:** both pages render `SessionWorkspace` inside the existing `BrandThemeProvider` (panel inherits brand vars), `lg:grid-cols-[1fr_22rem]`, panel `hidden lg:flex` so `< lg` keeps the full-width F7.1 chat; authed page SSR-seeds the panel (`loadAnswerPanelState`), the anonymous page can't (client-only token) so it skeletons briefly. **Migration** `…_add_config_answer_panel_scope` (one ADD COLUMN, create-only + phantom pgvector DDL stripped, drift 11/11). **Pure core** (`lib/app/questionnaire/panel/{types,answer-panel,confidence}.ts`) Prisma-free; the `_lib/answer-panel.ts` seam does one query incl. a turn-id→ordinal map for `answeredAtTurnIndex`; authoring internals (`weight`, tags, config) never projected. Confidence reads as a quiet semantic dot (high/moderate/low/unscored, the admin eval-chip thresholds), never a raw number. **Tests:** pure builder + confidence bands (unit); the answers route (gate order incl. flag-before-load, both access modes, scope projection, no-leak) at integration; panel/indicator/provenance/history component tests + the `use-answer-panel` hook + the `onTurnSettled` wiring; schema-shape + config-schema extended. `npm run validate` green; 3861 affected tests pass. **No CHANGELOG entry** (app surface). Tracker: `planning/features/f7.2.md`. Docs: `answer-slot-panel.md` + `configuration.md`. diff --git a/.context/app/planning/features/f3.2.md b/.context/app/planning/features/f3.2.md index 2c414f47..ac1d0719 100644 --- a/.context/app/planning/features/f3.2.md +++ b/.context/app/planning/features/f3.2.md @@ -179,5 +179,8 @@ the development-plan F3.2 status to done and add a decisions-log entry. `started`/`completed` transitions (P6/P7 sessions). Demo-client invitation branding + `demoClientId` denormalisation + themed email (**F3.4**). Pre-launch cost estimation -(**F3.3**). Claim-an-invite-via-existing-login (deferred; returns `409 ACCOUNT_EXISTS`). -Anonymous-mode session entry (P6/P7). `questionnaireId` denormalisation on the invitation. +(**F3.3**). ~~Claim-an-invite-via-existing-login (deferred; returns `409 ACCOUNT_EXISTS`).~~ +**Closed 2026-06-07** (deferred-gaps audit, Item 3): an already-registered email now +claims the invitation by signing in (verified via `signInEmail`, `401` on a wrong +password), bound to the existing account; `metadata` reports `accountExists` to drive the +form branch. Anonymous-mode session entry (P6/P7). `questionnaireId` denormalisation on the invitation. diff --git a/.context/app/planning/features/f4.3.md b/.context/app/planning/features/f4.3.md index 67547e3b..714db392 100644 --- a/.context/app/planning/features/f4.3.md +++ b/.context/app/planning/features/f4.3.md @@ -42,6 +42,9 @@ preview route returns findings and writes nothing. windowed by `windowN`) and `phase:'completion-sweep'` (= sweep_only) from day one. `every_n_turns` (a pure cost knob) waits for F4.6's real turn loop, where it's an additive field + a scheduler branch — cheap to add, expensive to guess now. + **Landed 2026-06-07** (deferred-gaps audit, as predicted): the additive + `contradictionEveryNTurns` config column + an optional `cadence` arg on + `shouldRunDetection`, consumed by the F6.1 orchestrator (skips off-boundary turns). - **Surface, never overwrite.** The capability returns `ContradictionFinding[]` (which slots conflict, why, a severity, a `confidence`, and — under `probe` — a `suggestedProbe`). It writes nothing. F4.4 acts on a confirmed conflict; the diff --git a/.context/app/planning/features/f6.1.md b/.context/app/planning/features/f6.1.md index 2829635f..ec0b8dc6 100644 --- a/.context/app/planning/features/f6.1.md +++ b/.context/app/planning/features/f6.1.md @@ -77,10 +77,17 @@ event) and of `AppQuestionnaireTurn` (firing `AppAnswerSlot.lastUpdatedTurnId`). ## Deferred / follow-ups -- **Attachment input** (extraction over an uploaded image/doc) — needs extending the F4.2 - extraction capability to accept content parts; tracked for F6.1 PR8 / a follow-up. -- **Full refinementHistory append** on refine persistence (PR4 persists the corrected value - via the upsert seam; the history append through `persistRefinement` is a follow-up). +- ~~**Attachment input** (extraction over an uploaded image/doc) — needs extending the F4.2 + extraction capability to accept content parts; tracked for F6.1 PR8 / a follow-up.~~ + **Closed 2026-06-07** (deferred-gaps audit, Item 5): the answer-extractor capability accepts + `attachments` (content parts + a model-capability gate), the `/messages` route + orchestrator + thread them, and the chat surface has a flag-gated affordance + (`APP_QUESTIONNAIRES_ATTACHMENT_INPUT_ENABLED`, seed 024). +- ~~**Full refinementHistory append** on refine persistence (PR4 persists the corrected value + via the upsert seam; the history append through `persistRefinement` is a follow-up).~~ + **Closed 2026-06-07** (deferred-gaps audit, Item 2): `persistTurn` now takes the full F4.4 + path (`loadAnswerSlot` → `applyRefinement` → `persistRefinement`), so live sessions append + to `refinementHistory`. - **Aggregate per-turn token usage** on the `done` event (the capabilities log their own `AiCostLog` rows; the turn record carries summed offer cost only). - **Admin-side anonymous redaction** (`anonId`, redacted session views) — a later phase. diff --git a/.context/app/planning/features/f7.1.md b/.context/app/planning/features/f7.1.md index 3a878a1c..0791c9e6 100644 --- a/.context/app/planning/features/f7.1.md +++ b/.context/app/planning/features/f7.1.md @@ -84,7 +84,9 @@ branding, and protect the sales-critical happy path with an end-to-end test. - **F7.3** — session lifecycle UX (pause/resume controls, completion-offer accept flow, explicit cost-cap surfacing beyond the terminal panel). - **F7.4** — PDF export. -- **Attachment input** — blocked on an F4.2 extension. +- ~~**Attachment input** — blocked on an F4.2 extension.~~ **Closed 2026-06-07** (deferred-gaps + audit, Item 5): the F4.2 capability now accepts content parts and the chat composer has a + flag-gated attachment affordance (`APP_QUESTIONNAIRES_ATTACHMENT_INPUT_ENABLED`). - **Authenticated e2e** — needs a login storage-state fixture (`globalSetup`); tracked in the e2e README. ## Tests diff --git a/.context/app/questionnaire/answer-refinement.md b/.context/app/questionnaire/answer-refinement.md index 91f9055b..573189e8 100644 --- a/.context/app/questionnaire/answer-refinement.md +++ b/.context/app/questionnaire/answer-refinement.md @@ -94,8 +94,11 @@ write seam, keeping `lib/app/questionnaire/refinement/**` Prisma-free): answer (the "seed then refine" step), keyed on `@@unique([sessionId, questionSlotId])`. - `loadAnswerSlot(sessionId, questionSlotId)` — shape a row for `applyRefinement`, narrowing the stored `provenanceLabel` to the enum. -- `persistRefinement(rowId, refined)` — write `value`, `provenanceLabel`, and the - extended `refinementHistory`, stamping `createdAt` on any unstamped entry. +- `persistRefinement(rowId, refined)` — write `value`, `provenanceLabel`, `confidence`, + and the extended `refinementHistory`, stamping `createdAt` on any unstamped entry. The + refinement's `confidence` (from the decision) **replaces** the slot's prior score — a + refine can raise or lower it, since improving a low-confidence capture is the point of + refining; `applyRefinement` carries `decision.confidence` onto `RefinedSlotState`. ## Persistence foundation (the F4.6 slice F4.4 introduces) diff --git a/.context/app/questionnaire/configuration.md b/.context/app/questionnaire/configuration.md index 901b3504..4af53477 100644 --- a/.context/app/questionnaire/configuration.md +++ b/.context/app/questionnaire/configuration.md @@ -22,19 +22,20 @@ and stores them; the consumers land later (see _Who consumes it_). like goal/audience and the section graph. One typed column per setting plus a single JSON column for the profile fields: -| Setting | Column | Type | Default | -| -------------------------------- | ------------------------ | ---------------------- | ----------------- | -| Question selection strategy | `selectionStrategy` | String (enum) | `'sequential'` | -| Completion: min questions | `minQuestionsAnswered` | Int | `0` | -| Completion: coverage threshold | `coverageThreshold` | Float (0–1) | `1.0` | -| Cost budget (USD / session) | `costBudgetUsd` | Float? (null = no cap) | `null` | -| Per-session question cap | `maxQuestionsPerSession` | Int? (null = no cap) | `null` | -| Voice input | `voiceEnabled` | Boolean | `false` | -| Contradiction-detection mode | `contradictionMode` | String (enum) | `'off'` | -| Contradiction look-back window N | `contradictionWindowN` | Int | `0` | -| Anonymous mode | `anonymousMode` | Boolean | `false` | -| Session-start profile fields | `profileFields` | Json (array) | `[]` | -| Answer panel scope | `answerSlotPanelScope` | String (enum) | `'full_progress'` | +| Setting | Column | Type | Default | +| ------------------------------------- | -------------------------- | ---------------------- | ----------------- | +| Question selection strategy | `selectionStrategy` | String (enum) | `'sequential'` | +| Completion: min questions | `minQuestionsAnswered` | Int | `0` | +| Completion: coverage threshold | `coverageThreshold` | Float (0–1) | `1.0` | +| Cost budget (USD / session) | `costBudgetUsd` | Float? (null = no cap) | `null` | +| Per-session question cap | `maxQuestionsPerSession` | Int? (null = no cap) | `null` | +| Voice input | `voiceEnabled` | Boolean | `false` | +| Contradiction-detection mode | `contradictionMode` | String (enum) | `'off'` | +| Contradiction look-back window N | `contradictionWindowN` | Int | `0` | +| Contradiction cadence (every N turns) | `contradictionEveryNTurns` | Int | `1` | +| Anonymous mode | `anonymousMode` | Boolean | `false` | +| Session-start profile fields | `profileFields` | Json (array) | `[]` | +| Answer panel scope | `answerSlotPanelScope` | String (enum) | `'full_progress'` | The enums are `const` tuples in `lib/app/questionnaire/types.ts` (single source of truth — the Zod schema, the read-view narrowing, and the editor's ` + + + + + None (generic copy) + {options.map((client) => ( + + {client.name} + + ))} + + + + +
+ + setNameSuffix(e.target.value)} + disabled={busy} + placeholder="Defaults to the client name" + maxLength={120} + /> +
+ + {error &&

{error}

} + + + + + + + + ); +} diff --git a/components/admin/questionnaires/config-editor.tsx b/components/admin/questionnaires/config-editor.tsx index cbd88b09..06723e99 100644 --- a/components/admin/questionnaires/config-editor.tsx +++ b/components/admin/questionnaires/config-editor.tsx @@ -181,6 +181,9 @@ export function ConfigEditor({ const [contradictionWindowN, setContradictionWindowN] = useState( String(config.contradictionWindowN) ); + const [contradictionEveryNTurns, setContradictionEveryNTurns] = useState( + String(config.contradictionEveryNTurns) + ); const [anonymousMode, setAnonymousMode] = useState(config.anonymousMode); const [answerSlotPanelScope, setAnswerSlotPanelScope] = useState( config.answerSlotPanelScope @@ -201,6 +204,7 @@ export function ConfigEditor({ setVoiceEnabled(config.voiceEnabled); setContradictionMode(config.contradictionMode); setContradictionWindowN(String(config.contradictionWindowN)); + setContradictionEveryNTurns(String(config.contradictionEveryNTurns)); setAnonymousMode(config.anonymousMode); setAnswerSlotPanelScope(config.answerSlotPanelScope); setProfileFields(config.profileFields.map(toRow)); @@ -244,6 +248,14 @@ export function ConfigEditor({ contradictionWindowN: contradictionOff ? 0 : boundedNumber(contradictionWindowN, 1, Number.MAX_SAFE_INTEGER, 1, true), + // Cadence: run detection every N turns (≥1). Irrelevant when off, but harmless to send. + contradictionEveryNTurns: boundedNumber( + contradictionEveryNTurns, + 1, + Number.MAX_SAFE_INTEGER, + 1, + true + ), anonymousMode, answerSlotPanelScope, profileFields: profileFields.map((f) => ({ @@ -451,6 +463,23 @@ export function ConfigEditor({ disabled={busy || contradictionOff} /> +
+ + setContradictionEveryNTurns(e.target.value)} + disabled={busy || contradictionOff} + /> +