V1 Web PWA · Journey

Journey: Crisis-aware tone policy interception

A source-reconciled journey record for V1 Web PWA — shipped behavior, state boundaries, failure modes, and the automation evidence available today.

automated · 3 specs verdict: partial· 2026-05-29
9sections19 minread

On this page
Journey at a glance
ActorsLong-tenured user who experiences acute distressFirst-time user surfacing distress on first contactUser the operator has flagged from a separate signalMobile user on a standalone PWAUser in a tenant with stricter institutional policy
Automation3 E2E spec(s) · verdict: partial· 2026-05-29

Journey flow#

Generated from the authored steps below — click a node to jump to that section.

flowchart TD s1["1. User submits input on a Lilith-wired surfa…"] s2["2. Lilith evaluates the signal"] s3["3. The frame cascades across substrates"] s4["4. The customer-side surface presents the saf…"] s5["5. Stillness window"] s6["6. Re-entry prompt at the end of the window"] s7["7. Visible audit"] s8["8. Follow-up cadence"] s9["9. Re-frame protection"] s10["10. Operator-side correlate (out of customer…"] s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 --> s8 --> s9 --> s10 click s1 href "#1-user-submits-input-on-a-lilith-wired-surface" click s2 href "#2-lilith-evaluates-the-signal" click s3 href "#3-the-frame-cascades-across-substrates" click s4 href "#4-the-customer-side-surface-presents-the-safety-response" click s5 href "#5-stillness-window" click s6 href "#6-re-entry-prompt-at-the-end-of-the-window" click s7 href "#7-visible-audit" click s8 href "#8-follow-up-cadence" click s9 href "#9-re-frame-protection" click s10 href "#10-operator-side-correlate-out-of-customer-scope" s10 --> v(["verdict: partial"]) click v href "../results/crisis-aware-tone-policy.html" style v stroke:#d6a531,color:#d6a531

This journey specifies how distress input must interrupt persona output, expose plain-language resources, and place the rest of the product inside a reversible crisis frame. It is the customer-side walk of the Crisis-frame cascade and the Crisis-Aware Behavior policy section of V1/features.md. It is also a walk of the Crisis recovery journey user-facing path back into the product.

This is a sensitive journey. It describes substrate behavior, never product messaging language, and never asks a reviewer to invent specific crisis indicators or therapeutic recommendations. Specific signal taxonomy lives in V1/features.md and is owned by the Lilith policy team.

Current implementation boundary. Four shipped seams are distinct and must not be read as one complete, universal interception path:

  1. /arete/offering preflights typed intention text through POST /v1/lilith/adapter/safety-assessment and fails closed before a keep/send write.
  2. An operator-scoped activation route publishes lilith.crisis_frame.activated; its worker stores markers for assistant, iris, lilith-video, psyche, and tara.
  3. Several BFF handlers consult the bound crisis-frame store before serving non-safety work.
  4. /profile/safety reads the member's markers, then builds its stillness, re-entry, and check-in controls in the browser from the recovery library.

The repository does not yet prove that typed input on every named surface activates the durable frame, that recovery decisions clear the server markers, or that every target telemetry event below is emitted. Unchecked steps are the remaining policy contract, not claims about the current runtime.

Personas#

  • Long-tenured user who experiences acute distress — primary persona; has Iris memory, declared lineage, ongoing Arete and Tara cadence
  • First-time user surfacing distress on first contact — no Iris memory; the substrate cannot read prior context; the policy must still fire cleanly
  • User the operator has flagged from a separate signal — an operator escalation triggers the frame; the customer-side experience is the same
  • Mobile user on a standalone PWA — verifies the safety surface renders without the URL bar and with safe-area insets
  • User in a tenant with stricter institutional policy — per V1/features.md#wiring-versioning-and-evaluation, tenant policy can tighten beyond platform baseline; the frame still fires; resources are localized per tenant where applicable

