Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions packages/opencode/src/effort-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {

const MAX_EFFORT_MARKERS = 512
const MAX_TRACKED_EFFORT_PLANS = 1024
const MAX_TRACKED_EFFORT_PLAN_HISTORY = 4096
const MARKER_CHECK_HEX_LENGTH = 32
const SCOPE_HEX_LENGTH = 32
const MESSAGE_ID_PATTERN = '[A-Za-z0-9_-]{1,128}'
Expand Down Expand Up @@ -569,7 +570,11 @@ export function applyOpenCodeEffortMarkers(

if (!hasCandidate) {
if (!requestPlan) return { found: 0, inserted: 0 }
if (requestPlan.markerCount !== 0) {
// A full trim is the `trimmedPrefix === expectedTransitions.length` case:
// every transition was consumed by the host's prefix trim, so fold them all
// into the baseline. Without a resolvable plan the request is untrusted and
// stays fail-closed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Total loss bypasses validation

If downstream processing removes every marker while retaining an earlier or otherwise non-suffix message, hasCandidate is false even though the body is not a valid complete prefix trim. This branch then folds every transition into the baseline and sends the request with the wrong effort instead of rejecting the non-prefix loss. Verify that the retained body ends at the correlated plan's current boundary before accepting a full trim.

Knowledge Base Used:

if (requestPlan.markerCount !== 0 && !expectedTransitions) {
throw new EffortMarkerCorrelationError(
`Fable 5.1 effort marker correlation failed: expected ${requestPlan.markerCount}, found 0`,
)
Expand Down Expand Up @@ -743,19 +748,33 @@ export function applyOpenCodeEffortMarkers(

export class OpenCodeEffortPlanTracker {
private readonly plans = new Map<string, OpenCodeEffortMarkerPlan>()
// A message's plan is re-recorded whenever the host trims the history prefix
// between provider calls, so the single per-message slot can be overwritten
// while a header generated from the previous plan is still in flight. Keep a
// bounded identity-keyed history so resolveHeader can still find that plan.
private readonly history = new Map<string, OpenCodeEffortMarkerPlan>()

record(plan: OpenCodeEffortMarkerPlan): void {
const key = this.key(plan.sessionId, plan.messageId)
this.plans.delete(key)
this.plans.set(key, {
const stored = {
...plan,
transitionTokens: [...plan.transitionTokens],
})
}
this.plans.delete(key)
this.plans.set(key, stored)
const encoded = encodeOpenCodeEffortPlan(stored)
this.history.delete(encoded)
this.history.set(encoded, stored)
while (this.plans.size > MAX_TRACKED_EFFORT_PLANS) {
const oldest = this.plans.keys().next().value
if (typeof oldest !== 'string') break
this.plans.delete(oldest)
}
while (this.history.size > MAX_TRACKED_EFFORT_PLAN_HISTORY) {
const oldest = this.history.keys().next().value
if (typeof oldest !== 'string') break
this.history.delete(oldest)
}
}

clear(sessionId: string, messageId: string): void {
Expand Down Expand Up @@ -784,10 +803,7 @@ export class OpenCodeEffortPlanTracker {
value: string | undefined,
): OpenCodeEffortMarkerPlan | undefined {
if (!value) return undefined
for (const plan of this.plans.values()) {
if (encodeOpenCodeEffortPlan(plan) === value) return plan
}
return undefined
return this.history.get(value)
}

private key(sessionId: string, messageId: string): string {
Expand Down
120 changes: 118 additions & 2 deletions packages/opencode/src/tests/effort-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EFFORT_MARKER_PREFIX,
encodeOpenCodeEffortPlan,
markOpenCodeEffortTransitions,
type OpenCodeEffortMarkerPlan,
OpenCodeEffortPlanTracker,
} from '../effort-history.ts'

Expand Down Expand Up @@ -282,14 +283,15 @@ describe('OpenCode Fable 5.1 effort markers', () => {
},
],
}
expect(() =>
expect(
applyOpenCodeEffortMarkers(
missingAnchorBody,
true,
encodeOpenCodeEffortPlan(plan as NonNullable<typeof plan>),
plan as NonNullable<typeof plan>,
),
).toThrow('Fable 5.1 effort marker correlation failed: expected 2, found 0')
).toEqual({ found: 0, inserted: 0 })
expect(missingAnchorBody.output_config).toEqual({ effort: 'high' })

const body = {
model: 'claude-fable-5-1',
Expand Down Expand Up @@ -323,6 +325,120 @@ describe('OpenCode Fable 5.1 effort markers', () => {
])
})

test('folds a full trim into the last consumed transition effort when the plan resolves', () => {
const messages = [
user('msg_low', 'ses_full_trim_fold', 'claude-fable-5-1', 'low'),
user('msg_medium', 'ses_full_trim_fold', 'claude-fable-5-1', 'medium'),
user('msg_high', 'ses_full_trim_fold', 'claude-fable-5-1', 'high'),
user('msg_current', 'ses_full_trim_fold', 'claude-fable-5-1', 'high'),
]
const plan = markOpenCodeEffortTransitions(messages)
expect(plan).not.toBeNull()
expect(plan?.markerCount).toBe(2)
const body = {
model: 'claude-fable-5-1',
output_config: { effort: 'low' },
messages: [
{ role: 'user', content: [{ type: 'text', text: 'msg_current' }] },
],
}

expect(
applyOpenCodeEffortMarkers(
body,
true,
encodeOpenCodeEffortPlan(plan as NonNullable<typeof plan>),
plan as NonNullable<typeof plan>,
),
).toEqual({ found: 0, inserted: 0 })
// The folded baseline is the last consumed transition's effort ('high'),
// not the plan's original baseline ('low').
expect(body.output_config).toEqual({ effort: 'high' })
})

test('rejects a full trim without a resolvable plan', () => {
const messages = [
user('msg_low', 'ses_full_trim_untrusted', 'claude-fable-5-1', 'low'),
user('msg_high', 'ses_full_trim_untrusted', 'claude-fable-5-1', 'high'),
]
const plan = markOpenCodeEffortTransitions(messages)
expect(plan).not.toBeNull()
const body = {
model: 'claude-fable-5-1',
output_config: { effort: 'high' },
messages: [
{ role: 'user', content: [{ type: 'text', text: 'msg_high' }] },
],
}

expect(() =>
applyOpenCodeEffortMarkers(
body,
true,
encodeOpenCodeEffortPlan(plan as NonNullable<typeof plan>),
),
).toThrow('Fable 5.1 effort marker correlation failed: expected 1, found 0')
})

test('resolves a plan header after the same message is re-recorded with a trimmed timeline', () => {
const full = [
user('msg_low', 'ses_overwrite', 'claude-fable-5-1', 'low'),
user('msg_medium', 'ses_overwrite', 'claude-fable-5-1', 'medium'),
user('msg_high', 'ses_overwrite', 'claude-fable-5-1', 'high'),
user('msg_current', 'ses_overwrite', 'claude-fable-5-1', 'high'),
]
const fullPlan = markOpenCodeEffortTransitions(full)
expect(fullPlan?.markerCount).toBe(2)
const tracker = new OpenCodeEffortPlanTracker()
tracker.record(fullPlan as NonNullable<typeof fullPlan>)
const header = encodeOpenCodeEffortPlan(
fullPlan as NonNullable<typeof fullPlan>,
)

// The host trims the history prefix between provider calls; the same current
// message is re-marked with a shorter timeline and overwrites the slot.
const trimmedPlan = markOpenCodeEffortTransitions(full.slice(2))
expect(trimmedPlan?.messageId).toBe('msg_current')
expect(trimmedPlan?.markerCount).toBe(0)
tracker.record(trimmedPlan as NonNullable<typeof trimmedPlan>)

// The header generated from the full plan must still resolve.
expect(tracker.resolveHeader(header)).toEqual(
fullPlan as NonNullable<typeof fullPlan>,
)
})

test('evicts the oldest plan history entry once the cap is exceeded', () => {
const tracker = new OpenCodeEffortPlanTracker()
const historySize = () =>
(tracker as unknown as { history: Map<string, unknown> }).history.size
const plan = (index: number): OpenCodeEffortMarkerPlan => ({
scope: 'a'.repeat(32),
baseline: 'high',
markerCount: 0,
digest: index.toString(16).padStart(64, '0'),
transitionTokens: [],
sessionId: `ses_history_${index}`,
messageId: `msg_history_${index}`,
})
const cap = 4096
const oldest = plan(0)
const newest = plan(cap)

for (let index = 0; index <= cap; index += 1) {
tracker.record(plan(index))
}

expect(historySize()).toBe(cap)
// Insertion-order eviction: the oldest plan is gone, the newest survives.
expect(
tracker.resolveHeader(encodeOpenCodeEffortPlan(oldest)),
).toBeUndefined()
expect(tracker.resolveHeader(encodeOpenCodeEffortPlan(newest))).toEqual(
newest,
)
})

test('rejects non-prefix transition loss even with the resolved plan', () => {
const messages = [
user('msg_low', 'ses_non_prefix', 'claude-fable-5-1', 'low'),
Expand Down
12 changes: 6 additions & 6 deletions packages/opencode/src/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10464,7 +10464,7 @@ describe('Fable 5.1 request-scoped effort history', () => {
])
})

test('fails locally when request-correlated effort markers cannot be validated', async () => {
test('folds a resolvable full trim and fails locally when effort markers cannot be validated', async () => {
await useTempAccountFile(
createFallbackStorage({
accounts: [],
Expand Down Expand Up @@ -10603,11 +10603,11 @@ describe('Fable 5.1 request-scoped effort history', () => {
'Missing or invalid internal Fable 5.1 effort request plan',
)

// A full trim with a resolvable plan is the trimmedPrefix === length case:
// every transition was consumed, so the request proceeds with the folded
// baseline instead of failing closed.
const missingAllMarkers = await send('ses_effort_missing_all', [])
expect(missingAllMarkers.status).toBe(400)
expect((await missingAllMarkers.json()).error.message).toBe(
'Fable 5.1 effort marker correlation failed: expected 1, found 0',
)
expect(missingAllMarkers.status).toBe(200)

const duplicateTransition = await send('ses_effort_duplicate_transition', [
transitionMarker,
Expand All @@ -10617,7 +10617,7 @@ describe('Fable 5.1 request-scoped effort history', () => {
expect((await duplicateTransition.json()).error.message).toBe(
'Multiple internal Fable 5.1 effort markers on one user boundary',
)
expect(messagesCalled).toBe(false)
expect(messagesCalled).toBe(true)
})
})

Expand Down
Loading