Answering a check advances the lesson (#9) #13

Merged
crowmaki merged 1 commit from fix/answer-advances-lesson into main 2026-08-29 20:48:26 +00:00
Owner

Closes #9.

Problem

POST .../check-answer graded the answer, recorded objective_evidence and appended the learner line — and stopped. No tutor_turn op was created, so the tutor never reacted and the lesson stalled until the learner typed something like "I got it!" by hand.

Fix

The grade now starts the follow-up tutor_turn op with a new synthetic learner intent __advance__ (mirroring __begin__). The tutor opens with the verdict — confirm a correct answer, explain the misconception for a wrong one, give prerequisite-level remediation for "I don't know" (REQ-008.2/3) — and continues the lesson through the normal checkpoint_saved transact, on every outcome.

Backend (plugin/)

  • check-answer keeps its synchronous 200 grade contract; the response gains an optional follow_up_op_id.
  • Idempotency: the tutor op's key is derived as {check-answer-key}:advance, so a replayed grade (same key → early return of the recorded result, which still carries the original op id) never starts a second turn. A second answer with a different key is refused before any op starts.
  • When a tutor turn is already in flight for the module the follow-up is skipped (follow_up_op_id: null): the learner keeps the inline feedback and can simply send a message (single-flight discipline preserved).
  • _tutor_handler returns the existing tutor line for a superseded ADVANCE op (BEGIN-style guard: a tutor line already follows the last check_answer line), so a retry never duplicates the verdict.
  • say rejects __advance__ as reserved text; tutor.detect_challenge short-circuits it (no model call — exactly one TutorTurn call per follow-up).
  • tutor.INSTRUCTIONS document the new intent: verdict first, then continue teaching / offer homework when every objective is covered.

Desktop (desktop/src/)

  • CheckAnswerResult carries follow_up_op_id; ModulePage.onAnswered adopts it as the tracked say op, reusing all existing machinery (pending label, transcript poll, transcript + course refetch on success).
  • The retrySay stale-text fallback never fires for an advance op, so a failed verdict turn can never resend an old learner message.

Docs

  • docs/specs/pedagogy-plugin/design.md: check-answer route line notes the follow-up turn; module/route docstrings updated (REQ-008.2/3, issue #9).

Verification

  • pytest tests/ -q: 453 passed; the 5 failures are the known pre-existing Windows failures on main (4× test_install.py, test_events.py::test_list_courses_skips_invalid_directory_names).
  • New/updated API tests: follow-up turn on a correct answer (single model call, learner_message == "__advance__", checkpoint_saved from the verdict), skip-while-in-flight, superseded-retry no-op, reserved-text refusal, and every existing check-answer flow now exercises the follow-up.
  • npm run typecheck, npm test (21/21), npm run build (lint gate ok, 315 KiB).
Closes #9. ## Problem `POST .../check-answer` graded the answer, recorded `objective_evidence` and appended the learner line — and stopped. No `tutor_turn` op was created, so the tutor never reacted and the lesson stalled until the learner typed something like "I got it!" by hand. ## Fix The grade now starts the follow-up `tutor_turn` op with a new synthetic learner intent `__advance__` (mirroring `__begin__`). The tutor opens with the verdict — confirm a correct answer, explain the misconception for a wrong one, give prerequisite-level remediation for "I don't know" (REQ-008.2/3) — and continues the lesson through the normal `checkpoint_saved` transact, on **every** outcome. ### Backend (`plugin/`) - `check-answer` keeps its synchronous 200 grade contract; the response gains an optional `follow_up_op_id`. - Idempotency: the tutor op's key is derived as `{check-answer-key}:advance`, so a replayed grade (same key → early return of the recorded result, which still carries the original op id) never starts a second turn. A second answer with a different key is refused before any op starts. - When a tutor turn is already in flight for the module the follow-up is **skipped** (`follow_up_op_id: null`): the learner keeps the inline feedback and can simply send a message (single-flight discipline preserved). - `_tutor_handler` returns the existing tutor line for a superseded `ADVANCE` op (BEGIN-style guard: a tutor line already follows the last `check_answer` line), so a retry never duplicates the verdict. - `say` rejects `__advance__` as reserved text; `tutor.detect_challenge` short-circuits it (no model call — exactly one `TutorTurn` call per follow-up). - `tutor.INSTRUCTIONS` document the new intent: verdict first, then continue teaching / offer homework when every objective is covered. ### Desktop (`desktop/src/`) - `CheckAnswerResult` carries `follow_up_op_id`; `ModulePage.onAnswered` adopts it as the tracked say op, reusing all existing machinery (pending label, transcript poll, transcript + course refetch on success). - The `retrySay` stale-text fallback never fires for an advance op, so a failed verdict turn can never resend an old learner message. ### Docs - `docs/specs/pedagogy-plugin/design.md`: check-answer route line notes the follow-up turn; module/route docstrings updated (REQ-008.2/3, issue #9). ## Verification - `pytest tests/ -q`: 453 passed; the 5 failures are the known pre-existing Windows failures on `main` (4× `test_install.py`, `test_events.py::test_list_courses_skips_invalid_directory_names`). - New/updated API tests: follow-up turn on a correct answer (single model call, `learner_message == "__advance__"`, `checkpoint_saved` from the verdict), skip-while-in-flight, superseded-retry no-op, reserved-text refusal, and every existing check-answer flow now exercises the follow-up. - `npm run typecheck`, `npm test` (21/21), `npm run build` (lint gate ok, 315 KiB).
POST .../check-answer graded the answer, recorded objective_evidence and
appended the learner line — and stopped. The tutor never reacted, so the
lesson stalled until the learner typed something like "I got it!" by hand.

The grade now starts the follow-up tutor_turn op with a new synthetic
learner intent __advance__ (mirroring __begin__): the tutor opens with the
verdict — confirm a correct answer, explain the misconception for a wrong
one, give prerequisite-level remediation for "I don't know" (REQ-008.2/3) —
and continues the lesson via the normal checkpoint_saved transact, on every
outcome.

- check-answer keeps its synchronous 200 grade contract and gains an
  optional follow_up_op_id; the derived idempotency key {key}:advance means
  a replayed grade never starts a second turn.
- When a tutor turn is already in flight the follow-up is skipped
  (follow_up_op_id null): the learner keeps the inline feedback and can
  simply send a message.
- The tutor handler returns the existing tutor line for a superseded
  ADVANCE op (BEGIN-style guard), so a retry never duplicates the verdict.
- say rejects __advance__ as reserved; detect_challenge short-circuits it
  (no model call).
- Desktop: ModulePage tracks follow_up_op_id like a say op (pending label,
  transcript poll, refetch on success); the stale-text retry fallback never
  fires for an advance op, so no old learner message is ever resent.

Closes #9
crowmaki deleted branch fix/answer-advances-lesson 2026-08-29 20:48:26 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crowmaki/hermes-learning!13
No description provided.