Preserve model-supplied filename identity for mutations while keeping existence-checked Unicode-equivalent fallback for reads.
Co-authored-by: yetval <yetvald@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
`openclaw onboard --help` advertised `claude-cli`, which the command
rejects, and hid `token`, which it accepts and its own error text
recommends. Three surfaces each rebuilt the accepted set independently:
Commander help asked for legacy aliases, the reset preflight added a
hardcoded `BUILT_IN_AUTH_CHOICES`, and the non-interactive dispatcher
added a hardcoded `GENERIC_NON_INTERACTIVE_AUTH_CHOICES`.
`formatAuthChoiceChoicesForCli` is now the single owner of that set. It
emits the generic token-provider choices (`setup-token`, `token`,
`apiKey`) that `AuthChoice` has always declared built-in, so every
surface renders and validates the same list.
Deprecated aliases stay out of it. `includeLegacyAliases: true` arrived
with a mechanical help-text refactor, never as a product decision: the
pre-refactor hardcoded help string listed no legacy alias, the reset
preflight's copy was unreachable because normalization runs first, and
the deprecation error already names the replacement. `oauth` is likewise
normalized to `setup-token` before any validator sees it, so the
dispatcher's `oauth` arm and its slot in the accepted list were dead.
Production LOC: -26.
* fix(gateway): recover credential-file accounts on secrets reload
Preserve independently discovered credential-file degradation across runtime snapshot refreshes, and re-inspect only the affected account when secrets are reloaded. Healthy sibling accounts remain running while status and doctor retain exact-owner diagnostics until recovery or teardown.
* test(gateway): prove credential-file reload recovery
* test(gateway): assert redacted reload error code
Bind scheduler-owned cron, hook, and heartbeat runs to lifecycle-fenced Gateway context so trusted built-in tools resolve after startup or reload without inheriting request client state.
Co-authored-by: Marvinthebored <marvin.assistant@lindsey.jp>
Co-authored-by: Marvinthebored <peter@lindsey.jp>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(cron): reject blank --command-cwd/--command-input before kind forge
Presence-only typeof checks counted blank cwd/input as command-specific
edits, forging {kind:"command"} patches that could convert agentTurn or
script jobs into empty command payloads. Require non-blank values first.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): allow empty --command-input while rejecting blank cwd
ClawSweeper: Gateway command stdin is an unrestricted string, so empty
or whitespace --command-input must still patch through. Keep the blank
--command-cwd reject that prevents forging a command payload with no cwd.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(cron): focus command blank option coverage
Amp-Thread-ID: https://ampcode.com/threads/T-01a0220d-eaa0-76b4-adb9-68841f015b75
* fix(cron): reject blank cwd before edit reads
Amp-Thread-ID: https://ampcode.com/threads/T-01a0220d-eaa0-76b4-adb9-68841f015b75
---------
Co-authored-by: zyw02 <zyw02@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
Two defects in one lifecycle, found by following one thread.
`agents delete` tries the Gateway first and falls back to a local path when the
Gateway is unreachable -- its own header calls this "gateway delegation and local
cleanup fallback". The Gateway path wraps deletion in
`withAgentExecApprovalsRemoved`; the local fallback did not. So deleting an agent
with no Gateway running left its exec approvals allowlist behind, silently.
That residue is not inert. Verified end to end: approve a pattern for an agent,
delete it through the offline path, recreate an agent with the same id, and
`approvals get` shows the old allowlist live under the new agent. The operator
never granted it. The offline path now uses the same journal-fenced,
rollback-capable helper as the Gateway path.
Removal rather than a warning: an exec approval is a latent authority grant, and
leaving one behind contradicts the deletion contract the Gateway already
enforces. `agents delete` already trashes the workspace and agent dir, so
removing a policy entry is not out of character for it.
The second defect is in the shared helper and affects the Gateway path too, which
means it is shipped today. It matched policy keys with
normalizeAgentId(policyKey) === key
and `normalizeAgentId` falls back to `"main"` for anything it cannot represent.
`"*"` normalizes to `""` and therefore returns `"main"`, so deleting the `main`
agent also matched -- and removed -- the `"*"` wildcard allowlist. Strict
normalization now skips unrepresentable keys, so valid aliases are still removed
while the wildcard and unrelated agents survive.
Sibling sweep of `pruneAgentConfig`, which already handled bindings, subagent
allowlists, heartbeat/system-agent ownership, and Talk ownership: broadcast
targets, hook mappings, and hook agent allowlists also retained the deleted id.
Fixed, preserving `"*"` in the hook allowlist.
Follow-ups deliberately not taken here, each needing its own owner or design:
offline deletion still lacks the Gateway's transactional cron-job cleanup;
plugin-owned routing and thread bindings need an agent-deletion lifecycle hook;
remote node approvals need distributed cleanup or explicit warning semantics;
approval `agentFilter` cleanup needs a disabled-state design, since removing its
last entry would widen policy rather than narrow it.
Production +45/-18.
The generic provider-missing-auth error told operators to create a new agent:
No API key found for provider "openai". Auth store: <path> (agentDir: <path>).
Configure auth for this agent (openclaw agents add <id>) or copy only portable
static auth profiles from the main agentDir.
`openclaw agents add <id>` creates an agent. It does nothing about a missing
provider credential, so following it leaves you with a second agent that also
has no key.
Reproduced on a real build: a fresh install with only an Anthropic key, then
`openclaw memory index`. There is exactly one configured agent, `main`, and it is
correctly configured -- nothing about `agents add` applies. Doctor, diagnosing the
identical condition on the same install, gets it right, listing how to supply the
key and how to disable memory search. The command that actually failed gave worse
advice than the advisory check.
`models auth paste-api-key --provider <id>` is the suggestion rather than
`models auth login`, because `login` requires an owning provider plugin while this
generic fallback is also reached for pluginless, inline, and custom providers, and
for a plugin-owned provider that returns no specialized message. Pasting a key
resolves every "No API key found" case the fallback can produce. The `--agent`
hint is folded in for non-default agents, and `formatCliCommand` keeps the command
correct under a profile or container.
One message rather than two: distinguishing "no credential anywhere" from "this
agent lacks a profile another agent has" would need cross-agent store reads the
resolver does not do today, and the combined sentence is correct for both.
Production +1/-1.
* feat(control-ui): stream live draft previews in the typing indicator
Multi-identity sessions now show what a teammate is typing, not just that
they are typing: the composer's per-keystroke session.typing sends carry a
bounded tail of the draft (optional preview field, 400 code points max),
the gateway throttle re-emits on changed payloads at 250ms (boolean-only
stays at 1s, trailing edge keeps the latest draft), and the transcript
renders a per-actor bubble with the live text plus a blinking caret.
Actors without preview data keep the three-dot bubble.
Previews are ephemeral presence: never persisted, never part of the
session transcript or model context, excluded from aria-live regions, and
gated by the existing >=2-live-viewers, sharing-role, and incognito
checks. No new config surface.
* chore(protocol): regenerate Swift gateway models for typing preview
* fix(gateway): aggregate typing previews across same-actor connections
A boolean-only session.typing update from a second connection of the same
actor (another tab or device) erased their live draft preview, because
typing liveness aggregated per actor while the broadcast preview came only
from the latest request. Preview aggregation now lives with the connection
aggregation owner: updateTypingConnections tracks per-connection previews
and returns the newest non-empty preview among live connections, so the
broadcast keeps the active draft until its connection stops or expires.
Regression fails pre-fix (event lost its preview field).
`main` is red: `check-lint-core-5` fails with
ui/src/pages/cron/view.test.ts
1124:4 error File has too many lines (1004). eslint(max-lines)
The file has been growing for a while (1104 lines at fa75cdd01c, 1116 at
2acfc47b7f) and crossed the 1000 code-line cap at a4b3f63a87 (#126945). Every
PR opened since inherits the failure, so this is not any one PR's fault to fix
in passing -- it blocks the merge gate for everyone.
Repo policy forbids a `max-lines` suppression, so the file is split along the
seam it already had: three top-level describes, one of which was two thirds of
the file. `cron view editor` moves to `view.editor.test.ts`; `cron view list
pane` and `cron view selects` stay in `view.test.ts`. The four DOM helpers the
blocks shared move into the existing `view.test-support.ts` sibling rather than
being duplicated, and each file imports only what it uses.
No test content changed: 42 tests before, 42 after, same 41 `it(` declarations.
Sizes drop to 373 / 721 / 143 lines, all well under the cap.
* fix(cron): reject blank/invalid --webhook before delivery.mode flip
Presence-only typeof checks treated empty or non-http --webhook as a
delivery edit, forging mode=webhook with no URL and clearing the prior
chat destination on merge. Validate with normalizeHttpWebhookUrl first.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(cron): cover webhook validation boundaries
Amp-Thread-ID: https://ampcode.com/threads/T-01a0220d-eaa0-76b4-adb9-68841f015b75
---------
Co-authored-by: zyw02 <zyw02@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
* fix(cron): reject blank --model/--thinking on cron edit
Empty Commander values skipped the clear-* mutex after normalize, so
--model '' --clear-model still cleared the override. Align with fallbacks
and delivery clear presence checks.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(cron): align empty model/thinking edit expectation with blank reject
The legacy cron-cli suite still expected blank --model/--thinking to be omitted; that contradicts the fail-closed blank validation and broke CI.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): mutex blank --model/--thinking with matching clear flags
Standalone blank overrides stay omitted. Flag presence now conflicts with
--clear-model/--clear-thinking instead of silently applying the clear.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(cron): focus blank clear mutex coverage
Amp-Thread-ID: https://ampcode.com/threads/T-01a0220d-eaa0-76b4-adb9-68841f015b75
---------
Co-authored-by: zyw02 <zyw02@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
`openclaw skills install <slug>` and `skills verify <slug>` both answered a
ClawHub 404 with:
Skill "nonexistent-skill-xyz" not found. Run `openclaw skills list` to see available skills.
ClawHub is saying the slug is not in the registry. `skills list` lists the skills
already installed locally, so it cannot resolve a registry miss -- the operator
is sent to look at what they already have when they were trying to acquire
something new. `skills search` exists for exactly this and is one line away in
`skills --help`.
The sibling gets this right, which is what makes it a defect rather than a
preference: `plugins install <unknown>` answers with "Run `openclaw plugins
list` to see installed plugins, or `openclaw plugins search <name>` to look for
installable plugins."
The 404 branch now names ClawHub as the source of the miss and suggests
`skills search <slug>`. It also routes through `formatCliCommand` like the rest
of the file's sibling messages, so the suggestion stays correct under `--profile`
or `--container`; the hardcoded string did not.
The local-lookup message in `skills-cli.format.ts` is unchanged: there
`skills list` is the right answer, because that path is looking for a skill the
operator should already have.
Production +4/-1.
Doctor is the designated repair owner and nearly every CLI failure footer in this
product ends with `Try: openclaw doctor`. With a corrupt shared state database it
produced this, and nothing else:
┌ OpenClaw doctor
database disk image is malformed
No path. No indication of which database. No next step. `doctor --fix` printed
the identical two lines and repaired nothing, `OPENCLAW_DEBUG=1` added nothing,
and the standard `Reason:`/`Debug:`/`Try:` envelope never appeared. The operator
was in a closed loop: every command told them to run doctor, and doctor told
them a SQLite string with no object attached to it.
Corrupt the agent database instead and doctor already does the right thing --
names the file, names the reason, warns visibly, completes the full run, exits 0.
Same corruption class, two databases, opposite treatment.
The mechanism: `assertDoctorDatabaseSchemasCompatible` read
`preflightOpenClawDatabaseSchemas` and inspected only `incompatible`, silently
discarding `indeterminate`, which is exactly where an unreadable shared database
is recorded with its path and reason already populated. Doctor then proceeded and
died deeper with the context stripped, at
`src/infra/sqlite-readonly-location.ts:428` by way of state ownership inspection
and the config preflight.
Doctor now consumes that dropped signal and stops with a diagnosis that names the
file, the reason, what it deliberately did not do, and how to recover. It stops
rather than continuing like the agent-database path because shared state owns
write admission and holds the persisted plugin index the health context is built
from; disabling migrations still fails on that index, so continuing would mean a
bespoke degraded doctor. It does not recreate the database: that file holds auth
profiles among other things, so silent rebuild is data loss.
Also fixes an adjacent leak found in the same investigation: `io.load.ts` passed
a raw `Error` to the logger, so `doctor --session-sqlite inspect` printed a stack
trace with absolute `dist/*.js` frames without `OPENCLAW_DEBUG=1`, contradicting
the CLI's own debug-gating convention. It now logs formatted message text.
`doctor --lint` exiting 1 while bare `doctor --json` exits 0 was investigated and
left alone: commit 6e5bf3ec55 established that advisory JSON exit behavior
deliberately, and register.maintenance.test.ts covers it.
Production +11/-1.