Files
ThothII/docs/superpowers/specs/2026-06-29-session-management-design.md
T
marcopanandClaude Opus 4.8 4364bdd4a1 docs(spec): frame cold-start resume correctness as a prerequisite
Add the SKILL.md design contract (persisted state is the truth) and a
dedicated Resume correctness section: backend new/resume prompt mode,
missing cold-start procedure in the skill, end-to-end verification gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:39:56 +02:00

16 KiB
Raw Blame History

Session Management — Design

Date: 2026-06-29 Status: Approved (design), pending implementation plan Layers: frontend, backend (Fastify), harness (tht CLI)

Problem

The session rail (NavSessions.tsx) is today a flat list whose only interaction is click = resume (spawns a Pi process). There is no way to read a past session, rename it, organize it, archive it, or delete it. Inspired by Claude's desktop session context menu, we want a complete management surface with five capabilities:

  1. Vista divisa — a read-only side panel showing the session's stored content.
  2. Rinomina — rename a session.
  3. Sposta nel gruppo — organize sessions into groups.
  4. Archivia — archive a session out of the active list (read-only retention).
  5. Elimina — permanently delete a session.

Key architectural insight (drives the whole design)

The workflow is phase-based. Each phase persists its own document into the session directory; advancing to the next phase does not require replaying the prior conversation. Therefore the phase documents ARE the persistence — we do not need to store the verbatim chat.

This is the explicit design contract of the orchestrator (SKILL.md): "the persisted state (ledger review_decisions.jsonl + artifacts) is the truth — what is not recorded did not happen." The documents are not meant to contain the chat; they are meant to make the chat reconstructable / unnecessary — the documents are the state, the skill is the procedure, and together they let a fresh model continue the task. Whether that holds for a cold-start resume is a verifiable prerequisite, treated explicitly below.

This was the crux decision in brainstorming. It means:

  • The "vista divisa" is a read-only document viewer driven by what's on disk, not a chat replay. No new transcript-persistence layer, no correlation of Pi's internal JSONL logs.
  • "Resuming an interrupted session" = the normal chat flow re-entering at the last incomplete phase; the earlier phases' documents are the context.

What each phase persists (from workflow.yaml)

Phase Document on disk
(manifest, always) session_manifest.yaml — original question, name, status, author, timestamps
F3 riscrittura question.md — revised question + assumptions
F4 schema_linking schema_linking.json
F6 cte cte_plan.json, cte_tests.json
F7 sql_finale sql_final.sql
finalize validation_report.md, evidence.json
(decisions, always) review_decisions.jsonl — decision ledger

tht session show --json already returns the manifest plus the computed phase (current phase, folded from the ledger by phase.py) and has_schema_linking.

Decisions (from brainstorming)

  • Read panel content (option C): a status header (Fase N di 8 · <nome> / Completata / Archiviata) + document cards in phase order (only those that exist) + a collapsible Decisioni block. No verbatim chat.
  • Resume rule: status == "finalized" OR archived == true → read-only, never resumable. Any other state (open, closed) → resumable into the last incomplete phase.
  • Row click = open the read-only panel (it no longer resumes). Resume is an explicit "Riprendi" button shown in the panel header only when the session is resumable.
  • Archive = filesystem flag. A boolean archived field on the manifest. The active rail filters out archived sessions; a separate "Archivio" view lists them and opens the same read-only panel. (A flag, not a directory move, so id→path resolution sessions_root / session_id and resume/show keep working unchanged.)
  • Delete = hard delete of the session directory, behind a confirmation dialog. Available from both the active list and the archive. No trash/soft-delete — the archive is the "soft" tier.
  • Groups = manifest field (model A). A nullable group string on the manifest. The rail derives the group list from distinct values. "Sposta nel gruppo ›" lists existing groups + "Nuovo gruppo…" (type a name). No separate registry, no colors/ordering, no persistent empty groups.
  • Rename = manifest name (field already exists). The rail shows name instead of the question when present.

Data model — manifest changes

In SessionManifest add two fields (name already exists):

archived: bool = False
group: str | None = None

Both default such that existing manifests load unchanged. They are written by the new mutating commands below and surfaced by session list/session show.

Harness layer (tht CLI)

