• v0.49.0 57f3f23738

    joeyr released this 2026-08-25 20:43:00 -05:00 | 67 commits to main since this release

    Canvas C6: list_discussion_entries. The tenant could read what a
    discussion ASKS and not a word of what anyone SAID — so a graded discussion
    that requires replying to two classmates was an assignment it could describe
    and not help with. The replies are the half that was missing.

    This plan declined discussions three times in one sentence and was wrong all
    three times.
    "Large free-text surfaces with no natural bound" was measured
    against pages (reversed in 0.43.0), against prompts (reversed in 0.44.0), and
    now against threads: the busiest thread on this account is 18 posts and 12,987
    characters, smaller than the syllabus this tenant already returns. The
    durable lesson is in CLAUDE.md already — measure a surface before declining
    it — and this is its third instance rather than a new one.

    Added

    • list_discussion_entries(course_id, topic_id) — every reply in one
      thread, in thread order. Takes the SAME topic_id as get_discussion, which
      now names it on any topic that has replies.

      Built on /view rather than /entries, which is the measurement that
      moved the design.
      One /view costs 0.1932 and returns the whole nested
      tree; /entries costs 0.3926 and returns only the top level. Half the
      price for twice the content. /entries hangs a recent_replies array off
      each entry that carried every child on the thread it was measured against —
      and "recent" is the API's word, not a promise. A tool built on it looks
      correct on a quiet thread and silently loses replies on a busy one.

      The tree is flattened, carrying depth and in_reply_to. A model
      answering "who replied to whom" from nested JSON has to hold the shape in
      mind; a flat list in thread order where each row names what it answers states
      the same relationship outright.

      Three truncations, reported separately, because they have different
      fixes: more_entries (thread longer than 100), deeper_replies (nested
      below 3), and per-entry message_truncated. A single flag would say
      something was missing without saying whether it was a person, a
      sub-conversation, or a paragraph.

    • CANVAS_THREAD_MAX_ENTRIES (100) and CANVAS_THREAD_MAX_DEPTH (3). A
      thread is the one Canvas surface whose size is a class's rather than a
      field's, so capping text bounds nothing. Depth is 3 rather than the
      observed 2 deliberately
      : every topic on this account is
      discussion_type: threaded, so Canvas imposes no ceiling and the observed
      depth is one class's habits rather than a limit.

    Fixed

    • A require_initial_post lock was being reported as a permission problem,
      and the advice was wrong in a way that costs someone an email to their
      instructor.
      A discussion set to hide replies until the student posts their
      own answers 403 with a body of exactly require_initial_post — measured
      against the one topic on this account that still has the lock closed. The
      shared 403 branch answered that with "this account is not permitted to read
      that": true about the request, false about the account, and the actual fix is
      thirty seconds of the student's own typing.

      CanvasNeedsInitialPost subclasses CanvasRefused, so every existing
      caller stays correct — the session is fine and the request was refused, which
      is what the parent promises — and only the advice changes. Responses carry
      needs_initial_post: true beside needs_reseed: false. The token is matched
      whole rather than by substring, and the body is never echoed into the error
      message
      : a 403 body on another path is an unknown quantity and error text
      travels.

    Privacy

    • Classmates' names are kept; their profiles are not. Canvas returns
      avatar_image_url, html_url, pronouns and anonymous_id beside each
      post, plus a participants roster covering people who never posted. All of
      it is dropped at the client boundary and user_name is kept, because a
      graded discussion asks the student to reply to named classmates and a tool
      that anonymises them returns a draft the user re-personalises line by line.

      Stated plainly because the reverse gets claimed later: this is a small
      reduction. The sensitive material in a discussion thread is the posts, and
      the name is attached to those either way. The boundary that does the real
      work is that entry text is never logged — counts, lengths and ids only,
      following get_discussion.

    Downloads