Attachment MIME gating and extraction dispatch used nine separate byte policies across five files; the allowlist could approve one MIME while dispatch observed another after a second sniff. @openclaw/media-core/attachment-classify now owns byte sniffing, text heuristics, extension mapping, and the seven attachment classes; the cache stores one classification that gates and dispatch share. Channel-declared MIME leads with the transport Content-Type as a byte-arbitrated hint, normalizeMimeType folds YAML/XML synonyms so existing configured allowlists keep matching, and input_file API callers keep declared-MIME precedence over cosmetic filenames. Net -135 production LOC.
Proof: 2,153 focused tests across media-core/media-understanding/media plus boundary-contract and deadcode-mirror suites; ClawSweeper local review clean (round 5, zero findings); live Telegram default-turn probe green.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(normalization-core): add promise-like guard
* refactor: consolidate promise-like guards
* fix(normalization-core): keep isPromiseLike non-throwing on hostile then getters
ClawSweeper finding on #121861: the diagnostics-path local guard caught throwing
then getters; the canonical guard must classify, never throw.
* test(normalization-core): annotate intentional hostile-thenable fixture
* feat(ui): path-based session and dashboard URLs
* docs(ui): document control UI URLs
* fix(ui): finalize session path routing
* feat(ui): anchor session URLs on stable keys
* docs(ui): clarify stable session URL identity
* fix(ui): resolve session prefixes with full prefix and pagination
Query sessions.list with the full supplied prefix instead of an eight-char
truncation, and paginate up to a bounded page count so longer disambiguation
links resolve instead of being reported ambiguous. Zero strict-prefix matches
now fall through to literal-key resolution rather than rendering an empty
ambiguity view.
Also document the ~dot/~dotdot segment escape: peer ids reach session keys
trimmed and lowercased only (src/routing/session-key.ts), so a literal '.' or
'..' segment is reachable and browsers would normalize it away.
* fix(ui): synchronize committed session routes
* test(ui): split native shell host coverage
* fix(ui): thread configured mainKey through session URL builders
Reserved-set disambiguation needs the operator-configured mainKey at runtime, so
thread it from agentsList through every session path builder and the ClickClack
control URL. Unambiguous non-hex single-segment rests now resolve literally while
short-id-shaped rests still fail closed, which restores ClickClack channel
compatibility detection and control-link reconciliation.
Also bound prefix-resolution retries, preserve catalog thread identity, and keep
draft state on ambiguous candidate links.
* fix(ui): repair session URL CI integration
* perf(ui): lazy-load session route resolution
* perf(ui): isolate session prefix resolution
* perf(ui): defer session path parsing
* perf(ui): defer session navigation startup
* fix(ui): preserve first-run and literal session navigation
* fix(ui): satisfy session routing type and export gates
* fix(clickclack): preserve unscoped control link agent
* style(ui): satisfy chat page line limit
* refactor(ui): move chat page helpers to owning modules
* fix(ui): preserve destination session route identity
* fix(ui): preserve agent identity in session routes
* fix(ui): escape dots in literal session path segments
encodeURIComponent leaves periods intact, so a literal key segment like
channel:release.js produced /chat/main/channel/release.js. In-app navigation is
intercepted by the SPA, but a refresh, an external link, or a ClickClack link
would be served as a static asset request and never reach the app.
pathForWorkboardBoard already escapes dots for this reason; mirror it in both the
session URL contract and the ClickClack encoder, route the agent id through the
same segment encoder, and pin the case in both shared vector tables.
* fix(ui): stop bootstrap after teardown race
* refactor(ui): centralize session navigation targets
* fix(ui): consume bootstrap teardown abort
* fix(ui): canonicalize configured main session routes
* fix(ui): preserve distinct session references
* fix(ui): redirect released session query links
* fix(ui): make bootstrap teardown abort-safe
* test: wire Control UI suite into CI, fix its broken tests, drop dead harness surface
- fix 7 Control UI tests broken on main: module-mock factories are unreliable
under isolate:false shared workers; use RealtimeTalkSession prototype spies
and real jsdom storage instead (chat-realtime, chat-pull-requests)
- add gated checks-ui CI job (runUiTests changed-scope output): chromium
provisioning, lint:ui:no-raw-window-open, pnpm --dir ui test; ~30s on a
4vcpu runner, runs only when ui-affecting paths change
- add weekly node22-compat workflow exercising the supported lower Node bound
with the same command set as the dispatch-only ci.yml job
- delete the unreachable channels entry in EXTENSION_TEST_CONFIG_ROUTES and
the never-populated extensionRoutedChannelTestFiles override machinery
(born empty in 2ccb5cff22); resolved globs are provably identical
- guard run-vitest.mjs hardcoded path maps with existence tests so renames
cannot silently drop extended stall watchdogs
- remove unenforced coverage thresholds; test:coverage is informational, docs
updated (reference/test, help/testing, plugins/sdk-testing)
Closes#104321
* test: pin OPENCLAW_CI_RUN_UI_TESTS in the manifest env expectation
* test(ui): capture the realtime start spy instead of referencing the unbound prototype method
* test(ui): raise ui vitest timeouts for real-browser layout tests on small CI runners
* test: fix changed-run routing, dedupe gateway package lanes, repair force-rerun triggers
- route extensions/active-memory changed runs to their dedicated lane; add a
generic guard that every catch-all-excluded extension root resolves to a
dedicated config so future split lanes cannot silently skip changed tests
- exclude packages/gateway-client and packages/gateway-protocol from the unit
lane; explicit targets now route to the gateway-client lane (was double-run)
- generate forceRerunTriggers from the test/vitest directory and resolve all
entries absolute: Vitest matches triggers against absolute changed-file
paths, so the old hand-maintained relative list never matched at all
- replace deprecated envFile:false with envDir:false (Vitest 4.1.9 warning)
- give scripts/test-live.mjs stall detection teeth: kill the process group and
exit non-zero when OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS elapses quietly
- consolidate the live-docker shell target list into changed-lanes.mjs so the
lane regex and check targets cannot drift; cover subagent-announce edits
- delete orphans: vitest.extension-clickclack.config.ts,
vitest.full-core-unit.config.ts, scripts/test-docker-all.sh,
scripts/test-live-acp-spawn-defaults-docker.sh
Closes#104231
* test: keep envDir literal so sharedVitestConfig satisfies Vite UserConfig
Vite 8 types envDir as string | false; the bare object literal widened false to
boolean and broke check:test-types at the defineConfig call sites.
* fix(test): make unit tests hermetic against host config and build state
Two host-state leaks made the full suite fail deterministically on developer
and agent machines while CI stayed green:
- Built checkouts: bundled-plugin manifest discovery prefers dist/extensions,
which by design excludes externalized official plugins (e.g. qwen). Tests
that depend on manifest-driven endpoint classification (DashScope cases in
media-understanding and model-compat) failed in any checkout after
pnpm build. test-env now pins OPENCLAW_BUNDLED_PLUGINS_DIR to the source
extensions tree (with the vitest trust opt-in) so discovery matches CI
regardless of local build state; discovery tests keep managing the env
per case.
- unit-fast shards: setupFiles was empty, so auto-curated tests that read
config saw the developer's real ~/.openclaw/openclaw.json; any key the
branch schema rejects failed those tests. unit-fast and
unit-fast-fake-timers now load a minimal env-isolation setup
(test/setup.env.ts) that installs the isolated test home without the
shared setup's module mocks.
* test: update unit-fast config contract for env-isolation setup
* fix(test): force unit-fast hermetic env isolation
* 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
Refactor the Control UI around route-owned page lifecycle and state while preserving existing behavior and design.
Prepared head SHA: bd51b6fa76
Co-authored-by: Shakker <165377636+shakkernerd@users.noreply.github.com>
Reviewed-by: @shakkernerd
Extract shared normalization/coercion helpers into private @openclaw/normalization-core workspace package while preserving existing plugin SDK helper subpaths.\n\nAlso keeps direct normalization-core imports internal, wires UI/build/loader resolution, and replaces the slow PR network CodeQL lane with a fast added-line boundary scan while retaining full CodeQL for scheduled/manual runs.\n\nVerification: local moved tests, plugin SDK boundary tests, extension loader tests, agents-support shard, UI build/test, build artifacts, lint, workflow guards, autoreview, and GitHub CI passed on PR head 963d893715.
Move model catalog normalization and package-owned catalog schema/types into model-catalog-core while keeping public plugin SDK model catalog declarations on the existing SDK surface. Verified focused tests, package-boundary compile, full build, changed gate, declaration leak grep, CI, and autoreview.
Move model catalog ref helpers into @openclaw/model-catalog-core/model-catalog-refs and update internal callers/package-boundary aliases. Also fix the timestamp predicate typing that blocked prod type checks on current main.
* refactor: extract model catalog core package
* refactor: route model catalog imports through package boundary
* build: include model catalog in plugin sdk package dts
* fix: preserve static fallback model metadata