chainlesschain 0.51.0 → 0.66.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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
  4. package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
  5. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
  6. package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
  7. package/src/assets/web-panel/index.html +2 -2
  8. package/src/commands/agent-network.js +785 -0
  9. package/src/commands/automation.js +654 -0
  10. package/src/commands/dao.js +565 -0
  11. package/src/commands/did-v2.js +620 -0
  12. package/src/commands/economy.js +578 -0
  13. package/src/commands/evolution.js +391 -0
  14. package/src/commands/hmemory.js +442 -0
  15. package/src/commands/perf.js +433 -0
  16. package/src/commands/pipeline.js +449 -0
  17. package/src/commands/plugin-ecosystem.js +517 -0
  18. package/src/commands/sandbox.js +401 -0
  19. package/src/commands/social.js +311 -0
  20. package/src/commands/sso.js +798 -0
  21. package/src/commands/workflow.js +320 -0
  22. package/src/commands/zkp.js +227 -1
  23. package/src/index.js +21 -0
  24. package/src/lib/agent-economy.js +479 -0
  25. package/src/lib/agent-network.js +1121 -0
  26. package/src/lib/automation-engine.js +948 -0
  27. package/src/lib/dao-governance.js +569 -0
  28. package/src/lib/did-v2-manager.js +1127 -0
  29. package/src/lib/evolution-system.js +453 -0
  30. package/src/lib/hierarchical-memory.js +481 -0
  31. package/src/lib/perf-tuning.js +734 -0
  32. package/src/lib/pipeline-orchestrator.js +928 -0
  33. package/src/lib/plugin-ecosystem.js +1109 -0
  34. package/src/lib/sandbox-v2.js +306 -0
  35. package/src/lib/social-graph-analytics.js +707 -0
  36. package/src/lib/sso-manager.js +841 -0
  37. package/src/lib/workflow-engine.js +454 -1
  38. package/src/lib/zkp-engine.js +249 -20
  39. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