New/changed subcommands in session_cmd.py, with corresponding helpers in store.py. All mutate the manifest via the existing touch_manifest/to_yaml pattern and update updated_at/updated_by.

  • tht session set-name <id> --name <name> → set manifest.name.
  • tht session set-group <id> --group <name> → set manifest.group (empty string clears it back to None).
  • tht session archive <id> / tht session unarchive <id> → flip archived. (Unarchive is included so an accidental archive is recoverable; it does not change resumability — a finalized session stays read-only.)
  • tht session delete <id> → remove the session directory (shutil.rmtree). Idempotent error if absent.
  • tht session documents <id> --json → new read command returning the ordered, available documents for the panel:
    [ { "phase": "F3", "key": "revised_question", "title": "Domanda rivista",
        "format": "markdown", "content": "..." }, ... ]
    
    Document set: original question (from manifest), question.md, schema_linking.json, sql_final.sql, validation_report.md, and the decision ledger. Only documents that exist on disk are returned. The F6 CTE artifacts (cte_plan.json, cte_tests.json) are intentionally excluded from the v1 panel as intermediate workflow state, not reviewer-facing deliverables; add later if needed. The backend cannot read these files directly because the sessions root lives in the workspace YAML (resolved by tht config, unknown to Node) — so reading goes through tht.
  • session list --json → each row additionally carries archived, group, name (in addition to the existing id/status/question/summary/created_at/updated_at/author). The active vs archive split and grouping are done in the frontend from these fields.

Backend layer (Fastify)

New routes in sessions.ts, each delegating to a new ThtRunner method that shells the matching tht subcommand:

  • POST /sessions/:id/rename { name } → tht session set-name.
  • POST /sessions/:id/group { group } → tht session set-group (group: "" clears).
  • POST /sessions/:id/archive / POST /sessions/:id/unarchive.
  • DELETE /sessions/:id → tht session delete. If a Pi runtime is live for that id, tear it down first (mgr.teardown).
  • GET /sessions/:id/documents → tht session documents --json.
  • GET /sessions → unchanged route; rows now include archived/group/name.

Resume guard. POST /sessions/:id/resume must refuse when the session is read-only. Before spawning, read the manifest (tht.sessionShow); if status == "finalized" or archived, return 409 with a clear message and do not spawn Pi.

Resume must rebuild phase context. This is the backend half of the resume-correctness prerequisite: spawnFor gains a new/resume mode (new → /nuova-domanda, resume → /riprendi-sessione <id>). See the dedicated Resume correctness section below for the full picture (backend prompt, skill cold-start procedure, end-to-end verification).

Frontend layer (React + base-ui + Tailwind + React Query)

Types (types.ts)

  • Extend SessionSummary with archived: boolean, group: string | null, name: string | null.
  • Add SessionDocument { phase: string; key: string; title: string; format: "markdown" | "sql" | "schema-linking" | "decisions" | "text"; content: string }.

API client (sessions.ts)

  • Add renameSession(id, name), setSessionGroup(id, group), archiveSession(id), unarchiveSession(id), deleteSession(id), getSessionDocuments(id).

NavSessions (NavSessions.tsx)

  • Row click → open the read-only panel (via a new onOpenPanel(id) prop), no longer resumes.
  • Render the active list (filter !archived) grouped under collapsible group headers derived from distinct group values, plus a "Senza gruppo" section for group == null.
  • Each row gets a kebab (⋮) menu on hover (base-ui Menu) mirroring the reference image:
    • Vista divisa → open the read-only panel
    • Rinomina → rename dialog (sets name)
    • Sposta nel gruppo › → submenu of existing groups + "Nuovo gruppo…"
    • Archivia → archiveSession
    • Elimina (red) → confirmation dialog → deleteSession
  • After each mutation, invalidate the ["sessions"] query.

Archive view

  • An "Archivio" entry at the bottom of the rail. Selecting it shows archived sessions (filter archived), each opening the same read-only panel. Archived rows offer Vista divisa, Ripristina (unarchive), and Elimina — no resume.

SessionDocumentsPanel (new, left drawer)

  • New frontend/src/shell/SessionDocumentsPanel.tsx. A left column/drawer, opened by onOpenPanel, closed with ✕.
  • Header: name/question, status chip (Fase N di 8 · <nome> / Completata / Archiviata), author, created/updated. A "Riprendi" button rendered only when resumable; it triggers the existing resume flow and focuses the center chat.
  • Body: one card per SessionDocument, in phase order, rendered by format reusing existing viewers — format: "sql" → SqlViewer, "schema-linking" → SchemaLinkingViewer, "markdown"/"text" → MarkdownView.
  • Decisioni: the decisions document rendered as a collapsible list at the bottom.

