openclaw-node-harness 2.0.4 → 2.1.1

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 (134) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/lane-watchdog.js +23 -2
  4. package/bin/mesh-agent.js +439 -28
  5. package/bin/mesh-bridge.js +69 -3
  6. package/bin/mesh-health-publisher.js +41 -1
  7. package/bin/mesh-task-daemon.js +821 -26
  8. package/bin/mesh.js +411 -20
  9. package/config/claude-settings.json +95 -0
  10. package/config/daemon.json.template +2 -1
  11. package/config/git-hooks/pre-commit +13 -0
  12. package/config/git-hooks/pre-push +12 -0
  13. package/config/harness-rules.json +174 -0
  14. package/config/plan-templates/team-bugfix.yaml +52 -0
  15. package/config/plan-templates/team-deploy.yaml +50 -0
  16. package/config/plan-templates/team-feature.yaml +71 -0
  17. package/config/roles/qa-engineer.yaml +36 -0
  18. package/config/roles/solidity-dev.yaml +51 -0
  19. package/config/roles/tech-architect.yaml +36 -0
  20. package/config/rules/framework/solidity.md +22 -0
  21. package/config/rules/framework/typescript.md +21 -0
  22. package/config/rules/framework/unity.md +21 -0
  23. package/config/rules/universal/design-docs.md +18 -0
  24. package/config/rules/universal/git-hygiene.md +18 -0
  25. package/config/rules/universal/security.md +19 -0
  26. package/config/rules/universal/test-standards.md +19 -0
  27. package/identity/DELEGATION.md +6 -6
  28. package/install.sh +296 -10
  29. package/lib/agent-activity.js +2 -2
  30. package/lib/circling-parser.js +119 -0
  31. package/lib/exec-safety.js +105 -0
  32. package/lib/hyperagent-store.mjs +652 -0
  33. package/lib/kanban-io.js +24 -31
  34. package/lib/llm-providers.js +16 -0
  35. package/lib/mcp-knowledge/bench.mjs +118 -0
  36. package/lib/mcp-knowledge/core.mjs +530 -0
  37. package/lib/mcp-knowledge/package.json +25 -0
  38. package/lib/mcp-knowledge/server.mjs +252 -0
  39. package/lib/mcp-knowledge/test.mjs +802 -0
  40. package/lib/memory-budget.mjs +261 -0
  41. package/lib/mesh-collab.js +483 -165
  42. package/lib/mesh-harness.js +427 -0
  43. package/lib/mesh-plans.js +79 -50
  44. package/lib/mesh-tasks.js +132 -49
  45. package/lib/nats-resolve.js +4 -4
  46. package/lib/plan-templates.js +226 -0
  47. package/lib/pre-compression-flush.mjs +322 -0
  48. package/lib/role-loader.js +292 -0
  49. package/lib/rule-loader.js +358 -0
  50. package/lib/session-store.mjs +461 -0
  51. package/lib/transcript-parser.mjs +292 -0
  52. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  53. package/mission-control/drizzle.config.ts +1 -4
  54. package/mission-control/package-lock.json +1571 -83
  55. package/mission-control/package.json +6 -2
  56. package/mission-control/scripts/gen-chronology.js +3 -3
  57. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  58. package/mission-control/scripts/import-pipeline.js +0 -15
  59. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  60. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  61. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  62. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  63. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  64. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  65. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  66. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  67. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  68. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  69. package/mission-control/src/app/api/memory/search/route.ts +6 -3
  70. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  71. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  72. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  73. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  74. package/mission-control/src/app/api/souls/[id]/evolution/route.ts +21 -5
  75. package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
  76. package/mission-control/src/app/api/souls/[id]/propagate/route.ts +14 -2
  77. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +8 -2
  78. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  79. package/mission-control/src/app/api/tasks/route.ts +21 -30
  80. package/mission-control/src/app/api/workspace/read/route.ts +11 -0
  81. package/mission-control/src/app/cowork/page.tsx +261 -0
  82. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  83. package/mission-control/src/app/graph/page.tsx +26 -0
  84. package/mission-control/src/app/memory/page.tsx +1 -1
  85. package/mission-control/src/app/obsidian/page.tsx +36 -6
  86. package/mission-control/src/app/roadmap/page.tsx +24 -0
  87. package/mission-control/src/app/souls/page.tsx +2 -2
  88. package/mission-control/src/components/board/execution-config.tsx +431 -0
  89. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  90. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  91. package/mission-control/src/components/board/task-card.tsx +55 -2
  92. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  93. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  94. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  95. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  96. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  97. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  98. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  99. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  100. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  101. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  102. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  103. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  104. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  105. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  106. package/mission-control/src/lib/config.ts +67 -0
  107. package/mission-control/src/lib/db/index.ts +85 -1
  108. package/mission-control/src/lib/db/schema.ts +61 -3
  109. package/mission-control/src/lib/hooks.ts +309 -0
  110. package/mission-control/src/lib/memory/entities.ts +3 -2
  111. package/mission-control/src/lib/memory/extract.ts +2 -1
  112. package/mission-control/src/lib/memory/retrieval.ts +3 -2
  113. package/mission-control/src/lib/nats.ts +66 -1
  114. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  115. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  116. package/mission-control/src/lib/scheduler.ts +12 -11
  117. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  118. package/mission-control/src/lib/sync/tasks.ts +23 -1
  119. package/mission-control/src/lib/task-id.ts +32 -0
  120. package/mission-control/src/lib/tts/index.ts +33 -9
  121. package/mission-control/src/middleware.ts +82 -0
  122. package/mission-control/tsconfig.json +2 -1
  123. package/mission-control/vitest.config.ts +14 -0
  124. package/package.json +15 -2
  125. package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
  126. package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
  127. package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
  128. package/services/launchd/ai.openclaw.mission-control.plist +1 -1
  129. package/services/service-manifest.json +1 -1
  130. package/skills/cc-godmode/references/agents.md +8 -8
  131. package/uninstall.sh +37 -9
  132. package/workspace-bin/memory-daemon.mjs +199 -5
  133. package/workspace-bin/session-search.mjs +204 -0
  134. package/workspace-bin/web-fetch.mjs +65 -0
