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.
Files changed (110) hide show
  1. package/.env.example +73 -0
  2. package/.github/workflows/ci.yml +99 -0
  3. package/.github/workflows/release-macos-trusted.yml +103 -0
  4. package/README.md +336 -0
  5. package/bin/idlewatch-agent.js +1053 -0
  6. package/docs/onboarding-external.md +58 -0
  7. package/docs/packaging/macos-dmg.md +199 -0
  8. package/docs/packaging/macos-launch-agent.md +70 -0
  9. package/docs/qa/archive/mac-qa-log-2026-02-17.md +5838 -0
  10. package/docs/qa/mac-qa-log.md +2864 -0
  11. package/docs/telemetry/idle-stale-policy.md +57 -0
  12. package/docs/telemetry/openclaw-mapping.md +80 -0
  13. package/package.json +76 -0
  14. package/scripts/build-dmg.sh +65 -0
  15. package/scripts/install-macos-launch-agent.sh +78 -0
  16. package/scripts/lib/telemetry-row-parser.mjs +100 -0
  17. package/scripts/package-macos.sh +228 -0
  18. package/scripts/uninstall-macos-launch-agent.sh +30 -0
  19. package/scripts/validate-all.sh +142 -0
  20. package/scripts/validate-bin.mjs +25 -0
  21. package/scripts/validate-dmg-checksum.sh +37 -0
  22. package/scripts/validate-dmg-install.sh +155 -0
  23. package/scripts/validate-dry-run-schema.mjs +257 -0
  24. package/scripts/validate-onboarding.mjs +63 -0
  25. package/scripts/validate-openclaw-cache-recovery-e2e.mjs +113 -0
  26. package/scripts/validate-openclaw-release-gates.mjs +51 -0
  27. package/scripts/validate-openclaw-stats-ingestion.mjs +372 -0
  28. package/scripts/validate-openclaw-usage-health.mjs +95 -0
  29. package/scripts/validate-packaged-artifact.mjs +233 -0
  30. package/scripts/validate-packaged-bundled-runtime.sh +191 -0
  31. package/scripts/validate-packaged-metadata.sh +43 -0
  32. package/scripts/validate-packaged-openclaw-cache-recovery-e2e.mjs +153 -0
  33. package/scripts/validate-packaged-openclaw-release-gates.mjs +72 -0
  34. package/scripts/validate-packaged-openclaw-stats-ingestion.mjs +402 -0
  35. package/scripts/validate-packaged-sourcemaps.mjs +82 -0
  36. package/scripts/validate-packaged-usage-alert-rate-e2e.mjs +98 -0
  37. package/scripts/validate-packaged-usage-probe-noise-e2e.mjs +87 -0
  38. package/scripts/validate-packaged-usage-recovery-e2e.mjs +90 -0
  39. package/scripts/validate-trusted-prereqs.sh +44 -0
  40. package/scripts/validate-usage-alert-rate-e2e.mjs +91 -0
  41. package/scripts/validate-usage-freshness-e2e.mjs +81 -0
  42. package/skill/SKILL.md +43 -0
  43. package/src/config.js +100 -0
  44. package/src/enrollment.js +176 -0
  45. package/src/gpu.js +115 -0
  46. package/src/memory.js +67 -0
  47. package/src/openclaw-cache.js +51 -0
  48. package/src/openclaw-usage.js +1020 -0
  49. package/src/telemetry-mapping.js +54 -0
  50. package/src/usage-alert.js +41 -0
  51. package/src/usage-freshness.js +31 -0
  52. package/test/config.test.mjs +112 -0
  53. package/test/fixtures/gpu-agx.txt +2 -0
  54. package/test/fixtures/gpu-iogpu.txt +2 -0
  55. package/test/fixtures/gpu-top-grep.txt +2 -0
  56. package/test/fixtures/openclaw-fleet-sample-v1.json +68 -0
  57. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-iso-ts.txt +2 -0
  58. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-newest.txt +2 -0
  59. package/test/fixtures/openclaw-mixed-equal-score-status-vs-generic-string-ts.txt +2 -0
  60. package/test/fixtures/openclaw-mixed-status-then-generic-output.txt +2 -0
  61. package/test/fixtures/openclaw-stats-current-wrapper.json +12 -0
  62. package/test/fixtures/openclaw-stats-current-wrapper2.json +15 -0
  63. package/test/fixtures/openclaw-stats-data-wrapper.json +21 -0
  64. package/test/fixtures/openclaw-stats-nested-session-wrapper.json +23 -0
  65. package/test/fixtures/openclaw-stats-payload-wrapper.json +1 -0
  66. package/test/fixtures/openclaw-stats-status-current-wrapper.json +19 -0
  67. package/test/fixtures/openclaw-stats.json +17 -0
  68. package/test/fixtures/openclaw-status-ansi-complex-noise.txt +3 -0
  69. package/test/fixtures/openclaw-status-ansi-noise.txt +2 -0
  70. package/test/fixtures/openclaw-status-control-noise.txt +1 -0
  71. package/test/fixtures/openclaw-status-data-wrapper.json +20 -0
  72. package/test/fixtures/openclaw-status-dcs-noise.txt +1 -0
  73. package/test/fixtures/openclaw-status-epoch-seconds.json +15 -0
  74. package/test/fixtures/openclaw-status-mixed-noise.txt +1 -0
  75. package/test/fixtures/openclaw-status-multi-json.txt +3 -0
  76. package/test/fixtures/openclaw-status-nested-recent.json +19 -0
  77. package/test/fixtures/openclaw-status-noisy-default-then-usage.txt +2 -0
  78. package/test/fixtures/openclaw-status-noisy.txt +3 -0
  79. package/test/fixtures/openclaw-status-osc-noise.txt +1 -0
  80. package/test/fixtures/openclaw-status-result-session.json +15 -0
  81. package/test/fixtures/openclaw-status-session-map-with-defaults.json +23 -0
  82. package/test/fixtures/openclaw-status-session-map.json +28 -0
  83. package/test/fixtures/openclaw-status-session-model-name.json +18 -0
  84. package/test/fixtures/openclaw-status-snake-session-wrapper.json +13 -0
  85. package/test/fixtures/openclaw-status-stats-current-sessions-snake-tokens.json +25 -0
  86. package/test/fixtures/openclaw-status-stats-current-sessions.json +28 -0
  87. package/test/fixtures/openclaw-status-stats-current-usage-time-camelcase.json +19 -0
  88. package/test/fixtures/openclaw-status-stats-session-default-model.json +27 -0
  89. package/test/fixtures/openclaw-status-status-wrapper.json +13 -0
  90. package/test/fixtures/openclaw-status-strings.json +38 -0
  91. package/test/fixtures/openclaw-status-ts-ms-alias.json +14 -0
  92. package/test/fixtures/openclaw-status-updated-at-ms-alias.json +14 -0
  93. package/test/fixtures/openclaw-status-usage-timestamp-ms-alias.json +14 -0
  94. package/test/fixtures/openclaw-status-usage-ts-alias.json +14 -0
  95. package/test/fixtures/openclaw-status-wrap-session-object.json +24 -0
  96. package/test/fixtures/openclaw-status.json +41 -0
  97. package/test/fixtures/openclaw-usage-model-name-generic.json +9 -0
  98. package/test/gpu.test.mjs +58 -0
  99. package/test/memory.test.mjs +35 -0
  100. package/test/openclaw-cache.test.mjs +48 -0
  101. package/test/openclaw-env.test.mjs +365 -0
  102. package/test/openclaw-usage.test.mjs +555 -0
  103. package/test/telemetry-mapping.test.mjs +69 -0
  104. package/test/telemetry-row-parser.test.mjs +44 -0
  105. package/test/usage-alert.test.mjs +73 -0
  106. package/test/usage-freshness.test.mjs +63 -0
  107. package/test/validate-dry-run-schema.test.mjs +146 -0
  108. package/tui/Cargo.lock +801 -0
  109. package/tui/Cargo.toml +11 -0
  110. package/tui/src/main.rs +368 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "status": {
3
+ "current_session": {
4
+ "session_id": "snake-current-1",
5
+ "agent_id": "agent-snake",
6
+ "model": "gpt-5.3-codex",
7
+ "total_tokens": 8888,
8
+ "updated_at": 1771299000,
9
+ "ts": 1771299012345
10
+ },
11
+ "default_model": "gpt-5.3-codex"
12
+ }
13
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "status": {
3
+ "stats": {
4
+ "current": {
5
+ "sessions": [
6
+ {
7
+ "sessionId": "legacy-1",
8
+ "agentId": "agent-legacy-1",
9
+ "model": "gpt-4.1",
10
+ "total_tokens": "111",
11
+ "updatedAt": 1771261000000
12
+ },
13
+ {
14
+ "session_id": "active-string-tokens",
15
+ "agent_id": "agent-legacy-2",
16
+ "defaultModel": "gpt-5.3-codex",
17
+ "total_tokens": "4321",
18
+ "updatedAt": 1771279100000,
19
+ "tokensPerMinute": 42.0
20
+ }
21
+ ]
22
+ }
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "status": {
3
+ "stats": {
4
+ "current": {
5
+ "sessions": [
6
+ {
7
+ "sessionId": "old-session",
8
+ "agentId": "agent-old",
9
+ "model": "gpt-4.1",
10
+ "totalTokens": 100,
11
+ "updatedAt": 1771270000000
12
+ },
13
+ {
14
+ "sessionId": "active-session",
15
+ "agentId": "agent-active",
16
+ "model": "gpt-5.3-codex",
17
+ "totalTokens": 4321,
18
+ "updatedAt": 1771279100000,
19
+ "tokensPerMinute": 66.6
20
+ }
21
+ ],
22
+ "totals": {
23
+ "totalTokens": 4421
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "status": {
3
+ "current": {
4
+ "stats": {
5
+ "current": {
6
+ "session": {
7
+ "sessionId": "camelcase-session",
8
+ "agentId": "agent-camel",
9
+ "model": "gpt-5.3-codex",
10
+ "totalTokens": 555,
11
+ "tokensPerMinute": 22,
12
+ "usageTime": "2026-02-27T11:30:00.000Z"
13
+ }
14
+ }
15
+ }
16
+ }
17
+ },
18
+ "ts": 1771323000000
19
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "status": {
3
+ "stats": {
4
+ "current": {
5
+ "sessions": [
6
+ {
7
+ "sessionId": "legacy-model",
8
+ "defaultModel": "claude-mini-3.7",
9
+ "agentId": "agent-default-model",
10
+ "totalTokens": "777",
11
+ "updatedAt": 1771274000000,
12
+ "tokensPerMinute": "12.34"
13
+ },
14
+ {
15
+ "sessionId": "better-total",
16
+ "default_model": "gpt-4.1-mini",
17
+ "defaultModel": "should-not-win",
18
+ "agentId": "agent-legacy",
19
+ "total_tokens": "999",
20
+ "updatedAt": 1771264000000,
21
+ "tokens_per_minute": 9.87
22
+ }
23
+ ]
24
+ }
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "status": {
3
+ "current": {
4
+ "model": "gpt-5.3-codex-spark",
5
+ "totalTokens": 4321,
6
+ "sessionId": "status-wrap-session",
7
+ "agent_id": "agent-status-wrap",
8
+ "updatedAt": 1771300100,
9
+ "updatedAtMs": 1771300100
10
+ },
11
+ "defaultModel": "gpt-5.3-codex-spark"
12
+ }
13
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "sessions": {
3
+ "defaults": {
4
+ "model": "gpt-5.3-codex",
5
+ "defaultModel": "gpt-5.3-codex-spark"
6
+ },
7
+ "recent": [
8
+ {
9
+ "agentId": "agent-1",
10
+ "sessionId": "a9f2d3",
11
+ "updatedAt": "1771278800000",
12
+ "age": "600000",
13
+ "total_tokens": "120",
14
+ "totalTokensFresh": "false",
15
+ "model": "gpt-5.3-codex",
16
+ "tokens_per_minute": "12.75"
17
+ },
18
+ {
19
+ "agentId": "agent-2",
20
+ "sessionId": "b7e1f8",
21
+ "updatedAt": 1771278820000,
22
+ "ageMs": "30000",
23
+ "totalTokens": "450",
24
+ "totalTokensFresh": "true",
25
+ "model": "claude-opus-4-6",
26
+ "tokensPerMinute": "45"
27
+ },
28
+ {
29
+ "agentId": "agent-3",
30
+ "sessionId": "f2c9aa",
31
+ "updatedAt": 1771278830000,
32
+ "age": 15000,
33
+ "totalTokens": null,
34
+ "model": "gpt-5.3-codex"
35
+ }
36
+ ]
37
+ }
38
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "status": {
3
+ "current": {
4
+ "session": {
5
+ "sessionId": "ts-ms-session",
6
+ "agentId": "agent-ts-ms",
7
+ "model": "gpt-5.3-codex",
8
+ "totalTokens": 1500,
9
+ "tokensPerMinute": 77,
10
+ "usage_ts_ms": 1771319900000
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "status": {
3
+ "current": {
4
+ "session": {
5
+ "sessionId": "ts-ms-updated-at-session",
6
+ "agentId": "agent-ts-ms-updated-at",
7
+ "model": "gpt-5.3-codex-spark",
8
+ "totalTokens": 2500,
9
+ "tokensPerMinute": 31.2,
10
+ "updated_at_ms": 1771325000000
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "status": {
3
+ "current": {
4
+ "session": {
5
+ "sessionId": "ts-ms-underscore-session",
6
+ "agentId": "agent-ts-ms-underscore",
7
+ "model": "qwen3",
8
+ "totalTokens": 1717,
9
+ "tokensPerMinute": 22,
10
+ "usage_timestamp_ms": 1771321200000
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "status": {
3
+ "current": {
4
+ "session": {
5
+ "sessionId": "usage-ts-session",
6
+ "agentId": "agent-usage-ts",
7
+ "model": "gpt-5.3-codex",
8
+ "totalTokens": 1900,
9
+ "tokensPerMinute": 44,
10
+ "usage_ts": 1771319000
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "result": {
3
+ "defaultModel": "gpt-4.1",
4
+ "defaults": {
5
+ "defaultModel": "gpt-4.1"
6
+ },
7
+ "sessions": [
8
+ {
9
+ "id": "wrapped-session-a",
10
+ "agentId": "agent-wrap",
11
+ "usage": {
12
+ "model": "claude-opus-4.6",
13
+ "totals": {
14
+ "totalTokens": "789"
15
+ },
16
+ "updatedAt": "2026-02-17T09:00:00Z",
17
+ "tokensPerMinute": "12.5",
18
+ "sessionId": "wrapped-session-a"
19
+ },
20
+ "age": 120000
21
+ }
22
+ ]
23
+ }
24
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "sessions": {
3
+ "defaults": {
4
+ "model": "gpt-5.3-codex",
5
+ "contextTokens": 272000
6
+ },
7
+ "recent": [
8
+ {
9
+ "agentId": "main",
10
+ "sessionId": "781280ba-01a6-49d9-9c75-09519f28f709",
11
+ "updatedAt": 1771278795632,
12
+ "age": 29308,
13
+ "totalTokens": null,
14
+ "totalTokensFresh": false,
15
+ "model": "gpt-5.3-codex"
16
+ },
17
+ {
18
+ "agentId": "main",
19
+ "sessionId": "45f23527-d596-469a-882c-9bba6dc8b5a0",
20
+ "updatedAt": 1771278693678,
21
+ "age": 131262,
22
+ "inputTokens": 1036,
23
+ "outputTokens": 96,
24
+ "totalTokens": 67980,
25
+ "totalTokensFresh": true,
26
+ "model": "gpt-5.3-codex"
27
+ },
28
+ {
29
+ "agentId": "main",
30
+ "sessionId": "90d2a820-6d77-42f0-8db4-12b90f9f7203",
31
+ "updatedAt": 1771278893678,
32
+ "age": 11000,
33
+ "inputTokens": 1200,
34
+ "outputTokens": 210,
35
+ "totalTokens": 70500,
36
+ "totalTokensFresh": true,
37
+ "model": "gpt-5.3-codex"
38
+ }
39
+ ]
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "model_name": "llama-3.3",
3
+ "total_tokens": "135",
4
+ "tokens_per_minute": "21.5",
5
+ "session_id": "generic-model-name",
6
+ "agent_id": "agent-generic",
7
+ "updatedAt": 1771324000000,
8
+ "ageMs": 12000
9
+ }
@@ -0,0 +1,58 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { gpuSampleDarwin, __gpuTestUtils } from '../src/gpu.js'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+ const fixture = (name) => fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8').trim()
11
+
12
+ const { parseIoregGpuUtilization, parseGpuPercent } = __gpuTestUtils
13
+
14
+ test('parses ioreg AGX performance statistics device utilization', () => {
15
+ const sample = fixture('gpu-agx.txt')
16
+ assert.equal(parseIoregGpuUtilization(sample), 34)
17
+ })
18
+
19
+ test('parses ioreg IOGPU performance statistics when AGX is unavailable', () => {
20
+ const sample = fixture('gpu-iogpu.txt')
21
+ assert.equal(parseIoregGpuUtilization(sample), 1.2)
22
+ })
23
+
24
+ test('parses powermetrics GPU residency percent', () => {
25
+ const sample = 'GPU HW active residency: 18.25%\nGPU SW requested state: 5'
26
+ assert.equal(parseGpuPercent(sample, 'powermetrics'), 18.25)
27
+ })
28
+
29
+ test('parses top grep fallback output', () => {
30
+ const sample = fixture('gpu-top-grep.txt')
31
+ assert.equal(parseGpuPercent(sample, 'top-grep'), 7.8)
32
+ })
33
+
34
+ test('gpu sampler captures three distinct macOS telemetry sources deterministically', () => {
35
+ const fakeExec = (cmd) => {
36
+ if (cmd.includes('AGXAccelerator')) {
37
+ return fixture('gpu-agx.txt')
38
+ }
39
+ if (cmd.includes('IOGPU')) {
40
+ return fixture('gpu-iogpu.txt')
41
+ }
42
+ if (cmd.includes('powermetrics')) {
43
+ return 'GPU HW active residency: 11.11%\nGPU SW requested state: 5'
44
+ }
45
+ if (cmd.includes('top -l 1 | grep -i \'GPU\'')) {
46
+ return fixture('gpu-top-grep.txt')
47
+ }
48
+ throw new Error(`unexpected command in test: ${cmd}`)
49
+ }
50
+
51
+ const gpu = gpuSampleDarwin(fakeExec)
52
+ assert.deepEqual(gpu, {
53
+ pct: 34,
54
+ source: 'ioreg-agx',
55
+ confidence: 'high',
56
+ sampleWindowMs: null
57
+ })
58
+ })
@@ -0,0 +1,35 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { parseMemoryPressurePct, memoryPressureDarwin } from '../src/memory.js'
4
+
5
+ test('parseMemoryPressurePct derives pressure from free percentage output', () => {
6
+ const raw = 'System-wide memory free percentage: 24%\nPages free: 12345.'
7
+ assert.equal(parseMemoryPressurePct(raw), 76)
8
+ })
9
+
10
+ test('parseMemoryPressurePct supports explicit pressure output', () => {
11
+ const raw = 'System-wide memory pressure: 67.5%\n'
12
+ assert.equal(parseMemoryPressurePct(raw), 67.5)
13
+ })
14
+
15
+ test('memoryPressureDarwin returns unavailable when command fails', () => {
16
+ const sample = memoryPressureDarwin(() => {
17
+ throw new Error('boom')
18
+ })
19
+
20
+ assert.deepEqual(sample, {
21
+ pct: null,
22
+ cls: 'unavailable',
23
+ source: 'unavailable'
24
+ })
25
+ })
26
+
27
+ test('memoryPressureDarwin returns classified value when parse succeeds', () => {
28
+ const sample = memoryPressureDarwin(() => 'System-wide memory free percentage: 12%')
29
+
30
+ assert.deepEqual(sample, {
31
+ pct: 88,
32
+ cls: 'warning',
33
+ source: 'memory_pressure'
34
+ })
35
+ })
@@ -0,0 +1,48 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, rmSync, writeFileSync, readdirSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import path from 'node:path'
6
+ import { loadLastGoodUsageSnapshot, persistLastGoodUsageSnapshot } from '../src/openclaw-cache.js'
7
+
8
+ test('persists and reloads last-good usage snapshot', () => {
9
+ const dir = mkdtempSync(path.join(tmpdir(), 'idlewatch-cache-test-'))
10
+ const cachePath = path.join(dir, 'last-good.json')
11
+ const now = Date.now()
12
+
13
+ const saved = persistLastGoodUsageSnapshot(cachePath, {
14
+ at: now - 500,
15
+ usage: { model: 'gpt-5.3-codex', totalTokens: 12345 }
16
+ })
17
+ assert.equal(saved, true)
18
+
19
+ const loaded = loadLastGoodUsageSnapshot(cachePath, now)
20
+ assert.ok(loaded)
21
+ assert.equal(loaded.usage.model, 'gpt-5.3-codex')
22
+ assert.equal(loaded.usage.totalTokens, 12345)
23
+ assert.ok(Number.isFinite(loaded.ageMs) && loaded.ageMs >= 0)
24
+
25
+ const files = readdirSync(dir)
26
+ assert.ok(files.includes('last-good.json'))
27
+ assert.equal(files.some((name) => name.startsWith('last-good.json.tmp-')), false)
28
+
29
+ rmSync(dir, { recursive: true, force: true })
30
+ })
31
+
32
+ test('returns null on corrupt cache payload', () => {
33
+ const dir = mkdtempSync(path.join(tmpdir(), 'idlewatch-cache-test-'))
34
+ const cachePath = path.join(dir, 'last-good.json')
35
+
36
+ persistLastGoodUsageSnapshot(cachePath, {
37
+ at: Date.now(),
38
+ usage: { model: 'ok' }
39
+ })
40
+
41
+ // corrupt file after write
42
+ writeFileSync(cachePath, '{nope', 'utf8')
43
+
44
+ const loaded = loadLastGoodUsageSnapshot(cachePath)
45
+ assert.equal(loaded, null)
46
+
47
+ rmSync(dir, { recursive: true, force: true })
48
+ })