* test(qa): align DM routing sender with conversation identity
The portable routing scenario used different direct-conversation and sender identities, which Telegram correctly rejects before ingress. Reuse the configured direct-conversation identity for the sender without changing channel policy.
Regression: channel-dm-group-routing failed twice through Crabline Telegram with the direct/sender normalization error on 5eeecd39f9. The corrected existing scenario passes through Crabline Telegram, Crabline Discord, and qa-channel on Blacksmith Testbox with mock-openai and isolated state. No production changes; scenario diff is +1/-1. Local hooks are disabled because this campaign prohibits local execution; final changed checks run remotely.
* test(qa): preserve one sender across routing scenario turns
Keep the direct and group turns on one participant alias. The real Telegram QA adapter binds a single leased participant to its first logical alias and correctly rejects a later alias switch. Exercise the shipped routing flow through that adapter with the existing mocked userbot, without credentials or a Gateway process.
Completes the DM routing fixture repair; production code and channel policy stay unchanged. Final remote proof checks the new regression against the prior fixture and then the corrected fixture. Local hooks are disabled under the campaign host restriction.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Construct the allowlisted cron history entry once. Absent optional fields are now present as undefined internally; retained JSON bytes, defined-field order, and the optional protocol shape remain unchanged.
Validated 29 codec cases, 12 history/import/Doctor sibling cases, and the complete changed-file gate on Blacksmith Testbox tbx_01m3qz2pv7anc1w5m22fzp8tae. The two new byte-contract cases took 3 ms combined; the cold file command took 30.62 s, dominated by transforms. Codex P2 review found no actionable issues.
Hosted CI preflight failed before product code loaded because .ci-harness/scripts/ci-build-manifest.mjs was missing (run 36664256510). security-fast and independent security/dependency review passed. Landed under the authorized unrelated-CI exception, retaining the failed hosted result.
Since #161534 the preflight step "Build CI manifest" runs
scripts/ci-build-manifest.mjs from the trusted .ci-harness checkout of the
workflow SHA, but checkout_harness only exported release-context.mjs and
release-version.mjs for the preflight kind. Every pull_request preflight
since then failed with "Cannot find module .ci-harness/scripts/ci-build-manifest.mjs"
before any test ran; main push runs skip preflight, so main stayed green.
Add the manifest builder to a shared preflight_scripts tuple and use it for
both the index export (checkout == workflow SHA) and the sparse fetch
(different-revision dispatch), which previously received no preflight
scripts at all. Regenerated ci.yml with scripts/generate-ci-git-owner.mts.
Proof: node scripts/generate-ci-git-owner.mts --check passes; owner.py
compiles; exporting the preflight pathspecs into .ci-harness/ and running
node .ci-harness/scripts/ci-build-manifest.mjs from the workspace resolves
every static import and reaches "CI release scope: full".
* fix(gateway): channel setup save stalls while a config reload drains channels
A config reload holds the plugin lifecycle lease while its channel-reload
drain waits for active Gateway work. A retained setup wizard counts as that
work, and its config save waits for the same lease, so sequential guided
channel setup (Telegram, then Discord, then Slack) sat on "Working…" until
the 300s deferral timeout.
The lifecycle lease owner now tracks, per state database, whether this
process holds the lease and how many in-process callers wait to acquire it.
The channel-reload drain, which always runs while this process holds the
lease, proceeds as soon as another caller is queued for it: that caller
cannot finish before the reload returns. The lease is still held for the
whole reload, so reload ordering and writer exclusion are unchanged.
* test(gateway): settle the reload holder before advancing the wizard lease clock
The lease test clock advances fake time to observed acquisition backoffs.
While the first reload was still settling real SQLite worker cleanup, that
could carry the waiting wizard past its ten-minute acquisition deadline, so
the committed mixed case reported a lease-held error under CI load. Await
the first reload before advancing the waiter, and check the wizard error
before its status so a failure prints the cause.
Record a hidden, call-specific transcript notice when an OpenAI Responses tool call is cut off by the output-token limit. Ask the model to split the remaining work into smaller calls, preserve completed results, and allow one bounded continuation per run before surfacing the existing unfinished-call warning.
Keep terminal diagnostics with the Responses transport and transcript persistence with the admitted session writer. Include transport, retry, durable-transcript, and opt-in live regression coverage plus logging documentation.
* fix(sessions): reject compaction of missing sessions
Return an actionable INVALID_REQUEST instead of a successful no-op for missing targets in both compaction modes. Preserve explicit no-op outcomes for existing empty sessions.
* fix(sessions): reject malformed reset agent selectors
Use strict session agent input validation before lifecycle cleanup so an invalid explicit selector cannot reset the default agent. Extract reset-target resolution from the oversized lifecycle service while retaining selected-global targeting.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Remove the naming-only retention wrapper, duplicate session-watch table probe, and conditional node-host gateway spreads. Preserve persisted JSON, migration ownership, retention, and update behavior.
Validated five runtime-open and Doctor migration cases, eight byte-parity fixtures, and the complete changed-file gate on Blacksmith Testbox tbx_01m3qz2pv7anc1w5m22fzp8tae. Codex P2 review found no actionable issues.
Hosted CI preflight failed before product code loaded because .ci-harness/scripts/ci-build-manifest.mjs was missing (run 36663171385). security-fast and independent security/dependency review passed. Landed under the authorized unrelated-CI exception, retaining the failed CI result.
* fix(ui): stop serving a failed Control UI build on the next Gateway start
Rolldown writes the complete bundle, runs the writeBundle hook, and only
then reports aggregated resolve errors and exits non-zero. scripts/ui.mts
built straight into the served dist/control-ui and reused the runtime build
identity, so a failed build left a correctly stamped tree that the Gateway's
asset health check accepted as ready on the next start (observed: a bare
markdown-it-emoji import broke the Control UI after a gateway-profile worktree
install skipped ui/ dependencies).
scripts/ui.mts now builds into a same-depth dist/control-ui.build-<pid>-*
sibling, runs both validators against it, and renames it into place only on
success, restoring the previous output if publication fails. Dead-process
leftovers are reclaimed on the next build and tsdown's dist cleaning skips
in-flight staging trees. The gateway worktree-setup workload now installs
./ui... because the Gateway builds the Control UI from source on first start.
* fix(ui): restore the previous Control UI bundle after an interrupted swap
A builder killed between moving dist/control-ui aside and renaming its staged build into place left the previous complete bundle only in a dead-process .retired sibling, which the next build's leftover cleanup deleted. The next build now restores the newest dead-process retired bundle when the served path is missing, before reclaiming leftovers, so a failed retry still serves it.
* fix(ui): keep interrupted Control UI builds out of packages
A killed builder cannot reach its cleanup, so a dist/control-ui.build-* staging or retired sibling can survive until the next UI build. Exclude those siblings from the package files list, which also drives the dist inventory, and type the new UI test fixtures for the scripts test lane.
* fix(ui): retry transient Windows denials when publishing the Control UI build
Windows scanners and indexers can briefly deny renaming a freshly written or served directory with EPERM, which failed an otherwise valid rebuild on its first attempt. Every publication rename now retries EPERM, EACCES, and EBUSY on a bounded 100-1600 ms schedule (about 3 s) and keeps restoring the previous bundle when the denial is permanent.
Share documentation translation response cleanup, simplify sorted translation-memory JSONL writing, use canonical Node inspector types, and remove redundant benchmark and preview-fixture state.
Preserve CLI flags, exit codes, output formats, generated bytes, fixture responses, and benchmark workloads. Focused tests, exact before/after JSONL and mock-response checks, and both import-cycle gates passed. The optional cron-reaper smoke reproduced the same existing worker timeout before and after the refactor.
Release notes: internal tooling cleanup only.
Hosted CI exception: run 36661925129 failed before tests because the trusted CI harness omitted ci-build-manifest.mjs. Current unrelated run 36663040449 failed identically. Security/dependency review and security-fast passed; skipped CI lanes remain unrun. Merged under the maintainer-authorized pre-existing-failure exception.
Two smoke guides still described the hidden Node installation retry removed by #161512. Describe the single install with the existing Bun launcher marker, its terminal failure behavior, and Node's preparation-only role.
Both guides pass formatting and whitespace checks; independent review found no actionable issues. Hosted preflight failed in unchanged main CI because it invoked the absent .ci-harness manifest path. That failure was reproduced on detached main and qualified through the native pre-existing-failure admin exception; security-fast passed independently.
* test(qa-lab): skip Bubblewrap hook-pressure e2e without network namespace support
Probe the pinned Codex workspace-write sandbox once per Linux suite.
Skip only recognized Bubblewrap namespace setup failures and retain the diagnostic.
Leave unknown probe failures and capable hosts on the existing Gateway assertions.
* test(qa-lab): match Codex's Bubblewrap namespace failure list
Align the probe's skip matcher with Codex's USER_NAMESPACE_FAILURES,
including the vendored "No permissions to create a new namespace" wording.
Generic namespace failures such as ENOSPC exhaustion keep failing the suite.
Confirm installation-only pending records for disabled channels and
installed plugins without config entries. Carry retained plugin IDs
through package convergence while preserving real migration obligations.
Fixes#161288 and #160523. Thanks @akennedytog and @g62nkcx4q7-wq.
Delegate timer cancellation and running-work joins to scheduler scopes while retaining idle authority checks, manual PR work draining, and refresh-waiter settlement. Await actual idle stop completion in the lifecycle fixture.
Production +10/-20/net -10. Independent Codex review and 147 focused tests plus all changed-file gates passed on Blacksmith Testbox. Operator-authorized pre-existing CI exception: the unchanged mock-Gateway widget test exhausted its native pointer-routing deadline and cannot reach either changed scheduler owner; exact-head CI remained cancelled, not green. Security gates passed. Full evidence and review disposition are in the PR body.
Consolidate core rendering, Doctor repair accumulation, ACP/session projections, media helpers, and state row/retirement handling while preserving public and storage contracts.
Retain selector snapshots and UTF-8 queue accounting, and catch synchronous Workshop activity-check failures. Preserve newer main behavior in overlapping state owners. Validation uses both local cycle checks, focused state/Workshop regressions, and hosted CI on the exact PR head; prior partial Testbox evidence is recorded in the PR.
The Gmail watcher stayed down for good when a Gateway restart briefly found its port still in use (EADDRINUSE). It now retries the bind a few times with bounded backoff and recovers once the port frees. If the port stays taken, it reports a clear error and stops retrying.
Fixes#161467. Reported by @kazuyuki-eguchi.
Proof: a real isolated Gateway, with the Gmail watcher on local fakes.
- Before the fix, the watcher stayed down after a transient port conflict. After it, the watcher recovered and answered.
- With the port held for the whole run, the watcher stopped after the initial attempt plus three retries, with a clear error.
- The regression test fails before the fix and passes after, and 31 focused tests pass.
- Updating from the published 2026.9.6 build to this build succeeded in two fresh runs (102 s and 97 s), and the installed build passed both the transient and the persistent control. The first updater run failed at service activation and didn't recur. The watcher can't reach that step: rehearsal disables hooks, and this diff doesn't touch activation or lease code.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Use one fixture handler for the fake GitHub CLI and fixture-scoped Node
preload, avoiding repeated Node startup for synchronous fake-gh reads.
Real Git operations and process supervision keep their native boundaries.
In a 4-CPU Linux container with Node 24.21.0, all 20 importer files passed
728/728 tests in both variants. Suite wall fell 36.4%, from 1,070.10s
(17m50s) to 680.06s (11m20s); the main-drift settlement case fell from
16.213s to 7.096s under its unchanged 20-second deadline.
Production code, deadlines, test cases and assertions are unchanged.
This reduces but does not eliminate load-sensitive deadline risk; it is
not claimed as a complete flake fix. The loaded comparison was incomplete.
Validation: full Linux importer comparison, check-changed, four root test
type shards, formatting and boundary lint, Knip, source-contract checks,
and independent P2 review.
* fix(mcp): keep dark-theme MCP App frames transparent
MCP Apps share the sandbox proxy page with inline widgets, but they get
their theme from MCP host context, not widget theme messages. An app that
applies the host theme through the SDK's applyDocumentTheme becomes
color-scheme dark while the proxy stayed light, so Chromium painted an
opaque canvas behind apps that leave their background transparent. The
proxy now also adopts the theme from the ui/initialize host context and
from host-context-changed notifications.
Inline the single-use encodeCsp helper, which re-normalized a CSP its
only caller had already normalized. Production lines: +15/-19.
* test(ui): measure the dashboard MCP frame on every host-size poll
The board MCP e2e read the frame size once after a viewport resize and
then waited for the app to report exactly that value. When the board
layout settled after that first read, the app reported the new size and
the poll could never match (CI: stale 752 px against the settled 952 px,
the full 200 px viewport growth). Compare the reported size with the
frame's current size on each poll instead.
Upgrade supplied port claims through the shared listener reservation owner before awaiting Gateway startup. Adopt that same HTTP listener and retain the existing close and failure cleanup ordering without retrying or reallocating explicit ports.
Extend the disabled-model HTTP case with a deterministic competing bind. The old fixture allowed the competing listener onto its claimed port; the repaired fixture rejects it and still returns the expected HTTP 404. The historical CI occupant remains unidentified.
Share pending local identity discovery across concurrent catalog requests while retaining successful IDs and retrying misses or failures. On a six-caller, 515-device fixture, maximum node.list latency drops from 3038.6 ms to 938.8 ms.
Process-census capability needed to distinguish a dead managed-service handoff owner from a surviving descendant: a Windows process census (PID, start identity, command line, cwd, owner SID with the foreign-owner rule from the Unix contract), verified Unix UID provenance for incomplete observations, and retained-artifact reference matching that reports matching versus unverified PIDs. Existing callers keep their classification when the new evidence is absent.
Refs #159897 (the reclaim itself follows in #160488).
Landed under the pre-existing-red rule: the remaining CI failures were current main reds in the merge window (fast-lane config expectation fixed by e0ec544eb1; cron service tests fixed by 5f76cc437d; update-candidate-canary from b36eb3e7b1).
Preinstall probed absent or non-executable PATH Node candidates, creating Node spawn attempts during Bun-only installs. The smoke also omitted the documented Bun launcher marker and masked the resulting failure with an install-time Node fallback.
Check execute permission before probing persistent Node candidates, preserve Node engine enforcement and Bun lifecycle-shim exclusion, and make the smoke install through the documented Bun 1.4+ launcher without a Node fallback. Remove the resolved expected blocker.
Proof: 132 focused preinstall, spawn-ledger, and install-smoke workflow tests pass after a patch-identical rebase. The reviewed Linux Bun-only smoke passes all ten steps with zero preinstall Node attempts and no fallback; both classifier negative controls reject invalid blocker inventories.
Remove redundant UI render wrappers, duplicate state markers, copied type shapes, and repeated completion logic. Preserve existing appearance, lifecycle ownership, and callbacks; retain agent-file save proof through the rendered button.
Net production LOC: -540. Both cycle checks and focused oxlint passed. Focused UI tests: 1232 passed, four inherited pasted-text preview race failures reproduced on the exact baseline, and one conditional skip. Hosted CI on the pinned PR head remains the merge gate.
* improve(ui): stop re-describing new sessions on every chat event
Creating a session from New Session and running two short turns made the
chat pane issue 32-36 sessions.describe calls (plus identical in-flight
branch/model/descriptor pairs) against a real Gateway. When the dashboard
session's parent (agent:main:main) does not exist, every sessions.changed
omits ancestorSessions, so descriptor observations were invalidated on
every event and the pane's active-resource owner re-described ~4 ms later.
The event already carries the full row; describe returns the same row and
cannot certify ancestry either.
- Row observations publish admitted rows immediately and deliver one paced
authoritative invalidation for incomplete ancestry through the existing
session event refresh coordinator (absorbed by newer reads, immediate
invalidations, retirement and reconnect).
- The shared describe owner treats an agent-qualified key with or without
its implied agentId as one read slot; refresh still supersedes pending
reads.
- A session-patch metadata refresh whose chat.metadata answers before its
concurrent models.list validates against that pending catalog read
instead of issuing an identical replacement.
- Concurrent chat branch loads share one pending sessions.branches.list.
Real Gateway, create + two turns: describes 32-36 -> 8-13, identical
in-flight pairs 1-3 -> 0, total requests 88-102 -> 61-72.
* fix(ui): keep chat metadata publication consistently async
Publication can wait for a pending catalog read, so return a Promise on every path instead of a sync-or-async union; side effects still apply synchronously when no read is pending. Await it in the metadata, command, model-catalog and model-control tests flagged by type-aware no-floating-promises.
* fix(ui): publish slash commands before catalog validation settles
A session-patch refresh whose chat.metadata answers before its concurrent models.list held the whole metadata result until the catalog read settled, delaying ready slash commands behind a slow model read. Publish the metadata synchronously again and let only catalog validation wait for the pending read; a changed, missing or retired catalog then invalidates and sends a follow-up catalogChanged notification while the writer is still current. Restores the synchronous publication contract, so the settlement restructure and the test awaits are no longer needed. Also waits for the held chat.startup request in the metadata-observation early-wake E2E before resolving it.
* refactor(config): deslop config sixth pass
Share config I/O execution, channel normalization, and validation owners.
Derive duplicate request and worker types from their canonical contracts,
remove the retired Tasks backing reader, and simplify metadata projection.
Preserve schema output, defaults, environment behavior, Doctor migrations,
redaction, and persisted state. The combined cleanup removes 648 net
production lines; the PR includes the complete large-file coverage log.
Validated on Blacksmith Testbox: check-changed, build, full config/caller
selection (5960 passed), schema byte parity, import boundaries, and both
import-cycle checks. Existing native Windows and benchmark skips remain.
* docs(config): refresh worker inventory after reader cleanup
* docs(config): align worker inventory with current main
* refactor(config): retain main config context during cutover
Adds `pnpm frv watch --run <parent>`: it resolves Full Release Validation children from the parent's dispatch-job logs and reports each attempt transition and failed job once, with runner labels. It tolerates transient GitHub failures and resumes from a small state file.
Adds `pnpm frv rerun --run <parent> --child <key|run-id> [--max-attempts N]`: a bounded, audited single rerun-failed-jobs request. It reruns the green producer when a consumer binds its run attempt (#161317) and checks the new attempt for duplicate or missing jobs.
Retires `pnpm frv prioritize --run`, since the priority variable no longer controls admission. `--restore` stays.
Moves the ~80 KB inline "Build CI manifest" program into scripts/ci-build-manifest.mjs, which runs from the trusted .ci-harness checkout while target-owned modules still resolve from the target checkout. ci.yml drops from 479,990 to 399,632 bytes; the 480,000-byte guard is unchanged.
Pins the QA Lab runtime-pair release lane to Blacksmith, because hosted runners failed its core lane after the backend flip, and prints a routing notice while the flip is active. Gives the Codex extension file-bounded Plugin Prerelease jobs instead of one 36-60 minute hosted batch.
Keep the pending acknowledgment and close/replacement race assertions while observing the existing provider-write callback directly.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
GitHub omits empty-string workflow_dispatch inputs from github.event.inputs,
so every sealed child receipt lacked the "" defaults that the parent-side
request filled in, and receipt validation rejected every candidate. The
per-candidate errors were swallowed, so dispatch logs never said why.
Treat empty and absent inputs as identical on both sides, only spend the
five full validations on receipts for this target and tooling, scan 100
runs, and log why each candidate was reused or rejected. Reuse now requires
the child's Tooling SHA to equal the current parent's, enforced at dispatch,
plan sealing, and final verification (previously any main-ancestor tooling
was accepted, which would have become live with this fix).
Avoid rebuilding live maintenance projections during synchronous protection-key captures while keeping published snapshots owned. With 5,000 runs and 1,000 publications plus reads, sampled allocations fell 30.3% and loop time fell 19.1%; focused tests, changed-file checks, and independent review passed on the candidate.
A catalog refresh re-resolves every saved-profile dashboard, including
refreshes started by another profile's change. For a retained document whose
native projection was still bound to the current socket, it replaced the
provider anyway, bumping the native-auth revision. That refused any
in-flight native challenge (retryable in the UI) and, in the saved-profile
reconnect test, swapped out the fixture-scoped provider, so the challenge
answered "The native gateway connection is no longer current."
Keep the projection while its socket is current. Only a replaced socket
installs the successor projection. The reconnect test now posts a
same-socket catalog change and requires the auth revision to hold.
Co-authored-by: Jason (Json) <263060202+fuller-stack-dev@users.noreply.github.com>
* refactor(codex): deslop Codex plugin eighth pass
Share native web-search projection, binding bookkeeping, and credential cache hashing; remove redundant forwarding and local coercion helpers. Preserve protocol, auth routing, subagent lifecycle, and transcript/tool bytes.
Also fix root-workspace path mapping and settle compressed rollout source I/O failures, with regressions for both defects. Remote validation remains pending; do not land until its gates pass.
* chore(codex): prune the removed assertion allowance
The SDK singleton replacement removed the only non-const assertion in config-utils.ts. Remove its obsolete baseline row without changing the ratchet policy.
* fix(codex): preserve typed submission history turns
Restore the typed JSON-object accumulator so the runtime guard narrows each Codex turn before returning it. Array.filter retained CodexTurn[] and failed typechecking because native error and item payloads have open unknown fields. Preserve every returned turn field and the existing admission order without type assertions.
* fix(codex): keep assignment owner projection out of map spreads
* fix(ui): hide worktree settings for non-Git folders
Keep placement isolation separate from local checkout selection, reject unsupported choices before persisting them, and share checkout visibility across composers. Preserve saved isolation on Git discovery failures without calling an unverified folder non-Git.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* fix(ui): hide worktree settings for non-Git folders
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: ad5f8b15-8c15-4df3-bfe1-ce3859b035a9
* test(ui): wait for Git discovery before choosing a worktree
The title handoff fixture selected a worktree while its Git request was still pending. Settle the existing fake clock and assert eligibility before selection, preserving the original explicit-name assertion.
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* fix(ui): hide worktree settings for non-Git folders
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: cfbd4e5a-8dc9-45e4-b404-794f4f136661
* fix(ui): keep restored remote isolation out of local intent
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* fix(ui): hide worktree settings for non-Git folders
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: aa9162fa-df3d-4218-9ae4-bff4c1abc7e4
* test(ui): settle retained draft before navigation race input
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
* fix(ui): hide worktree settings for non-Git folders
Worked on by:
- @steipete
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
OpenClaw-Publication: 21c31326-d808-49d1-8142-687753b3b808
---------
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Return ordinary-session metadata from the maintenance worker while retaining full cron-run snapshots for deletion guards. This avoids materializing cold prompt and workspace snapshots on the Gateway thread every minute without changing retention or listing validation.
Under load, a node's immediate answer to worker.launch.v1 could land while
markCredentialDelivered was still fenced in the worker inventory projection.
The fence digested environment and credential authority together, so the
credential-only commit made store.get() throw "unsettled mutation". The
node-worker tunnel's dispatch and cancellation authority reads that record;
at result settlement getPending swallowed the throw and dropped the answer as
"late", leaving the launch RPC to time out after 30 s, replay, and wait out a
30 s cancellation.
Split commit admission into independent environment and credential
authority so credential-only commits fence only credential readers. When
completion authority is closed or unreadable at settlement, settle the
pending invoke with non-retryable APPROVAL_AUTHORITY_CLOSED instead of
silently waiting for the deadline; closed owners still never receive the
node payload.
`openclaw update` could hang after printing its result because the retained updater runtime waited on the SQLite broker with no bound, and a force-exit watchdog was unsafe while accepted operations were still settling. The broker close is now split: settlement of accepted operations, pending opens and live references must complete; native close and worker termination are bounded afterwards, with the bound expiry recorded as a warning and the exit still happening.
Closes#160690.
Landed under the pre-existing-red rule: the remaining CI failures were current main reds in the merge window (check:architecture import cycle from b36eb3e7b1, fixed by 2a0a65c4ae; update-candidate-canary and cron service tests, fixed by 5f76cc437d).