* fix(cron): structural top-of-hour match in stagger heuristic
Top-of-hour detection used includes('*') on the hour field, accepting
malformed tokens like '5*'. Match only '*' or '*/N' structurally.
* fix(cron): preserve wildcard hour lists
* fix(cron): support question-mark wildcard
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* [AI] fix(memory): self-heal missing index identity by initializing provider during sync
When a gateway's memory index identity becomes "missing" with chunks
already indexed, canRebuildMissingIdentity stays false because
this.provider is null (async provider init hasn completed yet), so
needsMissingIdentityReindex is false, and the sync loop bails out
with dirty=true forever — the gateway never self-heals.
Now, when indexIdentity.status is "missing" and a provider is
configured but this.provider is null, runSync() calls
ensureProviderInitialized() first, then re-evaluates the identity
state. If the provider becomes available,
canRebuildMissingIdentity flips to true, unlocking the self-heal
reindex path.
Refs #91167
* [AI] fix(memory): allow FTS-only self-heal when chunks are all FTS-only and provider unavailable
When a gateway's memory index identity is 'missing' with chunks already
indexed, canRebuildMissingIdentity stays false if the embedding provider
is unavailable, causing the sync loop to bail out with dirty=true forever.
The previous approach (calling ensureProviderInitialized inside runSync)
was redundant because the public sync() method already initializes the
provider before runSyncWithReadonlyRecovery.
The real fix: when every existing chunk has model='fts-only', rebuilding
the index as FTS-only is safe — no semantic data is lost. So
canRebuildMissingIdentity should also be true when hasOnlyFtsChunks,
even if the provider is unavailable.
Also adds hasSemanticChunks() helper to detect whether any chunks have
a non-fts-only model.
Non-forced test: seeds FTS-only chunks with no meta, syncs without
force, verifies identity transitions from 'missing' to 'valid'.
Refs #91167
* [AI] fix(memory): gate hasSemanticChunks scan to missing-identity path only
Only compute hasOnlyFtsChunks when identity is missing, chunks exist,
and the provider is unavailable. This avoids scanning the chunks table
for model classification on every ordinary sync.
* test(memory): protect semantic index self-heal
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(control-ui): make bootstrap config endpoint base-path-relative (#66946)
CONTROL_UI_BOOTSTRAP_CONFIG_PATH embedded a hard-coded /__openclaw prefix
instead of being base-path-relative. When the Control UI is served under
/__openclaw__/, both the gateway and the browser loader compose
${basePath}${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}, producing the doubled
/__openclaw__/__openclaw/control-ui-config.json URL that 404s.
Make the constant base-path-relative (/control-ui-config.json) so the
composed URL is correct under any base path, align the Vite dev stub and
the docs, and add gateway.controlUi.basePath "/__openclaw__" coverage.
* fix(control-ui): serve bootstrap config at default __openclaw__ entry (#66946)
The reporter runs the default deployment (no gateway.controlUi.basePath),
so the Control UI SPA is mounted under the default /__openclaw__/ namespace.
A browser opening that entry infers basePath="/__openclaw__" from the URL
(inferBasePathFromPathname) and fetches /__openclaw__/control-ui-config.json,
but an empty-base-path gateway only served the bare /control-ui-config.json,
so the default-entry bootstrap request 404'd and chat never finished loading.
Make handleControlUiHttpRequest also accept the default-namespace alias
/__openclaw__/control-ui-config.json when no base path is configured. The
alias is derived from the existing CONTROL_UI_NAMESPACE_PREFIX mount constant
and is purely additive: the bare /control-ui-config.json endpoint and the
configured-base-path endpoint are both preserved (no route removed).
Add gateway HTTP coverage for the real default-entry scenario (empty base
path + /__openclaw__/... request) that fails without the alias, alongside the
configured-base-path, bare-path compatibility, and doubled-path 404 cases.
* fix(control-ui): preserve legacy bootstrap endpoint as compat alias (#66946)
Current main and v2026.6.1 serve and document the single-underscore
/__openclaw/control-ui-config.json bootstrap endpoint under an empty
base path (that literal was CONTROL_UI_BOOTSTRAP_CONFIG_PATH before the
path was made base-path-relative). Making the constant relative dropped
that match, so older bundles and clients hitting the documented endpoint
would 404 after upgrading.
Accept the legacy single-underscore path as an empty-base-path
compatibility alias in matchesControlUiBootstrapConfigPath, derived from
the legacy /__openclaw namespace joined with the canonical config
constant (so it tracks any filename rename) and named
LEGACY_BOOTSTRAP_CONFIG_PATH with a comment. The canonical
/control-ui-config.json and the default-namespace
/__openclaw__/control-ui-config.json aliases are unchanged; only this
path is added. The doubled /__openclaw__/__openclaw/... path still 404s.
Add a focused regression that the legacy endpoint returns config under an
empty base path; it 404s without the alias (verified non-vacuous).
* fix(control-ui): preserve legacy bootstrap route under configured base path (#66946)
The previous revision preserved the single-underscore
/__openclaw/control-ui-config.json bootstrap endpoint only under an empty
base path. A deployment with a configured gateway.controlUi.basePath
(e.g. /x) served and documented that endpoint at
${basePath}/__openclaw/control-ui-config.json before this PR made the
config path base-path-relative, so configured-base-path users, older
bundles, and clients that still request it would 404 after upgrading.
Extend matchesControlUiBootstrapConfigPath so the legacy single-underscore
suffix is accepted under every base path, not just the empty one. The
matcher now checks the canonical and legacy suffixes uniformly as
${basePath}${CONTROL_UI_BOOTSTRAP_CONFIG_PATH} and
${basePath}${LEGACY_BOOTSTRAP_CONFIG_PATH} for both the empty and
configured cases, reusing the existing LEGACY_BOOTSTRAP_CONFIG_PATH
constant (no new hard-coded literal). The default-namespace
/__openclaw__/control-ui-config.json alias stays empty-base-path-only
(it is the path the inferred default entry requests when no base path is
configured). All three empty-base-path behaviors are unchanged; the
doubled /__openclaw__/__openclaw/... path still 404s under both an empty
and a configured base path.
Add a focused regression that the configured-base-path legacy endpoint
returns the bootstrap config; it 404s without the alias (verified
non-vacuous). No CHANGELOG.md change.
* fix(ui): mount config stub under vite base
* fix(ui): preserve default config stub route
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(openai): remove chatgpt-responses transport override from gpt-5.3-codex catalog entry
The static catalog entry for gpt-5.3-codex hardcoded
api: "openai-chatgpt-responses" and baseUrl: "https://chatgpt.com/backend-api",
forcing all users through the ChatGPT backend — which requires OAuth, not
a standard API key. This broke gpt-5.3-codex for every API-key user after
v2026.6.1.
Remove the transport overrides so the model inherits the provider defaults
(openai-responses + api.openai.com/v1), restoring v2026.5.18 behavior.
OAuth/Codex users are unaffected — dynamic model resolution in
shouldResolveDynamicModelThroughCodex handles the ChatGPT routing based
on provider config, not the static catalog entry.
* ci: retrigger opengrep scan (transient install failure)
* fix(models): keep bundled provider catalog when configured base URL is blank
A models.providers.<id> entry with a blank baseUrl ("") erased the
bundled provider catalog from the generated model registry. The empty
base URL flowed into provider discovery and the catalog merge, where it
overrode the bundled transport URL; the resulting provider then failed
isWritableProviderConfig and was dropped from models.json entirely.
For Google this meant gemini-2.5-flash and gemini-flash-latest stopped
resolving on the embedded runtime with model_not_found, even though the
bundled Google catalog and forward-compat resolver know those ids. This
hit users in merge mode whose config was partially written by a plugin.
Strip blank provider base URLs before discovery and merge so a blank
value means "use the provider default" instead of clobbering it.
Fixes#91270
* test(models): cover blank provider base url
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): classify harness provider mismatch as format error (#91710)
When an agent harness rejects a model because the provider id is not in
its supported set, the error message was unclassified — falling through
to reason="unknown" in the model fallback notice. This made harness
provider mismatches (e.g. stale codex plugin rejecting openai/gpt-5.3-codex)
invisible to the user.
Add a format error pattern for the harness rejection message so the
fallback notice reports "format" instead of "unknown".
* ci: retrigger with real behavior proof in PR body
Adds `tsIso`, `runAtIso`, and `nextRunAtIso` to the JSON output of the `cron runs` command. This enhancement derives local-offset ISO 8601 strings from the existing numeric timestamps purely at the display layer, matching the diagnostic log format. The underlying SQLite storage, protocol schema, and raw numeric fields remain completely unchanged to ensure strict backward compatibility.
Co-authored-by: Cursor <cursoragent@cursor.com>
A chokidar watcher 'error' permanently disabled config hot-reload with a
single warn. Re-create the watcher with bounded backoff (500ms/2s/5s, 3
retries); on exhausted budget escalate to log.error and flip a persistent
hotReloadStatus to disabled. stop() clears any pending re-create timer.
splitMessagesByTokenShare wrapped each message in a 1-element array and
double-cloned it per message. Sanitize the full array once and pass a
precomputed per-message token count array; totals unchanged, allocations
reduced.
parseXmlAttribute compiled a fresh RegExp on every call in the DeepSeek DSML
streaming parser. Memoize by attribute name via a module-level Map and escape
the name; behavior unchanged (non-global regex).
An anyOf availability group swallowed a nested unsupported-signal authoring
error when any sibling branch parsed as available. Propagate unsupported-signal
diagnostics regardless of sibling availability.
deriveChannelFromKey returned undefined for direct/dm keys, so channel-scoped
send rules never fired for direct chats. Treat parts[0] as the channel for
direct/dm keys too, matching deriveChatTypeFromKey.
MCP channel-bridge notification failures were only logged when verbose,
otherwise swallowed. Emit one low-noise diagnostic always; gate only the
error detail behind verbose.
Honor caller-provided sessionKey values when stale label metadata is also present, and keep denied session-id sends from echoing the resolved canonical session key.
Supersedes openclaw/openclaw#74009 and fixesopenclaw/openclaw#64699.
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Keep state database startup working on filesystems without POSIX chmod support while failing closed for ordinary EPERM ownership failures. EPERM is tolerated only for already-private targets or when a disposable same-directory probe confirms chmod is unsupported.\n\nFixes #91919
* redact tool output secrets
* Expand tool-output secret redaction
* fix(security): keep redaction prefilter in sync with expanded defaults
- build DEFAULT_REDACT_PREFILTER_RE from sources covering every default
pattern family: new vendor prefixes, webhook hosts, bare query/form keys,
userinfo/connection-string passwords, and percent/plus/invisible
obfuscated keys (including trailing separator splices)
- run default-pattern redaction tests through the default options path and
redact the vendor corpus per token so prefilter gaps fail tests
- fix quoted standalone assignment values containing the other quote char
or an unterminated quote; never re-mask *** placeholders
- align net-policy URL query-name separator stripping with logging key
normalization (Hangul fillers)
* fix(security): keep base64-prefix redaction out of media payloads
- pure-base64-alphabet token prefixes (gAAAA, AKIA, ASIA, dapi,
ATCTT3xFfG, ATATT, ATBB) now require a non-alphanumeric left boundary,
skip explicit ;base64, payload spans, and run unchunked so chunk starts
cannot fake the boundary or hide the container from the lookbehind
- tokens after URL/path delimiters or assignments still mask; data-URL
media survives redaction byte-identical (fixes chat media mirror CI)
- regression tests: tiny-PNG data URL, in-blob plus boundary,
chunk-aligned large data URL, reset-path Fernet token, path AWS key
---------
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
Capture the originating sessionKey and agentId for cron wake tool calls so non-main session and multi-agent wakes return to the conversation lane that requested them.
Carry stored delivery context through queued wake events so topic/thread replies route correctly, while preserving the default no-origin wake behavior and explicit target:none opt-out.
Refs #46886.
Refs #64556.
Thanks @anagnorisis2peripeteia.
Co-authored-by: Cameron Beeley <cameron.beeley@gmail.com>