* fix(system-prompt): move exec-approval + Authorized Senders below cache boundary
buildExecApprovalPromptGuidance (channel-varying: CLI /approve vs native
approval UI) and buildUserIdentitySection / "## Authorized Senders"
(owner/identity-varying, dropped when minimal) were emitted into the static,
cacheable prefix *above* SYSTEM_PROMPT_CACHE_BOUNDARY. They fork the cacheable
prefix at ~token 1,460, invalidating client-side prefix caching for the rest of
the ~17.8K-token system prompt — causing minutes-long cold prefills on local
models (llama.cpp / MLX / Ollama) after any channel-varying cron/heartbeat turn.
Follow-on to #40256, which moved Messaging/Voice/Reactions below the boundary
but missed these two. Relocates both into the existing below-boundary
channel-guidance block. Pure cache-stability change with no behavioural change
(the guidance is position-independent for correctness). Extends the boundary
test to assert both sections sit below SYSTEM_PROMPT_CACHE_BOUNDARY.
Measured on a local deployment: shared cross-channel prefix grew ~1,460 ->
~15,486 tokens; post-cron interactive turns went from minutes to ~10s.
Fixes#98261
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6Hz9UEpxQ4W3d8XecvupH
* fix(system-prompt): suppress relocated exec-approval line under tool_call_style override
Addresses review (clawsweeper): the exec-approval guidance lived inside the
`tool_call_style` fallback, so a provider override of that section previously
replaced it. Relocating it below the boundary emitted it unconditionally, which
changed behaviour for providers/plugins that override `tool_call_style`. Gate the
relocated line on the absence of a `tool_call_style` override, restoring the
original "override replaces the whole section" contract. Extends the
provider-override test to assert the default approval line is suppressed when
the section is overridden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6Hz9UEpxQ4W3d8XecvupH
* fix(system-prompt): tighten cache boundary proof
* fix(system-prompt): tighten cache boundary proof
* fix(system-prompt): tighten cache boundary proof
---------
Co-authored-by: headbouyJB <23249268+headbouyJB@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(device-pairing): don't churn requestId on subset re-requests
A reconnect that re-requested a subset of an already-pending device
pairing request still superseded it with a fresh requestId. This is the
root cause of the "unknown requestId" failures during device approval:
1. A TUI connect files a broad scope-upgrade request; the owner copies its
id from `openclaw devices list`.
2. `openclaw devices approve <id>` reconnects as a CLI probe that only needs
`operator.pairing` — a subset of the pending scopes.
3. That subset re-request superseded the pending request with a new id, so
the originally-listed id no longer existed and approve failed.
Refresh the existing request in place when the incoming request only asks
for roles/scopes a single pending request (same device key + role) already
covers. Escalations that request *more* than the pending request still
supersede with a fresh id, so the requestId stays bound to at least the
scope snapshot the owner saw (the existing security-motivated behavior and
its test are preserved).
AI-assisted (Claude Code).
* fix(device-pairing): align subset pairing with scope coverage
* fix(utils): keep reply directive ids unicode-safe
* test(utils): catch trailing lone surrogate in hasUnpairedSurrogate helper
Per PR #96938 review: the test helper missed a high surrogate at end of
string because charCodeAt(out-of-bounds) returns NaN, and NaN comparisons
are always false. Guard bounds explicitly so the truncation test actually
proves what it claims, plus two cases pinning helper behavior.
* chore(utils): rerun QA smoke to confirm memory-index timeout flake on #96938
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Gap finding: tempdir-8h9i0j1k, tempdir-9i0j1k2l
Replace bare fs.mkdtemp/mkdtempSync calls in 4 test files with the
shared createSuiteTempRootTracker() helper, adding proper cleanup
where none existed.
Background: the project has standardized on shared temp dir helpers
since 88b87d893d (withTempDir) -> 13df67ebc8 (createSuiteTempRootTracker)
-> 06431fd99b / #87298 (CI guard + docs guidance). This PR continues
that migration for the highest-risk files.
Files changed:
- src/skills/lifecycle/install-fallback.test.ts: variable overwrite risk
- src/auto-reply/reply/session-hooks-context.test.ts: helper leaks temp dirs
- src/auto-reply/reply/abort.test.ts: createAbortConfig leaks root dir
- src/auto-reply/inbound.test.ts: test cases without cleanup
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cron): clear agentTurn thinking override when patched with null
Cron agentTurn patches could clear model/fallbacks/toolsAllow overrides by
sending an explicit null, but thinking had no clear path: the patch schema and
normalizer dropped thinking:null before it reached the merge logic, and the
payload merge only handled string values. Blanking the Thinking/Effort field in
the Cron Control UI therefore silently preserved the old value.
Add thinking:null support across the patch schema, exported type, normalizer,
and payload merge (mirroring model). The Control UI now sends an explicit clear
for model/thinking when an edited job blanks a previously stored override, and
the CLI gains --clear-thinking for parity with --clear-model.
* docs(cron): document --clear-thinking beside sibling clear flags
postJson reads the Anthropic OAuth token endpoint response body with
an unbounded await response.text(). A compromised or hijacked OAuth
endpoint can stream an arbitrarily large body and force the runtime
to buffer the entire payload before parsing — an OOM/DoS vector.
Replace with readResponseWithLimit at 16 MiB cap + TextDecoder decode
to match the sibling bounded-read pattern (provider-http-errors.ts:308).
Co-authored-by: Claude <noreply@anthropic.com>
* fix(doctor): merge colliding model-ref map keys instead of dropping
When two retired model-ref keys in a models map upgrade to the same
current ref, rewriteModelRefMapKeys dropped the second entry silently
and still logged a success line, so doctor --fix lost one entry's
params/streaming/alias/agentRuntime tuning while falsely reporting it
as upgraded.
Mirror the sibling provider-models-array merge path (#90047): deep-merge
the colliding entries preserving disjoint fields, keep the existing
value deterministically on a true field conflict, and push a merge or
collision change-log line instead of dropping.
* fix(doctor): block prototype keys when merging colliding model-ref map entries
mergeModelRefMapEntries copied arbitrary incoming config fields into a
plain object, so a colliding model entry carrying a __proto__ field could
mutate the merged entry prototype during legacy config migration. Skip
isBlockedObjectKey fields before assigning or recursing, matching the
shared config merge guard. Disjoint-field merging is unchanged.
* fix(doctor): preserve canonical model migration values
* fix(doctor): filter blocked keys on both merge sides and report retired source key
* fix(doctor): preserve colliding retired model settings
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
* fix(plugins): apply output text transforms to toolcall_delta and toolcall_end events
toolcall_delta and toolcall_end events bypassed the output replacement
pipeline, leaking masked tokens (e.g. PII placeholders) into tool call
arguments and external systems.
Closes#97761
* fix(plugins): transform nested tool call arguments in output replacements
Add recursive transformToolCallArgumentText to handle strings, arrays,
and nested objects in tool call arguments, not just the name field.
* fix(plugins): keep tool names unchanged and cover result path
- Remove toolCall.name transformation to prevent breaking tool routing
- Add arguments transform to stream.result()/done.message via
transformContentText for tool-call content blocks
* fix(plugins): narrow argument transform to toolCall content blocks only
Only transform arguments on content blocks with type=toolCall to avoid
touching non-tool-call blocks that happen to have an arguments field.
* test(plugins): assert stream.result() tool-call content block is transformed
* test(plugins): fix result-path fixture to actually contain toolCall block
The previous fixture used makeAssistantMessage('final') which produces
a plain text content block, not a toolCall block. The stream.result()
assertion was vacuously passing.
* style(plugins): fix type cast in result-path test assertion
* fix(agents): preserve tool argument transforms after repair
* refactor(agents): keep tool transform boundary narrow
---------
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
When no chat DM pairing channels are configured, `openclaw pairing list`
(no channel argument) threw `Channel required ... (expected one of: )`
with an empty enum that reads like a bug. Users who hit this are usually
trying to approve a TUI/device or scope-upgrade request, which lives under
`openclaw devices`, not `openclaw pairing` (which only handles chat DM
pairing).
- Guard the channel hint in help text and errors so an empty channel list
no longer renders a bare `()` / `(expected one of: )`.
- When no pairing channels exist, redirect to `openclaw devices list` /
`openclaw devices approve` instead of failing opaquely.
AI-assisted (Claude Code).
Expose `core/doctor/auth-profiles` as a default-disabled structured Doctor lint check.
This maps the existing auth-profile health diagnostics into structured findings while preserving legacy Doctor repair behavior. The check remains opt-in through `--only core/doctor/auth-profiles` or `--all`; default `doctor --lint` behavior is unchanged.
Validation:
- focused auth-profile/hints/contribution Vitest passed
- changed-file oxfmt and oxlint passed
- core tsgo passed
- git diff --check passed
- exact-head hosted CI/Testbox gates passed
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
* fix(usage): reject inverted startDate-endDate range
* chore: retrigger CI for real behavior proof check
* refactor(usage): resolve validated date ranges once
---------
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
The 64 KiB inter-event SSE sanitize buffer added in #96989 rejects a single
legitimate event larger than 64 KiB — e.g. a large gpt-5.5 reasoning summary on
the openai-chatgpt-responses API — throwing "SSE response exceeded max buffer
size (65536 bytes) without event boundary" and failing the whole request. The
default ChatGPT-subscription gpt-5.5 path is unusable (present in v2026.6.11-beta.1).
Decouple the two bounds: keep the non-OK error-body cap tight at 64 KiB
(SSE_NONOK_BODY_MAX_BYTES), and raise the inter-event sanitize buffer to the same
16 MiB ceiling as the JSON-synthesis path. The guard still trips on a genuinely
boundary-less (hostile/broken) stream, just not on a real large event.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A present-but-unreadable openclaw.json (for example EACCES after a sudo
command leaves it root-owned) returns an empty best-effort fallback
snapshot. A later full-file write (openclaw doctor, including the update
doctor pass) then serialized a skeletal config over the still-rich file,
dropping gateway.mode and bricking gateway startup.
The fallback base has no raw bytes and an empty resolved config, so the
existing size-drop and gateway-mode-removed guards never fired, and the
update path passes allowConfigSizeDrop=true.
Record the read failure on the snapshot (readError) and treat an
unreadable base as an always-blocking write reason
(unreadable-config-before-write) that allowConfigSizeDrop does not bypass.
The allowDestructiveWrite escape hatch and the rejected-artifact path are
preserved, so explicit recovery still works and the blocked payload is
saved to openclaw.json.rejected.<timestamp>.
Refs #78493.
When a proxied Anthropic-Messages endpoint degrades a native tool_use block
into plain text, the assistant emits antml:namespaced XML tags
(<antml:invoke>, <antml:parameter>). These were not recognized by the
visible-text sanitizer, leaking tool-call XML into user-visible replies.
Added antml: (and mm:) namespace handling to:
- TOOL_CALL_QUICK_RE regex for fast pre-check
- TOOL_CALL_TAG_NAMES set for tag name recognition
- Tag name scanner to accept ':' in tag names
- TOOL_CALL_XML_PAYLOAD_START_RE for XML payload detection
- shouldDetectXmlPayload to include antml:invoke tags
Fixes#97750.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agents): skip pre-prompt precheck when context engine owns compaction
When a context engine advertises ownsCompaction=true (e.g. lossless-claw),
skip the pre-prompt preemptive overflow precheck entirely. The engine
already manages the context budget through assemble() and its own
compaction lifecycle — the built-in precheck is redundant and causes
false-positive overflow errors for CJK-heavy sessions due to its
conservative token estimation formula.
Safety is preserved: if the model's actual context limit is exceeded,
the model API returns an error that the outer overflow-compaction retry
loop handles normally.
* fix(agents): assert non-null context engine in precheck skip log
* test(context-engine): cover owning precheck contract
* fix(agents): preserve precheck after context assembly failure
---------
Co-authored-by: Josh Lehman <josh@martian.engineering>
* test(globals): add unit tests for global CLI flag state helpers
Add unit tests for shouldLogVerbose, logVerbose, and logVerboseConsole
functions in src/globals.ts to verify verbose logging behavior.
Tests cover:
- shouldLogVerbose returns true when isVerbose is true
- shouldLogVerbose returns true when file log level is debug
- shouldLogVerbose returns false when both are false
- logVerbose does not log when shouldLogVerbose is false
- logVerbose logs to console when isVerbose is true
- logVerboseConsole does not log when isVerbose is false
- logVerboseConsole logs to console when isVerbose is true
* ci: trigger re-review
* test(globals): cover verbose logger behavior
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Preserve shared reasoning suppression by default while letting Telegram opt into durable reasoning payloads only when it has a deliverable reasoning lane. Covers persistent /reasoning on, separate reasoning stream lanes, and progress-stream suppression.\n\nVerification:\n- node scripts/run-vitest.mjs src/auto-reply/reply/dispatch-from-config.test.ts extensions/telegram/src/bot-message-dispatch.test.ts\n- git diff --check upstream/main...HEAD\n- .agents/skills/autoreview/scripts/autoreview --mode branch --base upstream/main --stream-engine-output\n- CI run 28411526182 green, including QA Smoke CI and check-test-types\n- Real behavior proof run 28411676681 passed\n\nPR: #97875