From 29ee5126de7cc2308b4b0eca9bf2247f864a1d4b Mon Sep 17 00:00:00 2001
From: "openclaw-barnacle[bot]"
<257215752+openclaw-barnacle[bot]@users.noreply.github.com>
Date: Tue, 11 Aug 2026 10:51:38 -0700
Subject: [PATCH] chore: update skills (#3406)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.agents/skills/axiomhq-skills.provenance.json | 2 +-
.../building-dashboards/scripts/chart-add | 19 ++-
.../scripts/metrics/axiom-api | 4 +
.../scripts/metrics/metrics-info | 137 ++++++++++++++----
.../scripts/metrics/metrics-query | 75 +++++++++-
.../scripts/metrics/metrics-spec | 35 ++---
.../tests/test-script-output.sh | 33 +++++
.../openclaw-carapace/references/tokens.md | 11 ++
.../references/tokens.md | 11 ++
.agents/skills/query-metrics/README.md | 2 +-
.agents/skills/query-metrics/SKILL.md | 65 +++++++--
.../skills/query-metrics/scripts/axiom-api | 4 +
.../skills/query-metrics/scripts/metrics-info | 137 ++++++++++++++----
.../query-metrics/scripts/metrics-query | 75 +++++++++-
.../skills/query-metrics/scripts/metrics-spec | 35 ++---
.agents/skills/query-metrics/scripts/setup | 2 +-
skills-lock.json | 8 +-
17 files changed, 523 insertions(+), 132 deletions(-)
diff --git a/.agents/skills/axiomhq-skills.provenance.json b/.agents/skills/axiomhq-skills.provenance.json
index 89f3fcf6..b19811e2 100644
--- a/.agents/skills/axiomhq-skills.provenance.json
+++ b/.agents/skills/axiomhq-skills.provenance.json
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/axiomhq/skills",
- "resolvedCommit": "0e98ebaeec76a70c8fda9a7737605800c2f1245d",
+ "resolvedCommit": "7f29f9a97ffd71bf2ad375e035ba6f3ba30dcc8b",
"license": "MIT",
"skills": {
"axiom-alerting": {
diff --git a/.agents/skills/building-dashboards/scripts/chart-add b/.agents/skills/building-dashboards/scripts/chart-add
index 79ad908d..5c7cb0c2 100755
--- a/.agents/skills/building-dashboards/scripts/chart-add
+++ b/.agents/skills/building-dashboards/scripts/chart-add
@@ -195,13 +195,28 @@ unit_fields_other() {
fi
}
+# Newline before each pipeline `|` so stored queries read one stage per line.
+# The split only tracks plain '...'/"..." literals, so it bails out and stores
+# the query untouched when it holds a construct whose string boundaries it
+# cannot follow: a backslash escape, an @-verbatim literal (where `\` is not an
+# escape), or a // comment. Formatting is cosmetic, silently rewriting a query
+# is not, so anything ambiguous stays on one line.
+format_pipeline() {
+ if [[ "$1" == *\\* || "$1" == *"@'"* || "$1" == *'@"'* || "$1" == *"//"* ]]; then
+ printf '%s' "$1"
+ return
+ fi
+ jq -rn --arg apl "$1" \
+ '$apl | gsub("(?\"[^\"]*\"|'\''[^'\'']*'\'')|(?
\\| )"; if .s then .s else "\n| " end)'
+}
+
# Build the query object. Both APL and MPL land in `query.apl` (shared API
# field); MPL also gets `query.metricsDataset`.
build_query() {
if [[ -n "$MPL" ]]; then
- jq -n --arg apl "$MPL" --arg ds "$DATASET" '{apl: $apl, metricsDataset: $ds}'
+ jq -n --arg apl "$(format_pipeline "$MPL")" --arg ds "$DATASET" '{apl: $apl, metricsDataset: $ds}'
else
- jq -n --arg apl "$APL" '{apl: $apl}'
+ jq -n --arg apl "$(format_pipeline "$APL")" '{apl: $apl}'
fi
}
diff --git a/.agents/skills/building-dashboards/scripts/metrics/axiom-api b/.agents/skills/building-dashboards/scripts/metrics/axiom-api
index 2c7bccef..dc5d83af 100755
--- a/.agents/skills/building-dashboards/scripts/metrics/axiom-api
+++ b/.agents/skills/building-dashboards/scripts/metrics/axiom-api
@@ -8,6 +8,8 @@
#
# Reads credentials from ~/.axiom.toml (shared with axiom-sre)
# Set AXIOM_URL_OVERRIDE to route requests to a specific edge deployment endpoint.
+# Set AXIOM_CONNECT_TIMEOUT / AXIOM_MAX_TIME (seconds) to override the default
+# connection (10s) and total request (120s) timeouts.
#
# Examples:
# axiom-api prod GET /v1/datasets
@@ -54,6 +56,8 @@ fi
CURL_ARGS=(
-s
+ --connect-timeout "${AXIOM_CONNECT_TIMEOUT:-10}"
+ --max-time "${AXIOM_MAX_TIME:-120}"
-w '\n%{http_code}'
-X "$METHOD"
-H "Authorization: Bearer $TOKEN"
diff --git a/.agents/skills/building-dashboards/scripts/metrics/metrics-info b/.agents/skills/building-dashboards/scripts/metrics/metrics-info
index b99e7ff8..6d3f156b 100755
--- a/.agents/skills/building-dashboards/scripts/metrics/metrics-info
+++ b/.agents/skills/building-dashboards/scripts/metrics/metrics-info
@@ -47,7 +47,12 @@
# specific entity name (service, host, device) to find which metrics carry it.
# To list metric names, use the `metrics` subcommand instead.
#
-# --start and --end default to the last 24 hours if omitted.
+# --start and --end accept RFC3339 (offsets allowed, e.g. 2025-06-01T00:00:00+02:00)
+# or relative now / now- with in s/m/h/d/w, resolved to RFC3339 UTC
+# client-side because the info endpoints only parse RFC3339. This is narrower than
+# metrics-query, which forwards times to the server unparsed and also accepts forms
+# like now-1y; here anything outside now / now-[smhdw] must already be RFC3339.
+# Defaults: last 24 hours.
# For sparse metrics (sensors, batch jobs), try --start with a wider range (e.g. 7 days).
#
# Examples:
@@ -67,6 +72,53 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# Percent-encode one URL component (path segment or query value). Dataset,
+# metric, and tag names are user/OTel-controlled and may contain characters
+# that are reserved in URLs (/ % + space); times may carry a `+02:00` offset
+# whose `+` would otherwise decode as a space server-side.
+urlencode() {
+ jq -rn --arg v "$1" '$v|@uri'
+}
+
+# Normalize a time argument to RFC3339 UTC. RFC3339 input passes through
+# verbatim; the relative forms `now` and `now-` (unit in s/m/h/d/w)
+# are resolved client-side because the info endpoints only parse RFC3339.
+# Note: metrics-query forwards times to the server unparsed, so it accepts a
+# broader set (e.g. now-1y); those forms are NOT handled here and, if passed,
+# fall through to the RFC3339-only endpoint and fail.
+normalize_time() {
+ local t="$1"
+ if [[ "$t" == "now" ]]; then
+ date -u '+%Y-%m-%dT%H:%M:%SZ'
+ elif [[ "$t" =~ ^now-([0-9]+)([smhdw])$ ]]; then
+ local n="${BASH_REMATCH[1]}" u="${BASH_REMATCH[2]}"
+ if date --version &>/dev/null; then
+ local word
+ case "$u" in
+ s) word="seconds" ;;
+ m) word="minutes" ;;
+ h) word="hours" ;;
+ d) word="days" ;;
+ w) word="weeks" ;;
+ esac
+ date -u -d "$n $word ago" '+%Y-%m-%dT%H:%M:%SZ'
+ else
+ # BSD date: -v units are case-sensitive (M = minute, m = month).
+ local unit
+ case "$u" in
+ s) unit="S" ;;
+ m) unit="M" ;;
+ h) unit="H" ;;
+ d) unit="d" ;;
+ w) unit="w" ;;
+ esac
+ date -u -v "-${n}${unit}" '+%Y-%m-%dT%H:%M:%SZ'
+ fi
+ else
+ printf '%s\n' "$t"
+ fi
+}
+
show_usage() {
echo "Usage:" >&2
echo " metrics-info metrics [--by-type] [--type T]..." >&2
@@ -80,8 +132,8 @@ show_usage() {
echo " metrics-info find-metrics (searches tag values, not metric names)" >&2
echo "" >&2
echo "Options:" >&2
- echo " --start T Start time (RFC3339). Default: 24h ago" >&2
- echo " --end T End time (RFC3339). Default: now" >&2
+ echo " --start T Start time (RFC3339 or relative, e.g. now-7d). Default: 24h ago" >&2
+ echo " --end T End time (RFC3339 or relative, e.g. now). Default: now" >&2
echo " --by-type (metrics listing) Group entries by metric type" >&2
echo " --type T (metrics listing) Filter to type T. Repeatable." >&2
echo " --no-values (describe) Return tag names only" >&2
@@ -118,20 +170,12 @@ while [[ $# -gt 0 ]]; do
esac
done
-# Default time range: last 24 hours
-if [[ -z "$START" ]]; then
- if date --version &>/dev/null 2>&1; then
- START=$(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ')
- else
- START=$(date -u -v-24H '+%Y-%m-%dT%H:%M:%SZ')
- fi
-fi
-if [[ -z "$END" ]]; then
- END=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
-fi
+# Default time range: last 24 hours. Relative forms are resolved to RFC3339 UTC.
+START=$(normalize_time "${START:-now-24h}")
+END=$(normalize_time "${END:-now}")
-TIME_PARAMS="start=${START}&end=${END}"
-BASE="/v1/query/metrics/info/datasets/${DATASET}"
+TIME_PARAMS="start=$(urlencode "$START")&end=$(urlencode "$END")"
+BASE="/v1/query/metrics/info/datasets/$(urlencode "$DATASET")"
# Resolve the regional edge URL for this dataset
RESOLVED_URL=$("$SCRIPT_DIR/resolve-url" "$DEPLOYMENT" "$DATASET" 2>/dev/null || true)
@@ -185,34 +229,62 @@ case "${POSITIONAL[0]}" in
# the typical 1+1+N round trips an agent would make to characterise
# an unfamiliar metric.
METRIC="${POSITIONAL[1]}"
+ METRIC_ENC=$(urlencode "$METRIC")
RAW=$(fetch_metrics_listing)
META=$(printf '%s' "$RAW" | jq -e --arg m "$METRIC" '.[$m] // error("metric not found in listing for the given time range: " + $m)')
- TAGS_JSON=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags?${TIME_PARAMS}")
+ TAGS_JSON=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC_ENC}/tags?${TIME_PARAMS}")
if [[ "$NO_VALUES" -eq 1 ]]; then
# tags as flat array of names
jq -n --argjson m "$META" --argjson tags "$TAGS_JSON" '$m + {tags: $tags}'
else
- # tags as object: { tag_name: [values…] }
- VALUES_OBJ='{}'
+ # tags as object: { tag_name: [values…] }. Per-tag value fetches
+ # are independent, so run them concurrently; tag counts are small
+ # (rarely more than a few dozen), so no concurrency cap is needed.
+ TAG_NAMES=()
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
- VALUES=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags/${tag}/values?${TIME_PARAMS}")
- if [[ "$VALUES_LIMIT" -gt 0 ]]; then
- VALUES=$(printf '%s' "$VALUES" | jq --argjson n "$VALUES_LIMIT" '.[:$n]')
- fi
- VALUES_OBJ=$(jq -n --argjson o "$VALUES_OBJ" --arg t "$tag" --argjson v "$VALUES" '$o + {($t): $v}')
+ TAG_NAMES+=("$tag")
done < <(printf '%s' "$TAGS_JSON" | jq -r '.[]?')
+ VALUES_OBJ='{}'
+ if [[ ${#TAG_NAMES[@]} -gt 0 ]]; then
+ TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/metrics-info.XXXXXX")
+ trap 'rm -rf "$TMP_DIR"' EXIT
+ PIDS=()
+ for i in "${!TAG_NAMES[@]}"; do
+ "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET \
+ "${BASE}/metrics/${METRIC_ENC}/tags/$(urlencode "${TAG_NAMES[$i]}")/values?${TIME_PARAMS}" \
+ > "$TMP_DIR/$i.json" &
+ PIDS+=($!)
+ done
+ FETCH_FAILED=0
+ for i in "${!PIDS[@]}"; do
+ if ! wait "${PIDS[$i]}"; then
+ echo "Error: failed to fetch values for tag '${TAG_NAMES[$i]}'" >&2
+ FETCH_FAILED=1
+ fi
+ done
+ if [[ "$FETCH_FAILED" -eq 1 ]]; then
+ exit 1
+ fi
+ for i in "${!TAG_NAMES[@]}"; do
+ VALUES=$(cat "$TMP_DIR/$i.json")
+ if [[ "$VALUES_LIMIT" -gt 0 ]]; then
+ VALUES=$(printf '%s' "$VALUES" | jq --argjson n "$VALUES_LIMIT" '.[:$n]')
+ fi
+ VALUES_OBJ=$(jq -n --argjson o "$VALUES_OBJ" --arg t "${TAG_NAMES[$i]}" --argjson v "$VALUES" '$o + {($t): $v}')
+ done
+ fi
jq -n --argjson m "$META" --argjson tags "$VALUES_OBJ" '$m + {tags: $tags}'
fi
elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "tags" ]]; then
# List tags for a metric
METRIC="${POSITIONAL[1]}"
- "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags?${TIME_PARAMS}"
+ "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/$(urlencode "$METRIC")/tags?${TIME_PARAMS}"
elif [[ ${#POSITIONAL[@]} -eq 5 && "${POSITIONAL[2]}" == "tags" && "${POSITIONAL[4]}" == "values" ]]; then
# List tag values for a metric+tag
METRIC="${POSITIONAL[1]}"
TAG="${POSITIONAL[3]}"
- "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags/${TAG}/values?${TIME_PARAMS}"
+ "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/$(urlencode "$METRIC")/tags/$(urlencode "$TAG")/values?${TIME_PARAMS}"
elif [[ ${#POSITIONAL[@]} -eq 5 && "${POSITIONAL[2]}" == "tags" && "${POSITIONAL[4]}" == "type" ]]; then
# Probe the typing of a metric+tag by running `metrics-query` with
# `filter is ` for each candidate type. The type(s) that
@@ -226,8 +298,15 @@ case "${POSITIONAL[0]}" in
# ``:`` | filter `` is | align to 5m using sum
# If is matches no rows, the response has empty `series`.
PROBE_QUERY='`'"$DATASET"'`:`'"$METRIC"'` | filter `'"$TAG"'` is '"$t"' | align to 5m using sum'
- RESPONSE=$("$SCRIPT_DIR/metrics-query" "$DEPLOYMENT" "$PROBE_QUERY" "$START" "$END" 2>/dev/null || echo '{}')
- COUNT=$(printf '%s' "$RESPONSE" | jq -r '(.series // []) | length' 2>/dev/null || echo 0)
+ # Propagate probe failures instead of swallowing them: a failed
+ # query (bad dataset, auth, network) must not be reported as the
+ # tag being "absent" — that would be a confident wrong answer.
+ if ! RESPONSE=$("$SCRIPT_DIR/metrics-query" "$DEPLOYMENT" "$PROBE_QUERY" "$START" "$END" 2>&1); then
+ echo "Error: type probe query failed (tag '$TAG' is $t):" >&2
+ printf '%s\n' "$RESPONSE" >&2
+ exit 1
+ fi
+ COUNT=$(printf '%s' "$RESPONSE" | jq -r '(.series // []) | length')
if [[ "$COUNT" -gt 0 ]]; then
PRESENT_JSON=$(printf '%s' "$PRESENT_JSON" | jq --arg t "$t" '. + [$t]')
fi
@@ -253,7 +332,7 @@ case "${POSITIONAL[0]}" in
elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "values" ]]; then
# List values for a tag
TAG="${POSITIONAL[1]}"
- "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/${TAG}/values?${TIME_PARAMS}"
+ "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/$(urlencode "$TAG")/values?${TIME_PARAMS}"
else
show_usage
fi
diff --git a/.agents/skills/building-dashboards/scripts/metrics/metrics-query b/.agents/skills/building-dashboards/scripts/metrics/metrics-query
index 9dbbbb76..b094bef7 100755
--- a/.agents/skills/building-dashboards/scripts/metrics/metrics-query
+++ b/.agents/skills/building-dashboards/scripts/metrics/metrics-query
@@ -1,10 +1,23 @@
#!/usr/bin/env bash
# metrics-query: Execute a metrics query against Axiom MetricsDB
#
-# Usage: metrics-query [-p name=value]...
+# Usage: metrics-query [-p name=value]... [-w pixels] [--pixel-per-point n] \
+#
#
# Times: RFC3339 (e.g. 2025-01-01T00:00:00Z) or relative (e.g. now-1h, now-1d).
#
+# Adaptive resolution ($__interval):
+# Reference $__interval anywhere a Duration is expected (e.g.
+# `align to $__interval using avg`, `bucket to $__interval ...`) and the
+# server resolves it to a "nice" step computed from the query time range and
+# the target chart width. No `param $__interval` declaration is needed -- the
+# metrics service registers it automatically. Tune the density with:
+# -w / --chart-width target chart width; the server aims for
+# ~chart-width/pixel-per-point buckets
+# (default ~500 buckets when -w is omitted).
+# --pixel-per-point pixels per data point (server default 10).
+# Both are forwarded under the request body's queryOptions object.
+#
# Parameter values (-p / --param name=value, repeatable):
# For each MPL parameter declared in the query (e.g. `param $svc: string;`),
# pass the variable name without the leading `$` and an MPL literal as the
@@ -32,6 +45,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PARAMS=()
POSITIONAL=()
+CHART_WIDTH=""
+PIXEL_PER_POINT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-p|--param)
@@ -46,6 +61,30 @@ while [[ $# -gt 0 ]]; do
PARAMS+=("${1#--param=}")
shift
;;
+ -w|--chart-width)
+ if [[ $# -lt 2 ]]; then
+ echo "Error: $1 requires a pixel-width argument" >&2
+ exit 1
+ fi
+ CHART_WIDTH="$2"
+ shift 2
+ ;;
+ --chart-width=*)
+ CHART_WIDTH="${1#--chart-width=}"
+ shift
+ ;;
+ --pixel-per-point)
+ if [[ $# -lt 2 ]]; then
+ echo "Error: $1 requires an integer argument" >&2
+ exit 1
+ fi
+ PIXEL_PER_POINT="$2"
+ shift 2
+ ;;
+ --pixel-per-point=*)
+ PIXEL_PER_POINT="${1#--pixel-per-point=}"
+ shift
+ ;;
--)
shift
while [[ $# -gt 0 ]]; do POSITIONAL+=("$1"); shift; done
@@ -63,13 +102,17 @@ START_TIME="${POSITIONAL[2]:-}"
END_TIME="${POSITIONAL[3]:-}"
if [[ -z "$DEPLOYMENT" || -z "$MPL" || -z "$START_TIME" || -z "$END_TIME" ]]; then
- echo "Usage: metrics-query [-p name=value]... " >&2
+ echo "Usage: metrics-query [-p name=value]... [-w pixels] [--pixel-per-point n] " >&2
echo "" >&2
echo "Times: RFC3339 (e.g. 2025-01-01T00:00:00Z) or relative (e.g. now-1h, now-1d)." >&2
echo "" >&2
echo "-p / --param name=value (repeatable): supply an MPL parameter value." >&2
echo " name - variable name without the leading \$ (e.g. 'svc' for \$svc)." >&2
echo " value - MPL literal, forwarded verbatim under params.param__." >&2
+ echo "" >&2
+ echo "-w / --chart-width target chart width; lets the server resolve" >&2
+ echo " \$__interval to a nice step (queryOptions)." >&2
+ echo "--pixel-per-point pixels per data point (server default 10)." >&2
exit 1
fi
@@ -97,6 +140,17 @@ if [[ ${#PARAMS[@]} -gt 0 ]]; then
done
fi
+# Validate the optional chart-sizing options. They must be positive integers;
+# they are forwarded under queryOptions so the server can resolve $__interval.
+if [[ -n "$CHART_WIDTH" && ! "$CHART_WIDTH" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Error: --chart-width must be a positive integer (got: $CHART_WIDTH)" >&2
+ exit 1
+fi
+if [[ -n "$PIXEL_PER_POINT" && ! "$PIXEL_PER_POINT" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Error: --pixel-per-point must be a positive integer (got: $PIXEL_PER_POINT)" >&2
+ exit 1
+fi
+
# Extract dataset name from MPL: `dataset`:`metric` ... or dataset:`metric` ...
# Strip leading `param : ;` declarations first so their `:` doesn't
# get mistaken for the dataset:metric separator.
@@ -141,6 +195,23 @@ if [[ ${#PARAM_NAMES[@]} -gt 0 ]]; then
JQ_EXPR="$JQ_EXPR + {params: ($PARAMS_EXPR)}"
fi
+# Forward chart-sizing hints under queryOptions. The edge translates these into
+# the x-axiom-chart-width / x-axiom-pixel-per-point headers, which the metrics
+# service uses to resolve $__interval. Values are JSON numbers (--argjson).
+if [[ -n "$CHART_WIDTH" || -n "$PIXEL_PER_POINT" ]]; then
+ QO_EXPR=""
+ if [[ -n "$CHART_WIDTH" ]]; then
+ JQ_ARGS+=(--argjson chartWidth "$CHART_WIDTH")
+ QO_EXPR="{\"chart-width\": \$chartWidth}"
+ fi
+ if [[ -n "$PIXEL_PER_POINT" ]]; then
+ JQ_ARGS+=(--argjson pixelPerPoint "$PIXEL_PER_POINT")
+ if [[ -n "$QO_EXPR" ]]; then QO_EXPR+=" + "; fi
+ QO_EXPR+="{\"pixel-per-point\": \$pixelPerPoint}"
+ fi
+ JQ_EXPR="$JQ_EXPR + {queryOptions: ($QO_EXPR)}"
+fi
+
BODY=$(jq -n "${JQ_ARGS[@]}" "$JQ_EXPR")
AXIOM_ACCEPT="application/json+metrics.v2" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" POST "/v1/query/_mpl" "$BODY"
diff --git a/.agents/skills/building-dashboards/scripts/metrics/metrics-spec b/.agents/skills/building-dashboards/scripts/metrics/metrics-spec
index 5f022253..6a1cd014 100755
--- a/.agents/skills/building-dashboards/scripts/metrics/metrics-spec
+++ b/.agents/skills/building-dashboards/scripts/metrics/metrics-spec
@@ -1,31 +1,18 @@
#!/usr/bin/env bash
-# metrics-spec: Fetch the metrics query specification from Axiom
+# metrics-spec: Fetch the MPL metrics query specification from Axiom
#
-# Usage: metrics-spec
+# Usage: metrics-spec
#
-# Calls OPTIONS /v1/query/_mpl to retrieve the complete metrics query
-# spec with syntax, operators, and examples. Read this before composing queries.
-#
-# The dataset is needed to resolve the correct edge deployment URL.
-#
-# Example:
-# metrics-spec prod my-metrics-dataset
+# Retrieves the complete MPL query spec with syntax, operators, and examples.
+# Read this before composing queries.
set -euo pipefail
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SPEC_URL="https://us-east-1.aws.edge.axiom.co/v1/query/_mpl"
-DEPLOYMENT="${1:-}"
-DATASET="${2:-}"
-
-if [[ -z "$DEPLOYMENT" || -z "$DATASET" ]]; then
- echo "Usage: metrics-spec " >&2
- exit 1
-fi
-
-RESOLVED_URL=$("$SCRIPT_DIR/resolve-url" "$DEPLOYMENT" "$DATASET" 2>/dev/null || true)
-if [[ -n "$RESOLVED_URL" ]]; then
- export AXIOM_URL_OVERRIDE="$RESOLVED_URL"
-fi
-
-AXIOM_ACCEPT="text/markdown" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" OPTIONS "/v1/query/_mpl"
+# Match the timeout convention used by axiom-api so a stalled edge can't hang
+# the caller indefinitely. Override via AXIOM_CONNECT_TIMEOUT / AXIOM_MAX_TIME.
+curl -sS -X OPTIONS -H "Accept: text/markdown" \
+ --connect-timeout "${AXIOM_CONNECT_TIMEOUT:-10}" \
+ --max-time "${AXIOM_MAX_TIME:-120}" \
+ "$SPEC_URL"
diff --git a/.agents/skills/building-dashboards/tests/test-script-output.sh b/.agents/skills/building-dashboards/tests/test-script-output.sh
index 42375435..c18848df 100755
--- a/.agents/skills/building-dashboards/tests/test-script-output.sh
+++ b/.agents/skills/building-dashboards/tests/test-script-output.sh
@@ -125,6 +125,39 @@ else
fail "dashboard-chart-patch outputs valid JSON only" "got: $patch_out"
fi
+apl_fmt=$("$SCRIPTS_DIR/chart-add" --type Statistic --id t --name T \
+ --apl "['logs'] | where a=='x' | summarize c=count()" | jq -r '.query.apl')
+if [[ "$(printf '%s' "$apl_fmt" | grep -c '^| ')" == "2" && "$apl_fmt" != *" | "* ]]; then
+ ok "chart-add breaks each pipeline stage onto its own line"
+else
+ fail "chart-add breaks each pipeline stage onto its own line" "got: $apl_fmt"
+fi
+
+apl_str=$("$SCRIPTS_DIR/chart-add" --type Statistic --id t --name T \
+ --apl "['logs'] | where msg=='a | b'" | jq -r '.query.apl')
+if [[ "$apl_str" == *"msg=='a | b'"* ]]; then
+ ok "chart-add leaves a pipe inside a string literal untouched"
+else
+ fail "chart-add leaves a pipe inside a string literal untouched" "got: $apl_str"
+fi
+
+# Constructs whose string boundaries the split cannot follow must round-trip
+# byte-for-byte rather than risk a newline landing inside a literal.
+check_verbatim() {
+ local label="$1" input="$2" got
+ got=$("$SCRIPTS_DIR/chart-add" --type Statistic --id t --name T --apl "$input" | jq -r '.query.apl')
+ if [[ "$got" == "$input" ]]; then
+ ok "chart-add stores $label untouched"
+ else
+ fail "chart-add stores $label untouched" "got: $got"
+ fi
+}
+
+check_verbatim "a backslash-escaped quote" '["logs"] | where msg == "a \" b | c" | project msg'
+check_verbatim "an @-verbatim literal" '["logs"] | where p == @"c:\x | y" | project p'
+check_verbatim "a // comment" '["logs"] // note | here
+| count'
+
echo ""
echo "======================"
echo "Passed: $passed | Failed: $failed"
diff --git a/.agents/skills/openclaw-carapace/references/tokens.md b/.agents/skills/openclaw-carapace/references/tokens.md
index 64cf8463..29e35d35 100644
--- a/.agents/skills/openclaw-carapace/references/tokens.md
+++ b/.agents/skills/openclaw-carapace/references/tokens.md
@@ -11,9 +11,16 @@ exports when the consumer must control reset and adapter order.
| Semantic | `--oc-bg-*`, `--oc-text-*`, `--oc-accent-*` | Theme-aware UI intent |
| Scale | `--oc-space-*`, `--oc-font-size-*`, `--oc-radius-*` | Shared dimensions |
| Motion | `--oc-duration-*`, `--oc-ease-*` | Shared interaction timing |
+| Layer | `--oc-layer-*` | Popover and non-modal notification stacking roles |
| Product | `--oc-status-*`, `--oc-input-*`, `--oc-diff-*` | Opt-in operational UI |
| Consumer alias | Unprefixed legacy names | Migration compatibility only |
+Component styles consume semantic and product roles in their owning
+stylesheet; Carapace does not define a global component-token namespace. When
+a component genuinely needs a local custom property, scope it to the component
+root and document the override point beside that component rather than turning
+it into a second palette.
+
## Semantic Choices
- Page background: `--oc-bg-page`
@@ -27,6 +34,10 @@ exports when the consumer must control reset and adapter order.
`--oc-accent-primary-hover`
- Secondary accent: `--oc-accent-secondary`
- Neutral control backgrounds: `--oc-control-bg`, `--oc-control-bg-hover`
+- Modal isolation: `--oc-surface-modal-backdrop`; ordinary translucent
+ surfaces continue to use `--oc-surface-overlay`
+- Product fields: `--oc-input-*`; status feedback: paired `--oc-status-*-bg`
+ and `--oc-status-*-fg` roles
- Subtle, strong, and accent borders: `--oc-border-subtle`,
`--oc-border-strong`, `--oc-border-accent`
- Focus: `--oc-focus-ring`
diff --git a/.agents/skills/openclaw-design-system/references/tokens.md b/.agents/skills/openclaw-design-system/references/tokens.md
index 20263ce9..1d121942 100644
--- a/.agents/skills/openclaw-design-system/references/tokens.md
+++ b/.agents/skills/openclaw-design-system/references/tokens.md
@@ -13,9 +13,16 @@ to `@openclaw/carapace`.
| Semantic | `--oc-bg-*`, `--oc-text-*`, `--oc-accent-*` | Theme-aware UI intent |
| Scale | `--oc-space-*`, `--oc-font-size-*`, `--oc-radius-*` | Shared dimensions |
| Motion | `--oc-duration-*`, `--oc-ease-*` | Shared interaction timing |
+| Layer | `--oc-layer-*` | Popover and non-modal notification stacking roles |
| Product | `--oc-status-*`, `--oc-input-*`, `--oc-diff-*` | Opt-in operational UI |
| Consumer alias | Unprefixed legacy names | Migration compatibility only |
+Component styles consume semantic and product roles in their owning
+stylesheet; Carapace does not define a global component-token namespace. When
+a component genuinely needs a local custom property, scope it to the component
+root and document the override point beside that component rather than turning
+it into a second palette.
+
## Semantic Choices
- Page background: `--oc-bg-page`
@@ -29,6 +36,10 @@ to `@openclaw/carapace`.
`--oc-accent-primary-hover`
- Secondary accent: `--oc-accent-secondary`
- Neutral control backgrounds: `--oc-control-bg`, `--oc-control-bg-hover`
+- Modal isolation: `--oc-surface-modal-backdrop`; ordinary translucent
+ surfaces continue to use `--oc-surface-overlay`
+- Product fields: `--oc-input-*`; status feedback: paired `--oc-status-*-bg`
+ and `--oc-status-*-fg` roles
- Subtle, strong, and accent borders: `--oc-border-subtle`,
`--oc-border-strong`, `--oc-border-accent`
- Focus: `--oc-focus-ring`
diff --git a/.agents/skills/query-metrics/README.md b/.agents/skills/query-metrics/README.md
index 82a4f0b4..b9e966cf 100644
--- a/.agents/skills/query-metrics/README.md
+++ b/.agents/skills/query-metrics/README.md
@@ -53,7 +53,7 @@ scripts/datasets prod
scripts/datasets prod --kind otel:metrics:v1
# Fetch the metrics query spec
-scripts/metrics-spec prod
+scripts/metrics-spec
# List available metrics in a dataset
scripts/metrics-info prod my-dataset metrics
diff --git a/.agents/skills/query-metrics/SKILL.md b/.agents/skills/query-metrics/SKILL.md
index f00ef14e..9bab11b5 100644
--- a/.agents/skills/query-metrics/SKILL.md
+++ b/.agents/skills/query-metrics/SKILL.md
@@ -12,7 +12,7 @@ Setup, prerequisites, and `~/.axiom.toml` configuration: see `README.md`. Edge-d
## Workflow
1. `scripts/datasets --kind otel:metrics:v1` — list metrics datasets.
-2. `scripts/metrics-spec ` — **required** before composing any query. MPL evolves; the spec is the source of truth.
+2. `scripts/metrics-spec` — **required** before composing any query. MPL evolves; the spec is the source of truth. Also use it to answer general MPL/metrics questions.
3. `scripts/metrics-info metrics` — list metrics with `{type, temporality, unit}` metadata. Read this before writing the query (see [Choosing a Query Shape](#choosing-a-query-shape)).
4. `scripts/metrics-info tags [ values]` — explore filter dimensions.
5. `scripts/metrics-query '' ` — execute. Iterate.
@@ -35,7 +35,7 @@ Rules per type (consult `metrics-spec` for exact operator names — they evolve)
- **CounterMonotonic + Cumulative** — running total (resets aside). The raw values are rarely what you want. Convert to a per-second rate first, **then** align/aggregate.
- **CounterMonotonic + Delta** — already per-interval. Sum/align without a rate step.
- **CounterNonMonotonic** — can go up or down (queue depth, balance). Intent is ambiguous: rate, delta, or current value all make sense for different questions. **Ask the user** before picking one.
-- **Histogram** — not a scalar. `align using avg` produces nonsense. Use the bucket/quantile operators from `metrics-spec`.
+- **Histogram** — not a scalar. `align using avg` produces nonsense. Use `bucket … using` with the histogram functions from `metrics-spec`; quantiles are float specs to those functions, and `temporality` selects the variant (`Cumulative` vs `Delta` interpolation). Consult `metrics-spec` for the exact signatures.
- **`temporality: null`** — "not applicable for this instrument type" (the norm for Gauges), not "missing data".
When surfacing numbers, attach the `unit` (treat `null` as unitless). If you combine metrics with mismatched units in arithmetic, warn rather than silently producing a meaningless number.
@@ -43,7 +43,7 @@ When surfacing numbers, attach the `unit` (treat `null` as unitless). If you com
## Query Metrics
```bash
-scripts/metrics-query ''
+scripts/metrics-query [-w pixels] [--pixel-per-point n] ''
```
| Parameter | Notes |
@@ -51,22 +51,57 @@ scripts/metrics-query ''
| `deploy` | Name from `~/.axiom.toml` (e.g. `prod`). |
| `MPL` | Pipeline string. Dataset is parsed from the MPL itself. |
| `start` / `end` | RFC3339 (`2025-01-01T00:00:00Z`) or relative (`now-1h`, `now`). |
+| `-w` / `--chart-width ` | Optional. Target chart width in pixels; lets the server resolve `$__interval`. |
+| `--pixel-per-point ` | Optional. Pixels per point (server default 10); with `-w` sets the bucket count. |
+
+**Always single-quote the MPL string in the shell.** MPL is full of backticks; inside double quotes the shell executes them as command substitution, silently mangling the query (or running whatever the identifier names).
+
+**Bound the output before grouping.** `group by ` returns one series per tag value with no cap — on a high-cardinality tag this floods the output. Check cardinality first (`describe`, or `tags values`) and prefer plain `group using ` while exploring.
Examples:
```bash
-scripts/metrics-query prod \
- '`my-dataset`:`http.server.duration` | align to 5m using avg' \
+scripts/metrics-query prod -w 1200 \
+ '`my-dataset`:`http.server.duration` | align to $__interval using avg' \
now-1h now
-scripts/metrics-query prod \
+scripts/metrics-query prod -w 1200 \
'`my-dataset`:`http.server.duration`
| where `service.name` == "frontend" and method == "GET"
- | align to 5m using avg
+ | align to $__interval using avg
| group by status_code using sum' \
now-1d now
```
+### Adaptive resolution (`$__interval`)
+
+Hardcoding a step (`align to 5m`) makes charts look wrong at other zoom
+levels — too sparse zoomed in, too dense zoomed out. Prefer the system
+parameter `$__interval` wherever a `Duration` is expected, and pass the chart
+width so the server picks the step:
+
+```bash
+scripts/metrics-query prod -w 1200 \
+ '`my-dataset`:`http.server.duration` | align to $__interval using avg' \
+ now-7d now
+```
+
+The metrics service computes `$__interval` from the query's time range and the
+target chart width, then snaps it **up** to a nice resolution from the ladder
+`1s, 5s, 10s, 15s, 30s, 1m, 5m, 10m, 15m, 30m, 1h, 12h, 1d, 1w, 1M, 1Y`. It
+never drops below a metric's stored resolution.
+
+- **No declaration needed** — the server auto-registers `$__interval`; do *not*
+ add `param $__interval: Duration;` (the edge forwards the query verbatim and
+ the metrics service injects the parameter).
+- **Bucket count** ≈ `chart-width / pixel-per-point` (`pixel-per-point` default
+ 10). Omit `-w` and the server targets ~500 buckets.
+- Works anywhere a `Duration` is valid, e.g. `bucket to $__interval using
+ histogram(0.5, 0.95)`.
+- Set `-w` to your render width (e.g. the `metrics-chart` skill's plot width)
+ so one bucket ≈ one pixel column. The value is forwarded under the request
+ body's `queryOptions` (`chart-width`, `pixel-per-point`).
+
### Parameters
MPL can declare parameters (`param $svc: string;`). Pass values with repeated `-p name=value`. The script applies the API's `param__` prefix; values are forwarded verbatim as MPL literals (string literals include their quotes).
@@ -96,7 +131,7 @@ Literal syntax per type lives in `metrics-spec`.
## Discovery (`metrics-info`)
-Time range defaults to the last 24h; override with `--start` / `--end`.
+Time range defaults to the last 24h; override with `--start` / `--end`. Both accept RFC3339 (offsets allowed) or relative `now` / `now-` with `` in `s m h d w`, resolved to RFC3339 UTC client-side. This is **narrower** than `metrics-query`, which forwards times to the server unparsed and so also accepts forms like `now-1y`; in `metrics-info` anything outside `now` / `now-[smhdw]` must already be RFC3339 or the request 400s.
| Command | Returns |
|---|---|
@@ -114,21 +149,25 @@ Time range defaults to the last 24h; override with `--start` / `--end`.
## Error Handling
-HTTP errors return JSON with `message`, `code`, and optional `detail`:
+HTTP errors return JSON with `code` and `message`; some include a `detail` object:
```json
-{"message": "...", "code": 400, "detail": {"errorType": 1, "message": "raw error"}}
+{"code": 400, "message": "MPL syntax error: …"}
```
+Syntax errors (400) include an annotated source pointer listing the valid operators at the failure position — read it, it usually names the fix.
+
| Code | Cause |
|---|---|
| 400 | Invalid query syntax or bad dataset name |
| 401 | Missing/invalid auth |
| 403 | No permission |
| 404 | Dataset not found |
-| 429 | Rate limited |
+| 429 | Rate limited — back off and retry; don't tight-loop |
| 500 | Internal error |
+Requests time out client-side after 120s (`AXIOM_MAX_TIME` to override; `AXIOM_CONNECT_TIMEOUT` for the 10s connect timeout).
+
On 500, re-run with `curl -v` to capture the `traceparent` / `x-axiom-trace-id` header and report it — the trace ID is what the backend team needs to debug.
## Scripts
@@ -137,8 +176,8 @@ On 500, re-run with `curl -v` to capture the `traceparent` / `x-axiom-trace-id`
|---|---|
| `scripts/setup` | Check requirements and config. |
| `scripts/datasets [--kind ]` | List datasets with edge deployment. |
-| `scripts/metrics-spec ` | Fetch the MPL query spec. |
-| `scripts/metrics-query ` | Execute a query. |
+| `scripts/metrics-spec` | Fetch the MPL query spec. |
+| `scripts/metrics-query [-w px] [--pixel-per-point n] ` | Execute a query; use `$__interval` + `-w` for adaptive resolution. |
| `scripts/metrics-info ...` | Discover metrics, tags, values. |
| `scripts/axiom-api [body]` | Low-level API calls. |
| `scripts/resolve-url ` | Resolve to the edge deployment URL. |
diff --git a/.agents/skills/query-metrics/scripts/axiom-api b/.agents/skills/query-metrics/scripts/axiom-api
index 8f63e353..0e607039 100755
--- a/.agents/skills/query-metrics/scripts/axiom-api
+++ b/.agents/skills/query-metrics/scripts/axiom-api
@@ -5,6 +5,8 @@
#
# Reads credentials from ~/.axiom.toml (shared with axiom-sre)
# Set AXIOM_URL_OVERRIDE to route requests to a specific edge deployment endpoint.
+# Set AXIOM_CONNECT_TIMEOUT / AXIOM_MAX_TIME (seconds) to override the default
+# connection (10s) and total request (120s) timeouts.
#
# Examples:
# axiom-api prod GET /v1/datasets
@@ -51,6 +53,8 @@ fi
CURL_ARGS=(
-s
+ --connect-timeout "${AXIOM_CONNECT_TIMEOUT:-10}"
+ --max-time "${AXIOM_MAX_TIME:-120}"
-w '\n%{http_code}'
-X "$METHOD"
-H "Authorization: Bearer $TOKEN"
diff --git a/.agents/skills/query-metrics/scripts/metrics-info b/.agents/skills/query-metrics/scripts/metrics-info
index b99e7ff8..6d3f156b 100755
--- a/.agents/skills/query-metrics/scripts/metrics-info
+++ b/.agents/skills/query-metrics/scripts/metrics-info
@@ -47,7 +47,12 @@
# specific entity name (service, host, device) to find which metrics carry it.
# To list metric names, use the `metrics` subcommand instead.
#
-# --start and --end default to the last 24 hours if omitted.
+# --start and --end accept RFC3339 (offsets allowed, e.g. 2025-06-01T00:00:00+02:00)
+# or relative now / now- with in s/m/h/d/w, resolved to RFC3339 UTC
+# client-side because the info endpoints only parse RFC3339. This is narrower than
+# metrics-query, which forwards times to the server unparsed and also accepts forms
+# like now-1y; here anything outside now / now-[smhdw] must already be RFC3339.
+# Defaults: last 24 hours.
# For sparse metrics (sensors, batch jobs), try --start with a wider range (e.g. 7 days).
#
# Examples:
@@ -67,6 +72,53 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# Percent-encode one URL component (path segment or query value). Dataset,
+# metric, and tag names are user/OTel-controlled and may contain characters
+# that are reserved in URLs (/ % + space); times may carry a `+02:00` offset
+# whose `+` would otherwise decode as a space server-side.
+urlencode() {
+ jq -rn --arg v "$1" '$v|@uri'
+}
+
+# Normalize a time argument to RFC3339 UTC. RFC3339 input passes through
+# verbatim; the relative forms `now` and `now-` (unit in s/m/h/d/w)
+# are resolved client-side because the info endpoints only parse RFC3339.
+# Note: metrics-query forwards times to the server unparsed, so it accepts a
+# broader set (e.g. now-1y); those forms are NOT handled here and, if passed,
+# fall through to the RFC3339-only endpoint and fail.
+normalize_time() {
+ local t="$1"
+ if [[ "$t" == "now" ]]; then
+ date -u '+%Y-%m-%dT%H:%M:%SZ'
+ elif [[ "$t" =~ ^now-([0-9]+)([smhdw])$ ]]; then
+ local n="${BASH_REMATCH[1]}" u="${BASH_REMATCH[2]}"
+ if date --version &>/dev/null; then
+ local word
+ case "$u" in
+ s) word="seconds" ;;
+ m) word="minutes" ;;
+ h) word="hours" ;;
+ d) word="days" ;;
+ w) word="weeks" ;;
+ esac
+ date -u -d "$n $word ago" '+%Y-%m-%dT%H:%M:%SZ'
+ else
+ # BSD date: -v units are case-sensitive (M = minute, m = month).
+ local unit
+ case "$u" in
+ s) unit="S" ;;
+ m) unit="M" ;;
+ h) unit="H" ;;
+ d) unit="d" ;;
+ w) unit="w" ;;
+ esac
+ date -u -v "-${n}${unit}" '+%Y-%m-%dT%H:%M:%SZ'
+ fi
+ else
+ printf '%s\n' "$t"
+ fi
+}
+
show_usage() {
echo "Usage:" >&2
echo " metrics-info metrics [--by-type] [--type T]..." >&2
@@ -80,8 +132,8 @@ show_usage() {
echo " metrics-info find-metrics (searches tag values, not metric names)" >&2
echo "" >&2
echo "Options:" >&2
- echo " --start T Start time (RFC3339). Default: 24h ago" >&2
- echo " --end T End time (RFC3339). Default: now" >&2
+ echo " --start T Start time (RFC3339 or relative, e.g. now-7d). Default: 24h ago" >&2
+ echo " --end T End time (RFC3339 or relative, e.g. now). Default: now" >&2
echo " --by-type (metrics listing) Group entries by metric type" >&2
echo " --type T (metrics listing) Filter to type T. Repeatable." >&2
echo " --no-values (describe) Return tag names only" >&2
@@ -118,20 +170,12 @@ while [[ $# -gt 0 ]]; do
esac
done
-# Default time range: last 24 hours
-if [[ -z "$START" ]]; then
- if date --version &>/dev/null 2>&1; then
- START=$(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ')
- else
- START=$(date -u -v-24H '+%Y-%m-%dT%H:%M:%SZ')
- fi
-fi
-if [[ -z "$END" ]]; then
- END=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
-fi
+# Default time range: last 24 hours. Relative forms are resolved to RFC3339 UTC.
+START=$(normalize_time "${START:-now-24h}")
+END=$(normalize_time "${END:-now}")
-TIME_PARAMS="start=${START}&end=${END}"
-BASE="/v1/query/metrics/info/datasets/${DATASET}"
+TIME_PARAMS="start=$(urlencode "$START")&end=$(urlencode "$END")"
+BASE="/v1/query/metrics/info/datasets/$(urlencode "$DATASET")"
# Resolve the regional edge URL for this dataset
RESOLVED_URL=$("$SCRIPT_DIR/resolve-url" "$DEPLOYMENT" "$DATASET" 2>/dev/null || true)
@@ -185,34 +229,62 @@ case "${POSITIONAL[0]}" in
# the typical 1+1+N round trips an agent would make to characterise
# an unfamiliar metric.
METRIC="${POSITIONAL[1]}"
+ METRIC_ENC=$(urlencode "$METRIC")
RAW=$(fetch_metrics_listing)
META=$(printf '%s' "$RAW" | jq -e --arg m "$METRIC" '.[$m] // error("metric not found in listing for the given time range: " + $m)')
- TAGS_JSON=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags?${TIME_PARAMS}")
+ TAGS_JSON=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC_ENC}/tags?${TIME_PARAMS}")
if [[ "$NO_VALUES" -eq 1 ]]; then
# tags as flat array of names
jq -n --argjson m "$META" --argjson tags "$TAGS_JSON" '$m + {tags: $tags}'
else
- # tags as object: { tag_name: [values…] }
- VALUES_OBJ='{}'
+ # tags as object: { tag_name: [values…] }. Per-tag value fetches
+ # are independent, so run them concurrently; tag counts are small
+ # (rarely more than a few dozen), so no concurrency cap is needed.
+ TAG_NAMES=()
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
- VALUES=$("$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags/${tag}/values?${TIME_PARAMS}")
- if [[ "$VALUES_LIMIT" -gt 0 ]]; then
- VALUES=$(printf '%s' "$VALUES" | jq --argjson n "$VALUES_LIMIT" '.[:$n]')
- fi
- VALUES_OBJ=$(jq -n --argjson o "$VALUES_OBJ" --arg t "$tag" --argjson v "$VALUES" '$o + {($t): $v}')
+ TAG_NAMES+=("$tag")
done < <(printf '%s' "$TAGS_JSON" | jq -r '.[]?')
+ VALUES_OBJ='{}'
+ if [[ ${#TAG_NAMES[@]} -gt 0 ]]; then
+ TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/metrics-info.XXXXXX")
+ trap 'rm -rf "$TMP_DIR"' EXIT
+ PIDS=()
+ for i in "${!TAG_NAMES[@]}"; do
+ "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET \
+ "${BASE}/metrics/${METRIC_ENC}/tags/$(urlencode "${TAG_NAMES[$i]}")/values?${TIME_PARAMS}" \
+ > "$TMP_DIR/$i.json" &
+ PIDS+=($!)
+ done
+ FETCH_FAILED=0
+ for i in "${!PIDS[@]}"; do
+ if ! wait "${PIDS[$i]}"; then
+ echo "Error: failed to fetch values for tag '${TAG_NAMES[$i]}'" >&2
+ FETCH_FAILED=1
+ fi
+ done
+ if [[ "$FETCH_FAILED" -eq 1 ]]; then
+ exit 1
+ fi
+ for i in "${!TAG_NAMES[@]}"; do
+ VALUES=$(cat "$TMP_DIR/$i.json")
+ if [[ "$VALUES_LIMIT" -gt 0 ]]; then
+ VALUES=$(printf '%s' "$VALUES" | jq --argjson n "$VALUES_LIMIT" '.[:$n]')
+ fi
+ VALUES_OBJ=$(jq -n --argjson o "$VALUES_OBJ" --arg t "${TAG_NAMES[$i]}" --argjson v "$VALUES" '$o + {($t): $v}')
+ done
+ fi
jq -n --argjson m "$META" --argjson tags "$VALUES_OBJ" '$m + {tags: $tags}'
fi
elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "tags" ]]; then
# List tags for a metric
METRIC="${POSITIONAL[1]}"
- "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags?${TIME_PARAMS}"
+ "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/$(urlencode "$METRIC")/tags?${TIME_PARAMS}"
elif [[ ${#POSITIONAL[@]} -eq 5 && "${POSITIONAL[2]}" == "tags" && "${POSITIONAL[4]}" == "values" ]]; then
# List tag values for a metric+tag
METRIC="${POSITIONAL[1]}"
TAG="${POSITIONAL[3]}"
- "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/${METRIC}/tags/${TAG}/values?${TIME_PARAMS}"
+ "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/metrics/$(urlencode "$METRIC")/tags/$(urlencode "$TAG")/values?${TIME_PARAMS}"
elif [[ ${#POSITIONAL[@]} -eq 5 && "${POSITIONAL[2]}" == "tags" && "${POSITIONAL[4]}" == "type" ]]; then
# Probe the typing of a metric+tag by running `metrics-query` with
# `filter is ` for each candidate type. The type(s) that
@@ -226,8 +298,15 @@ case "${POSITIONAL[0]}" in
# ``:`` | filter `` is | align to 5m using sum
# If is matches no rows, the response has empty `series`.
PROBE_QUERY='`'"$DATASET"'`:`'"$METRIC"'` | filter `'"$TAG"'` is '"$t"' | align to 5m using sum'
- RESPONSE=$("$SCRIPT_DIR/metrics-query" "$DEPLOYMENT" "$PROBE_QUERY" "$START" "$END" 2>/dev/null || echo '{}')
- COUNT=$(printf '%s' "$RESPONSE" | jq -r '(.series // []) | length' 2>/dev/null || echo 0)
+ # Propagate probe failures instead of swallowing them: a failed
+ # query (bad dataset, auth, network) must not be reported as the
+ # tag being "absent" — that would be a confident wrong answer.
+ if ! RESPONSE=$("$SCRIPT_DIR/metrics-query" "$DEPLOYMENT" "$PROBE_QUERY" "$START" "$END" 2>&1); then
+ echo "Error: type probe query failed (tag '$TAG' is $t):" >&2
+ printf '%s\n' "$RESPONSE" >&2
+ exit 1
+ fi
+ COUNT=$(printf '%s' "$RESPONSE" | jq -r '(.series // []) | length')
if [[ "$COUNT" -gt 0 ]]; then
PRESENT_JSON=$(printf '%s' "$PRESENT_JSON" | jq --arg t "$t" '. + [$t]')
fi
@@ -253,7 +332,7 @@ case "${POSITIONAL[0]}" in
elif [[ ${#POSITIONAL[@]} -eq 3 && "${POSITIONAL[2]}" == "values" ]]; then
# List values for a tag
TAG="${POSITIONAL[1]}"
- "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/${TAG}/values?${TIME_PARAMS}"
+ "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" GET "${BASE}/tags/$(urlencode "$TAG")/values?${TIME_PARAMS}"
else
show_usage
fi
diff --git a/.agents/skills/query-metrics/scripts/metrics-query b/.agents/skills/query-metrics/scripts/metrics-query
index 9dbbbb76..b094bef7 100755
--- a/.agents/skills/query-metrics/scripts/metrics-query
+++ b/.agents/skills/query-metrics/scripts/metrics-query
@@ -1,10 +1,23 @@
#!/usr/bin/env bash
# metrics-query: Execute a metrics query against Axiom MetricsDB
#
-# Usage: metrics-query [-p name=value]...
+# Usage: metrics-query [-p name=value]... [-w pixels] [--pixel-per-point n] \
+#
#
# Times: RFC3339 (e.g. 2025-01-01T00:00:00Z) or relative (e.g. now-1h, now-1d).
#
+# Adaptive resolution ($__interval):
+# Reference $__interval anywhere a Duration is expected (e.g.
+# `align to $__interval using avg`, `bucket to $__interval ...`) and the
+# server resolves it to a "nice" step computed from the query time range and
+# the target chart width. No `param $__interval` declaration is needed -- the
+# metrics service registers it automatically. Tune the density with:
+# -w / --chart-width target chart width; the server aims for
+# ~chart-width/pixel-per-point buckets
+# (default ~500 buckets when -w is omitted).
+# --pixel-per-point pixels per data point (server default 10).
+# Both are forwarded under the request body's queryOptions object.
+#
# Parameter values (-p / --param name=value, repeatable):
# For each MPL parameter declared in the query (e.g. `param $svc: string;`),
# pass the variable name without the leading `$` and an MPL literal as the
@@ -32,6 +45,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PARAMS=()
POSITIONAL=()
+CHART_WIDTH=""
+PIXEL_PER_POINT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-p|--param)
@@ -46,6 +61,30 @@ while [[ $# -gt 0 ]]; do
PARAMS+=("${1#--param=}")
shift
;;
+ -w|--chart-width)
+ if [[ $# -lt 2 ]]; then
+ echo "Error: $1 requires a pixel-width argument" >&2
+ exit 1
+ fi
+ CHART_WIDTH="$2"
+ shift 2
+ ;;
+ --chart-width=*)
+ CHART_WIDTH="${1#--chart-width=}"
+ shift
+ ;;
+ --pixel-per-point)
+ if [[ $# -lt 2 ]]; then
+ echo "Error: $1 requires an integer argument" >&2
+ exit 1
+ fi
+ PIXEL_PER_POINT="$2"
+ shift 2
+ ;;
+ --pixel-per-point=*)
+ PIXEL_PER_POINT="${1#--pixel-per-point=}"
+ shift
+ ;;
--)
shift
while [[ $# -gt 0 ]]; do POSITIONAL+=("$1"); shift; done
@@ -63,13 +102,17 @@ START_TIME="${POSITIONAL[2]:-}"
END_TIME="${POSITIONAL[3]:-}"
if [[ -z "$DEPLOYMENT" || -z "$MPL" || -z "$START_TIME" || -z "$END_TIME" ]]; then
- echo "Usage: metrics-query [-p name=value]... " >&2
+ echo "Usage: metrics-query [-p name=value]... [-w pixels] [--pixel-per-point n] " >&2
echo "" >&2
echo "Times: RFC3339 (e.g. 2025-01-01T00:00:00Z) or relative (e.g. now-1h, now-1d)." >&2
echo "" >&2
echo "-p / --param name=value (repeatable): supply an MPL parameter value." >&2
echo " name - variable name without the leading \$ (e.g. 'svc' for \$svc)." >&2
echo " value - MPL literal, forwarded verbatim under params.param__." >&2
+ echo "" >&2
+ echo "-w / --chart-width target chart width; lets the server resolve" >&2
+ echo " \$__interval to a nice step (queryOptions)." >&2
+ echo "--pixel-per-point pixels per data point (server default 10)." >&2
exit 1
fi
@@ -97,6 +140,17 @@ if [[ ${#PARAMS[@]} -gt 0 ]]; then
done
fi
+# Validate the optional chart-sizing options. They must be positive integers;
+# they are forwarded under queryOptions so the server can resolve $__interval.
+if [[ -n "$CHART_WIDTH" && ! "$CHART_WIDTH" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Error: --chart-width must be a positive integer (got: $CHART_WIDTH)" >&2
+ exit 1
+fi
+if [[ -n "$PIXEL_PER_POINT" && ! "$PIXEL_PER_POINT" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Error: --pixel-per-point must be a positive integer (got: $PIXEL_PER_POINT)" >&2
+ exit 1
+fi
+
# Extract dataset name from MPL: `dataset`:`metric` ... or dataset:`metric` ...
# Strip leading `param : ;` declarations first so their `:` doesn't
# get mistaken for the dataset:metric separator.
@@ -141,6 +195,23 @@ if [[ ${#PARAM_NAMES[@]} -gt 0 ]]; then
JQ_EXPR="$JQ_EXPR + {params: ($PARAMS_EXPR)}"
fi
+# Forward chart-sizing hints under queryOptions. The edge translates these into
+# the x-axiom-chart-width / x-axiom-pixel-per-point headers, which the metrics
+# service uses to resolve $__interval. Values are JSON numbers (--argjson).
+if [[ -n "$CHART_WIDTH" || -n "$PIXEL_PER_POINT" ]]; then
+ QO_EXPR=""
+ if [[ -n "$CHART_WIDTH" ]]; then
+ JQ_ARGS+=(--argjson chartWidth "$CHART_WIDTH")
+ QO_EXPR="{\"chart-width\": \$chartWidth}"
+ fi
+ if [[ -n "$PIXEL_PER_POINT" ]]; then
+ JQ_ARGS+=(--argjson pixelPerPoint "$PIXEL_PER_POINT")
+ if [[ -n "$QO_EXPR" ]]; then QO_EXPR+=" + "; fi
+ QO_EXPR+="{\"pixel-per-point\": \$pixelPerPoint}"
+ fi
+ JQ_EXPR="$JQ_EXPR + {queryOptions: ($QO_EXPR)}"
+fi
+
BODY=$(jq -n "${JQ_ARGS[@]}" "$JQ_EXPR")
AXIOM_ACCEPT="application/json+metrics.v2" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" POST "/v1/query/_mpl" "$BODY"
diff --git a/.agents/skills/query-metrics/scripts/metrics-spec b/.agents/skills/query-metrics/scripts/metrics-spec
index 5f022253..6a1cd014 100755
--- a/.agents/skills/query-metrics/scripts/metrics-spec
+++ b/.agents/skills/query-metrics/scripts/metrics-spec
@@ -1,31 +1,18 @@
#!/usr/bin/env bash
-# metrics-spec: Fetch the metrics query specification from Axiom
+# metrics-spec: Fetch the MPL metrics query specification from Axiom
#
-# Usage: metrics-spec
+# Usage: metrics-spec
#
-# Calls OPTIONS /v1/query/_mpl to retrieve the complete metrics query
-# spec with syntax, operators, and examples. Read this before composing queries.
-#
-# The dataset is needed to resolve the correct edge deployment URL.
-#
-# Example:
-# metrics-spec prod my-metrics-dataset
+# Retrieves the complete MPL query spec with syntax, operators, and examples.
+# Read this before composing queries.
set -euo pipefail
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SPEC_URL="https://us-east-1.aws.edge.axiom.co/v1/query/_mpl"
-DEPLOYMENT="${1:-}"
-DATASET="${2:-}"
-
-if [[ -z "$DEPLOYMENT" || -z "$DATASET" ]]; then
- echo "Usage: metrics-spec " >&2
- exit 1
-fi
-
-RESOLVED_URL=$("$SCRIPT_DIR/resolve-url" "$DEPLOYMENT" "$DATASET" 2>/dev/null || true)
-if [[ -n "$RESOLVED_URL" ]]; then
- export AXIOM_URL_OVERRIDE="$RESOLVED_URL"
-fi
-
-AXIOM_ACCEPT="text/markdown" "$SCRIPT_DIR/axiom-api" "$DEPLOYMENT" OPTIONS "/v1/query/_mpl"
+# Match the timeout convention used by axiom-api so a stalled edge can't hang
+# the caller indefinitely. Override via AXIOM_CONNECT_TIMEOUT / AXIOM_MAX_TIME.
+curl -sS -X OPTIONS -H "Accept: text/markdown" \
+ --connect-timeout "${AXIOM_CONNECT_TIMEOUT:-10}" \
+ --max-time "${AXIOM_MAX_TIME:-120}" \
+ "$SPEC_URL"
diff --git a/.agents/skills/query-metrics/scripts/setup b/.agents/skills/query-metrics/scripts/setup
index 7467df38..0e872a22 100755
--- a/.agents/skills/query-metrics/scripts/setup
+++ b/.agents/skills/query-metrics/scripts/setup
@@ -79,7 +79,7 @@ echo ""
echo "Usage:"
echo " scripts/datasets prod # List datasets"
echo " scripts/datasets prod --kind otel:metrics:v1 # List metrics datasets"
-echo " scripts/metrics-spec prod # Fetch query spec"
+echo " scripts/metrics-spec # Fetch query spec"
echo " scripts/metrics-info prod metrics # List metrics"
echo " scripts/metrics-info prod tags # List tags"
echo " scripts/metrics-query prod '' '' '' # Run query"
diff --git a/skills-lock.json b/skills-lock.json
index 2d86d4f3..d3dae2a2 100644
--- a/skills-lock.json
+++ b/skills-lock.json
@@ -17,7 +17,7 @@
"source": "axiomhq/skills",
"sourceType": "github",
"skillPath": "skills/building-dashboards/SKILL.md",
- "computedHash": "331e80b2aa6d7a8a9d5ec69c50d5a843f125534ed01d93b9cc916a5a92ca9552"
+ "computedHash": "84a130aab324297fe3fe1203b56743359732d4386dff6f0d065726d175b6e413"
},
"controlling-costs": {
"source": "axiomhq/skills",
@@ -35,7 +35,7 @@
"source": "openclaw/carapace",
"sourceType": "github",
"skillPath": "openclaw-carapace/SKILL.md",
- "computedHash": "6d7a33003875107ab35d4ac978753cc95b38b38aaaa306ae3db54f935d5451f7"
+ "computedHash": "84d8602ecdf8e1ad39cd4c5ae8f1165da571619746ab705a471d6fdfe7f4ef3e"
},
"openclaw-design": {
"source": "openclaw/carapace",
@@ -53,7 +53,7 @@
"source": "openclaw/carapace",
"sourceType": "github",
"skillPath": "openclaw-design-system/SKILL.md",
- "computedHash": "6d94e825c58fc5650a532e4ed8bc96208ede8a166089598d1924393cc46f3275"
+ "computedHash": "e7ff42933430aaf28bd2ca7a9e861c62e4aadddc7604d777118b7fec3304b042"
},
"openclaw-marketing-pages": {
"source": "openclaw/carapace",
@@ -65,7 +65,7 @@
"source": "axiomhq/skills",
"sourceType": "github",
"skillPath": "skills/query-metrics/SKILL.md",
- "computedHash": "3da9845936d9cb99f8fa6fdcdced06cc25eb9f16aa401e4838991d7f039fc177"
+ "computedHash": "955a35d22ca2244c14abafe10c57b8314ddbbc5e9ce1040ddc91edf6cfb05f8e"
},
"sentry-fix-issues": {
"source": "getsentry/sentry-for-ai",