principles-disciple 1.7.3 → 1.7.5

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 (67) hide show
  1. package/dist/commands/evolution-status.js +4 -2
  2. package/dist/commands/focus.js +30 -155
  3. package/dist/constants/diagnostician.d.ts +16 -0
  4. package/dist/constants/diagnostician.js +60 -0
  5. package/dist/constants/tools.d.ts +2 -2
  6. package/dist/constants/tools.js +1 -1
  7. package/dist/core/config.d.ts +23 -0
  8. package/dist/core/config.js +26 -1
  9. package/dist/core/evolution-engine.js +1 -1
  10. package/dist/core/evolution-logger.d.ts +137 -0
  11. package/dist/core/evolution-logger.js +256 -0
  12. package/dist/core/evolution-reducer.d.ts +23 -0
  13. package/dist/core/evolution-reducer.js +73 -29
  14. package/dist/core/evolution-types.d.ts +6 -0
  15. package/dist/core/focus-history.d.ts +145 -0
  16. package/dist/core/focus-history.js +919 -0
  17. package/dist/core/init.js +24 -0
  18. package/dist/core/profile.js +1 -1
  19. package/dist/core/risk-calculator.d.ts +15 -0
  20. package/dist/core/risk-calculator.js +48 -0
  21. package/dist/core/trajectory.d.ts +73 -0
  22. package/dist/core/trajectory.js +206 -0
  23. package/dist/hooks/gate.js +130 -20
  24. package/dist/hooks/lifecycle.js +104 -0
  25. package/dist/hooks/pain.js +31 -0
  26. package/dist/hooks/prompt.js +136 -38
  27. package/dist/hooks/subagent.d.ts +1 -0
  28. package/dist/hooks/subagent.js +200 -18
  29. package/dist/http/principles-console-route.d.ts +7 -0
  30. package/dist/http/principles-console-route.js +301 -1
  31. package/dist/index.js +0 -2
  32. package/dist/service/central-database.d.ts +104 -0
  33. package/dist/service/central-database.js +648 -0
  34. package/dist/service/control-ui-query-service.d.ts +2 -0
  35. package/dist/service/control-ui-query-service.js +4 -0
  36. package/dist/service/empathy-observer-manager.d.ts +8 -0
  37. package/dist/service/empathy-observer-manager.js +40 -0
  38. package/dist/service/evolution-query-service.d.ts +155 -0
  39. package/dist/service/evolution-query-service.js +258 -0
  40. package/dist/service/evolution-worker.d.ts +4 -0
  41. package/dist/service/evolution-worker.js +185 -63
  42. package/dist/service/phase3-input-filter.d.ts +37 -0
  43. package/dist/service/phase3-input-filter.js +106 -0
  44. package/dist/service/runtime-summary-service.d.ts +15 -0
  45. package/dist/service/runtime-summary-service.js +111 -23
  46. package/dist/tools/deep-reflect.js +8 -2
  47. package/dist/utils/subagent-probe.d.ts +34 -0
  48. package/dist/utils/subagent-probe.js +81 -0
  49. package/openclaw.plugin.json +1 -1
  50. package/package.json +6 -4
  51. package/templates/langs/en/core/AGENTS.md +15 -3
  52. package/templates/langs/en/core/BOOTSTRAP.md +24 -1
  53. package/templates/langs/en/core/TOOLS.md +9 -0
  54. package/templates/langs/zh/core/AGENTS.md +15 -3
  55. package/templates/langs/zh/core/BOOTSTRAP.md +24 -1
  56. package/templates/langs/zh/core/TOOLS.md +9 -0
  57. package/templates/langs/zh/skills/pd-auditor/SKILL.md +61 -0
  58. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +287 -0
  59. package/templates/langs/zh/skills/pd-explorer/SKILL.md +65 -0
  60. package/templates/langs/zh/skills/pd-implementer/SKILL.md +68 -0
  61. package/templates/langs/zh/skills/pd-planner/SKILL.md +65 -0
  62. package/templates/langs/zh/skills/pd-reporter/SKILL.md +78 -0
  63. package/templates/langs/zh/skills/pd-reviewer/SKILL.md +66 -0
  64. package/dist/core/agent-loader.d.ts +0 -44
  65. package/dist/core/agent-loader.js +0 -147
  66. package/dist/tools/agent-spawn.d.ts +0 -54
  67. package/dist/tools/agent-spawn.js +0 -445
