* [AI] fix(memory-wiki): gracefully handle unparsable YAML frontmatter in vault scans (#96125)
toWikiPageSummary now catches YAML parse errors during vault-wide scans
(compile/lint/query/status) and returns a degraded WikiPageSummary with
frontmatterError set instead of letting the error propagate through
readPageSummaries -> compileMemoryWikiVault, which previously caused one
bad page to crash the entire vault.
Edit paths (apply, chatgpt-import) still throw on bad frontmatter to
prevent silent metadata loss on write.
- markdown.ts: add frontmatterError field to WikiPageSummary, extract
splitWikiFrontmatterBlock, catch YAML.parse in toWikiPageSummary
- lint.ts: add invalid-frontmatter lint code, skip frontmatter-derived
checks when frontmatterError is present
- markdown.test.ts: test degraded parse preserves body, empty frontmatter
- lint.test.ts: test invalid-frontmatter lint on broken page, healthy
page unaffected
Related to #96125
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(memory-wiki): isolate invalid frontmatter scans
* fix(memory-wiki): keep malformed lint reports fail-closed
* fix(memory-wiki): reject non-mapping frontmatter
* fix(memory-wiki): validate generated index targets
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
* 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>
* fix(imessage): detect MiniMax mm: namespaced reasoning tags in reflection guard
The iMessage reflection guard's THINKING_TAG_RE only matched bare
`<think>/<thought>` tags, so MiniMax's `<mm:think>` namespaced reasoning
tags (and Anthropic's `antml:` prefix) were not recognized as reflected
assistant content. When such reasoning leaked back inbound, the guard
treated it as a normal user message instead of dropping it, allowing
recursive echo amplification.
Accept the known reasoning-tag namespace prefixes `(?:antml:|mm:)?` on
the think/thought alternatives, mirroring the shared reasoning-tag
contract in src/shared/text/reasoning-tags.ts (PR #93767). This is the
remaining channel-monitor complement of #93767 for the iMessage path;
the optional prefix is a pure recognition enhancement with no change to
the bare-tag, code-region exemption, or other reflection patterns.
Verified by driving the real detectReflectedContent export:
<mm:think>secret</mm:think>visible -> isReflection=true (was false)
<think>...</think> -> isReflection=true
<think>secret</think> -> isReflection=true (no regression)
"thinking about minimax algorithms" -> isReflection=false (no false-positive)
`<mm:think>x</mm:think>` in code -> isReflection=false (code exemption kept)
* chore: retrigger CI for real behavior proof check
* refactor(imessage): tighten reflection guard coverage
---------
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
* fix(moonshot): bound video description JSON response reads
The Moonshot video description endpoint used an unbounded await res.json()
to parse the media understanding response. Route through
readProviderJsonResponse (16 MiB cap) to match the bound already in
place for other media understanding providers (xai, openrouter).
AI-assisted.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(moonshot): add bounds and malformed-JSON coverage for video description
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
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.
* fix(signal): guard containerRestRequest JSON.parse against malformed responses
Wrap JSON.parse(text) in containerRestRequest with try/catch to prevent a malformed Signal REST container response from throwing an unhandled SyntaxError.
On parse failure, throw a descriptive Error. The success body is already bounded by readProviderTextResponse (16 MiB cap, D1 protection).
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
* fix(signal): add real HTTP server proof for malformed JSON guard
Starts a local node:http server returning malformed JSON, then calls
containerRestRequest against it. This exercises the actual changed
try/catch code path through the real fetch stack (no mock override).
Proof output:
PASS malformed JSON: throws Error :: type=Error
PASS malformed JSON: message describes malformed JSON
PASS malformed JSON: NOT raw SyntaxError
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
* test(signal): remove committed proof script
---------
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Add sendTarget param to createFeishuReplyDispatcher that overrides
the to: field for visible sends. Fix streaming card delivery target
by stripping routing prefixes (user:, chat:) before the Feishu
streaming API call. Route streaming card reply metadata through
suppressed sendReplyToMessageId.
Closes#83730
* fix(slack): expose sender bot status in context
* fix(slack): expose sender bot status in context
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(googlechat): expose sender bot status in context
* fix(googlechat): expose sender bot status in context
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(discord): expose sender bot status in context
* fix(discord): expose sender bot status in context
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(memory-wiki): strip fenced code blocks before wikilink extraction
extractWikiLinks runs OBSIDIAN_LINK_PATTERN against full markdown
including fenced code blocks and inline code spans, causing false
positive 'Broken wikilink target' warnings for bash [[...]] test
syntax and Scala generics inside code blocks.
Strip fenced code blocks and inline code before running the wikilink
regex to eliminate code-block false positives while preserving real
wikilinks in prose.
Fixes#97945
* fix(memory-wiki): strip fenced code blocks before wikilink extraction
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>