openclaw-node-harness 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/mesh-agent.js +603 -81
- package/bin/mesh-bridge.js +340 -11
- package/bin/mesh-deploy-listener.js +119 -97
- package/bin/mesh-deploy.js +8 -0
- package/bin/mesh-task-daemon.js +1005 -40
- package/bin/mesh.js +423 -6
- package/config/claude-settings.json +95 -0
- package/config/daemon.json.template +2 -1
- package/config/git-hooks/pre-commit +13 -0
- package/config/git-hooks/pre-push +12 -0
- package/config/harness-rules.json +174 -0
- package/config/plan-templates/team-bugfix.yaml +52 -0
- package/config/plan-templates/team-deploy.yaml +50 -0
- package/config/plan-templates/team-feature.yaml +71 -0
- package/config/roles/qa-engineer.yaml +36 -0
- package/config/roles/solidity-dev.yaml +51 -0
- package/config/roles/tech-architect.yaml +36 -0
- package/config/rules/framework/solidity.md +22 -0
- package/config/rules/framework/typescript.md +21 -0
- package/config/rules/framework/unity.md +21 -0
- package/config/rules/universal/design-docs.md +18 -0
- package/config/rules/universal/git-hygiene.md +18 -0
- package/config/rules/universal/security.md +19 -0
- package/config/rules/universal/test-standards.md +19 -0
- package/identity/DELEGATION.md +6 -6
- package/install.sh +300 -8
- package/lib/circling-parser.js +119 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +59 -10
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +528 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +245 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +354 -4
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +13 -5
- package/lib/mesh-registry.js +11 -2
- package/lib/mesh-tasks.js +67 -0
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +320 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +458 -0
- package/lib/transcript-parser.mjs +292 -0
- package/mission-control/drizzle/soul_schema_update.sql +29 -0
- package/mission-control/drizzle.config.ts +1 -4
- package/mission-control/package-lock.json +1571 -83
- package/mission-control/package.json +6 -2
- package/mission-control/scripts/gen-chronology.js +3 -3
- package/mission-control/scripts/import-pipeline-v2.js +0 -16
- package/mission-control/scripts/import-pipeline.js +0 -15
- package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
- package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
- package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
- package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
- package/mission-control/src/app/api/cowork/events/route.ts +65 -0
- package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
- package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
- package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
- package/mission-control/src/app/api/diagnostics/route.ts +97 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
- package/mission-control/src/app/api/mesh/events/route.ts +95 -19
- package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
- package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
- package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
- package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
- package/mission-control/src/app/api/tasks/route.ts +21 -30
- package/mission-control/src/app/cowork/page.tsx +261 -0
- package/mission-control/src/app/diagnostics/page.tsx +385 -0
- package/mission-control/src/app/graph/page.tsx +26 -0
- package/mission-control/src/app/memory/page.tsx +1 -1
- package/mission-control/src/app/obsidian/page.tsx +36 -6
- package/mission-control/src/app/roadmap/page.tsx +24 -0
- package/mission-control/src/app/souls/page.tsx +2 -2
- package/mission-control/src/components/board/execution-config.tsx +431 -0
- package/mission-control/src/components/board/kanban-board.tsx +75 -9
- package/mission-control/src/components/board/kanban-column.tsx +135 -19
- package/mission-control/src/components/board/task-card.tsx +55 -2
- package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
- package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
- package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
- package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
- package/mission-control/src/components/cowork/role-picker.tsx +102 -0
- package/mission-control/src/components/cowork/session-card.tsx +284 -0
- package/mission-control/src/components/layout/sidebar.tsx +39 -2
- package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
- package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
- package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
- package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
- package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
- package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
- package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
- package/mission-control/src/lib/config.ts +58 -0
- package/mission-control/src/lib/db/index.ts +69 -0
- package/mission-control/src/lib/db/schema.ts +61 -3
- package/mission-control/src/lib/hooks.ts +309 -0
- package/mission-control/src/lib/memory/entities.ts +3 -2
- package/mission-control/src/lib/nats.ts +66 -1
- package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
- package/mission-control/src/lib/parsers/transcript.ts +4 -4
- package/mission-control/src/lib/scheduler.ts +12 -11
- package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
- package/mission-control/src/lib/sync/tasks.ts +23 -1
- package/mission-control/src/lib/task-id.ts +32 -0
- package/mission-control/src/lib/tts/index.ts +33 -9
- package/mission-control/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- package/workspace-bin/web-fetch.mjs +65 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* memory-daemon.mjs — OpenClaw platform-level memory lifecycle daemon (
|
|
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
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* web-fetch.mjs — Playwright-based web fetcher
|
|
4
|
+
*
|
|
5
|
+
* Renders JS-heavy pages and returns clean text/HTML.
|
|
6
|
+
* Fallback for when WebFetch gets blocked by anti-bot or JS rendering.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node bin/web-fetch.mjs <url> # returns text content
|
|
10
|
+
* node bin/web-fetch.mjs <url> --html # returns raw HTML
|
|
11
|
+
* node bin/web-fetch.mjs <url> --selector "article" # extract specific element
|
|
12
|
+
* node bin/web-fetch.mjs <url> --wait 5000 # custom wait (ms)
|
|
13
|
+
* node bin/web-fetch.mjs <url> --screenshot out.png # save screenshot
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { chromium } from 'playwright';
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const url = args.find(a => !a.startsWith('--'));
|
|
20
|
+
|
|
21
|
+
if (!url) {
|
|
22
|
+
console.error('Usage: web-fetch.mjs <url> [--html] [--selector "css"] [--wait ms] [--screenshot file]');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const flags = {
|
|
27
|
+
html: args.includes('--html'),
|
|
28
|
+
selector: null,
|
|
29
|
+
wait: 3000,
|
|
30
|
+
screenshot: null,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
if (args[i] === '--selector' && args[i + 1]) flags.selector = args[++i];
|
|
35
|
+
if (args[i] === '--wait' && args[i + 1]) flags.wait = parseInt(args[++i], 10);
|
|
36
|
+
if (args[i] === '--screenshot' && args[i + 1]) flags.screenshot = args[++i];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const browser = await chromium.launch({ headless: true });
|
|
40
|
+
try {
|
|
41
|
+
const page = await browser.newPage();
|
|
42
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
|
43
|
+
|
|
44
|
+
if (flags.wait > 0) {
|
|
45
|
+
await page.waitForTimeout(flags.wait);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (flags.screenshot) {
|
|
49
|
+
await page.screenshot({ path: flags.screenshot, fullPage: true });
|
|
50
|
+
console.error(`Screenshot saved: ${flags.screenshot}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (flags.selector) {
|
|
54
|
+
const el = await page.$(flags.selector);
|
|
55
|
+
if (!el) {
|
|
56
|
+
console.error(`Selector "${flags.selector}" not found`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
console.log(flags.html ? await el.innerHTML() : await el.innerText());
|
|
60
|
+
} else {
|
|
61
|
+
console.log(flags.html ? await page.content() : await page.innerText('body'));
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
await browser.close();
|
|
65
|
+
}
|