From 0a13f71b4d1e8a9f003dae8ef1abc1e1b78c607d Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 30 Jun 2026 18:19:10 +0200 Subject: [PATCH] feat(resume): Resume in the kebab menu (A1) + harden the resume kickoff (A2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A1 — SessionMenu gains a Resume item, gated to status!=="finalized" && !archived (matching the backend's 409 read-only guard), wired in AppShell to doResume -> POST /sessions/:id/resume. SessionMenu.test.tsx (3 tests); frontend 96/96, tsc clean. A2 — diagnosis-first clean-room repro driving `pi --mode rpc` with the backend's exact resume handshake shows the cold-start stall NO LONGER reproduces on pi 0.79.4 (8/8 chained into `tht session show` + `read SKILL.md` in-turn, fresh and partway sessions). The earlier narrate-and-stop predates the pi upgrade. Defense-in-depth anyway: RIPRENDI_KICKOFF hardened to force the in-turn tool call (gate_resume_kickoff.test.js + live regression 2/2). Gate JS 34/34. PROJECT_STATE open-item #1 (resume stall) flipped to RESOLVED; cross-model resume robustness folded into workstream G. Co-Authored-By: Claude Opus 4.8 --- PROJECT_STATE.md | 37 ++++++++------ frontend/src/shell/AppShell.tsx | 1 + frontend/src/shell/SessionMenu.test.tsx | 49 +++++++++++++++++++ frontend/src/shell/SessionMenu.tsx | 9 +++- .../__tests__/gate_resume_kickoff.test.js | 21 ++++++++ harness/.pi/extensions/tht-gate.js | 3 ++ 6 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 frontend/src/shell/SessionMenu.test.tsx create mode 100644 harness/.pi/extensions/gate/__tests__/gate_resume_kickoff.test.js diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 88181b64..51981af8 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -81,8 +81,8 @@ Opens frontend at http://localhost:5173 → backend :8787. ## UI/UX redesign + Resume — IN PROGRESS (2026-06-30, evening) Approved multi-workstream plan: **`~/.claude/plans/foamy-forging-dahl.md`** (read it to resume). -Memory: `thothii-ui-redesign-inprogress.md`. **D + E merged @ `0eeb3f7` (pushed); B + C @ `b056ff3` -(live-verified); F implemented + committed (live check deferred to G). All on `main`, not yet pushed.** +Memory: `thothii-ui-redesign-inprogress.md`. **D + E @ `0eeb3f7`, B + C @ `b056ff3`, F @ `cef9ae4` +all pushed to origin. A (Resume menu + stall diagnosis/hardening) DONE, uncommitted. Only G remains.** - **D — DONE** (`c12bdcd`): session display `name` = 3-5 Italian keywords via **YAKE** (no LLM), derived in `tht session new` (CLI layer); `create_session` core unchanged (`name=None` default). @@ -114,12 +114,20 @@ Memory: `thothii-ui-redesign-inprogress.md`. **D + E merged @ `0eeb3f7` (pushed) disciplines 2-3, Phase-1 single-pick) + the `CLAUDE.md` gate note. Gate JS **33/33**, harness 269. **Live verification (model actually uses `reviewer_select`+decision, no follow-up gate, decision in `review_decisions.jsonl`) deferred to G** — it is model-behavior-dependent. -- **A (pending, riskiest):** Resume command + **FIX the resume cold-start stall** (open item #1). -- **G (later):** cross-model behavior matrix (Qwen3.6 / GLM 5.2 / Deepseek V4 / others) — also the - home for F's live verification. +- **A — DONE** (uncommitted): **A1** — `SessionMenu` gains a **Resume** item (gated to + `status!=="finalized" && !archived`), wired in `AppShell` to the existing `doResume` → `POST + /sessions/:id/resume`. 3 tests (`SessionMenu.test.tsx`); frontend **96/96**, tsc clean. **A2** — + diagnosis-first clean-room repro shows the **resume cold-start stall NO LONGER reproduces on pi + 0.79.4** (8/8 chained into the tool calls, fresh + partway; GLM 5.2 now narrates AND emits + `tht session show`+`read SKILL.md` in-turn). The earlier narrate-and-stop predates the pi upgrade. + Defense-in-depth applied: `RIPRENDI_KICKOFF` hardened to force the in-turn tool call (gate test + + live regression 2/2). The cross-model angle (weaker/older models) lives in **G**. +- **G (last, separate):** cross-model behavior matrix (Qwen3.6 / GLM 5.2 / Deepseek V4 / others) — + also the home for **F's live check** and **A's cross-model resume robustness**. -**Next chunk:** **A** (Resume in the kebab + the resume cold-start stall fix, diagnosis-first). -**G** (cross-model, incl. F's live check) later. +**Status:** A-G core work done (D, E, B, C, F, A). **Remaining:** **G** (cross-model matrix, +incl. F's live single-select check + A's resume-on-weaker-models), plus a one-off manual Playwright +kebab→resume pass through the live UI (open item 2). ## Live verification + reviewer_select fix (2026-06-30, afternoon) @@ -190,13 +198,14 @@ Full session management modeled on Claude's UI, all three layers: `docs/superpowers/plans/2026-06-29-session-management.md`. ### ⚠️ Open items / pending gates -1. **Resume cold-start STALLS (root cause confirmed 2026-06-30) — NOT fixed.** On - `/riprendi-sessione` GLM 5.2 narrates the bootstrap step ("esamino la sessione…") then ends - the turn without the tool call → Pi idle, unrecoverable from the UI (steer doesn't revive an - ended turn). The new-question path works, so it's resume-specific. Fix direction: harden the - resume kickoff/SKILL so the model chains into the tool call. Memory: - `thothii-resume-cold-start-stall.md`. **Do not advertise "Resume" as working until fixed.** - *(Scheduled as workstream A of the redesign plan above.)* +1. **Resume cold-start stall — RESOLVED on pi 0.79.4 (workstream A, 2026-06-30).** The earlier + narrate-and-stop (GLM 5.2 narrating the bootstrap step then ending the turn without the tool + call) **no longer reproduces**: a clean-room repro of the backend's exact resume handshake + chained into `tht session show`+`read SKILL.md` in-turn **8/8** (fresh + partway sessions). The + pi upgrade is the likely fix. Defense-in-depth: `RIPRENDI_KICKOFF` hardened to force the in-turn + tool call (gate test + live 2/2). Memory: `thothii-resume-cold-start-stall.md`. **Remaining:** + the cross-model angle (older/weaker models) is folded into **G**; a full Playwright kebab→resume + pass through the live UI is still worth one manual run (item 2). 2. **Full Playwright live-stack verification (MANUAL, not yet run).** 3. **Minor backlog (non-blocking):** explicit id-traversal guard in `delete_session` (today gated by `load_session`); `close_session` could reuse `_save_touched` (DRY); diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index 29885232..022b00ed 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -118,6 +118,7 @@ export function AppShell() { doResume(s.id)} onView={() => setPanelSession(s)} onRename={() => setRenameTarget(s)} onMove={(g) => move(s, g)} diff --git a/frontend/src/shell/SessionMenu.test.tsx b/frontend/src/shell/SessionMenu.test.tsx new file mode 100644 index 00000000..e15771fc --- /dev/null +++ b/frontend/src/shell/SessionMenu.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { vi } from "vitest"; +import { SessionMenu } from "./SessionMenu"; +import type { SessionSummary } from "../api/types"; + +const base: SessionSummary = { + id: "s1", status: "open", question: "q", summary: null, + created_at: "2026-01-01T00:00:00Z", updated_at: null, author: null, + name: null, group: null, archived: false, +}; + +function renderMenu(over: Partial, onResume = vi.fn()) { + render( + , + ); + return onResume; +} + +test("Resume shows for an open, non-archived session and calls onResume", async () => { + const onResume = renderMenu({ status: "open", archived: false }); + await userEvent.click(screen.getByRole("button", { name: /session actions/i })); + await userEvent.click(await screen.findByText("Resume")); + expect(onResume).toHaveBeenCalledTimes(1); +}); + +test("Resume is hidden for a finalized session", async () => { + renderMenu({ status: "finalized" }); + await userEvent.click(screen.getByRole("button", { name: /session actions/i })); + await screen.findByText("Split view"); // menu is open + expect(screen.queryByText("Resume")).not.toBeInTheDocument(); +}); + +test("Resume is hidden for an archived session", async () => { + renderMenu({ status: "open", archived: true }); + await userEvent.click(screen.getByRole("button", { name: /session actions/i })); + await screen.findByText("Split view"); // menu is open + expect(screen.queryByText("Resume")).not.toBeInTheDocument(); +}); diff --git a/frontend/src/shell/SessionMenu.tsx b/frontend/src/shell/SessionMenu.tsx index d44c2937..70b2dd60 100644 --- a/frontend/src/shell/SessionMenu.tsx +++ b/frontend/src/shell/SessionMenu.tsx @@ -5,6 +5,7 @@ import type { SessionSummary } from "../api/types"; interface Props { session: SessionSummary; groups: string[]; + onResume: () => void; onView: () => void; onRename: () => void; onMove: (group: string) => void; @@ -16,7 +17,10 @@ interface Props { const itemCls = "flex w-full cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground"; -export function SessionMenu({ session, groups, onView, onRename, onMove, onNewGroup, onArchiveToggle, onDelete }: Props) { +export function SessionMenu({ session, groups, onResume, onView, onRename, onMove, onNewGroup, onArchiveToggle, onDelete }: Props) { + // Resume re-enters the workflow; only meaningful while the session is still live + // (the backend returns 409 for finalized/archived, matching this guard). + const canResume = session.status !== "finalized" && !session.archived; return ( + {canResume && ( + Resume + )} Split view Rename diff --git a/harness/.pi/extensions/gate/__tests__/gate_resume_kickoff.test.js b/harness/.pi/extensions/gate/__tests__/gate_resume_kickoff.test.js new file mode 100644 index 00000000..d21a9b54 --- /dev/null +++ b/harness/.pi/extensions/gate/__tests__/gate_resume_kickoff.test.js @@ -0,0 +1,21 @@ +const test = require("node:test"); +const assert = require("node:assert"); +const { createFakePi } = require("./fake_pi_runtime.js"); +const installGate = require("../../tht-gate.js").default ?? require("../../tht-gate.js"); + +// Workstream A (defense-in-depth): the resume cold-start stall (model narrates step 1 +// then ends the turn without the tool call) no longer reproduces on pi 0.79.4, but the +// resume kickoff is hardened to force the in-turn tool call so weaker/older models don't +// regress into a dropped turn. + +test("the resume kickoff injects the bootstrap steps and forces in-turn action", async () => { + const { pi } = createFakePi(); + installGate(pi); + await pi.emit("input", { source: "rpc", text: "/riprendi-sessione 2026-06-30-000000-x" }); + const injected = await pi.emit("before_agent_start", { systemPrompt: "" }); + const text = injected?.systemPrompt ?? ""; + assert.match(text, /tht session show/); // the resume kickoff is the one injected + // hardening: act now, in this same turn; do not stop after merely stating intent. + assert.match(text, /in QUESTO stesso turno/i); + assert.match(text, /NON terminare il turno/i); +}); diff --git a/harness/.pi/extensions/tht-gate.js b/harness/.pi/extensions/tht-gate.js index 926d4921..96772680 100644 --- a/harness/.pi/extensions/tht-gate.js +++ b/harness/.pi/extensions/tht-gate.js @@ -104,6 +104,9 @@ const RIPRENDI_KICKOFF = "comando di shell ne' `/skill:...`).\n" + "3. Determina l'ultima fase completata dai fatti persistiti (le decisioni sono la verita': " + "cio' che non e' registrato non e' avvenuto) e riprendi da li'.\n" + + "Esegui i passi 1-2 ORA, in QUESTO stesso turno, chiamando subito i tool (`bash` per " + + "`tht session show`, `read` per la skill): NON limitarti a dichiarare l'intenzione e NON " + + "terminare il turno prima di aver chiamato i tool.\n" + "Valgono le stesse regole non negoziabili: una domanda per volta, conferma esplicita, tool " + "reviewer_*, niente phase advance/reopen o decision add da shell, una fase alla volta.";