* fix(cron): keep valid command env vars when a sibling value is non-string (#105433)
normalizeTrimmedStringRecord returned undefined whenever any env value was non-string, so payload normalization deleted the entire env map before gateway schema validation, silently dropping valid siblings (e.g. env {DEBUG:true, PATH:'/bin'} lost PATH). It now filters invalid entries and only normalizes the map away when no valid entries remain, matching the sibling normalizeTrimmedStringArray behavior in the same file.
* fix(cron): reject malformed command env maps
Co-authored-by: MoerAI <friendnt@g.skku.edu>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): cap trigger script tool access
Persist the creator tool cap for any cron job with a trigger script and pass it to the headless trigger runtime. System event and command payloads retain the cap across cron updates.
* fix(cron): persist trigger tool cap for systemEvent and command payloads
The creator tool cap is stamped on systemEvent and command payloads whenever a
cron job carries a trigger script, but the SQLite codec dropped
payload.toolsAllow for those kinds. After a gateway restart the reloaded job
carried no cap and the trigger regained the full coding surface. Persist and
restore toolsAllow through the existing payload_tools_allow_json column for all
payload kinds, and add restart-aware round-trip coverage.
* ci: re-trigger workflows (transient self-hosted runner flake)
* fix(cron): use expectDefined for store test job access
* refactor(cron): hoist cron tool types, payload merge, and wire types into companion modules
Keeps the oversized cron files at or below their baseline line counts for the
changed-file TypeScript LOC ratchet on main. No behavior change.
* fix(cron): retain freshly stamped cap markers
* test(ci): align compact shard count
* fix(cron): make trigger cap updates atomic
* fix(cron): preserve explicit cap provenance
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
A failed, refused, superseded, or thrown restart emission could leave the
reversible restart-signal admission fence closed forever: concurrent emitters
could overwrite the live rollback lease with a dead stand-in, the fenced body
had no try/finally, and the outer catch swallowed errors precisely because the
stuck fence made isGatewayRestartDraining() true. The gateway then rejected
every new task with GatewayDrainingError - silently - until an operator
restarted the process.
beginGatewayRestartSignalAdmission now returns null instead of stand-in
leases (single fence owner), emitPreparedGatewayRestart reopens the fence on
every non-delivery path via try/finally while preserving it whenever a queued
SIGUSR1 is unconsumed, refused-signal cleanup force-clears orphaned fences,
and admission close/reopen transitions are logged with their reason. The
self-contained SQLite restart-intent persistence moves to restart-intent.ts
to keep restart.ts within the LOC ratchet.
Fixes#107322
* fix(cron): reject non-string values in delivery target validation
assertNonBlankStringField had inverted logic — non-string values
(number, boolean, object) were silently accepted because typeof !== 'string'
was grouped with the undefined/null early return. The function name says
'assert non-blank string field' but it was bypassing validation for any
non-string type, defeating the purpose of input validation.
* test(cron): cover invalid delivery fields at entrypoints
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor: delete dead infra and config exports
* refactor: preserve live infra and config contracts
* refactor(config): remove obsolete file-store lifecycle APIs
* refactor(infra): finish current-main dead export cleanup
Stops the legacy run-log dual-write, auto-imports remaining rows and drops
the table at state-DB open (table-presence guard: self-heals downgrade
round-trips), keeps the newest 2000 terminal runs per job in the ledger,
retires cron.runLog config via doctor, and moves live-run cancellation to
src/cron behind the shared task control seam.
Part 2 of 2 for #106041.
CI caught two real PR1 regressions. (1) Wiring the cron-run-logs migration
into state-db open transitively pulled agents/sandbox via the run-log error
codec, breaking ~13 tests that mock config/paths without STATE_DIR: split the
event->entry write codec into task-run-event-codec.ts and dependency-inject
the failover reason resolver so live run-log reads keep recompute while the
ledger read + migration stay dependency-light. (2) cron/service/task-runs.ts
reached into task-registry internals: route through the task-executor facade.
Known bounded limitation documented in the migration: legacy rows lacking a
stored errorReason are imported without a recomputed one (write-time authoring
is canonical going forward).
Cron executions now write full-fidelity task_runs rows (detail_json column,
store-key scoped) and cron.runs/CLI/task-maintenance read from the ledger;
legacy cron_run_logs dual-write stays as a revert safety net. Existing
history auto-imports at first state-DB open (verified against a production
DB copy: 2750/2750 rows, byte-identical parity, idempotent). Startup crash
recovery restores finished runs from finalized ledger rows instead of
reporting synthetic interruptions.
Part 1 of 2 for #106041.
* feat(gateway-protocol): add session placement schema
Closed state discriminator for session execution placement (local/requested/provisioning/syncing/starting/active/draining/reconciling/reclaimed/failed), sessions.dispatch params, and worker-admission transcript/live cursor extensions. Swift protocol models mirror the schema.
* feat(state): add worker session placement table
worker_session_placements rows carry placement state, transition generation, worker ownership metadata, ACK cursors, and the turn claim columns used for atomic admission.
* feat(cloud-workers): add durable placement state machine store
SQLite-backed placement store split by concern: state table (placement-state), discriminated record types + shape invariants (placement-record), row codec + CAS transition values (placement-row-codec), atomic turn-claim admission/release/waiters (placement-turn-claims), and lifecycle CAS transitions (placement-store).
* feat(cloud-workers): sync workspaces and attach sessions to worker environments
Environment service session attachment + turn credentials, tunnel workspace commands over a dedicated SSH runner, and git/plain workspace sync into $HOME/.openclaw-worker/workspaces with an immutable manifest. Symlink escapes are rejected locally before transfer (macOS openrsync stat-fails them opaquely) and again by the remote manifest guard.
* feat(worker): run one-shot embedded turns from launch descriptors
Worker runtime executes a single embedded turn from a stdin launch descriptor and reports completed/failed/fenced on stdout for the gateway launcher. Terminal lifecycle live events are deferred past the final transcript flush; transcript projection helpers are shared via transcript-message instead of duplicated in the runtime.
* feat(cloud-workers): dispatch placements and route worker turns
Dispatch service drives local->requested->provisioning->syncing->starting->active with failure teardown (placement-dispatch-failure) and restart/runtime recovery incl. lost-worker reclaim (placement-dispatch-recovery). Worker turn launcher claims the placement turn atomically, builds a windowed launch descriptor (worker-turn-payload), runs the remote one-shot worker, and reconciles the committed transcript; agent runners route turns through the session placement admission provider.
* feat(gateway): expose session placement RPCs and startup reconciliation
sessions.dispatch RPC with lifecycle admission barriers, operator-facing placement projection on session listings, placement-aware session reset guard, and startup/interval reconciliation wiring for worker placements.
* refactor(deadcode): trim auto-reply and CLI exports
* refactor(deadcode): trim cron and task exports
* refactor(deadcode): trim fleet and process exports
* test(deadcode): exercise live task and process seams
* test(fleet): cover stream redaction through owner module
* refactor(security): trim dead internal exports
* refactor(secrets): trim dead internal exports
* refactor(deadcode): trim remaining src exports
* refactor(deadcode): remove test-only runtime exports
* refactor(deadcode): trim pairing test exports
* refactor(deadcode): reconcile refreshed baseline
* test(auto-reply): deduplicate queue state imports
* fix(gateway): make hot reload transactional
Replace partial reload side effects with a deferred transaction that publishes config, secrets, auth, and subsystem state together, and drains in-flight reload work before shutdown.
Co-authored-by: LZY3538 <293718838+LZY3538@users.noreply.github.com>
* fix(auth): preserve state-only credential ownership
Keep derived runtime snapshots in place for main-store state mutations so order refreshes do not look like credential replacement.
* fix(gateway): close reload transaction gaps
* fix(gateway): close merged reload gaps
* chore: move reload note to PR context
* fix(gateway): exclude restart emission root
---------
Co-authored-by: LZY3538 <293718838+LZY3538@users.noreply.github.com>
* fix(cron): keep isolated run status ok when delivery fails
A successful isolated cron agentTurn whose post-run delivery phase returns
an error collapsed the execution status into the delivery error, so the
outer scheduled run persisted status=error even though the isolated session
ended successfully.
Decouple execution status from delivery outcome in finalizeCronRun: when the
agent payload is non-fatal and the run was not aborted, a delivery dispatch
error now keeps status=ok and records the failure separately via
delivered/deliveryAttempted plus delivery diagnostics, which drive the
already-decoupled run-log deliveryStatus (not-delivered). Aborted runs and
fatal agent payloads keep their error status.
Fixes#94058
* fix(cron): keep deliberate delivery-target refusals as error status
The #94058 fix kept isolated turn status=ok when post-run delivery
fails, but the broad `status === "error"` check also caught the
#91613 keyless implicit last-target refusal, which is a deliberate
delivery-target guard (errorKind "delivery-target") and must stay
status=error. Exclude that errorKind so genuine delivery dispatch
failures still become ok while target-guard refusals remain errors.
* fix(cron): surface successful run delivery errors to run logs
A successful isolated agent turn keeps status=ok when post-run delivery
fails (#94058), but the ok/not-delivered resolveRunOutcome branch only
forwarded delivery diagnostics and dropped the delivery dispatch error.
That left lastDeliveryError empty and the finished-event deliveryError
field blank, so CLI/UI/API run logs could not show why delivery did not
land for an otherwise successful run.
Thread the delivery error on a dedicated CronRunOutcome.deliveryError
field from resolveRunOutcome through executeDetachedCronJob and
applyJobResult into resolveDeliveryState, so it persists as
lastDeliveryError and reaches the finished event (and thus the run log)
without conflating it with a run-level error (lastError stays empty on a
successful run).
Add a service/run-log readback test proving the delivery error survives
the cron boundary into a persisted run-log entry, and extend the
isolated-turn regression to assert the dedicated deliveryError field.
* fix(cron): preserve successful delivery outcomes
* fix(cron): preserve startup delivery errors
* fix(cron): retain best-effort delivery errors
* fix(cron): preserve delivery errors on fallback paths
* fix(cron): preserve delivery errors on fallback paths
---------
Co-authored-by: Alix-007 <li.long15@xydigit.com>
* feat(gateway): flag sessions with attached automations and disable bound cron jobs on archive
Session rows now carry hasAutomation, derived from a lifecycle-owned index
over the cron service's in-memory jobs; cron events push refreshed rows so
badges stay live. sessions.patch { archived: true } disables enabled cron
jobs bound to the session (locked binding re-check, internal/operator-admin
callers only); restore intentionally does not re-enable them.
Refs #104700
* feat(ui): sidebar session state slot and worktree/automation badges
The run spinner moves into a leading state slot shared with the unread dot,
keeping the timestamp visible during runs; muted fork/clock badges sit after
the title (outside the trail/action overlap so pinned rows and touch devices
keep them). New strings localized via ui:i18n:sync (English fallback pending
a keyed translation run).
Refs #104700
* test(gateway): pin cron binding broadcast test to the event mechanism
The shard shares one process; session-store state from earlier tests can make
the row load return null, which legitimately produces a keyless refresh
payload. Row-field projection stays covered by session-utils and
session-automation-index tests.