Describe the bug
When Think’s proactive context-overflow guard compacts the conversation before a step, beforeStep still receives the original, uncompacted event.messages.
If beforeStep performs an additive message override based on that context:
override beforeStep(ctx: PrepareStepContext): StepConfig {
return {
messages: [
...ctx.messages,
{ role: "user", content: "Additional per-step context" }
]
};
}
Think treats the returned messages as authoritative and does not apply the compacted messages. Consequently, the upcoming model request uses the original history again, effectively discarding the proactive compaction.
This only occurs when:
- Proactive compaction successfully fires.
beforeStep returns a messages override.
Proactive compaction works normally when beforeStep does not return messages.
To Reproduce
- Configure a Think agent with session compaction and a low proactive threshold:
override contextOverflow = {
proactive: {
maxInputTokens: 10,
headroom: 0.9
}
};
- Override
beforeStep to append context to the messages it receives:
override beforeStep(ctx: PrepareStepContext): StepConfig {
return {
messages: [
...ctx.messages,
{ role: "user", content: "Additional per-step context" }
]
};
}
-
Run a multi-step turn where the first model step reports input usage above the proactive threshold—for example, a tool-call followed by another model step.
-
Observe that:
- Session compaction runs.
- A
chat:context:compacted event is emitted.
beforeStep receives the original messages without the compaction summary.
- The next model request contains the original history plus the additional message, rather than the compacted history plus that message.
The relevant control flow currently appears to be:
const guarded = await this._maybeProactiveContextCompact(event);
const result = await this.beforeStep(event);
const base = result == null ? {} : result;
const baseMessages = (base as { messages?: unknown }).messages;
const withMessages =
guarded && baseMessages === undefined
? { ...base, messages: guarded }
: base;
Because _maybeProactiveContextCompact returns a new message array without mutating event.messages, beforeStep(event) receives the old array. When beforeStep returns messages, guarded is then ignored.
Expected behavior
When proactive compaction succeeds, beforeStep should receive a prepare-step context containing the compacted messages:
const guarded = await this._maybeProactiveContextCompact(event);
const stepEvent = guarded
? { ...event, messages: guarded }
: event;
const result = await this.beforeStep(stepEvent);
This would preserve the documented override behavior: beforeStep could still replace or extend the messages, but its input would be the history Think actually intends to send after compaction.
This also matches the existing source comment:
Proactive context guard runs first so beforeStep sees the recompacted messages and can still override them if it wants to.
Version:
@cloudflare/think: 0.17.0
agents: 0.22.0
Describe the bug
When Think’s proactive context-overflow guard compacts the conversation before a step,
beforeStepstill receives the original, uncompactedevent.messages.If
beforeStepperforms an additive message override based on that context:Think treats the returned
messagesas authoritative and does not apply the compacted messages. Consequently, the upcoming model request uses the original history again, effectively discarding the proactive compaction.This only occurs when:
beforeStepreturns amessagesoverride.Proactive compaction works normally when
beforeStepdoes not returnmessages.To Reproduce
beforeStepto append context to the messages it receives:Run a multi-step turn where the first model step reports input usage above the proactive threshold—for example, a tool-call followed by another model step.
Observe that:
chat:context:compactedevent is emitted.beforeStepreceives the original messages without the compaction summary.The relevant control flow currently appears to be:
Because
_maybeProactiveContextCompactreturns a new message array without mutatingevent.messages,beforeStep(event)receives the old array. WhenbeforeStepreturnsmessages,guardedis then ignored.Expected behavior
When proactive compaction succeeds,
beforeStepshould receive a prepare-step context containing the compacted messages:This would preserve the documented override behavior:
beforeStepcould still replace or extend the messages, but its input would be the history Think actually intends to send after compaction.This also matches the existing source comment:
Version:
@cloudflare/think:0.17.0agents:0.22.0