* fix(irc): chunk PRIVMSG by UTF-8 byte budget so long non-ASCII messages are not truncated
IRC caps a full protocol line at 512 bytes, but sendPrivmsg split outbound
text by UTF-16 code units against the 350 char default. Multi-byte text such
as CJK or emoji produced lines far beyond 512 bytes and servers silently
dropped the tail of every oversized chunk. The chunker now also enforces a
UTF-8 byte budget derived from the line framing overhead, splitting on code
point boundaries and preferring spaces, while ASCII chunking is unchanged.
* test(irc): move loopback IRC server helper to shared test helpers
The colocated node:net import tripped the network-runtime-boundary PR diff
scan for extensions/irc/src. The loopback server now lives in test/helpers,
outside the scanned network runtime paths, and the CJK, emoji, and ASCII
chunking cases keep driving the real client over a real TCP socket.
* fix(irc): decouple byte budget from character cap and move loopback helper to irc test-support
The byte budget is now derived only from the 512-byte line limit and framing
overhead, independent of messageChunkMaxChars, so low character caps with
multibyte text keep advancing instead of dropping the message. The loopback
IRC server helper moves from test/helpers to the extension-local package-root
test-support surface so extension tests stay off repo helper bridges and raw
socket use stays outside extensions/irc/src.
* fix(irc): enforce UTF-8 wire limits
* test(irc): satisfy loopback harness lint
* test(irc): avoid implicit Promise return
* test(irc): handle loopback close errors
* fix(irc): preserve boundary word splitting
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(browser): resolve act targetId aliases before mismatch check
The /act top-level and batch targetId guards compared the caller-supplied
targetId against the resolved canonical tab.targetId with raw string
equality. Any supported alias form (tabId, label, suggestedTargetId, or a
unique id prefix) resolves to a different canonical id, so act requests that
followed the documented 'prefer suggestedTargetId/tabId/label' guidance were
rejected with 403 ACT_TARGET_ID_MISMATCH even though they named the correct
tab. snapshot/open/close/tabs lack this guard and kept working, matching the
reported symptom matrix.
Resolve the action targetId through the same tab alias resolution the route
used and reject only ids that resolve to a different tab.
* fix(browser): canonicalize act targetId aliases before Playwright dispatch
The /act gate accepted tabId/label/suggested/prefix aliases of the request
tab but left them on action.targetId. The managed executor reads
action.targetId ?? targetId for an exact page lookup (executeSingleAction ->
getPageForTargetId), so an alias missed the lookup and broke the action at
runtime whenever more than one page was open (single-page masked it via the
pages.length===1 fallback). Canonicalize the action targetId (top-level and
nested batch sub-actions) to the resolved tab id before dispatch; reject ids
that resolve to a different tab. Replace the mock-masked contract assertions
with executor-action assertions plus direct canonicalizer unit tests.
* test(browser): brace act-targetId guard clauses for curly lint
* docs(changelog): note browser target alias fix
* docs(changelog): note browser target alias fix
* fix(browser): reject ambiguous batch target aliases
* test(browser): type targetless act fixture
* docs(changelog): move browser alias fix to unreleased
* chore: drop nonessential browser changelog entry
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(irc): chunk PRIVMSG on UTF-16 boundary to avoid lone surrogates
sendPrivmsg split long messages with String.slice on a UTF-16 code-unit
index, so an emoji (or other astral character) straddling the split
point was cut into a lone high/low surrogate, sending broken bytes in
the PRIVMSG to the IRC server.
Slice each chunk with sliceUtf16Safe so a surrogate pair is never split.
When the budget is too small to fit even the leading astral character,
emit that character whole so chunking still makes progress.
Adds a socket-level test (fake net/tls socket) asserting no PRIVMSG
chunk contains a lone surrogate, including the one-code-unit budget
edge case, while the existing space-preferring split is preserved.
* refactor(irc): make surrogate-safe chunking direct
* fix(irc): preserve one-unit chunk limits
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(voice-call): resolve completed calls from the persisted store on status misses
get_status, the legacy status mode, and the voicecall.status gateway method
only consulted the in-memory call manager. Once a call was evicted (finalize,
gateway restart, or max-duration expiry) they reported { found: false } even
though the full record remained on disk. Fall back to the persisted call
history and resolve the NEWEST matching snapshot — history is oldest-first, so
a forward find() returns a stale record (the regression in the prior attempt).
Closes#96586
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(voice-call): use bracket access for mocked getCallHistory assertion
Avoids the typescript(unbound-method) lint rule that flags referencing a
typed method (`runtimeStub.manager.getCallHistory`) as an unbound value.
Bracket access matches the existing mock-assertion pattern in this file
(e.g. `runtimeStub.manager["sendDtmf"]`).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: re-trigger QA Smoke (transient build-OOM, also failing main run 28695162780)
* fix(voice-call): resolve persisted calls in the CLI status fallback too
The local CLI `voicecall status` gateway-unavailable fallback only consulted
the in-memory manager, so a completed/evicted call still returned
{ found: false } even though the gateway/tool/legacy status paths now fall
back to the persisted store. Align this fourth status reader: consult
getCallByProviderCallId in addition to getCall, and on an active miss resolve
the NEWEST matching persisted snapshot via getCallHistory(100) +
toReversed().find(...) (history is oldest-first), mirroring the gateway/tool
paths. Return shape is unchanged.
Per review on #96586 (align CLI status before merge).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(voice-call): centralize persisted status lookup
Co-authored-by: 曾文锋0668000834 <zeng.wenfeng@xydigit.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(logbook): automatic work journal plugin with a plugin-contributed Control UI tab
Squash of PR #99930 work for rebase onto the Control UI route refactor:
- extensions/logbook: Dayflow-style capture -> observations -> timeline cards
pipeline with SQLite store, node capture commands, standup/ask, retention
- plugin SDK/gateway seam: surface "tab" Control UI descriptors projected
into hello-ok controlUiTabs (scope-filtered, deterministic order)
- Control UI: dynamic plugin tabs with bundled Logbook view
- docs, tests, labeler wiring
* feat(ui): port plugin tabs and Logbook to the route-owned Control UI architecture
- shared /plugin route carries the tab id in the query (?id=<tab>), matching
the router's exact-path contract
- openclaw-plugin-page renders bundled views (Logbook), sandboxed plugin
frames (descriptor path), or the unavailable card
- sidebar renders hello controlUiTabs after each group's static routes
- Logbook view/controller live under ui/src/pages/plugin/
* fix(ui): namespace plugin tabs by pluginId to prevent cross-plugin tab id collisions
* fix(logbook): prefer app capture nodes and rotate off failing nodes
* fix(plugins): reject protocol-relative Control UI tab paths
* fix(logbook): harden automatic journal
* docs(changelog): remove maintainer self-credit
* chore(ui): refresh locale metadata after rebase
* fix(logbook): preserve analysis window boundaries
* fix(logbook): align status privacy and timezone
* fix(ui): stop hidden plugin tab polling
* fix(cron): reject sub-millisecond durations
* fix(skill-workshop): preserve proposal terminal newline
Preserve proposal_content exactly at the agent tool boundary and make
renderProposalMarkdown defensively emit a terminal newline.
Add focused regressions for the tool write path and markdown renderer.
* fix(skill-workshop): reject blank raw proposal content
* fix: treat empty-string optional integer tool params as unset
Optional positive-integer tool params (e.g. Telegram replyTo/threadId)
threw ToolInputError when a tool-calling model populated them with an
empty-string or whitespace-only default. Those defaults carry no value,
so readPositiveIntegerParam/readNonNegativeIntegerParam now treat a
blank string as unset (undefined) instead of throwing, while still
rejecting genuinely invalid present values (0, "42.5", "-3"). This
prevents silent message-delivery failures when models emit empty
routing-param defaults. Adds unit tests covering blank vs invalid.
* fix(gateway): log start session persistence failures
The gateway session-lifecycle "start" event persistence
(persistGatewaySessionLifecycleEvent, which surfaces write failures via
requireWriteSuccess) was fired as void ...catch(() => undefined), swallowing
the rejection with zero logging. A failed start-marker write silently dropped
the run's start record from restart-recovery accounting, with no
operator-visible trace.
The sibling terminal-phase catch already logs this since #97839; the start
path was the unfixed sibling. Mirror that fix: log the swallowed start-phase
persistence failure with the same redacted message shape via formatForLog,
keeping the fire-and-forget semantics unchanged. Adds a focused regression
test asserting the log fires on start-persist rejection.
* fix(memory): report close-time pending work failures
* fix(shared): return "" from sliceUtf16Safe when end <= start, matching native .slice
sliceUtf16Safe silently swapped reversed bounds (to < from) instead of
returning "" like String.prototype.slice, creating a subtle footgun for
callers with dynamic start/end pairs.
Caller scan across src/, extensions/, and packages/ confirmed no production
code relies on the old swap behavior — all callers use (text, 0, N) or
(text, -N) forms only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): require plugin manifest in npm verifier
* fix(android): propagate CancellationException in CameraHandler catch blocks
Catch (err: Throwable) swallows kotlinx.coroutines.CancellationException,
breaking structured concurrency when the coroutine scope is cancelled
during camera operations (handleList/handleSnap/handleClip).
Add CancellationException rethrow before each Throwable catch to match
the existing pattern used in GatewaySession and TalkModeManager.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(android): propagate CancellationException past GatewaySession invoke boundary
* chore: sync native i18n inventory after gateway session line shift
* fix(agents): prevent native hook relay bridge race condition on renew and registration
Remove synchronous bridge record write after server.listen() that races
before the TCP server binds, and guard renew handler with server.listening
check to prevent stale relay registrations.
Closes#98650
* fix: harden small reliability fixes
Co-authored-by: cxbAsDev <cxbAsDev@users.noreply.github.com>
* docs(agents): explain buffered LSP spawn failures
* docs(agents): clarify LSP spawn timeout invariant
---------
Co-authored-by: qingminlong <qing.minlong@xydigit.com>
Co-authored-by: anyech <anyech@gmail.com>
Co-authored-by: snotty <snotty@users.noreply.github.com>
Co-authored-by: masatohoshino <g515hoshino@gmail.com>
Co-authored-by: lin-hongkuan <lin-hongkuan@users.noreply.github.com>
Co-authored-by: simon-w <weng.qimeng@xydigit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: 宇宙熊Yzx <53250620+849261680@users.noreply.github.com>
Co-authored-by: xialonglee <li.xialong@xydigit.com>
Co-authored-by: nankingjing <1079826437@qq.com>
Co-authored-by: cxbAsDev <cxbAsDev@users.noreply.github.com>
Wires replyOptions.onModelSelected so the resolved provider/model and
thinking level for each turn (including after fallback) are sent as
author_model / author_thinking fields on activity rows and the final
reply. Servers without these columns ignore the unknown JSON fields, so
the wire shape is backward compatible; servers that persist them get
per-message attribution.
- docs: document agentActivity option, the non-inherited agent_activity:write
scope, default-off and best-effort degradation behavior
- activity: type the enqueue catch parameter as unknown (lint lane)
- activity: treat toolCallId as opaque; normalize only itemId lane prefixes
- activity: skip identical/stale-shorter commentary snapshots so no
redundant PATCHes queue
- http-client: fail fast when createActivityMessage has no channel or
conversation target; narrow mocked request bodies before JSON.parse
Opt-in per-account (agentActivity: true): the ClickClack channel extension
now mirrors streamed commentary and tool progress into durable
agent_commentary / agent_tool message rows via the existing
replyOptions.onItemEvent seam, coalesced so one logical step is one row.
Requires a bot token carrying the agent_activity:write scope; publishing
is best-effort and never interrupts final text delivery.
* fix(whatsapp): wrap JSON.parse with try-catch in auth store and test helpers
Add defensive try-catch around JSON.parse calls in WhatsApp extension
to prevent crashes from corrupted state files.
- restoreCredsFromBackupIfNeeded: wrap creds.json/backup validation
JSON.parse with try-catch; corrupted creds.json now properly falls
through to backup restoration instead of skipping it entirely
- updateLastRouteMock: wrap JSON.parse with try-catch, initialize
empty store on corrupted file
* test(whatsapp): add regression test for malformed creds.json longer than one byte
- Add a focused regression test for the exact case ClawSweeper
flagged: readWebCredsJsonRawSync returns non-null content for
files with stat.size > 1, so malformed JSON like "{x" (2 bytes)
reaches JSON.parse — the inner try-catch now catches the parse
failure and falls through to backup restoration
- Without this patch, JSON.parse("{x") throws to the outer catch
and restoreCredsFromBackupIfNeeded returns false, skipping backup
🦞 diamond lobster: L2 evidence (real function call + real filesystem objects)
Ref. https://github.com/openclaw/openclaw/pull/99070
* fix(whatsapp): restore malformed creds from backup
Co-authored-by: LeonidasLux <LeonidasLux@users.noreply.github.com>
* docs(changelog): defer credential recovery entry to aggregate
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: LeonidasLux <LeonidasLux@users.noreply.github.com>
* fix(whatsapp): cache bot's own outbound messages for quote metadata
When a user swipe-replies to a message the bot itself sent, the outbound
quote-key lookup misses the inbound-only metadata cache and falls back to
fromMe:false with the replying user's JID as participant. That mismatched
quoted.key is silently dropped by WhatsApp Desktop, so the bot's reply
bubble never renders there (it renders on Android, which is more lenient).
Cache quote metadata for the bot's own outbound messages at the send
choke point (rememberOutboundMessage) with fromMe:true and the bot's own
participant JID (group only; omitted for direct chats, matching WhatsApp
semantics). Future swipe-reply lookups then build a correct quoted.key.
Closes#91445.
* fix(whatsapp): preserve outbound quote metadata
Co-authored-by: Bartok9 <danielrpike9@gmail.com>
* docs(changelog): defer quoted replies entry to aggregate
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): prevent restart loops after terminal WhatsApp disconnects
Track `terminalDisconnect` through the WhatsApp status controller, channel
runtime snapshot, and `ChannelAccountSnapshot` so the health-monitor and
the `ChannelManager` task-exit handler both skip auto-restart when Baileys
signals a terminal session end (loggedOut / connectionReplaced).
Adds a `terminal-disconnect` `ChannelHealthEvaluationReason` so the policy
layer returns a stable, named reason rather than falling through to
`not-running`, preventing unbounded WebSocket/heap growth on multi-tenant
gateways. Fixes#78419.
* fix(gateway): prioritize terminal disconnect recovery
Co-authored-by: openperf <16864032@qq.com>
* docs(changelog): move WhatsApp restart fix to unreleased
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(imessage): only warn about empty group allowlist when messages actually drop
With groupPolicy="allowlist", a non-empty effective groupAllowFrom admits
group messages even when channels.imessage.groups is empty (senderFilterBypass
in src/config/group-policy.ts), so the startup warning "Every inbound group
message will be dropped" fired as a false positive for that configuration.
The warning now mirrors the runtime gate's effective sender allowlist (same
allowFrom fallback + legacy chat-target merge) and fires only when both the
groups registry and the effective group sender allowlist are empty - the only
startup-provable drop-all config. The message now names groupAllowFrom as the
fix, since adding groups entries alone leaves the sender gate blocking.
Docs: describe the two group gates' warnings separately with per-warning
remedies instead of implying both fire for the same config.
* docs(imessage): format migration guide
* fix(imessage): warn on empty group sender allowlist
* docs: move iMessage fix to unreleased
* fix(qqbot): publish disconnected channel status when the gateway closes or gives up
The QQBot channel only ever set connected: true (onReady/onResumed);
no close path updated the status, so a fatal close (bot banned or
offline, 4914/4915) or reconnect exhaustion left channels.status
claiming a live connection forever, and the channel-health monitor
never saw connected=false for a dead gateway.
Thread an onDisconnected callback from GatewayConnection.handleClose
(fatal and pre-reconnect branches) and the reconnect-exhaustion path
through the engine/bridge layers into channel.ts, which now records
connected: false and, for fatal closes, the close reason as lastError.
The running flag stays owned by the gateway lifecycle store, matching
the sibling channel convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(qqbot): import ChannelAccountSnapshot from the channel-contract subpath
The monolithic openclaw/plugin-sdk root entry is a legacy surface;
plugin-sdk contract guardrails and extension boundary checks require
focused subpath imports in bundled plugin sources.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(qqbot): ignore stale socket closes from superseded gateway connections
A server-driven RECONNECT / INVALID_SESSION tears the old socket down
and brings up a replacement; the old socket's close event can arrive
after the replacement is live. Reacting to it again would tear down the
new socket and regress the connected status, so the close handler now
ignores closes from sockets that are no longer current.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(qqbot): harden disconnect status recovery
* fix(qqbot): report opcode-driven reconnects
* fix(qqbot): keep fatal disconnect health visible
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Live-model flow lanes (QA Smoke CI) flake on model latency spikes: step
timeouts and agent.wait timeouts failed 8 consecutive smoke runs on unrelated
scenarios, including main's own push CI. A first failure now gets one bounded
rerun; a retried pass keeps the first attempt visible in details, and a retry
failure keeps the original diagnostics so deterministic regressions still fail.
* feat(crestodian): AI-first conversational onboarding with typed-op guardrails
Interactive `openclaw onboard` (and bare `openclaw` on a fresh install) now
opens the Crestodian conversation: detection-backed first-run proposal
(Claude Code/Codex logins, API keys), persona AI turns for every free-form
message (configless local-runtime fallback, 60s deadline, deterministic
degradation), approval-gated typed operations, chat-hosted channel setup
(`connect <channel>`), config get/schema read ops with secret redaction, and
a post-write validation hook that feeds schema errors back for a self-fix
turn. Adds the additive gateway `crestodian.chat` method so app clients run
the same conversation. Classic wizard stays behind --classic/explicit flags;
non-interactive automation unchanged; `--modern` becomes a deprecated alias
for `openclaw crestodian`.
* feat(macos): Crestodian chat onboarding and importance-ordered permissions
Replace the gateway step-wizard page with a Crestodian chat over the new
crestodian.chat method (works before any model auth exists), sort the
permissions page by importance with no scrolling, drop the redundant manual
refresh, and bump the onboarding version.
* feat(crestodian): run the custodian on the real agent loop with a ring-zero tool
Crestodian conversations now execute through the same embedded agent runner
as regular agents: a persistent agent session with a single construction-gated
`crestodian` tool wrapping the typed operations (read actions free; mutations
require approved=true asserted from explicit user consent, audited, with
post-write config validation fed back into the loop). The engine prefers the
loop (configured models or the Codex app-server fallback) and degrades to the
single-turn planner, then to deterministic commands. Setup approval seeds the
crestodian exec approval so local model harnesses can run; the configless
Codex backend config now enables exec and direct tool loading (it was
dead-on-arrival behind tools.exec.mode=deny and the tool-search index).
* test(crestodian): type the engine mock signatures for the core test lane
* fix(crestodian): map the advertised create_agent tool action
* fix(crestodian): host-verified approval arming and a macOS setup completion gate
Review findings: the model-supplied approved flag alone could authorize
ring-zero mutations (prompt injection / model error), and removing the macOS
wizard gate let users Next past the Crestodian page with nothing configured.
Mutating tool actions now also require host-verified consent (the engine arms
approval only when the user's actual message is an explicit yes), and local
macOS onboarding blocks advancing until setup authored the config, using the
same signal the old step wizard checked.
* fix(crestodian): bind approval to the exact proposed operation and gate dot navigation
A generic yes no longer authorizes arbitrary mutations: denied mutating tool
calls register a canonical operation fingerprint (host-owned, per session),
and an armed turn executes only the identical call, once. The denial message
is arming-aware so the approved turn self-heals in one roundtrip, and the
agent protocol pre-registers proposals. macOS onboarding page dots now honor
the same setup-completion gate as the Next button.
* fix(crestodian): redact sensitive wizard answers, skip logged-out CLIs, gate programmatic advance
Sensitive channel-wizard answers (tokens, passwords) are redacted from the
AI-visible conversation history; setup and the onboarding welcome never pick
or advertise a definitively logged-out CLI as the model; and macOS
handleNext() honors the page gates for programmatic callers (chat handoff)
just like the Next button.
* fix(crestodian): align the onboarding welcome's configured predicate with the app gate
A valid config carrying only a default model (partial/hand-written) now still
gets the first-run proposal instead of the ready guide, so the macOS setup
gate can always be satisfied from the conversation.
* fix(crestodian): armed turns can never mint their own executable proposal
An approval-mismatched call inside an armed turn no longer re-registers and
invites a retry (which let the model swap the approved operation for another
in the same turn); it voids the approval entirely and requires a fresh yes.
Proposals register only in unarmed turns, which the agent protocol already
does when proposing.
* fix(onboard): route any explicit setup flag to the classic wizard
* fix(ci): satisfy new lint rules, tool-display guard, and generated artifacts for crestodian
* chore(i18n): refresh native inventory after permissions copy wrap
* fix(crestodian): harden conversational onboarding
* docs(crestodian): document conversational onboarding
* test(crestodian): type embedded runner mock
* fix(crestodian): close onboarding security gaps
* chore: retrigger ci
* fix(google-meet): bound JSON response body reads to prevent OOM
Replace raw response.json() with readProviderJsonResponse (16 MiB cap)
across meet.ts, calendar.ts, and oauth.ts to prevent unbounded memory
allocation from oversized HTTP response bodies.
Also extract duplicated hasConfig check in createGoogleMeetSpace.
Test: loopback HTTP server test with 4 MiB oversized body + valid body.
* fix(test): prove readProviderJsonResponse bounds the 200 success path
Replace loopback server with synthetic Response objects. The oversized
test now returns HTTP 200 with a 17 MiB body — readProviderJsonResponse
rejects it at its 16 MiB cap, proving the bounded read fires on the
success path (not just the already-bounded 500 error path the old test
exercised).
Ref. https://github.com/openclaw/openclaw/pull/98850
* fix(test): add real HTTP transport integration tests for bounded JSON reads
- Add two vitest integration tests that start a real local TCP server,
fetch through native HTTP transport, and verify readProviderJsonResponse
correctly rejects oversized responses and passes normal ones.
- Existing synthetic-boundary tests unchanged.
Ref. https://github.com/openclaw/openclaw/pull/98850
* fix(test): wrap server.listen in promise executor to satisfy oxlint no-promise-executor-return
* fix(google-meet): add real SSRF-guard transport integration tests for bounded JSON reads
Adds two new integration tests that route meet.googleapis.com requests
through a local TCP server via spyOnGoogleMeetFetch, exercising the
actual fetchGoogleMeetSpace function end-to-end through the SSRF guard
mock. This addresses ClawSweeper review feedback requesting real
transport-level proof of the bounded read contract.
* fix(google-meet): preserve original fetch reference in spyOnGoogleMeetFetch to avoid recursion
The mockImplementation replaced globalThis.fetch, so calling fetch() inside
the mock called the mock itself. Capture globalThis.fetch before spying and
use the saved reference inside the implementation to avoid infinite recursion.
* fix(google-meet): add console.log debug output to loopback proof tests
Add structured console.log evidence markers to the four real HTTP
transport integration tests so that reviewers can see the bounded-read
contract working through actual TCP connections. Format matches the
established pattern from Alix-007's approved bound-read PRs.
* Revert "fix(google-meet): add console.log debug output to loopback proof tests"
This reverts commit e64c26d17e.
* fix(google-meet): tighten response body limits
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor: extract reusable AI runtime package
* refactor: complete AI provider relocation
* refactor: keep llm core internal
* refactor(ai): make @openclaw/ai self-contained with host policy ports
Move pure transport helpers (tool projections, strict-schema normalization,
prompt-cache boundary, stream guards, anthropic/openai compat, request
activity) from src into packages/ai; move utf16-slice into
normalization-core. Inject host policy (guarded fetch, redaction,
strict-tool defaults, diagnostics logging) through AiTransportHost with
inert library defaults installed by src/llm/stream.ts. Narrow the public
barrel to instance-scoped createApiRegistry/createLlmRuntime; the
process-default runtime moves behind internal/ and
registerBuiltInApiProviders takes an explicit registry. Delete the
src/llm/api-registry re-export facade.
* fix(ai): teach node, jiti, and vite resolvers the @openclaw/ai and utf16-slice subpaths
The workspace alias tables in root-alias.cjs, plugin-sdk-native-resolver,
sdk-alias, the shared vitest config, and the Control UI vite config only
knew @openclaw/llm-core; Node-side plugin loading resolved @openclaw/ai
through the pnpm symlink to the unbuilt dist (checks-node-compact CI
failures), and the Control UI build broke on the new
normalization-core/utf16-slice subpath.
* chore(ui): drop leftover service-worker debug logging
* build(release): ship @openclaw/ai with its own shrinkwrap and honest dependency set
packages/ai declares only its six real runtime deps (kysely, chalk, json5,
tslog, zod, fs-safe, and proxyline were never imported); orphaned root deps
removed. generate-npm-shrinkwrap now treats publishable packages/* like
publishable plugins so the AI tarball pins its transitive tree even though
workspace deps are omitted from the root shrinkwrap. knip learns the
package entry points; the tsdown dts neverBundle option moves to its
documented deps.dts home; the README documents the no-semver internal/*
contract and host ports.
* docs(ai): add minimal external-consumer example app
examples/ai-chat consumes only the public @openclaw/ai surface (built dist
via the workspace link): isolated runtime, built-in provider registration,
one streamed completion. Supports Anthropic/OpenAI via env keys and a
keyless local Ollama target; live-verified against Ollama.
* docs(ai): document the @openclaw/ai package and workspace shrinkwrap boundary
* chore(check): include examples/ in duplicate-scan targets
* fix: emit normalization package subpaths
* fix: complete AI package boundary artifacts
* fix: align AI package boundary contracts
* fix(ci): stabilize package release contracts
* test: align documentation contract checks
* test: keep cron docs guard aligned
* test: align restored docs contract guards
* test: follow upstream docs contracts
* docs: drop superseded talk wording
The /set/media control route accepts dark|light|no-preference|none, but the
CLI rejected no-preference, so prefers-color-scheme: no-preference could not
be emulated from the CLI. Align the CLI argument, validation, and error text
with the route contract and update the browser-control docs media line.