Select changed extension packages and consumers of changed public source through the existing import graph, including type-only imports. Keep the complete typed programs, native lint chunks, resource limits, artifact preparation, and non-extension checks.
Retain full extension lint for scheduled/hourly main and release validation, shared lint/type policy, uncertain prior source types, and the OPENCLAW_CI_EXTENSION_LINT_FULL repository switch. Read the pinned diff base so removing a global augmentation cannot hide its consumers. Publish selected package reasons in the check-plan summary.
Twenty recent PR path scenarios emit 54 -> 40 hosted extension-lint rows (25.9% fewer): four 3 -> 0, one 3 -> 1, two already 0 -> 0, and thirteen unchanged. On Linux with four CPUs and a 16 GiB limit, warm maximum full-stripe compute was 64.44 s and the qa-lab-only stripe was 14.58 s. Shared artifact preparation took 114.24 s separately; these are not end-to-end Actions job timings. Full typed programs are unchanged. Shared loose extension consumers still retain full coverage.
The bounded historical search found one source-attributed extension-lint failure among 42 inspected runs; all three failing packages remain covered. Ten causal runs were unavailable, so this is limited historical evidence.
Proof on Linux Testboxes: whole tooling plus both owning fast configs; full core/extension/root test types; final check-changed, typed lint, boundary guards, architecture, source contracts and dead exports; eight same/different-SHA native preflight cells; native before/after manifests for twenty PR scenarios; final helper parity for all twenty plus two probes; and P2 review. Initial new-fixture errors were corrected and replayed. The sole inherited suppression-inventory failure reproduces on the unmodified parent and passes with already-landed d346309979. No workflow dispatches or CI reruns. New helper tests cost 5.25 s locally, with their Linux replay included in the focused proof.
Retire the five SDK compatibility facades under the approved September 30 owner decision, and migrate in-repository callers onto their focused contracts. Keep implementations with their canonical owners and remove forwarding exports that become unused after the cutover.
BREAKING CHANGE: remove openclaw/plugin-sdk/channel-lifecycle, channel-message, channel-reply-pipeline, config-runtime, and infra-runtime. Use channel-outbound/channel-inbound, config-contracts and focused configuration/infra entrypoints. The new system-event-runtime entrypoint supplies public snapshot inspection and consumption. Update affected external plugins before upgrading the host; this retirement does not certify universal external migration.
Package exports, SDK entry inventories, compatibility tombstones, migration docs, and surface budgets move together. Canonical API comparison confirms exactly five removed entrypoints, 869 removed export paths, 35 focused additions, and no retained-export signature changes. Net production reduction: 1,320 lines.
* refactor(state): register worker operations once per domain
Infer shared-state worker command contracts from lazy per-domain handler tables. Migrate Web Push, APNs, worktree registry operations, and fleet registry while preserving the existing broker and transaction owners.
* refactor(sessions): run health store summaries in history worker
* feat(ios): snooze sessions from the session menu
Add shared calendar presets and identity-bound snooze/wake patches while preserving existing Swift transport call sites. iOS gains Active, Snoozed, and Archived scopes, wake labels, and deadline-driven updates for Sessions, the sidebar, and Overview. Snoozing keeps ongoing work and the open conversation intact.
* test(ios): capture session snooze proof states
Extend the synthetic Gateway with awake and snoozed sessions. Capture Active and Snoozed lists, snooze presets, and Wake through the real iOS session menu, using All Sessions navigation and a row selector that excludes the retained sidebar.
* fix(ios): show the weekday for the next-week snooze preset
* chore(ios): record snooze strings in the native i18n source inventory
* fix(ios): format snooze wake labels through the localized catalog
Apple i18n verification rejects runtime-interpolated localized strings
because they bypass the generated catalog, so the "Wake session" menu item
and the "tomorrow" wake description now use the repository's
String(format: String(localized: "… %@")) pattern, and the native string
inventory records the format sources.
* refactor(ios): drop the superseded mutation lease initializer
* fix(ios): keep snoozed sessions browsable offline
Separate cached browsing scopes from connected mutation controls. Keep Archived connected-only and reset an archived selection to Active on disconnect. Cover scope availability and the offline cached roster, including snoozed rows and wake labels.
Keep app and XCTest compilation on every admitted iOS smoke job. Select the
voice/media/typography and Access/chat lifecycle simulator groups from their
runtime, test, fixture and build owners, and omit simulator preparation when
neither group is selected. Preserve full scheduled/manual/release coverage.
Add OPENCLAW_CI_IOS_SIMULATOR_FULL to restore both PR groups, emit selection
reasons in preflight and job summaries, and include the planner in the trusted
preflight/platform harnesses. Retain every existing test case and assertion.
Test cost: the complete iOS workflow file passed 67 cases in 70.65s locally
while native compilation overlapped; the three integrated workflow/checkout
files passed 653 cases in 182.09s on Linux. Native build-only and lifecycle-only
paths passed with an unchanged app executable hash and no simulator use for build-only.
Keep direct runtime consumers, expanding to depth two only below 20 direct
importers. Select adjacent tests only in directories with at most 30 tests;
larger directories use name prefixes and explicit ownership. Keep package
consumers direct so mixed import styles cannot bypass the hub cutoff.
Use config-loading and plugin-registry smoke within two Node rows. Preserve
the source-inventory guards, full-selection kill switch, and full hourly plans.
A controlled 48-diff replay drops from 2,193 to 1,083 rows, with 44 of 48 at or
below 40. Historical failure replay has 29 hits, one process-fixture miss
retained by hourly main, and three unavailable historical test files.
Restore missing fs-safe prebuilds without relying on Node directory-merge semantics, preserve Bun native-launch diagnostics, and recover malformed Codex JSON without consuming the next valid frame. Keep package custody, bounded diagnostics, warning-only lifecycle failures, and installed-updater ordering intact.
Proof: 53 changed-test cases plus six catalog sibling cases pass on each of Node 24 and Bun; full changed-file validation and both import-cycle checks pass. Published Node-driver upgrade and Node-free Bun lifecycle cells passed on AWS. The published Bun driver's owning-npm preflight limitation remains explicitly documented.
CI still used the separately maintained Bun artifact and kept native-compiler tests on Node. Pin the OpenClaw Bun fork prerelease at 57fadf566d with release metadata verification and independent archive/executable checksums. Admit the 17 compiler files and library through their existing runtime owners, retain Node siblings and dual-mode coverage, and require native PTY success in the Bun-only smoke.
Proof: all ten Linux AWS selections passed (44,534 case executions), Node focused tests passed 243/243, Bun routing tests passed 208/208, and changed-file, workflow, and import-cycle checks passed. The old-first full smoke was fail/pass/pass/pass, attributing Chrome's fresh-host first launch to a shared startup issue. Eight forced-Bun ledger global-stub failures reproduce unchanged on main; that tooling suite stays on Node.
Supersedes only the pin portion of #159988. The batch-2 pin bump remains separate.
Adds named storage locations as a generic, pluggable capability, with backup as its first consumer.
- Core storage owner (src/storage): storage.locations config, a location marker that binds identity (runtime never creates it, so unplugged disks and different disks at the same path are refused), client-side streaming encryption (scrypt key from a SecretRef passphrase, per-object HKDF keys, AES-256-GCM segments), and a built-in filesystem provider for external disks and mounts.
- Plugin SDK: api.registerStorageProvider plus manifest contracts.storageProviders; providers move opaque bytes only.
- Bundled cloudflare plugin: an r2 provider over the S3 API with conditional writes and bounded multipart uploads; auto-enabled when a location uses provider "r2".
- Backups: backup create --to <location> with verified archives, UTC retention, list/verify/restore --from, Gateway-owned offsite schedules (installed Git schedules unchanged), per-installation namespace claims fenced at publication and deletion, backup record for external jobs, backup.status RPC, Doctor/status hints, and a Systems page Backups section.
No config or state migration; the storage section is new and optional. Proof: live R2 and mounted-disk round trips, namespace takeover trace, and a published 2026.9.7 upgrade cell with an existing Git backup schedule.
Prepare trusted install-smoke sealing to inline a fixed shared policy member from the immutable candidate archive. Preserve existing standalone sources and payload identity/integrity checks; never execute a candidate generator.
Seven boundary tests passed on Linux Testbox. Changed checks and both import-cycle checks passed; independent review found no actionable issues.
* refactor(state): register worker operations once per domain
Infer shared-state worker command contracts from lazy per-domain handler tables. Migrate Web Push, APNs, worktree registry operations, and fleet registry while preserving the existing broker and transaction owners.
* refactor(memory): move retained index reads into workers
* test(memory): align fixtures with worker read ownership
Repair the ten fixture failures exposed by retained worker reads. Install the embedding generation and token budget, retain a file-backed startup owner, and preserve the explicit scheduler-yield proof through source-wide snapshots.
Follow-up to #161053 (shared-session emoji reactions).
Reaction writes now run through the SQLite worker admission the sibling
session stores use (runOpenClawAgentWorkerWrite); the native path stays
only for process-held incognito databases the worker cannot reopen by
path, and the handler revalidates live authority around the awaited
write.
The plugin action dispatch path awaits onPlatformSendDispatch right
before the synchronous handoff fence, exactly like the send path, so the
reaction mirror re-reads the conversation binding at the final handoff
and refuses a message whose conversation was rebound while the action
runner prepared delivery.
Channels with one bot reaction per message (Telegram bots, WhatsApp)
declare the new optional ChannelPlugin.capabilities.reactionSlots =
"single"; when a person removes one emoji while others remain, the
mirror re-sets the newest surviving emoji instead of clearing the slot.
Multi-slot channels are unchanged.
Also trims redundant scaffolding in the reaction handler, kernel, UI
component and worker.
Proof: 119 focused tests across store, handler, dispatch and UI; mocked
Gateway reactions e2e; typecheck lanes; database-worker inventory check;
live two-person Gateway proof with the qa-channel mirror reporting
delivered through the final-dispatch hook.
The native Android chat shows reaction chips under saved prompts and
replies (emoji, count, highlighted when the viewer reacted, TalkBack
lists who reacted) plus an add-reaction chip; tapping a chip toggles the
viewer's own reaction, and the add chip or message actions open a Quick
reactions sheet with the Control UI's palette plus "More…" for any single
emoji. Updates arrive live from the session.reaction event; nothing
notifies.
Reaction wire models are selected into the generated GatewayProtocol.kt,
the hello summary retains sessionCap, sessions carry sharingRole and
visibility, and ChatReactions.kt owns state, the permission projection
ported from the web's canReactToSession, the palette and the
single-grapheme rule. Writes run FIFO per message with per-emoji response
guards, list snapshots advance revisions so late set responses cannot
overwrite refreshed state, and message ids are canonical entry ids from
__openclaw.id. Generated locale artifacts are left to the refresh
workflow.
Proof: pnpm android:assemble, focused Robolectric/Compose tests (hello
summary, controller list/set/event/race regressions, chips/palette),
ktlint and Android lint, protocol and i18n checks, emulator before/after
screenshots in the PR, Codex branch review with its findings fixed.
* fix(gateway): keep model metadata available during plugin drains
Keep the active model publication readable while admitted plugin work settles,
then retire it before resource replacement. Preserve execution fencing, auth
revocation, decision cancellation, rollback, and cleanup ownership.
Retain an automatic drain failure only while its original plugin configuration
delta remains unresolved. Allow explicit wait recovery and reversion alongside
changes to a different plugin without retrying unrelated failed work.
Validate on Blacksmith Testbox with real Gateway reader latency proof, 378
focused tests, 145 follow-up tests, negative controls, types, lint, and guards.
* fix(plugins): fence admission while preserving owned cleanup
* test(gateway): preserve plugin record helpers in reload fixture
* test: repair reload mocks and supervised process joins
* test(models): preserve queue receiver in drain observer
* fix(lint): prepare schema types before cold core checks
Generate Kysely declarations through the existing owner before direct typed lint and before striped children skip preparation. Keep core lint independent of plugin declaration builds and preserve metadata, focused syntax-only, and caller-owned preparation paths.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* fix(lint): cold core checks fail on missing database types
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: 1845cf04-95a0-4fb5-8975-0c309cc08106
* test(lint): include generated-type owner in runner fixtures
Keep copied lint-status and changed-lint fixtures complete after typed runners gained their Kysely prerequisite. Preserve all process-lifetime, signal, timeout, and semantic-lint assertions.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* fix(lint): cold core checks fail on missing database types
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: 6962f022-9a87-4b9e-98b9-c48b9f678a13
* fix(lint): copied runner checks fail after schema preparation
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: f87db3b2-5303-4e82-abd2-215b0ad81c97
* fix(test): measure CI group ownership at the Vitest launcher
Mixed threads and forks add different immediate OS parent hops. Record each real Vitest CLI PID and PPID in the existing fixture, bind every borrower to its launcher, and continue requiring exactly two CI group owners. Preserve three borrowers, generation/input equality, compiler counts and disposal guarantees; strengthen per-group and launcher cleanup checks.
The original first-case failure was reproduced with bounded ancestry observations before its assertion. Full original-order CI fixture plus batch/shutdown siblings: 81 passed. The failing case also passes with the reported bun-compatible runtime policy. Scoped changed checks passed; independent P2 review is clean. No production owner, scheduling, timeout, retry, or pool configuration changes.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* fix(ci): repair cold lint fixtures and mixed-pool worker checks
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: 898d5732-9c30-4a0e-a6ae-83e81453a9a3
* fix(lint): copied runner checks fail after schema preparation
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: 411d1e40-6d79-485c-b42e-63843f69fd68
---------
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Keep the packed prepare hook independent of the unshipped source generator:
invoke its CLI only when source schemas exist and preserve failure exits.
Prepare declarations through their existing owner before direct tsgo profiling.
This completes the source preparation gaps from 6337d159e3 (#162288).
Update behavior: published npm installs have no source schemas and skip
source preparation. No database, rollback, or runtime update behavior changes.
No published version contains the regressing commit; 2026.9.7 predates it.
Validation: real pnpm-pack tarball fails the missing-import guard before and
passes after; cold profiler fails before and passes after. Focused owner and
package tests, changed checks, boundary lint, architecture, and P2 review pass.
* refactor: simplify shared session assignment
Reuse the admitted run as the MCP authority source and keep session action restrictions in the core tool. Remove redundant adapter plumbing and retain the full handoff composition in the existing release-only tier.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* refactor: simplify session assignment across channels
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: dcf1fabf-dd61-4437-a180-1ad5b273a3ac
* test: migrate session archive fixture to shared inputs
Keep the upstream assignee self-archive regression on the actual non-owner posture and its issued operator authority after removing controlOnly. The unchanged assertion fails with the retired option and all 21 authority cases pass after migration.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* refactor: simplify session assignment across channels
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: 66f5d3b4-102b-4791-8296-fc5e5dace253
* refactor: simplify session assignment across channels
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: 8a6f1107-834b-40b2-b9a0-6338d592ddba
* test(lint): include generated-type owner in runner fixtures
Keep copied lint-status and changed-lint fixtures complete after typed runners gained their Kysely prerequisite. Preserve all process-lifetime, signal, timeout, and semantic-lint assertions.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
(cherry picked from commit e568672241)
* refactor: simplify session assignment across channels
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: f3b666e3-ce9e-4662-b32c-1e4a677d12c7
---------
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* fix(doctor): retain migrations for supported config formats
* fix(doctor): retain the typed sandbox scope for reporting
* fix(doctor): include agent policy in wrapper extraction
Remove five obsolete release-only entries after moving mocked Control UI
compositions into PR owner selection. The generic hourly planner now retains
these files, and its tests require their periodic coverage. Every browser
assertion, screenshot and deadline remains unchanged.
The selection policy has four known historical backtest misses:
- ui/src/e2e/sidebar-customization.e2e.test.ts
- ui/src/e2e/plugin-bundled-view-recovery.e2e.test.ts
- ui/src/e2e/debug-diagnostics.e2e.test.ts
- ui/src/e2e/config-controls-visual.e2e.test.ts
Hourly main still covers all four, and full release validation retains them.
Set repository variable OPENCLAW_CI_UI_E2E_FULL=true to restore full PR coverage.
Hourly queue and runtime mean detection within one hour is not guaranteed.
Across 20 recent UI PRs, median selected files fall from 654 to 6; 15 select
under 60. Median modeled shard wall is 219.6 seconds including a 200-second
reserve, versus 641.2 seconds before. These are projections, not observed CI
wall. The original ten failing runs and all 20 PRs retain exact selection
reports, including every miss.
Validation reuses the completed Linux owning-config, full type, architecture,
source-contract and seven real manifest preflight proofs. The final planner
file passes 114/114 on Linux (61.11s command); its changed-file static gate
passes (363.24s), including typed lint, root types and dead-export checks.
Boundary lint and independent P2 delta review pass.
Defer unmapped and indirect runtime browser compositions to hourly main and
release validation. PRs keep five documented smoke files, edited tests,
transitive fixture/test imports, and declared runtime owners plus their
direct imports. Narrow full fallback to core E2E harness, UI bundle config,
and explicit app-shell inputs. Whole-route watches now name route entries
rather than every leaf under the route; source-backed CSS owners remain.
Shared shell/store cycles previously selected hundreds of unrelated routes
for a single leaf change. Across 20 recent UI PRs, median selected files
fall from 654 to 6; 15 select fewer than 60. Pack 30 files per browser row,
retaining existing full-mode caps and worker limits. Median modeled wall
falls from 641.2 to 219.6 seconds including a conservative 200-second reserve.
This is a native timing/LPT projection, not observed CI wall.
The original ten causal failing runs produce four file misses across three
runs: sidebar-customization, plugin-bundled-view-recovery, debug-diagnostics,
and config-controls-visual. All remain in the full hourly inventory. The
next hourly is the accepted safety net; queue/runtime mean detection within
one hour is not guaranteed. The existing full-PR kill switch is unchanged.
No browser assertion, screenshot, test deadline or product behavior changes.
Add planner coverage for deferred unknown/transitive owners, direct fixture
imports, full periodic inventory, kill switch and bounded matrix rows.
## What Problem This Solves
`pnpm tsgo:profile constructor` treated an inherited object property as a configured graph, created artifact directories, and failed with an unrelated TypeError.
## User Impact
Inherited names now receive the existing `Unknown graph` diagnostic before artifacts or compiler work. Valid graph names, flags, and output remain unchanged.
## Why This Change Was Made
Require an own property in the graph registry instead of accepting JavaScript's prototype chain.
## Evidence
- Blacksmith Testbox `tbx_01m3v0m906fej4qb20j8wy64gd`: the CLI regression fails on the original source with the misleading profiling/error output, then passes with the fix. It exercises the supported CLI preload and verifies exit 1, the existing diagnostic, no stdout, no artifacts, and no compiler launch.
- Single-worker regression suite wall time: 1.63s. One CLI process is necessary to cover admission before the private parser's side effects; there is no production test hook, timer, sleep, or polling.
- Changed checks and SDK surface passed. Madge and runtime import-cycle checks both reported zero.
- Independent isolated Codex review completed with no P0–P2 findings.
No configuration option, schema, dependency, or protocol change.
### Hosted CI evidence
Exact-head run https://github.com/openclaw/openclaw/actions/runs/36825865133 failed `check-lint-core-1` and `check-lint-core-2`; fail-fast cancelled sibling coverage. Security checks and independent review passed. Cancelled checks remain incomplete, not green.
All twenty root diagnostics have independent executed CI witnesses. Nineteen match current unrelated PR CI: core-1 in https://github.com/openclaw/openclaw/actions/runs/36823091648 (head `488d5300b95fa56568284d014f43c44d3fdc2143`), and core-2 in https://github.com/openclaw/openclaw/actions/runs/36822965519 (head `2ca2ff4326be38e6c890f2c80dddb03c84e06c79`). The remaining `update-candidate-canary.test.ts` failure is independently witnessed by scheduled main run https://github.com/openclaw/openclaw/actions/runs/36827062752, head `867a82ce61`, job `check-lint-core-5` (`110255224713`): `File has too many lines (1006). Maximum allowed is 1000.` The executed job annotation names the same file at line 1035. All root failures therefore qualify for the explicitly approved inherited-failure exception; cancelled coverage remains incomplete. This PR changes only the profile CLI and its own regression; none of the listed core files or their lint configuration is changed.
| File | Rule | Diagnostic |
| --- | --- | --- |
| `src/skills/library/store.ts` | `typescript(no-redundant-type-constituents)` | 'StateDatabase' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/agents/harness/native-hook-relay-store.kernel.ts` | `typescript(no-redundant-type-constituents)` | 'OpenClawStateKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-store.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-personal-publication-store.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-recovery.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-repository-publication-store.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-repository-publication.kernel.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-coordinator-methods.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-repository-publication-recovery.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-personal-pending.test.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-shared-read.kernel.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/proxy-capture/store.kernel.ts` | `typescript(no-redundant-type-constituents)` | 'OpenClawStateKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this intersection type. |
| `src/channels/message/ingress-queue.test.ts` | `typescript(no-unnecessary-type-parameters)` | Type parameter TColumn is used only once in the function signature. |
| `src/config/sessions/session-transcript-projection-rebuild.ts` | `typescript(no-redundant-type-constituents)` | 'OpenClawAgentKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this intersection type. |
| `src/state/openclaw-state-read.types.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/infra/update-candidate-canary.test.ts` | `eslint(max-lines)` | File has too many lines (1006). |
| `src/infra/delivery-queue-sqlite.ts` | `typescript(no-redundant-type-constituents)` | 'OpenClawStateKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/infra/deferred-plugin-migrations.test.ts` | `typescript(require-array-sort-compare)` | Require 'compare' argument. |
| `src/infra/update-repair-agent.self-lease.test.ts` | `typescript(require-array-sort-compare)` | Require 'compare' argument. |
## What Problem This Solves
Translation generation and its diagnostic report maintain duplicate language labels and copies of metadata types already owned by the producers.
## User Impact
No user-visible change. Report locales, language labels, unknown-locale handling, generated prompts, and metadata formats remain the same.
## Why This Change Was Made
The existing locale configuration module now owns the shared display-label lookup. The report derives its accepted locales from the same existing configuration, while English and Swedish remain generator-only labels. A private Map retains safe fallback for unknown and prototype-like names. Report data types are imported from their producers with type-only imports.
Measured reduction: **69 net production lines** across four script files. The report test only changes its type import.
## Evidence
Independent isolated Codex review completed with no P0–P2 findings.
Blacksmith Testbox `tbx_01m3v3q8tfefne2t0npghczhex`, pinned source base `8d99007436` plus the verified candidate diff:
- Actual report CLI before/after: 27 cases per phase (20 accepted and 7 rejected); stdout, stderr and exit status are byte-identical.
- Actual translation prompt capture before/after: 27 synthetic cases per phase; system prompts are byte-identical. This uses the existing test LLM mock, without live inference.
- All eight affected suites passed: 162 tests. The changed report suite took 1.95 seconds wall time with `--maxWorkers=1`.
- `node scripts/check-changed.mjs`: passed, including script/core test types, changed-file lint, dead exports and Docker boundaries.
- `node --max-old-space-size=8192 --import ./scripts/tsx.mjs scripts/plugin-sdk-surface-report.mts --check`: passed.
- `node --import ./scripts/tsx.mjs scripts/check-madge-import-cycles.ts`: 0 cycles.
- `node --import ./scripts/tsx.mjs scripts/check-import-cycles.ts`: 0 runtime value cycles.
The remote command completed successfully; its external portal synchronization emitted a timeout warning after proof completed. Hosted exact-head CI is pending.
No configuration keys, generated artifacts, CLI text, or visible UI states change.
* fix(config): stop a republished snapshot from invalidating session rows
publishRuntimeConfigSnapshot emitted an unconditional all/config session change,
so a reload that resolves to the values already published still invalidated every
projected session row, moved the projection epoch, and started a drain the next
reload abandoned. A watcher that rewrites the same config every few minutes holds
readers on a projection that cannot converge, and concurrent sessions.list calls
join the same pending promise and land together.
Compare the snapshot consumers actually read, plus its authored source, so only a
publication that is not a change reaches subscribers.
* fix(config): guard only the session change, not the publication
Keep every side effect of a runtime config publication and skip the emit alone
when the snapshot resolves to the one already published, so a republishing
watcher no longer invalidates every projected session row.
* refactor(config): move snapshot comparison into its own module
The line-cap ratchet rejected src/config/runtime-snapshot.ts growing from 700 to 703 counted lines, and that file already sits at its cap. Move the byte-stable serializer and the snapshot comparison into runtime-config-snapshot-match.ts so the guard in the publish path costs no growth.
* fix(config): keep invalidating same-object config publications
A same-object publication can follow an in-place edit or a source-only
provenance copy, so identity equality must not withhold the session change.
Only a distinct object that compares equivalent is withheld.
* chore(config): drop an unrelated send.ts assertion-baseline entry
This branch does not touch src/gateway/server-methods/send.ts, and main
carries no baseline entry for it, so the allowance only widened the safety
baseline. Removing it keeps the change to a shrink.
* test(config): pin each boundary the republish guard decides
The guard withholds the session change only for a distinct snapshot whose values and resolution provenance both match the published one. Add a table of cases at the config owner that republishes after the same first publication: a distinct equivalent object is withheld, the published object republished without an edit invalidates, and equal bytes whose provenance changed or was dropped invalidate. Extend the projected-row test so equal bytes with changed provenance and a source-only republish both dirty rows and drain back to clean.
Each boundary is pinned by its own cases: removing the guard fails the two withheld cases, ignoring provenance fails the two provenance cases, and withholding same-object publications fails the same-object and source-only cases.
* fix(config): withhold a source-only republish that changes no row input
A value-identical config.apply only restamps the file's meta, so the gateway takes its effective-config-unchanged branch: the runtime object stays and only its source advances through setRuntimeConfigSourceSnapshotIfCurrent. That republishes the same object, which the guard always invalidates, so every projected session row was still rebuilt on each such apply.
Session rows read only the runtime object, never the source snapshot. A source-only republish can change them only through an in-place edit of the runtime object since its last publication, or through the resolution facts copied onto it. The setter now checks both, comparing the object's hash with the fingerprint recorded at its last publication and the serialized facts before and after the copy, and withholds the session change only when neither moved. Every other same-object publication still invalidates.
The config-owner case advances the source three times: a meta-only rewrite is withheld, while changed provenance and an in-place edit each invalidate. Each of those checks fails when its half of the condition is removed. The projected-row case drives the secrets source-advance path that config.apply takes and fails on the previous head with all four rows dirty.
* docs(gateway): record the source-only republish exception to broad refresh
The session row projection guide says same-object config publications retain broad refresh behavior. The config owner now withholds the session change for one same-object case, a source-only republish that neither follows an in-place edit nor changes resolution provenance, so state that exception and its exact preconditions next to the rule it narrows.
* fix(config): compare a republish against the recorded publication
The guard withheld the session change for a distinct object that configSnapshotsMatch found equal to the previous snapshot. That compared the live previous object, so publishing a mutable config, editing a session-row field such as the default model on it in place, and then publishing a distinct object carrying the edited values matched the already-edited object and emitted nothing, leaving rows built from the earlier publication.
Record the serialized resolution facts at each publication, beside the value fingerprint the metadata already keeps, and withhold only a distinct object whose fingerprint and facts equal that record. The source-only path compares the newer source's facts with the same record, so a provenance change made in place on the published object also refreshes rows.
Tests: two owner table rows (a distinct object matching values, or provenance, changed in place on the published one invalidates), a provenance-changed-in-place step in the source-only case, and a projected-row case for the reviewer's exact sequence, which fails on the previous head with no row dirtied.
* docs(gateway): describe config refresh as validated against the publication record
The guide listed the source-only republish as an exception to broad refresh. It is the same check made a different way: the rule exists to catch an in-place edit or changed provenance, which object identity cannot see and the recorded fingerprint and resolution facts can. State the rule in those terms.
* test(gateway): drop duplicate setRuntimeConfigSnapshot import after main merge
Main now imports setRuntimeConfigSnapshot in the grouped runtime-snapshot import, so the standalone import this branch added became a duplicate identifier (oxlint, tsgo, oxfmt and the line-cap ratchet all failed on it).
* fix(config): classify publication scope from the recorded snapshot
The main merge replaced the unconditional `config` session change with
runtimeSessionChangeScope, which classifies against the live previous object.
When that object was edited in place after it was published, the scope came
back presentation-only, so projected rows built from the pre-edit values were
left stale.
Fall back to the full `config` scope whenever the recorded publication
fingerprint no longer matches the live object, and only otherwise ask
runtimeSessionChangeScope. This restores the PR's separate-object contract for
a config that matches an in-place edit of the published one, while keeping the
narrower presentation and profile scopes for genuine changes.
* fix(config): include resolution provenance in previous-publication drift
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Retain focused test-only plans while source and support module edits select the inventory guards. Align planner assertions with that safety contract and use typed nonmutating summary sorting. Preserve native merge rejection coverage with future-dated fixtures after #162249 retired twelve-hour review expiry; older-review acceptance remains covered by the review-gate unit suite.
Limit supplemental protected-test expansion to depth two instead of whole owner areas. Preserve the previous PR selection behind OPENCLAW_CI_NODE_SELECTION=full and summarize selected files and rules. Keep six non-import inventory guards on source edits, including the wrapper and swap-fixture regressions from #162200 and #162246. Scheduled and ordinary release inventories stay complete.
## What Problem This Solves
A failed package.json overlay write could leave openclaw.plugin.json modified in the source checkout during plugin packaging.
## User Impact
Failed temporary packaging writes now restore both source files to their original bytes and preserve the original error. Successful packaging, generated metadata, and publication policy are unchanged.
## Why This Change Was Made
The two overlay writes ran before the existing restoration try/finally. Move them inside that same lifetime so cleanup also owns failures during setup. The restoration order and callback behavior stay unchanged.
## Evidence
- Blacksmith Testbox `tbx_01m3twvthghx89ftszttge14nd`: the new regression fails against the original source at the manifest-byte comparison and passes with the fix. It confirms that the first overlay was active, injects exactly one second-write failure, retains the same Error object, refuses the callback, and compares both originals byte-for-byte, including CRLF formatting.
- Full primary suite: 60 tests passed; single-worker wall time 35.56s. The new regression is synchronous fixture I/O with no timer or subprocess. Existing cases account for the suite's package-install/process cost.
- Full ClawHub metadata, capability-catalog artifact, and CLI argument sibling suites: 23 tests passed.
- Independent isolated Codex review: no P0–P2 findings.
- Changed checks and SDK surface passed; madge and runtime import-cycle checks both reported zero.
No release, deployment, schema, config key, or public wire-format change.
### Pre-existing hosted CI failure
Run https://github.com/openclaw/openclaw/actions/runs/36821570250 failed `check-lint-core-2` and cancelled dependent coverage. All seven diagnostics also occur unchanged in the same job of the unrelated current-head run https://github.com/openclaw/openclaw/actions/runs/36822965519 (head `2ca2ff4326be38e6c890f2c80dddb03c84e06c79`). None of these core paths is changed by this two-file packaging fix, and the failing core modules/tests do not import the changed packaging helper or its root test. The exact-head remote tests/checks above passed; cancelled hosted coverage remains unrun.
- `src/proxy-capture/store.kernel.ts`: 'OpenClawStateKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this intersection type. (`typescript(no-redundant-type-constituents)`).
- `src/channels/message/ingress-queue.test.ts`: Type parameter TColumn is used only once in the function signature. (`typescript(no-unnecessary-type-parameters)`).
- `src/state/openclaw-state-read.types.ts`: 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. (`typescript(no-redundant-type-constituents)`).
- `src/config/sessions/session-transcript-projection-rebuild.ts`: 'OpenClawAgentKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this intersection type. (`typescript(no-redundant-type-constituents)`).
- `src/infra/delivery-queue-sqlite.ts`: 'OpenClawStateKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this union type. (`typescript(no-redundant-type-constituents)`).
- `src/infra/deferred-plugin-migrations.test.ts`: Require 'compare' argument. (`typescript(require-array-sort-compare)`).
- `src/infra/update-repair-agent.self-lease.test.ts`: Require 'compare' argument. (`typescript(require-array-sort-compare)`).
* perf(ui): remove boot facades by isolating the desktop bundle
noVNC's optional browser codec detection uses top-level await. Rolldown
therefore disables common-chunk and dynamic-entry optimizations across the
entire Control UI graph, retaining tiny initializer facades for unrelated
chat and new-session boot imports.
Bundle the unchanged desktop dependency separately through Vite and emit
its prebuilt module into the normal asset/compression/manifest pipeline.
Keep the desktop owner's dynamic import and awaited exports, every existing
application lazy boundary, and strictExecutionOrder. Match measured boot
entries inside merged chunks so route preload templates remain complete.
Do not change the generated boot manifest or its generator.
Same-host production measurements (before -> after):
- Chat boot: 55 -> 36 files; JS 48 -> 29 requests;
JS gzip 1,464,950 -> 1,462,405 B; facade/CSS wrappers 23 -> 4.
- New-session boot: 56 -> 36 files; JS 49 -> 29 requests;
JS gzip 1,544,068 -> 1,541,477 B; facade/CSS wrappers 24 -> 4.
- Initial entry: 7 -> 7 JS requests; gzip 365,248 -> 364,770 B.
- All 68 stylesheets remain byte-identical. Largest JS is 211.3 KiB
against the unchanged 215 KiB limit. Lower the gzip baseline to 364,757 B;
the final build is 13 B higher, within the existing variance allowance.
Retain the index facade and three CSS wrappers per route. An isolated
experiment excluding the HTML entry from its initial group expanded startup
to 25 JS requests / 1,440,065 B gzip, so reject it. CSS initializer exports
prevent Vite's pure-CSS cleanup; retain their stylesheet insertion ordering.
Validation: frozen dependency install; ui:build and ui:check-performance;
tsgo:ui and tsgo:core; both cycle checks report zero; targeted oxlint,
oxfmt and diff checks; 102 focused tests across five files; six bundled
browser cases covering chat send/render, new-session catalog startup and
the real noVNC client against a scripted RFB peer. Independent Codex P2
review returned scoped-clean.
The new isolated-runtime build regression exercises the original TLA
failure, merged preload coverage, independent lazy initializers and awaited
desktop exports. Single-worker file cost: 13.18 s wall, 676 ms test time.
No live Gateway, latency, service-worker or web-push campaign was run.
* perf(ui): budget route boot requests
Guard the facade reduction against optional top-level await disabling
Rolldown chunk optimizations again. Enforce 32 JavaScript requests for each
chat and new-session boot set: 29 measured requests plus 3 headroom. Keep
legacy builds without route preload templates on the existing null path,
and print the route request limit alongside each measurement.
Document the facade-regression reason and cover both routes at 32 requests
and one over the limit with cheap metric fixtures. Both over-limit cases
failed on the original evaluator because it returned no violation.
Validation: ui:check-performance passes on the existing build without a
rebuild; chat and new each report 29 requests against the 32-request limit.
All 54 focused performance tests pass. Test file cost: 26.56 s wall; new
chat/new cases: 1 ms combined. Targeted oxlint, oxfmt and git diff --check
pass. Independent Codex P2 review completed before committing.
* test(ui): resolve recovery chunks from bundled source maps
Restored Rolldown optimization removes the login-gate and placement-startup
facades. Both recovery E2Es still intercepted the old facade filenames,
so the login test never injected a failure and the cloud reload test never
observed or held its runtime request.
Expose the existing bundled fixture's build directory to test workers and
resolve each target source module to its actual emitted implementation via
source maps. Retain the login gate's lazy-admission, failed-load and reload
assertions, plus cloud recovery's held-load ordering, attachment restoration,
delivery check and no-replay assertions. Product code is unchanged.
At base f1f0157815, both exact filtered commands passed twice. Before the
repair, each command failed once on this branch at the same assertion as CI
run 36674183907. After repair, both targeted cases pass; three consecutive
full-file runs passed all 24 tests (72 executions), with wall times 53.78,
53.29 and 62.52 seconds. In the final run, the lazy recovery file took
36.753 seconds and the cloud startup file 5.385 seconds of test time.
ui:check-performance still passes on the existing build: chat/new each
29 JS requests against a 32-request budget; initial JS gzip 364,770 B.
Typed lint passed without diagnostics using the checkout's supported CI
syntax --only=core --split-core --core-stripe=4/5. The requested bare
--core-stripe=4 was rejected by its argument parser. Targeted oxlint,
oxfmt and diff checks pass; independent Codex P2 review precedes commit.
* fix(test): declare UI build root in root test context
Register controlUiE2eBuildRoot beside controlUiE2ePrebuiltAssets in the
root-owned E2E setup augmentation. Root test projects explicitly load this
owner, while UI test projects retain their existing shared-preview context
declaration. No runtime code, casts, or suppressions change.
Validation: full pnpm tsgo:core:test (27 projects), pnpm tsgo:test:root
(all four root projects, including test-root-e2e and test-root-other), and
pnpm tsgo:ui pass. This checkout routes the root projects through the
separate tsgo:test:root command.
Both recovery E2E files pass all 24 tests. Typed lint core stripe 4/5 with
--threads=1 passes without diagnostics; oxfmt and git diff --check pass.
Independent Codex P2 autoreview returned scoped-clean.
* test(ui): resolve bundled asset requests from build output
Facade optimization removes dynamic-entry filenames, so E2E routes that guess
those filenames can silently miss the requested implementation. Resolve all
55 audited matcher/provenance sites from source maps, raw asset bytes, route
preloads, or the emitted HTML entry list.
Preserve selective import-failure coverage in one production-mode fixture
using the existing includeBootGroups=false chunking option. Keep shipped
grouping in the other suites and retain every assertion and test case.
Validation: full bundled UI E2E command ran all 688 configured files locally
in an isolated Linux container: 684 passed, 3 failed, 1 skipped; 3270 tests
passed, 2 failed, 2 skipped (6111.70s). Both Unicode download-name failures
and the native-plugin preview startup timeout reproduce on f1f0157815 in
the same image. The desktop-resize case retains its external-fixture gate.
The new aggregate fixture passed 15 existing cases in 46.085s including its
private production build; runtime-load passed 3 cases in 26.550s.
Full core:test (27 projects), test:root (4 projects), UI types, typed lint
stripe 4/5, touched-file lint/format, ownership guard, and diff checks pass.
Independent review found no actionable P0-P2 findings.
* perf(ui): preserve startup and route budget headroom
Restore the startup baseline exactly from main at 371771 B. The facade
optimization saves only a few hundred startup bytes; retain main's shared
headroom for other UI changes.
Allow 35 route-boot JS requests: chat/new measured 31/32 on main
098173f9f5 plus this PR, leaving three requests above the maximum while
still catching the roughly 19-request facade regression. Update the
35-pass/36-fail boundary cases and the development note.
Existing-build performance check passes: startup JS 368377 B against
372347 B; chat/new requests 31/32 against 35; largest JS 211027 B against
220160 B. All 54 performance tests pass (10.37s wall, one worker), and the
updated boundary cases fail against the old limit. Formatting, lint,
diff checks, and independent review pass.
* test(ui): scope desktop proof asset provenance
Recording every served JS chunk made desktop proof export reject 41 assets
with Invalid served asset identity after the node E2E passed, before SSH ran.
Capture entry and desktop assets plus the isolated novnc- runtime, and admit
that new family in the bounded exporter. Preserve all hash, completion,
geometry, ownership, and cleanup assertions.
Extend the existing complete-export fixture to include desktop and noVNC
assets, assert their preserved identities, and reject unrelated shared chunks.
The regression fails on the original exporter with the reproduced error.
Linux Blacksmith Testbox proof: acbf17f26d1b reproduced the export failure;
main f57a952ab0 and the candidate both completed node and SSH carriers.
Candidate E2E command costs: node 38.307s, SSH 33.390s. The 99 helper tests
passed in 52.38s wall including compiler preparation. Full core:test (27
projects), typed lint stripe 4/5, formatting, diff checks, and independent
P2 review passed.
main measures 4587 public exports and 2693 callable exports, one below
the pinned 4588/2694, so plugin-sdk-surface-report.test.ts fails on main
and on every PR merge ref (first seen on #162307 and #162434). Shrink-only
ratchet update to the exact current counts.
The native iOS chat shows reaction chips under saved prompts and replies
(emoji, count, highlighted when the viewer reacted, VoiceOver lists who
reacted), toggles the viewer's own reaction on tap, and offers "Add
Reaction" in the message long-press menu with the Control UI's quick
palette plus "More…" for any single emoji. Updates arrive live from the
session.reaction event; nothing notifies.
Reaction state is route-bound (ChatViewModel+Reactions): listed on load,
switch and reconnect, applied from events, written with stale-response
guards keyed by route, agent, session, session id, message id and a
per-message revision. The permission projection ports the web's
canReactToSession rule from hello role/scopes/sessionCap/methods and the
session sharing role; the viewer's profile id comes from presence or
users.self. Message ids are transcript ids from __openclaw.id, never local
row UUIDs or optimistic rows. Every row now carries the long-press menu
on every platform.
Proof: pnpm ios:build, 48 Xcode unit tests, 2 snapshot UI tests with
before/after screenshots in the PR, 14 shared Swift Testing cases,
swiftlint clean, protocol and native i18n checks, Codex branch review
clean.
Use the existing PR-exempt inventory, policy watches and import graph to
select Control UI browser proofs for changed route/component owners, while
retaining five cross-cutting smoke files and tests without proven ownership.
Shared UI, harness and build inputs retain full coverage. Publish each
selected file and its reasons; OPENCLAW_CI_UI_E2E_FULL restores full PR runs.
Scheduled main and full release keep all 654 Control UI files and the
separately owned 36 real-Gateway files.
A representative usage diff selects 239/654 Control UI files. Committed
weights project test work from 441.171 to 190.808 seconds; with a conservative
200-second setup reserve this is 6m31s, not a measured 5.5-minute result.
Natural PR timing remains follow-up. Ten causal historical failing PR runs
across nine PRs retain their failing files (zero misses); eight use shared
fallback, so narrow-selection backtest evidence remains limited.
Proof: Linux full test-types, full tooling with final affected-file deltas,
explicit-path check-changed, boundary lint, source contracts and architecture;
seven real manifest preflight cells cover same/different workflow SHA,
PR/schedule, kill-switch true/1 and full release. Independent P2 review clean.
Whole unit-fast: 1,498 files / 16,784 tests passed. Whole unit-fast-isolated:
138 files / 1,669 tests passed. Final affected planner proof repairs all
candidate failures from the whole-tooling run. Remaining tooling failures
are unchanged-parent PR review-expiry, Windows partition, and update-backup
fixture mismatches. Production behavior, browser assertions, screenshots,
workers and deadlines are unchanged.
* refactor(cron): await standalone quarantine registration
* docs(db): refresh quarantine worker inventory
* test(cron): colocate legacy crontab warning coverage
Move the existing warning cases to their owning suite while preserving their assertions. This leaves room for the quarantine SQL regression under the Doctor fixture line-count ratchet and avoids unnecessary SQLite fixture setup.
* docs(sqlite): refresh worker inventory after main merge
Regenerate the existing inventory from the merged source. Keep the quarantine classification and PR production delta unchanged.
## What Problem This Solves
Production code still carries duplicated narration over function names, types, branches and CSS selectors. Some of that prose has drifted: Zalo polling is described as development-only even though it is the default production route, and a joined hook helper is called fire-and-forget.
## User Impact
No user-visible behavior changes. Runtime logic, templates, CSS declarations, configuration, persisted state, wire formats, public API documentation, licenses and lint-suppression reasons remain intact.
## Why This Change Was Made
This maintainer-requested cleanup removes redundant internal helper/registrar summaries, repeated section labels, and obsolete inline font-size history. Existing declarations and shared owners already express these facts; no new abstraction is needed. Comments explaining authority, lifecycle, ordering, cleanup, platform constraints, dependencies and public contracts stay.
The measured reduction is 696 net production/tooling lines: 604 standalone comment lines and 92 adjacent blank lines, plus 59 inline comment removals without net line savings. No tests or generated files changed. This is a bounded contextual sweep, not a claim of exhaustive repository coverage; the local census records exact findings, retained candidates, and unread files. Filename-header cleanup from #161768 is excluded.
## Evidence
Independent review completed; all accepted documentation findings were addressed by restoring base comments. The remaining changed files are byte-identical to the reviewed and remotely frozen candidate.
Blacksmith Testbox validation:
- Parser comparison: identical non-comment TypeScript tokens and CSS structure.
- Both import-cycle checks: 0 cycles.
- Focused existing tests: 40 Vitest shards passed (521.71 seconds).
- Plugin contracts: 48 files / 1,153 tests passed.
- Plugin, source-to-extension, and SDK/package import-boundary checks passed.
- Feishu asset hook check: no build hooks; no plugin browser/control-UI source changed.
SDK API comparison passed with no API changes. The full changed-file gate passed remotely. The lowered-threshold duplicate census (12 lines / 80 tokens) completed; its raw 155 records include deliberate probe/fixture matches and are not claimed as removable production code. No tests were added or changed.
Public JSDoc was audited independently: the SDK API comparison strips comments, while shipped declarations can preserve them, so API-shape equality alone would not prove documentation preservation.
### Inherited hosted CI failure
Exact-head [CI run 36815106181](https://github.com/openclaw/openclaw/actions/runs/36815106181) tested `ff26a4c05d3b41d25477df41cb94010c6cac5cb0` merged with main `75d1f82c18`. The only failing test job was `checks-node-compact-small-19`: `test/helpers/openclaw-test-instance.acquisition.test.ts:33`, “keeps an absent Gateway unreachable while retaining its port claims,” expected `free` but received `busy`. The other failure is the aggregate CI gate. This attempt has 84 successful jobs, 16 skipped jobs, and 88 collateral cancellations; cancelled coverage is not counted as passing.
The identical assertion and error were independently verified in [job 110052175838](https://github.com/openclaw/openclaw/actions/runs/36763489663/job/110052175838), the latest attempt (1) for unrelated PR #162070's final head `d07a6981d4`. The acquisition test, instance helper, cleanup wrapper, isolated-state writer, port allocator, claim owner, claim-lock owner and TCP probe are byte-identical between that head and this PR. No changed file participates in the failing acquisition/probe path. The failure precedes Gateway startup; the logs do not identify the competing listener, so no root-cause repair is claimed.
Landing uses the maintainer-authorized inherited-failure exception pinned to this exact head, backed by the passing remote gates above. The fixture defect remains with the main-CI coordinator. No workflow rerun, test weakening, timeout increase, or source repush was used to obtain green. GitHub's GraphQL writer rejected auto-merge because its quota was exhausted; the request was reconciled as absent before selecting the supported REST squash path.
Related: #140086, #141885, #156535, #157838
## What Problem This Solves
A running Gateway doesn't see a newly downloaded hosted model catalog until it restarts. This PR publishes each accepted catalog through the existing prepared-runtime owner, without a restart. Model rows and their prices switch together as one generation, which keeps the invariant from #140086.
## User Impact
- Compatible downloads are adopted at the Gateway's background catalog check, or after an explicit `models.list` refresh. That refresh returns the currently accepted rows right away and runs adoption afterward.
- A turn admitted on catalog N keeps N's rows and prices until it finishes. New turns use N+1. Rows and prices are never mixed.
- A concurrent auth or config publication no longer postpones adoption to the next scheduled check (up to 6 h). Adoption waits for that publication to settle, then retries, up to 3 attempts.
- An owner whose build failed or timed out ends the adoption instead of waiting on unbounded work. Gateway shutdown cancels an adoption that is still preparing.
- Malformed, schema-invalid, too-new (`minVersion`) and older catalogs are rejected, and the previously accepted catalog stays in use.
- Changing `models.catalogRefresh.url` no longer needs a restart: the previous source's catalog stops applying, and the mirror's catalog is adopted at the next catalog check.
**Bad-catalog exposure:** with live apply, a *valid but wrong* published catalog reaches running Gateways at their next catalog check (at most every 6 h) or on the next explicit `models.list` refresh. It no longer waits for a restart. Recovery uses existing mechanisms only:
- Republish a corrected catalog with a newer `generatedAt`; Gateways adopt it the same way.
- Operators can set `models.catalogRefresh.enabled: false`, which withdraws remote rows and prices without a restart (covered by the Gateway integration test).
This PR adds no new kill switch, config option or env knob.
### Compatibility
No config keys, defaults, types, validation, stored rows, protocol or SDK contracts change. The only config-surface change is the `models.catalogRefresh.url` help text, which drops the stale "Changes apply after a Gateway restart" sentence, and its regenerated config-doc baseline hash. Existing configs validate unchanged and need no Doctor migration (maintainer confirmation: https://github.com/openclaw/openclaw/pull/158000#issuecomment-5913462783). Startup behavior is unchanged. Upgrade impact for existing installs: an accepted download activates at the next catalog check instead of the next restart.
## Why This Change Was Made
- `prepared-model-runtime.configured-refresh.ts` builds a complete candidate generation of the configured owners under the new catalog. One serialized commit then publishes rows, the accepted bundle, the pricing context and the reply-dispatch projection together.
- Adoption re-reads the stored catalog until the config it read under is still current, so a stale caller can't cancel a current adoption.
- Each preparation attempt has its own abort signal. A config advance restarts only the attempt; a newer catalog or shutdown ends the whole adoption, including pricing preparation.
- Between attempts, adoption waits only on publication gates: a pending replacement or an owner's pending publication.
- Adopted owners install the same plugin-retirement recovery as configured publication (#161267). After commit, a lost Gateway plugin loan republishes them through the normal recovery. Before commit, it restarts the adoption attempt, and the commit refuses any candidate whose plugin generation retired.
### Why downloads were restart-only, and what this keeps
Restart-only activation was a mechanism, not the goal. #140086 chose it to stop rows and prices from different catalog versions mixing, and #157838 was merged as "the prerequisite for applying new remote catalogs without a Gateway restart (rows and prices must switch together)". This PR is that follow-up. Every requirement those PRs set still holds:
| Original requirement | Source | How it holds here |
|---|---|---|
| Rows and prices from one catalog version; never mixed across reloads or new requests | #140086 | One serialized commit publishes owners, bundle, pricing context and dispatch; pricing contexts are keyed by the exact accepted catalog. Integration test: new rows appear only with new prices |
| Admitted work keeps its pair | #140086, #157838 | Runs carry their plugin generation's catalog; usage operations capture one pricing context. Integration test and live proof: the in-flight turn keeps the old price |
| Startup absence is a real state (no downloaded rows without prices) | #140086 | Absence → catalog goes through the same atomic commit; overlay absence tests unchanged |
| Worker replacement inherits the host's accepted pair, not a later download | #140086 | The commit updates the inherited pair; later workers and a worker-exit recovery keep it (overlay and integration tests) |
| Current enablement and source URL still gate eligibility | #140086, #156535 | Checked on every read and before adoption, including the default-install v1 fallback; disablement withdraws rows and prices together (integration test) |
| Bad or superseded downloads never replace the active pair | #140086, #141885 | Compatibility, `minVersion`, revision and `generatedAt` checks; stale reads can't cancel a current adoption (regression test) |
| Failed catalog checks retry at the remaining fresh interval, not a full TTL | #141885 | Unchanged scheduler behavior; the deleted notice test's retry case is restored for failed adoption (fails if the retry falls back to the full TTL) |
| Operators learn when a downloaded catalog is not yet active | #141885 | No longer needed: downloads activate at the next check. The restart notice and its tests are removed; `models refresh` says when a running Gateway applies the update |
| Billing-route prices switch with their rows | #156535 | `upstreamPricing` and `providerPricing` are part of the accepted catalog pair |
## Evidence
**Regressions.** Each fails with its fix reverted and passes with it:
- *Retries a scheduled adoption when its pending auth owner settles.* Runs through the real Gateway update scheduler. Reverted, it logs `remote model catalog check superseded; deferred to the next check`.
- *Does not let a read under a superseded config cancel the current adoption.* Reverted, both calls end `superseded`.
- *Ends adoption instead of joining a timed-out owner build.* Reverted, adoption never settles.
- *Does not hold Gateway shutdown on an adoption's pricing preparation.* Reverted, shutdown waits on the held preparation until the test times out.
- *Recovers adopted owners when their borrowed Gateway plugin retires after commit / before commit.* Without the recovery, both fail: `Prepared model runtime plugin generation retired` and `prepared reply dispatch runtime owner was not published`.
- *Uses the remaining stored TTL after a fresh startup check when adoption fails.* With the retry reverted to the full TTL, the second check doesn't run.
**Suites:**
| Suite | Result |
|---|---|
| `prepared-model-runtime.remote-publication.test.ts` | 10/10 |
| Gateway integration (`models-list.remote-catalog`) | v1 and v2 pass. Config and auth churn during preparation end `published` on the settled owners. Also covers retained admitted runs, rejected and stale bundles, worker replacement and disablement |
| `prepared-model-runtime*`, `server-plugin-reload*`, `update-startup`, and all PR-touched test files | pass |
| `tsgo:core`, all `tsgo:test:src` shards | pass |
| oxlint and oxfmt on changed files; `config:docs:check`, `config:schema:check`; max-lines, assertion-safety and test-timeout-race ratchets | pass |
Tests wait on owned completion signals (`withinTest`), not wall-clock deadlines.
**Live proof** on an isolated Gateway built from `cb8c9197fd` (no provider mocks). Later commits add plugin-retirement recovery for adopted owners, covered by the regression tests above, and rebases onto `main`. It used a real OpenAI key through `openai/gpt-4.1-mini`, and the build stamp was set before the real catalog's publication date. A client polled `models.list` back to back over one WebSocket for the whole run (1023 polls, no errors). One Gateway process (PID unchanged) and no restart:
1. The stored catalog was seeded with an older revision of the real `catalog.openclaw.ai` v2 catalog: generated 2 days earlier, `gpt-4.1-mini` priced ×10, plus one extra kimi row. `models.list` listed the extra row, and a turn priced **$4.00 / $16.00 per M** input/output.
2. `openclaw models refresh` downloaded the real catalog (`updated`, 1039 models). The listed rows didn't change for the next 7.1 s, and a turn in that window still priced **$4.00 / $16.00 per M**: a download stays inactive until the Gateway adopts it.
3. A long turn was admitted on the older catalog, then `models.list {refresh:true}` returned the older rows (extra kimi row still listed) and started adoption. The new catalog was visible 0.9 s later, while the long turn was still running: the extra kimi row was gone.
4. The in-flight turn finished at **$4.00 / $16.00 per M** (older rows and prices). The next turn priced **$0.40 / $1.60 per M** (real catalog).
5. `models.catalogRefresh.url` was moved to a local mirror of the real catalog through `config.patch`, and the mirror's catalog was adopted without a restart. The mirror then served malformed JSON: `models refresh` failed with `SyntaxError`, the model list was unchanged, and the next turn still priced $0.40 / $1.60 per M.
**Model picker during republication (also on `main`).** Right after the new generation commits, `models.list` shows the new generation's configured and static rows until its full catalog loads, then the full list. In the live run this lasted 109 ms. The same poller against a `main` build shows the same window after a `models.*` config reload (20 → 6 → 16 rows for about 350 ms), so this PR adds a new trigger for an existing behavior. It doesn't change it. The short list comes from the new generation, so rows and prices stay paired.
**Published-driver upgrade cells.** Candidate tarball built from a fresh clone at `6f682c7944` with the canonical Docker packaging script and no build-time overrides. sha256 `46d881a0…ef0ad`; embedded commit `6f682c7944`, version 2026.9.7. `6f682c7944` already includes the shutdown-cancellation and pricing-deadline commits. The current head the current head differs from it only by rebases onto `main`: `main` had moved the scheduled catalog check into `update-startup-catalog.ts`, and this PR's adoption call moved there unchanged (`git range-diff` shows no other production change; a `remoteCatalog: null` test-fixture field moved to main's relocated `cli-compaction.test-support.ts`).
| Driver → candidate | Scenario | Result |
|---|---|---|
| `openclaw@2026.9.6` | base | passed (930 s); updater outcome success, no recovery |
| `openclaw@2026.9.6` | plugin-deps-cleanup | passed (923 s); updater outcome success, no recovery |
| `openclaw@2026.9.7` (latest) | base | passed (813 s); updater outcome success, no recovery |
In every cell:
- Migration, post-Doctor config validation, survival, plugin-dependency cleanup and runtime-deps repair checks passed.
- The candidate Gateway logged ready, then its catalog check fetched and saved the hosted catalog about 0.2 s after starting. The hosted catalog is older than the candidate's build stamp, so adoption ends `unchanged`, which isn't logged. After a 300 s settlement window, `/readyz` (`ready:true`, nothing failing) and a Gateway `status` RPC passed, and the Gateway shut down cleanly.
- To show a logged terminal outcome, each cell was repeated with a loopback mirror serving a newer copy of the same catalog. Each check logged `remote model catalog applied` about 0.26 s after it started, followed by `/readyz` and `status` passing.
Not run: `openclaw@2026.9.7` plugin-deps-cleanup. At the previous head it failed inside the 2026.9.7 driver's retained-runtime verification (`Retained runtime entry does not reference its inventoried file: dist/a2ui-…mjs`), identically for a merge-base control package with none of this PR's commits.
Harness note for the 2026.9.7 cell: unchanged, the upgrade harness can't run against 2026.9.7. It seeds the retired `tools.toolSearch {mode:"code"}` setting, which 2026.9.7 rejects. The 2026.9.7 cell used the harness's existing Tool Search "absent" mode, a one-line local change that skips only that seed and its check. The 2026.9.6 cells used the unchanged harness.
**CI:** fully green on the final head ([run 36818367970](https://github.com/openclaw/openclaw/actions/runs/36818367970)). Earlier heads hit failures that reproduce on `main` in code this PR doesn't touch: `update-cli.target-schema` (main reproduction: https://github.com/openclaw/openclaw/pull/158000#issuecomment-5913464830), the type-suppression inventory, the Windows partition owner test and the Windows backup-rename test. Main has since fixed the last three. Config compatibility confirmation: https://github.com/openclaw/openclaw/pull/158000#issuecomment-5913462783.
No overlap with Pash/Sarah changes.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
## What Problem This Solves
Repository scripts retain private copies of shared tooling helpers and an unused translation subprocess runner, increasing maintenance work across sibling scripts.
## User Impact
Internal tooling cleanup: existing flags, diagnostics, generated bytes, JSON formats, exit codes, and cleanup policies stay the same. The build wrapper additionally detects forbidden dynamic-import warnings when compiler output splits the warning marker across chunks.
## Why This Change Was Made
- Remove the retired i18n process runner. Its final production caller moved to the shared formatter in #95534; retain real CLI/privacy/provider tests and run their subprocesses with the existing dependency.
- Share Docker resource, signal-trap, platform, workspace-staging, and plugin-selection helpers while retaining each caller's environment precedence and lifecycle decisions.
- Reuse existing E2E fixture JSON/assertion and package-path helpers; retain distinct write modes, recovery ordering, and mounted/frozen harness contracts.
- Share macOS guest desktop-user/home resolution through its existing owner while preserving per-caller timeout policy.
- Route release/mobile flags through the existing version flag specifications, preserving split-only values, duplicate/mode rules, help timing, and error text. Reuse existing retry sleep and comparator owners and remove an unreachable iOS output branch.
- Reuse existing guard entrypoint, failure-trailer, diagnostic-line, and metadata-normalization owners.
Measured reduction: **732 net production lines**, with test changes counted separately. PR tooling and its protected import closure, CI planners/shards, baselines, and generated artifacts are unchanged.
## Fixes Found Along the Way
The tsdown scanner checked each raw output chunk for its warning marker. It now checks the existing combined-line buffer. The regression exercises every split inside the marker and fails on the original source for the intended missing-warning assertion.
## Evidence
- Blacksmith Testbox `tbx_01m3tp4sabwd0807kj9jkqqmbr`: frozen dependency install and candidate source-byte verification; all nine changed test files and 59 selected sibling files passed, including real CLI, package fixture, recovery, Docker harness, and Parallels transport contracts.
- Generated channel metadata is byte-identical; differential metadata normalization cases passed.
- SDK surface check passed. Independent isolated Codex review completed with no P0–P2 findings.
- Initial changed checks caught an invalid direct source import in a proposed snapshot-distance consolidation; that independent change was withdrawn. No boundary exception was added. The final changed checks pass, including script/test lint and Docker shell/scheduler checks; madge reports 0 cycles and the runtime import check reports 0 cycles.
- Per-file single-worker wall time for changed suites: translation 9.92s; Docker helper 38.88s; live Docker auth 1.71s; mobile ref 2.36s; mobile release 6.23s; release preparation 2.42s; version 2.04s; installer 15.01s; tsdown 2.70s. The existing Docker helper suite executes shell/process cleanup and container-command boundary fixtures; new assertions reuse those fixtures. New parser/scanner cases use no sleeps or polling.
Hosted CI will be verified against this PR's exact pushed head. No live deployment or release was performed.
* feat(sessions): import Claude Code and Codex transcripts into OpenClaw
Add sessions.catalog.import, which preserves a native catalog transcript
(Claude Code, Codex, OpenCode, Pi, or shared OpenClaw sessions) as an
ordinary OpenClaw session so it survives the source tool's cleanup or the
loss of the source computer. It reads through the existing catalog read
path, so Gateway-local, headless node, macOS app, and Linux app sources
all work without app changes.
Re-importing the same source reuses a deterministic agent-scoped session
key and appends only items not yet imported, so repeated imports act as
an explicit sync. Imports keep up to 50,000 items or 64 MiB (newest first)
and report complete: false when older history is cut. Continuation keeps
its 200-item / 512 KiB seed; adoption, continue, and fork are unchanged.
Surfaces: the sessions.catalog.import Gateway RPC (operator.write, same
row visibility as sessions.catalog.read), `openclaw sessions import`
(single transcript or --all with paging, --dry-run, --json), and an
"Import to OpenClaw" item in the Control UI catalog row menu. The shared
catalog history reader now pages at 50 items, the Claude and Codex
transcript read cap.
* chore: merge main into transcript import branch
* fix(sessions): fence catalog import source access through commit
Retain the catalog visibility owner's matched source and require current
read authority for destination creation, transcript appends, and state events.
Compare access facts without rejecting ordinary transcript generations.
Cover forbidden readers, pre-write sharing revocation, and mid-append
revocation through real Gateway owners. Share the integration state fixture.
Use typed CLI options and local paging state to satisfy assertion and lint gates.
* fix(sessions): default catalog imports to drafts
Preserve published copies on re-import and honor the Gateway no-drafts policy. Split cheap import authorization checks from retained release-tier owner integration proof. Refresh the Workboard asset manifest required by the generation gate.
* Merge remote-tracking branch 'origin/main' into steipete/transcript-import-mode-a262c5
* test(ui): expect the import action in adopted catalog session menus
* Merge remote-tracking branch 'origin/main' into steipete/transcript-import-mode-a262c5
* Merge remote-tracking branch 'origin/main' into steipete/transcript-import-mode-a262c5
* Merge remote-tracking branch 'origin/main' into steipete/transcript-import-mode-a262c5
* Merge remote-tracking branch 'origin/main' into steipete/transcript-import-mode-a262c5
* Merge remote-tracking branch 'origin/main' into steipete/transcript-import-mode-a262c5
# Conflicts:
# docs/nodes/session-catalogs.md
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* build: generate Kysely declarations at build time
Prepare ignored type-only schema projections through existing install, build, compiler, test, SDK API, and package-boundary entrypoints. Preserve existing import paths and generated bytes, cache unchanged inputs, and cover unbundled SDK declaration consumers. No SQL, data, or runtime migration.
* build: finish Kysely preparation contract integration
Point explicit test roots at the type entrypoints and exercise generation through its production preparation owner without a test-only export.
* build: include Kysely generator in trusted tooling archive