Three-phase session status: derived summaries for the picker

- core/phases.ts: Phase3 (probe/plan/teach) + pure derivations —
  phaseOfTree (point-in-time, from the stored plan tree: no nodes → probe,
  no occupied node → plan, occupied → teach) and phaseAt3 (monotonic
  transcript fold for the dividers: failed plan_init never marks a seam;
  phase only advances).
- contracts.ts: ChatSummary gains record-derived phase + pendingQuestion;
  ChatListItem adds liveness-merged busy.
- store/chats.ts listChats computes the record-derived fields (tree +
  pending question scan).
- api/chats route merges busy = turn running || running background job.

Lane crossing granted by master: contracts/store/api (schema arbitration).
Data-only inference: pre-plan planning reads as probe until the plan lands.
This commit is contained in:
Amos
2026-09-01 18:24:13 +03:00
parent cf3225fb23
commit 1c3d288d08
5 changed files with 241 additions and 5 deletions
+24 -1
View File
@@ -5,6 +5,7 @@
*/
import type { UIMessage } from "ai";
import type { Phase3 } from "./phases.ts";
// ─── Plan tree (vision §2 "formal plan tree") ────────────────────────────────
// A session-scoped dependency tree authored by the teacher via plan tools.
@@ -257,13 +258,35 @@ export interface ChatRecord {
tree?: PlanTree;
}
/** The list view's shape (messageCount instead of full messages). */
/** The list view's shape (messageCount instead of full messages) + the
* per-session status the picker renders. `phase` and `pendingQuestion`
* derive from the stored record (tree + messages); the store computes
* them in listChats. Liveness (`busy`) is NOT record-derived — the API
* route merges it in (see ChatListItem).
*
* Phase semantics (three phases, data-only): probe = no plan yet, plan =
* tree exists but no node occupied, teach = a node is occupied
* (core/phases.ts phaseOfTree).
*/
export interface ChatSummary {
id: string;
title: string;
createdAt: string;
updatedAt: string;
messageCount: number;
/** The session's current phase, derived from the stored plan tree. */
phase: Phase3;
/** An unanswered quiz/ask question is waiting for the learner. */
pendingQuestion: boolean;
}
/** The sidebar's session row: a summary + live liveness. The chats list
* route attaches `busy` from the turn-liveness registry and the job store
* (a running turn OR a background job = the session is working). The store
* itself can't see liveness (import law: store never imports server),
* hence the split at exactly the record/liveness seam. */
export interface ChatListItem extends ChatSummary {
busy: boolean;
}
// ─── Async sub-agent jobs (vision §2.5; plan.md Wave 0 protocol) ────────────