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
@@ -50,7 +50,7 @@ Knowledge Discovery Specialist - expert in web research, documentation lookup, a
50
50
  - If timeout reached: STOP → Report partial results → Indicate what's incomplete
51
51
  - Uses graceful degradation: Full → Partial → Search Results Only → Failure Report
52
52
 
53
- **Model:** haiku (fast & cost-effective)
53
+ **Model:** fast tier (e.g. haiku, gpt-4o-mini, gemini-flash)
54
54
 
55
55
  </details>
56
56
 
@@ -101,7 +101,7 @@ System Architect - strategic planner for React/Node.js/TypeScript enterprise app
101
101
  - Props Drilling Max 2 Levels (then Context)
102
102
  - Server State Separation (React Query/SWR)
103
103
 
104
- **Model:** opus (complex reasoning, high-impact decisions)
104
+ **Model:** reasoning tier (e.g. opus, o1, deepseek-r1)
105
105
 
106
106
  </details>
107
107
 
@@ -148,7 +148,7 @@ API Lifecycle Expert - specialist for REST/GraphQL APIs, TypeScript type systems
148
148
  - [ ] Run `npm run typecheck`
149
149
  ```
150
150
 
151
- **Model:** sonnet (balanced analysis + documentation)
151
+ **Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
152
152
 
153
153
  </details>
154
154
 
@@ -207,7 +207,7 @@ Senior Full-Stack Developer - specialist for React/Node.js/TypeScript implementa
207
207
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
208
208
  ```
209
209
 
210
- **Model:** sonnet (optimal for implementation)
210
+ **Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
211
211
 
212
212
  </details>
213
213
 
@@ -260,7 +260,7 @@ Code Quality Engineer - specialist for verification and quality assurance.
260
260
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
261
261
  ```
262
262
 
263
- **Model:** sonnet (balanced verification)
263
+ **Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
264
264
 
265
265
  </details>
266
266
 
@@ -320,7 +320,7 @@ UX Quality Engineer - specialist for E2E testing, visual regression, accessibili
320
320
  **BLOCKING:** Console errors, E2E failures, LCP > 4s, CLS > 0.25
321
321
  **NON-BLOCKING:** Minor A11y issues, "needs improvement" performance
322
322
 
323
- **Model:** sonnet (MCP coordination + analysis)
323
+ **Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
324
324
 
325
325
  </details>
326
326
 
@@ -380,7 +380,7 @@ Technical Writer - specialist for developer documentation.
380
380
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
381
381
  ```
382
382
 
383
- **Model:** sonnet (reading + writing capability)
383
+ **Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
384
384
 
385
385
  </details>
386
386
 
@@ -428,6 +428,6 @@ gh run view [run-id] --log-failed
428
428
  Types: feat, fix, docs, style, refactor, test, chore