AppShell (AppShell.tsx)

  • Add the left panel column to the existing layout, giving three columns: [ documents panel (left) | chat (center) | sessions rail (right) ].
  • Track two distinct ids: the active chat session (drives useSessionStream + the center) and the panel session (read-only, drives the left drawer). Opening the panel does not touch the active chat; "Riprendi" promotes the panel session to the active one.

UX summary

  • Layout: three columns, panel on the left as requested, rail stays on the right.
  • Context menu: kebab ⋮ on hover, items as above; "Elimina" in red with confirm.
  • Rail organization: collapsible group headers + "Senza gruppo"; "Archivio" as a separate section, never mixed into the active list.

Resume correctness — verifiable prerequisite

The whole "no chat persistence" premise rests on a fresh Pi process being able to re-enter the workflow at the last incomplete phase using only the persisted state. That is the design contract quoted above (ledger + artifacts are the truth; each phase's prerequisites are prior artifacts; rollback /torna N, discipline 11, already resumes "reviewing the existing artifacts" without replaying chat). The premise is sound — but the cold-start resume path (new process, zero conversation) is today thin and unproven. This feature treats resume correctness as a prerequisite and must close three gaps:

  1. Backend sends the wrong prompt. PiProcessManager.spawnFor (pi-process-manager.ts) always sends /nuova-domanda "kickoff", and resume calls the same path — so a "resume" kicks off a new question. spawnFor must take a new/resume mode: new → /nuova-domanda, resume → /riprendi-sessione <id> (the prompt riprendi-sessione.md).
  2. No cold-start procedure in the skill. The phases assume "you must already be in Phase 1"; there is no "Phase 0 / Resume" step telling a fresh process to read tht session show <id>, take the current phase N, load that phase's artifacts, and resume its procedure. Add a short Resume section to SKILL.md (or confirm the model reliably bootstraps from tht session show + artifact reads). The one-line resume prompt is not sufficient on its own — intra-session rollback works only because the conversation is still live; cold start has nothing to lean on.
  3. End-to-end verification. A test that creates a session, drives it partway (e.g. into F4), tears down the Pi process, resumes in a fresh process, and asserts it lands on the correct phase with the prior artifacts available and presents the next gate — not a new-question kickoff.

Until (1)–(3) pass, the phase-document model is a design assumption, not a proven fact. The read panel, rename, group, archive, and delete work regardless of resume; but the "Riprendi" action is only trustworthy once resume correctness is verified, so it must not ship as "working" before this gate is green.

Testing approach (TDD per repo convention)

  • Harness (pytest): new store.py helpers (set-name/group, archive/unarchive, delete) and the session documents --json shape; manifest round-trips with the new fields; session list --json includes them.
  • Backend (vitest): each new route maps to the right tht invocation (injected ThtRunner/spawn double); resume guard returns 409 for finalized/archived; spawnFor sends /riprendi-sessione in resume mode and /nuova-domanda in new mode.
  • Frontend (vitest + RTL + MSW): kebab menu actions call the right endpoints and invalidate the query; grouping/archived filtering renders correctly; the panel renders documents via the right viewers and shows/hides "Riprendi" by resumability; delete-confirm flow.
  • Resume correctness (end-to-end): the verification described in the Resume correctness section — partway session → teardown → fresh-process resume → lands on the correct phase with prior artifacts, not a new-question kickoff. This gate must be green before "Riprendi" is considered working.

Out of scope / deferred (with triggers)

  • Application database / pgvector. Not introduced. The only DB is the client's read-only DWH; an app DB is new infrastructure not justified by store/browse/view/ no-resume. Revisit when the archive must outlive the workspace filesystem (→ app Postgres + archived_sessions, full-text via tsvector) or when "reuse a semantically similar past session" becomes a goal (→ add a vector column + pgvector). Both are non-breaking additions later.
  • Keyboard shortcuts (R/A/D/F in the reference image) — polish, addable later.
  • Group colors, ordering, persistent empty groups — the model-B registry, only if needed.
  • Verbatim chat persistence — explicitly unnecessary given the phase-document model.