Session Feedback
An explicit end-of-session feedback collector that captures a thumbs
rating when the user finishes a session. The data is stored in a
dedicated SQLite file (session_feedback.sqlite3) parallel to the
crisis log — always-on, session-opaque, incognito-safe.
What's collected
A single SessionFeedbackRecord per end-session event, containing:
| Field | Value | Source |
|---|---|---|
id | Fresh uuid4 | Server-generated |
session_id_opaque | SHA-256 of thread_id | Server-derived, never the raw id |
user_id_or_null | state.user_id (LOCAL/SYNCED) or None (incognito) | Server-derived, scrubbed in incognito |
recorded_at | ISO-8601 with Z suffix | iso_now() at write time |
label | "positive" / "negative" / "skip" | User's explicit choice |
turn_count_at_end | state.progress.turn_count | Read from checkpoint |
source | "cli_end" / "cli_exit" / "api_end" | Which end-session surface captured it |
schema_version | 1 | Fixed for Phase 1 |
No user text is stored. The record captures only the classification label and structural metadata. This is a deliberate privacy boundary — mirroring the crisis log's "metadata-only, no content" contract.
Where it lives
| Memory mode | Backend | Persistence |
|---|---|---|
| LOCAL / SYNCED | SqliteSessionFeedbackBackend at .store/session_feedback.sqlite3 | Survives CLI / server restarts |
| INCOGNITO | InMemorySessionFeedbackBackend | Dies at process exit |
Fourth SQLite file under .store/, alongside threads.sqlite3,
memory.sqlite3, and crisis.sqlite3. Same isolation reasoning
— each subsystem owns its schema independently.
Default retention: 180 days (wider than crisis log's 90 because
feedback analytics benefit from a longer lookback). Enforced via
apurge_before — an operator / scheduled-job concern, not touched
by normal write paths.
Collection surfaces
CLI /end
The /end command triggers the full end-session flow:
- Feedback prompt —
"Quick check — did today feel helpful? [y/n/s] (or Enter to skip without recording)" - Feedback write —
runtime.record_session_feedback(thread_id, label=..., source="cli_end") - Summarization —
runtime.end_session(thread_id, ...) - Farewell
Pressing Enter, Ctrl-C, or EOF at step 1 produces no record — the
flow skips to step 3 without writing feedback. Explicit y/n/s
maps to positive/negative/skip.
CLI /exit (save=y branch)
Same flow as /end but with source="cli_exit". The /exit
command first prompts to save a summary ([Y/n]). If the user says
yes (or presses Enter — default Y), the feedback + summarization
flow runs.
/exit with save=n intentionally skips both feedback and
summary. The user said "don't save my conversation" — asking for a
rating on that path would be inconsistent.
HTTP POST /threads/{id}/end
The endpoint accepts an optional body:
{"feedback": "positive"}
Valid labels: "positive", "negative", "skip". Invalid values
are rejected with HTTP 422 by Pydantic before any code runs.
When feedback is null or the body is absent, no feedback record
is created — summarization runs unchanged. The response shape is
unaffected by the feedback write (no status surfaced).
Not covered (Phase 1)
- Closing mode — the therapeutic closing node does NOT emit a
feedback prompt. Closing is a tonal farewell, not a session
termination. If we later want in-conversation feedback hints, the
same
record_session_feedback()method works — the closing node would call the runtime directly. - Voice disconnect —
voice/realtime.pybypassesPersistentAgentRuntime.end_session()entirely and callsrun_summarize_session()directly. Adding voice feedback requires moving voice onto theend_session()seam first.
Inspection
CLI
/memory status
The memory status panel now includes a session feedback records
row alongside the crisis log count.
API
GET /api/memory/status?thread_id=...
Response includes "session_feedback_count": <int>.
Programmatic
count = await runtime.session_feedback_backend.arecord_count()
records = await runtime.session_feedback_backend.alist_by_session(session_id_opaque)