* fix(exec): avoid splitting surrogate pairs in approval display
Use the shared UTF-16 safe truncation helper for exec approval display caps so long sanitized command previews remain well-formed when the cutoff lands inside a surrogate pair.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(exec): sanitize malformed approval text
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* [AI] fix(auto-reply): strip stray punctuation before silent-reply token detection
When the model emits .NO_REPLY or *NO_REPLY* instead of bare NO_REPLY,
isSilentReplyText() failed to detect it because the regex requires
the text to consist only of the token with optional whitespace.
Add stripEdgeNonAlnum() that strips leading/trailing non-letter,
non-number characters (Unicode-aware) before the regex test. This
catches stray punctuation while preserving the existing safeguard
against suppressing mixed-content text.
Related to #98166
Co-Authored-By: Claude <noreply@anthropic.com>
* [AI] fix: strip only punct not emoji before silent-reply detection
Change stripEdgeNonAlnum to stripEdgePunct: strip only punctuation (\p{P}) instead of all non-letter/non-number characters. Emoji would be stripped by the former, causing false silent-reply on mixed content.
Related to #98166
Co-Authored-By: Claude <noreply@anthropic.com>
* [AI] fix: preserve exact custom-token match before edge-punct normalization
Try exact regex match first, then fall back to stripEdgePunct.
This preserves exact matching for custom tokens whose first/last
character is punctuation (e.g. *SILENT*, #QUIET#), while still
catching the common case of NO_REPLY wrapped in stray punctuation.
Add regression test for punctuation-edged custom tokens.
ClawSweeper: https://github.com/openclaw/openclaw/pull/98224#issuecomment-4846415801
Related to #98166
Co-Authored-By: Claude <noreply@anthropic.com>
* [AI] fix: handle whitespace-wrapped punctuation in silent-reply fallback
Trim whitespace before stripping edge punctuation in the fallback
path so variants like " .NO_REPLY " and "NO_REPLY. " are also
caught by the silent-reply detector.
Add regression coverage for whitespace-wrapped edge punctuation.
ClawSweeper: https://github.com/openclaw/openclaw/pull/98224#issuecomment-4846415801
Related to #98166
Co-Authored-By: Claude <noreply@anthropic.com>
* test(auto-reply): tighten silent-token boundaries
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(media): guard ffprobe JSON parse against malformed output
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: trigger CI re-run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: trigger CI re-run
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(media): validate ffprobe JSON shape
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Adds a default-disabled Doctor lint check for WhatsApp responsiveness pressure while keeping real cleanup in the existing doctor --fix path.
Verification:
- Focused WhatsApp responsiveness/contribution Vitest passed (71 tests).
- Changed-file oxfmt passed.
- Changed-file oxlint with core tsconfig passed.
- SDK surface report passed.
- SDK export sync passed.
- git diff --check passed.
- Selected source CLI proof passed earlier on this head: doctor --lint --json --only core/doctor/whatsapp-responsiveness exited 0, ran 1 check, and did not create <state>/identity/device.json.
- Exact-head hosted checks passed/skipped on 39f003931ff8cd93f945e75bf7a480cf2271c910; older cancelled proof/auto-response runs were superseded by passing current runs.
Maintainer notes:
- ClawSweeper platinum hermit / ready for maintainer review.
- Accepted the opt-in lint check id and warning copy on the PR.
- No public config/plugin/SDK/persisted data contract change; default doctor --lint remains unchanged.
Adds a default-disabled Doctor lint check for stale plugin-runtime symlinks while keeping real deletion in the existing legacy doctor --fix cleanup path.
Verification:
- Focused plugin-runtime-symlink/contribution Vitest passed (65 tests).
- Changed-file oxfmt passed.
- Changed-file oxlint with core tsconfig passed.
- SDK surface report passed.
- git diff --check passed.
- Exact-head hosted checks passed/skipped on 287d2230ee.
Maintainer notes:
- ClawSweeper platinum hermit / ready for maintainer look.
- Accepted the new opt-in lint check id and warning copy on the PR.
- Local prepare wrapper pnpm check still fails on the npm shrinkwrap guard, and the same failure reproduces on current origin/main; treated as baseline local gate noise, not PR-caused.
Keep CLI session reuse hashes keyed to stable session prompt facts while preserving per-turn room-event message-tool delivery for execution and MCP scoping.
Thread cliSessionBindingFacts from reply assembly through queued/followup CLI runs, make the stable CLI prompt delivery-neutral, and cover production-shaped room-event opts so forced message_tool_only delivery cannot poison binding identity.
Fixes#99372
Release note: Telegram/room-event CLI backends can resume sessions across interactive turns again instead of replaying full history after system-prompt invalidations.
Expose default-disabled Doctor lint findings for legacy plugin dependency state.
The check maps the existing safe cleanup-target detection into structured lint output while keeping real cleanup in the legacy doctor --fix path. Default doctor --lint remains unchanged; explicit --only and --all can include the new warning.
Validation:
- GitHub checks green on head 53c11a6f82
- ClawSweeper: proof sufficient, platinum hermit, ready for maintainer look
- Local post-rebase proof: focused Doctor Vitest, changed-file oxlint, changed-file oxfmt --check, and git diff --check all passed
Maintainer note: WSL maintainer wrapper path is unavailable on this host with Wsl/Service/CreateInstance/0x80072745, so this uses the steerable head-pinned squash path allowed by the current maintainer workflow docs.
* feat(signal): add target aliases
* fix(signal): canonicalize alias delivery targets
* fix(signal): harden alias target resolution
* test(signal): cover formatted media aliases
* test(signal): align approval alias proof with request shape
* Reuse Signal alias map for directory listing
* fix(subagents): match requesterSessionKey when controllerSessionKey differs in list filter
The listControlledSubagentRuns filter used a fallback (||) to pick one
key for matching. When controllerSessionKey was set but did not match the
caller's session key, the filter returned false even though
requesterSessionKey could have matched.
Change the filter to OR logic: if either controllerSessionKey or
requesterSessionKey matches the caller key, include the run.
Fixes#75593
* fix(subagents): add braces for lint compliance and real behavior proof
- Add braces around if-return statements to pass eslint(curly)
- Add evidence script verifying OR-match filter behavior
Evidence (3 scenarios, all pass):
S1: controllerKey=agent:main:main, requesterKey=telegram → 1 result (PASS)
S2: controllerKey=telegram, requesterKey=agent:main:main → 1 result (PASS)
S3: both keys unrelated → 0 results (PASS — scope isolation)
🦞 diamond lobster: L2 evidence (isolated tsx + real exports)
Ref. https://github.com/openclaw/openclaw/pull/99410
* fix(subagents): replace evidence scripts with focused regression tests
- Remove evidence/ scripts (failing check-guards per review)
- Add listControlledSubagentRuns test coverage in subagent-control.test.ts
- Three scenarios: controller match, requester match (new fix), scope isolation
Ref. https://github.com/openclaw/openclaw/pull/99410
* refactor(subagents): clarify read visibility ownership
---------
Co-authored-by: sheyanmin <sheyanmin@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(config): use Object.hasOwn instead of in operator in restoreOriginalValueOrThrow
* test(config): add regression test for prototype-named keys in restoreRedactedValues
* test(config): focus prototype restore regression
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(anthropic): guard Cloudflare Anthropic fetches
Wire the Cloudflare AI Gateway Anthropic SDK client through buildGuardedModelFetch so this path gets the same SSRF and transport policy as sibling providers.
Keep the behavior proof focused on the regression: a Cloudflare Anthropic request pointed at a link-local address must fail before the SDK reaches global fetch.
* fix(anthropic): add loopback proof test to cloudflare fetch wiring
* test(anthropic-cloudflare): trim proof to SSRF-block only, remove synthetic loopback server
* fix(infra): close fd and remove lock file on writeFile failure (#98958)
When fs.open(lockPath, "wx") succeeds but handle.writeFile() fails
(e.g. disk full / ENOSPC), close the file handle and remove the
partially-written lock file before re-throwing to avoid a file
descriptor leak and stale lock artifact.
Changes:
- src/infra/gateway-lock.ts: nested try-catch around writeFile
- src/infra/gateway-lock.test.ts: test for fd close + lock cleanup
- scripts/verify-gateway-lock-fd-leak*.mjs: fault-injection proof
Co-Authored-By: Claude <noreply@anthropic.com>
* test(infra): strengthen gateway lock cleanup proof
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): include session label in deriveSessionTitle fallback chain
When a session is renamed with /name, the user-provided label is persisted
in sessions.json but was ignored by deriveSessionTitle. This caused the
TUI session picker and other derived-title consumers to show auto-generated
or UUID-based names instead of the user's label.
Add entry.label to the deriveSessionTitle fallback chain after
displayName and subject, before the auto-derived firstUserMessage and
sessionId fallbacks. This aligns with the existing ACP translator
precedence (derivedTitle ?? displayName ?? label ?? key).
Related to #98742
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(gateway): tighten session label precedence coverage
* fix(gateway): prioritize explicit session labels
* fix(commands): preserve named-session suggestions
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Addressed source-reply turns now carry the message-tool-only delivery hint in runtime prompt context, matching the room-event per-turn contract without persisting the hint into transcript rows.
Surface: auto-reply prompt envelopes and agent messaging guidance. Fixes#99371.
Release-note: fixes Telegram group replies under message_tool_only delivery being composed privately instead of being prompted to use message(action=send).
Ambient transcript watermarks now carry the transcript session id, resolve only for the current session entry, and skip stale room-event hooks that no longer match the prepared transcript session.
This protects Telegram group prompt windows after reset by backfilling rows that are no longer present in the new session transcript, while preserving steady-state watermark filtering within one session.
Fixes#99373
Release-note: fixes Telegram group context loss after session reset when ambient transcript watermarks outlived the transcript they referenced.
Persist room-event observations as durable bare user transcript rows and carry an ambient transcript watermark through session state so Telegram chat windows only include the unpersisted gap.
Fixes#99257