Pinned session-extension registries now remain the owner even when empty, preventing later active registry churn from leaking agent-owned extensions into the gateway surface.
Recover assistant turns that complete tool work without producing a visible final answer, while preserving intentional silent replies.
Use concrete tool-instance replay safety across embedded, Codex, and Copilot runtimes so unknown, mutating, async-started, and durable recall operations fail closed. Preserve genuine empty Codex final items without promoting commentary or tool-progress echoes.
Supersedes #90872. Thanks @fuller-stack-dev.
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
* fix(memory): accept local default model path migration
Treat the official local default embedding model's hf URI and downloaded GGUF path identities as equivalent so upgraded local memory indexes do not pause solely on path-format changes.
* fix(memory): satisfy local identity lint
Avoid filtered array tail access in the local model filename helper while preserving the same compatibility behavior.
* fix(memory): preserve local embedding identity aliases
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
* fix(reply): deliver final reply when queued follow-up claims session; scope dedupe to routed thread
Two core bugs caused composed replies to be silently dropped (no delivery,
no error) when a second message arrived in the same thread mid-run:
1. dispatch-from-config: ensureDispatchReplyOperation only kept the
dispatch-owned operation authoritative while it had no result. Once
runReplyAgent completed the operation to drain queued follow-ups, a
second same-thread inbound could claim the session and the first final
reply would try to re-acquire the lane instead of finishing delivery,
deadlocking behind the queued work. Keep the dispatch-owned operation
authoritative through final delivery.
2. reply-payloads-dedupe: messaging-tool reply dedupe compared only the
channel target, not the routed thread, so a send in one thread could
suppress a later reply in a different thread. Thread the routed thread
id through buildReplyPayloads + follow-up delivery and only fall back to
channel-only matching for providers without a thread-aware suppression
matcher when neither side carries thread evidence.
Adds regression tests; existing Telegram topic-suppression behavior is
preserved by gating the thread guard to providers lacking a plugin matcher.
* fix(reply): preserve threaded message delivery evidence
* fix(reply): dedupe final payloads by delivery route
* fix(slack): preserve native send thread evidence
* fix(reply): preserve explicit reply thread evidence
* fix(reply): align explicit reply route dedupe
* fix(reply): preserve delivery lane through final dispatch
* fix(mattermost): preserve threaded tool send routes
* chore(plugin-sdk): refresh API baseline
* fix(reply): align final delivery route dedupe
* fix(reply): gate followups on final delivery
* fix(reply): keep send receipts private
* fix(reply): infer implicit message provider
* fix(reply): align routed threading policy
* fix(reply): preserve queued delivery context
* fix(reply): hydrate queued system event routes
* fix(reply): hydrate queued execution routes
* fix(reply): scope final delivery barriers
* fix(slack): preserve DM target aliases
* fix(reply): mirror resolved source thread routes
* fix(mattermost): retain delayed delivery barrier
* fix(codex): separate message routing from tool policy
* fix(reply): consume normalized Slack DM targets once
* fix(slack): remove stale target alias
* style(reply): satisfy changed lint gates
* fix(mattermost): preserve explicit reply targets
* test: align Slack reply branch checks
* fix(reply): persist overflow summaries to admitted session
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Fixes#92207.
Normalize public memory artifacts at the memory host boundary so providers that omit agentIds produce an empty list instead of throwing during artifact cloning, sorting, or memory-wiki bridge import. The bridge now renders those artifacts with unknown agents while downstream consumers still receive stable array-shaped metadata.
Verification:
- node scripts/run-vitest.mjs src/plugins/memory-state.test.ts extensions/memory-wiki/src/bridge.test.ts --maxWorkers=1
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- Crabbox run_2a30de5d0a00 / cbx_3684cb0b7ea5: OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed
- GitHub PR checks clean on 19678ed60f
* Mark active main sessions during restart shutdown
* Type restart marker mock in close tests
* fix(gateway): preserve active run ownership across restart
* fix(gateway): preserve active runs across restart
* fix(gateway): close restart recovery edge cases
* fix(cron): preserve lifecycle ownership across restart
* fix(gateway): release rejected run contexts
* fix(gateway): preserve restart lifecycle ownership
* fix(cron): retain overlapping run ownership
* fix(agents): preserve restart terminal precedence
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Bounds default model browsing to configured/read-only discovery while preserving explicit full-catalog browsing. Reuses prepared plugin metadata and auth state without triggering external CLI discovery on the picker hot path, while retaining provider normalization and canonical runtime aliases.
Verified with focused model tests, official OpenAI and Anthropic transport suites, fresh live tool calls for both providers, a full build, AWS check:changed, remote Docker OpenAI tools E2E, and green PR CI.
Fixes#91809.
Co-authored-by: samson1357924 <98934496+samson1357924@users.noreply.github.com>
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 (#89835).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses review feedback on #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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Preserve the first native Kimi tool-call ID while rewriting repeated replay occurrences to deterministic OpenAI-style IDs and keeping paired tool results aligned. Moonshot responses-family behavior and providers that do not opt in remain unchanged.
Closes#51593
Co-authored-by: Pluviobyte <Pluviobyte@users.noreply.github.com>
* feat(moonshot): add Kimi K2.7 Code support
* test(moonshot): surface K2.7 live provider errors
* ci(live): accept Kimi key for Moonshot sweeps
* test(moonshot): verify K2.7 across API regions
* fix(cron): report SQLite storage path in cron.status instead of legacy jobs.json
The `cron.status` gateway response returned `storePath` pointing to the
legacy `jobs.json` path, but cron jobs are actually stored in the shared
SQLite state database. This misled operators and agents into looking for
a JSON file that no longer exists.
- Add `storage: "sqlite"` and `sqlitePath` fields to CronStatusSummary
- Mark legacy `storePath` as @deprecated (kept for backward compat)
- Update CLI warning to prefer sqlitePath over storePath
- Add regression assertions in read-ops test
Fixes#91766
* fix(macos): prefer sqlitePath in cron status display
* fix(macos): add sqlitePath to CronSchedulerStatus type
Since 5734193fdf ("fix(plugins): keep metadata snapshot memo fresh",
first shipped in v2026.5.18), the in-process plugin metadata snapshot
memo stores derived-registry results under a key recomputed from the
freshly built snapshot.index, while lookups key off the persisted-index
registry state. On installs where the registry resolves as "derived"
(persisted index absent or not covering the running checkout), the two
keys never match. Worse, the lookup-side adoption loop returns the most
recently stored registryState for the context, so two alternating call
shapes (e.g. the model-catalog build mixing workspace-scoped and global
lookups) each adopt the other shape's state, compute a key that was
never stored, and re-run the full plugin manifest scan - on every call,
forever. Chat /models (and each subsequent provider/model pick) pays
multiple full manifest scans plus all downstream snapshot-identity cache
invalidation per step, pinning a CPU core for seconds on every
interaction, in every chat channel.
Fix: store the memo under the exact memoKey/registryState the call
looked up by, instead of re-deriving a second key from snapshot.index.
Freshness is unchanged - the lookup context hash and the plugin metadata
lifecycle clears (install/reload/doctor) still own invalidation. The
now-unused index parameter of resolvePersistedRegistryMemoState is
removed.
Measured on the author's VPS (real plugin discovery, identical catalog
output of 263 entries on both sides): the full-discovery model catalog
build behind chat /models dropped from ~6.3s to ~0.3s (~21x), with
repeat snapshot lookups going from full rescans to memo hits.
Regression test: alternating derived call shapes must not re-scan
(red on main: 4 scans; green with this fix: 2).
* fix(thinking): apply Claude profile to anthropic-messages catalog rows
When a custom provider (e.g. `jdcloud-anthropic`) fronted Claude Opus over
the native anthropic-messages adapter, `--thinking xhigh` was silently
clamped to `off`. The thinking-profile dispatcher resolves bundled plugin
policy surfaces by exact provider id, so a renamed Anthropic-compatible
provider never reached the anthropic plugin's policy and `xhigh` was not
in the resulting profile.
`auto-reply/thinking.ts` already had a fallback keyed on
`context.api === "anthropic-messages"` that attached
`CLAUDE_FABLE_5_THINKING_PROFILE` for Fable models. Generalize it to use
`resolveClaudeThinkingProfile(modelId, params)` instead — the same
canonical helper the anthropic plugin uses — which still returns the Fable
profile for Fable models and now returns the correct Opus 4.7/4.8 profile
(with `xhigh`/`adaptive`/`max`) for Claude Opus regardless of provider id.
Non-Claude models on anthropic-messages routes still get the base
profile, and a Claude id on a non-Anthropic transport (e.g. an
openai-completions catalog row) is unaffected.
Fixes#91975
* fix(thinking): match native Anthropic includeNativeMax in fallback
Address ClawSweeper P2 review on #92053. The anthropic-messages fallback
in `resolveThinkingProfile` calls `resolveClaudeThinkingProfile` but
omits the `{ includeNativeMax: true }` option that the bundled anthropic
plugin uses (extensions/anthropic/provider-policy-api.ts:38,45).
For native-xhigh Claude families (Opus 4.7/4.8) this had no effect since
the native-xhigh branch already exposes `max`. But adaptive Claude
families that take the adaptive-default branch (e.g. claude-sonnet-4-6,
claude-opus-4-6) silently lost `max` parity on custom anthropic-messages
providers compared to native Anthropic policy.
Also add a regression test on `claude-sonnet-4-6` that verifies the
adaptive-branch path keeps `max` for custom providers.
* docs(thinking): document deliberate compat.xhigh bypass on anthropic-messages
Self-review surfaced a subtle behavior change worth documenting: when the
anthropic-messages fallback was generalized, non-Claude models on this
transport stop honoring catalog `compat.supportedReasoningEfforts: ["xhigh"]`
because they take the Claude base profile instead of falling through to the
later `catalogSupportsXHigh` upgrade path.
This is intentional — anthropic-messages does not carry a generic xhigh
contract; xhigh on this protocol is a Claude-family capability. Add an
inline comment at the resolver site and a regression test that locks the
suppression so the next reader (or a future patch) doesn't accidentally
restore the upgrade path.
* fix(thinking): extract Claude profile to leaf to break import cycle
The previous commits added a `resolveClaudeThinkingProfile` import from
`auto-reply/thinking.ts` to `plugin-sdk/provider-model-shared.ts`. The
shared barrel re-exports `provider-replay-helpers` and `plugins/types`,
which transitively reach back into `auto-reply` via the gateway server
methods chain — creating the madge cycle reported by
`check:madge-import-cycles`:
auto-reply/thinking.ts
-> ... -> plugin-sdk/provider-model-shared.ts
-> plugins/{config-schema, host-hooks, ...} -> plugins/types.ts
Move `BASE_CLAUDE_THINKING_LEVELS`, `isClaudeAdaptiveThinkingDefaultModelId`,
and `resolveClaudeThinkingProfile` to a new leaf module
`src/plugins/provider-claude-thinking.ts` whose only imports are
`@openclaw/llm-core` and the existing leaf `provider-thinking.types`.
`provider-model-shared.ts` continues to re-export both helpers so existing
consumers (`extensions/anthropic/*`, the public test surface) are
unaffected. `auto-reply/thinking.ts` now imports the leaf directly,
breaking the cycle.
* test(thinking): add live proof harness for #91975 anthropic-messages clamp
---------
Co-authored-by: wanglu241 <wanglu241@jd.com>
Allow an explicit canonical ClickClack enable/setup selection to record ClickClack in a nonempty plugin allowlist, while preserving unrelated allowlist rejection, denylist authority, and global plugin disablement.
Validated at source head 24af9d8e75 with focused regressions, built-CLI disposable-config E2E, security checks, and autoreview. Merged under owner authorization despite the two documented untouched-main agent-core baseline failures.
Gemma 4 models via openai-completions (vLLM, OpenRouter, etc.) were
silently losing in-turn reasoning_content during multi-turn tool replay.
Unlike DeepSeek/Xiaomi which 400 on the missing field, these providers
accept the request but produce degraded tool-call quality — arguments
collapse to {} and identical tool calls repeat.
Root cause: two places explicitly excluded Gemma 4 from reasoning replay:
1. shouldTrustReasoningContentReplayMetadata() in openai-transport-stream
skipped Gemma 4 by model ID, causing sanitizeCompletionsReasoningReplayFields
to strip reasoning_content before sending.
2. buildOpenAICompatibleReplayPolicy() in provider-replay-helpers forced
dropReasoningFromHistory=true for Gemma 4, discarding reasoning blocks
during history sanitization before they reached the transport layer.
Remove both exclusions. Gemma 4 on non-OpenAI providers now preserves
in-turn reasoning through the existing provider/metadata checks that
already gate this correctly for other reasoning models.
Co-authored-by: wangyk <prowangyankun@foxmail.com>