Operations
Startup, diagnostics, and safe remediation practices.
This page collects operational guidance for running Clapilot: container startup, local and native installs, GHCR (GitHub Container Registry) image publishing, CI browser checks, runtime diagnostics, and safe remediation for common failure modes. It is written for whoever operates an instance — self-hosted, CI, or production.
Clapilot.com mail deliverability
Clapilot-managed transactional mail currently uses the IONOS mailbox [email protected]. Set [email protected], the IONOS SMTP/IMAP hosts and credentials, and [email protected]. The envelope and visible From domain then remain aligned with the working clapilot.de SPF policy while replies go to [email protected]. Do not send with a visible From address at clapilot.com through IONOS.
The authoritative clapilot.com DNS zone is managed outside this repository. Keep exactly one SPF TXT record at the zone apex and use this rollout state:
clapilot.com. TXT "v=spf1 include:_spf.google.com ~all"
_dmarc.clapilot.com. TXT "v=DMARC1; p=quarantine; adkim=s; aspf=s; rua=mailto:[email protected]"
Before publishing the DMARC record, create or verify the monitored [email protected] mailbox/alias. Remove the previous Plesk SPF mechanisms (a, mx, and a:sweet-jang.93-90-204-86.plesk.page) rather than adding a second SPF record. Keep softfail while aggregate reports are reviewed. A later switch to Google SMTP requires a separate operational decision and Google Workspace configuration; only then may Clapilot use [email protected] as its From address.
After DNS propagation, verify there is exactly one SPF record and that DMARC reporting is published:
dig +short TXT clapilot.com
dig +short TXT _dmarc.clapilot.com
dig +short TXT google._domainkey.clapilot.com
Send one message through the configured Clapilot path to Gmail, Outlook, and GMX. For the current IONOS policy, confirm the visible From is [email protected], Reply-To is [email protected], and each provider reports spf=pass, dkim=pass, and dmarc=pass in the received headers. For a future Google path, run the same test with From: [email protected].
Container startup sequence
- validate required environment
- resolve native runtime config plus any mounted legacy compatibility state
- seed workspace profile files
- apply migrations (if enabled)
- start
clapilot-agent, workers, and app server
Core diagnostics
docker compose ps
docker compose logs -f clapilot
docker compose logs -f postgres
docker system df -v
Agent runtime transport diagnostics
Every app → ClapilotAICore call (/internal/... on clapilot-agent) goes through one shared transport
(src/lib/agent-runtime/transport.ts). It replaces the bare TypeError: fetch failed that Node's fetch raises
for any socket problem with a classified failure and retries transient ones:
- Failure kinds:
dns(Docker service name did not resolve, e.g.EAI_AGAIN/ENOTFOUND),connect(ECONNREFUSED,EHOSTUNREACH, connect timeout – the request never reached the runtime),reset(ECONNRESET/EPIPE/socket closed mid-request, typically a runtime restart or stale keep-alive socket),timeout,tls,aborted,unknown. - Retry policy:
dnsandconnectfailures are retried for every method (the runtime never saw the request);reset/timeout/unknownonly for idempotent calls (GET/HEAD/PUT/DELETEor callers that passidempotent: true). Backoff is exponential with jitter (300ms,600ms,1200ms, … capped at3s). Streaming request bodies and caller-aborted requests are never retried. - Errors surface as
NativeAgentTransportErrorwhose message names the method, full URL, cause code, attempt count, elapsed time and a remediation hint, for exampleAgent runtime unreachable (connect/ECONNREFUSED): POST http://clapilot-agent:3210/internal/chat/completions failed after 3 attempts in 2100ms — connect ECONNREFUSED 172.18.0.5:3210. The runtime host refused …. The same text lands inagent_runs/project error columns and in the app logs with the[agent-runtime-transport]prefix (one warning per retry burst, one error per final failure). - Monitoring: the transport keeps in-process counters (requests, transport failures, retries, recoveries,
consecutive failures, failures by kind, last 20 failures) and derives a health verdict:
unreachableafter three consecutive failures inside a minute,degradedfor five minutes after any failure, otherwisehealthy. Admins read it viaGET /api/agent-runtime/transport-status(?probe=1adds a live probe) or the runtime console commandagent-transport-check. Only calls that target the native runtime origin (CLAPILOT_AGENT_BASE_URL) feed these counters: absolute compatibility-provider/gateway URLs that some routes (Aufgaben delegate/comments, Video Studio completions) pass through the same helper still get retry/backoff, but their failures are rethrown as-is, never counted as runtime failures and never flip the runtime health tounreachable. - The live probe resolves the runtime hostname with DNS and calls
GET /healthwithout retries, reporting both layers separately so "name does not resolve" (Compose network / service name), "connection refused" (container down / wrong port) and "runtime unhealthy" (/health503, usually the database) are distinct outcomes.
Environment knobs (.env): CLAPILOT_AGENT_TRANSPORT_RETRIES (default 2 extra attempts, max 6),
CLAPILOT_AGENT_TRANSPORT_RETRY_BASE_MS (default 300), CLAPILOT_AGENT_TRANSPORT_HEADERS_TIMEOUT_MS
(per-attempt budget until response headers; default 0 = disabled because runs and chat completions stream for
minutes).
Triage order for sporadic fetch failed reports:
agent-transport-checkin the admin runtime console (orcurl -s "https://<host>/api/agent-runtime/transport-status?probe=1"as admin).- If DNS fails:
docker compose ps,docker network inspect <project>_default, confirmCLAPILOT_AGENT_BASE_URLuses the Compose service name. - If connect fails:
docker compose logs --tail=200 clapilot-agent, check for restarts (Watchtower updates restart the runtime container and reassign its IP; the transport retries through those windows). - If
/healthanswers 503: the runtime cannot reach PostgreSQL – seedb-host-checkand the database logs.
Fleet connector updates
Fleet provisioning now enrolls every Hub-created customer instance into the external reachability monitor and issues its instance-bound telemetry sender credential before deployment. The instance adopts that credential on first boot and runs Hub telemetry synchronization immediately and every five minutes, even when no product event triggers a sync. Redeploying an older Fleet instance rotates and installs the credential; the customer instance adopts that current environment value on every boot, even when an older credential is already stored. This provides the onboarding and credential-recovery path for instances created before this behavior existed. Monitoring continues to probe the public HTTPS URL; a lifecycle recovery is successful only after three consecutive externally reachable responses.
Standalone Fleet connector 1.2.0 and newer self-update from the Hub. After a successful heartbeat, an idle connector downloads the allowlisted connector and event-outbox files, verifies the advertised version and JavaScript syntax, replaces the outbox first and connector last, and exits. Its launchd KeepAlive or systemd Restart=always service then starts the new version. Set CLAPILOT_FLEET_SELF_UPDATE=0 only when updates must be managed externally.
Connectors older than 1.2.0 still require one manual reinstall with the command generated on the Hub Fleet Machines page. That reinstall is the one-time bridge to self-updating releases and now downloads both required connector files. Clapilot Remote Runner.app follows its Sparkle release channel instead of the standalone self-update path.
Public reachability and HTTP 530 recovery
Fleet connector 1.4.2 adds robust project reconciliation to the public URL
gate introduced in 1.3.0. A
deploy is reported running only after both the local Compose services are
healthy and the external Cloudflare URL returns 2xx/3xx, 401, or 403. The public
gate requires three consecutive successful responses over at least ten seconds
and has its own two-minute budget. Any HTTP 5xx response or network error resets
the confirmation streak. If Compose applied but HTTP 5xx responses,
including 530, persist, the job retains the public error and the instance is
reported degraded rather than falsely running or treating Compose as absent.
The Hub customer monitor confirms a public outage with two consecutive probes.
On the transition to down, it emits the existing one-time Team Chat alert and,
for an active Fleet-managed kunde instance only, queues one deduplicated
restart job. The connector reconciles the full Compose project with
docker compose up -d --remove-orphans, so a stopped or missing cloudflared
container is recreated as well as restarted, then applies the same external
HTTP gate. If the public probe stays down, the Hub retries once after five
minutes. Ten minutes after the second unsuccessful attempt it emits one Team
Chat escalation naming the instance and the latest HTTP cause. A later
confirmed healthy transition resets the recovery budget and emits one recovery
alert.
Automatic recovery has a customer-fleet circuit breaker. Customer instances are
probed and processed before development/internal targets, and only customer
probe failures count toward the breaker. When at least three customer probes
and at least half of that class fail together, the Hub still records and alerts
the transitions but skips restart jobs because Hub egress, DNS, or a shared
edge incident may be responsible. Isolated recovery is sent only to a
machine seen within the last two minutes and running connector 1.4.2 or newer.
Detailed failed-restart diagnostics require connector 1.4.5 or newer.
Unclaimed restart jobs expire after 15 minutes, and a claimed recovery has a
10-minute job budget.
Incident 2026-08-16: yes.clapilot.com
- At 04:35 UTC the daily check recorded Cloudflare HTTP 530 with 328 ms latency. This is a public ingress/origin failure and is distinct from the earlier deploy-time Error 1033 incident.
- The endpoint had recovered to HTTP 307 (
/login) by 2026-08-17 16:06 UTC. The execution environment did not retain the affected host's container or tunnel logs, so the initiating process exit cannot be identified reliably after the fact. - The actionable root cause was a control-plane blind spot: Fleet considered a
deployment healthy from Docker-local container state only. The periodic
external monitor detected 530 but had no path to restart the origin/tunnel,
and deploy completion did not prove that Cloudflare could reach the origin.
A transient
cloudflaredor origin failure could therefore remain visible until an unrelated/manual restart. - Recovery is now closed-loop: confirmed external outage → one Compose restart
→ three consecutive external URL confirmations → status-change recovery
alert. Job logs retain
compose_up,waiting_external,confirming_external, andexternal_reachableprogress for future incident attribution.
Operational validation after rollout:
curl -sS -o /dev/null -D - --max-time 15 https://yes.clapilot.com
docker compose -p clapilot-yes ps
docker compose -p clapilot-yes logs --since 15m cloudflared clapilot
For a controlled recovery test, stop the Fleet project's cloudflared
container, wait for two five-minute monitoring observations, and verify that a
single restart job is created, docker compose up starts the tunnel, the
connector records three confirmations followed by external_reachable, and
Team Chat receives exactly one down and one recovery message. To exercise
escalation, keep the public endpoint
unreachable through both attempts and ten further minutes; verify one alert
contains the instance hostname and latest HTTP/network cause. Perform this only
in an approved maintenance window.
Local source-debug build
Use the local override when you want to rebuild clapilot from the checked-out source instead of the published GHCR image:
docker compose -f docker-compose.yml -f docker-compose.local-build.yml up -d --build clapilot
For a full local source-built stack:
docker compose -f docker-compose.yml -f docker-compose.local-build.yml up -d --build
The override switches clapilot back to build: . and keeps watchtower behind an opt-in profile.
The settings sidebar footer shows the current Clapilot web build tag, commit hash, local-vs-published build status, and the native agent build.
For the full runtime inventory, use Settings -> Versionen. That page expands the footer data into a live table with the current Clapilot web/native builds plus the installed versions and resolved paths for the directly integrated CLI tools Clapilot uses, including Codex CLI and Claude Code CLI.
Native host install CLI
For non-Docker installs on macOS or Linux, use the repository CLI instead of the dev launcher:
node scripts/clapilot-cli.mjs --help
node scripts/clapilot-cli.mjs install
If you want a global clapilot command for the checked-out repo:
npm link
clapilot --help
The installer supports both:
- interactive wizard flow
- scripted installs via flags like
--env-file,--install-dir,--app-port,--agent-port,--db-port,--public-base-url,--database-url, and--yes
The GitHub-release macOS Apple client embeds the bundled host runtime and can install or control the local server directly. That client variant is built with the Clapilot GitHub Release Xcode scheme or clients/apple/ClapilotApple/scripts/package-github-release-client.sh; it is not used for Xcode Cloud/TestFlight builds. On first launch, when ~/.clapilot is missing, it installs from the bundled clapilot-runtime.tgz, creates launchd-managed web/agent services, seeds a local admin user, and then signs in without showing the normal login screen. On later launches it reads ADMIN_EMAIL, ADMIN_PASSWORD, and PORT from ~/.clapilot/.env.local, starts the managed services when needed, and signs in to the local web app.
When building the Clapilot GitHub Release scheme locally in Xcode, generate the bundled runtime first:
npm run build
npm --prefix clients/host run runtime:bundle
What the install command does:
- installs from the packaged runtime bundle when
CLAPILOT_BUNDLED_RUNTIME_ARCHIVEis provided, otherwise bootstraps or reuses the target git checkout - reads installer defaults and extra runtime vars from
--env-filewhen provided - writes
.env.localfor the native host runtime - defaults the install path to
~/.clapilot - installs or starts a local PostgreSQL service on supported hosts
- creates the local DB role/database when using the managed local PostgreSQL path
- runs the required runtime setup:
- bundled runtime installs run the DB migration/admin-seed scripts directly without
git clone,npm ci, ornpm run build - source-checkout installs still run
npm ci,npm run db:migrate, optional admin seed, andnpm run build
- bundled runtime installs run the DB migration/admin-seed scripts directly without
- creates persistent native services:
- Linux:
systemdunits - macOS:
launchdagents
- Linux:
- can configure a Cloudflare tunnel by delegating to
scripts/cloudflare-tunnel-setup.sh --env-file <native-env-file>when the required Cloudflare vars are present or explicitly requested - writes install metadata under
ops/native-runtime/install-meta.json
There is also a matching uninstall flow:
clapilot uninstall --install-dir ~/.clapilot --yes
That removes the native services, deletes the install directory, and drops the managed local DB objects when they belong to that install. It does not uninstall the system PostgreSQL package itself.
Example non-interactive install:
clapilot install \
--yes \
--env-file .env \
--install-dir /srv/clapilot/current \
--app-port 3000 \
--agent-port 3211 \
--db-port 5432 \
--public-base-url https://app.example.com
Cloudflare tunnel env expected by the installer:
CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_IDCLOUDFLARE_ZONE_IDCLOUDFLARE_TUNNEL_HOSTNAME- optional
CLOUDFLARE_TUNNEL_NAME - optional
CLOUDFLARE_TUNNEL_SERVICE(defaults tohttp://127.0.0.1:<app-port>for native installs)
To pull and redeploy later:
clapilot update --install-dir /srv/clapilot/current
Host app prototype
The repository also includes a desktop host shell in clients/host.
This is a separate gateway/controller app, not the existing Apple end-user client. It wraps the same native install flow so the GUI and terminal stay on one install path.
Use it when you want a local desktop control surface for:
- guided setup with defaults for a local machine
- install directory and env-file pickers
- optional Cloudflare tunnel setup through the existing repo script
- installed-state monitoring for web, agent, database, and Cloudflare
- menu bar / tray icon controls for opening settings, checking running services, and starting or stopping managed services
- host app release checks through GitHub Releases
Local development:
npm --prefix clients/host install
npm run host:dev
Bundle build:
npm run host:build
Current scope:
- macOS and Linux desktop targets
- plain Tauri shell with a static frontend
- runs the existing
scripts/clapilot-cli.mjs install ...flow behind the wizard - packaged builds include an embedded runtime archive so installs do not clone the full repo
- uses
~/.clapilotas the default install path - shows a post-install status dashboard instead of raw command output
- keeps running in the menu bar / tray when the main window is closed
- includes a full-clean uninstall action in the dashboard
- does not yet embed PostgreSQL or
cloudflaredbinaries inside the app bundle itself - still depends on the underlying CLI for host-specific privilege handling
Release packaging:
clients/host/src-tauri/resources/clapilot-runtime.tgzis generated fresh during builds and ignored by git.github/workflows/publish-host-release-tags.ymlruns on release tags, builds the runtime archive and macOS host app on the self-hosted mac runner, signs the.appwith aDeveloper ID Applicationcertificate, notarizes and staples it, then uploads the signed app zip plus runtime archive as GitHub release assets- required Actions secrets for signed releases:
APPLE_CERTIFICATE: base64-encoded.p12for theDeveloper ID ApplicationcertAPPLE_CERTIFICATE_PASSWORDAPPLE_SIGNING_IDENTITYAPPLE_API_KEY: App Store Connect API key.p8contentsAPPLE_API_KEY_IDAPPLE_API_ISSUER
Sparkle updates for GitHub-release macOS apps
The standalone Clapilot macOS client and Clapilot Remote Runner use Sparkle 2 for in-app updates. The tag-release workflow stamps both app bundles with the release version/build, embeds Sparkle.framework, signs and notarizes the apps, and generates a separate EdDSA-signed appcast for each final zip. It publishes those appcasts beside the existing zip and checksum assets in the private f1rede/Clapilot GitHub release.
End-user clients never access the private repository directly. app.clapilot.com uses the server-only GITHUB_RELEASES_TOKEN to locate recent releases, proxy the appcasts, and stream the signed zip or delta asset:
GET /api/desktop-updates/clapilot/appcast.xmlGET /api/desktop-updates/remote-runner/appcast.xmlGET /api/desktop-updates/clapilot/download/<filename>GET /api/desktop-updates/remote-runner/download/<filename>
These endpoints are intentionally public because Sparkle calls them before any Clapilot web session exists. Configure GITHUB_RELEASES_TOKEN on the Clapilot web service as a fine-grained GitHub PAT with read access to the f1rede/Clapilot repository contents/releases. The token stays server-side and is never written into an appcast or app bundle.
The release workflow also requires the Actions secret SPARKLE_ED_PRIVATE_KEY. It is written only to a temporary runner file while generate_appcast signs the two feeds and is removed before the step exits. Both packaged Info.plists contain the matching EdDSA public key (SUPublicEDKey), the app-specific server feed URL, automatic daily checks, and the 24-hour scheduled-check interval. Treat the EdDSA key pair as durable release infrastructure: rotating the baked-in public key breaks the update chain for already-installed builds unless a deliberate key-rotation migration is shipped first.
The first Sparkle-enabled release is a bootstrap release and must be downloaded and installed manually. Once that build is installed, later signed releases can be discovered and installed from the app menu or settings/menu-bar update action.
Host-local native dev
For fast debugging without the container wrapper, the repo also ships a host-local launcher for the Next.js app plus clapilot-agent:
npm run dev:local-native
Running the same command again restarts the launcher by stopping the previous host-local instance first.
Default loopback ports:
- app:
http://localhost:3100 - native runtime:
http://localhost:3211
Optional: start only the PostgreSQL Compose service before launching host processes:
npm run dev:local-native:docker-db
Port overrides:
CLAPILOT_LOCAL_APP_PORT=3200 CLAPILOT_LOCAL_AGENT_PORT=3311 npm run dev:local-native
The launcher:
- reads
.env.localfirst, then.env - points the app to
CLAPILOT_AGENT_BASE_URL=http://127.0.0.1:<agent-port> - points
clapilot-agentback toCLAPILOT_INTERNAL_BASE_URL=http://127.0.0.1:<app-port> - uses the checked-out repository as
CLAPILOT_WORKSPACE_DIR - stores native runtime state under the local
.clapilotaicore/directory - disables background Telegram/WhatsApp channel startup by default so the host-local runtime does not collide with a Docker runtime that is already polling those channels
- prewarms common top-level routes in the background by default, including
/login,/,/chat,/dokumente,/mandanten,/emails,/aufgaben, and/calendar, so the first manual browser visit pays less cold-compile cost innext dev
Stop it explicitly with:
npm run dev:local-native:stop
In-instance developer E2E suites
When developer mode is enabled, administrators can run the isolated regression suites from the developer settings panel. Cleanup mode deletes throwaway sessions and seeded artifacts by default; keep mode retains them for debugging.
| Suite ID | Verification | Live model |
|---|---|---|
webchat-history-replay-context | Stored webchat replay remains capped and ignores synthetic stale-history wrappers. | Yes |
canvas-create-edit-file | A Canvas file is created with the global style and edited in place. | Yes |
chat-pending-run-lifecycle | Pending personal-chat turns recover completed output, expire when stale, and finalize on stop. | No |
automation-delivery-idempotency | Automation delivery reservations and chat-result keys enforce exactly-once delivery, and lease-expired reservations are recovered exactly once (finalized against the persisted message or failed with a lease diagnosis). | No |
aufgaben-live-crud | Aufgaben create/update turns agree across tool events, database state, and UI mutation actions. | Yes |
wiki-context-page-edit | A Wiki page is created, revisioned, and edited in place through active-page context. | Yes |
adaptive-routing-decision-audit | No-override and explicit-model turns agree across adaptive decisions, terminal outcomes, and bootstrap metadata. | Yes |
heartbeat-no-message-gate | User heartbeat runs suppress NO_MESSAGE, persist silent WATCH directives, and deliver a marked message exactly once. | Yes |
notizen-live-note-page-edit | A Notizen note and its first page are created and edited in place with matching UI mutations. | Yes |
run-source-type-and-history-cap | Chat and harness source types persist correctly and apply the expected stored-history cap behavior. | Yes |
module-tool-gating | Temporarily opts the main agent into explicit per-tool restrictions, then proves direct execution rejects a blocked tool, allows it after removal from the denylist, and leaves an unrelated core tool available. | No |
excel-canvas-cell-mutation | A live Excel Canvas turn mutates B2 with a raw numeric value and persists the workbook plus UI action. | Yes |
whiteboard-live-scene-mutation | A live Whiteboard turn adds one text item and advances the existing board revision without image generation. | Yes |
specialist-personal-memory-isolation | A specialist's personal memory remains invisible to shared search and another specialist. | Yes |
memory-dream-wiki-proposal | Memory Dream keeps any optional Wiki output review-gated, never creates a direct page, keeps stats and persisted proposal rows consistent, and rollback compensates without inventing output. | Yes |
run-stop-abort-roundtrip | An active model run stops through the runtime abort path and persists consistent cancellation state. | Yes |
prompt-cache-prefix-stability | Two identical turns retain a byte-identical cache-stable prefix hash and replay the first turn without resetting it. | Yes |
capability-tool-catalog-consistency | Module gates, tool-granting auth resources, and developer-suite tool references resolve against the runtime catalog. | No |
channel-inbound-dedup | Blocked Telegram inbound events persist once per event id, deduplicate identical payloads, and start no run. | No |
harness-todo-uiactions-parity | Configured native, Claude, and Codex harnesses emit normalized todo lists and lifted Copilot UI card actions. | Yes |
The scheduled workflow in .github/workflows/nightly-e2e.yml runs this complete
catalog after its browser suite against the same temporary source-built
instance. It temporarily enables Developer mode, executes every registered
suite sequentially with artifact cleanup enabled, restores the previous
Developer mode value, and fails if any suite does not pass. Results are stored
as instance-e2e-results.json and instance-e2e-report.md and are included in
the hosted nightly report. The optional
CLAPILOT_CI_INSTANCE_E2E_MODEL repository variable selects an explicit model;
otherwise the suites verify the instance's normal default routing.
GHCR auth for default image deploys
The default compose stack requires two GHCR auth paths:
- host Docker auth in
${HOME}/.docker/config.jsonfordocker pullanddocker compose up - Watchtower auth in
${HOME}/.docker/watchtower-config.json
Populate the host Docker auth with:
mkdir -p "${HOME}/.docker"
echo "<GHCR_READ_PACKAGES_TOKEN>" | docker login ghcr.io -u "<github-username>" --password-stdin
The Watchtower file should contain inline registry auth for ghcr.io, for example:
{
"auths": {
"ghcr.io": {
"auth": "<base64(GITHUB_USERNAME:GHCR_READ_PACKAGES_TOKEN)>"
}
}
}
Requirements:
- the token needs
read:packages - use
${HOME}/.docker/..., not~/.docker/..., in Compose-backed paths docker login ghcr.io ...only solves host pulls; it does not configure Watchtower- Watchtower needs a separate mounted file because host Docker configs commonly rely on
credsStore, which is not available inside the Watchtower container - on another Mac, create both files in that user's home directory or change the bind mount in
docker-compose.yml - the local source-build override in
docker-compose.local-build.ymldoes not require this file unless you explicitly enable thewatchtowerprofile
Container publishing
The repository includes a GitHub Actions workflow at .github/workflows/publish-container-main.yml.
It publishes the runtime image to GHCR on every push to main from the self-hosted mac-mini-m4-pro (macOS/ARM64) runner.
Published tags:
ghcr.io/<owner>/<repo>:latest-arm64ghcr.io/<owner>/<repo>:main-arm64ghcr.io/<owner>/<repo>:sha-<full-commit-sha>-arm64
When an immutable release container package is needed, run .github/workflows/publish-container-release-tags.yml manually with the release git tag. The workflow waits for the main image with the same commit SHA and retags that image into a separate release package:
ghcr.io/<owner>/<repo>-release:<git-tag>-arm64ghcr.io/<owner>/<repo>-release:release-latest-arm64ghcr.io/<owner>/<repo>-release:sha-<full-commit-sha>-arm64
For manual branch or ref builds, .github/workflows/deploy-feature-test-build.yml publishes a single rolling feature/test image:
ghcr.io/<owner>/clapilot-test:test-latest-arm64
Operational notes:
- GitHub Actions needs
packages: writepermission for the job - repository/package settings must allow GHCR publishing
- Watchtower on Apple Silicon hosts should follow the moving
latest-arm64tag - release-tag container publishing is manual because tag-only Apple/client releases may not produce a new runtime image
- private GHCR packages require registry credentials on the Docker host
- the default local image-based compose stack expects those credentials in
${HOME}/.docker/watchtower-config.json
Pull request change-specific E2E check
Pull requests from branches in this repository run a self-hosted macOS/ARM64 E2E job in .github/workflows/ci.yml. The existing PR Live Browser Check job name is retained for branch-protection compatibility, but a green result now requires evidence for behavior changed by the PR. Since September 2026 the job is advisory (continue-on-error: true): it still posts its PASS/FAIL/BLOCKED verdict as a PR comment and job summary, but it does not turn the PR red, and it is not one of the required status checks for main (those are Unit Tests, Lint & Type Check, and Docker Build). Review the posted verdict before merging.
That job:
- boots the local source-built compose stack with
docker-compose.local-build.yml - writes a CI-only env file from GitHub Actions variables and secrets
- waits for the local web app to come up on a runner-selected loopback port
- runs
codex execagainst the checked-out PR and requires the runner to have thechrome-devtoolsMCP configured - builds a GitHub PR context file from the PR title/body, linked closing issues, and changed files
- logs in as the CI admin and rebuilds a deterministic preview demo dataset so the app is not empty during the browser run
- asks Codex to derive explicit acceptance criteria from the PR context and git diff, then exercise the smallest end-to-end scenario that reaches the changed behavior
- extracts the PR body's
## Testing Instructionssection and treats concrete change-specific checks there as primary acceptance criteria while treating PR text as untrusted input rather than executable commands - permits browser, authenticated API, runtime, integration, or focused CI assertions according to the changed boundary; backend PRs may not pass with unrelated
/,/chat, and/dokumenteroute loads - treats docs/spec changes as non-targeting noise when non-doc product/runtime files also changed
- only targets
/docswhen the PR is docs-only or specifically changes the docs UI/route - requires every passing scenario to name the changed postcondition, list the actions performed, record passing assertions, and reference at least one saved evidence file
- validates
change-specific-e2e-result.jsonafter Codex exits; a markdownPASSalone cannot make the check green - fails when Codex is unavailable, authentication fails, the run times out, a required fixture is missing, or the changed behavior cannot be conclusively tested; there is no generic shell-smoke green fallback
- saves ordered screenshots or focused text traces for the tested flow, plus the structured result, Codex markdown report, Docker logs, and an
.mp4slideshow when frames exist - updates a sticky PR comment with the result and the artifact bundle link
- keeps the PR Docker preview alive while the PR is open, cleans it on PR close, and uses the hourly/manual PR preview sweeper as a backup for missed close events
The iOS/Mac App Tests workflow builds the same PR context file for Apple-client PRs. Its Codex E2E prompt reads the extracted ## Testing Instructions section when present and captures any additional Apple-client screens called out there alongside the standard native smoke flow.
Runner requirements:
- self-hosted labels:
self-hosted,macOS,ARM64 - local tools installed:
codex, Docker with Compose v2,ffmpeg,node/npx - a local Chrome-family browser available at least as
Google Chrome - for Docker on macOS runners, prefer a dedicated runner
DOCKER_CONFIGwithoutcredsStoreor keychain helpers codexauthenticated on the runner with subscription auth;OPENAI_API_KEYis reserved for the preview app unlessCI_CODEX_AUTH_MODE=api_keyis explicitly set
CI env contract:
- required secret:
CLAPILOT_CI_AUTH_SECRET(falls back toAUTH_SECRETif you intentionally mirror it there) - optional secret:
CLAPILOT_CI_ADMIN_PASSWORD - optional variable:
CLAPILOT_CI_ADMIN_EMAIL - optional multiline variable:
CLAPILOT_CI_ENV_VARS - optional multiline secret:
CLAPILOT_CI_ENV_SECRETS - fallback multiline secret already used elsewhere in this repo:
CLOUD_RUN_ENV_VARS - optional variable:
CLAPILOT_CI_DEMO_SEED(trueby default) - optional variable:
CLAPILOT_CI_DEMO_SEED_SCENARIO(defaults to the app's default demo scenario) - optional variable:
CLAPILOT_CI_OPENAI_MODELSfor the native OpenAI provider catalog shown only in preview/demo instances - optional variable:
CLAPILOT_CI_EMAIL_ANALYSIS_MODELfor lightweight mail/document analysis defaults in preview/demo instances - optional variable:
CI_CODEX_AUTH_MODEfor the Codex test runner auth source (subscriptionby default,api_keyonly for explicit fallback) - optional variable:
CI_CODEX_MODELfor the Codex test runner model (gpt-5.6-solby default) - optional variable:
CI_CODEX_REASONING_EFFORTfor the Codex test runner reasoning level (mediumby default) - optional variable:
CLAPILOT_CI_TEST_EMAIL_ADDRESS - optional secret:
CLAPILOT_CI_TEST_EMAIL_PASSWORD - optional variables:
CLAPILOT_CI_TEST_IMAP_HOST,CLAPILOT_CI_TEST_IMAP_PORT,CLAPILOT_CI_TEST_SMTP_HOST,CLAPILOT_CI_TEST_SMTP_PORT - optional hosted upload API variable:
LIVE_CHECK_UPLOAD_API_URL - optional hosted upload API secret:
LIVE_CHECK_UPLOAD_TOKEN - optional hosted publish variables:
LIVE_CHECK_SHARE_HOST,LIVE_CHECK_SHARE_USER,LIVE_CHECK_SHARE_REMOTE_PATH,LIVE_CHECK_SHARE_BASE_URL - optional hosted publish secrets:
LIVE_CHECK_SHARE_SSH_KEY,LIVE_CHECK_SHARE_KNOWN_HOSTS - PR preview cleanup/sweeper variables/secrets:
CLAPILOT_PREVIEW_RUNNER_LABELS, optionalCLAPILOT_PREVIEW_CLEANUP_RUNNER_LABELS, optionalCLAPILOT_PREVIEW_POOL_LABEL(defaultclapilot-preview, shared label on every preview-capable runner for load-based first assignment),CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_ZONE_ID,CLOUDFLARE_PREVIEW_DOMAIN
The live browser seed reuses that test mailbox for both the agent mailbox defaults and the seeded admin user's personal mailbox profile, so /emails and background agent mail actions can both run against the same non-production account.
For new self-hosted macOS runners, bootstrap Docker once with scripts/ci/setup-runner-docker-config.sh. That script writes a clean config such as ${HOME}/.docker-ci/config.json with no credsStore, so GitHub Actions jobs do not depend on an interactive login keychain session.
Install the machine dependencies first with scripts/ci/setup-macos-runner-deps.sh. It installs codex, ffmpeg, node, and Google Chrome via Homebrew, which covers the prerequisites used by the self-hosted browser jobs.
Recommended runner setup:
- run
scripts/ci/setup-macos-runner-deps.sh - authenticate
codex loginfor the runner user - run
scripts/ci/setup-runner-docker-config.sh - set the runner service environment to
DOCKER_CONFIG=${HOME}/.docker-ci - if the runner needs registry auth, log in with
docker --config "${HOME}/.docker-ci" login ... - the live-browser workflow bootstraps the
chrome-devtoolsMCP itself withnpx; no permanent MCP config is required on the machine - do not rely on Docker Desktop's default keychain-backed credential store for headless runner jobs
The browser-oriented self-hosted workflows also run scripts/ci/setup-macos-runner-deps.sh before tool verification, so a newly provisioned runner can install missing node/npx, ffmpeg, codex, or Google Chrome automatically instead of failing immediately.
Use the multiline env entries for extra compose env such as provider keys or feature flags that should only exist in the live-browser CI run.
If the hosted upload API config is present, the workflow uploads the captured frames, report, gallery page, and optional .mp4 over HTTPS and embeds the public frame URLs into the PR comment.
If the upload API is not configured but the SFTP-based hosted publish config is present, the workflow falls back to SFTP publishing.
E2E performance gate
The live-browser E2E runner executes node scripts/perf-radar.mjs --gate --smoke inside the running clapilot container after a successful browser suite. The historical gate reads terminal agent_runs for the current Berlin calendar day, so one successful chat turn is one latency sample even when tool loops or provider fallback produced several agent_model_request_logs. It performs one active authenticated /api/chat smoke turn in a fresh, titled, disposable chat session. The fixed title prevents the probe from starting an unrelated model-backed auto-title request. Only a smoke request carrying the app's internal secret is tagged as ci_perf_gate and excluded from the historical rows; a caller-controlled page context alone cannot exclude traffic. A smoke turn passes only when its response ends with a fresh per-run verification token, its exact runtime session has a completed ci_perf_gate run created after the probe began, and the same run has a completed provider request log. This correlation prevents an HTTP-200 error stream or an unpersisted runtime failure from being reported as successful. The runner always writes perf-gate-report.txt into the E2E artifact directory, including an explanation when the gate is intentionally skipped.
The default gate thresholds are configurable through CI environment variables:
PERF_MAX_FAIL_PCT: maximum daily terminal agent-run failure rate, default10.PERF_MAX_CHAT_P90_S: interactive chat p90 SLO in seconds, default90. This initial unfiltered target should be tightened only after enough post-rollout samples establish the new baseline.PERF_MAX_CHAT_P90_REGRESSION_PCT: maximum increase over the average p90 of preceding days that meet the minimum request count, default20. The gate fails on either the absolute SLO or the relative regression.PERF_MIN_REQS_FOR_GATE: minimum daily count of successful chat turns before historical thresholds fail the run, default15.PERF_SMOKE_TIMEOUT_S: per-request timeout for login, session setup, and the streamed smoke response; defaults to at least120seconds.PERF_SMOKE_MESSAGE: optional main-chat smoke prompt. It is forwarded from either the Compose/CI environment or the workflow host. It must not contain an@handle; the radar rejects such prompts before login so the probe cannot enter specialist-agent routing. The radar appends its own unique verification-token instruction to custom prompts.
These variables may be supplied through the CI container env file or exported by the workflow host. Empty values fall back to the defaults. When active smoke is enabled, missing smoke credentials fail --gate instead of silently skipping the probe.
Normal native one-to-one chat turns are intentionally persisted with source_type=chat, so they participate in the chat p50/p90 latency gate. This can shift the latency baseline compared with older builds where some native rows had a null source type. The daily failure-rate population does not change for those rows because it already counts every non-ci_perf_gate request regardless of source type. Treat the first full day after rollout as the new latency baseline and change thresholds only when the production SLO calls for it.
The radar does not discard turns longer than five minutes. It reports the ten slowest successful chat turns with a hashed run ID and an end-to-end split from agent_chat_turn_phase_metrics: queue time, provider/orchestration time outside tools, tool execution, failed provider/fallback time, and finalization after the last provider or tool event. The five phases add up to the complete agent_runs.created_at to completed_at duration (apart from millisecond rounding). Provider/model, hashed session, estimated prompt-size, and initial versus post-tool request breakdowns remain diagnostic attempt-level views. Use --compare-cutover <ISO timestamp> --compare-sample-size <N> to compare the last N successful chat turns before a deployment with the first N after it; when either cohort is short, the report shows the available counts instead of silently omitting the comparison. This is the canonical before/after measurement for latency changes.
The 29 July 2026 investigation showed that final run status alone was not a useful latency signal. In the available Top-10 sample, seven turns spent more than 100 seconds end to end. Six of those repeatedly crossed provider/tool boundaries (11-46 recorded tool starts), while one 297-second turn spent about 253 seconds in the queue before a 45-second execution. No failed provider attempt was needed for most of these successful outliers. This is why the gate uses one completed run per sample and why the Top-10 phase report must be reviewed even when the run failure rate remains below its threshold.
OpenAI-compatible streaming requests have a time-to-first-response and stream-idle deadline controlled by CLAPILOT_PROVIDER_REQUEST_TIMEOUT_MS (default 120000). Data events and SSE comment heartbeats refresh the deadline, so healthy long generations are not aborted by total wall-clock duration. Non-streaming Responses and chat-completions calls use the separate wall-clock cap CLAPILOT_PROVIDER_NON_STREAMING_REQUEST_TIMEOUT_MS (default 300000) because they cannot expose progress heartbeats. Expiry before user-visible output enters configured provider fallback/circuit handling without replaying the same slow request; expiry after output terminates the stalled stream because replaying it would duplicate visible content. Other short-lived transient failures retain the bounded retry policy controlled by CLAPILOT_PROVIDER_TIMEOUT_MAX_ATTEMPTS. OpenAI and Azure OpenAI streaming chat-completions requests send stream_options.include_usage=true. OpenAI-compatible servers receive the same standard field only when the existing compatible prompt-cache opt-in is enabled, preserving the strict-server fallback. Request-log metadata records timeToFirstTokenMs from the first meaningful reasoning, text, or tool-call delta; missing usage remains NULL rather than being recorded as a misleading zero. The same guards cover OpenAI Responses API requests.
Direct human-facing chat turns intentionally have no end-to-end runtime deadline. A model/tool loop may therefore continue for as long as the task requires; only an explicit caller-owned timeout can cap the whole conversation. The stdio MCP bridge and Codex MCP rescue path likewise add no independent wall-clock deadline around /api/agent-runtime/tool-proxy; the retired CLAPILOT_MCP_TOOL_PROXY_TIMEOUT_MS and CLAPILOT_MCP_IMAGE_TOOL_PROXY_TIMEOUT_MS settings are ignored. Independent provider-stall, native tool-phase and fallback safeguards default to 120, 120 and 45 seconds and are never clipped by elapsed turn time. A chat turn queued behind a long turn in the same session survives up to CLAPILOT_CHAT_QUEUE_BUDGET_MS=300000 before it is failed (and the failure is persisted as a failed run). The paid mixture-of-agents reference fan-out is exempt from the preflight cutoff, and finalization overruns are logged warnings that never fail an already-successful run. Long tools emit tool.progress every 30 seconds. These phase safeguards never apply to jobs, automations, channel runs, or orchestrator turns; long-running background work is first-class. Phase defaults and their environment variables are documented in .env.example. Repository Orchestrator turns are reported separately and keep the 45-minute broker timeout (CLAPILOT_AGENT_TURN_TIMEOUT_MS=2700000, 10-minute floor); an expired orchestrator turn is actively interrupted on the app-server (turn/interrupt) instead of being left running detached. The Performance Radar computes user-visible latency from agent_runs, not individual model requests, and prints separate direct_chat, orchestrator, and automation_channel_api cohorts. Its slow-run section joins agent_events to expose queue/preflight, provider, tool and finalization timing for the top outliers.
Provider fallbacks are failure-domain aware. The provider slug is the failure-domain key, so a connection failure or provider stall quarantines that gateway for the current request and skips later models on the same slug. The remaining fallback budget is divided across the still-reachable independent provider slugs, preserving time for the final emergency provider. Provider-attempt telemetry records skipped_failure_domain candidates and remainingFallbackBudgetMs. Saving runtime provider configuration returns a warning when a configured fallback chain contains models but no enabled independent provider slug.
Every turn also has a hard provider envelope: three upstream requests, two provider failure domains, and two identical failures at one provider/model/endpoint boundary by default. Configure these with CLAPILOT_PROVIDER_TURN_MAX_REQUESTS, CLAPILOT_PROVIDER_TURN_MAX_FAILURE_DOMAINS, and CLAPILOT_PROVIDER_TURN_MAX_IDENTICAL_FAILURES. An open timeout circuit permits one half-open recovery request per CLAPILOT_PROVIDER_TIMEOUT_CIRCUIT_PROBE_INTERVAL_MS (30 seconds by default); every failed probe doubles that wait, capped by the open duration, and the backoff is carried across re-opens until a probe succeeds. Every re-open without a success in between doubles the open window itself (10, 20, 40, 80 minutes, capped by CLAPILOT_PROVIDER_TIMEOUT_CIRCUIT_MAX_OPEN_MS, default 2 hours). When a fallback can answer the turn, the probe or the first request after the window receives a bounded stall budget instead of the full one: CLAPILOT_PROVIDER_TIMEOUT_CIRCUIT_PROBE_TIMEOUT_MS (20 seconds by default) doubled per failed probe up to the provider's normal budget, never more than half of the remaining conversation deadline, so a stalling provider such as Grok costs the turn one bounded probe and the independent fallback still answers; the attempt records circuitProbe=true, probeRequestTimeoutMs, and probeBudgetReduced=true when the budget was shorter than the request would normally get. A probe that stalls against a reduced budget re-opens the circuit without doubling the open window (only failures against the normal budget escalate), so a slow-but-healthy model is measured against its real budget after a few probes instead of being ratcheted out of routing for hours. The same escalation applies to the endpoint (connection) circuit when its probe fails with a timeout. The timeout circuit counts fixed provider timeouts (stall deadlines, HTTP 408/504/524, hard conversation deadlines that fired on the provider request) both consecutively and inside a sliding CLAPILOT_PROVIDER_TIMEOUT_CIRCUIT_WINDOW_MS window (15 minutes by default), and it also counts stalls that happen after tool progress in the same run. Once open, later runs skip the provider with status=circuit_open and continue with the configured fallback; the terminal finalProvider attempt names the model that actually answered. The provider status endpoint (GET /api/agent-runtime/provider-status) and the provider dialog in Einstellungen show the open, half-open, and degraded circuits per model with the counted timeouts, open-until time, next probe time, and last error, so an unstable provider such as a stalling GLM endpoint is visible without reading logs. The state is mirrored into agent_provider_circuit_states (migration 305) and restored on the first provider request after a runtime restart, so a Watchtower image update or crash no longer resets an open circuit; a successful probe closes the circuit and deletes the row, a restore that fails because the database is not reachable yet is retried on the next request, and stale rows are pruned at restore time. Because a restart no longer clears the breaker, every circuit row in the provider dialog has a reset button (DELETE /api/agent-runtime/provider-status/circuit) that removes the in-memory state and the database row; the dialog also shows the failed probe count, whether the state was restored after a restart, and whether the database mirror is currently active. Attempt telemetry includes used and remaining request budget, used failure domains, identical-failure count, circuit state (circuitOpenTrigger, circuitReopenStreak, circuitOpenDurationMs), and a terminal finalProvider marker.
Verifying a provider timeout regression fix in production (for example the recurring Grok-4.6 Provider conversation exceeded its 60s limit / Provider request stalled for 22s cascade) requires evidence from the running instance, not only a merged pull request. Keep the ticket open until all of the following hold on .24 after the image rolled out: (1) GET /api/agent-runtime/provider-status?slug=<xai-slug> shows the circuit as open with reopenStreak growing while the provider still stalls, persisted: true, and restored: true after a container restart; (2) SELECT circuit_key, open_until, state->>'reopenStreak' FROM agent_provider_circuit_states lists the tripped model; (3) the daily count of failed Grok attempts in agent_model_request_logs drops to the bounded probe schedule (single-digit probes per hour at most, each stalled at the probe budget rather than the 45s/60s conversation limit) while the same turns show finalProvider on an independent fallback provider; (4) at least one independent fallback provider slug is enabled in the xAI provider's fallback chain, because without it the breaker can only fail faster, not answer.
The official Clapilot runtime image installs Cursor's official CLI and exposes it as cursor-agent; verify it with cursor-agent --version inside the ClapilotAICore container. Custom images can opt out at build time with INSTALL_CURSOR_CLI=false. Set CLAPILOT_AGENT_CURSOR_CLI_COMMAND when a custom deployment keeps the executable outside PATH, then add the Cursor provider and its User API key in Settings. Clapilot does not embed the proprietary Cursor SDK. A missing CLI disables only the Cursor row; direct providers and other subscription bridges remain available as fallbacks.
Chat phase budgets can be overridden per provider and per model in runtime config instead of environment variables. The provider edit dialog (settings → ClapilotAICore → providers) exposes dedicated second-based fields under "Timeouts & latency budgets" for model response without output, tool execution, queue wait, fallback budget, and the background stall timeout; empty fields keep the system defaults. The fields persist to chatLatencyBudgets in the provider's metadata as an object with any of queueMs, preflightMs, providerMs, nonStreamingProviderMs (whole-response budget for non-streaming interactive turns, advanced metadata JSON only), toolMs, fallbackMs, finalizationMs; totalMs from older configurations is ignored and cannot restore a whole-conversation deadline. Add modelChatLatencyBudgets keyed by model name (advanced metadata JSON) for model-specific values that win over the provider-level object. Example for a slow self-hosted gateway: {"chatLatencyBudgets": {"providerMs": 300000, "queueMs": 900000}}. Model overrides beat provider overrides, which beat the CLAPILOT_CHAT_* environment defaults; the environment variables remain global fallbacks only. Because the queue check for a waiting turn runs before provider selection, it uses the budgets resolved by the previous turn in the same session. These budgets apply to interactive turns: direct chat and interactive channel turns (Telegram, WhatsApp, Slack, Signal, iMessage, Team-Chat mirrors) alike, so a stalled Team-Chat request fails at the phase budget with a saved checkpoint instead of running into the 20-minute channel cap. The tool budget protects only turns that have not yet confirmed a successful tool completion. After confirmed tool progress, later validation/finalization tools are not phase-aborted because replay could duplicate an already executed mutation; explicit user cancellation remains active. Background workloads (jobs, automations, automation-keyed channel deliveries) stay unbudgeted, but their provider stall timeout can be raised per provider with metadata providerRequestTimeoutMs (optionally modelProviderRequestTimeoutMs keyed by model) when a self-hosted model needs more than the default 120 seconds before its first streamed token.
OpenAI-compatible providers can also opt into per-model request scheduling through provider metadata. Set maxConcurrentRequests to cap simultaneous upstream requests and maxBackgroundConcurrentRequests to reserve the remaining capacity for interactive chat. Interactive requests are always dequeued before background automation. The scheduler is disabled when these metadata fields are absent, and request logs record queue wait, priority, and the active limits for diagnosis.
Set CI_PERF_GATE_ENABLED=false only for emergency runner diagnostics. The active smoke remains useful when the day has too little traffic for the historical minimum, because smoke failures and smoke latency breaches are evaluated independently.
Agent runtime console (admin UI)
For Cloud Run setups without container shell access, use:
- the admin runtime console in Clapilot settings
Built-in diagnostics:
helpls-state-dirshow-clapilotaicore-jsonvalidate-clapilotaicore-jsonnode-versiondb-host-checkagent-transport-check(app →clapilot-agenttransport counters plus a live DNS +/healthprobe; see "Agent runtime transport diagnostics")
Notes:
- access is admin-only (same role check as other admin APIs)
- the packaged OpenClaw CLI is no longer present in the Docker image
- this is intentionally not a full unrestricted OS shell
- use it to diagnose native runtime connectivity and state remotely
RAG runtime controls
RAG (retrieval-augmented generation) runtime toggles (container env):
RAG_ENABLED=trueenables retrieval injection in/api/chat(default: enabled)RAG_INDEXER_ENABLED=trueenables background indexing worker (default: enabled)RAG_RETRIEVAL_TOP_KandRAG_RETRIEVAL_MIN_SCOREtune retrieval breadth/qualityRAG_CHUNK_SIZEandRAG_CHUNK_OVERLAPtune chunking granularity- embeddings key source:
OPENAI_API_KEYorapp_settings.openai_api_key
RAG diagnostics:
docker compose logs -f clapilot | rg -n "rag-indexer|chat-rag"
Instance cleanup worker
scripts/instance-cleanup-worker.mjs is the retention engine of an instance. Its decision logic lives in scripts/lib/instance-cleanup-core.mjs and is unit tested in tests/scripts/instance-cleanup-core.test.ts.
CLAPILOT_INSTANCE_CLEANUP_ENABLED=truestarts the web-container cleanup worker (default: enabled)CLAPILOT_INSTANCE_CLEANUP_POLL_SECONDS=1800controls the cleanup intervalCLAPILOT_INSTANCE_CLEANUP_MIN_FREE_GB=20andCLAPILOT_INSTANCE_CLEANUP_TARGET_FREE_GB=40define the disk-pressure windowCLAPILOT_INSTANCE_CLEANUP_MAX_DELETE_GB=25caps deletion per cycleCLAPILOT_INSTANCE_CLEANUP_DRY_RUN=truelogs planned deletions without removing filesCLAPILOT_INSTANCE_CLEANUP_LINKED_SESSION_IDLE_HOURS=72limits how long an idle (ready,failed, ...) external session protects its workspace; running sessions always protectCLAPILOT_INSTANCE_CLEANUP_TOOL_CACHE_AGE_HOURS=72is the minimum age for cache-tier entriesCLAPILOT_INSTANCE_STORAGE_REPORT_SECONDS=3600controls how often the storage attribution report is refreshed
Retention tiers:
- Scratch tier (deleted as soon as the retention window has passed, regardless of disk pressure): Agent Orchestrator PR-review (
6h), issue (24h), main-CI (24h), tracked-PR (24h), manual-job (24h), and Symphony (72h) workspaces, orchestrator session directories (72h), unlinked bare repo caches (336h), andtmp/.tmp(72h). Worktrees with unpublished commits are kept forCLAPILOT_INSTANCE_CLEANUP_UNPUBLISHED_RETENTION_HOURS(168h). - Cache tier (only drained while free space is below
CLAPILOT_INSTANCE_CLEANUP_MIN_FREE_GB): the Claude CLI runtime'spipandnpmcaches under.clapilotaicore/claude-cli. They are re-downloadable, so they are sacrificed before anything else has to be touched manually, but never while the disk is healthy. - Protected (never deleted automatically):
mandanten,.clapilot/*generated media,.clapilotaicoreruntime state and attachments, installedmodules/skills, and every other workspace directory such asvideo-studio,livestream,podcasts, orexports. These only appear in the storage report.
Protection rules:
- Candidates are always direct children of an allowlisted cleanup root; roots that equal or contain the workspace root or a protected path are rejected at startup.
- Workspace paths reported by live
agent_external_sessionsrows and queued/running orchestrator jobs protect exactly the workspace they name. A session bound to a broad path such as the workspace root itself is ignored and counted in the cycle log, because honoring it would shadow every candidate (this was the bug that let orphaned worktrees fill the disk to 99%). - If the session table or
jobs.jsoncannot be read, worktree roots are skipped for that cycle instead of guessing. - Under pressure the largest expired entries go first; otherwise the oldest go first.
Each cycle logs [instance-cleanup] cycle complete: candidates=… removed=… reclaimed=… free=… level=ok|warning|critical. Once per CLAPILOT_INSTANCE_STORAGE_REPORT_SECONDS the worker measures the workspace by source (orchestrator worktrees, repo caches, runtime caches, generated media, work data, database size) and writes <state dir>/instance-storage/report.json. The report feeds Einstellungen → Speicher (/admin/storage), GET /api/admin/storage, and the instance_storage_status agent tool.
Instance storage monitor and alerts
The Next.js server starts an in-process monitor from src/instrumentation.ts (src/lib/instance-storage.ts). Every five minutes it reads the workspace filesystem via statfs, logs a workspace_disk_pressure event when the level is not ok, and posts to the configured monitoring alert room as Speicher-Monitoring:
- warning when free space drops below
CLAPILOT_INSTANCE_STORAGE_WARN_FREE_GB(15) or usage exceedsCLAPILOT_INSTANCE_STORAGE_WARN_USED_PERCENT(90) - critical when free space drops below
CLAPILOT_INSTANCE_STORAGE_CRITICAL_FREE_GB(5) or usage exceedsCLAPILOT_INSTANCE_STORAGE_CRITICAL_USED_PERCENT(97) - a single recovery message once the level returns to
ok
Level changes always post immediately; the same level is repeated only after CLAPILOT_INSTANCE_STORAGE_ALERT_COOLDOWN_HOURS (6). Alert state is derived from the latest chat_group_messages row with message_meta.source = instance_storage_monitor, so restarts do not re-alert. The same thresholds drive the level shown on /admin/storage and in the cleanup worker log.
The target room is shared with hub customer monitoring: Settings -> Hub -> Monitoring -> Target channel for system alerts (app_settings.monitoring_alert_room_id, GET/PUT /api/hub/monitoring/alert-room). Empty means the main Team Chat room clapilot-members (#general). At delivery time the monitor verifies that the configured room still exists and is neither deleted nor archived; otherwise it posts to the main room, logs monitoring_alert_room_fallback with source, configuredRoomId, and reason, and marks the message with message_meta.roomFallback = true. A cycle is skipped with room_missing only when the resolved room itself does not exist.
The Agent Orchestrator additionally refuses to materialize a new worktree when free space is below AGENT_ORCHESTRATOR_MIN_FREE_DISK_GIB (5; 0 disables), and its own hourly workspace GC stops treating idle linked chat sessions as live after AGENT_ORCHESTRATOR_LINKED_SESSION_IDLE_TTL_HOURS (72). AGENT_ORCHESTRATOR_WORKSPACE_HARD_LIMIT_GIB remains available as an absolute worktree quota.
Video Studio project reconciler
src/instrumentation.tsstarts the in-process Video Studio reconciler with the Node server.CLAPILOT_VIDEO_STUDIO_RECONCILE_SECONDS=60controls the interval;0disables it.- Each non-overlapping tick checks at most ten owner-fair active projects in
generatingorconcatenating, plus stalestoryboard_generatingprojects. - Create requests are deduplicated: every AI project stores a
metadata.createFingerprint(hash of the effective create parameters). An identical request from the same user inside 15 minutes resumes the existing project instead of creating another one – a still-running storyboard is returned as-is, a project that failed before any scene existed gets its storyboard retried, and a failed project that already has scenes is returned for resumption. This is what keeps a transient runtime outage during storyboard generation from producing several identical projects when the user or the agent submits the request again. - The worker uses the same
reconcileAiProjectpath as the project status API, including the guarded single-winner transition before ffmpeg concatenation. It logs a tick summary only when project or scene state changed. - OpenAI-compatible/ComfyUI submissions use the generated-video UUID as both
Idempotency-KeyandX-Request-ID. A submit-side 504 never triggers anotherPOST: Clapilot checks the provider by that stable key with bounded exponential backoff (three checks by default), then either persists the recovered provider task id for normal polling or marks the scene and project terminally failed with the attempt count and last cause. A 504 response that already carries a task id is treated as an accepted job. - Provider settings can override
submitReconcileEndpoint(placeholders{idempotency_key}or{idempotencyKey}),submitMaxAttempts(1–5), andsubmitRetryBaseDelayMs(0–60000). Without an override, reconciliation usesGET <configured video submit endpoint>/{idempotency_key}. The provider/gateway must map the supplied idempotency key to the accepted job.
Automation delivery recovery worker
src/instrumentation.tsstarts the in-process automation delivery recovery worker with the Node server, so a deploy or container restart that killed a delivery mid-flight is repaired right after boot.- Every reservation in
automation_target_deliveriescarries a lease:CLAPILOT_AUTOMATION_DELIVERY_LEASE_MS=900000(15 minutes; clamped to 1 minute–24 hours). Areservedrow whose lease is older thannow()is treated as abandoned by its owner. CLAPILOT_AUTOMATION_DELIVERY_RECOVERY_SECONDS=60controls the sweep interval;0disables the worker. Each tick claims at most 25 expired rows withFOR UPDATE SKIP LOCKEDand renews their lease in the same statement, so parallel web replicas and a genuine retry through the delivery routes never recover the same reservation twice.- Decision per claimed row, recorded in
recovery_outcome,recovered_at, anderror_message:finalized_persisted_message: achat_nachrichtenorchat_group_messagesrow already carries the delivery key, so the message landed and only the status write was lost. The row becomesdeliveredwithout sending anything.redelivered/redelivery_duplicate: the payload snapshot stored at reservation time (delivery_payload) is delivered again with the same delivery key; the chat unique indexes make the replay idempotent. Replayed messages carrymessage_meta.automation_delivery_recovered = true. Only the replay itself is treated as a redelivery failure: if the final status write fails after the message was persisted, the row staysreservedand the next tick finalizes it asfinalized_persisted_message.no_replay_payload,redelivery_failed,recovery_attempts_exhausted(after three recovery claims),external_channel_unverifiable(approved external-channel sends are never repeated): the row becomesfailedwith a diagnosis naming the reservation time, lease length, and expiry. A later genuine retry can still reclaim afailedrow.
- The worker logs one
automation_delivery_recoveryJSON line per tick that claimed rows. Verify on an instance with:
SELECT delivery_key, target_kind, status, created_at, lease_expires_at, recovery_outcome, error_message
FROM automation_target_deliveries
WHERE status = 'reserved' AND lease_expires_at <= now();
The query must return no rows once the worker has run.
Agent Orchestrator supervisor
- Migration
139_bundled_automations_and_supervisor.sqlinstalls a bundled specialized agent and a bundled automation withbundled_key=agent-orchestrator-supervisor. - The bundled automation controls whether the supervisor is active and how often it runs; the default interval is 30 minutes and the default row is shipped paused until an admin enables it.
- The bundled specialized agent stores the supervisor model/default prompt. The records are editable through the normal Automationen and Spezial-Agenten settings sections but cannot be deleted.
- Each supervisor pass releases stale/terminal orchestrator run locks, clears old failure backoffs, reconciles merged tracked PRs back to source GitHub issues, and wakes the normal issue/PR/task-board polling path when recovery work is needed.
- Automation completion is committed only after all configured result targets are delivered. Delivery failures retain the completed, occurrence-idempotent agent run, mark the job attempt failed, and use the normal bounded retry/dead-letter policy; retries therefore repeat delivery without replaying completed agent side effects. A failed required tool call marks the run
failedwithautomation_required_tool_failed. An automation whose delivered final answer is an explicit failure report (first line starts with⚠️/❌/🚨/⛔/🛑and names a failure such asfehlgeschlagen,failed, ornon riuscita) is delivered as usual, but the run flips tofailedwithautomation_reported_failure, the job stores the headline inlast_error, a warningjob.completedevent keeps the report, and the Team Chat message carriesmessage_meta.ok=false; the schedule is deliberately not re-run because the agent already exhausted its own retries. Automations that require an additional internal post can callteam_chat_post_message; omittingroom_iduses the active Team Chat room and otherwise publishes to#general.
Admin runtime controls:
/settings/clapilotaicore/ragincludes RAG Index Health (coverage, queue, failed jobs) plus explicit document vision fallback controls- Reindex actions: failed-only, full reindex, document-scoped, mandant-scoped
- API endpoints:
GET /api/admin/rag/statusPOST /api/admin/rag/statusPOST /api/admin/rag/reindex
Technical details:
No space left on device
Check the workspace volume first: open Einstellungen → Speicher (/admin/storage) or ask the agent for the instance storage status. The page shows free/used space, the level, usage by source, the retention rules, and the last cleanup cycle. A PostgreSQL error such as could not extend file base/…: No space left on device means the database shares the full filesystem and needs free space immediately.
Order of operations:
- Read the storage report. Large retention categories (orchestrator worktrees, repo caches, temp) are reclaimed automatically by the cleanup worker within one poll interval; check
docker compose logs clapilot | rg instance-cleanupforremoved=lines andignored … session/job workspace path(s)notices. - If reclaimable data exists but nothing is removed, run one dry-run cycle to see the plan without deleting:
CLAPILOT_INSTANCE_CLEANUP_DRY_RUN=true node scripts/instance-cleanup-worker.mjsinside the web container. - Protected categories (
mandanten, generated media, work data) are never removed automatically. Deciding what to archive there is a human decision; export or move data instead of deleting it in place. - The Agent Orchestrator will not start new worktrees below
AGENT_ORCHESTRATOR_MIN_FREE_DISK_GIB. A manual run rejected by this guard is recorded as failed and is not retried or queued; start it again once space has been freed.
If the workspace volume is healthy, the pressure usually comes from Docker root storage instead.
Safe cleanup without data loss:
docker builder prune -af
docker image prune -f
docker container prune -f
Avoid docker system prune --volumes unless you explicitly accept volume data loss.
Mailbox credential mismatch
- agent mailbox:
app_settings.agent_email_*+ default host/port - user mailboxes:
user_email_accountsrows (per-user, multiple accounts;companyaccounts useapp_settings.default_imap/smtp_*,customaccounts use their own host/port)
Different answers across channels often mean different resolver path or runtime target.
Legacy compatibility device token mismatch
This recovery path was removed with the packaged OpenClaw CLI. Native session/model control now lives inside clapilot-agent; there is no local device-pairing flow to repair in the Docker image anymore.
No-data-loss backup sequence
mkdir -p ~/clapilot-backups
# workspace
docker run --rm -v clapilot_workspace:/src -v ~/clapilot-backups:/bk alpine \
sh -lc 'tar -czf /bk/workspace-$(date +%F-%H%M).tgz -C /src .'
# native runtime state
docker run --rm -v clapilot_workspace:/src -v ~/clapilot-backups:/bk alpine \
sh -lc 'tar -czf /bk/clapilotaicore-state-$(date +%F-%H%M).tgz -C /src .clapilotaicore'
# postgres logical dump
docker compose exec -T postgres pg_dump -U "${POSTGRES_USER:-clapilot}" "${POSTGRES_DB:-clapilot}" \
> ~/clapilot-backups/db-$(date +%F-%H%M).sql
