Skip to content
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ changes accumulate. Track in-flight protocol changes via PRs touching

### Fixed

- `chat/usage` reducer now updates the matching turn in `turns` when the
`turnId` refers to a completed (non-active) turn, rather than ignoring the
action.
- Corrected the `ACTION_INTRODUCED_IN` entries for `annotations/set`,
`annotations/removed`, `annotations/entrySet`, and `annotations/entryRemoved`
from `0.3.0` to `0.4.0`. The annotations channel first shipped in the
Expand Down
6 changes: 6 additions & 0 deletions clients/go/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file.
published tools by re-dispatching `SessionActiveClientSetAction` with its
full, updated entry.

### Fixed

- `ApplyActionToChat` now updates the matching turn in `Turns` when a
`ChatUsageAction` targets a completed (non-active) turn, rather than
ignoring the action.

## [0.4.0] — 2026-06-19

Implements AHP 0.4.0.
Expand Down
17 changes: 12 additions & 5 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,12 +472,19 @@ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) R
return tc
})
case *ahptypes.ChatUsageAction:
if state.ActiveTurn == nil || state.ActiveTurn.Id != a.TurnId {
return ReduceOutcomeNoOp
if state.ActiveTurn != nil && state.ActiveTurn.Id == a.TurnId {
usage := a.Usage
state.ActiveTurn.Usage = &usage
return ReduceOutcomeApplied
}
usage := a.Usage
state.ActiveTurn.Usage = &usage
return ReduceOutcomeApplied
for i := range state.Turns {
if state.Turns[i].Id == a.TurnId {
usage := a.Usage
state.Turns[i].Usage = &usage
return ReduceOutcomeApplied
}
}
return ReduceOutcomeNoOp
case *ahptypes.ChatReasoningAction:
return updateResponsePart(state, a.TurnId, a.PartId, func(p *ahptypes.ResponsePart) {
if r, ok := p.Value.(*ahptypes.ReasoningResponsePart); ok {
Expand Down
6 changes: 6 additions & 0 deletions clients/kotlin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump
published tools by re-dispatching `StateActionSessionActiveClientSet` with its
full, updated entry.

### Fixed

- `chatReducer` now updates the matching turn in `turns` when a
`StateActionChatUsage` targets a completed (non-active) turn, rather than
ignoring the action.

## [0.4.0] — 2026-06-19

Implements AHP 0.4.0.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -975,10 +975,17 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when
is StateActionChatUsage -> {
val a = action.value
val activeTurn = state.activeTurn
if (activeTurn == null || activeTurn.id != a.turnId) {
state
} else {
if (activeTurn != null && activeTurn.id == a.turnId) {
state.copy(activeTurn = activeTurn.copy(usage = a.usage))
} else {
val idx = state.turns.indexOfFirst { it.id == a.turnId }
if (idx < 0) {
state
} else {
val turns = state.turns.toMutableList()
turns[idx] = turns[idx].copy(usage = a.usage)
state.copy(turns = turns)
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions clients/rust/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ matching `## [X.Y.Z]` heading is missing from this file.
published tools by re-dispatching `SessionActiveClientSet` with its full,
updated entry.

### Fixed

- `apply_action_to_chat` now updates the matching turn in `turns` when a
`StateAction::ChatUsage` targets a completed (non-active) turn, rather than
ignoring the action.

## [0.4.0] — 2026-06-19

Implements AHP 0.4.0.
Expand Down
13 changes: 8 additions & 5 deletions clients/rust/crates/ahp/src/reducers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,13 +850,16 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu
}
StateAction::ChatToolCallContentChanged(a) => apply_tool_call_content_changed(state, a),
StateAction::ChatUsage(a) => {
let Some(active) = state.active_turn.as_mut() else {
if let Some(active) = state.active_turn.as_mut() {
if active.id == a.turn_id {
active.usage = Some(a.usage.clone());
return ReduceOutcome::Applied;
}
}
let Some(turn) = state.turns.iter_mut().find(|t| t.id == a.turn_id) else {
return ReduceOutcome::NoOp;
};
if active.id != a.turn_id {
return ReduceOutcome::NoOp;
}
active.usage = Some(a.usage.clone());
turn.usage = Some(a.usage.clone());
ReduceOutcome::Applied
}
StateAction::ChatReasoning(a) => update_response_part(state, &a.turn_id, &a.part_id, |p| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,17 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState {
}

case .chatUsage(let a):
guard var activeTurn = state.activeTurn, activeTurn.id == a.turnId else {
if var activeTurn = state.activeTurn, activeTurn.id == a.turnId {
activeTurn.usage = a.usage
var next = state
next.activeTurn = activeTurn
return next
}
guard let idx = state.turns.firstIndex(where: { $0.id == a.turnId }) else {
return state
}
activeTurn.usage = a.usage
var next = state
next.activeTurn = activeTurn
next.turns[idx].usage = a.usage
return next

case .chatReasoning(let a):
Expand Down
6 changes: 6 additions & 0 deletions clients/swift/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ the tag matches the version pinned in [`VERSION`](VERSION).
published tools by re-dispatching `StateAction.sessionActiveClientSet` with its
full, updated entry.

### Fixed

- `chatReducer` now updates the matching turn in `turns` when a
`StateAction.chatUsage` targets a completed (non-active) turn, rather than
ignoring the action.

## [0.4.0] — 2026-06-19

Implements AHP 0.4.0.
Expand Down
2 changes: 2 additions & 0 deletions clients/typescript/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ hotfix escape hatch.

### Fixed

- `chatReducer` now updates the matching turn in `turns` when a `chat/usage`
action targets a completed (non-active) turn, rather than ignoring the action.
- Hosted session summary caches now apply `_meta` updates from
`root/sessionSummaryChanged` notifications.
- Corrected the `ACTION_INTRODUCED_IN` entries for `annotations/set`,
Expand Down
19 changes: 13 additions & 6 deletions types/channels-chat/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,14 +502,21 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st
});


case ActionType.ChatUsage:
if (!state.activeTurn || state.activeTurn.id !== action.turnId) {
case ActionType.ChatUsage: {
if (state.activeTurn && state.activeTurn.id === action.turnId) {
return {
...state,
activeTurn: { ...state.activeTurn, usage: action.usage },
};
}
const idx = state.turns.findIndex(t => t.id === action.turnId);
if (idx === -1) {
return state;
}
return {
...state,
activeTurn: { ...state.activeTurn, usage: action.usage },
};
const turns = state.turns.slice();
turns[idx] = { ...turns[idx], usage: action.usage };
return { ...state, turns };
}

case ActionType.ChatReasoning:
return updateResponsePart(state, action.turnId, action.partId, part => {
Expand Down