429
429
  ```
430
430
 
431
- **Model:** haiku (simple operations, cost-optimized)
431
+ **Model:** fast tier (e.g. haiku, gpt-4o-mini, gemini-flash)
432
432
 
433
433
  </details>
package/uninstall.sh CHANGED
@@ -35,24 +35,52 @@ if [ -f "$WORKSPACE/bin/install-daemon" ]; then
35
35
  bash "$WORKSPACE/bin/install-daemon" --uninstall 2>/dev/null || true
36
36
  fi
37
37
 
38
- # Stop and remove mesh agent service (if installed)
38
+ # Stop and remove services
39
39
  OS="$(uname -s)"
40
40
  if [ "$OS" = "Linux" ]; then
41
+ # --- Current services: openclaw-*.service under user systemd ---
42
+ SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
43
+ if [ -d "$SYSTEMD_USER_DIR" ]; then
44
+ for unit in "$SYSTEMD_USER_DIR"/openclaw-*.service "$SYSTEMD_USER_DIR"/openclaw-*.timer; do
45
+ [ -f "$unit" ] || continue
46
+ UNIT_NAME="$(basename "$unit")"
47
+ info "Stopping $UNIT_NAME..."
48
+ systemctl --user stop "$UNIT_NAME" 2>/dev/null || true
49
+ systemctl --user disable "$UNIT_NAME" 2>/dev/null || true
50
+ rm -f "$unit"
51
+ info "Removed $UNIT_NAME"
52
+ done
53
+ systemctl --user daemon-reload 2>/dev/null || true
54
+ fi
55
+ # --- Legacy fallback: old system-level openclaw-agent ---
41
56
  if systemctl is-active --quiet openclaw-agent 2>/dev/null; then
42
- info "Stopping mesh agent..."
57
+ info "Stopping legacy mesh agent (openclaw-agent)..."
43
58
  sudo systemctl stop openclaw-agent 2>/dev/null || true
44
59
  sudo systemctl disable openclaw-agent 2>/dev/null || true
45
60
  sudo rm -f /etc/systemd/system/openclaw-agent.service
46
61
  sudo systemctl daemon-reload 2>/dev/null || true
47
- info "Mesh agent service removed"
62
+ info "Legacy mesh agent service removed"
48
63
  fi
49
64
  elif [ "$OS" = "Darwin" ]; then
50
- MESH_PLIST="/Library/LaunchDaemons/com.openclaw.agent.plist"
51
- if [ -f "$MESH_PLIST" ]; then
52
- info "Stopping mesh agent..."
53
- sudo launchctl unload "$MESH_PLIST" 2>/dev/null || true
54
- sudo rm -f "$MESH_PLIST"
55
- info "Mesh agent LaunchDaemon removed"
65
+ # --- Current services: ai.openclaw.*.plist under ~/Library/LaunchAgents ---
66
+ LAUNCHD_AGENTS_DIR="$HOME/Library/LaunchAgents"
67
+ if [ -d "$LAUNCHD_AGENTS_DIR" ]; then
68
+ for plist in "$LAUNCHD_AGENTS_DIR"/ai.openclaw.*.plist; do
69
+ [ -f "$plist" ] || continue
70
+ PLIST_NAME="$(basename "$plist")"
71
+ info "Unloading $PLIST_NAME..."
72
+ launchctl unload "$plist" 2>/dev/null || true
73
+ rm -f "$plist"
74
+ info "Removed $PLIST_NAME"
75
+ done
76
+ fi
77
+ # --- Legacy fallback: old system-level com.openclaw.agent ---
78
+ LEGACY_PLIST="/Library/LaunchDaemons/com.openclaw.agent.plist"
79
+ if [ -f "$LEGACY_PLIST" ]; then
80
+ info "Stopping legacy mesh agent (com.openclaw.agent)..."
81
+ sudo launchctl unload "$LEGACY_PLIST" 2>/dev/null || true
82
+ sudo rm -f "$LEGACY_PLIST"
83
+ info "Legacy mesh agent LaunchDaemon removed"
56
84
  fi
57
85
  fi
58
86
  # Remove mesh symlinks
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * memory-daemon.mjs — OpenClaw platform-level memory lifecycle daemon (v2)
3
+ * memory-daemon.mjs — OpenClaw platform-level memory lifecycle daemon (v3)
4
4
  *
5
5
  * Long-running Node.js process that detects activity from ANY frontend
6
6
  * by polling JSONL transcript mtimes. No touchfiles. No hooks required.
@@ -9,9 +9,16 @@
9
9
  *
10
10
  * Phases:
11
11
  * 0. Session-start bootstrap (once per new session)
12
+ * - Freezes MEMORY.md snapshot (memory-budget)
13
+ * - Imports sessions into SQLite archive (session-store)
12
14
  * 1. Status sync (every tick when active, ~5ms)
13
15
  * 2. Throttled background work (recap 10min, maintenance 30min,
14
- * obsidian-sync 30min, trust-health 30min)
16
+ * obsidian-sync 30min, trust-health 30min, session-import 10min)
17
+ *
18
+ * v3 additions (Hermes-inspired):
19
+ * - Pre-compression memory flush (durable fact extraction before context loss)
20
+ * - MEMORY.md character budget with frozen session snapshots
21
+ * - SQLite session archive with FTS5 for episodic recall
15
22
  *
16
23
  * Install: bin/install-daemon (detects OS, sets up launchd/systemd/pm2)
17
24
  * Manual: node bin/memory-daemon.mjs [--test] [--verbose]
@@ -24,6 +31,38 @@ import os from 'os';
24
31
  import { execFile, spawn } from 'child_process';
25
32
  import { promisify } from 'util';
26
33
 
34
+ // --- Hermes-inspired modules ---
35
+ import { shouldFlush, runFlush } from '../lib/pre-compression-flush.mjs';
36
+ import { createBudget } from '../lib/memory-budget.mjs';
37
+
38
+ // Session store loaded lazily (requires better-sqlite3)
39
+ let _sessionStore = null;
40
+ async function getSessionStore() {
41
+ if (_sessionStore) return _sessionStore;
42
+ try {
43
+ const { SessionStore } = await import('../lib/session-store.mjs');
44
+ _sessionStore = new SessionStore();
45
+ return _sessionStore;
46
+ } catch (err) {
47
+ log(`session-store unavailable: ${err.message}`);
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // HyperAgent store loaded lazily (requires better-sqlite3)
53
+ let _haStore = null;
54
+ async function getHyperAgentStore() {
55
+ if (_haStore) return _haStore;
56
+ try {
57
+ const { createHyperAgentStore } = await import('../lib/hyperagent-store.mjs');
58
+ _haStore = createHyperAgentStore();
59
+ return _haStore;
60
+ } catch (err) {
61
+ log(`hyperagent-store unavailable: ${err.message}`);
62
+ return null;
63
+ }
64
+ }
65
+
27
66
  const execFileAsync = promisify(execFile);
28
67
  const __filename = fileURLToPath(import.meta.url);
29
68
  const __dirname = path.dirname(__filename);
@@ -51,6 +90,8 @@ function loadConfig() {
51
90
  maintenanceMs: 1800000, // 30 min
52
91
  obsidianSyncMs: 1800000, // 30 min
53
92
  },
93
+ contextWindowTokens: 200000, // active model's context window (override per LLM)
94
+ memoryCharBudget: 2200, // MEMORY.md character cap
54
95
  clawvaultBin: 'bin/clawvault-local',
55
96
  obsidianVault: 'projects/arcane-vault',
56
97
  };
@@ -141,6 +182,7 @@ function detectActivity(sources, activityWindowMs) {
141
182
  let newestSession = null;
142
183
  let newestMtime = 0;
143
184
  let newestSource = null;
185
+ let newestFormat = null;
144
186
 
145
187
  for (const source of sources) {
146
188
  if (!fs.existsSync(source.path)) continue;
@@ -160,12 +202,13 @@ function detectActivity(sources, activityWindowMs) {
160
202
  newestMtime = mtime;
161
203
  newestSession = path.basename(f, '.jsonl');
162
204
  newestSource = source.name;
205
+ newestFormat = source.format || null;
163
206
  }
164
207
  } catch { continue; }
165
208
  }
166
209
  }
167
210
 
168
- return { active, newestSession, newestMtime, newestSource };
211
+ return { active, newestSession, newestMtime, newestSource, newestFormat };
169
212
  }
170
213
 
171
214
  // ============================================================
@@ -275,6 +318,31 @@ class SessionStateMachine {
275
318
  }
276
319
  }
277
320
 
321
+ // ============================================================
322
+ // MEMORY BUDGET (Hermes-inspired frozen snapshot)
323
+ // ============================================================
324
+
325
+ let memoryBudget = null;
326
+
327
+ function initMemoryBudget(config) {
328
+ if (memoryBudget) return memoryBudget;
329
+ memoryBudget = createBudget(config.workspace || WORKSPACE, {
330
+ charBudget: config.memoryCharBudget || 2200,
331
+ });
332
+
333
+ memoryBudget.on('add', ({ entry, pctUsed, charsRemaining }) => {
334
+ log(` [memory] +added (${pctUsed}% used, ${charsRemaining} chars free)`);
335
+ });
336
+ memoryBudget.on('warning', ({ pctUsed, message }) => {
337
+ log(` [memory] WARNING: ${message}`);
338
+ });
339
+ memoryBudget.on('trim', ({ removed }) => {
340
+ log(` [memory] trimmed: ${removed.slice(0, 60)}...`);
341
+ });
342
+
343
+ return memoryBudget;
344
+ }
345
+
278
346
  // ============================================================
279
347
  // PHASE 0: SESSION-START BOOTSTRAP
280
348
  // ============================================================
@@ -361,6 +429,29 @@ async function runPhase0Bootstrap(sessionId, config) {
361
429
  } catch (e) { log(` clawvault doctor failed: ${e.message}`); }
362
430
  }
363
431
 
432
+ // 9. Freeze MEMORY.md snapshot for deterministic prompt content
433
+ try {
434
+ const budget = initMemoryBudget(config);
435
+ budget.startSession();
436
+ const stats = budget.getStats();
437
+ log(` memory-budget frozen ${stats.meterDisplay}`);
438
+ } catch (e) { log(` memory-budget freeze failed: ${e.message}`); }
439
+
440
+ // 10. Import recent sessions into SQLite archive
441
+ try {
442
+ const store = await getSessionStore();
443
+ if (store) {
444
+ const sources = loadTranscriptSources();
445
+ let totalImported = 0;
446
+ for (const source of sources) {
447
+ if (!fs.existsSync(source.path)) continue;
448
+ const result = await store.importDirectory(source.path, { source: source.name, format: source.format });
449
+ totalImported += result.imported;
450
+ }
451
+ if (totalImported > 0) log(` session-store: imported ${totalImported} sessions`);
452
+ }
453
+ } catch (e) { log(` session-store import failed: ${e.message}`); }
454
+
364
455
  log('Phase 0: Bootstrap complete');
365
456
  }
366
457
 
@@ -499,6 +590,7 @@ function loadThrottleState() {
499
590
  return {
500
591
  lastRecap: 0, lastMaintenance: 0, lastObsidianSync: 0, lastTrustHealth: 0,
501
592
  lastClawvaultReflect: 0, lastClawvaultArchive: 0, lastClawvaultObserve: 0,
593
+ lastSessionImport: 0, lastHyperagentReflect: 0,
502
594
  };
503
595
  }
504
596
 
@@ -553,6 +645,48 @@ async function runPhase2ThrottledWork(config) {
553
645
  }
554
646
  }
555
647
 
648
+ // Session archive import — incremental (every 10min, aligned with recap)
649
+ if (now - throttle.lastSessionImport >= config.intervals.sessionRecapMs) {
650
+ throttle.lastSessionImport = now;
651
+ stage1.push(
652
+ (async () => {
653
+ try {
654
+ const store = await getSessionStore();
655
+ if (!store) return;
656
+ const sources = loadTranscriptSources();
657
+ let totalImported = 0;
658
+ for (const source of sources) {
659
+ if (!fs.existsSync(source.path)) continue;
660
+ const result = await store.importDirectory(source.path, { source: source.name, format: source.format });
661
+ totalImported += result.imported;
662
+ }
663
+ if (totalImported > 0) log(` Phase 2: session-store imported ${totalImported} sessions`);
664
+ } catch (e) { log(` Phase 2: session-import failed: ${e.message}`); }
665
+ })()
666
+ );
667
+ }
668
+
669
+ // HyperAgent: reflection check + shadow window expiry (every 30min)
670
+ if (now - throttle.lastHyperagentReflect >= (config.intervals.hyperagentReflectMs || 1800000)) {
671
+ throttle.lastHyperagentReflect = now;
672
+ stage1.push(
673
+ (async () => {
674
+ try {
675
+ const ha = await getHyperAgentStore();
676
+ if (!ha) return;
677
+ const unreflected = ha.getUnreflectedCount(); // sync — better-sqlite3
678
+ if (unreflected >= 5) {
679
+ await runSubprocess('node', [
680
+ path.join(HOME, '.openclaw/bin/hyperagent.mjs'), 'reflect'
681
+ ], 30000);
682
+ log(` Phase 2: hyperagent reflect (${unreflected} entries)`);
683
+ }
684
+ ha.checkShadowWindows(); // sync
685
+ } catch (e) { log(` Phase 2: hyperagent failed: ${e.message}`); }
686
+ })()
687
+ );
688
+ }
689
+
556
690
  const clawvault = path.join(WORKSPACE, config.clawvaultBin);
557
691
  if (fs.existsSync(clawvault)) {
558
692
  // ClawVault observe — incremental session compression (every 10min)
@@ -650,12 +784,32 @@ async function handleTransitions(transitions, config) {
650
784
  return STATES.ACTIVE; // signal to complete boot
651
785
  }
652
786
 
653
- // ACTIVE → IDLE: Observe + recap + checkpoint
787
+ // ACTIVE → IDLE: Observe + recap + checkpoint + pre-compression flush
654
788
  if (t.from === STATES.ACTIVE && t.to === STATES.IDLE) {
655
- log('Entering idle — running observe + recap + checkpoint');
789
+ log('Entering idle — running observe + recap + checkpoint + flush');
656
790
  const recap = path.join(WORKSPACE, 'bin/session-recap');
657
791
  const clawvault = path.join(WORKSPACE, config.clawvaultBin);
658
792
 
793
+ // Pre-compression flush: extract durable facts before context may be lost
794
+ const sources = loadTranscriptSources();
795
+ const currentJsonl = findCurrentJsonl(sources);
796
+ if (currentJsonl) {
797
+ try {
798
+ const flushCheck = await shouldFlush(currentJsonl, {
799
+ contextWindowTokens: config.contextWindowTokens || 200000,
800
+ });
801
+ if (flushCheck.shouldFlush) {
802
+ log(` pre-compression flush triggered (${flushCheck.pctUsed}% of ${flushCheck.threshold} token threshold)`);
803
+ const memoryMd = path.join(WORKSPACE, 'MEMORY.md');
804
+ const budget = initMemoryBudget(config);
805
+ const result = await runFlush(currentJsonl, memoryMd, {
806
+ charBudget: budget.charBudget,
807
+ });
808
+ log(` flush: ${result.facts} facts found, ${result.added} added, ${result.merged} merged, ${result.skipped} skipped`);
809
+ }
810
+ } catch (e) { log(` pre-compression flush failed: ${e.message}`); }
811
+ }
812
+
659
813
  const tasks = [];
660
814
  if (fs.existsSync(recap)) {
661
815
  tasks.push(runSubprocess('node', [recap], 30000).catch(() => {}));
@@ -674,6 +828,46 @@ async function handleTransitions(transitions, config) {
674
828
  const obsSync = path.join(WORKSPACE, 'bin/obsidian-sync.mjs');
675
829
  const subagentAudit = path.join(WORKSPACE, 'bin/subagent-audit.mjs');
676
830
 
831
+ // 0. Pre-compression flush (final chance to capture facts)
832
+ const sources = loadTranscriptSources();
833
+ const currentJsonl = findCurrentJsonl(sources);
834
+ if (currentJsonl) {
835
+ try {
836
+ const memoryMd = path.join(WORKSPACE, 'MEMORY.md');
837
+ const budget = initMemoryBudget(config);
838
+ const result = await runFlush(currentJsonl, memoryMd, {
839
+ charBudget: budget.charBudget,
840
+ });
841
+ if (result.added > 0 || result.merged > 0) {
842
+ log(` end-of-session flush: ${result.added} added, ${result.merged} merged`);
843
+ }
844
+ } catch (e) { log(` end-of-session flush failed: ${e.message}`); }
845
+ }
846
+
847
+ // 0b. Archive current session to SQLite
848
+ if (currentJsonl) {
849
+ try {
850
+ const store = await getSessionStore();
851
+ if (store) {
852
+ // Detect which transcript source this JSONL came from
853
+ const activity = detectActivity(loadTranscriptSources(), config.intervals.activityWindowMs);
854
+ const result = await store.importSession(currentJsonl, {
855
+ source: activity.newestSource || 'unknown',
856
+ format: activity.newestFormat,
857
+ });
858
+ if (result.imported) {
859
+ log(` session-store: archived ${result.sessionId.slice(0, 8)} (${result.messageCount} msgs)`);
860
+ }
861
+ }
862
+ } catch (e) { log(` session-store archive failed: ${e.message}`); }
863
+ }
864
+
865
+ // 0c. Release frozen MEMORY.md snapshot
866
+ if (memoryBudget) {
867
+ memoryBudget.endSession();
868
+ log(' memory-budget: snapshot released');
869
+ }
870
+
677
871
  // 1. ClawVault: final observe + reflect + archive → persist all learnings
678
872
  if (fs.existsSync(clawvault)) {
679
873
  await runSubprocess(clawvault, ['observe', '--cron'], 60000).catch(() => {});
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * session-search.mjs — FTS5 Session Search CLI
4
+ *
5
+ * Searches the SQLite session archive for episodic recall.
6
+ * Auto-detects piped output: JSON to stdout for tool consumption,
7
+ * human-readable to stderr for interactive use.
8
+ *
9
+ * Usage:
10
+ * node workspace-bin/session-search.mjs "NATS URL fix"
11
+ * node workspace-bin/session-search.mjs --role user "API key"
12
+ * node workspace-bin/session-search.mjs --limit 5 --json "memory daemon"
13
+ * node workspace-bin/session-search.mjs --import # import all sessions
14
+ * node workspace-bin/session-search.mjs --stats # show db stats
15
+ *
16
+ * Registered as an OpenClaw skill for model-callable search.
17
+ */
18
+
19
+ import { fileURLToPath } from 'url';
20
+ import path from 'path';
21
+ import fs from 'fs';
22
+ import os from 'os';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+ const WORKSPACE = process.env.OPENCLAW_WORKSPACE || path.dirname(__dirname);
27
+ const HOME = os.homedir();
28
+
29
+ // ── Args ────────────────────────────────────
30
+
31
+ const args = process.argv.slice(2);
32
+ const flags = {
33
+ limit: 10,
34
+ role: null,
35
+ json: false,
36
+ import: false,
37
+ stats: false,
38
+ help: false,
39
+ };
40
+ const positional = [];
41
+
42
+ for (let i = 0; i < args.length; i++) {
43
+ const arg = args[i];
44
+ if (arg === '--limit' && args[i + 1]) { flags.limit = parseInt(args[++i], 10); }
45
+ else if (arg === '--role' && args[i + 1]) { flags.role = args[++i]; }
46
+ else if (arg === '--json') { flags.json = true; }
47
+ else if (arg === '--import') { flags.import = true; }
48
+ else if (arg === '--stats') { flags.stats = true; }
49
+ else if (arg === '--help' || arg === '-h') { flags.help = true; }
50
+ else { positional.push(arg); }
51
+ }
52
+
53
+ const query = positional.join(' ');
54
+ const isPiped = !process.stdout.isTTY;
55
+ const useJson = flags.json || isPiped;
56
+
57
+ // ── Help ────────────────────────────────────
58
+
59
+ if (flags.help) {
60
+ console.log(`
61
+ session-search — FTS5 episodic recall for OpenClaw sessions
62
+
63
+ Usage:
64
+ session-search <query> Search transcripts
65
+ session-search --import Import JSONL sessions into SQLite
66
+ session-search --stats Show database stats
67
+
68
+ Options:
69
+ --limit <n> Max sessions to return (default: 10)
70
+ --role <role> Filter by role: user, assistant
71
+ --json Force JSON output (auto-detected when piped)
72
+ --help Show this help
73
+
74
+ Examples:
75
+ session-search "NATS URL fix"
76
+ session-search --role user "API key"
77
+ session-search --limit 5 "memory daemon"
78
+ `);
79
+ process.exit(0);
80
+ }
81
+
82
+ // ── Dynamic Import ────────────────────────────────────
83
+
84
+ async function main() {
85
+ // Import session-store (ESM)
86
+ const { SessionStore } = await import(path.join(WORKSPACE, 'lib/session-store.mjs'));
87
+ const store = new SessionStore();
88
+
89
+ // ── Import mode ────────────────────────────────────
90
+ if (flags.import) {
91
+ const transcriptRegistry = path.join(HOME, '.openclaw/config/transcript-sources.json');
92
+ let sources = []; // { path, name, format }
93
+
94
+ if (fs.existsSync(transcriptRegistry)) {
95
+ try {
96
+ const reg = JSON.parse(fs.readFileSync(transcriptRegistry, 'utf-8'));
97
+ sources = (reg.sources || [])
98
+ .filter(s => s.enabled !== false)
99
+ .map(s => ({
100
+ path: s.path.startsWith('~') ? path.join(HOME, s.path.slice(1)) : s.path,
101
+ name: s.name || 'unknown',
102
+ format: s.format || null,
103
+ }));
104
+ } catch { /* fall through */ }
105
+ }
106
+
107
+ if (sources.length === 0) {
108
+ // Fallback: known transcript locations with correct format tags
109
+ const wsAbs = fs.existsSync(WORKSPACE) ? fs.realpathSync(WORKSPACE) : WORKSPACE;
110
+ const slug = wsAbs.replace(/[/.]/g, '-');
111
+ sources = [
112
+ { path: path.join(HOME, '.claude/projects', slug), name: 'claude-code', format: 'claude-code' },
113
+ { path: path.join(HOME, '.claude/projects', '-' + path.basename(HOME)), name: 'claude-home', format: 'claude-code' },
114
+ { path: path.join(HOME, '.openclaw/agents/main/sessions'), name: 'gateway', format: 'openclaw-gateway' },
115
+ ];
116
+ }
117
+
118
+ let totalImported = 0, totalSkipped = 0;
119
+ for (const src of sources) {
120
+ if (!fs.existsSync(src.path)) continue;
121
+ const result = await store.importDirectory(src.path, { source: src.name, format: src.format });
122
+ totalImported += result.imported;
123
+ totalSkipped += result.skipped;
124
+ if (!useJson) {
125
+ process.stderr.write(` ${src.path}: ${result.imported} imported, ${result.skipped} skipped\n`);
126
+ }
127
+ }
128
+
129
+ if (useJson) {
130
+ console.log(JSON.stringify({ imported: totalImported, skipped: totalSkipped }));
131
+ } else {
132
+ console.log(`\nTotal: ${totalImported} imported, ${totalSkipped} skipped`);
133
+ }
134
+
135
+ store.close();
136
+ return;
137
+ }
138
+
139
+ // ── Stats mode ────────────────────────────────────
140
+ if (flags.stats) {
141
+ const stats = store.getStats();
142
+ if (useJson) {
143
+ console.log(JSON.stringify(stats));
144
+ } else {
145
+ console.log(`Sessions: ${stats.sessionCount}`);
146
+ console.log(`Messages: ${stats.messageCount}`);
147
+ console.log(`DB size: ${stats.dbSizeMb} MB`);
148
+ }
149
+ store.close();
150
+ return;
151
+ }
152
+
153
+ // ── Search mode ────────────────────────────────────
154
+ if (!query) {
155
+ console.error('Usage: session-search <query>');
156
+ console.error(' session-search --help for more options');
157
+ process.exit(1);
158
+ }
159
+
160
+ const results = store.search(query, {
161
+ limit: flags.limit,
162
+ role: flags.role,
163
+ });
164
+
165
+ if (useJson) {
166
+ console.log(JSON.stringify(results, null, 2));
167
+ } else {
168
+ if (results.length === 0) {
169
+ console.log('No matches found.');
170
+ store.close();
171
+ return;
172
+ }
173
+
174
+ for (const result of results) {
175
+ const date = result.startTime
176
+ ? new Date(result.startTime).toLocaleDateString('en-CA')
177
+ : 'unknown';
178
+
179
+ console.log(`\n${'─'.repeat(60)}`);
180
+ console.log(`Session: ${result.sessionId.slice(0, 12)} (${result.source})`);
181
+ console.log(`Date: ${date} | Matches: ${result.matchCount} | Score: ${result.score}`);
182
+
183
+ for (const excerpt of result.excerpts) {
184
+ console.log(` ┌ turns ${excerpt.startTurn}–${excerpt.endTurn}`);
185
+ for (const turn of excerpt.turns) {
186
+ const marker = turn.isMatch ? '►' : ' ';
187
+ const role = turn.role.padEnd(10);
188
+ const text = turn.content.replace(/\n/g, ' ').slice(0, 120);
189
+ console.log(` ${marker} [${role}] ${text}`);
190
+ }
191
+ console.log(` └`);
192
+ }
193
+ }
194
+ console.log(`\n${'─'.repeat(60)}`);
195
+ console.log(`${results.length} session(s) matched.`);
196
+ }
197
+
198
+ store.close();
199
+ }
200
+
201
+ main().catch(err => {
202
+ console.error(`Error: ${err.message}`);
203
+ process.exit(1);
204
+ });