@@ -0,0 +1,648 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ const CENTRAL_DB_DIR = '.central';
6
+ const CENTRAL_DB_NAME = 'aggregated.db';
7
+ /**
8
+ * Central database that aggregates data from all agent workspaces.
9
+ * Stored in ~/.openclaw/.central/ (NOT in memory/ which is for embeddings)
10
+ */
11
+ export class CentralDatabase {
12
+ dbPath;
13
+ db;
14
+ workspaces = [];
15
+ constructor() {
16
+ const openClawDir = os.homedir();
17
+ this.dbPath = path.join(openClawDir, '.openclaw', CENTRAL_DB_DIR, CENTRAL_DB_NAME);
18
+ // Ensure directory exists
19
+ fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
20
+ this.db = new Database(this.dbPath);
21
+ this.db.pragma('journal_mode = WAL');
22
+ this.db.pragma('synchronous = NORMAL');
23
+ this.initSchema();
24
+ this.discoverWorkspaces();
25
+ }
26
+ dispose() {
27
+ this.db.close();
28
+ }
29
+ tableExists(db, tableName) {
30
+ const result = db.prepare(`
31
+ SELECT name FROM sqlite_master WHERE type='table' AND name=?
32
+ `).get(tableName);
33
+ return !!result;
34
+ }
35
+ initSchema() {
36
+ this.db.exec(`
37
+ CREATE TABLE IF NOT EXISTS schema_version (
38
+ version INTEGER NOT NULL
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS workspaces (
42
+ name TEXT PRIMARY KEY,
43
+ path TEXT NOT NULL,
44
+ last_sync TEXT
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS workspace_config (
48
+ workspace_name TEXT PRIMARY KEY,
49
+ enabled INTEGER NOT NULL DEFAULT 1,
50
+ display_name TEXT,
51
+ sync_enabled INTEGER NOT NULL DEFAULT 1,
52
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
53
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS global_config (
57
+ key TEXT PRIMARY KEY,
58
+ value TEXT NOT NULL,
59
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
60
+ );
61
+
62
+ CREATE TABLE IF NOT EXISTS aggregated_sessions (
63
+ session_id TEXT PRIMARY KEY,
64
+ workspace TEXT NOT NULL,
65
+ started_at TEXT NOT NULL,
66
+ updated_at TEXT NOT NULL
67
+ );
68
+
69
+ CREATE TABLE IF NOT EXISTS aggregated_tool_calls (
70
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ workspace TEXT NOT NULL,
72
+ session_id TEXT NOT NULL,
73
+ tool_name TEXT NOT NULL,
74
+ outcome TEXT NOT NULL,
75
+ duration_ms INTEGER,
76
+ error_type TEXT,
77
+ error_message TEXT,
78
+ created_at TEXT NOT NULL
79
+ );
80
+
81
+ CREATE TABLE IF NOT EXISTS aggregated_pain_events (
82
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
83
+ workspace TEXT NOT NULL,
84
+ session_id TEXT NOT NULL,
85
+ source TEXT NOT NULL,
86
+ score REAL NOT NULL,
87
+ reason TEXT,
88
+ created_at TEXT NOT NULL
89
+ );
90
+
91
+ CREATE TABLE IF NOT EXISTS aggregated_user_corrections (
92
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
93
+ workspace TEXT NOT NULL,
94
+ session_id TEXT NOT NULL,
95
+ correction_cue TEXT,
96
+ created_at TEXT NOT NULL
97
+ );
98
+
99
+ CREATE TABLE IF NOT EXISTS aggregated_principle_events (
100
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
101
+ workspace TEXT NOT NULL,
102
+ principle_id TEXT,
103
+ event_type TEXT NOT NULL,
104
+ created_at TEXT NOT NULL
105
+ );
106
+
107
+ CREATE TABLE IF NOT EXISTS aggregated_thinking_events (
108
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
109
+ workspace TEXT NOT NULL,
110
+ session_id TEXT NOT NULL,
111
+ model_id TEXT NOT NULL,
112
+ matched_pattern TEXT NOT NULL,
113
+ created_at TEXT NOT NULL
114
+ );
115
+
116
+ CREATE TABLE IF NOT EXISTS aggregated_correction_samples (
117
+ sample_id TEXT PRIMARY KEY,
118
+ workspace TEXT NOT NULL,
119
+ session_id TEXT NOT NULL,
120
+ bad_assistant_turn_id INTEGER NOT NULL,
121
+ quality_score REAL,
122
+ review_status TEXT,
123
+ created_at TEXT NOT NULL
124
+ );
125
+
126
+ CREATE TABLE IF NOT EXISTS aggregated_task_outcomes (
127
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
128
+ workspace TEXT NOT NULL,
129
+ session_id TEXT NOT NULL,
130
+ task_id TEXT,
131
+ outcome TEXT NOT NULL,
132
+ created_at TEXT NOT NULL
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS sync_log (
136
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
137
+ workspace TEXT NOT NULL,
138
+ synced_at TEXT NOT NULL,
139
+ records_synced INTEGER NOT NULL
140
+ );
141
+
142
+ -- Indexes for performance
143
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_workspace ON aggregated_tool_calls(workspace);
144
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_outcome ON aggregated_tool_calls(outcome);
145
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_created ON aggregated_tool_calls(created_at);
146
+ CREATE INDEX IF NOT EXISTS idx_pain_workspace ON aggregated_pain_events(workspace);
147
+ CREATE INDEX IF NOT EXISTS idx_pain_created ON aggregated_pain_events(created_at);
148
+ CREATE INDEX IF NOT EXISTS idx_thinking_workspace ON aggregated_thinking_events(workspace);
149
+ CREATE INDEX IF NOT EXISTS idx_thinking_model ON aggregated_thinking_events(model_id);
150
+ CREATE INDEX IF NOT EXISTS idx_corrections_workspace ON aggregated_correction_samples(workspace);
151
+ CREATE INDEX IF NOT EXISTS idx_sessions_workspace ON aggregated_sessions(workspace);
152
+ `);
153
+ }
154
+ discoverWorkspaces() {
155
+ const openClawDir = os.homedir();
156
+ const workspacesDir = path.join(openClawDir, '.openclaw');
157
+ this.workspaces.length = 0;
158
+ const entries = fs.readdirSync(workspacesDir);
159
+ for (const entry of entries) {
160
+ if (entry.startsWith('workspace-') && entry !== 'workspace') {
161
+ const workspacePath = path.join(workspacesDir, entry);
162
+ const stat = fs.statSync(workspacePath);
163
+ if (stat.isDirectory()) {
164
+ this.workspaces.push({
165
+ name: entry,
166
+ path: workspacePath,
167
+ lastSync: null,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Sync data from a single workspace into the central database
175
+ */
176
+ syncWorkspace(workspaceName) {
177
+ const workspace = this.workspaces.find(w => w.name === workspaceName);
178
+ if (!workspace) {
179
+ throw new Error(`Workspace not found: ${workspaceName}`);
180
+ }
181
+ const trajectoryDbPath = path.join(workspace.path, '.state', 'trajectory.db');
182
+ if (!fs.existsSync(trajectoryDbPath)) {
183
+ return 0;
184
+ }
185
+ const sourceDb = new Database(trajectoryDbPath, { readonly: true });
186
+ let totalSynced = 0;
187
+ try {
188
+ // Sync sessions
189
+ const sessions = sourceDb.prepare(`
190
+ SELECT session_id, started_at, updated_at FROM sessions
191
+ `).all();
192
+ const insertSession = this.db.prepare(`
193
+ INSERT OR REPLACE INTO aggregated_sessions (session_id, workspace, started_at, updated_at)
194
+ VALUES (?, ?, ?, ?)
195
+ `);
196
+ for (const s of sessions) {
197
+ insertSession.run(s.session_id, workspaceName, s.started_at, s.updated_at);
198
+ totalSynced++;
199
+ }
200
+ // Sync tool_calls
201
+ const toolCalls = sourceDb.prepare(`
202
+ SELECT session_id, tool_name, outcome, duration_ms, error_type, error_message, created_at
203
+ FROM tool_calls
204
+ `).all();
205
+ const insertTool = this.db.prepare(`
206
+ INSERT INTO aggregated_tool_calls
207
+ (workspace, session_id, tool_name, outcome, duration_ms, error_type, error_message, created_at)
208
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
209
+ `);
210
+ for (const t of toolCalls) {
211
+ insertTool.run(workspaceName, t.session_id, t.tool_name, t.outcome, t.duration_ms, t.error_type, t.error_message, t.created_at);
212
+ totalSynced++;
213
+ }
214
+ // Sync pain_events
215
+ const painEvents = sourceDb.prepare(`
216
+ SELECT session_id, source, score, reason, created_at FROM pain_events
217
+ `).all();
218
+ const insertPain = this.db.prepare(`
219
+ INSERT INTO aggregated_pain_events (workspace, session_id, source, score, reason, created_at)
220
+ VALUES (?, ?, ?, ?, ?, ?)
221
+ `);
222
+ for (const p of painEvents) {
223
+ insertPain.run(workspaceName, p.session_id, p.source, p.score, p.reason, p.created_at);
224
+ totalSynced++;
225
+ }
226
+ // Sync user corrections
227
+ const corrections = sourceDb.prepare(`
228
+ SELECT session_id, correction_cue, created_at FROM user_turns
229
+ WHERE correction_detected = 1
230
+ `).all();
231
+ const insertCorr = this.db.prepare(`
232
+ INSERT INTO aggregated_user_corrections (workspace, session_id, correction_cue, created_at)
233
+ VALUES (?, ?, ?, ?)
234
+ `);
235
+ for (const c of corrections) {
236
+ insertCorr.run(workspaceName, c.session_id, c.correction_cue, c.created_at);
237
+ totalSynced++;
238
+ }
239
+ // Sync principle_events
240
+ const principles = sourceDb.prepare(`
241
+ SELECT principle_id, event_type, created_at FROM principle_events
242
+ `).all();
243
+ const insertPrinciple = this.db.prepare(`
244
+ INSERT INTO aggregated_principle_events (workspace, principle_id, event_type, created_at)
245
+ VALUES (?, ?, ?, ?)
246
+ `);
247
+ for (const p of principles) {
248
+ insertPrinciple.run(workspaceName, p.principle_id, p.event_type, p.created_at);
249
+ totalSynced++;
250
+ }
251
+ // Sync thinking_model_events (may not exist in older workspaces)
252
+ if (this.tableExists(sourceDb, 'thinking_model_events')) {
253
+ const thinking = sourceDb.prepare(`
254
+ SELECT session_id, model_id, matched_pattern, created_at FROM thinking_model_events
255
+ `).all();
256
+ const insertThinking = this.db.prepare(`
257
+ INSERT INTO aggregated_thinking_events (workspace, session_id, model_id, matched_pattern, created_at)
258
+ VALUES (?, ?, ?, ?, ?)
259
+ `);
260
+ for (const t of thinking) {
261
+ insertThinking.run(workspaceName, t.session_id, t.model_id, t.matched_pattern, t.created_at);
262
+ totalSynced++;
263
+ }
264
+ }
265
+ // Sync correction_samples
266
+ const samples = sourceDb.prepare(`
267
+ SELECT sample_id, session_id, bad_assistant_turn_id, quality_score, review_status, created_at
268
+ FROM correction_samples
269
+ `).all();
270
+ const insertSample = this.db.prepare(`
271
+ INSERT OR REPLACE INTO aggregated_correction_samples
272
+ (sample_id, workspace, session_id, bad_assistant_turn_id, quality_score, review_status, created_at)
273
+ VALUES (?, ?, ?, ?, ?, ?, ?)
274
+ `);
275
+ for (const s of samples) {
276
+ insertSample.run(s.sample_id, workspaceName, s.session_id, s.bad_assistant_turn_id, s.quality_score, s.review_status, s.created_at);
277
+ totalSynced++;
278
+ }
279
+ // Sync task_outcomes
280
+ const outcomes = sourceDb.prepare(`
281
+ SELECT session_id, task_id, outcome, created_at FROM task_outcomes
282
+ `).all();
283
+ const insertOutcome = this.db.prepare(`
284
+ INSERT INTO aggregated_task_outcomes (workspace, session_id, task_id, outcome, created_at)
285
+ VALUES (?, ?, ?, ?, ?)
286
+ `);
287
+ for (const o of outcomes) {
288
+ insertOutcome.run(workspaceName, o.session_id, o.task_id, o.outcome, o.created_at);
289
+ totalSynced++;
290
+ }
291
+ // Update last sync time
292
+ this.db.prepare(`
293
+ INSERT OR REPLACE INTO workspaces (name, path, last_sync)
294
+ VALUES (?, ?, datetime('now'))
295
+ `).run(workspaceName, workspace.path);
296
+ // Log sync
297
+ this.db.prepare(`
298
+ INSERT INTO sync_log (workspace, synced_at, records_synced)
299
+ VALUES (?, datetime('now'), ?)
300
+ `).run(workspaceName, totalSynced);
301
+ return totalSynced;
302
+ }
303
+ finally {
304
+ sourceDb.close();
305
+ }
306
+ }
307
+ syncEnabled() {
308
+ const results = new Map();
309
+ for (const ws of this.getEnabledWorkspaces()) {
310
+ try {
311
+ const count = this.syncWorkspace(ws.name);
312
+ results.set(ws.name, count);
313
+ }
314
+ catch (error) {
315
+ console.error(`Failed to sync workspace ${ws.name}:`, error);
316
+ results.set(ws.name, 0);
317
+ }
318
+ }
319
+ return results;
320
+ }
321
+ /**
322
+ * Sync all workspaces (legacy method - syncs all regardless of config)
323
+ */
324
+ syncAll() {
325
+ const results = new Map();
326
+ for (const ws of this.workspaces) {
327
+ try {
328
+ const count = this.syncWorkspace(ws.name);
329
+ results.set(ws.name, count);
330
+ }
331
+ catch (error) {
332
+ console.error(`Failed to sync workspace ${ws.name}:`, error);
333
+ results.set(ws.name, 0);
334
+ }
335
+ }
336
+ return results;
337
+ }
338
+ getEnabledWorkspaceFilter() {
339
+ const enabled = this.getWorkspaceConfigs().filter(c => c.enabled && c.syncEnabled);
340
+ if (enabled.length === 0)
341
+ return "''";
342
+ return enabled.map(c => `'${c.workspaceName.replace(/'/g, "''")}'`).join(', ');
343
+ }
344
+ /**
345
+ * Get aggregated overview stats (only from enabled workspaces)
346
+ */
347
+ getOverviewStats() {
348
+ const filter = this.getEnabledWorkspaceFilter();
349
+ const totalSessions = this.db.prepare(`
350
+ SELECT COUNT(DISTINCT session_id) as count FROM aggregated_sessions
351
+ WHERE workspace IN (${filter})
352
+ `).get();
353
+ const toolStats = this.db.prepare(`
354
+ SELECT
355
+ COUNT(*) as total,
356
+ SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END) as failures
357
+ FROM aggregated_tool_calls
358
+ WHERE workspace IN (${filter})
359
+ `).get();
360
+ const painEvents = this.db.prepare(`
361
+ SELECT COUNT(*) as count FROM aggregated_pain_events
362
+ WHERE workspace IN (${filter})
363
+ `).get();
364
+ const corrections = this.db.prepare(`
365
+ SELECT COUNT(*) as count FROM aggregated_user_corrections
366
+ WHERE workspace IN (${filter})
367
+ `).get();
368
+ const thinkingEvents = this.db.prepare(`
369
+ SELECT COUNT(*) as count FROM aggregated_thinking_events
370
+ WHERE workspace IN (${filter})
371
+ `).get();
372
+ const sampleStats = this.db.prepare(`
373
+ SELECT
374
+ COUNT(*) as total,
375
+ SUM(CASE WHEN review_status = 'pending' THEN 1 ELSE 0 END) as pending,
376
+ SUM(CASE WHEN review_status = 'approved' THEN 1 ELSE 0 END) as approved,
377
+ SUM(CASE WHEN review_status = 'rejected' THEN 1 ELSE 0 END) as rejected
378
+ FROM aggregated_correction_samples
379
+ WHERE workspace IN (${filter})
380
+ `).get();
381
+ const workspaces = this.db.prepare(`
382
+ SELECT name FROM workspaces ORDER BY name
383
+ `).all();
384
+ const enabledConfigs = this.getWorkspaceConfigs().filter(c => c.enabled && c.syncEnabled);
385
+ const enabledWorkspaceNames = enabledConfigs.map(c => c.workspaceName);
386
+ return {
387
+ totalSessions: totalSessions.count,
388
+ totalToolCalls: toolStats.total,
389
+ totalFailures: toolStats.failures || 0,
390
+ totalPainEvents: painEvents.count,
391
+ totalCorrections: corrections.count,
392
+ totalThinkingEvents: thinkingEvents.count,
393
+ totalSamples: sampleStats.total,
394
+ pendingSamples: sampleStats.pending || 0,
395
+ approvedSamples: sampleStats.approved || 0,
396
+ rejectedSamples: sampleStats.rejected || 0,
397
+ workspaceCount: workspaces.length,
398
+ enabledWorkspaceCount: enabledConfigs.length,
399
+ workspaceNames: workspaces.map(w => w.name),
400
+ enabledWorkspaceNames,
401
+ };
402
+ }
403
+ /**
404
+ * Get daily trend data
405
+ */
406
+ getDailyTrend(days = 7) {
407
+ const cutoffDate = new Date();
408
+ cutoffDate.setDate(cutoffDate.getDate() - days);
409
+ const cutoffStr = cutoffDate.toISOString().split('T')[0];
410
+ const toolDaily = this.db.prepare(`
411
+ SELECT
412
+ substr(created_at, 1, 10) as day,
413
+ COUNT(*) as tool_calls,
414
+ SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END) as failures
415
+ FROM aggregated_tool_calls
416
+ WHERE substr(created_at, 1, 10) >= ?
417
+ GROUP BY substr(created_at, 1, 10)
418
+ ORDER BY day
419
+ `).all(cutoffStr);
420
+ const correctionsDaily = this.db.prepare(`
421
+ SELECT
422
+ substr(created_at, 1, 10) as day,
423
+ COUNT(*) as corrections
424
+ FROM aggregated_user_corrections
425
+ WHERE substr(created_at, 1, 10) >= ?
426
+ GROUP BY substr(created_at, 1, 10)
427
+ `).all(cutoffStr);
428
+ const thinkingDaily = this.db.prepare(`
429
+ SELECT
430
+ substr(created_at, 1, 10) as day,
431
+ COUNT(*) as thinking_turns
432
+ FROM aggregated_thinking_events
433
+ WHERE substr(created_at, 1, 10) >= ?
434
+ GROUP BY substr(created_at, 1, 10)
435
+ `).all(cutoffStr);
436
+ // Merge all trends
437
+ const dayMap = new Map();
438
+ for (const t of toolDaily) {
439
+ dayMap.set(t.day, {
440
+ day: t.day,
441
+ toolCalls: t.tool_calls,
442
+ failures: t.failures || 0,
443
+ userCorrections: 0,
444
+ thinkingTurns: 0,
445
+ });
446
+ }
447
+ for (const c of correctionsDaily) {
448
+ const existing = dayMap.get(c.day);
449
+ if (existing) {
450
+ existing.userCorrections = c.corrections;
451
+ }
452
+ else {
453
+ dayMap.set(c.day, {
454
+ day: c.day,
455
+ toolCalls: 0,
456
+ failures: 0,
457
+ userCorrections: c.corrections,
458
+ thinkingTurns: 0,
459
+ });
460
+ }
461
+ }
462
+ for (const t of thinkingDaily) {
463
+ const existing = dayMap.get(t.day);
464
+ if (existing) {
465
+ existing.thinkingTurns = t.thinking_turns;
466
+ }
467
+ else {
468
+ dayMap.set(t.day, {
469
+ day: t.day,
470
+ toolCalls: 0,
471
+ failures: 0,
472
+ userCorrections: 0,
473
+ thinkingTurns: t.thinking_turns,
474
+ });
475
+ }
476
+ }
477
+ return Array.from(dayMap.values()).sort((a, b) => a.day.localeCompare(b.day));
478
+ }
479
+ /**
480
+ * Get top regressions
481
+ */
482
+ getTopRegressions(limit = 5) {
483
+ return this.db.prepare(`
484
+ SELECT
485
+ tool_name as toolName,
486
+ error_type as errorType,
487
+ COUNT(*) as occurrences
488
+ FROM aggregated_tool_calls
489
+ WHERE outcome = 'failure' AND error_type IS NOT NULL
490
+ GROUP BY tool_name, error_type
491
+ ORDER BY occurrences DESC
492
+ LIMIT ?
493
+ `).all(limit);
494
+ }
495
+ /**
496
+ * Get thinking model stats
497
+ */
498
+ getThinkingModelStats() {
499
+ const totalModels = this.db.prepare(`
500
+ SELECT COUNT(DISTINCT model_id) as count FROM aggregated_thinking_events
501
+ `).get();
502
+ // Consider a model "active" if it has events in the last 7 days
503
+ const recentDate = new Date();
504
+ recentDate.setDate(recentDate.getDate() - 7);
505
+ const recentStr = recentDate.toISOString();
506
+ const activeModels = this.db.prepare(`
507
+ SELECT COUNT(DISTINCT model_id) as count FROM aggregated_thinking_events
508
+ WHERE created_at >= ?
509
+ `).get(recentStr);
510
+ const totalToolCalls = this.db.prepare(`
511
+ SELECT COUNT(*) as count FROM aggregated_tool_calls
512
+ `).get();
513
+ const models = this.db.prepare(`
514
+ SELECT
515
+ model_id as modelId,
516
+ COUNT(*) as hits
517
+ FROM aggregated_thinking_events
518
+ GROUP BY model_id
519
+ ORDER BY hits DESC
520
+ `).all();
521
+ const coverageRate = totalToolCalls.count > 0
522
+ ? models.reduce((sum, m) => sum + m.hits, 0) / totalToolCalls.count
523
+ : 0;
524
+ return {
525
+ totalModels: totalModels.count,
526
+ activeModels: activeModels.count,
527
+ models: models.map(m => ({
528
+ ...m,
529
+ coverageRate: totalToolCalls.count > 0 ? m.hits / totalToolCalls.count : 0,
530
+ })),
531
+ };
532
+ }
533
+ /**
534
+ * Get workspace list
535
+ */
536
+ getWorkspaces() {
537
+ return this.db.prepare(`
538
+ SELECT name, path, last_sync as lastSync FROM workspaces ORDER BY name
539
+ `).all();
540
+ }
541
+ getWorkspaceConfigs() {
542
+ const configs = this.db.prepare(`
543
+ SELECT workspace_name, enabled, display_name, sync_enabled
544
+ FROM workspace_config
545
+ ORDER BY workspace_name
546
+ `).all();
547
+ return configs.map(c => ({
548
+ workspaceName: c.workspace_name,
549
+ enabled: c.enabled === 1,
550
+ displayName: c.display_name,
551
+ syncEnabled: c.sync_enabled === 1,
552
+ }));
553
+ }
554
+ updateWorkspaceConfig(workspaceName, updates) {
555
+ const existing = this.db.prepare(`
556
+ SELECT workspace_name FROM workspace_config WHERE workspace_name = ?
557
+ `).get(workspaceName);
558
+ if (existing) {
559
+ const setClauses = ['updated_at = datetime(\'now\')'];
560
+ const params = [];
561
+ if (updates.enabled !== undefined) {
562
+ setClauses.push('enabled = ?');
563
+ params.push(updates.enabled ? 1 : 0);
564
+ }
565
+ if (updates.displayName !== undefined) {
566
+ setClauses.push('display_name = ?');
567
+ params.push(updates.displayName);
568
+ }
569
+ if (updates.syncEnabled !== undefined) {
570
+ setClauses.push('sync_enabled = ?');
571
+ params.push(updates.syncEnabled ? 1 : 0);
572
+ }
573
+ params.push(workspaceName);
574
+ this.db.prepare(`
575
+ UPDATE workspace_config SET ${setClauses.join(', ')} WHERE workspace_name = ?
576
+ `).run(...params);
577
+ }
578
+ else {
579
+ this.db.prepare(`
580
+ INSERT INTO workspace_config (workspace_name, enabled, display_name, sync_enabled)
581
+ VALUES (?, ?, ?, ?)
582
+ `).run(workspaceName, updates.enabled !== undefined ? (updates.enabled ? 1 : 0) : 1, updates.displayName ?? null, updates.syncEnabled !== undefined ? (updates.syncEnabled ? 1 : 0) : 1);
583
+ }
584
+ }
585
+ isWorkspaceEnabled(workspaceName) {
586
+ const config = this.db.prepare(`
587
+ SELECT enabled, sync_enabled FROM workspace_config WHERE workspace_name = ?
588
+ `).get(workspaceName);
589
+ if (!config)
590
+ return true;
591
+ return config.enabled === 1 && config.sync_enabled === 1;
592
+ }
593
+ getEnabledWorkspaces() {
594
+ return this.workspaces.filter(ws => this.isWorkspaceEnabled(ws.name));
595
+ }
596
+ addCustomWorkspace(name, workspacePath) {
597
+ if (!this.workspaces.find(ws => ws.name === name)) {
598
+ this.workspaces.push({ name, path: workspacePath, lastSync: null });
599
+ this.db.prepare(`
600
+ INSERT INTO workspaces (name, path, last_sync) VALUES (?, ?, NULL)
601
+ `).run(name, workspacePath);
602
+ this.db.prepare(`
603
+ INSERT INTO workspace_config (workspace_name, enabled, display_name, sync_enabled)
604
+ VALUES (?, 1, ?, 1)
605
+ `).run(name, name);
606
+ }
607
+ }
608
+ removeWorkspace(workspaceName) {
609
+ this.updateWorkspaceConfig(workspaceName, { enabled: false, syncEnabled: false });
610
+ }
611
+ getGlobalConfig(key) {
612
+ const result = this.db.prepare(`
613
+ SELECT value FROM global_config WHERE key = ?
614
+ `).get(key);
615
+ return result?.value ?? null;
616
+ }
617
+ setGlobalConfig(key, value) {
618
+ this.db.prepare(`
619
+ INSERT OR REPLACE INTO global_config (key, value, updated_at)
620
+ VALUES (?, ?, datetime('now'))
621
+ `).run(key, value);
622
+ }
623
+ /**
624
+ * Clear all aggregated data (for testing/reset)
625
+ */
626
+ clearAll() {
627
+ this.db.exec(`
628
+ DELETE FROM aggregated_sessions;
629
+ DELETE FROM aggregated_tool_calls;
630
+ DELETE FROM aggregated_pain_events;
631
+ DELETE FROM aggregated_user_corrections;
632
+ DELETE FROM aggregated_principle_events;
633
+ DELETE FROM aggregated_thinking_events;
634
+ DELETE FROM aggregated_correction_samples;
635
+ DELETE FROM aggregated_task_outcomes;
636
+ DELETE FROM workspaces;
637
+ DELETE FROM sync_log;
638
+ `);
639
+ }
640
+ }
641
+ // Singleton instance
642
+ let centralDbInstance = null;
643
+ export function getCentralDatabase() {
644
+ if (!centralDbInstance) {
645
+ centralDbInstance = new CentralDatabase();
646
+ }
647
+ return centralDbInstance;
648
+ }
@@ -12,6 +12,8 @@ export interface OverviewResponse {
12
12
  thinkingCoverageRate: number;
13
13
  painEvents: number;
14
14
  principleEventCount: number;
15
+ gateBlocks: number;
16
+ taskOutcomes: number;
15
17
  };
16
18
  dailyTrend: Array<{
17
19
  day: string;
@@ -54,6 +54,8 @@ export class ControlUiQueryService {
54
54
  `) ?? { total_failures: 0, repeated_failures: 0 };
55
55
  const correctionTotal = this.uiDb.get('SELECT COUNT(*) AS count FROM user_turns WHERE correction_detected = 1')?.count ?? 0;
56
56
  const principleEventCount = this.uiDb.get('SELECT COUNT(*) AS count FROM principle_events')?.count ?? 0;
57
+ const gateBlockCount = this.uiDb.get('SELECT COUNT(*) AS count FROM gate_blocks')?.count ?? 0;
58
+ const taskOutcomeCount = this.uiDb.get('SELECT COUNT(*) AS count FROM task_outcomes')?.count ?? 0;
57
59
  const sampleCounters = this.uiDb.all('SELECT review_status, total FROM v_sample_queue');
58
60
  const samplePreview = this.uiDb.all(`
59
61
  SELECT sample_id, session_id, quality_score, review_status, created_at
@@ -108,6 +110,8 @@ export class ControlUiQueryService {
108
110
  thinkingCoverageRate: roundRate(coverageRow.thinking_turns, coverageRow.assistant_turns),
109
111
  painEvents: stats.painEvents,
110
112
  principleEventCount,
113
+ gateBlocks: Number(gateBlockCount),
114
+ taskOutcomes: Number(taskOutcomeCount),
111
115
  },
112
116
  dailyTrend: dailyTrend.map((row) => ({
113
117
  day: row.day,
@@ -30,6 +30,14 @@ export declare class EmpathyObserverManager {
30
30
  private sessionLocks;
31
31
  private constructor();
32
32
  static getInstance(): EmpathyObserverManager;
33
+ /**
34
+ * Probe whether the subagent runtime is actually functional.
35
+ * api.runtime.subagent always exists (it's a Proxy), but in embedded mode
36
+ * every method throws "only available during a gateway request".
37
+ * We cache the result to avoid repeated probing.
38
+ */
39
+ private subagentAvailableCache;
40
+ private isSubagentAvailable;
33
41
  shouldTrigger(api: EmpathyObserverApi | null | undefined, sessionId: string): boolean;
34
42
  spawn(api: EmpathyObserverApi | null | undefined, sessionId: string, userMessage: string): Promise<string | null>;
35
43
  reap(api: EmpathyObserverApi | null | undefined, targetSessionKey: string, workspaceDir: string): Promise<void>;