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:
amosblomqvist
2026-10-04 22:06:22 +03:00
parent e76e42f764
commit 9362baf36e
9 changed files with 101 additions and 7 deletions
+40
View File
@@ -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);
});
});