Pre-conditions#

  • A signed-in member is required for the member-owned /profile/safety record. The resource catalog itself is public at GET /v1/safety/crisis-resources; there is not yet an anonymous customer web safety view.
  • The target policy requires Lilith interception on Tara teacher personas, contemplative assistants, journal capture, Veritas explainers, Sophia answers, Metis tutoring, and support copilot output — per V1/features.md#wiring-versioning-and-evaluation
  • The shipped active-input proof is currently /arete/offering; the durable cross-surface proof starts from the operator activation route.
  • A safety-resource set is loadable. The public BFF currently returns its globally scoped catalog; tenant/locale selection remains a target requirement per V1/features.md#crisis-aware-behavior
  • Redis and the durable snapshot store must be bound for operator activation to project member-visible crisis-frame markers. Without that composition, the member endpoint honestly returns an empty set.
  • The architecture requires an audit-platform attestation per V1/ARCHITECTURE.md#crisis-frame-cascade
  • There is no /lilith/crisis customer-facing route in V1; this journey describes substrate behavior across the existing surfaces (assistant, Tara, journals, etc.)

Steps#

1. User submits input on a Lilith-wired surface#

  • On /arete/offering, the user enters an intention and chooses Save draft or Send. AreteOfferingActions sends the exact text and region to the Lilith safety-assessment route before any offering write.
  • Extend the same preflight guarantee to every target free-text surface: assistant chat (see shell/05-notifications.md message center / assistant), Arete journal capture (customer/04-arete/domains-arete-journal.md), Tara session reflection, Metis tutor turn, Veritas explainer prompt, Living Scenes prompt input
  • The user submits text expressing distress (the substrate decides classification; this walkthrough does not invent specific tokens)
  • The input flows through the per-surface Lilith hook before any persona, generation pipeline, or review queue ingests it (per V1/features.md#lilith-persona-policy: "wires them into every persona, generation pipeline, and review queue")

2. Lilith evaluates the signal#

  • POST /v1/lilith/adapter/safety-assessment evaluates the request through the current 13-rule classifier and returns allow or a blocking crisis disposition with escalation metadata and resources.
  • Every target surface evaluates input against the Crisis-Aware Behavior signal taxonomy using its versioned classifier
  • An authorized operator can activate a durable frame through POST /v1/admin/safety/crisis-frame/activate; the route publishes lilith.crisis_frame.activated with the normalized crisis detection and target surfaces.
  • A blocking user-input assessment directly activates that same durable frame without a separate operator call. This bridge is not wired today.
  • If the evaluation reaches the crisis threshold (severity × scope per V1/ARCHITECTURE.md#crisis-frame-cascade), the activation carries an explicit expiry/deactivation lifecycle. The current event stores activation time and surfaces, not a TTL.
  • If the input is below threshold but adjacent (e.g., distress that doesn't meet the crisis signal threshold), the substrate may apply a softer reflective tone band, hedge claims, and surface a "we're here" affordance — without firing the full frame

3. The frame cascades across substrates#

The shipped worker projects one marker for each of assistant, iris, lilith-video, psyche, and tara. Route handlers including assistant, reminders, Living Scenes, customer messages, mentor presence, and Telegram consult the bound store through isCrisisFrameActiveForUser. Per the broader cascade in V1/ARCHITECTURE.md#crisis-frame-cascade, the product contract still requires, in parallel:

  • Tara: suspends scheduled rituals and humane recovery prompts; no new Tara push, no scheduled invitations
  • Living Scenes: locks renders into reduced motion and disables opt-in share
  • Isis: caps generation audacity, enforces reduced provenance, blocks premium personas
  • Assistant: switches to a crisis-aware persona that surfaces support resources
  • Iris: tags active session memory so recall doesn't re-surface charged content; suspends new memory writes during the frame per V1/features.md#iris-memory-and-identity "Crisis-frame suppression"

4. The customer-side surface presents the safety response#

Per the Per-signal response and the Crisis recovery journey "Entry":

  • The Arete offering branch replaces the action area with a plain safety interruption, exposes 988 support and /profile/safety, redacts the submitted text, and performs no keep/send write.
  • /profile/safety exposes a member-owned marker timeline and an actionable tel:988 resource link; opening it emits lilith_safety_resource_opened.
  • Every Lilith-wired persona breaks to plain operator voice (no teacher voice, no contemplative narration, no avatar render)
  • The surface presents:
    • A clear, non-clinical message in plain operator voice
    • The region-aware safety-resource set for the user's locale + declared frame (hotlines, text lines, emergency services as appropriate)
    • A "we're here" affordance — routes to a human-support handoff where the user has consented to support contact, or to the asynchronous support inbox otherwise
  • Ongoing audio synthesis halts immediately; no ritual continuation; no voice / avatar render
  • Telemetry: a crisis-frame entry event is recorded; the event is audit-platform visible per V1/ARCHITECTURE.md#crisis-frame-cascade

5. Stillness window#

Per the Crisis recovery journey "Stillness window":

/profile/safety currently derives this window from the marker's activation time with openStillnessWindow and stores member interaction state in frame-namespaced localStorage. This is a real recovery UI over a real marker, but it is not yet a server-authoritative recovery state machine.

  • A configurable minimum interval (default 10 minutes, never less than 3) elapses before any non-safety surface re-enables
  • Tara recovery rituals are surfaced as invitations during the window, never as scheduled events
  • Living Scenes, Veritas briefings, and Isis generation remain disabled for the window
  • Push notifications scheduled to fire during the window are deferred or suppressed per shell/05-notifications.md quiet hours pattern

6. Re-entry prompt at the end of the window#

  • At the end of the stillness window, the product asks once whether the user wants to continue
  • If the user declines, the surface routes to an idle-safe surface (a chosen calm Tara ritual or the device home screen); the product does NOT re-ask until a fresh session
  • If the user accepts, the user lands on a re-entry home with conservative defaults: contemplative-strict band, no scheduled invitations, no recommendations from sensitive categories
  • The user can manually re-enable categories from profile settings

7. Visible audit#

Per the Visible audit rule:

  • A "what happened" surface in profile/safety shows the user the audit-platform-visible record of the frame (entry time, exit time, suppressed surfaces, operator actions if any)
  • The user owns the record of what occurred — no operator-only hidden log
  • Per V1/features.md#iris-memory-and-identity, the user can elect (consent-gated) to keep a memory of the event; otherwise the suppression remains

8. Follow-up cadence#

Per the Follow-up cadence rule:

  • 24h and 7d soft check-ins (default opt-in; user can disable)
  • Check-ins flow via the assistant only — NEVER via push or notification
  • Each check-in offers the same support-resource set and the same idle-safe surface

9. Re-frame protection#

  • If a new crisis indicator fires during the stillness window or within 24h, the frame EXTENDS rather than re-fires — protects against thrash and re-traumatization per the Re-frame protection rule

10. Operator-side correlate (out of customer scope)#

For reference (not a customer step, but the customer experience depends on it firing): every crisis frame generates an Incident record per V1/features.md#crisis-aware-behavior and a Persona Lifecycle Event-style attestation; operator review is required within tenant-policy time-box. The customer-side journey assumes the operator-side path is healthy.

Post-conditions#

  • A blocking Arete assessment prevents the offering write and leaves the safety handoff visible.
  • Operator activation yields member-owned durable markers for all five declared crisis-frame surfaces when Redis and snapshot persistence are bound.
  • GET /v1/safety/crisis-frames/mine returns only the authenticated member's markers with cache-control: no-store; /profile/safety renders those markers or an honest empty/unavailable state.
  • The member can operate the browser-local stillness, re-entry, check-in, and resource UI. These choices do not currently deactivate or delete the BFF markers.
  • The verified customer telemetry event is lilith_safety_resource_opened. The larger activation/surface/re-entry/ follow-up telemetry sequence remains a release requirement until source and automation prove each event.

Failure modes to verify#

False positive (system intervenes when it shouldn't)#

  • Benign-but-adjacent input — the classifier mis-classifies a contemplative phrase or a journal entry quoting a teacher's lineage text; the safety surface fires unnecessarily. The user MUST have a low-friction "this isn't a crisis for me" affordance that:
    • Records feedback for the evaluation suite per V1/features.md#wiring-versioning-and-evaluation
    • Does NOT require the user to re-explain
    • Restores the prior surface state if appropriate; the audit record notes the user-initiated dismissal
  • Cross-lineage misreading — distress markers in one tradition are benign in another (e.g., devotional surrender language); per the Cultural and lineage sensitivity policy, the classifier must respect declared lineage; verify the eval suite includes lineage-respect false-positive cases

Missed true (system doesn't intervene when it should)#

  • Below-threshold-but-real distress — input reads as ambient but the user is in actual distress; the substrate may apply the softer reflective tone band; the user MUST still have a discoverable "we're here" affordance on every Lilith-wired surface (not just on the safety surface). The journey verifies this affordance exists on Tara reflection, Arete journal, and assistant chat
  • Operator-flagged session — when the signal source is operator escalation rather than user input, the frame must fire as cleanly as a user-input trigger; verify the customer experience is identical
  • Voice-input distress — per V1/features.md#voice-quality-policy, naturalness floor and silence-density floor apply; verify the classifier reads voice input as well as text and the surface degrades to text + recorded-voice library + apology if needed

Recovery (user recovers gracefully from intervention)#

  • User dismisses the safety surface — the journey continues with the stillness window enforced; even on dismiss, the contemplative-strict band holds, no scheduled invitations fire, and no premium personas are available until the frame clears
  • User accepts the re-entry prompt — lands on the re-entry home with conservative defaults; the audit record carries the timing; Iris memory writes resume per the user's consent state
  • User declines the re-entry prompt — routes to a chosen calm Tara ritual or the device home; the product does NOT re-ask until a fresh session
  • User explicitly opts out of follow-up check-ins — per the Follow-up cadence "user can disable" rule, the 24h and 7d check-ins are suppressed
  • User wants to keep a memory of the event — consent-gated memory of the event is recorded per V1/features.md#iris-memory-and-identity; otherwise the suppression remains
  • Tenant-specific safety resources missing for the user's locale — the default frame copy must have been reviewed by a regional safety advisor before launch in that locale per the Localization rule; missing resources fall back to a globally-vetted default set and trigger an operator alert
  • Anonymous user encounters distress — the safety surface and region-aware resources must still render (PUBLIC_PATHS includes the paths the user can reach); per shell/04-auth-session.md, the sign-in gate must NOT block the safety surface
  • Crisis frame extends rather than re-fires — a new indicator within the stillness window or 24h must extend, not re-fire, per Re-frame protection; verify the telemetry shows a single activated event, not multiple

Other failure modes#

  • Override attempts — neither user prompt nor admin copilot can override the crisis policy per V1/features.md#crisis-aware-behavior "No-bypass guarantees"; verify by attempting (in a test environment) to instruct the assistant to ignore the frame
  • Voice abuse coincident with distress — per V1/features.md#voice-abuse-policy, an above-threshold abuse score halts generation independently; verify the two policies compose without conflict (the more restrictive applies)
  • Crisis-frame deactivation — the frame is reversible; verify the deactivation event is audited and the user-facing record reflects both entry and exit times; verify Tara invitations resume per cadence and Iris memory writes resume per user consent
  • Performance budget for the safety surface — the surface must render even when the network is degraded; the resource set must be cached on the SW; verify per shell/03-pwa-behavior.md

E2E coverage#

  • Backed by apps/oshun/web/e2e/profile-safety-crisis-recovery.spec.ts — exercises the profile/safety surface directly against real dev infra: signed-in member starts with an empty GET /v1/safety/crisis-frames/mine; operator activation through POST /v1/admin/safety/crisis-frame/activate projects the durable frame; the browser then renders /profile/safety from the live member readback with no route interception. The spec asserts that the four recovery-model rows (teaching, invitation, scheduled-generation, studio) report suspended together during the stillness window and lift together in the browser model after it. This assertion does not clear the durable BFF markers. The spec also covers the re-entry prompt accept flow (landing on contemplative-strict); 24h check-in delivery and dismissal; opt-out of the 7d follow-up; and the re-entry decline failure mode (routed to an idle-safe surface via data-profile-safety-reentry-status="idle") with the visible "what happened" audit card (data-profile-safety-what-happened) and the 988 crisis resource (data-profile-safety-resource="us-988") staying available. The same real-BFF profile-safety flow now verifies that the 988 resource is an actionable tel:988 link and that opening it emits lilith_safety_resource_opened with the active frame ID. It also verifies the live cross-surface frames[] timeline across assistant, Iris, Lilith video, Psyche, and Tara markers with an expandable assistant-frame metadata row, plus the no-frame empty state and anonymous redirect-before-render branch.
  • Backed by apps/oshun/web/e2e/crisis-safety-policy-bff.spec.ts — real HTTP integration against the live BFF safety substrate (steps 2 + 4): POST /v1/lilith/adapter/safety-assessment runs the real 13-rule crisis classifier (a distress message classifies as a crisis → block disposition + escalation + suicide-ideation/self_harm crisisTypes + the 988 Suicide & Crisis Lifeline / 741741 Crisis Text Line resources; a benign self-care message → allow with no crisis types; missing userMessage → 400; wrong-domain token → 403; no bearer → 401). The public GET /v1/safety/crisis-resources returns the 988 + 741741 resource set with no Authorization header, proving the no-sign-in-gate-blocks-resources guarantee at the BFF layer; POST /v1/safety/resources/suggest exercises the real relevance scorer (401 no bearer, 400 invalid observations, 200 with a scored direct safety suggestion). The member-owned crisis-frame audit API is also proved through the live BFF: unauthenticated GET /v1/safety/crisis-frames/mine returns 401, an authenticated member with no active frame receives an uncached (cache-control: no-store) honest empty record, customer-scoped activation is rejected with admin_scope_missing, and an authorized malformed activation is rejected with invalid_request. The same spec now drives the substrate cascade through real dev infrastructure: an operator-scoped POST /v1/admin/safety/crisis-frame/activate publishes the activation to the Redis-backed worker, the worker persists one crisis-frame marker per required contemplative surface (assistant, iris, lilith-video, psyche, tara), and the affected member reads those durable markers back from GET /v1/safety/crisis-frames/mine while an unrelated member receives an empty set.
  • Backed by apps/oshun/web/e2e/arete-offering-actions.spec.ts — real browser coverage for the shipped active-surface preflight branch on /arete/offering: the user types a blocking intention into the offering textarea, clicks Save draft, and the browser sends the live POST /v1/lilith/adapter/safety-assessment?role=safety request with the auth-context bearer and exact { userMessage, region: 'US' } payload. The same browser state then swaps the action region into the plain safety interruption, renders 988 resources plus the /profile/safety handoff, and proves no /v1/arete/offerings/keep write occurs.
  • Coverage depth: partial — see coverage.md.
  • Uncovered:
    • A shipped browser active-surface preflight is now proven for Arete, but the full durable cross-substrate cascade is still not triggered from real active-surface user input. Coverage proves operator activation → Redis worker → durable per-surface markers → browser /profile/safety member read-back, and Arete typed distress → direct Lilith BFF preflight → in-place safety interruption. It does not yet prove typed distress creates the same durable crisis-frame markers without operator activation, nor that assistant/Tara/Metis surfaces swap in place from their own text input.
    • False-positive dismissal affordance ("this isn't a crisis for me") is not asserted (the affordance is feature-absent in the web surface).
    • The anonymous web safety surface is not exercised — /profile/safety is auth-gated and redirects before render; the public-resource guarantee is verified at the BFF (GET /v1/safety/crisis-resources, no auth) but not on a customer-facing public web view.
    • Re-frame protection (extension rather than re-fire on a second indicator within 24h) remains library-only and is not e2e-surfaced.

Per-view files touched by this journey#

The journey crosses every Lilith-wired customer surface. Most relevant:

Cross-references#

Open questions / known gaps#

  • No /lilith/crisis customer-facing route exists in V1 — confirmed by design. The crisis-aware substrate lives in libs/oshun/persona-policy-lilith/src/crisis-recovery/ (check-ins, incident-record, locales, reentry-flow, reframe-protection, stillness-window) and is wired through each Lilith-using surface individually. The user-facing audit lives at /profile/safety (per customer/09-account/profile-safety.md). A dedicated /lilith/crisis route would conflict with the cascade model — the safety surface mounts in-place on the active surface.
  • Snapshot the customer-side "this isn't a crisis for me" affordance shape (button label, placement, telemetry event) so the false-positive feedback loop has a concrete UI.
  • Confirm the offline-availability of the region-aware safety-resource set on first cold-start (the SW must precache the default-locale resource set).
  • Confirm the assistant follow-up check-ins at 24h and 7d use the assistant inbox channel (per customer/02-home-discovery/home.md Companion bridge rail) rather than push.
  • Verify the tenant-policy-tighten path: institutional tenants can tighten unsafe-claim thresholds and crisis thresholds — document where the tenant-policy review surface lives in operator tooling.
  • Pin the visible audit copy at profile/safety so a reviewer can confirm the user actually owns the record of what occurred. The browser spec asserts data-profile-safety-what-happened includes the crisis label and redacted excerpt, and the BFF spec asserts the backing /v1/safety/crisis-frames/mine record is authenticated, uncached, and member-owned.
  • Add the classifier-to-activation bridge so typed distress on each named active surface creates the same durable markers currently created by the operator route.
  • Add and verify a server-side close/extend/deactivate lifecycle. The current recovery UI advances local state, while the snapshot store retains active markers and exposes no deactivation route.
  • Move re-entry decisions and check-in preferences from frame-namespaced localStorage into member-owned server state before calling recovery cross-device or audit-complete.