idlewatch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +73 -0
- package/.github/workflows/ci.yml +99 -0
- package/.github/workflows/release-macos-trusted.yml +103 -0
- package/README.md +336 -0
- package/bin/idlewatch-agent.js +1053 -0
- package/docs/onboarding-external.md +58 -0
- package/docs/packaging/macos-dmg.md +199 -0
- package/docs/packaging/macos-launch-agent.md +70 -0
- package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
- package/docs/qa/mac-qa-log.md +2864 -0
- package/docs/telemetry/idle-stale-policy.md +57 -0
- package/docs/telemetry/openclaw-mapping.md +80 -0
- package/package.json +76 -0
- package/scripts/build-dmg.sh +65 -0
- package/scripts/install-macos-launch-agent.sh +78 -0
- package/scripts/lib/telemetry-row-parser.mjs +100 -0
- package/scripts/package-macos.sh +228 -0
- package/scripts/uninstall-macos-launch-agent.sh +30 -0
- package/scripts/validate-all.sh +142 -0
- package/scripts/validate-bin.mjs +25 -0
- package/scripts/validate-dmg-checksum.sh +37 -0
- package/scripts/validate-dmg-install.sh +155 -0
- package/scripts/validate-dry-run-schema.mjs +257 -0
- package/scripts/validate-onboarding.mjs +63 -0
- package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
- package/scripts/validate-openclaw-release-gates.mjs +51 -0
- package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
- package/scripts/validate-openclaw-usage-health.mjs +95 -0
- package/scripts/validate-packaged-artifact.mjs +233 -0
- package/scripts/validate-packaged-bundled-runtime.sh +191 -0
- package/scripts/validate-packaged-metadata.sh +43 -0
- package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
- package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
- package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
- package/scripts/validate-packaged-sourcemaps.mjs +82 -0
- package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
- package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
- package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
- package/scripts/validate-trusted-prereqs.sh +44 -0
- package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
- package/scripts/validate-usage-freshness-e2e.mjs +81 -0
- package/skill/SKILL.md +43 -0
- package/src/config.js +100 -0
- package/src/enrollment.js +176 -0
- package/src/gpu.js +115 -0
- package/src/memory.js +67 -0
- package/src/openclaw-cache.js +51 -0
- package/src/openclaw-usage.js +1020 -0
- package/src/telemetry-mapping.js +54 -0
- package/src/usage-alert.js +41 -0
- package/src/usage-freshness.js +31 -0
- package/test/config.test.mjs +112 -0
- package/test/fixtures/gpu-agx.txt +2 -0
- package/test/fixtures/gpu-iogpu.txt +2 -0
- package/test/fixtures/gpu-top-grep.txt +2 -0
- package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
- package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
- package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
- package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
- package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
- package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
- package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
- package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
- package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
- package/test/fixtures/openclaw-stats.json +17 -0
- package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
- package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
- package/test/fixtures/openclaw-status-control-noise.txt +1 -0
- package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
- package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
- package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
- package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
- package/test/fixtures/openclaw-status-multi-json.txt +3 -0
- package/test/fixtures/openclaw-status-nested-recent.json +19 -0
- package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
- package/test/fixtures/openclaw-status-noisy.txt +3 -0
- package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
- package/test/fixtures/openclaw-status-result-session.json +15 -0
- package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
- package/test/fixtures/openclaw-status-session-map.json +28 -0
- package/test/fixtures/openclaw-status-session-model-name.json +18 -0
- package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
- package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
- package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
- package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
- package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
- package/test/fixtures/openclaw-status-strings.json +38 -0
- package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
- package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
- package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
- package/test/fixtures/openclaw-status.json +41 -0
- package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
- package/test/gpu.test.mjs +58 -0
- package/test/memory.test.mjs +35 -0
- package/test/openclaw-cache.test.mjs +48 -0
- package/test/openclaw-env.test.mjs +365 -0
- package/test/openclaw-usage.test.mjs +555 -0
- package/test/telemetry-mapping.test.mjs +69 -0
- package/test/telemetry-row-parser.test.mjs +44 -0
- package/test/usage-alert.test.mjs +73 -0
- package/test/usage-freshness.test.mjs +63 -0
- package/test/validate-dry-run-schema.test.mjs +146 -0
- package/tui/Cargo.lock +801 -0
- package/tui/Cargo.toml +11 -0
- package/tui/src/main.rs +368 -0
package/.env.example
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Optional custom host label
|
|
2
|
+
IDLEWATCH_HOST=my-host
|
|
3
|
+
|
|
4
|
+
# Sampling interval in milliseconds
|
|
5
|
+
IDLEWATCH_INTERVAL_MS=10000
|
|
6
|
+
|
|
7
|
+
# Optional macOS LaunchAgent install settings
|
|
8
|
+
# IDLEWATCH_APP_PATH=/Applications/IdleWatch.app
|
|
9
|
+
# IDLEWATCH_APP_BIN=/Applications/IdleWatch.app/Contents/MacOS/IdleWatch
|
|
10
|
+
# IDLEWATCH_LAUNCH_AGENT_LABEL=com.idlewatch.agent
|
|
11
|
+
# IDLEWATCH_LAUNCH_AGENT_PLIST_ROOT=$HOME/Library/LaunchAgents
|
|
12
|
+
# IDLEWATCH_LAUNCH_AGENT_LOG_DIR=$HOME/Library/Logs/IdleWatch
|
|
13
|
+
|
|
14
|
+
# Optional durable local telemetry log path (NDJSON)
|
|
15
|
+
IDLEWATCH_LOCAL_LOG_PATH=./logs/idlewatch-metrics.ndjson
|
|
16
|
+
|
|
17
|
+
# OpenClaw usage integration mode: auto|off
|
|
18
|
+
IDLEWATCH_OPENCLAW_USAGE=auto
|
|
19
|
+
|
|
20
|
+
# Optional explicit OpenClaw binary path (recommended for packaged/non-interactive runtime)
|
|
21
|
+
# IDLEWATCH_OPENCLAW_BIN=/opt/homebrew/bin/openclaw
|
|
22
|
+
# Optional strict probe mode to avoid system probing and force only IDLEWATCH_OPENCLAW_BIN path:
|
|
23
|
+
# IDLEWATCH_OPENCLAW_BIN_STRICT=1
|
|
24
|
+
# Legacy compatibility hint (kept for older launchers):
|
|
25
|
+
# IDLEWATCH_OPENCLAW_BIN_HINT=/opt/homebrew/bin/openclaw
|
|
26
|
+
# IDLEWATCH_NODE_BIN=/opt/homebrew/bin/node
|
|
27
|
+
# Optional portable Node runtime directory to bundle in IdleWatch.app (must contain bin/node)
|
|
28
|
+
# IDLEWATCH_NODE_RUNTIME_DIR=/opt/node-v20.18.0-darwin-arm64
|
|
29
|
+
# Optional per-probe OpenClaw command timeout (ms)
|
|
30
|
+
# IDLEWATCH_OPENCLAW_PROBE_TIMEOUT_MS=2500
|
|
31
|
+
# Optional extra probe sweeps after first pass (helps with transient CLI failures)
|
|
32
|
+
# IDLEWATCH_OPENCLAW_PROBE_RETRIES=1
|
|
33
|
+
|
|
34
|
+
# Optional usage freshness tuning windows (ms)
|
|
35
|
+
# IDLEWATCH_USAGE_STALE_MS=60000
|
|
36
|
+
# IDLEWATCH_USAGE_NEAR_STALE_MS=59500
|
|
37
|
+
# IDLEWATCH_USAGE_STALE_GRACE_MS=10000
|
|
38
|
+
# Optional forced stale-threshold reprobe controls
|
|
39
|
+
# IDLEWATCH_USAGE_REFRESH_REPROBES=1
|
|
40
|
+
# IDLEWATCH_USAGE_REFRESH_DELAY_MS=250
|
|
41
|
+
# IDLEWATCH_USAGE_REFRESH_ON_NEAR_STALE=1
|
|
42
|
+
# IDLEWATCH_USAGE_IDLE_AFTER_MS=21600000
|
|
43
|
+
# IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS=120000
|
|
44
|
+
# Optional persistent last-good usage snapshot path (supports restart recovery)
|
|
45
|
+
# IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH=./logs/openclaw-last-good.json
|
|
46
|
+
# Optional validation gate override for max acceptable OpenClaw usage age (ms)
|
|
47
|
+
# Used by schema validators (e.g., validate:packaged-usage-age-slo, trusted release gate)
|
|
48
|
+
# IDLEWATCH_MAX_OPENCLAW_USAGE_AGE_MS=300000
|
|
49
|
+
|
|
50
|
+
# Enforce signed + notarized packaging scripts (1=true, 0=false)
|
|
51
|
+
# Requires MACOS_CODESIGN_IDENTITY and MACOS_NOTARY_PROFILE for package:dmg
|
|
52
|
+
# IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1
|
|
53
|
+
# Break-glass CI bypass for tag refs (refs/tags/*); default guard blocks unsigned tag packaging
|
|
54
|
+
# IDLEWATCH_ALLOW_UNSIGNED_TAG_RELEASE=1
|
|
55
|
+
|
|
56
|
+
# Firebase settings
|
|
57
|
+
FIREBASE_PROJECT_ID=your-firebase-project-id
|
|
58
|
+
|
|
59
|
+
# Preferred (production): path to service-account JSON file generated for least-privilege IdleWatch writer
|
|
60
|
+
# FIREBASE_SERVICE_ACCOUNT_FILE=~/Library/Application Support/IdleWatch/credentials/my-project-service-account.json
|
|
61
|
+
|
|
62
|
+
# Supported alternative: raw JSON service account in one line (less secure for shell history/process snapshots)
|
|
63
|
+
# Example: {"type":"service_account",...}
|
|
64
|
+
# FIREBASE_SERVICE_ACCOUNT_JSON=
|
|
65
|
+
|
|
66
|
+
# Legacy alternative: base64-encoded service account JSON
|
|
67
|
+
# FIREBASE_SERVICE_ACCOUNT_B64=
|
|
68
|
+
|
|
69
|
+
# Optional strict one-shot publish requirement (fails --once if write path is not configured/working)
|
|
70
|
+
# IDLEWATCH_REQUIRE_FIREBASE_WRITES=1
|
|
71
|
+
|
|
72
|
+
# Optional emulator mode (no service-account credentials required)
|
|
73
|
+
# FIRESTORE_EMULATOR_HOST=127.0.0.1:8080
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
node-tests:
|
|
9
|
+
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
include:
|
|
14
|
+
- os: ubuntu-latest
|
|
15
|
+
node-version: 20
|
|
16
|
+
- os: ubuntu-latest
|
|
17
|
+
node-version: 22
|
|
18
|
+
- os: macos-latest
|
|
19
|
+
node-version: 22
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Setup Node
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: ${{ matrix.node-version }}
|
|
28
|
+
|
|
29
|
+
- name: Install
|
|
30
|
+
run: npm install
|
|
31
|
+
|
|
32
|
+
- name: Validate package bin entries
|
|
33
|
+
run: npm run validate:bin
|
|
34
|
+
|
|
35
|
+
- name: Lint (if present)
|
|
36
|
+
run: npm run lint --if-present
|
|
37
|
+
|
|
38
|
+
- name: Unit tests
|
|
39
|
+
run: npm run test:unit
|
|
40
|
+
|
|
41
|
+
- name: Smoke: CLI help
|
|
42
|
+
run: npm run smoke:help
|
|
43
|
+
|
|
44
|
+
- name: Smoke: dry run
|
|
45
|
+
run: npm run smoke:dry-run
|
|
46
|
+
|
|
47
|
+
- name: Validate dry-run telemetry schema
|
|
48
|
+
run: npm run validate:dry-run-schema
|
|
49
|
+
|
|
50
|
+
- name: Validate usage freshness long-window transitions
|
|
51
|
+
run: npm run validate:usage-freshness-e2e
|
|
52
|
+
|
|
53
|
+
- name: Validate usage alert-rate quality gate
|
|
54
|
+
run: npm run validate:usage-alert-rate-e2e
|
|
55
|
+
|
|
56
|
+
- name: Validate OpenClaw host release gates (usage health + stats + stale cache recovery)
|
|
57
|
+
run: npm run validate:release-gate-all --silent
|
|
58
|
+
|
|
59
|
+
macos-packaging-smoke:
|
|
60
|
+
name: macOS packaging scaffold smoke
|
|
61
|
+
runs-on: macos-latest
|
|
62
|
+
steps:
|
|
63
|
+
- name: Checkout
|
|
64
|
+
uses: actions/checkout@v4
|
|
65
|
+
|
|
66
|
+
- name: Setup Node
|
|
67
|
+
uses: actions/setup-node@v4
|
|
68
|
+
with:
|
|
69
|
+
node-version: 22
|
|
70
|
+
|
|
71
|
+
- name: Install
|
|
72
|
+
run: npm install
|
|
73
|
+
|
|
74
|
+
- name: Build macOS app scaffold
|
|
75
|
+
run: npm run package:macos
|
|
76
|
+
|
|
77
|
+
- name: Validate packaged metadata
|
|
78
|
+
run: npm run validate:packaged-metadata
|
|
79
|
+
|
|
80
|
+
- name: App scaffold dry-run
|
|
81
|
+
run: ./dist/IdleWatch.app/Contents/MacOS/IdleWatch --dry-run
|
|
82
|
+
|
|
83
|
+
- name: Validate packaged app dry-run telemetry schema
|
|
84
|
+
run: npm run validate:packaged-dry-run-schema:reuse-artifact
|
|
85
|
+
|
|
86
|
+
- name: Validate packaged bundled-runtime launcher path (no PATH node)
|
|
87
|
+
run: npm run validate:packaged-bundled-runtime
|
|
88
|
+
|
|
89
|
+
- name: Validate packaged OpenClaw robustness gate (age SLO + alert-rate + probe-noise + release)
|
|
90
|
+
run: npm run validate:packaged-openclaw-robustness:reuse-artifact --silent
|
|
91
|
+
|
|
92
|
+
- name: Build unsigned DMG
|
|
93
|
+
run: npm run package:dmg
|
|
94
|
+
|
|
95
|
+
- name: Validate DMG checksum
|
|
96
|
+
run: npm run validate:dmg-checksum
|
|
97
|
+
|
|
98
|
+
- name: Validate DMG install + launcher dry-run schema
|
|
99
|
+
run: npm run validate:dmg-install
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
name: Release macOS trusted artifact
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- 'v*'
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
macos-trusted-release:
|
|
11
|
+
name: macOS signed + notarized DMG
|
|
12
|
+
runs-on: macos-latest
|
|
13
|
+
env:
|
|
14
|
+
CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }}
|
|
15
|
+
P12_B64: ${{ secrets.APPLE_DEVELOPER_ID_APP_P12_BASE64 }}
|
|
16
|
+
P12_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_APP_P12_PASSWORD }}
|
|
17
|
+
KEYCHAIN_PASSWORD: ${{ secrets.APPLE_BUILD_KEYCHAIN_PASSWORD }}
|
|
18
|
+
NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
|
|
19
|
+
NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
|
|
20
|
+
NOTARY_API_KEY_P8: ${{ secrets.APPLE_NOTARY_API_KEY_P8 }}
|
|
21
|
+
steps:
|
|
22
|
+
- name: Assert required release secrets
|
|
23
|
+
run: |
|
|
24
|
+
required=(CODESIGN_IDENTITY P12_B64 P12_PASSWORD KEYCHAIN_PASSWORD NOTARY_KEY_ID NOTARY_ISSUER_ID NOTARY_API_KEY_P8)
|
|
25
|
+
missing=0
|
|
26
|
+
for name in "${required[@]}"; do
|
|
27
|
+
if [[ -z "${!name:-}" ]]; then
|
|
28
|
+
echo "Missing required secret/env: $name" >&2
|
|
29
|
+
missing=1
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
if [[ "$missing" -ne 0 ]]; then
|
|
33
|
+
echo "Configure required GitHub Actions secrets before running trusted release workflow." >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
- name: Checkout
|
|
38
|
+
uses: actions/checkout@v4
|
|
39
|
+
|
|
40
|
+
- name: Setup Node
|
|
41
|
+
uses: actions/setup-node@v4
|
|
42
|
+
with:
|
|
43
|
+
node-version: 22
|
|
44
|
+
|
|
45
|
+
- name: Install dependencies
|
|
46
|
+
run: npm ci
|
|
47
|
+
|
|
48
|
+
- name: Run tests
|
|
49
|
+
run: npm test --silent
|
|
50
|
+
|
|
51
|
+
- name: Import Developer ID certificate into temporary keychain
|
|
52
|
+
run: |
|
|
53
|
+
KEYCHAIN_PATH="$RUNNER_TEMP/idlewatch-build.keychain-db"
|
|
54
|
+
CERT_PATH="$RUNNER_TEMP/developer-id-application.p12"
|
|
55
|
+
python - <<'PY'
|
|
56
|
+
import base64, os, pathlib
|
|
57
|
+
cert_b64 = os.environ['P12_B64']
|
|
58
|
+
out = pathlib.Path(os.environ['RUNNER_TEMP']) / 'developer-id-application.p12'
|
|
59
|
+
out.write_bytes(base64.b64decode(cert_b64))
|
|
60
|
+
print(f'wrote {out}')
|
|
61
|
+
PY
|
|
62
|
+
|
|
63
|
+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
64
|
+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
|
65
|
+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
66
|
+
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$P12_PASSWORD" -T /usr/bin/codesign
|
|
67
|
+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
68
|
+
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/[\"]//g')
|
|
69
|
+
security default-keychain -s "$KEYCHAIN_PATH"
|
|
70
|
+
|
|
71
|
+
- name: Configure notarytool profile
|
|
72
|
+
run: |
|
|
73
|
+
KEY_PATH="$RUNNER_TEMP/AuthKey_${NOTARY_KEY_ID}.p8"
|
|
74
|
+
printf "%s" "$NOTARY_API_KEY_P8" > "$KEY_PATH"
|
|
75
|
+
xcrun notarytool store-credentials idlewatch-ci \
|
|
76
|
+
--key "$KEY_PATH" \
|
|
77
|
+
--key-id "$NOTARY_KEY_ID" \
|
|
78
|
+
--issuer "$NOTARY_ISSUER_ID"
|
|
79
|
+
|
|
80
|
+
- name: Build signed app + notarized DMG
|
|
81
|
+
env:
|
|
82
|
+
MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }}
|
|
83
|
+
MACOS_NOTARY_PROFILE: idlewatch-ci
|
|
84
|
+
run: |
|
|
85
|
+
npm run package:macos --silent
|
|
86
|
+
npm run package:dmg --silent
|
|
87
|
+
|
|
88
|
+
- name: Validate host OpenClaw release gate
|
|
89
|
+
run: npm run validate:openclaw-release-gates --silent
|
|
90
|
+
|
|
91
|
+
- name: Enforce packaged OpenClaw release gates (reuse artifact)
|
|
92
|
+
env:
|
|
93
|
+
IDLEWATCH_MAX_OPENCLAW_USAGE_AGE_MS: '300000'
|
|
94
|
+
MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }}
|
|
95
|
+
MACOS_NOTARY_PROFILE: idlewatch-ci
|
|
96
|
+
run: npm run validate:packaged-openclaw-release-gates:reuse-artifact --silent
|
|
97
|
+
|
|
98
|
+
- name: Upload trusted DMG artifact
|
|
99
|
+
uses: actions/upload-artifact@v4
|
|
100
|
+
with:
|
|
101
|
+
name: idlewatch-macos-dmg-trusted
|
|
102
|
+
path: dist/IdleWatch-*-signed.dmg
|
|
103
|
+
if-no-files-found: error
|
package/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# idlewatch-skill
|
|
2
|
+
|
|
3
|
+
Telemetry collector for IdleWatch.
|
|
4
|
+
|
|
5
|
+
## Install / Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm start
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
With npx (after publish):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx idlewatch-skill --help
|
|
16
|
+
npx idlewatch-skill quickstart
|
|
17
|
+
npx idlewatch-skill --dry-run
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`quickstart` runs a first-run setup wizard that writes a local env file and (for production mode) stores a locked-down copy of the service-account key under `~/.idlewatch/`. On hosts with Rust/Cargo available, quickstart launches a ratatui-powered onboarding flow first; otherwise it falls back to the text wizard.
|
|
21
|
+
|
|
22
|
+
## CLI options
|
|
23
|
+
|
|
24
|
+
- `quickstart`: run first-run enrollment wizard
|
|
25
|
+
- `--help`: show usage
|
|
26
|
+
- `--dry-run`: collect one sample and exit (no Firebase write)
|
|
27
|
+
- `--once`: collect one sample, publish to Firebase when configured, then exit
|
|
28
|
+
|
|
29
|
+
## Reliability improvements
|
|
30
|
+
|
|
31
|
+
- Local NDJSON durability log at `~/.idlewatch/logs/<host>-metrics.ndjson` (override via `IDLEWATCH_LOCAL_LOG_PATH`)
|
|
32
|
+
- Retry-once+ for transient Firestore write failures
|
|
33
|
+
- Non-overlapping scheduler loop (prevents concurrent sample overlap when host is busy)
|
|
34
|
+
- Non-blocking CPU sampling using per-tick CPU deltas (no `Atomics.wait` stall)
|
|
35
|
+
- Darwin GPU probing fallback chain (AGX/IOGPU `ioreg` → `powermetrics` → `top` grep) with provenance fields (`gpuSource`, `gpuConfidence`, `gpuSampleWindowMs`)
|
|
36
|
+
- macOS memory pressure enrichment via `memory_pressure -Q` (`memPressurePct`, `memPressureClass`, `source.memPressureSource`)
|
|
37
|
+
|
|
38
|
+
## macOS GPU support matrix (observed)
|
|
39
|
+
|
|
40
|
+
The collector is tuned for these macOS probe paths by platform:
|
|
41
|
+
|
|
42
|
+
- Apple Silicon (AGX/iGPU): prefer `ioreg` performance statistics (`AGX`) first for live GPU utilization.
|
|
43
|
+
- Intel Macs: prefer `powermetrics` if permission profile allows; fall back to `top` parser.
|
|
44
|
+
- Unsupported hosts / older macOS: emit `gpuSource: "unavailable"` with `gpuConfidence: "none"` and clear source metadata.
|
|
45
|
+
|
|
46
|
+
Use `gpuSource` + `gpuConfidence` in dashboards to decide whether to trust values:
|
|
47
|
+
- `high`: authoritative per-command path for host class
|
|
48
|
+
- `medium`: derived/proxied path with best-effort parsing
|
|
49
|
+
- `low`: constrained probe path
|
|
50
|
+
- `none`: no usable sample for that sample window
|
|
51
|
+
|
|
52
|
+
## Firebase wiring
|
|
53
|
+
|
|
54
|
+
### Recommended: guided enrollment (external users)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx idlewatch-skill quickstart
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The wizard supports:
|
|
61
|
+
- **Production mode**: prompts for project id + service-account JSON file path, validates it, copies credentials to a local secure path (0600), and writes `FIREBASE_SERVICE_ACCOUNT_FILE=...`.
|
|
62
|
+
- **Emulator mode**: writes `FIREBASE_PROJECT_ID` + `FIRESTORE_EMULATOR_HOST` only.
|
|
63
|
+
- **Local-only mode**: writes no Firebase credentials.
|
|
64
|
+
|
|
65
|
+
Then load generated env and run:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
set -a; source "$HOME/.idlewatch/idlewatch.env"; set +a
|
|
69
|
+
idlewatch-agent --once
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Manual wiring
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export FIREBASE_PROJECT_ID=your-project
|
|
76
|
+
export FIREBASE_SERVICE_ACCOUNT_FILE="$HOME/.idlewatch/credentials/your-project-service-account.json"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Raw JSON and base64 are still supported for compatibility, but **file-path credentials are preferred**:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export FIREBASE_SERVICE_ACCOUNT_JSON='{"type":"service_account",...}'
|
|
83
|
+
# or
|
|
84
|
+
export FIREBASE_SERVICE_ACCOUNT_B64=$(base64 -i serviceAccount.json)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Firestore emulator mode (no service-account JSON required):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
export FIREBASE_PROJECT_ID=idlewatch-dev
|
|
91
|
+
export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Least-privilege guidance:
|
|
95
|
+
- Create a dedicated IdleWatch writer service account per environment/project.
|
|
96
|
+
- Grant only the Firestore write scope needed for `metrics` ingestion (avoid Owner/Editor roles).
|
|
97
|
+
- Store credentials as a file with user-only permissions (`chmod 600`) and reference via `FIREBASE_SERVICE_ACCOUNT_FILE`.
|
|
98
|
+
|
|
99
|
+
If Firebase env vars are incomplete or invalid, the CLI exits with a clear configuration error.
|
|
100
|
+
If Firebase vars are omitted entirely, it runs in local-only mode and prints telemetry to stdout.
|
|
101
|
+
`firebase-admin` is loaded lazily only when Firebase publish mode is configured, so dry-run/local-only flows remain resilient in minimal packaged/runtime environments.
|
|
102
|
+
Set `IDLEWATCH_REQUIRE_FIREBASE_WRITES=1` to fail fast when running `--once` without a working Firebase publish path.
|
|
103
|
+
|
|
104
|
+
Validation helpers:
|
|
105
|
+
- `npm run validate:onboarding` validates non-interactive quickstart enrollment output (env + secure credential copy).
|
|
106
|
+
- `npm run validate:firebase-emulator-mode` verifies emulator-only config wiring in dry-run mode.
|
|
107
|
+
- `npm run validate:firebase-write-once` performs a single real write attempt (use with emulator or production credentials).
|
|
108
|
+
- `npm run validate:firebase-write-required-once` is the strict variant and fails fast unless a Firebase write path is configured and successful.
|
|
109
|
+
- `npm run validate:openclaw-usage-health` validates that dry-run telemetry stays on `source.usage=openclaw` with healthy integration/ingestion in OpenClaw-required mode (mocked CLI probe path).
|
|
110
|
+
- `npm run validate:openclaw-stats-ingestion` validates `openclaw stats --json`-only payload ingestion (mocked CLI probe fallback path), covering `status.result.stats.current`, `status.current.stats.current`, and adjacent legacy variants, including millisecond timestamp aliases (`usage_ts_ms`, `usage_timestamp_ms`, `updated_at_ms`, `ts_ms`).
|
|
111
|
+
- `npm run validate:openclaw-release-gates` validates host OpenClaw checks (`validate:openclaw-usage-health`, `validate:openclaw-stats-ingestion`, and `validate:openclaw-cache-recovery-e2e`) in one gate.
|
|
112
|
+
- `npm run validate:openclaw-release-gates:all` runs host OpenClaw checks, and on macOS also appends packaged reuse checks (`validate:packaged-openclaw-release-gates:reuse-artifact`) before proceeding.
|
|
113
|
+
- `npm run validate:packaged-artifact` validates a reusable `dist/IdleWatch.app` before running any `:reuse-artifact` validator (metadata integrity, launcher executability, and matching source commit with current `HEAD` by default).
|
|
114
|
+
- Disable commit matching for one-off local experiments by setting `IDLEWATCH_REQUIRE_SOURCE_COMMIT_MATCH=0`.
|
|
115
|
+
- If the artifact lacks `sourceGitCommit`, validation fails fast in strict mode; set `IDLEWATCH_ALLOW_LEGACY_SOURCE_GIT_COMMIT=1` only as a temporary compatibility bridge while you repackage.
|
|
116
|
+
- If the reuse gate requires bundled runtime, use `npm run validate:packaged-artifact:bundled-runtime`.
|
|
117
|
+
- `npm run validate:packaged-openclaw-stats-ingestion` validates packaged-app stats fallback ingestion under a mocked `openclaw` binary (end-to-end packaged dry-run + `stats --json` command selection), including `status.result`, `status.current`, and timestamp alias payload variants (`usage_ts_ms`, `usage_timestamp_ms`, `usage_timestamp`, `usageTime`, `updated_at_ms`, `ts_ms`, `ts`).
|
|
118
|
+
- `npm run validate:packaged-openclaw-cache-recovery-e2e` validates packaged-app stale-cache recovery behavior with temporary probe failures and reprobe refresh logic.
|
|
119
|
+
- `npm run validate:packaged-openclaw-release-gates` validates `validate:packaged-usage-health`, `validate:packaged-openclaw-stats-ingestion`, and `validate:packaged-openclaw-cache-recovery-e2e` together as one release gate.
|
|
120
|
+
- `npm run validate:packaged-openclaw-release-gates:all` runs both fresh-package and reuse-artifact OpenClaw packaged checks (for local validation when packaging cost is acceptable).
|
|
121
|
+
- `npm run validate:packaged-openclaw-release-gates:reuse-artifact` validates the same three checks against an already-packaged artifact (`IDLEWATCH_SKIP_PACKAGE_MACOS=1`) and is the command used in CI/release smoke for repeatable execution.
|
|
122
|
+
- `npm run validate:packaged-openclaw-release-gates:reuse-artifact` and all packaged `:reuse-artifact` validators now run through `validate:packaged-artifact` first, so stale artifacts from older commits fail fast with a rebuild hint.
|
|
123
|
+
- `npm run validate:packaged-openclaw-robustness` runs the full packaged resilience slice in one command (`packaged-usage-age-slo`, `packaged-usage-alert-rate-e2e`, `packaged-usage-probe-noise-e2e`, `packaged-openclaw-release-gates`).
|
|
124
|
+
- `npm run validate:packaged-openclaw-robustness:reuse-artifact` runs the same resilience slice against an already-packaged artifact.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## OpenClaw usage ingestion (best effort)
|
|
128
|
+
|
|
129
|
+
By default (`IDLEWATCH_OPENCLAW_USAGE=auto`), the agent attempts to read OpenClaw
|
|
130
|
+
session usage from local CLI JSON endpoints when available, then enriches samples with.
|
|
131
|
+
|
|
132
|
+
Binary resolution order for the OpenClaw probe:
|
|
133
|
+
1. `IDLEWATCH_OPENCLAW_BIN` (if set)
|
|
134
|
+
2. `IDLEWATCH_OPENCLAW_BIN_HINT` (legacy packaged-launcher hint)
|
|
135
|
+
3. `/opt/homebrew/bin/openclaw`
|
|
136
|
+
4. `/usr/local/bin/openclaw`
|
|
137
|
+
5. `openclaw` (PATH lookup)
|
|
138
|
+
|
|
139
|
+
- Successful probe command/args are cached for the life of the process so subsequent samples and forced stale-threshold refreshes reuse the known-good command first before full probe sweep.
|
|
140
|
+
|
|
141
|
+
- `IDLEWATCH_OPENCLAW_BIN` optionally pins the exact OpenClaw binary path for packaged/non-interactive runtimes.
|
|
142
|
+
- `IDLEWATCH_OPENCLAW_BIN_STRICT=1` (optional) limits probing to only the explicit bin above when set, useful for deterministic tests. If `IDLEWATCH_OPENCLAW_BIN` is unset, `IDLEWATCH_OPENCLAW_BIN_HINT` is used as the explicit fallback in strict mode for launcher compatibility.
|
|
143
|
+
- `IDLEWATCH_OPENCLAW_BIN_HINT` is also supported for launcher compatibility in existing packaged flows.
|
|
144
|
+
- `IDLEWATCH_NODE_BIN` optionally pins the Node binary used by packaged app launcher (`IdleWatch.app`).
|
|
145
|
+
- `IDLEWATCH_NODE_RUNTIME_DIR` optionally bundles a portable Node runtime into `IdleWatch.app` (`<runtime>/bin/node` required) so installed apps can run on hosts without a global Node install.
|
|
146
|
+
- OpenClaw probe command preference used by the agent (first successful parse wins):
|
|
147
|
+
1) `status --json`
|
|
148
|
+
2) `usage --json`
|
|
149
|
+
3) `session status --json`
|
|
150
|
+
4) `session_status --json`
|
|
151
|
+
5) `stats --json` (fallback compatibility for CLI variants)
|
|
152
|
+
(default: `max(IDLEWATCH_INTERVAL_MS*3, 60000)`).
|
|
153
|
+
- `IDLEWATCH_USAGE_NEAR_STALE_MS` controls "aging" classification before stale
|
|
154
|
+
(default: `floor((IDLEWATCH_USAGE_STALE_MS + IDLEWATCH_USAGE_STALE_GRACE_MS)*0.85)`).
|
|
155
|
+
- `IDLEWATCH_USAGE_STALE_GRACE_MS` adds a grace window before `usageIntegrationStatus`
|
|
156
|
+
flips to `stale` (default: `min(IDLEWATCH_INTERVAL_MS, 10000)`).
|
|
157
|
+
- `IDLEWATCH_OPENCLAW_MAX_OUTPUT_BYTES` caps OpenClaw probe output capture size for each command
|
|
158
|
+
(default: `2097152` / 2MB). Increasing helps on noisy terminals, reducing ENOBUFS-like parse failures.
|
|
159
|
+
- `IDLEWATCH_OPENCLAW_MAX_OUTPUT_BYTES_HARD_CAP` sets the upper ceiling for adaptive output-buffer retries when
|
|
160
|
+
the command output exceeds the base cap (default: `16777216` / 16MB). Keep this conservative in shared/legacy
|
|
161
|
+
environments to avoid memory spikes from runaway process logs.
|
|
162
|
+
- Probe output parsing merges both stdout and stderr in the probe collector, so mixed-output CLIs
|
|
163
|
+
that print progress in stdout but emit JSON in stderr still parse successfully in one sweep.
|
|
164
|
+
- `IDLEWATCH_OPENCLAW_PROBE_RETRIES` retries full OpenClaw probe sweeps after the first pass
|
|
165
|
+
to reduce transient command failures (default: `1`).
|
|
166
|
+
- `IDLEWATCH_USAGE_REFRESH_REPROBES` controls how many extra forced uncached reprobes run
|
|
167
|
+
after crossing stale threshold (default: `1`, total attempts = reprobes + initial refresh).
|
|
168
|
+
- `IDLEWATCH_USAGE_REFRESH_DELAY_MS` waits between forced stale-threshold reprobes
|
|
169
|
+
(default: `250`).
|
|
170
|
+
- `IDLEWATCH_USAGE_REFRESH_ON_NEAR_STALE` triggers proactive refresh when usage is near-stale
|
|
171
|
+
to reduce stale flips in long packaging/QA loops (default: `1`).
|
|
172
|
+
- `IDLEWATCH_USAGE_IDLE_AFTER_MS` downgrades stale activity alerts to `activity-idle`
|
|
173
|
+
- stale usage after a failed freshness refresh now downgrades to `activity-no-new-usage` (ingestion healthy, but no newer usage observed)
|
|
174
|
+
notice state after prolonged inactivity (default: `21600000` = 6h).
|
|
175
|
+
- `IDLEWATCH_OPENCLAW_LAST_GOOD_MAX_AGE_MS` reuses the last successful OpenClaw usage
|
|
176
|
+
snapshot after transient probe failures for up to this age (default: `max(stale+grace, 120000)`).
|
|
177
|
+
- `IDLEWATCH_OPENCLAW_LAST_GOOD_CACHE_PATH` persists/reuses the last-good OpenClaw usage snapshot
|
|
178
|
+
across process restarts (default: OS temp dir path keyed by host).
|
|
179
|
+
|
|
180
|
+
- `tokensPerMin`: explicit rate if available from OpenClaw, otherwise derived from `totalTokens / ageMinutes` for the selected recent session.
|
|
181
|
+
- `openclawModel`: active model name (from the selected recent session or defaults).
|
|
182
|
+
- `openclawTotalTokens`: total tokens for the selected recent session.
|
|
183
|
+
- `openclawSessionId`, `openclawAgentId`, `openclawUsageTs`: stable identifiers + timestamp alignment fields.
|
|
184
|
+
- `openclawUsageAgeMs`: derived age of usage snapshot (`sampleTs - openclawUsageTs`) when available, where `sampleTs` is the end-of-sample collector timestamp.
|
|
185
|
+
- `ts` and fleet `collectedAtMs` are the same collection timestamp (end of the collect cycle), which is typically a few milliseconds after final probe completion.
|
|
186
|
+
|
|
187
|
+
Selection logic for `openclaw status --json`:
|
|
188
|
+
1. Pick the most recently updated session among entries with non-null `totalTokens` and `totalTokensFresh !== false`.
|
|
189
|
+
2. Fallback to the most recently updated session among entries with non-null tokens.
|
|
190
|
+
3. Fallback to the most recently updated session entry.
|
|
191
|
+
|
|
192
|
+
Source metadata fields:
|
|
193
|
+
- `source.usage`: `openclaw | disabled | unavailable`
|
|
194
|
+
- `source.usageIntegrationStatus`: `ok | stale | disabled | unavailable`
|
|
195
|
+
- `source.usageIngestionStatus`: `ok | disabled | unavailable` (probe/path health independent of usage age).
|
|
196
|
+
- `source.usageActivityStatus`: `fresh | aging | stale | unknown | disabled | unavailable` (age-based activity state).
|
|
197
|
+
- `source.usageAlertLevel`: `ok | notice | warning | critical | off` (operator-facing alert severity derived from ingestion + activity semantics).
|
|
198
|
+
- `source.usageAlertReason`: `healthy | activity-idle | activity-near-stale | activity-past-threshold | activity-stale | activity-no-new-usage | ingestion-unavailable | usage-disabled`.
|
|
199
|
+
- `source.usageFreshnessState`: `fresh | aging | stale | unknown`
|
|
200
|
+
- `source.usageNearStale`: boolean early warning signal when age crosses near-stale threshold.
|
|
201
|
+
- `source.usagePastStaleThreshold`: boolean showing age crossed stale threshold (before grace).
|
|
202
|
+
- `source.usageRefreshAttempted`: true when collector forced stale-threshold refresh logic.
|
|
203
|
+
- `source.usageRefreshRecovered`: true when forced refresh recovered below stale-threshold crossing.
|
|
204
|
+
- `source.usageRefreshAttempts`: number of forced refresh attempts actually executed.
|
|
205
|
+
- `source.usageRefreshReprobes`: configured extra forced reprobes (`IDLEWATCH_USAGE_REFRESH_REPROBES`).
|
|
206
|
+
- `source.usageRefreshDelayMs`: configured delay between reprobes (`IDLEWATCH_USAGE_REFRESH_DELAY_MS`).
|
|
207
|
+
- `source.usageRefreshDurationMs`: total elapsed ms spent in the stale-threshold/proactive refresh path when triggered.
|
|
208
|
+
- `source.usageRefreshOnNearStale`: whether near-stale proactive refresh is enabled (`IDLEWATCH_USAGE_REFRESH_ON_NEAR_STALE`).
|
|
209
|
+
- `source.usageIdle`: boolean indicating usage age crossed idle window (`IDLEWATCH_USAGE_IDLE_AFTER_MS`).
|
|
210
|
+
- `source.usageCommand`: command used (`openclaw status --json`, etc.)
|
|
211
|
+
|
|
212
|
+
OpenClaw parsing hardened in this release:
|
|
213
|
+
- stringified numeric fields (for example `"totalTokens": "12345"` or `"updatedAt": "1771278999999"`) are now accepted
|
|
214
|
+
- mixed timestamp names, epoch-seconds variants (`1771278800`), and alternate session container keys are supported
|
|
215
|
+
- wrapped status payload shapes (`result` root object, `data.result` wrappers, top-level `sessions` array, nested usage totals/`totals` object) are supported with precedence-aware session selection
|
|
216
|
+
- timestamp aliases in both `snake_case` and millis variants are normalized (for example `usage_ts`, `usage_ts_ms`, `usage_timestamp`, `usage_timestamp_ms`, `updated_at_ms`, `ts_ms`) so parser keeps working across CLI serializers
|
|
217
|
+
- direct session object payloads (`session`, `activeSession`, `currentSession`) are now handled alongside array/map forms
|
|
218
|
+
- sessions as arrays are supported (for example `status.stats.current.sessions`) in addition to map/object `sessions` containers
|
|
219
|
+
- sessions maps keyed by session id are supported (`sessions` as object map) to avoid regressions on alternate OpenClaw serializers
|
|
220
|
+
- metadata keys like `sessions.defaults` are ignored during session-map selection so tokenized sessions are not shadowed by defaults payloads
|
|
221
|
+
- stale-token markers like `"totalTokensFresh": "false"` are correctly interpreted as freshness metadata rather than causing parser failure
|
|
222
|
+
- `source.usageProbeResult`: `ok | fallback-cache | disabled | command-missing | command-error | parse-error | unavailable`.
|
|
223
|
+
- `source.usageProbeAttempts`: number of probe attempts in the current refresh window.
|
|
224
|
+
- `source.usageProbeSweeps`: number of probe sweeps performed in the current refresh window.
|
|
225
|
+
- `source.usageProbeRetries`: configured retry count (`IDLEWATCH_OPENCLAW_PROBE_RETRIES`).
|
|
226
|
+
- `source.usageProbeError`: compact failure reason when probing fails.
|
|
227
|
+
- `source.usageUsedFallbackCache`: boolean indicating whether last-good usage cache was used this sample.
|
|
228
|
+
- `source.usageFallbackCacheAgeMs`: age of fallback cache snapshot when used, otherwise `null`.
|
|
229
|
+
- `source.usageFallbackCacheSource`: `memory | disk | null` indicating fallback cache origin.
|
|
230
|
+
- `source.usageStaleMsThreshold`: threshold used for stale classification.
|
|
231
|
+
- `source.usageNearStaleMsThreshold`: threshold used for aging classification.
|
|
232
|
+
- `source.usageStaleGraceMs`: grace window before stale status activation.
|
|
233
|
+
- `source.usageIdleAfterMsThreshold`: threshold used to classify prolonged inactivity.
|
|
234
|
+
- `source.memPressureSource`: `memory_pressure | unavailable | unsupported`.
|
|
235
|
+
|
|
236
|
+
Memory field semantics:
|
|
237
|
+
- `memPct` and `memUsedPct`: host memory used percent (`(total - free) / total`) retained for backward compatibility.
|
|
238
|
+
- `memPressurePct`: macOS pressure estimate derived from `memory_pressure -Q` output when available.
|
|
239
|
+
- `memPressureClass`: `normal | warning | critical | unavailable` using thresholds `<75`, `75-89.99`, `>=90`.
|
|
240
|
+
|
|
241
|
+
Alerting guidance (recommended):
|
|
242
|
+
- Prefer `memPressureClass` as the primary memory alert signal.
|
|
243
|
+
- Suggested warning threshold: trigger when `memPressureClass=warning` for 3+ consecutive samples.
|
|
244
|
+
- Suggested critical threshold: trigger immediately when `memPressureClass=critical`, or when `memPressurePct>=90` for 2+ consecutive samples.
|
|
245
|
+
- Keep `memPct`/`memUsedPct` as informational context only (do not page solely on these).
|
|
246
|
+
- For OpenClaw reliability alerts, page on `source.usageIngestionStatus=unavailable` (or sustained probe failures), not on `usageActivityStatus=stale` alone.
|
|
247
|
+
- If you want one-field routing in dashboards, use `source.usageAlertLevel`: page on `critical`, ticket on sustained `warning`, and keep `notice` informational.
|
|
248
|
+
|
|
249
|
+
Usage field semantics:
|
|
250
|
+
- `openclawTotalTokens`: session-level cumulative total tokens reported by OpenClaw.
|
|
251
|
+
- `tokensPerMin`: reported directly by OpenClaw when available; otherwise derived from `openclawTotalTokens / session age minutes`.
|
|
252
|
+
- Additional parser compatibility covered for data-wrapper/session wrapper stats payloads (`payload`, `data.result`, `data.stats`, `result`), direct `current` payloads (`current`, `data.current`, `result.current`, `status.current`, `payload.current`) and snake_case session aliases under status-like envelopes (for example `current_session`, `active_session`, `session_id`, `agent_id`, `default_model`, `usage_ts`, `recent_sessions`) so common OpenClaw CLI variants map into a stable row shape.
|
|
253
|
+
- Prompt/completion token fields and request/min are **not currently exposed as first-class metrics** in IdleWatch rows; keep `null`/absent rather than synthesizing fake values.
|
|
254
|
+
|
|
255
|
+
If OpenClaw stats are unavailable, usage fields are emitted as `null` and collection continues.
|
|
256
|
+
Set `IDLEWATCH_OPENCLAW_USAGE=off` to disable lookup.
|
|
257
|
+
|
|
258
|
+
## Packaging scaffold
|
|
259
|
+
|
|
260
|
+
DMG release scaffolding is included:
|
|
261
|
+
|
|
262
|
+
- `docs/onboarding-external.md` (external-user quickstart + signed DMG rollout)
|
|
263
|
+
- `docs/packaging/macos-dmg.md`
|
|
264
|
+
- `scripts/package-macos.sh`
|
|
265
|
+
- Produces `dist/IdleWatch.app/Contents/Resources/packaging-metadata.json` with build provenance for QA/supportability.
|
|
266
|
+
- `scripts/build-dmg.sh`
|
|
267
|
+
- `npm run validate:trusted-prereqs` (local preflight for signing identity + notary profile)
|
|
268
|
+
- `npm run validate:dmg-checksum` (verifies SHA-256 checksum generated by `package:dmg`)
|
|
269
|
+
- `npm run package:trusted` (strict signed + notarized local path)
|
|
270
|
+
- `npm run package:release` (trusted packaging + checksum validation in one step)
|
|
271
|
+
- `.github/workflows/release-macos-trusted.yml` (signed + notarized CI path)
|
|
272
|
+
- CI dry-run schema gates via `npm run validate:dry-run-schema` and `npm run validate:packaged-dry-run-schema` (packaged validator auto-rebuilds `IdleWatch.app` first to avoid stale-artifact mismatches)
|
|
273
|
+
- Usage freshness transition gate via `npm run validate:usage-freshness-e2e` (simulates long-window aging→stale transitions end-to-end)
|
|
274
|
+
- Usage alert-rate quality gate via `npm run validate:usage-alert-rate-e2e` (asserts typical low-traffic ages stay `usageAlertLevel=ok`, with deterministic boundary escalation)
|
|
275
|
+
- Packaged usage alert-rate gate via `npm run validate:packaged-usage-alert-rate-e2e` (verifies alert transitions in packaged launcher runtime path)
|
|
276
|
+
- Packaged usage-age SLO gate via `npm run validate:packaged-usage-age-slo` (requires OpenClaw usage and enforces `openclawUsageAgeMs <= 300000` on packaged dry-run)
|
|
277
|
+
- Dry-run gate timeout via `IDLEWATCH_DRY_RUN_TIMEOUT_MS` (default: `15000`)
|
|
278
|
+
- Applied by `scripts/validate-dry-run-schema.mjs` to all `--dry-run` schema checks (direct and packaged).
|
|
279
|
+
- On timeout, validators keep the latest captured row and still validate it when possible, preventing hangs on non-terminating launcher output.
|
|
280
|
+
- Validation parsers now use a shared noise-tolerant JSON extractor (`scripts/lib/telemetry-row-parser.mjs`) that ignores ANSI/control noise and selects the latest valid JSON candidate in mixed stdout/stderr logs.
|
|
281
|
+
- Packaged runtime/DMG validators (`validate:packaged-bundled-runtime`, `validate:dmg-install`) default this to `90000` while release-gate helpers (`validate:openclaw-release-gates` and `validate:packaged-openclaw-release-gates`) default to `60000` for parity with host checks.
|
|
282
|
+
- Packaged stale-threshold recovery gate via `npm run validate:packaged-usage-recovery-e2e` (asserts packaged launcher performs forced reprobe recovery when initial usage age is post-threshold)
|
|
283
|
+
- OpenClaw fallback-cache recovery gate via `npm run validate:openclaw-cache-recovery-e2e` (asserts fallback cache usage with stale age still attempts a forced reprobe and recovers to fresh state when the command comes back)
|
|
284
|
+
- DMG install smoke gate via `npm run validate:dmg-install` (mounts DMG, copies app, validates launcher dry-run schema)
|
|
285
|
+
- Optional portable Node runtime bundling for packaged launcher (`IDLEWATCH_NODE_RUNTIME_DIR=/path/to/runtime` with `<runtime>/bin/node`), enabling resolution order: `IDLEWATCH_NODE_BIN` → bundled runtime → `PATH` (`node`).
|
|
286
|
+
- Runtime copy is now limited to `bin`, `lib`, and `include` directories (with symlink dereference) to keep runtime payloads portable and avoid noise from host-specific completion symlinks.
|
|
287
|
+
- Bundled-runtime packaging gate via `npm run validate:packaged-bundled-runtime` (repackages with a bundled runtime and verifies launcher dry-run succeeds with `PATH=/usr/bin:/bin` where `node` is absent).
|
|
288
|
+
- Background execution lifecycle helpers:
|
|
289
|
+
- `scripts/install-macos-launch-agent.sh`
|
|
290
|
+
- `scripts/uninstall-macos-launch-agent.sh`
|
|
291
|
+
- Install an auto-starting `LaunchAgent` via `IDLEWATCH_APP_PATH`, `IDLEWATCH_LAUNCH_AGENT_LABEL`, `IDLEWATCH_LAUNCH_AGENT_PLIST_ROOT`, and `IDLEWATCH_LAUNCH_AGENT_LOG_DIR`.
|
|
292
|
+
|
|
293
|
+
Strict packaging mode:
|
|
294
|
+
- Set `IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION=1` to hard-fail packaging unless trust prerequisites are configured.
|
|
295
|
+
- In strict mode, `package-macos.sh` requires `MACOS_CODESIGN_IDENTITY`.
|
|
296
|
+
- In strict mode, `build-dmg.sh` requires both `MACOS_CODESIGN_IDENTITY` and `MACOS_NOTARY_PROFILE`.
|
|
297
|
+
- `npm run package:trusted` now runs `npm run validate:trusted-prereqs` first to fail fast when local keychain/notary setup is missing.
|
|
298
|
+
- CI safety guard: tag builds (`refs/tags/*`) now auto-enforce strict trusted requirements even if `IDLEWATCH_REQUIRE_TRUSTED_DISTRIBUTION` is unset.
|
|
299
|
+
- Emergency bypass (explicit): set `IDLEWATCH_ALLOW_UNSIGNED_TAG_RELEASE=1` to allow unsigned tag packaging in CI.
|
|
300
|
+
|
|
301
|
+
Trusted-release workflow required secrets:
|
|
302
|
+
|
|
303
|
+
- `MACOS_CODESIGN_IDENTITY`
|
|
304
|
+
- `APPLE_DEVELOPER_ID_APP_P12_BASE64`
|
|
305
|
+
- `APPLE_DEVELOPER_ID_APP_P12_PASSWORD`
|
|
306
|
+
- `APPLE_BUILD_KEYCHAIN_PASSWORD`
|
|
307
|
+
- `APPLE_NOTARY_KEY_ID`
|
|
308
|
+
- `APPLE_NOTARY_ISSUER_ID`
|
|
309
|
+
- `APPLE_NOTARY_API_KEY_P8`
|
|
310
|
+
|
|
311
|
+
Trusted release workflow policy:
|
|
312
|
+
- OpenClaw usage-health is enforced by default in `.github/workflows/release-macos-trusted.yml` via `npm run validate:packaged-usage-health:reuse-artifact` before artifact upload.
|
|
313
|
+
- The trusted pipeline now also runs `npm run validate:packaged-openclaw-release-gates:reuse-artifact`, which validates: `validate:packaged-usage-health:reuse-artifact`, `validate:packaged-openclaw-stats-ingestion:reuse-artifact`, and `validate:packaged-openclaw-cache-recovery-e2e:reuse-artifact` against the signed artifact before upload (with the wrapper setting `IDLEWATCH_SKIP_PACKAGE_MACOS=1` so checks validate the already-built artifact directly). By default this gate enforces OpenClaw presence (`IDLEWATCH_REQUIRE_OPENCLAW_USAGE=1`) unless explicitly disabled (`0|false|off|no`); set `1|true|on|yes` to force on.
|
|
314
|
+
- Trusted release gate also enforces `IDLEWATCH_MAX_OPENCLAW_USAGE_AGE_MS=300000` to fail fast if packaged usage age is excessively stale.
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
## Reusable OpenClaw release-gate helpers
|
|
318
|
+
|
|
319
|
+
For CI / script chaining, artifact-aware convenience helpers are available:
|
|
320
|
+
|
|
321
|
+
- `npm run validate:packaged-openclaw-release-gates:reuse-artifact`
|
|
322
|
+
- `npm run validate:packaged-openclaw-cache-recovery-e2e:reuse-artifact`
|
|
323
|
+
- `npm run validate:packaged-dry-run-schema:reuse-artifact`
|
|
324
|
+
- `npm run validate:packaged-usage-health:reuse-artifact`
|
|
325
|
+
- `npm run validate:packaged-usage-age-slo:reuse-artifact`
|
|
326
|
+
- `npm run validate:packaged-openclaw-stats-ingestion:reuse-artifact`
|
|
327
|
+
- `npm run validate:packaged-usage-recovery-e2e:reuse-artifact`
|
|
328
|
+
- `npm run validate:packaged-usage-probe-noise-e2e:reuse-artifact`
|
|
329
|
+
- `npm run validate:packaged-usage-alert-rate-e2e:reuse-artifact`
|
|
330
|
+
|
|
331
|
+
Each wrapper sets `IDLEWATCH_SKIP_PACKAGE_MACOS=1` so it reuses the already-built packaged artifact in a run.
|
|
332
|
+
|
|
333
|
+
## Release validation helpers
|
|
334
|
+
- `validate:release-gate`: host OpenClaw release checks plus packaged robustness checks in one command on macOS; on non-macOS it performs host checks only.
|
|
335
|
+
- `validate:release-gate-all`: full host OpenClaw checks + packaged release checks (`validate:openclaw-release-gates:all`) and packaged robustness checks (fresh `validate:packaged-openclaw-robustness`) on macOS; host-only on non-macOS.
|
|
336
|
+
- `validate:packaged-openclaw-robustness` is the fresh-packaging packaged resilience slice (`usage-age-slo`, `usage-alert-rate-e2e`, `usage-probe-noise-e2e`, and `packaged-openclaw-release-gates`).
|