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.
- package/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/lane-watchdog.js +23 -2
- package/bin/mesh-agent.js +439 -28
- package/bin/mesh-bridge.js +69 -3
- package/bin/mesh-health-publisher.js +41 -1
- package/bin/mesh-task-daemon.js +821 -26
- package/bin/mesh.js +411 -20
- 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 +296 -10
- package/lib/agent-activity.js +2 -2
- package/lib/circling-parser.js +119 -0
- package/lib/exec-safety.js +105 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +24 -31
- package/lib/llm-providers.js +16 -0
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +530 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +252 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +483 -165
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +79 -50
- package/lib/mesh-tasks.js +132 -49
- package/lib/nats-resolve.js +4 -4
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +322 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +461 -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/memory/search/route.ts +6 -3
- 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/souls/[id]/evolution/route.ts +21 -5
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +14 -2
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +8 -2
- 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/api/workspace/read/route.ts +11 -0
- 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 +67 -0
- package/mission-control/src/lib/db/index.ts +85 -1
- 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/memory/extract.ts +2 -1
- package/mission-control/src/lib/memory/retrieval.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/src/middleware.ts +82 -0
- package/mission-control/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +1 -1
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/uninstall.sh +37 -9
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- package/workspace-bin/web-fetch.mjs +65 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-budget.mjs — MEMORY.md Character Budget + Frozen Snapshot
|
|
3
|
+
*
|
|
4
|
+
* Enforces a hard character cap on MEMORY.md (~2,200 chars, matching Hermes's
|
|
5
|
+
* proven bound) and provides frozen-snapshot semantics per session.
|
|
6
|
+
*
|
|
7
|
+
* Key concepts:
|
|
8
|
+
* - Session start: snapshot MEMORY.md and freeze it for the prompt
|
|
9
|
+
* - Mid-session writes: persist to disk but DON'T mutate the active prompt
|
|
10
|
+
* - On compression rebuild or new session: reload from disk
|
|
11
|
+
* - Usage meter: tracks % used, emits warnings at thresholds
|
|
12
|
+
*
|
|
13
|
+
* EventEmitter events:
|
|
14
|
+
* - 'add' { entry, usedChars, totalBudget, pctUsed }
|
|
15
|
+
* - 'warning' { pctUsed, message }
|
|
16
|
+
* - 'trim' { removed, reason }
|
|
17
|
+
* - 'freeze' { charCount, lineCount }
|
|
18
|
+
* - 'reload' { charCount, lineCount }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { EventEmitter } from 'events';
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_CHAR_BUDGET = 2200;
|
|
26
|
+
const WARNING_THRESHOLD = 0.80; // warn at 80%
|
|
27
|
+
const CRITICAL_THRESHOLD = 0.95; // critical at 95%
|
|
28
|
+
|
|
29
|
+
export class MemoryBudget extends EventEmitter {
|
|
30
|
+
#filePath;
|
|
31
|
+
#charBudget;
|
|
32
|
+
#frozenContent = null; // snapshot at session start
|
|
33
|
+
#frozenAt = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} filePath - Absolute path to MEMORY.md
|
|
37
|
+
* @param {Object} opts
|
|
38
|
+
* @param {number} opts.charBudget - Character budget (default 2200)
|
|
39
|
+
*/
|
|
40
|
+
constructor(filePath, opts = {}) {
|
|
41
|
+
super();
|
|
42
|
+
this.#filePath = filePath;
|
|
43
|
+
this.#charBudget = opts.charBudget || DEFAULT_CHAR_BUDGET;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get filePath() { return this.#filePath; }
|
|
47
|
+
get charBudget() { return this.#charBudget; }
|
|
48
|
+
get isFrozen() { return this.#frozenContent !== null; }
|
|
49
|
+
|
|
50
|
+
// ── Snapshot Lifecycle ────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Freeze MEMORY.md content for this session.
|
|
54
|
+
* Call at session start (Phase 0 bootstrap).
|
|
55
|
+
* Returns the frozen content for prompt injection.
|
|
56
|
+
*/
|
|
57
|
+
startSession() {
|
|
58
|
+
this.#frozenContent = this.#readFile();
|
|
59
|
+
this.#frozenAt = Date.now();
|
|
60
|
+
|
|
61
|
+
const stats = this.#computeStats(this.#frozenContent);
|
|
62
|
+
this.emit('freeze', {
|
|
63
|
+
charCount: this.#frozenContent.length,
|
|
64
|
+
lineCount: this.#frozenContent.split('\n').length,
|
|
65
|
+
...stats,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return this.#frozenContent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the frozen (session-start) content for prompt rendering.
|
|
73
|
+
* Returns the frozen snapshot — NOT the live disk content.
|
|
74
|
+
* This is the core of deterministic prompt content per session.
|
|
75
|
+
*/
|
|
76
|
+
getRendered() {
|
|
77
|
+
if (this.#frozenContent === null) {
|
|
78
|
+
// Not frozen yet — return live content (pre-session or fallback)
|
|
79
|
+
return this.#readFile();
|
|
80
|
+
}
|
|
81
|
+
return this.#frozenContent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reload from disk and update the frozen snapshot.
|
|
86
|
+
* Call after compression rebuild or at new session start.
|
|
87
|
+
*/
|
|
88
|
+
reload() {
|
|
89
|
+
this.#frozenContent = this.#readFile();
|
|
90
|
+
this.#frozenAt = Date.now();
|
|
91
|
+
|
|
92
|
+
const stats = this.#computeStats(this.#frozenContent);
|
|
93
|
+
this.emit('reload', {
|
|
94
|
+
charCount: this.#frozenContent.length,
|
|
95
|
+
lineCount: this.#frozenContent.split('\n').length,
|
|
96
|
+
...stats,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return this.#frozenContent;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* End the session — clear the frozen snapshot.
|
|
104
|
+
*/
|
|
105
|
+
endSession() {
|
|
106
|
+
this.#frozenContent = null;
|
|
107
|
+
this.#frozenAt = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Budget-Aware Write ────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Add an entry to MEMORY.md on disk (not the frozen prompt).
|
|
114
|
+
* Respects character budget. Returns false if over budget.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} entry - The line to add (without leading "- ")
|
|
117
|
+
* @param {Object} opts
|
|
118
|
+
* @param {string} opts.section - Section to add under (default "Recent")
|
|
119
|
+
* @returns {{ added: boolean, pctUsed: number, charsRemaining: number }}
|
|
120
|
+
*/
|
|
121
|
+
addEntry(entry, opts = {}) {
|
|
122
|
+
const { section = 'Recent' } = opts;
|
|
123
|
+
let content = this.#readFile();
|
|
124
|
+
const line = `- ${entry}`;
|
|
125
|
+
const newLength = content.length + line.length + 1; // +1 for newline
|
|
126
|
+
|
|
127
|
+
if (newLength > this.#charBudget) {
|
|
128
|
+
// Try trimming oldest entries first
|
|
129
|
+
const trimmed = this.#trimOldest(content, line.length + 1);
|
|
130
|
+
if (trimmed) {
|
|
131
|
+
content = trimmed;
|
|
132
|
+
} else {
|
|
133
|
+
return { added: false, pctUsed: this.#pctUsed(content), charsRemaining: 0 };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Ensure section exists
|
|
138
|
+
if (!content.includes(`## ${section}`)) {
|
|
139
|
+
content = content.trimEnd() + `\n\n## ${section}\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
content = content.trimEnd() + `\n${line}\n`;
|
|
143
|
+
this.#writeFile(content);
|
|
144
|
+
|
|
145
|
+
const pctUsed = this.#pctUsed(content);
|
|
146
|
+
const charsRemaining = Math.max(0, this.#charBudget - content.length);
|
|
147
|
+
|
|
148
|
+
this.emit('add', {
|
|
149
|
+
entry,
|
|
150
|
+
usedChars: content.length,
|
|
151
|
+
totalBudget: this.#charBudget,
|
|
152
|
+
pctUsed,
|
|
153
|
+
charsRemaining,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Threshold warnings
|
|
157
|
+
if (pctUsed >= CRITICAL_THRESHOLD * 100) {
|
|
158
|
+
this.emit('warning', {
|
|
159
|
+
pctUsed,
|
|
160
|
+
message: `MEMORY.md at ${pctUsed}% capacity (${charsRemaining} chars remaining)`,
|
|
161
|
+
});
|
|
162
|
+
} else if (pctUsed >= WARNING_THRESHOLD * 100) {
|
|
163
|
+
this.emit('warning', {
|
|
164
|
+
pctUsed,
|
|
165
|
+
message: `MEMORY.md approaching limit: ${pctUsed}% used (${charsRemaining} chars remaining)`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { added: true, pctUsed, charsRemaining };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Usage Meter ────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get current budget usage stats.
|
|
176
|
+
* @returns {{ usedChars: number, totalBudget: number, pctUsed: number, charsRemaining: number, lineCount: number, meterDisplay: string }}
|
|
177
|
+
*/
|
|
178
|
+
getStats() {
|
|
179
|
+
const content = this.#readFile();
|
|
180
|
+
return this.#computeStats(content);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Render a usage meter string for logging/display.
|
|
185
|
+
* Example: "[67% — 1,474/2,200 chars]"
|
|
186
|
+
*/
|
|
187
|
+
getMeterDisplay() {
|
|
188
|
+
return this.#computeStats(this.#readFile()).meterDisplay;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Private Helpers ────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
#readFile() {
|
|
194
|
+
if (!fs.existsSync(this.#filePath)) return '';
|
|
195
|
+
return fs.readFileSync(this.#filePath, 'utf-8');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#writeFile(content) {
|
|
199
|
+
const dir = path.dirname(this.#filePath);
|
|
200
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
201
|
+
fs.writeFileSync(this.#filePath, content);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#pctUsed(content) {
|
|
205
|
+
return Math.round((content.length / this.#charBudget) * 100);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#computeStats(content) {
|
|
209
|
+
const usedChars = content.length;
|
|
210
|
+
const pctUsed = this.#pctUsed(content);
|
|
211
|
+
const charsRemaining = Math.max(0, this.#charBudget - usedChars);
|
|
212
|
+
const lineCount = content.split('\n').length;
|
|
213
|
+
const meterDisplay = `[${pctUsed}% — ${usedChars.toLocaleString()}/${this.#charBudget.toLocaleString()} chars]`;
|
|
214
|
+
|
|
215
|
+
return { usedChars, totalBudget: this.#charBudget, pctUsed, charsRemaining, lineCount, meterDisplay };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Trim the oldest bullet entries to free up space.
|
|
220
|
+
* Returns the trimmed content, or null if can't free enough.
|
|
221
|
+
*/
|
|
222
|
+
#trimOldest(content, bytesNeeded) {
|
|
223
|
+
const lines = content.split('\n');
|
|
224
|
+
const bulletIndices = [];
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < lines.length; i++) {
|
|
227
|
+
if (lines[i].startsWith('- ') || lines[i].startsWith('* ')) {
|
|
228
|
+
bulletIndices.push(i);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (bulletIndices.length === 0) return null;
|
|
233
|
+
|
|
234
|
+
// Remove bullets from the top until we have enough space
|
|
235
|
+
let freed = 0;
|
|
236
|
+
const toRemove = new Set();
|
|
237
|
+
|
|
238
|
+
for (const idx of bulletIndices) {
|
|
239
|
+
if (freed >= bytesNeeded) break;
|
|
240
|
+
freed += lines[idx].length + 1;
|
|
241
|
+
toRemove.add(idx);
|
|
242
|
+
|
|
243
|
+
this.emit('trim', { removed: lines[idx], reason: 'budget overflow' });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (freed < bytesNeeded) return null;
|
|
247
|
+
|
|
248
|
+
const trimmed = lines.filter((_, i) => !toRemove.has(i)).join('\n');
|
|
249
|
+
return trimmed;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create a MemoryBudget instance with default OpenClaw paths.
|
|
255
|
+
* @param {string} workspace - OpenClaw workspace root
|
|
256
|
+
* @param {Object} opts - Options forwarded to MemoryBudget constructor
|
|
257
|
+
*/
|
|
258
|
+
export function createBudget(workspace, opts = {}) {
|
|
259
|
+
const filePath = path.join(workspace, 'MEMORY.md');
|
|
260
|
+
return new MemoryBudget(filePath, opts);
|
|
261
|
+
}
|