@@ -0,0 +1,652 @@
1
+ /**
2
+ * hyperagent-store.mjs — HyperAgent protocol persistence layer.
3
+ *
4
+ * SQLite tables in ~/.openclaw/state.db for the self-improving agent loop:
5
+ * - ha_telemetry: per-task performance data with auto-detected pattern flags
6
+ * - ha_strategies: reusable approaches indexed by domain/subdomain
7
+ * - ha_reflections: periodic structured analysis (raw stats + LLM synthesis)
8
+ * - ha_proposals: self-modification proposals with shadow eval + human gate
9
+ * - ha_telemetry_proposals: junction for overlapping eval windows
10
+ *
11
+ * Follows session-store.mjs patterns: same DB, WAL mode, better-sqlite3, sync API.
12
+ *
13
+ * External dependency: better-sqlite3
14
+ */
15
+
16
+ import Database from 'better-sqlite3';
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import os from 'os';
20
+
21
+ const DEFAULT_DB_PATH = path.join(os.homedir(), '.openclaw/state.db');
22
+
23
+ export class HyperAgentStore {
24
+ #db;
25
+ #dbPath;
26
+
27
+ constructor(opts = {}) {
28
+ this.#dbPath = opts.dbPath || DEFAULT_DB_PATH;
29
+
30
+ const dir = path.dirname(this.#dbPath);
31
+ if (!fs.existsSync(dir)) {
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ }
34
+
35
+ this.#db = new Database(this.#dbPath);
36
+ this.#db.pragma('journal_mode = WAL');
37
+ this.#db.pragma('foreign_keys = ON');
38
+ this.#db.pragma('busy_timeout = 5000');
39
+
40
+ this.#runMigrations();
41
+ }
42
+
43
+ get dbPath() { return this.#dbPath; }
44
+
45
+ // ── Schema ────────────────────────────────────
46
+
47
+ #runMigrations() {
48
+ this.#db.exec(`
49
+ CREATE TABLE IF NOT EXISTS ha_telemetry (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ node_id TEXT NOT NULL,
52
+ soul_id TEXT NOT NULL,
53
+ task_id TEXT,
54
+ domain TEXT NOT NULL,
55
+ subdomain TEXT,
56
+ strategy_id INTEGER REFERENCES ha_strategies(id),
57
+ outcome TEXT NOT NULL CHECK(outcome IN ('success','partial','failure')),
58
+ iterations INTEGER DEFAULT 1,
59
+ duration_minutes REAL,
60
+ pattern_flags TEXT DEFAULT '[]',
61
+ meta_notes TEXT,
62
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
63
+ );
64
+ CREATE INDEX IF NOT EXISTS idx_ha_tel_domain ON ha_telemetry(domain, subdomain);
65
+ CREATE INDEX IF NOT EXISTS idx_ha_tel_node ON ha_telemetry(node_id);
66
+
67
+ CREATE TABLE IF NOT EXISTS ha_strategies (
68
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
69
+ domain TEXT NOT NULL,
70
+ subdomain TEXT,
71
+ title TEXT NOT NULL,
72
+ content TEXT NOT NULL,
73
+ source TEXT DEFAULT 'manual',
74
+ version INTEGER DEFAULT 1,
75
+ supersedes INTEGER REFERENCES ha_strategies(id),
76
+ active INTEGER DEFAULT 1,
77
+ node_id TEXT,
78
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
79
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
80
+ );
81
+ CREATE INDEX IF NOT EXISTS idx_ha_strat_domain ON ha_strategies(domain, subdomain);
82
+ CREATE INDEX IF NOT EXISTS idx_ha_strat_active ON ha_strategies(active);
83
+
84
+ CREATE TABLE IF NOT EXISTS ha_reflections (
85
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ node_id TEXT NOT NULL,
87
+ soul_id TEXT NOT NULL,
88
+ telemetry_from_id INTEGER NOT NULL,
89
+ telemetry_to_id INTEGER NOT NULL,
90
+ telemetry_count INTEGER NOT NULL,
91
+ raw_stats TEXT NOT NULL,
92
+ hypotheses TEXT,
93
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
94
+ );
95
+
96
+ CREATE TABLE IF NOT EXISTS ha_proposals (
97
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98
+ reflection_id INTEGER NOT NULL REFERENCES ha_reflections(id),
99
+ node_id TEXT NOT NULL,
100
+ soul_id TEXT NOT NULL,
101
+ title TEXT NOT NULL,
102
+ description TEXT NOT NULL,
103
+ proposal_type TEXT NOT NULL CHECK(proposal_type IN ('strategy_update','strategy_new','harness_rule','workflow_change')),
104
+ target_ref TEXT,
105
+ diff_content TEXT,
106
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','shadow','approved','rejected','expired')),
107
+ eval_window_start TEXT,
108
+ eval_window_end TEXT,
109
+ eval_telemetry_count INTEGER DEFAULT 0,
110
+ eval_result TEXT,
111
+ reviewed_by TEXT,
112
+ reviewed_at TEXT,
113
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
114
+ );
115
+ CREATE INDEX IF NOT EXISTS idx_ha_prop_status ON ha_proposals(status);
116
+
117
+ CREATE TABLE IF NOT EXISTS ha_telemetry_proposals (
118
+ telemetry_id INTEGER NOT NULL REFERENCES ha_telemetry(id),
119
+ proposal_id INTEGER NOT NULL REFERENCES ha_proposals(id),
120
+ PRIMARY KEY (telemetry_id, proposal_id)
121
+ );
122
+ `);
123
+ }
124
+
125
+ // ── Telemetry ────────────────────────────────────
126
+
127
+ /**
128
+ * Log a telemetry entry. Pattern flags are auto-detected after insert.
129
+ * Also links to any active shadow eval proposals via junction table.
130
+ *
131
+ * @param {Object} entry
132
+ * @returns {Object} The inserted row with auto-detected pattern_flags
133
+ */
134
+ logTelemetry(entry) {
135
+ const {
136
+ node_id, soul_id, task_id = null, domain, subdomain = null,
137
+ strategy_id = null, outcome, iterations = 1,
138
+ duration_minutes = null, meta_notes = null,
139
+ } = entry;
140
+
141
+ const insert = this.#db.prepare(`
142
+ INSERT INTO ha_telemetry (node_id, soul_id, task_id, domain, subdomain,
143
+ strategy_id, outcome, iterations, duration_minutes, meta_notes)
144
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
145
+ `);
146
+
147
+ const result = insert.run(
148
+ node_id, soul_id, task_id, domain, subdomain,
149
+ strategy_id, outcome, iterations, duration_minutes, meta_notes
150
+ );
151
+ const rowId = result.lastInsertRowid;
152
+
153
+ // Auto-detect pattern flags
154
+ const flags = this.#detectPatternFlags(rowId, {
155
+ domain, subdomain, strategy_id, outcome, iterations, meta_notes, soul_id,
156
+ });
157
+
158
+ if (flags.length > 0) {
159
+ this.#db.prepare('UPDATE ha_telemetry SET pattern_flags = ? WHERE id = ?')
160
+ .run(JSON.stringify(flags), rowId);
161
+ }
162
+
163
+ // Link to active shadow eval proposals
164
+ this.#linkToShadowEvals(rowId);
165
+
166
+ return this.#db.prepare('SELECT * FROM ha_telemetry WHERE id = ?').get(rowId);
167
+ }
168
+
169
+ /**
170
+ * Auto-detect pattern flags from telemetry history.
171
+ */
172
+ #detectPatternFlags(rowId, entry) {
173
+ const flags = [];
174
+
175
+ // repeated-approach: same strategy on last 3+ tasks in same domain
176
+ if (entry.strategy_id != null) {
177
+ const recent = this.#db.prepare(`
178
+ SELECT COUNT(*) as n FROM ha_telemetry
179
+ WHERE domain = ? AND strategy_id = ? AND soul_id = ?
180
+ AND id >= (SELECT MAX(0, ? - 3))
181
+ AND id <= ?
182
+ `).get(entry.domain, entry.strategy_id, entry.soul_id, rowId, rowId);
183
+ if (recent.n >= 3) flags.push('repeated-approach');
184
+ }
185
+
186
+ // multiple-iterations: > 3 attempts
187
+ if (entry.iterations > 3) flags.push('multiple-iterations');
188
+
189
+ // no-meta-notes: empty or too short
190
+ if (!entry.meta_notes || entry.meta_notes.trim().length < 20) {
191
+ flags.push('no-meta-notes');
192
+ }
193
+
194
+ // always-escalated: failure with only 1 iteration (didn't really try)
195
+ if (entry.outcome === 'failure' && entry.iterations <= 1) {
196
+ flags.push('always-escalated');
197
+ }
198
+
199
+ return flags;
200
+ }
201
+
202
+ /**
203
+ * Link a telemetry entry to any active shadow eval proposals.
204
+ */
205
+ #linkToShadowEvals(telemetryId) {
206
+ const shadowProposals = this.#db.prepare(`
207
+ SELECT id FROM ha_proposals
208
+ WHERE status = 'shadow'
209
+ AND eval_window_start IS NOT NULL
210
+ AND eval_window_end IS NOT NULL
211
+ AND datetime('now') BETWEEN eval_window_start AND eval_window_end
212
+ `).all();
213
+
214
+ const linkStmt = this.#db.prepare(
215
+ 'INSERT OR IGNORE INTO ha_telemetry_proposals (telemetry_id, proposal_id) VALUES (?, ?)'
216
+ );
217
+
218
+ for (const p of shadowProposals) {
219
+ linkStmt.run(telemetryId, p.id);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Get telemetry entries since a given ID.
225
+ */
226
+ getTelemetrySince(sinceId = 0) {
227
+ return this.#db.prepare(
228
+ 'SELECT * FROM ha_telemetry WHERE id > ? ORDER BY id'
229
+ ).all(sinceId);
230
+ }
231
+
232
+ /**
233
+ * Get recent telemetry with optional filters.
234
+ */
235
+ getTelemetry(opts = {}) {
236
+ const { domain, last = 20 } = opts;
237
+ if (domain) {
238
+ return this.#db.prepare(
239
+ 'SELECT * FROM ha_telemetry WHERE domain = ? ORDER BY id DESC LIMIT ?'
240
+ ).all(domain, last);
241
+ }
242
+ return this.#db.prepare(
243
+ 'SELECT * FROM ha_telemetry ORDER BY id DESC LIMIT ?'
244
+ ).all(last);
245
+ }
246
+
247
+ /**
248
+ * Count telemetry entries not yet covered by a reflection.
249
+ */
250
+ getUnreflectedCount() {
251
+ const row = this.#db.prepare(`
252
+ SELECT COUNT(*) as n FROM ha_telemetry
253
+ WHERE id > COALESCE(
254
+ (SELECT telemetry_to_id FROM ha_reflections ORDER BY created_at DESC LIMIT 1),
255
+ 0
256
+ )
257
+ `).get();
258
+ return row.n;
259
+ }
260
+
261
+ /**
262
+ * Compute aggregated stats from telemetry since a given ID.
263
+ */
264
+ computeStats(sinceId = 0) {
265
+ const entries = this.getTelemetrySince(sinceId);
266
+ if (entries.length === 0) return null;
267
+
268
+ const byDomain = {};
269
+ const allFlags = {};
270
+ let totalIterations = 0;
271
+ let successCount = 0;
272
+
273
+ for (const e of entries) {
274
+ // Domain stats
275
+ if (!byDomain[e.domain]) {
276
+ byDomain[e.domain] = { count: 0, success: 0, totalIterations: 0, subdomains: {} };
277
+ }
278
+ const d = byDomain[e.domain];
279
+ d.count++;
280
+ if (e.outcome === 'success') d.success++;
281
+ d.totalIterations += e.iterations;
282
+
283
+ if (e.subdomain) {
284
+ d.subdomains[e.subdomain] = (d.subdomains[e.subdomain] || 0) + 1;
285
+ }
286
+
287
+ // Global stats
288
+ totalIterations += e.iterations;
289
+ if (e.outcome === 'success') successCount++;
290
+
291
+ // Flag frequencies
292
+ const flags = JSON.parse(e.pattern_flags || '[]');
293
+ for (const f of flags) {
294
+ allFlags[f] = (allFlags[f] || 0) + 1;
295
+ }
296
+ }
297
+
298
+ // Per-domain averages
299
+ for (const d of Object.values(byDomain)) {
300
+ d.successRate = d.count > 0 ? Math.round(d.success / d.count * 100) : 0;
301
+ d.avgIterations = d.count > 0 ? Math.round(d.totalIterations / d.count * 10) / 10 : 0;
302
+ }
303
+
304
+ // Strategy hit rate
305
+ const withStrategy = entries.filter(e => e.strategy_id != null).length;
306
+
307
+ return {
308
+ totalTasks: entries.length,
309
+ successRate: Math.round(successCount / entries.length * 100),
310
+ avgIterations: Math.round(totalIterations / entries.length * 10) / 10,
311
+ strategyHitRate: Math.round(withStrategy / entries.length * 100),
312
+ byDomain,
313
+ flagFrequencies: allFlags,
314
+ fromId: entries[0].id,
315
+ toId: entries[entries.length - 1].id,
316
+ };
317
+ }
318
+
319
+ // ── Strategies ────────────────────────────────────
320
+
321
+ /**
322
+ * Create or update a strategy. If supersedes is set, atomically deactivate the old one.
323
+ */
324
+ putStrategy({ domain, subdomain = null, title, content, source = 'manual', node_id = null, supersedes = null }) {
325
+ const transaction = this.#db.transaction(() => {
326
+ if (supersedes != null) {
327
+ this.#db.prepare('UPDATE ha_strategies SET active = 0, updated_at = datetime(\'now\') WHERE id = ?')
328
+ .run(supersedes);
329
+ }
330
+
331
+ const result = this.#db.prepare(`
332
+ INSERT INTO ha_strategies (domain, subdomain, title, content, source, node_id, supersedes,
333
+ version)
334
+ VALUES (?, ?, ?, ?, ?, ?, ?,
335
+ COALESCE((SELECT version + 1 FROM ha_strategies WHERE id = ?), 1))
336
+ `).run(domain, subdomain, title, content, source, node_id, supersedes, supersedes);
337
+
338
+ return result.lastInsertRowid;
339
+ });
340
+
341
+ const id = transaction();
342
+ return this.#db.prepare('SELECT * FROM ha_strategies WHERE id = ?').get(id);
343
+ }
344
+
345
+ /**
346
+ * Get the best active strategy for a domain/subdomain.
347
+ * Prefers exact subdomain match, falls back to domain-only.
348
+ */
349
+ getStrategy(domain, subdomain = null) {
350
+ if (subdomain) {
351
+ const exact = this.#db.prepare(
352
+ 'SELECT * FROM ha_strategies WHERE domain = ? AND subdomain = ? AND active = 1 ORDER BY version DESC LIMIT 1'
353
+ ).get(domain, subdomain);
354
+ if (exact) return exact;
355
+ }
356
+ return this.#db.prepare(
357
+ 'SELECT * FROM ha_strategies WHERE domain = ? AND (subdomain IS NULL OR subdomain = ?) AND active = 1 ORDER BY version DESC LIMIT 1'
358
+ ).get(domain, subdomain);
359
+ }
360
+
361
+ /**
362
+ * List strategies with optional filters.
363
+ */
364
+ listStrategies(opts = {}) {
365
+ const { domain, active = true } = opts;
366
+ if (domain) {
367
+ return this.#db.prepare(
368
+ 'SELECT * FROM ha_strategies WHERE domain = ? AND active = ? ORDER BY updated_at DESC'
369
+ ).all(domain, active ? 1 : 0);
370
+ }
371
+ return this.#db.prepare(
372
+ 'SELECT * FROM ha_strategies WHERE active = ? ORDER BY domain, updated_at DESC'
373
+ ).all(active ? 1 : 0);
374
+ }
375
+
376
+ /**
377
+ * Archive (deactivate) a strategy.
378
+ */
379
+ archiveStrategy(id) {
380
+ this.#db.prepare('UPDATE ha_strategies SET active = 0, updated_at = datetime(\'now\') WHERE id = ?').run(id);
381
+ }
382
+
383
+ // ── Reflections ────────────────────────────────────
384
+
385
+ /**
386
+ * Create a reflection with raw stats. hypotheses filled later by --write-synthesis.
387
+ */
388
+ putReflection({ node_id, soul_id, telemetry_from_id, telemetry_to_id, telemetry_count, raw_stats }) {
389
+ const result = this.#db.prepare(`
390
+ INSERT INTO ha_reflections (node_id, soul_id, telemetry_from_id, telemetry_to_id,
391
+ telemetry_count, raw_stats)
392
+ VALUES (?, ?, ?, ?, ?, ?)
393
+ `).run(node_id, soul_id, telemetry_from_id, telemetry_to_id, telemetry_count,
394
+ typeof raw_stats === 'string' ? raw_stats : JSON.stringify(raw_stats));
395
+
396
+ return this.#db.prepare('SELECT * FROM ha_reflections WHERE id = ?').get(result.lastInsertRowid);
397
+ }
398
+
399
+ /**
400
+ * Write LLM-generated synthesis to an existing reflection.
401
+ */
402
+ writeSynthesis(reflectionId, { hypotheses }) {
403
+ this.#db.prepare('UPDATE ha_reflections SET hypotheses = ? WHERE id = ?')
404
+ .run(typeof hypotheses === 'string' ? hypotheses : JSON.stringify(hypotheses), reflectionId);
405
+ return this.#db.prepare('SELECT * FROM ha_reflections WHERE id = ?').get(reflectionId);
406
+ }
407
+
408
+ /**
409
+ * List recent reflections.
410
+ */
411
+ listReflections(opts = {}) {
412
+ const { limit = 10 } = opts;
413
+ return this.#db.prepare(
414
+ 'SELECT * FROM ha_reflections ORDER BY created_at DESC LIMIT ?'
415
+ ).all(limit);
416
+ }
417
+
418
+ /**
419
+ * Get the last reflection (for chaining previous_hypotheses).
420
+ */
421
+ getLastReflection() {
422
+ return this.#db.prepare(
423
+ 'SELECT * FROM ha_reflections ORDER BY created_at DESC LIMIT 1'
424
+ ).get() || null;
425
+ }
426
+
427
+ /**
428
+ * Get the oldest pending-synthesis reflection (< 24h old).
429
+ * Returns null if none pending.
430
+ */
431
+ getPendingSynthesis() {
432
+ return this.#db.prepare(`
433
+ SELECT * FROM ha_reflections
434
+ WHERE hypotheses IS NULL
435
+ AND created_at > datetime('now', '-24 hours')
436
+ ORDER BY created_at ASC
437
+ LIMIT 1
438
+ `).get() || null;
439
+ }
440
+
441
+ /**
442
+ * Get the reflection immediately before a given one (for hypothesis chaining).
443
+ */
444
+ getPreviousReflection(reflectionId) {
445
+ return this.#db.prepare(`
446
+ SELECT * FROM ha_reflections
447
+ WHERE id < ? AND hypotheses IS NOT NULL
448
+ ORDER BY id DESC
449
+ LIMIT 1
450
+ `).get(reflectionId) || null;
451
+ }
452
+
453
+ /**
454
+ * Expire stale pending reflections (> 24h without synthesis).
455
+ */
456
+ expireStalePending() {
457
+ const result = this.#db.prepare(`
458
+ UPDATE ha_reflections SET hypotheses = '["expired — no synthesis within 24h"]'
459
+ WHERE hypotheses IS NULL
460
+ AND created_at < datetime('now', '-24 hours')
461
+ `).run();
462
+ return result.changes;
463
+ }
464
+
465
+ // ── Proposals ────────────────────────────────────
466
+
467
+ /**
468
+ * Create a proposal linked to a reflection.
469
+ */
470
+ putProposal({ reflection_id, node_id, soul_id, title, description, proposal_type, target_ref = null, diff_content = null }) {
471
+ const result = this.#db.prepare(`
472
+ INSERT INTO ha_proposals (reflection_id, node_id, soul_id, title, description,
473
+ proposal_type, target_ref, diff_content)
474
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
475
+ `).run(reflection_id, node_id, soul_id, title, description, proposal_type, target_ref, diff_content);
476
+
477
+ return this.#db.prepare('SELECT * FROM ha_proposals WHERE id = ?').get(result.lastInsertRowid);
478
+ }
479
+
480
+ /**
481
+ * Start shadow evaluation for a proposal.
482
+ */
483
+ startShadowEval(proposalId, windowMinutes = 60) {
484
+ const start = new Date().toISOString();
485
+ const end = new Date(Date.now() + windowMinutes * 60 * 1000).toISOString();
486
+
487
+ this.#db.prepare(`
488
+ UPDATE ha_proposals SET status = 'shadow', eval_window_start = ?, eval_window_end = ?
489
+ WHERE id = ? AND status = 'pending'
490
+ `).run(start, end, proposalId);
491
+
492
+ return this.#db.prepare('SELECT * FROM ha_proposals WHERE id = ?').get(proposalId);
493
+ }
494
+
495
+ /**
496
+ * Approve a proposal. If it's a strategy proposal, apply the change.
497
+ */
498
+ approveProposal(proposalId, reviewedBy = 'human') {
499
+ const proposal = this.#db.prepare('SELECT * FROM ha_proposals WHERE id = ?').get(proposalId);
500
+ if (!proposal) return null;
501
+
502
+ const transaction = this.#db.transaction(() => {
503
+ this.#db.prepare(`
504
+ UPDATE ha_proposals SET status = 'approved', reviewed_by = ?, reviewed_at = datetime('now')
505
+ WHERE id = ?
506
+ `).run(reviewedBy, proposalId);
507
+
508
+ // Auto-apply strategy proposals
509
+ if (proposal.proposal_type === 'strategy_new' && proposal.diff_content) {
510
+ try {
511
+ const stratData = JSON.parse(proposal.diff_content);
512
+ this.putStrategy({
513
+ domain: stratData.domain,
514
+ subdomain: stratData.subdomain || null,
515
+ title: stratData.title || proposal.title,
516
+ content: stratData.content,
517
+ source: 'reflection',
518
+ node_id: proposal.node_id,
519
+ });
520
+ } catch { /* diff_content not JSON — manual apply needed */ }
521
+ }
522
+
523
+ if (proposal.proposal_type === 'strategy_update' && proposal.target_ref && proposal.diff_content) {
524
+ try {
525
+ const stratId = parseInt(proposal.target_ref);
526
+ const updates = JSON.parse(proposal.diff_content);
527
+ this.putStrategy({
528
+ ...updates,
529
+ source: 'reflection',
530
+ supersedes: stratId,
531
+ });
532
+ } catch { /* manual apply needed */ }
533
+ }
534
+ });
535
+
536
+ transaction();
537
+ return this.#db.prepare('SELECT * FROM ha_proposals WHERE id = ?').get(proposalId);
538
+ }
539
+
540
+ /**
541
+ * Reject a proposal.
542
+ */
543
+ rejectProposal(proposalId, reviewedBy = 'human') {
544
+ this.#db.prepare(`
545
+ UPDATE ha_proposals SET status = 'rejected', reviewed_by = ?, reviewed_at = datetime('now')
546
+ WHERE id = ?
547
+ `).run(reviewedBy, proposalId);
548
+
549
+ return this.#db.prepare('SELECT * FROM ha_proposals WHERE id = ?').get(proposalId);
550
+ }
551
+
552
+ /**
553
+ * Get proposals by status.
554
+ */
555
+ getProposals(opts = {}) {
556
+ const { status } = opts;
557
+ if (status) {
558
+ return this.#db.prepare(
559
+ 'SELECT * FROM ha_proposals WHERE status = ? ORDER BY created_at DESC'
560
+ ).all(status);
561
+ }
562
+ return this.#db.prepare(
563
+ 'SELECT * FROM ha_proposals ORDER BY created_at DESC'
564
+ ).all();
565
+ }
566
+
567
+ /**
568
+ * Check expired shadow eval windows and compute results.
569
+ */
570
+ checkShadowWindows() {
571
+ const expired = this.#db.prepare(`
572
+ SELECT * FROM ha_proposals
573
+ WHERE status = 'shadow'
574
+ AND eval_window_end IS NOT NULL
575
+ AND eval_window_end < datetime('now')
576
+ `).all();
577
+
578
+ for (const proposal of expired) {
579
+ // Count telemetry entries in the eval window
580
+ const evalCount = this.#db.prepare(`
581
+ SELECT COUNT(*) as n FROM ha_telemetry_proposals WHERE proposal_id = ?
582
+ `).get(proposal.id).n;
583
+
584
+ // Compute success rate during eval window
585
+ const evalEntries = this.#db.prepare(`
586
+ SELECT t.outcome FROM ha_telemetry t
587
+ JOIN ha_telemetry_proposals tp ON t.id = tp.telemetry_id
588
+ WHERE tp.proposal_id = ?
589
+ `).all(proposal.id);
590
+
591
+ const evalSuccessRate = evalEntries.length > 0
592
+ ? Math.round(evalEntries.filter(e => e.outcome === 'success').length / evalEntries.length * 100)
593
+ : null;
594
+
595
+ // Compute success rate before eval window (last N tasks before window start)
596
+ const beforeEntries = this.#db.prepare(`
597
+ SELECT outcome FROM ha_telemetry
598
+ WHERE created_at < ? ORDER BY created_at DESC LIMIT ?
599
+ `).all(proposal.eval_window_start, Math.max(evalEntries.length, 5));
600
+
601
+ const beforeSuccessRate = beforeEntries.length > 0
602
+ ? Math.round(beforeEntries.filter(e => e.outcome === 'success').length / beforeEntries.length * 100)
603
+ : null;
604
+
605
+ const evalResult = {
606
+ tasks_in_window: evalCount,
607
+ success_rate_before: beforeSuccessRate,
608
+ success_rate_during: evalSuccessRate,
609
+ delta: evalSuccessRate != null && beforeSuccessRate != null
610
+ ? evalSuccessRate - beforeSuccessRate
611
+ : null,
612
+ };
613
+
614
+ this.#db.prepare(`
615
+ UPDATE ha_proposals
616
+ SET status = 'pending', eval_telemetry_count = ?, eval_result = ?
617
+ WHERE id = ?
618
+ `).run(evalCount, JSON.stringify(evalResult), proposal.id);
619
+ }
620
+
621
+ return expired.length;
622
+ }
623
+
624
+ // ── Stats ────────────────────────────────────
625
+
626
+ /**
627
+ * Get overview stats for the hyperagent store.
628
+ */
629
+ getStats() {
630
+ const telemetry = this.#db.prepare('SELECT COUNT(*) as n FROM ha_telemetry').get().n;
631
+ const strategies = this.#db.prepare('SELECT COUNT(*) as n FROM ha_strategies WHERE active = 1').get().n;
632
+ const reflections = this.#db.prepare('SELECT COUNT(*) as n FROM ha_reflections').get().n;
633
+ const pendingProposals = this.#db.prepare("SELECT COUNT(*) as n FROM ha_proposals WHERE status IN ('pending','shadow')").get().n;
634
+ const unreflected = this.getUnreflectedCount();
635
+
636
+ return { telemetry, strategies, reflections, pendingProposals, unreflected };
637
+ }
638
+
639
+ /**
640
+ * Close the database connection.
641
+ */
642
+ close() {
643
+ this.#db.close();
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Create a HyperAgentStore instance with default path.
649
+ */
650
+ export function createHyperAgentStore(opts = {}) {
651
+ return new HyperAgentStore(opts);
652
+ }