Add optional per-job pacing bounds across the cron API, CLI, tool schema, public output, and SQLite job envelope, requiring at least one bound. Allow only the currently running paced job to record a one-shot next_check proposal and carry it through isolated-run completion.
After successful runs, clamp the proposal to the job bounds and persist an exact one-shot slot marker so maintenance preserves only that timestamp. Clear the marker on runs, edits, and schedule normalization; preserve existing no-proposal, skip, timeout, and error scheduling behavior.
* fix: bound misc unbounded fs.readFile calls; remove unused fs import
* fix: decode buffer to string before passing to string consumers
readRegularFile and readRegularFileSync return { buffer, stat },
not a string. All 4 new call sites passed the raw object to functions
expecting a string (JSON.parse, RegExp.test, template literals, etc.),
causing TS2345 type errors and runtime failures.
Fix each call by extracting .buffer and calling .toString('utf8')
before passing the result to string consumers.
* style: fix oxfmt formatting in config-set-input.ts
* fix: bound config and trajectory metadata reads
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
* refactor: isolate bounded read ownership
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cli): restore terminal state before exit in logs and hooks commands
* fix(cli): route logs/hooks error exit through canonical defaultRuntime.exit
* fix(cli): route terminal reset to stderr in JSON mode to keep stdout parseable
* fix(cli): centralize stream-aware terminal reset exit
Add optional resetStream parameter to RuntimeEnv.exit so JSON-mode
callers can route the terminal reset to stderr through the shared
defaultRuntime.exit path, keeping structured stdout parseable.
- Extend RuntimeEnv.exit signature with optional resetStream option
- Route JSON-mode logs fatal exit through unified defaultRuntime.exit
instead of manually pairing restoreTerminalState with process.exit
- Update test mock to exercise real terminal restore during exit
* refactor(cli): tighten terminal exit contract
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cli): reject --channel/--to for systemEvent cron jobs with a clear error
* test(cli): add regression coverage for cron-add delivery-option validation
* fix(cli): fix optionSource reference and add chat-delivery guard to cron edit
- Fix bare optionSource() call in cron-add guard to use cmd.getOptionValueSource()
- Add equivalent resulting-job validation to cron edit: reject --channel/--to/--account/--thread-id when the resolved job is a main-session systemEvent
- Add parity tests covering fast-path reject, existing-job reject, isolated allow, and clear-flag allow cases
* test(cli): return a real CronJob from default cron.get mock
* fix(cli): reject dropped cron delivery targets
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* improve(gateway): restart only the changed account on channel config reload
When a config change is scoped entirely to one channel account
(channels.{kind}.accounts.{accountId}[.*]), restart just that account
instead of the whole channel. Wholesale channel restarts disconnect every
account on the channel; on gateways running many accounts, each
account-scoped config write (adding an account, changing one account's
settings) briefly dropped every other account's connection.
The reload plan gains a restartChannelAccounts bucket populated only when
all of a channel's changed paths are account-scoped; any channel-global
path falls back to the existing wholesale restart, and a channel scheduled
for wholesale restart drops its per-account entries so each (channel,
account) pair restarts at most once. The executor runs per-account
restarts through the existing stopChannel/startChannel accountId parameter.
* fix(gateway): preserve account reload admission
* fix(gateway): scope surgical reloads to isolated plugins
* test(gateway): avoid preactivating reload snapshot
* docs(channels): define account reload isolation contract
* fix(gateway): preflight scoped account reloads
* fix(gateway): re-drain live scoped reload targets
* docs: refresh generated docs map
* fix(mattermost): align scoped reload with durable ingress
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(secrets): use resolveStateDir for .env path to cover legacy .clawdbot dir
* fix(secrets): replace override-based legacy .env test with true automatic fallback, add apply-path coverage
* fix(config-cli): emit JSON error on config get --json for missing paths
When is used with a nonexistent
path, the command emits no JSON on stdout, only a text error on stderr.
This breaks automated consumers that expect machine-readable errors.
Fix: output JSON to stdout before exit(1) when
opts.json is set. This matches the existing JSON error output pattern
already used by runConfigValidate and handleConfigMutationError.
Also re-throw synthetic exit errors in the catch block so the
--json branch's writeRuntimeJson+exit is not caught and re-emitted
as a text error.
* fix(config-cli): use typed ExitError signal instead of __exit__ string sentinel
* fix(secrets): reuse resolved stateDir for .env path in audit
* fix(secrets): pass resolved stateDir into apply .env scrubbing, add root-switch regression
* test(config-cli): migrate remaining __exit__ sentinel to ExitError
* fix(secrets): cover config and state dotenv roots
* style(secrets): format dotenv path helper
* test(secrets): track temporary directories
* test(config): expect typed exit for missing patch files
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
* fix(cli): wrap readFileSync ENOENT with user-friendly messages and suppress cause leak
* fix(secrets): type missing plan errors
* style(secrets): format missing plan error branch
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* perf(sqlite): gate schema version before integrity scans and quarantine terminal open failures
* feat(gateway): refuse incompatible database schemas at boot and verify integrity in the background
* feat(update): refuse installing builds that cannot open the current databases
* docs: add database schema reference with version history and downgrade guidance
* test(update): tolerate optional runner options in git-mutation mocks
* refactor(update): split npm package-target metadata out of update-check
* chore: model verifier internals for knip and regenerate docs map
* chore(i18n): resync stale iOS localization catalog
* chore: allowlist read-only preflight and verifier raw SQLite access
Respawn one-shot macOS commands with Node's OpenSSL-backed CA store when system CA loading is inherited. This avoids an indefinite shutdown join on the Keychain loader worker while preserving system CA for Gateway, interactive, and explicit --use-system-ca processes.
* fix(mcp): bound doctor server checks
* fix(mcp): defer doctor concurrency helper loading
* fix(mcp): preserve doctor check coverage
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(onboarding): persist app recommendations once
* feat(onboarding): add self-naming birth sequence
* chore(wizard): hoist bootstrap-defer imports to the top of the module
* docs(bootstrap): persist agreed identity into IDENTITY.md and SOUL.md as well
* docs: regenerate docs map after onboarding plan merge
* fix(wizard): reuse a pending stored offer instead of rescanning apps
* fix(onboard): deduplicate bootstrap recommendations
node identity, promos list, daemon restart, and tasks show emitted their --json result via runtime.log() (console.log), which --json mode redirects to stderr — leaving stdout empty and breaking pipes like `openclaw node identity --json | jq`. Use writeRuntimeJson to write the payload to stdout, matching health/tasks-list/capability. Human output unchanged.
`config file` (print the active config path) called readConfigFileSnapshot(), eagerly creating the state dir + state/openclaw.sqlite — so a --profile typo minted a stray profile tree. Resolve the path via resolveConfigPath() instead; no state initialized.
* feat(onboard): add browser-first guided handoff
* chore: drop release-only CHANGELOG edit from browser-handoff branch
* fix(onboard): make browser handoff reach the gateway on real installs
The presence probe connected as a raw shared-auth client with a possibly
SecretRef-managed token, which the gateway rejects as an unpaired Control UI
client ("device identity required"), so the handoff never fired and always
fell through to the terminal hatch. Read presence as a CLI-mode loopback client
(the trusted path every openclaw command uses) so operator.read is granted, and
share that one auth path between the reachability gate and the wait loop. Also
short-circuit when gateway.controlUi.enabled is false so onboarding does not open
a dead dashboard URL and block for the full timeout.
* fix(onboard): share resolved target between handoff gate and wait
The reachability preflight now resolves the same target (and shared secret) the
wait loop uses, and presents the configured secret to the CLI-mode presence
read. Known remaining limitation: on fresh quickstart gateways whose auth token
is written to config but resolved differently at runtime, the presence read is
rejected with a token mismatch and the handoff falls back to the terminal hatch.
Tracked for follow-up before enabling the browser handoff by default.
* docs(onboarding): mark phase 3 browser handoff as held with diagnosis
* docs(onboarding): phase 3 proven end-to-end; token-mismatch hold was a test artifact
* fix(onboard): satisfy docs-map and test-type gates for the handoff branch
* refactor(onboard): split guided manual stage
* feat(onboarding): recommend plugins and skills from installed apps
Scan installed macOS apps during classic onboarding (TCC-free), gather
candidates from official catalogs + ClawHub search, let the configured
model pick genuine matches, and offer an opt-in multiselect install step.
Adds a device.apps node-host command (default-off sharing, Android-parity
envelope) so remote gateways can request a paired Mac's inventory, and a
wizard.appRecommendations kill switch. Custom setup-inference completions
no longer inherit the 32-token verification-probe output cap.
* feat(onboarding): recommend apps in guided flow
* fix(onboarding): harden app recommendations against ClawHub self-promotion
Third-party ClawHub skills are never pre-selected regardless of model tier
(publisher-controlled listing text reaches the matcher prompt and could
promote itself); their labels now say they install third-party code.
Installed-app scans follow symlinked .app bundles. Matcher output stays
bounded by the resolved model's own maxTokens budget (documented invariant).
* fix(onboarding): key official catalog candidates by resolved plugin id
Real catalog entries are package manifests without a top-level id; keying the
candidate map and channel/provider classification by entry.id collapsed the
whole official catalog into one undefined-keyed entry, so no official plugin
or channel was ever recommended. Regression test runs against the bundled
catalogs.
* fix(onboarding): satisfy lint, types, deadcode, and migration gates
Split the guided-onboarding test into a self-contained custodian suite to stay
under max-lines. Narrow app-recommendation exports (drop dead node-payload
normalizer, unexport internal types/helpers, route candidate tests through the
public API), replace map-spread with a helper, unexport device.apps result
types, add installedAppsSharing to node-host migration expectations, cast the
wizard multiselect mock, and regenerate the docs map.
* test(onboarding): register new live test in the shard classifier
* fix(cli): guard secrets plan JSON.parse against malformed input
readPlanFile reads a user-specified file and parses it with JSON.parse
without a try-catch. A malformed or corrupted plan file produces a raw
SyntaxError that is not user-actionable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: tighten malformed secrets plan proof
Co-authored-by: 丁宇婷0668001435 <ding.yuting@xydigit.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
`config file`, `config patch --dry-run --json`, and `config unset --json` ran
the config path through shortenHomePath, emitting `~/…` or the literal
`$OPENCLAW_HOME/…` token in machine output — breaking `cat $(openclaw config
file)` and corrupting JSON path fields. `agents list --json` already resolves.
Emit the raw resolved snapshot path at these machine-output sites; human table
rendering keeps the friendly abbreviation.
* fix(config): improve validation error messages with line numbers, bracket paths, and received values
- Add JSON5-aware path navigator (issue-location.ts) that resolves line
numbers and received values from raw config text without a full parser
- Add formatConfigIssuePath() for bracket notation (agents.list[3])
- Add appendReceivedValueHint() for safe value display (got: "none")
- Add attachConfigIssueDiagnostics() to enrich issues with location info
- Wire enrichment into runConfigValidate() and loadValidConfig() in config-cli.ts
- Add resolveIssueLocationPrefix() to issue-format.ts for sourceFile:line prefix
- Sensitive paths and secret refs are never leaked in received value hints
- Paths in 'd files gracefully degrade (no line number shown)
Closes#104854
* fix(config): type fixes from tsgo:core validation
- Add sourceFile to ConfigIssueFormatOptions (used in resolveIssueLocationPrefix)
- Fix readKey regex test for undefined raw[c.pos]
* fix(config): satisfy eslint(curly) and formatting for issue-location.ts
- Add braces to all single-line if/while/for statements
- Run oxfmt for consistent formatting
* fix(config): trim issue-location.ts to 500 lines for LOC ratchet
* fix(config): fix formatConfigIssuePath ternary bug from trimming
* fix(config): trim issue-location.ts to 469 lines for LOC ratchet
* fix(config): format issue-location.test.ts
* fix(config): document JSON5 subset and add 20 edge case tests
- Add explicit supported-subset documentation to issue-location.ts header
- Add 20 tests covering hex numbers, leading decimals, Infinity, NaN,
null/bool, trailing commas, deep nesting, unicode escapes, multi-line
strings, unicode keys, escaped quotes, numeric separators, block comments,
mixed quotes, empty objects/arrays, and graceful degradation
* fix(config): address autoreview findings on JSON5 docs and tests
- Remove numeric separator claim (not valid JSON5)
- Remove 'None known' unsupported-syntax claim, document known limitations
- Restructure scalar tests to exercise skipVal (resolve sibling after exotic value)
- Remove numeric separator test
* fix(config): constrain received value diagnostics
* fix(config): preserve JSON5 diagnostic locations
* fix(config): preserve validation path ownership
* fix(config): redact dotted plugin paths
* test(config): avoid diagnostic fixture shadowing
* refactor(config): privatize diagnostic helpers
---------
Co-authored-by: Erick Kinnee <erick@ekinnee.dev>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(channels): restart gateway when channels.start fails for unloaded plugin
After channels login, the CLI calls channels.start to reconcile the running
gateway. If the channel plugin was installed or enabled after the gateway
started, the plugin is not in the runtime registry and channels.start rejects
with 'invalid channels.start channel'. Detect this error and fall back to
gateway.restart.request so the gateway reloads plugins and starts the channel
automatically.
Fixes#90296
* ci: retrigger checks (update proof section format)
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: retrigger proof check (real behavior evidence)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(channels): narrow unloaded-plugin restart
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* [AI] fix(daemon): set default --max-old-space-size=8192 in gateway service env
Generate a managed NODE_OPTIONS with --max-old-space-size=8192 for the
gateway systemd service, discarding ambient host NODE_OPTIONS to prevent
--require/--inspect persistence. 8192 MB is chosen as a safe default
that prevents the common OOM crash-loop with the default Node ~4 GB heap
under sustained load.
Related to #96203
* fix(daemon): adapt gateway heap limit to host memory
* fix(daemon): keep gateway heap helpers internal
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>