practice polish: no phantom wake, Practice header tag, honest meta
- needsContinuation: a persisted problem-pose ack (terminate:true ended the turn deliberately) owes no continuation — reloads after a pose no longer wake a phantom turn; error results stay owed like any interrupted tool. - Chat header: practice sessions swap the phase chip for a quiet Practice tag (no probe→teach arc applies) and hide the coverage ring (practice never extracts — an ever-filling ring would lie). - ChatSummary gains pendingProblem (activeProblemOf at projection); the sidebar meta says 'Problem waiting' for a practice row with a posed problem. Kind threads through Chat/[id] page. - Home picker chip gains Clear (picks were otherwise irremovable).
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
concludedByDismissalPair,
|
||||
newProblemDismissedMarker,
|
||||
newThinkingMessage,
|
||||
needsContinuation,
|
||||
newUserMessage,
|
||||
pendingProblemDismissalOf,
|
||||
toLlmMessages,
|
||||
@@ -262,3 +263,42 @@ describe("toLlmMessages (practice shapes)", () => {
|
||||
assert.equal(out[1]!.role, "toolResult");
|
||||
});
|
||||
});
|
||||
|
||||
// ── the continuation fold's practice carve-out ──────────────────────────────
|
||||
|
||||
describe("needsContinuation (a persisted pose ack owes nothing)", () => {
|
||||
it("a record ending on a problem-pose ack does NOT wake a continuation", () => {
|
||||
// The pose's terminate:true ended that turn deliberately; the
|
||||
// ack persists (unlike a question's placeholder). Without the
|
||||
// carve-out, a reload after a posed problem would wake a
|
||||
// phantom turn (owner design, 2026-10-04).
|
||||
assert.equal(needsContinuation(problemPose()), false);
|
||||
});
|
||||
|
||||
it("a mixed batch (conclude ack then pose ack) still owes nothing — the pose is terminal", () => {
|
||||
const messages = [
|
||||
...problemPose("call_p0", "the earlier one"),
|
||||
concludeCall("solved"),
|
||||
{
|
||||
id: "msg_r_conclude",
|
||||
role: "toolResult",
|
||||
toolCallId: "call_conclude_solved",
|
||||
toolName: PRACTICE_CONCLUDE_TOOL,
|
||||
content: [{ type: "text", text: CONCLUDE_ACK_TEXT }],
|
||||
details: null,
|
||||
isError: false,
|
||||
timestamp: 4,
|
||||
} as LhMessage & ToolResultMessage,
|
||||
...problemPose("call_p2", "the next one"),
|
||||
];
|
||||
assert.equal(needsContinuation(messages), false);
|
||||
});
|
||||
|
||||
it("an ERROR result is not a pose — the agent loop continues past it, so a record ending there (an abort) is owed the wake like any interrupted tool", () => {
|
||||
const messages = [...problemPose("call_pe")];
|
||||
const err = messages[1] as LhMessage & ToolResultMessage;
|
||||
err.isError = true;
|
||||
err.content = [{ type: "text", text: "A problem is already active." }];
|
||||
assert.equal(needsContinuation(messages), true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ function item(overrides: Partial<ChatListItem>): ChatListItem {
|
||||
messageCount: 4,
|
||||
phase: "probe",
|
||||
pendingQuestion: false,
|
||||
pendingProblem: false,
|
||||
busy: false,
|
||||
...overrides,
|
||||
};
|
||||
@@ -135,4 +136,9 @@ describe("SessionRow practice badge (practice sessions, 2026-10-04)", () => {
|
||||
const html = render(item({ busy: true }));
|
||||
assert.match(html, /Teacher is working…/);
|
||||
});
|
||||
|
||||
it("a practice row with a posed problem says the problem is waiting", () => {
|
||||
const html = render(item({ kind: "practice", pendingProblem: true }));
|
||||
assert.match(html, /Problem waiting ·/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user