Skip to content

Fable 5.1 effort-marker correlation fails when a turn has more than one user message #237

Description

@iceteaSA

Symptom

A Fable 5.1 turn dies with a user-visible 400 carrying an internal message:

Missing or invalid internal Fable 5.1 effort anchor

Recurring, not a one-off — 8 occurrences across 3 sessions in one week, in four shapes, all EffortMarkerCorrelationError:

count message
4 Fable 5.1 effort marker correlation failed: expected N, found M
2 Multiple internal Fable 5.1 effort markers on one user boundary
2 Missing or invalid internal Fable 5.1 effort anchor

Root cause

buildEffortRequestPlan pushes the anchor onto the user message that is current when the messages hook runs (effort-history.ts:409, currentUser.parts.push(...)), and stamps that message's id as anchor.boundary.

Validation at lowering time then requires the anchor to sit on the last user message:

// effort-history.ts:636-646
if (
  requestPlan.markerCount > 0 &&
  (anchors.length !== 1 ||
    anchorMessageIndex !== lastUserMessageIndex ||   // <-- this
    !anchor ||
    anchor.scope !== requestPlan.scope)
) throw new EffortMarkerCorrelationError('Missing or invalid internal Fable 5.1 effort anchor')

The invariant assumed is "exactly one user message at the current boundary." That holds for a plain chat turn and breaks as soon as the host puts a second user message at the same boundary — after which the anchor is on an earlier user message and the check fails.

Evidence

From the failing session (ids abbreviated):

13:48:37  user       "Do a work laptop sync, ORW as well"      <- operator prompt
13:48:37  user       <external-context source="sibling-session" …>   <- second user message, same second
13:48:37  assistant  ok
13:49:10  assistant  ERR   <- tool continuation, no new user message
13:49:43  user       <task id="…" state="completed">           <- another injected user message
13:49:43  assistant  ok
13:49:58  assistant  ERR   <- tool continuation

Both failures are the second provider call within a turn (tool continuation), in a turn whose boundary carries two user messages. Neither injected message is flagged synthetic or ignored — they are ordinary user messages as far as the plugin can see.

The Multiple internal Fable 5.1 effort markers on one user boundary variant is the same root cause seen from the other side: more than one user message at a boundary means more than one marker-bearing candidate.

Why it matters

The fail-closed itself is behaving as designed — refusing before cache shaping, signing and transport is the right call versus shipping a bad prefix. Two things make it worth fixing anyway:

  1. The trigger is benign. A trailing user message carrying no marker does not corrupt the effort timeline the plan describes, so the refusal costs a turn for nothing.
  2. The failure is diagnostically dark. effortMarkerFailureResponse (index.ts:322) synthesises a 400 for the user, and the throw happens before the dump path, so there is no log line and no dump artifact. Reconstructing the above required the host database. At minimum this path should log the mismatch it refused on (anchor boundary, last user id, marker count) — a fail-closed nobody can diagnose gets worked around instead of fixed.

Not fixed by f9c867d

f9c867d bounds and orders desktop notice identities. These injected user messages are not desktop notices, and the strict check is still present on current main (effort-history.ts:639).

Suggested direction

Tolerate user messages appended after the plan was built, as long as they carry no markers — i.e. require the anchor to be on the last user message the plan knew about, rather than the last user message in the lowered history. Holes, reordering, mutation, and scope/digest mismatch should keep failing exactly as they do now.

Environment: this is heaviest in multi-seat setups where session-to-session frames and subagent completion frames are delivered as ordinary user messages, but any host that appends a second user message at a turn boundary will hit it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions