feat(hooks): per-turn usageState on reply_payload_sending#89629
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 13, 2026, 4:50 AM ET / 08:50 UTC. Summary PR surface: Source +228, Tests +93, Docs +4. Total +325 across 8 files. Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path. Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Review detailsBest possible solution: Retry the Codex review after fixing the execution failure. Do we have a high-confidence way to reproduce the issue? Unclear. The review failed before ClawSweeper could establish a reproduction path. Is this the best way to solve the issue? Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction. AGENTS.md: unclear because the file could not be read completely. Codex review notes: model internal, reasoning high; reviewed against 7bd533a80e9d. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +228, Tests +93, Docs +4. Total +325 across 8 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
301834c to
a7ca7f5
Compare
a7ca7f5 to
79ed4ad
Compare
…ffort delivery Addresses review feedback on openclaw#89629. 1) Provider-limit resolution no longer defaults to OAuth. getProviderUsageLimits and getProviderUsageLimitsCached resolved with `credentialType ?? "oauth"`, and the agent-runner snapshot call passed no credential type, so an api-key OpenAI turn could borrow cached OAuth/ChatGPT usage windows. Drop the "oauth" default (missing credential type => no OpenAI usage provider) and thread the turn's authMode through at the call site. Adds provider-usage.limits.test.ts covering api-key/no-credential (no fetch), oauth/token (resolves), and non-OpenAI. 2) usageState is documented as best-effort, present only on live dispatcher delivery. Routed durable and recovered queue replays re-run this hook as a stateless transform over the original payload (see QueuedDeliveryPayload); a point-in-time usage snapshot is not stateless and would replay stale after a restart, so it is intentionally omitted there. Consumers must treat the field as optional. Co-Authored-By: Claude Opus 4.8 <[email protected]>
a3b31df to
69a8974
Compare
…ffort delivery Addresses review feedback on openclaw#89629. 1) Provider-limit resolution no longer defaults to OAuth. getProviderUsageLimits and getProviderUsageLimitsCached resolved with `credentialType ?? "oauth"`, and the agent-runner snapshot call passed no credential type, so an api-key OpenAI turn could borrow cached OAuth/ChatGPT usage windows. Drop the "oauth" default (missing credential type => no OpenAI usage provider) and thread the turn's authMode through at the call site. Adds provider-usage.limits.test.ts covering api-key/no-credential (no fetch), oauth/token (resolves), and non-OpenAI. 2) usageState is documented as best-effort, present only on live dispatcher delivery. Routed durable and recovered queue replays re-run this hook as a stateless transform over the original payload (see QueuedDeliveryPayload); a point-in-time usage snapshot is not stateless and would replay stale after a restart, so it is intentionally omitted there. Consumers must treat the field as optional. Co-Authored-By: Claude Opus 4.8 <[email protected]>
4b7b510 to
750fbf6
Compare
…on reply_payload_sending Attach a per-turn execution snapshot to the reply_payload_sending hook as `usageState`, so a plugin (or the future in-core /usage renderer) can render a per-response usage readout as a pure consumer of the contract — no side calls. Recorded in agent-runner, consumed in dispatch. Fields: provider, model, resolvedRef/requested, reasoningEffort, fastMode, fallbackUsed, is_override (overrideSource), authMode, compactionCount, contextTokenBudget, token usage, turn cost (USD), duration, owning agentId/sessionId, chatType, the agent identity (name/emoji), and the active provider's subscription `limits` windows. reply_payload_sending is the one reply hook universal across every surface (incl. the Codex app-server, which emits no llm_output/agent_end), so it is the correct harness-agnostic place for per-turn usage. Limits are resolved by a core-internal non-blocking SWR helper (src/infra/provider-usage.limits.ts) and attached to the snapshot — no new plugin-SDK accessor. All fields optional/additive. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ffort delivery Addresses review feedback on openclaw#89629. 1) Provider-limit resolution no longer defaults to OAuth. getProviderUsageLimits and getProviderUsageLimitsCached resolved with `credentialType ?? "oauth"`, and the agent-runner snapshot call passed no credential type, so an api-key OpenAI turn could borrow cached OAuth/ChatGPT usage windows. Drop the "oauth" default (missing credential type => no OpenAI usage provider) and thread the turn's authMode through at the call site. Adds provider-usage.limits.test.ts covering api-key/no-credential (no fetch), oauth/token (resolves), and non-OpenAI. 2) usageState is documented as best-effort, present only on live dispatcher delivery. Routed durable and recovered queue replays re-run this hook as a stateless transform over the original payload (see QueuedDeliveryPayload); a point-in-time usage snapshot is not stateless and would replay stale after a restart, so it is intentionally omitted there. Consumers must treat the field as optional. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…limits requestShaping.authMode is the auth *mechanism* (e.g. "auth-profile" for a configured auth profile), not the credential *type* resolveUsageProviderId expects. Gating limits on it === "oauth"/"token" dropped 📊 for legit OAuth (profile-based) turns. Map it: api-key/aws-sdk -> no usage provider (cannot borrow cached oauth windows); oauth/token/auth-profile/absent -> usage-eligible, with the real credential re-checked at fetch time. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…eState context.used_tokens / pct_used were derived from the snapshot's aggregate prompt total (cacheRead+cacheWrite+input). Over a multi-call tool-loop turn that is the run AGGREGATE, overstating window occupancy (often past 100%) so a footer's context gauge pins full while /status shows the true ~7%. Add two optional fields to PluginHookReplyUsageState and populate them in the reply path: - contextUsedTokens: the final call's prompt size (agentMeta.promptTokens) = real end-of-turn occupancy, a point-in-time state, not the aggregate. - lastUsage: the final model call's usage only (vs `usage`, the turn aggregate), so a footer can render the last exchange's i/o + cache. Both optional and additive; consumers fall back to the aggregate when absent (correct for single-call turns). Renderer consumption lands separately (openclaw#89835). Co-Authored-By: Claude Opus 4.8 <[email protected]>
A missing per-turn authMode was mapped to "oauth", so an OpenAI api-key turn that arrived without an explicit auth mechanism could resolve and display ChatGPT subscription windows that aren't its own — served straight from the 60s limits cache, which the "re-checked at fetch time" guard does not cover. Treat a genuinely absent signal as non-eligible (same as api-key): no usage provider resolves and the footer omits limit windows. Present mechanisms are unchanged — oauth/auth-profile/token stay eligible, and only OpenAI is gated on the credential type so other providers are unaffected. A real oauth/profile turn always carries its mechanism; one arriving blank is an upstream tagging bug to fix at the source. Inverts the now-incorrect "absent => oauth-eligible" test into regression coverage for the absent/api-key case. Co-Authored-By: Claude Opus 4.8 <[email protected]>
750fbf6 to
7a86ed4
Compare
|
Landed via rebase onto main.
Thanks @Marvinthebored! |




What
Attaches a per-turn execution snapshot to the
reply_payload_sendingplugin hook as an optionalusageStatefield, so hook consumers can render per-response model/usage/context metadata without extra side calls.agent-runnerrecords the snapshot after the model turn.dispatchattaches it to the livereply_payload_sendingevent only when it can correlate the payload to the exact agent run id.Contract
usageStateis best-effort live turn metadata. It is omitted for durable/recovered replay and any delivery path without exact run correlation.Fields are optional/additive. Existing hook consumers are unaffected.
Per-turn atoms included:
Provider subscription/limit windows are intentionally not included. The current run path does not carry exact credential identity for every provider/account, so attaching quota windows here would risk showing another profile's limits.
Review response
Maintainer repair on 2026-06-13:
usageState.limitscontract from this PR.onAgentRunStartinto the delivery hook, so normal live replies still get the snapshot even when the caller did not providereplyOptions.runId.docs/plugins/hooks.md.Verification
node scripts/run-vitest.mjs src/auto-reply/dispatch.test.ts src/auto-reply/reply/reply-usage-state.test.ts src/plugins/wired-hooks-reply-payload-sending.test.ts src/auto-reply/reply/dispatch-from-config.test.tstsgo -p tsconfig.core.json --declaration false --singleThreaded --checkers 1git diff --check.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt 'Review PR 89629 after maintainer repair. Focus on exact run correlation for reply usageState, provider limit scoping, public hook docs, and regressions introduced by the repair commit.'Real behavior proof
Behavior or issue addressed: This PR exposes live per-turn
usageStateonreply_payload_sendingso hook consumers can render model, usage, context, cost, timing, routing, and identity atoms without side calls. Provider subscription/limit windows are intentionally not part of this PR after maintainer repair because the live run path does not carry exact credential identity for every provider/account.Real environment tested: The contributor tested the
usageStatecontract on a live OpenClaw gateway with downstream footer consumption in Telegram and Discord. The PR itself has no in-core UX; the proof uses a hook consumer to show the live contract data reaching real channel replies.Exact steps or command run after this patch: Built and deployed the branch to a live OpenClaw gateway, sent real replies through Telegram and Discord, and observed a downstream
reply_payload_sendingfooter consumer render the liveusageStateatoms.Evidence after fix: Live downstream screenshots from the contributor show the footer rendered from
usageStatedata: Telegram https://raw.githubusercontent.com/Marvinthebored/openclaw/proof-assets/pr-proof/proof_telegram2.png and Discord https://raw.githubusercontent.com/Marvinthebored/openclaw/proof-assets/pr-proof/proof_discord1.jpegObserved result after fix: Real Telegram and Discord replies included the footer generated from the hook
usageStatedata. After maintainer repair, the supported contract is the same live per-turnusageStatedelivery minus provider limit windows; uncorrelated and durable replay paths intentionally omit it.What was not tested: No fresh maintainer-run live gateway screenshot was captured after removing the provider-limit field. The live proof covers downstream
usageStatedelivery; scoped maintainer tests cover the repaired exact-run correlation and omission behavior.