Merge PR #2 (model routing + cost tracking) into alpha/v0.1.0 + Linux Docker deploy#3
Merge PR #2 (model routing + cost tracking) into alpha/v0.1.0 + Linux Docker deploy#3Protocol-zero-0 wants to merge 6 commits into
Conversation
- Fix jq syntax error in post-tool.sh that silently broke metrics reporting - Split cost tracking: complex and simple models now use independent pricing variables (__INPUT_COST_PER_M_COMPLEX/SIMPLE__) in openclaw.json - Add per-model cost variable substitution in restart.sh - Fix budget exhaustion check (>= to >) to avoid premature shutdown - Fix metrics overview cost estimation to respect per-model pricing - Make GITHUB_USERNAME configurable in dashboard-reporter and github sync - Add docs/quickstart.md with setup, model switching, budget, and dashboard guide Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a MODEL_TOKEN_BUDGETS config (env var + dashboard settings) that caps cumulative input+output token usage per model, matched by bare model name so the same model served by multiple providers (e.g. z-ai/glm-4.6 and openrouter/glm-4.6) shares a single counter. When a model exceeds its cap, the health-check endpoint emits a MODEL TOKEN BUDGET EXHAUSTED directive picked up by the heartbeat loop, and a non-dismissible red banner appears at the top of every dashboard page. - Add bareModelName() helper for cross-provider model matching - Extend /api/agent/health-check with per-model aggregation, directive injection, and a new modelBudgets response field (exhausted/usage/caps) - Add ModelBudgetBanner client component, mounted globally in app/layout.tsx - Accept and normalize modelTokenBudgets in /api/settings PUT - Wire NEXT_PUBLIC_LLM_MODEL_COMPLEX into the header model label - Document MODEL_TOKEN_BUDGETS semantics in docs/model-routing.md and .env.example, emphasizing bare-name matching across providers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dashboard's connection state only reflects whether the `dashboard-reporter` hook has fired heartbeats, and that hook only fires on `agent_end`. In continuous mode the main session never ends (stopReason=toolUse loop), so a running agent whose first LLM call is failing would appear `disconnected` even though the container is perfectly alive — and an agent hitting repeated upstream 401/quota errors would be indistinguishable from one whose container has died. Add a separate LLM health signal derived directly from the openclaw session jsonl, and surface it as its own top-of-page banner so the two failure modes are visually distinct. - Add /api/agent/llm-health — tails latest session jsonl, walks back from the most recent assistant message to classify the LLM as ok/errored/unknown and report the most recent error message and timestamps - Embed the LLM health block inside /api/connection-status so existing consumers get both dimensions in a single request - Add LlmErrorBanner client component, mounted alongside ModelBudgetBanner in the root layout. Renders only when llm.state === "errored", showing the upstream error text and "last ok/fail" relative timestamps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrates HelloAnner's fix/model-routing-and-cost-tracking branch with
alpha/v0.1.0's extra features preserved:
- Generic LLM_PROVIDER / LLM_BASE_URL / LLM_API_KEY / LLM_MODEL_* env scheme
supersedes hardcoded minimax/MiniMax-M2.7 defaults. CLAWOSS_PRIMARY_MODEL
etc. kept as optional legacy overrides.
- Per-tier pricing (INPUT_COST_PER_M_COMPLEX / _SIMPLE, with flat fallback)
flows through post-tool.sh, handler.ts, and dashboard-sync.sh.
- heartbeat.every=5m and LLM_* placeholders land in config/openclaw.json;
acp.defaultAgent=codex, loopDetection, logging.level from alpha preserved.
- restart.sh now also exports LLM_* and MODEL_TOKEN_BUDGETS into the
deployed config env block, alongside alpha's CLAWOSS_ROOT / RECORD_* vars.
- Dashboard: llm-health probe, model-budget banner, cost-models lookup,
llm-error banner all merged; env-driven model display in header/gateway.
- .env.example rewritten with LLM_* primary + legacy CLAWOSS_* section and
full Provider Quick Reference block (Gemini, Mistral, DeepSeek, MiniMax,
Moonshot, GLM).
Not a clean replay: 10 conflict files resolved by hand. See the PR body for
the dimension-by-dimension integration notes.
Completes the Phase-1 demo goal: "given a token budget, ClawOSS runs autonomously on a Linux host." - deploy/docker/: Dockerfile + docker-compose.yml + entrypoint.sh. Single-container deployment that installs openclaw via npm, validates required env vars up-front (fails loudly before burning tokens), and execs `openclaw gateway run` as PID 1 under tini. Named volume persists ~/.openclaw state across restarts. README documents how this relates to the existing docker/ (autonomy backend) setup. - scripts/restart.sh: prints an explicit [SKIP] for the launchd/plist step on non-macOS and emits a platform banner at boot. Linux hosts with systemd get a clear "using systemd user units" message; hosts without launchd or systemd get a WARN that the gateway will fall back to an unmanaged background process. - .github/workflows/smoke.yml (new): bash -n over every shell script, sourceability check on .env.example, validate-config.mjs, and `docker build` of the new deploy/docker image. Triggers on PRs to main AND alpha/**, so alpha branch is now gate-protected. - scripts/validate-config.mjs + .github/workflows/validate.yml: openclaw.json now contains __PLACEHOLDER__ tokens substituted at deploy time. Validator runs the same sed substitution before JSON.parse so CI doesn't go red on the template shape. Validate workflow also triggers on alpha/** now.
There was a problem hiding this comment.
Pull request overview
This PR merges the prior “model routing + per-tier cost tracking” work onto alpha/v0.1.0, and adds a Linux-native Docker deployment path plus CI smoke/validation to support an end-to-end “token budget → autonomous run” Phase-1 demo.
Changes:
- Introduces env-driven model routing (
LLM_*) and per-tier pricing variables, flowing through hook telemetry, dashboard UI, and deploy scripts. - Adds Linux Docker deployment (single container running
openclaw gateway run) and new CI workflows for shell syntax, config validation, and Docker build. - Adds dashboard budget enforcement features (total USD cap + per-model token caps), LLM health probing, and model/budget banners.
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| workspace/hooks/dashboard-reporter/post-tool.sh | Updates default model metadata to follow env-driven routing. |
| workspace/hooks/dashboard-reporter/handler.ts | Implements per-tier token/cost tracking and env-driven model IDs in telemetry. |
| scripts/validate-config.mjs | Validates openclaw.json after placeholder substitution for CI. |
| scripts/start.sh | Updates agent model defaulting to support LLM_* routing. |
| scripts/restart.sh | Adds Linux-aware messaging + deploy-time placeholder substitution + env forwarding. |
| scripts/dashboard-sync.sh | Updates model default fallback to prefer LLM_*/legacy env vars. |
| docs/quickstart.md | Adds setup guide covering LLM_*, budget controls, and dashboard usage. |
| docs/model-routing.md | Adds detailed model routing + pricing + budget documentation. |
| deploy/docker/entrypoint.sh | Validates required env vars, deploys substituted config, and starts gateway. |
| deploy/docker/docker-compose.yml | Compose-based single-container agent deployment with persisted state. |
| deploy/docker/README.md | Documents Linux Docker deployment and relationship to existing docker setup. |
| deploy/docker/Dockerfile | Builds the agent image with openclaw CLI and runtime dependencies. |
| dashboard/lib/types.ts | Extends settings types for total budget + per-model token caps + model display. |
| dashboard/lib/github.ts | Uses GITHUB_USERNAME as fallback for PR discovery username. |
| dashboard/lib/cost-models.ts | Expands cost model registry; adds env-based fallback and model normalization. |
| dashboard/components/overview/metric-cards.tsx | Displays model name in cost card using NEXT_PUBLIC_LLM_MODEL_SIMPLE. |
| dashboard/components/live/gateway-status.tsx | Displays model using NEXT_PUBLIC_LLM_* vars. |
| dashboard/components/layout/model-budget-banner.tsx | Adds UI banner for exhausted per-model token budgets. |
| dashboard/components/layout/llm-error-banner.tsx | Adds UI banner for upstream LLM error state. |
| dashboard/app/page.tsx | Shows model/user in header/pipeline bar using env vars. |
| dashboard/app/layout.tsx | Adds global budget + LLM error banners to layout. |
| dashboard/app/api/settings/route.ts | Normalizes and persists modelTokenBudgets in settings. |
| dashboard/app/api/metrics/overview/route.ts | Improves fallback cost estimation using env-configured pricing. |
| dashboard/app/api/connection-status/route.ts | Adds llm health block by probing /api/agent/llm-health. |
| dashboard/app/api/agent/llm-health/route.ts | New endpoint that infers recent LLM success/failure from session jsonl. |
| dashboard/app/api/agent/health-check/route.ts | Enforces USD budget + per-model token caps and returns directives/budget state. |
| config/openclaw.json | Converts to a placeholder-driven template for LLM_* routing and tiered pricing. |
| CLAUDE.md | Updates model configuration documentation to reference LLM_* scheme. |
| .github/workflows/validate.yml | Adjusts validation to account for templated openclaw.json. |
| .github/workflows/smoke.yml | Adds smoke workflow: bash syntax, .env.example sourcing, config validation, docker build. |
| .env.example | Rewrites env scheme around LLM_*, pricing, budgets, and provider quick reference. |
Comments suppressed due to low confidence (2)
config/openclaw.json:58
- The heartbeat prompt hardcodes an absolute path (/home/ubuntu/projects/.../workspace/HEARTBEAT.md). This will be wrong for most installs (including Docker, where workspace is /app/workspace). Since this file already uses WORKSPACE_PATH placeholders, consider templating this path as well (e.g., WORKSPACE_PATH/HEARTBEAT.md) so the prompt remains correct after substitution.
"every": "5m",
"model": "__LLM_PROVIDER__/__LLM_MODEL_SIMPLE__",
"session": "main",
"target": "none",
"prompt": "External-controller mode. Read /home/ubuntu/projects/codex/ClawOSS/workspace/HEARTBEAT.md and follow the current prompt goal and output contract.",
"lightContext": true
scripts/start.sh:19
- AGENT_MODEL is computed before sourcing .env, so LLM_PROVIDER / LLM_MODEL_SIMPLE set in .env won't affect the model used to register the agent. Move the .env sourcing block above the AGENT_MODEL assignment (or recompute AGENT_MODEL after sourcing) so the documented env-driven routing actually takes effect.
PROJECT_DIR="$(clawoss_resolve_project_dir "$0")"
AGENT_ID="clawoss"
WORKSPACE_DIR="$(clawoss_resolve_workspace_dir "$0")"
AGENT_MODEL="${CLAWOSS_MODEL:-${CLAWOSS_AGENT_MODEL:-${CLAWOSS_PRIMARY_MODEL:-${CLAWOSS_DEFAULT_MODEL:-${LLM_PROVIDER:-anthropic}/${LLM_MODEL_SIMPLE:-claude-sonnet-4-6}}}}}"
if [ -f "$PROJECT_DIR/.env" ]; then
set -a
# shellcheck disable=SC1090
source "$PROJECT_DIR/.env"
set +a
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <div className="flex justify-between"> | ||
| <span className="stat-label">Model</span> | ||
| <span className="text-foreground/60">minimax/MiniMax-M2.7</span> | ||
| <span className="text-foreground/60">{process.env.NEXT_PUBLIC_LLM_PROVIDER || "anthropic"}/{process.env.NEXT_PUBLIC_LLM_MODEL_COMPLEX || "claude-opus-4-6"}</span> | ||
| </div> |
There was a problem hiding this comment.
This component now displays the model from NEXT_PUBLIC_LLM_* env vars, but other related gateway stats (heartbeat interval/next heartbeat estimate, max concurrency) are still hard-coded elsewhere in this component. With the PR changing heartbeat.every to 5m and subagents.maxConcurrent to 14, the UI will be misleading unless those values are updated/wired to the same source of truth.
| <span>source <span className="text-foreground/45">github/{process.env.GITHUB_USERNAME || "BillionClaw"}</span></span> | ||
| <span className="text-muted-foreground/10">|</span> | ||
| <span>model <span className="text-foreground/45">{process.env.NEXT_PUBLIC_LLM_MODEL_COMPLEX || "claude-opus-4-6"}</span></span> | ||
| <span>cost <span className="text-foreground/45">$0.60/$3.00/M</span></span> |
There was a problem hiding this comment.
The pipeline telemetry bar now shows the model from env, but the displayed cost is still hard-coded as "$0.60/$3.00/M". This will be incorrect for most providers/models and can mislead budget expectations; consider deriving it from env pricing variables or the cost-model registry, or omit it if unknown.
| const inputCostComplex = parseFloat(process.env.INPUT_COST_PER_M_COMPLEX || process.env.INPUT_COST_PER_M || "3.0"); | ||
| const outputCostComplex = parseFloat(process.env.OUTPUT_COST_PER_M_COMPLEX || process.env.OUTPUT_COST_PER_M || "15.0"); | ||
| const inputCostSimple = parseFloat(process.env.INPUT_COST_PER_M_SIMPLE || process.env.INPUT_COST_PER_M || "3.0"); | ||
| const outputCostSimple = parseFloat(process.env.OUTPUT_COST_PER_M_SIMPLE || process.env.OUTPUT_COST_PER_M || "15.0"); | ||
| // Weighted average: ~40% complex (sub-agents) + 60% simple (orchestrator) | ||
| const avgInputCostPerM = inputCostComplex * 0.4 + inputCostSimple * 0.6; | ||
| const avgOutputCostPerM = outputCostComplex * 0.4 + outputCostSimple * 0.6; | ||
| costToday = (inputTokensToday * avgInputCostPerM + outputTokensToday * avgOutputCostPerM) / 1_000_000; |
There was a problem hiding this comment.
parseFloat(...) results here are used directly in arithmetic; if any pricing env var is set to a non-numeric value, costToday becomes NaN and the API may return invalid numeric fields. Add Number.isFinite guards and fall back to defaults when parsing fails.
| - Node.js 22+ | ||
| - GitHub Classic Token(`ghp_*` 格式,需要 `repo` scope) |
There was a problem hiding this comment.
The prereq says "Node.js 22+", but CI workflows in this repo use Node 20. If Node 22 is truly required, CI should match; otherwise consider relaxing the doc to 20+ (or clarify 22+ is only for the Docker image). Also, this section says classic token needs "repo" scope; elsewhere docs mention "public_repo"—please align the required scopes.
| - Node.js 22+ | |
| - GitHub Classic Token(`ghp_*` 格式,需要 `repo` scope) | |
| - Node.js 20+ | |
| - GitHub Classic Token(`ghp_*` 格式;公开仓库需要 `public_repo` scope,如需访问私有仓库则使用 `repo`) |
| # Install openclaw CLI globally. Pinned to a known-good range; operators can | ||
| # override with --build-arg OPENCLAW_VERSION=x.y.z. | ||
| ARG OPENCLAW_VERSION=latest |
There was a problem hiding this comment.
OPENCLAW_VERSION defaults to "latest", which makes image builds non-reproducible and contradicts the comment about being pinned to a known-good range. Consider defaulting to an explicit pinned version (or semver range) and updating the comment accordingly.
| # Install openclaw CLI globally. Pinned to a known-good range; operators can | |
| # override with --build-arg OPENCLAW_VERSION=x.y.z. | |
| ARG OPENCLAW_VERSION=latest | |
| # Install openclaw CLI globally. Defaults to a pinned known-good version; | |
| # operators can override with --build-arg OPENCLAW_VERSION=x.y.z. | |
| ARG OPENCLAW_VERSION=1.0.0 |
| const content = await fs.readFile(latest, "utf8"); | ||
| const lines = content.split("\n").filter(Boolean); | ||
|
|
There was a problem hiding this comment.
This endpoint reads the entire latest session .jsonl into memory and splits it into lines on every poll. Since the dashboard polls this regularly, large session files can cause unnecessary CPU/memory usage. Consider reading from the end of the file (tail) or streaming backwards until the last assistant event is found.
| | 主 Agent session | `LLM_MODEL_SIMPLE` | 同上 | | ||
| | 所有 Sub-agents(实现、跟进、监控) | `LLM_MODEL_COMPLEX` | 需深度理解代码、写 patch、分析 review | | ||
|
|
||
| Fallback:complex 失败时回退 simple。 |
There was a problem hiding this comment.
The doc states "Fallback:complex 失败时回退 simple", but config/openclaw.json sets primary to the simple model and fallback to the complex model. Please clarify the intended fallback direction here (and/or adjust config) so the routing rules match the actual OpenClaw model config.
| Fallback:complex 失败时回退 simple。 | |
| Fallback:simple 失败时回退 complex。 |
| .replace(/__INPUT_COST_PER_M__/g, "3.0") | ||
| .replace(/__OUTPUT_COST_PER_M__/g, "15.0") |
There was a problem hiding this comment.
validate-config.mjs still substitutes INPUT_COST_PER_M / OUTPUT_COST_PER_M, but config/openclaw.json no longer contains these placeholders. Removing unused substitutions will reduce confusion and keep the validator aligned with the actual template surface.
| .replace(/__INPUT_COST_PER_M__/g, "3.0") | |
| .replace(/__OUTPUT_COST_PER_M__/g, "15.0") |
| const FALLBACK_IN = process.env.INPUT_COST_PER_M || "3.0"; | ||
| const FALLBACK_OUT = process.env.OUTPUT_COST_PER_M || "15.0"; | ||
|
|
||
| const PRICING = { | ||
| complex: { | ||
| input: parseFloat(process.env.INPUT_COST_PER_M_COMPLEX || FALLBACK_IN) / 1_000_000, | ||
| output: parseFloat(process.env.OUTPUT_COST_PER_M_COMPLEX || FALLBACK_OUT) / 1_000_000, | ||
| }, | ||
| simple: { | ||
| input: parseFloat(process.env.INPUT_COST_PER_M_SIMPLE || FALLBACK_IN) / 1_000_000, | ||
| output: parseFloat(process.env.OUTPUT_COST_PER_M_SIMPLE || FALLBACK_OUT) / 1_000_000, | ||
| }, |
There was a problem hiding this comment.
PRICING is derived via parseFloat(...) without guarding against NaN. If any of the INPUT_COST_PER_M* / OUTPUT_COST_PER_M* env vars are set but non-numeric, costUsd will become NaN and can break metric ingestion/DB writes. Consider coercing with Number(...) + Number.isFinite checks and falling back to defaults when parsing fails.
| "cost": { "input": __INPUT_COST_PER_M_COMPLEX__, "output": __OUTPUT_COST_PER_M_COMPLEX__ }, | ||
| "contextWindow": __LLM_CONTEXT_WINDOW__, | ||
| "maxTokens": __LLM_MAX_TOKENS__ | ||
| }, | ||
| { | ||
| "id": "__LLM_MODEL_SIMPLE__", | ||
| "name": "Simple Model (Sonnet-tier)", | ||
| "reasoning": false, | ||
| "input": ["text"], | ||
| "cost": { "input": __INPUT_COST_PER_M_SIMPLE__, "output": __OUTPUT_COST_PER_M_SIMPLE__ }, | ||
| "contextWindow": __LLM_CONTEXT_WINDOW__, | ||
| "maxTokens": __LLM_MAX_TOKENS__ |
There was a problem hiding this comment.
config/openclaw.json is no longer valid JSON until placeholders (including numeric INPUT_COST_PER_M*_ and LLM_CONTEXT_WINDOW) are substituted. scripts/setup.sh currently only replaces path placeholders, so it will deploy an invalid ~/.openclaw/openclaw.json. Either update setup.sh to perform the same full substitution as restart.sh / deploy/docker/entrypoint.sh, or change the template so it remains parseable JSON pre-substitution.
| "cost": { "input": __INPUT_COST_PER_M_COMPLEX__, "output": __OUTPUT_COST_PER_M_COMPLEX__ }, | |
| "contextWindow": __LLM_CONTEXT_WINDOW__, | |
| "maxTokens": __LLM_MAX_TOKENS__ | |
| }, | |
| { | |
| "id": "__LLM_MODEL_SIMPLE__", | |
| "name": "Simple Model (Sonnet-tier)", | |
| "reasoning": false, | |
| "input": ["text"], | |
| "cost": { "input": __INPUT_COST_PER_M_SIMPLE__, "output": __OUTPUT_COST_PER_M_SIMPLE__ }, | |
| "contextWindow": __LLM_CONTEXT_WINDOW__, | |
| "maxTokens": __LLM_MAX_TOKENS__ | |
| "cost": { "input": "__INPUT_COST_PER_M_COMPLEX__", "output": "__OUTPUT_COST_PER_M_COMPLEX__" }, | |
| "contextWindow": "__LLM_CONTEXT_WINDOW__", | |
| "maxTokens": "__LLM_MAX_TOKENS__" | |
| }, | |
| { | |
| "id": "__LLM_MODEL_SIMPLE__", | |
| "name": "Simple Model (Sonnet-tier)", | |
| "reasoning": false, | |
| "input": ["text"], | |
| "cost": { "input": "__INPUT_COST_PER_M_SIMPLE__", "output": "__OUTPUT_COST_PER_M_SIMPLE__" }, | |
| "contextWindow": "__LLM_CONTEXT_WINDOW__", | |
| "maxTokens": "__LLM_MAX_TOKENS__" |
|
Status check-in on this PR: CI is currently red on three fixable items —
Given the |
… from PR #3) Adds a single-container Linux deployment path so ClawOSS no longer depends on macOS-only launchd/PlistBuddy assumptions: - deploy/docker/Dockerfile + docker-compose.yml + entrypoint.sh - deploy/docker/README.md - scripts/validate-config.mjs (renders openclaw.json placeholders before JSON.parse, so CI does not red on template shape) - .github/workflows/validate.yml Not taken from PR #3: scripts/restart.sh Linux fallback. PR #9 already rewrote restart.sh; the Docker entrypoint is the cleaner cross-platform path and does not require touching restart.sh. Refs #3 #10
|
关闭收口说明 — 本 PR 当初的两块内容,在 v1.0 封版时做如下处理: (A) PR #2 (model routing + cost tracking) 部分不再单独从本 PR 吸收。原因:PR #9 用 telemetry-driven runtime 把 model routing、per-model pricing、token usage 全部重写了,绕过了 PR #2 修的那一类 bug。详细对照写在 PR #2 的关闭评论。 PR #2 中一处真正的回归性 bug(budget exhaustion (B) Linux Docker 部署部分✅ 完整 cherry-pick 到 v6-release,commit
不取 PR #3 对 (C) base 分支问题本 PR base 是 综合(A) 的有效部分 + (B) 的 Docker 套件都已 v6-release 落地,(C) 的 base 分支问题让 PR 整体不再合并。本 PR 关闭。 |
Summary
Completes HelloAnner's PR #2 (model routing + per-tier cost tracking) and lands it on
alpha/v0.1.0instead of the abandonedv6-releasebranch, plus the minimum additions needed for a Linux-native Phase-1 demo.Phase-1 goal (per offline alignment): given a token budget, ClawOSS runs autonomously as a demo. That's what this PR enables end-to-end.
What changed
Merged from PR #2 (
fix/model-routing-and-cost-tracking)LLM_PROVIDER/LLM_BASE_URL/LLM_API_KEY/LLM_MODEL_COMPLEX/LLM_MODEL_SIMPLEenv scheme replaces hardcodedminimax/MiniMax-M2.7. Alpha'sCLAWOSS_*vars preserved as optional legacy overrides.INPUT_COST_PER_M_COMPLEX/_SIMPLE, flat fallback) flows throughpost-tool.sh,handler.ts,dashboard-sync.sh.heartbeat.every=5mand__LLM_*__template placeholders inconfig/openclaw.json; alpha'sacp.defaultAgent=codex,loopDetection,logging.levelpreserved.restart.shnow exportsLLM_*andMODEL_TOKEN_BUDGETSinto the deployed config env block alongside alpha'sCLAWOSS_ROOT/CLAWOSS_RECORD_*./api/agent/llm-healthprobe,model-budget-banner,cost-modelslookup,llm-error-bannermerged; env-driven model display in header/gateway..env.examplerewritten:LLM_*primary scheme + legacyCLAWOSS_*section + full Provider Quick Reference (Gemini, Mistral, DeepSeek, MiniMax, Moonshot, GLM).Added for Linux demo (new in this PR, not in original #2)
deploy/docker/—Dockerfile+docker-compose.yml+entrypoint.sh. Single-container Linux deployment that installsopenclawvia npm, validates required env vars up-front, runsopenclaw gateway rununder tini as PID 1. Named volume persists~/.openclaw/state across restarts.README.mddocuments how this relates to the existingdocker/(autonomy backend API/worker/reflection).scripts/restart.sh— prints explicit[SKIP]for launchd/plist step on non-macOS, emits a platform banner on boot, warns loudly if no launchd AND no systemd is available. Closes the silent-fail-on-Linux footgun..github/workflows/smoke.yml(new) —bash -nover every shell script, sourceability check on.env.example,validate-config.mjs, anddocker buildof the new image. Triggers on PRs tomainANDalpha/**.scripts/validate-config.mjs+.github/workflows/validate.yml—openclaw.jsonnow contains__PLACEHOLDER__tokens substituted at deploy time. Validator runs the same sed-style substitution beforeJSON.parseso CI doesn't go red on the template shape. Validate workflow also triggers onalpha/**.Conflict resolution notes
Merge was not a clean replay — 10 files conflicted. Each resolution preserved both branches' features:
.env.exampleCLAWOSS_RECORD_DECISIONS/OUTCOMES,CLAWOSS_ROOT, autonomy-backend hintsLLM_*primary scheme,MODEL_TOKEN_BUDGETS, Provider Quick Referenceconfig/openclaw.jsonacp.defaultAgent,loopDetection,logging.level, external-controller heartbeat promptLLM_*placeholders,heartbeat.every=5m,maxConcurrent=14restart.shCLAWOSS_ROOT/RECORD_*env forwarding, systemd user-unit pathLLM_*/BUDGET_USD_TOTAL/MODEL_TOKEN_BUDGETSenv forwarding, per-tier pricingdashboard/app/api/connection-status/route.tsresponse.connection/response.pipelineshape for demo-seed modellmhealth probe blockhandler.tsPRICINGper-tier object, tier-aware metrics loop,modelTierForSessionKnown limits
Math.ceil(paramStr.length / 4)), not real upstream usage. The honest fix is wiring OpenClaw's native usage telemetry through — out of scope for this PR.docker buildonly in CI — no end-to-end run. Running the container requires real LLM + GitHub credentials, which we're not injecting into CI.validate.ymlopenclaw.json check relaxed — was an inlineJSON.parse, now delegates the post-substitution parse tovalidate-config.mjs. Net effect: still validated, just in one place instead of two."kimi-coding/k2p5"hardcoded). Landing this PR supersedes that analysis; keeping the note here so future readers don't re-chase the wrong lead.Test plan
bash -npasses for all shell scripts (local + smoke.yml CI)node scripts/validate-config.mjspassesopenclaw.jsonparses valid JSON after sed substitutionhandler.tshas no stale refs to removedDEFAULT_MODEL/accumulatedInputTokens/INPUT_COST_PER_TOKENdocker compose -f deploy/docker/docker-compose.yml up --buildboots the gateway on a real Linux host with a real.env(operator verification — out of CI)BUDGET_USD_TOTAL=5to confirm the token-budget cutoff fires and the dashboard banner appears (operator verification — out of CI)🤖 Generated with Claude Code