@@ -0,0 +1,734 @@
1
+ /**
2
+ * Performance Auto-Tuning — CLI port of Phase 22
3
+ * (docs/design/modules/22_性能自动调优.md).
4
+ *
5
+ * Desktop exposes 25 IPC handlers (12 AutoTuner + 13 PerformanceMonitor)
6
+ * around a 10s ring buffer (8640 samples × 10s = 24h) with 5 built-in rules,
7
+ * hysteresis protection, cooldown windows, and EventEmitter-driven alerts.
8
+ *
9
+ * CLI port ships:
10
+ *
11
+ * - Real `os` / `process.memoryUsage()` sampling (no simulation)
12
+ * - SQLite-backed ring buffer (survives across invocations)
13
+ * - 5 built-in rule templates — `evaluate` returns recommendations,
14
+ * the CLI NEVER auto-applies actions (explicit `apply-recommendation`)
15
+ * - Hysteresis (consecutiveRequired + cooldownMs) tracked per rule
16
+ * - Simple threshold alerts
17
+ *
18
+ * What does NOT port: IPC interceptor, database query wrapper, auto-start
19
+ * timer, per-process electron handle count, disk-io deltas, EventEmitter
20
+ * event bus, auto-vacuum/GC actions (CLI just reports).
21
+ */
22
+
23
+ import os from "os";
24
+ import crypto from "crypto";
25
+
26
+ /* ── Constants ──────────────────────────────────────────── */
27
+
28
+ export const DEFAULT_MAX_SAMPLES = 8640; // 24h @ 10s
29
+ export const DEFAULT_SAMPLE_INTERVAL_MS = 10_000;
30
+
31
+ export const ALERT_LEVELS = Object.freeze({
32
+ INFO: "info",
33
+ WARNING: "warning",
34
+ CRITICAL: "critical",
35
+ });
36
+
37
+ export const RECOMMENDATION_STATUS = Object.freeze({
38
+ PENDING: "pending",
39
+ APPLIED: "applied",
40
+ DISMISSED: "dismissed",
41
+ });
42
+
43
+ export const DEFAULT_ALERT_THRESHOLDS = Object.freeze({
44
+ cpuPercent: 85,
45
+ memoryPercent: 85,
46
+ heapPercent: 90,
47
+ loadPerCore: 1.5,
48
+ });
49
+
50
+ export const BUILTIN_RULES = Object.freeze([
51
+ Object.freeze({
52
+ id: "memory-pressure",
53
+ name: "内存压力",
54
+ description: "进程堆使用率或系统内存使用率过高",
55
+ condition: { metric: "memoryPercent", op: ">", value: 80 },
56
+ action: "清理缓存 / 触发 GC / 减少并发",
57
+ severity: ALERT_LEVELS.WARNING,
58
+ consecutiveRequired: 3,
59
+ cooldownMs: 2 * 60_000,
60
+ }),
61
+ Object.freeze({
62
+ id: "cpu-saturation",
63
+ name: "CPU 饱和",
64
+ description: "CPU 使用率持续高于阈值",
65
+ condition: { metric: "cpuPercent", op: ">", value: 90 },
66
+ action: "降低采样频率 / 限制并行任务",
67
+ severity: ALERT_LEVELS.WARNING,
68
+ consecutiveRequired: 3,
69
+ cooldownMs: 3 * 60_000,
70
+ }),
71
+ Object.freeze({
72
+ id: "heap-leak",
73
+ name: "堆增长异常",
74
+ description: "进程堆使用率持续接近上限,疑似内存泄漏",
75
+ condition: { metric: "heapPercent", op: ">", value: 90 },
76
+ action: "Dump heap / 重启 worker / 缩小 cache",
77
+ severity: ALERT_LEVELS.CRITICAL,
78
+ consecutiveRequired: 5,
79
+ cooldownMs: 10 * 60_000,
80
+ }),
81
+ Object.freeze({
82
+ id: "load-average",
83
+ name: "负载过高",
84
+ description: "1 分钟系统负载除以核心数超过阈值",
85
+ condition: { metric: "loadPerCore", op: ">", value: 1.5 },
86
+ action: "限流后台任务 / 延迟批处理",
87
+ severity: ALERT_LEVELS.WARNING,
88
+ consecutiveRequired: 3,
89
+ cooldownMs: 5 * 60_000,
90
+ }),
91
+ Object.freeze({
92
+ id: "db-slow-queries",
93
+ name: "慢查询",
94
+ description: "慢查询计数器超过阈值 (需外部 feeder)",
95
+ condition: { metric: "slowQueries", op: ">", value: 5 },
96
+ action: "建索引 / 重写查询 / 切分事务",
97
+ severity: ALERT_LEVELS.WARNING,
98
+ consecutiveRequired: 2,
99
+ cooldownMs: 5 * 60_000,
100
+ }),
101
+ ]);
102
+
103
+ /* ── Helpers ────────────────────────────────────────────── */
104
+
105
+ function _now() {
106
+ return Date.now();
107
+ }
108
+
109
+ function _strip(row) {
110
+ if (!row) return null;
111
+ const out = {};
112
+ for (const [k, v] of Object.entries(row)) {
113
+ if (k !== "_rowid_" && k !== "rowid") out[k] = v;
114
+ }
115
+ return out;
116
+ }
117
+
118
+ function _parseMaybe(raw) {
119
+ if (raw == null) return null;
120
+ if (typeof raw !== "string") return raw;
121
+ try {
122
+ return JSON.parse(raw);
123
+ } catch (_e) {
124
+ return raw;
125
+ }
126
+ }
127
+
128
+ function _round(v, digits = 2) {
129
+ const m = 10 ** digits;
130
+ return Math.round(v * m) / m;
131
+ }
132
+
133
+ /* ── Schema ─────────────────────────────────────────────── */
134
+
135
+ export function ensurePerfTables(db) {
136
+ db.exec(`CREATE TABLE IF NOT EXISTS perf_samples (
137
+ id TEXT PRIMARY KEY,
138
+ ts INTEGER NOT NULL,
139
+ cpu_percent REAL,
140
+ memory_percent REAL,
141
+ heap_used INTEGER,
142
+ heap_total INTEGER,
143
+ heap_percent REAL,
144
+ rss INTEGER,
145
+ load1 REAL,
146
+ load_per_core REAL,
147
+ free_mem INTEGER,
148
+ total_mem INTEGER,
149
+ extra TEXT
150
+ )`);
151
+
152
+ db.exec(`CREATE TABLE IF NOT EXISTS perf_rule_state (
153
+ rule_id TEXT PRIMARY KEY,
154
+ enabled INTEGER DEFAULT 1,
155
+ consecutive_count INTEGER DEFAULT 0,
156
+ last_triggered_at INTEGER,
157
+ total_triggered INTEGER DEFAULT 0,
158
+ overrides TEXT
159
+ )`);
160
+
161
+ db.exec(`CREATE TABLE IF NOT EXISTS perf_recommendations (
162
+ id TEXT PRIMARY KEY,
163
+ rule_id TEXT NOT NULL,
164
+ severity TEXT,
165
+ description TEXT,
166
+ metric TEXT,
167
+ metric_value REAL,
168
+ threshold REAL,
169
+ status TEXT DEFAULT 'pending',
170
+ created_at INTEGER NOT NULL,
171
+ resolved_at INTEGER,
172
+ note TEXT
173
+ )`);
174
+
175
+ db.exec(`CREATE TABLE IF NOT EXISTS perf_tuning_history (
176
+ id TEXT PRIMARY KEY,
177
+ rule_id TEXT NOT NULL,
178
+ action TEXT,
179
+ result TEXT,
180
+ created_at INTEGER NOT NULL
181
+ )`);
182
+
183
+ db.exec(`CREATE TABLE IF NOT EXISTS perf_config (
184
+ key TEXT PRIMARY KEY,
185
+ value TEXT
186
+ )`);
187
+ }
188
+
189
+ /* ── Config ─────────────────────────────────────────────── */
190
+
191
+ export function getPerfConfig(db) {
192
+ const rows = db.prepare("SELECT key, value FROM perf_config").all();
193
+ const out = {
194
+ maxSamples: DEFAULT_MAX_SAMPLES,
195
+ sampleIntervalMs: DEFAULT_SAMPLE_INTERVAL_MS,
196
+ thresholds: { ...DEFAULT_ALERT_THRESHOLDS },
197
+ };
198
+ for (const r of rows) {
199
+ const v = _parseMaybe(r.value);
200
+ if (r.key === "maxSamples" && typeof v === "number") out.maxSamples = v;
201
+ else if (r.key === "sampleIntervalMs" && typeof v === "number")
202
+ out.sampleIntervalMs = v;
203
+ else if (r.key === "thresholds" && v && typeof v === "object")
204
+ out.thresholds = { ...out.thresholds, ...v };
205
+ }
206
+ return out;
207
+ }
208
+
209
+ export function setPerfConfig(db, patch = {}) {
210
+ const merged = { ...getPerfConfig(db), ...patch };
211
+ if (patch.thresholds) {
212
+ merged.thresholds = {
213
+ ...getPerfConfig(db).thresholds,
214
+ ...patch.thresholds,
215
+ };
216
+ }
217
+ const upsert = (k, v) =>
218
+ db
219
+ .prepare(
220
+ "INSERT INTO perf_config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
221
+ )
222
+ .run(k, JSON.stringify(v));
223
+ upsert("maxSamples", merged.maxSamples);
224
+ upsert("sampleIntervalMs", merged.sampleIntervalMs);
225
+ upsert("thresholds", merged.thresholds);
226
+ return merged;
227
+ }
228
+
229
+ /* ── Sampling ───────────────────────────────────────────── */
230
+
231
+ export function collectSampleRaw({ slowQueries } = {}) {
232
+ const cpus = os.cpus() || [];
233
+ const totalCpu = cpus.reduce((acc, c) => {
234
+ for (const [k, v] of Object.entries(c.times)) acc[k] = (acc[k] || 0) + v;
235
+ return acc;
236
+ }, {});
237
+ const cpuBusy =
238
+ (totalCpu.user || 0) + (totalCpu.sys || 0) + (totalCpu.irq || 0);
239
+ const cpuTotal = Object.values(totalCpu).reduce((a, b) => a + b, 0) || 1;
240
+ const cpuPercent = _round((cpuBusy / cpuTotal) * 100);
241
+
242
+ const totalMem = os.totalmem();
243
+ const freeMem = os.freemem();
244
+ const memoryPercent = _round(((totalMem - freeMem) / totalMem) * 100);
245
+
246
+ const mem = process.memoryUsage?.() || {
247
+ heapUsed: 0,
248
+ heapTotal: 1,
249
+ rss: 0,
250
+ };
251
+ const heapPercent = _round((mem.heapUsed / (mem.heapTotal || 1)) * 100);
252
+
253
+ const load = os.loadavg?.() || [0, 0, 0];
254
+ const cores = cpus.length || 1;
255
+ const loadPerCore = _round((load[0] || 0) / cores, 3);
256
+
257
+ return {
258
+ ts: _now(),
259
+ cpuPercent,
260
+ memoryPercent,
261
+ heapUsed: mem.heapUsed,
262
+ heapTotal: mem.heapTotal,
263
+ heapPercent,
264
+ rss: mem.rss,
265
+ load1: _round(load[0] || 0, 3),
266
+ loadPerCore,
267
+ freeMem,
268
+ totalMem,
269
+ extra: { slowQueries: slowQueries ?? 0, cores },
270
+ };
271
+ }
272
+
273
+ export function collectSample(db, input = {}) {
274
+ const s = collectSampleRaw(input);
275
+ const id = crypto.randomUUID();
276
+ db.prepare(
277
+ `INSERT INTO perf_samples (id, ts, cpu_percent, memory_percent, heap_used, heap_total, heap_percent, rss, load1, load_per_core, free_mem, total_mem, extra)
278
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
279
+ ).run(
280
+ id,
281
+ s.ts,
282
+ s.cpuPercent,
283
+ s.memoryPercent,
284
+ s.heapUsed,
285
+ s.heapTotal,
286
+ s.heapPercent,
287
+ s.rss,
288
+ s.load1,
289
+ s.loadPerCore,
290
+ s.freeMem,
291
+ s.totalMem,
292
+ JSON.stringify(s.extra),
293
+ );
294
+ _trimRingBuffer(db);
295
+ return { id, ...s };
296
+ }
297
+
298
+ function _trimRingBuffer(db) {
299
+ const cfg = getPerfConfig(db);
300
+ const all = db
301
+ .prepare("SELECT id, ts FROM perf_samples")
302
+ .all()
303
+ .sort((a, b) => b.ts - a.ts);
304
+ if (all.length <= cfg.maxSamples) return 0;
305
+ const toDelete = all.slice(cfg.maxSamples);
306
+ for (const r of toDelete) {
307
+ db.prepare("DELETE FROM perf_samples WHERE id = ?").run(r.id);
308
+ }
309
+ return toDelete.length;
310
+ }
311
+
312
+ export function listSamples(db, { limit = 100, sinceMs } = {}) {
313
+ let rows = db.prepare("SELECT * FROM perf_samples").all();
314
+ rows = rows.map(_strip);
315
+ if (sinceMs) {
316
+ const cutoff = _now() - sinceMs;
317
+ rows = rows.filter((r) => r.ts >= cutoff);
318
+ }
319
+ return rows
320
+ .sort((a, b) => b.ts - a.ts)
321
+ .slice(0, limit)
322
+ .map(_toSampleOut)
323
+ .reverse();
324
+ }
325
+
326
+ function _toSampleOut(r) {
327
+ return {
328
+ id: r.id,
329
+ ts: r.ts,
330
+ cpuPercent: r.cpu_percent,
331
+ memoryPercent: r.memory_percent,
332
+ heapUsed: r.heap_used,
333
+ heapTotal: r.heap_total,
334
+ heapPercent: r.heap_percent,
335
+ rss: r.rss,
336
+ load1: r.load1,
337
+ loadPerCore: r.load_per_core,
338
+ freeMem: r.free_mem,
339
+ totalMem: r.total_mem,
340
+ extra: _parseMaybe(r.extra) || {},
341
+ };
342
+ }
343
+
344
+ export function getLatestSample(db) {
345
+ const rows = listSamples(db, { limit: 1 });
346
+ return rows[rows.length - 1] || null;
347
+ }
348
+
349
+ export function clearHistory(db) {
350
+ const rows = db.prepare("SELECT id FROM perf_samples").all();
351
+ for (const r of rows)
352
+ db.prepare("DELETE FROM perf_samples WHERE id = ?").run(r.id);
353
+ return { cleared: rows.length };
354
+ }
355
+
356
+ /* ── Rules ──────────────────────────────────────────────── */
357
+
358
+ export function listRules(db) {
359
+ const states = db.prepare("SELECT * FROM perf_rule_state").all().map(_strip);
360
+ const byId = new Map(states.map((s) => [s.rule_id, s]));
361
+ return BUILTIN_RULES.map((r) => {
362
+ const st = byId.get(r.id);
363
+ return {
364
+ ...r,
365
+ enabled: st ? !!st.enabled : true,
366
+ consecutiveCount: st?.consecutive_count || 0,
367
+ lastTriggeredAt: st?.last_triggered_at || null,
368
+ totalTriggered: st?.total_triggered || 0,
369
+ };
370
+ });
371
+ }
372
+
373
+ export function getRule(db, ruleId) {
374
+ return listRules(db).find((r) => r.id === ruleId) || null;
375
+ }
376
+
377
+ function _ensureRuleState(db, ruleId) {
378
+ const existing = db
379
+ .prepare("SELECT * FROM perf_rule_state WHERE rule_id = ?")
380
+ .get(ruleId);
381
+ if (existing) return _strip(existing);
382
+ db.prepare(
383
+ `INSERT INTO perf_rule_state (rule_id, enabled, consecutive_count, last_triggered_at, total_triggered)
384
+ VALUES (?, ?, ?, ?, ?)`,
385
+ ).run(ruleId, 1, 0, null, 0);
386
+ return {
387
+ rule_id: ruleId,
388
+ enabled: 1,
389
+ consecutive_count: 0,
390
+ last_triggered_at: null,
391
+ total_triggered: 0,
392
+ };
393
+ }
394
+
395
+ export function setRuleEnabled(db, ruleId, enabled) {
396
+ if (!BUILTIN_RULES.find((r) => r.id === ruleId))
397
+ return { updated: false, reason: "unknown_rule" };
398
+ _ensureRuleState(db, ruleId);
399
+ db.prepare("UPDATE perf_rule_state SET enabled = ? WHERE rule_id = ?").run(
400
+ enabled ? 1 : 0,
401
+ ruleId,
402
+ );
403
+ return { updated: true, ruleId, enabled: !!enabled };
404
+ }
405
+
406
+ /* ── Evaluation ─────────────────────────────────────────── */
407
+
408
+ function _pickMetric(sample, name) {
409
+ if (!sample) return null;
410
+ if (name === "cpuPercent") return sample.cpuPercent;
411
+ if (name === "memoryPercent") return sample.memoryPercent;
412
+ if (name === "heapPercent") return sample.heapPercent;
413
+ if (name === "loadPerCore") return sample.loadPerCore;
414
+ if (name === "slowQueries") return sample.extra?.slowQueries ?? 0;
415
+ return sample[name] ?? null;
416
+ }
417
+
418
+ function _testCondition(value, op, threshold) {
419
+ if (value == null) return false;
420
+ switch (op) {
421
+ case ">":
422
+ return value > threshold;
423
+ case ">=":
424
+ return value >= threshold;
425
+ case "<":
426
+ return value < threshold;
427
+ case "<=":
428
+ return value <= threshold;
429
+ case "==":
430
+ return value === threshold;
431
+ default:
432
+ return false;
433
+ }
434
+ }
435
+
436
+ export function evaluateRules(db, { sample } = {}) {
437
+ const s = sample || getLatestSample(db) || collectSampleRaw();
438
+ const rules = listRules(db);
439
+ const now = _now();
440
+ const triggered = [];
441
+ const skipped = [];
442
+
443
+ for (const rule of rules) {
444
+ if (!rule.enabled) {
445
+ skipped.push({ ruleId: rule.id, reason: "disabled" });
446
+ continue;
447
+ }
448
+ const val = _pickMetric(s, rule.condition.metric);
449
+ const conditionMet = _testCondition(
450
+ val,
451
+ rule.condition.op,
452
+ rule.condition.value,
453
+ );
454
+
455
+ const st = _ensureRuleState(db, rule.id);
456
+ if (!conditionMet) {
457
+ if (st.consecutive_count > 0) {
458
+ db.prepare(
459
+ "UPDATE perf_rule_state SET consecutive_count = ? WHERE rule_id = ?",
460
+ ).run(0, rule.id);
461
+ }
462
+ skipped.push({
463
+ ruleId: rule.id,
464
+ reason: "condition_unmet",
465
+ metric: rule.condition.metric,
466
+ value: val,
467
+ });
468
+ continue;
469
+ }
470
+
471
+ const newConsecutive = (st.consecutive_count || 0) + 1;
472
+ if (newConsecutive < rule.consecutiveRequired) {
473
+ db.prepare(
474
+ "UPDATE perf_rule_state SET consecutive_count = ? WHERE rule_id = ?",
475
+ ).run(newConsecutive, rule.id);
476
+ skipped.push({
477
+ ruleId: rule.id,
478
+ reason: "hysteresis",
479
+ consecutive: newConsecutive,
480
+ required: rule.consecutiveRequired,
481
+ });
482
+ continue;
483
+ }
484
+
485
+ const cooldownRemaining =
486
+ st.last_triggered_at != null
487
+ ? rule.cooldownMs - (now - st.last_triggered_at)
488
+ : 0;
489
+ if (cooldownRemaining > 0) {
490
+ skipped.push({
491
+ ruleId: rule.id,
492
+ reason: "cooldown",
493
+ remainingMs: cooldownRemaining,
494
+ });
495
+ continue;
496
+ }
497
+
498
+ const nextTotal = (st.total_triggered || 0) + 1;
499
+ db.prepare(
500
+ `UPDATE perf_rule_state SET consecutive_count = ?, last_triggered_at = ?, total_triggered = ? WHERE rule_id = ?`,
501
+ ).run(0, now, nextTotal, rule.id);
502
+
503
+ const recId = crypto.randomUUID();
504
+ db.prepare(
505
+ `INSERT INTO perf_recommendations (id, rule_id, severity, description, metric, metric_value, threshold, status, created_at)
506
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
507
+ ).run(
508
+ recId,
509
+ rule.id,
510
+ rule.severity,
511
+ rule.action,
512
+ rule.condition.metric,
513
+ val,
514
+ rule.condition.value,
515
+ RECOMMENDATION_STATUS.PENDING,
516
+ now,
517
+ );
518
+
519
+ db.prepare(
520
+ `INSERT INTO perf_tuning_history (id, rule_id, action, result, created_at)
521
+ VALUES (?, ?, ?, ?, ?)`,
522
+ ).run(
523
+ crypto.randomUUID(),
524
+ rule.id,
525
+ "trigger",
526
+ JSON.stringify({
527
+ metric: rule.condition.metric,
528
+ value: val,
529
+ threshold: rule.condition.value,
530
+ }),
531
+ now,
532
+ );
533
+
534
+ triggered.push({
535
+ ruleId: rule.id,
536
+ severity: rule.severity,
537
+ metric: rule.condition.metric,
538
+ value: val,
539
+ threshold: rule.condition.value,
540
+ action: rule.action,
541
+ recommendationId: recId,
542
+ });
543
+ }
544
+
545
+ return { triggered, skipped, sample: s, evaluatedAt: now };
546
+ }
547
+
548
+ /* ── Recommendations ────────────────────────────────────── */
549
+
550
+ export function listRecommendations(db, { status, limit = 100 } = {}) {
551
+ let rows = db.prepare("SELECT * FROM perf_recommendations").all();
552
+ rows = rows.map(_strip);
553
+ if (status) rows = rows.filter((r) => r.status === status);
554
+ return rows
555
+ .sort((a, b) => b.created_at - a.created_at)
556
+ .slice(0, limit)
557
+ .map(_toRecOut);
558
+ }
559
+
560
+ function _toRecOut(r) {
561
+ return {
562
+ id: r.id,
563
+ ruleId: r.rule_id,
564
+ severity: r.severity,
565
+ description: r.description,
566
+ metric: r.metric,
567
+ metricValue: r.metric_value,
568
+ threshold: r.threshold,
569
+ status: r.status,
570
+ createdAt: r.created_at,
571
+ resolvedAt: r.resolved_at,
572
+ note: r.note,
573
+ };
574
+ }
575
+
576
+ export function applyRecommendation(db, id, { note } = {}) {
577
+ const row = _strip(
578
+ db.prepare("SELECT * FROM perf_recommendations WHERE id = ?").get(id),
579
+ );
580
+ if (!row) return { applied: false, reason: "not_found" };
581
+ if (row.status !== RECOMMENDATION_STATUS.PENDING)
582
+ return { applied: false, reason: "not_pending", status: row.status };
583
+ const now = _now();
584
+ db.prepare(
585
+ "UPDATE perf_recommendations SET status = ?, resolved_at = ?, note = ? WHERE id = ?",
586
+ ).run(RECOMMENDATION_STATUS.APPLIED, now, note || null, id);
587
+ db.prepare(
588
+ `INSERT INTO perf_tuning_history (id, rule_id, action, result, created_at)
589
+ VALUES (?, ?, ?, ?, ?)`,
590
+ ).run(
591
+ crypto.randomUUID(),
592
+ row.rule_id,
593
+ "apply",
594
+ JSON.stringify({ recommendationId: id, note: note || null }),
595
+ now,
596
+ );
597
+ return { applied: true, id, appliedAt: now };
598
+ }
599
+
600
+ export function dismissRecommendation(db, id, { note } = {}) {
601
+ const row = _strip(
602
+ db.prepare("SELECT * FROM perf_recommendations WHERE id = ?").get(id),
603
+ );
604
+ if (!row) return { dismissed: false, reason: "not_found" };
605
+ if (row.status !== RECOMMENDATION_STATUS.PENDING)
606
+ return { dismissed: false, reason: "not_pending", status: row.status };
607
+ const now = _now();
608
+ db.prepare(
609
+ "UPDATE perf_recommendations SET status = ?, resolved_at = ?, note = ? WHERE id = ?",
610
+ ).run(RECOMMENDATION_STATUS.DISMISSED, now, note || null, id);
611
+ return { dismissed: true, id, dismissedAt: now };
612
+ }
613
+
614
+ /* ── History & Stats ────────────────────────────────────── */
615
+
616
+ export function listHistory(db, { ruleId, limit = 100 } = {}) {
617
+ let rows = db.prepare("SELECT * FROM perf_tuning_history").all();
618
+ rows = rows.map(_strip);
619
+ if (ruleId) rows = rows.filter((r) => r.rule_id === ruleId);
620
+ return rows
621
+ .sort((a, b) => b.created_at - a.created_at)
622
+ .slice(0, limit)
623
+ .map((r) => ({
624
+ id: r.id,
625
+ ruleId: r.rule_id,
626
+ action: r.action,
627
+ result: _parseMaybe(r.result),
628
+ createdAt: r.created_at,
629
+ }));
630
+ }
631
+
632
+ export function getAlerts(db, { sample } = {}) {
633
+ const s = sample || getLatestSample(db);
634
+ if (!s) return [];
635
+ const cfg = getPerfConfig(db);
636
+ const alerts = [];
637
+ const push = (metric, value, threshold, level) => {
638
+ if (value > threshold) alerts.push({ metric, value, threshold, level });
639
+ };
640
+ push(
641
+ "cpuPercent",
642
+ s.cpuPercent,
643
+ cfg.thresholds.cpuPercent,
644
+ ALERT_LEVELS.WARNING,
645
+ );
646
+ push(
647
+ "memoryPercent",
648
+ s.memoryPercent,
649
+ cfg.thresholds.memoryPercent,
650
+ ALERT_LEVELS.WARNING,
651
+ );
652
+ push(
653
+ "heapPercent",
654
+ s.heapPercent,
655
+ cfg.thresholds.heapPercent,
656
+ ALERT_LEVELS.CRITICAL,
657
+ );
658
+ push(
659
+ "loadPerCore",
660
+ s.loadPerCore,
661
+ cfg.thresholds.loadPerCore,
662
+ ALERT_LEVELS.WARNING,
663
+ );
664
+ return alerts;
665
+ }
666
+
667
+ export function getPerfStats(db) {
668
+ const samples = db.prepare("SELECT * FROM perf_samples").all().map(_strip);
669
+ const recs = db
670
+ .prepare("SELECT status FROM perf_recommendations")
671
+ .all()
672
+ .map(_strip);
673
+ const history = db.prepare("SELECT * FROM perf_tuning_history").all().length;
674
+ const rules = listRules(db);
675
+
676
+ const byStatus = { pending: 0, applied: 0, dismissed: 0 };
677
+ for (const r of recs) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
678
+
679
+ let avgCpu = null;
680
+ let avgMem = null;
681
+ let avgHeap = null;
682
+ if (samples.length) {
683
+ avgCpu = _round(
684
+ samples.reduce((s, r) => s + (r.cpu_percent || 0), 0) / samples.length,
685
+ );
686
+ avgMem = _round(
687
+ samples.reduce((s, r) => s + (r.memory_percent || 0), 0) / samples.length,
688
+ );
689
+ avgHeap = _round(
690
+ samples.reduce((s, r) => s + (r.heap_percent || 0), 0) / samples.length,
691
+ );
692
+ }
693
+
694
+ return {
695
+ samples: samples.length,
696
+ rules: {
697
+ total: rules.length,
698
+ enabled: rules.filter((r) => r.enabled).length,
699
+ triggered: rules.reduce((s, r) => s + (r.totalTriggered || 0), 0),
700
+ },
701
+ recommendations: { ...byStatus, total: recs.length },
702
+ historyEntries: history,
703
+ averages: {
704
+ cpuPercent: avgCpu,
705
+ memoryPercent: avgMem,
706
+ heapPercent: avgHeap,
707
+ },
708
+ };
709
+ }
710
+
711
+ export function getPerformanceReport(db) {
712
+ const latest = getLatestSample(db);
713
+ const alerts = getAlerts(db, { sample: latest });
714
+ const stats = getPerfStats(db);
715
+ const recentHistory = listHistory(db, { limit: 10 });
716
+ const pending = listRecommendations(db, {
717
+ status: RECOMMENDATION_STATUS.PENDING,
718
+ limit: 10,
719
+ });
720
+ return {
721
+ generatedAt: _now(),
722
+ sample: latest,
723
+ alerts,
724
+ stats,
725
+ pendingRecommendations: pending,
726
+ recentHistory,
727
+ };
728
+ }
729
+
730
+ /* ── Reset (for tests) ──────────────────────────────────── */
731
+
732
+ export function _resetState() {
733
+ /* CLI is stateless; helper exists for parity with other libs */
734
+ }