@karmaniverous/jeeves 0.5.0 → 0.5.3
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 +4 -4
- package/content/agents-section.md +1 -31
- package/content/skill.md +23 -0
- package/content/soul-section.md +1 -11
- package/dist/cli/jeeves/index.js +96 -71
- package/dist/cli/plugin/index.js +105 -20
- package/dist/cli/service/index.js +36 -27
- package/dist/index.d.ts +87 -24
- package/dist/index.js +426 -209
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs, { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, mkdirSync, readdirSync, copyFileSync, rmSync, cpSync } from 'node:fs';
|
|
2
|
-
import path, { join, dirname,
|
|
2
|
+
import path, { join, dirname, basename, resolve } from 'node:path';
|
|
3
|
+
import crypto, { randomUUID } from 'node:crypto';
|
|
3
4
|
import { lock } from 'proper-lockfile';
|
|
4
5
|
import { JSONPath } from 'jsonpath-plus';
|
|
5
6
|
import { major, valid, gte, gt } from 'semver';
|
|
@@ -9,7 +10,6 @@ import { packageDirectorySync } from 'package-directory';
|
|
|
9
10
|
import { homedir } from 'node:os';
|
|
10
11
|
import cp, { execSync } from 'node:child_process';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import crypto from 'node:crypto';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Comment markers for managed content blocks.
|
|
@@ -183,14 +183,14 @@ const PLATFORM_COMPONENTS = [
|
|
|
183
183
|
* Core library version, inlined at build time.
|
|
184
184
|
*
|
|
185
185
|
* @remarks
|
|
186
|
-
* The `0.
|
|
186
|
+
* The `0.5.2` placeholder is replaced by
|
|
187
187
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
188
188
|
* from `package.json`. This ensures the correct version survives
|
|
189
189
|
* when consumers bundle core into their own dist (where runtime
|
|
190
190
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
191
191
|
*/
|
|
192
192
|
/** The core library version from package.json (inlined at build time). */
|
|
193
|
-
const CORE_VERSION = '0.
|
|
193
|
+
const CORE_VERSION = '0.5.2';
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* Workspace and config root initialization.
|
|
@@ -288,27 +288,46 @@ const STALE_LOCK_MS = 120_000;
|
|
|
288
288
|
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
289
289
|
/** Lock retry options. */
|
|
290
290
|
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
291
|
+
/** Maximum rename retry attempts on EPERM. */
|
|
292
|
+
const ATOMIC_WRITE_MAX_RETRIES = 3;
|
|
293
|
+
/** Delay between EPERM retries in milliseconds. */
|
|
294
|
+
const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
|
|
291
295
|
/**
|
|
292
296
|
* Write content to a file atomically via a temp file + rename.
|
|
293
297
|
*
|
|
298
|
+
* @remarks
|
|
299
|
+
* Retries the rename up to three times on EPERM (Windows file-handle
|
|
300
|
+
* contention) with a 100 ms synchronous delay between attempts.
|
|
301
|
+
*
|
|
294
302
|
* @param filePath - Absolute path to the target file.
|
|
295
303
|
* @param content - Content to write.
|
|
296
304
|
*/
|
|
297
305
|
function atomicWrite(filePath, content) {
|
|
298
306
|
const dir = dirname(filePath);
|
|
299
|
-
const
|
|
307
|
+
const base = basename(filePath, '.md');
|
|
308
|
+
const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
|
|
300
309
|
writeFileSync(tempPath, content, 'utf-8');
|
|
301
|
-
|
|
302
|
-
renameSync(tempPath, filePath);
|
|
303
|
-
}
|
|
304
|
-
catch (err) {
|
|
310
|
+
for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
|
|
305
311
|
try {
|
|
306
|
-
|
|
312
|
+
renameSync(tempPath, filePath);
|
|
313
|
+
return;
|
|
307
314
|
}
|
|
308
|
-
catch {
|
|
309
|
-
|
|
315
|
+
catch (err) {
|
|
316
|
+
const isEperm = err instanceof Error &&
|
|
317
|
+
'code' in err &&
|
|
318
|
+
err.code === 'EPERM';
|
|
319
|
+
if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
|
|
320
|
+
try {
|
|
321
|
+
unlinkSync(tempPath);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
/* best-effort cleanup */
|
|
325
|
+
}
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
// Synchronous sleep before retry (acceptable in atomic write context)
|
|
329
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
|
|
310
330
|
}
|
|
311
|
-
throw err;
|
|
312
331
|
}
|
|
313
332
|
}
|
|
314
333
|
/**
|
|
@@ -343,6 +362,21 @@ async function withFileLock(filePath, fn) {
|
|
|
343
362
|
}
|
|
344
363
|
}
|
|
345
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Shared internal utility functions.
|
|
367
|
+
*
|
|
368
|
+
* @packageDocumentation
|
|
369
|
+
*/
|
|
370
|
+
/**
|
|
371
|
+
* Extract a human-readable message from an unknown caught value.
|
|
372
|
+
*
|
|
373
|
+
* @param err - The caught value (typically `unknown`).
|
|
374
|
+
* @returns The error message string.
|
|
375
|
+
*/
|
|
376
|
+
function getErrorMessage(err) {
|
|
377
|
+
return err instanceof Error ? err.message : String(err);
|
|
378
|
+
}
|
|
379
|
+
|
|
346
380
|
/**
|
|
347
381
|
* Factory for a framework-agnostic config apply HTTP handler.
|
|
348
382
|
*
|
|
@@ -395,8 +429,7 @@ function readConfigFile(filePath) {
|
|
|
395
429
|
return JSON.parse(raw);
|
|
396
430
|
}
|
|
397
431
|
catch (err) {
|
|
398
|
-
|
|
399
|
-
console.warn(`jeeves-core: Could not read config file ${filePath}: ${msg}`);
|
|
432
|
+
console.warn(`jeeves-core: Could not read config file ${filePath}: ${getErrorMessage(err)}`);
|
|
400
433
|
return {};
|
|
401
434
|
}
|
|
402
435
|
}
|
|
@@ -445,10 +478,9 @@ function createConfigApplyHandler(descriptor) {
|
|
|
445
478
|
atomicWrite(configPath, json);
|
|
446
479
|
}
|
|
447
480
|
catch (err) {
|
|
448
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
449
481
|
return {
|
|
450
482
|
status: 500,
|
|
451
|
-
body: { error: `Failed to write config: ${
|
|
483
|
+
body: { error: `Failed to write config: ${getErrorMessage(err)}` },
|
|
452
484
|
};
|
|
453
485
|
}
|
|
454
486
|
// Call onConfigApply callback if defined
|
|
@@ -457,12 +489,11 @@ function createConfigApplyHandler(descriptor) {
|
|
|
457
489
|
await descriptor.onConfigApply(validatedConfig);
|
|
458
490
|
}
|
|
459
491
|
catch (err) {
|
|
460
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
461
492
|
return {
|
|
462
493
|
status: 200,
|
|
463
494
|
body: {
|
|
464
495
|
applied: true,
|
|
465
|
-
warning: `Config written but callback failed: ${
|
|
496
|
+
warning: `Config written but callback failed: ${getErrorMessage(err)}`,
|
|
466
497
|
config: validatedConfig,
|
|
467
498
|
},
|
|
468
499
|
};
|
|
@@ -545,8 +576,7 @@ function createStatusHandler(options) {
|
|
|
545
576
|
health = await options.getHealth();
|
|
546
577
|
}
|
|
547
578
|
catch (err) {
|
|
548
|
-
|
|
549
|
-
health = { error: message };
|
|
579
|
+
health = { error: getErrorMessage(err) };
|
|
550
580
|
overallStatus = 'degraded';
|
|
551
581
|
}
|
|
552
582
|
}
|
|
@@ -635,7 +665,13 @@ const workspaceConfigSchema = z.object({
|
|
|
635
665
|
/** Memory hygiene shared defaults. */
|
|
636
666
|
memory: workspaceMemoryConfigSchema.optional(),
|
|
637
667
|
});
|
|
638
|
-
/**
|
|
668
|
+
/**
|
|
669
|
+
* Built-in workspace config defaults.
|
|
670
|
+
*
|
|
671
|
+
* @remarks
|
|
672
|
+
* These defaults are used as the lowest-priority tier in config resolution
|
|
673
|
+
* (below CLI flags, env vars, and `jeeves.config.json` values).
|
|
674
|
+
*/
|
|
639
675
|
const WORKSPACE_CONFIG_DEFAULTS = {
|
|
640
676
|
core: {
|
|
641
677
|
workspace: '.',
|
|
@@ -664,8 +700,7 @@ function loadWorkspaceConfig(workspacePath) {
|
|
|
664
700
|
return workspaceConfigSchema.parse(parsed);
|
|
665
701
|
}
|
|
666
702
|
catch (err) {
|
|
667
|
-
|
|
668
|
-
console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
|
|
703
|
+
console.warn(`jeeves-core: failed to load ${configPath}: ${getErrorMessage(err)}`);
|
|
669
704
|
return undefined;
|
|
670
705
|
}
|
|
671
706
|
}
|
|
@@ -1007,7 +1042,7 @@ function parseHeartbeat(fileContent) {
|
|
|
1007
1042
|
const userContent = fileContent.slice(0, headingIndex).trim();
|
|
1008
1043
|
const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
|
|
1009
1044
|
const entries = [];
|
|
1010
|
-
const h2Re = /^## (jeeves-\S
|
|
1045
|
+
const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
|
|
1011
1046
|
let match;
|
|
1012
1047
|
const h2Positions = [];
|
|
1013
1048
|
while ((match = h2Re.exec(sectionContent)) !== null) {
|
|
@@ -1088,8 +1123,7 @@ async function writeHeartbeatSection(filePath, entries) {
|
|
|
1088
1123
|
});
|
|
1089
1124
|
}
|
|
1090
1125
|
catch (err) {
|
|
1091
|
-
|
|
1092
|
-
console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${message}`);
|
|
1126
|
+
console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${getErrorMessage(err)}`);
|
|
1093
1127
|
}
|
|
1094
1128
|
}
|
|
1095
1129
|
|
|
@@ -1126,6 +1160,15 @@ function sortSectionsByOrder(sections) {
|
|
|
1126
1160
|
* sections within the block, and returns the structured result plus
|
|
1127
1161
|
* user content outside the markers.
|
|
1128
1162
|
*/
|
|
1163
|
+
/**
|
|
1164
|
+
* Escape a string for safe use as a literal in a RegExp pattern.
|
|
1165
|
+
*
|
|
1166
|
+
* @param str - The string to escape.
|
|
1167
|
+
* @returns The escaped string.
|
|
1168
|
+
*/
|
|
1169
|
+
function escapeForRegex(str) {
|
|
1170
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1171
|
+
}
|
|
1129
1172
|
/**
|
|
1130
1173
|
* Build regex patterns for the given markers.
|
|
1131
1174
|
*
|
|
@@ -1133,11 +1176,9 @@ function sortSectionsByOrder(sections) {
|
|
|
1133
1176
|
* @returns Object with begin and end regex patterns.
|
|
1134
1177
|
*/
|
|
1135
1178
|
function buildMarkerPatterns(markers) {
|
|
1136
|
-
const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1137
|
-
const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1138
1179
|
return {
|
|
1139
|
-
beginRe: new RegExp(`^<!--\\s*${
|
|
1140
|
-
endRe: new RegExp(`^<!--\\s*${
|
|
1180
|
+
beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
|
|
1181
|
+
endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
|
|
1141
1182
|
};
|
|
1142
1183
|
}
|
|
1143
1184
|
/**
|
|
@@ -1477,6 +1518,29 @@ MEMORY.md has a character budget (default 20,000). Core tracks:
|
|
|
1477
1518
|
- Evergreen sections (no dates) are never flagged
|
|
1478
1519
|
|
|
1479
1520
|
Review is human/agent-mediated — core does not auto-delete.
|
|
1521
|
+
|
|
1522
|
+
### HEARTBEAT Integration
|
|
1523
|
+
|
|
1524
|
+
Memory hygiene is checked on every \`ComponentWriter\` cycle alongside component health. When budget or staleness thresholds are breached, a \`## MEMORY.md\` alert appears in HEARTBEAT.md under \`# Jeeves Platform Status\`. The alert includes character count, budget usage percentage, and any stale section names. When memory is healthy, the heading is absent — no alert content, no LLM cost on heartbeat polls.
|
|
1525
|
+
|
|
1526
|
+
The \`## MEMORY.md\` heading follows the same declined/active lifecycle as component headings (\`## jeeves-{name}\`). Users can decline memory alerts by changing the heading to \`## MEMORY.md: declined\`.
|
|
1527
|
+
|
|
1528
|
+
## Workspace File Size Monitoring
|
|
1529
|
+
|
|
1530
|
+
OpenClaw applies a ~20,000-char injection limit to all workspace bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, USER.md, MEMORY.md). Files exceeding the limit are silently truncated.
|
|
1531
|
+
|
|
1532
|
+
Core monitors all five files on every \`ComponentWriter\` cycle:
|
|
1533
|
+
- Warning at 80% of budget (fixed threshold; not configurable via \`jeeves.config.json\`)
|
|
1534
|
+
- Over-budget alert when charCount exceeds the budget
|
|
1535
|
+
- Missing files are silently skipped
|
|
1536
|
+
|
|
1537
|
+
### HEARTBEAT Integration
|
|
1538
|
+
|
|
1539
|
+
When a workspace file exceeds the warning threshold, a \`## {filename}\` alert appears in HEARTBEAT.md (e.g., \`## AGENTS.md\`). The alert includes:
|
|
1540
|
+
- Character count, budget, and usage percentage
|
|
1541
|
+
- Trimming guidance in priority order: (1) move domain-specific content to a local skill, (2) extract reference material to companion files with a pointer, (3) summarize verbose instructions, (4) remove stale content
|
|
1542
|
+
|
|
1543
|
+
Each file heading follows the same declined/active lifecycle as component headings. Users can decline alerts by changing the heading to \`## {filename}: declined\` (e.g., \`## AGENTS.md: declined\`).
|
|
1480
1544
|
`;
|
|
1481
1545
|
|
|
1482
1546
|
/**
|
|
@@ -1577,16 +1641,18 @@ function patchAllowList(parent, key, label, pluginId, mode) {
|
|
|
1577
1641
|
* Patch an OpenClaw config for plugin install or uninstall.
|
|
1578
1642
|
*
|
|
1579
1643
|
* @remarks
|
|
1580
|
-
* Manages `plugins.entries.{pluginId}
|
|
1644
|
+
* Manages `plugins.entries.{pluginId}`, `plugins.installs.{pluginId}`,
|
|
1645
|
+
* and `tools.alsoAllow`.
|
|
1581
1646
|
* Idempotent: adding twice produces no duplicates; removing when absent
|
|
1582
1647
|
* produces no errors.
|
|
1583
1648
|
*
|
|
1584
1649
|
* @param config - The parsed OpenClaw config object (mutated in place).
|
|
1585
1650
|
* @param pluginId - The plugin identifier.
|
|
1586
1651
|
* @param mode - Whether to add or remove the plugin.
|
|
1652
|
+
* @param installRecord - Install provenance record (required when mode is 'add').
|
|
1587
1653
|
* @returns Array of log messages describing changes made.
|
|
1588
1654
|
*/
|
|
1589
|
-
function patchConfig(config, pluginId, mode) {
|
|
1655
|
+
function patchConfig(config, pluginId, mode, installRecord) {
|
|
1590
1656
|
const messages = [];
|
|
1591
1657
|
// Ensure plugins section
|
|
1592
1658
|
if (!config.plugins || typeof config.plugins !== 'object') {
|
|
@@ -1608,6 +1674,24 @@ function patchConfig(config, pluginId, mode) {
|
|
|
1608
1674
|
Reflect.deleteProperty(entries, pluginId);
|
|
1609
1675
|
messages.push(`Removed "${pluginId}" from plugins.entries`);
|
|
1610
1676
|
}
|
|
1677
|
+
// plugins.installs
|
|
1678
|
+
if (!plugins.installs || typeof plugins.installs !== 'object') {
|
|
1679
|
+
plugins.installs = {};
|
|
1680
|
+
}
|
|
1681
|
+
const installs = plugins.installs;
|
|
1682
|
+
if (mode === 'add' && installRecord) {
|
|
1683
|
+
installs[pluginId] = {
|
|
1684
|
+
source: 'path',
|
|
1685
|
+
installPath: installRecord.installPath,
|
|
1686
|
+
version: installRecord.version,
|
|
1687
|
+
installedAt: installRecord.installedAt ?? new Date().toISOString(),
|
|
1688
|
+
};
|
|
1689
|
+
messages.push(`Wrote install record for "${pluginId}" to plugins.installs`);
|
|
1690
|
+
}
|
|
1691
|
+
else if (mode === 'remove' && pluginId in installs) {
|
|
1692
|
+
Reflect.deleteProperty(installs, pluginId);
|
|
1693
|
+
messages.push(`Removed install record for "${pluginId}" from plugins.installs`);
|
|
1694
|
+
}
|
|
1611
1695
|
// tools.alsoAllow
|
|
1612
1696
|
if (!config.tools || typeof config.tools !== 'object') {
|
|
1613
1697
|
config.tools = {};
|
|
@@ -1716,7 +1800,22 @@ function createPluginCli(options) {
|
|
|
1716
1800
|
// 2. Patch openclaw.json
|
|
1717
1801
|
console.log('Patching OpenClaw config...');
|
|
1718
1802
|
const config = readJsonFile(configPath);
|
|
1719
|
-
const
|
|
1803
|
+
const pkgJsonPathForVersion = join(extensionsDir, 'package.json');
|
|
1804
|
+
let pluginVersionForRecord;
|
|
1805
|
+
try {
|
|
1806
|
+
const pkgJsonForRecord = readJsonFile(pkgJsonPathForVersion);
|
|
1807
|
+
pluginVersionForRecord =
|
|
1808
|
+
typeof pkgJsonForRecord.version === 'string'
|
|
1809
|
+
? pkgJsonForRecord.version
|
|
1810
|
+
: undefined;
|
|
1811
|
+
}
|
|
1812
|
+
catch {
|
|
1813
|
+
// best-effort: version may not be available yet
|
|
1814
|
+
}
|
|
1815
|
+
const messages = patchConfig(config, pluginId, 'add', {
|
|
1816
|
+
installPath: extensionsDir,
|
|
1817
|
+
version: pluginVersionForRecord,
|
|
1818
|
+
});
|
|
1720
1819
|
// 3. Memory slot claim
|
|
1721
1820
|
if (opts.memory) {
|
|
1722
1821
|
if (!config.agents || typeof config.agents !== 'object') {
|
|
@@ -2440,6 +2539,10 @@ function createServiceManager(descriptor) {
|
|
|
2440
2539
|
* a component descriptor. Components add domain-specific commands
|
|
2441
2540
|
* via `descriptor.customCliCommands`.
|
|
2442
2541
|
*/
|
|
2542
|
+
function handleCommandError(action, err) {
|
|
2543
|
+
console.error(`${action} failed: ${getErrorMessage(err)}`);
|
|
2544
|
+
process.exitCode = 1;
|
|
2545
|
+
}
|
|
2443
2546
|
/**
|
|
2444
2547
|
* Create a standard service CLI program from a component descriptor.
|
|
2445
2548
|
*
|
|
@@ -2492,8 +2595,7 @@ function createServiceCli(descriptor) {
|
|
|
2492
2595
|
console.log(JSON.stringify(result, null, 2));
|
|
2493
2596
|
}
|
|
2494
2597
|
catch (err) {
|
|
2495
|
-
|
|
2496
|
-
console.error(`Service unreachable: ${msg}`);
|
|
2598
|
+
console.error(`Service unreachable: ${getErrorMessage(err)}`);
|
|
2497
2599
|
process.exitCode = 1;
|
|
2498
2600
|
}
|
|
2499
2601
|
});
|
|
@@ -2511,8 +2613,7 @@ function createServiceCli(descriptor) {
|
|
|
2511
2613
|
console.log(JSON.stringify(result, null, 2));
|
|
2512
2614
|
}
|
|
2513
2615
|
catch (err) {
|
|
2514
|
-
|
|
2515
|
-
console.error(`Config query failed: ${msg}`);
|
|
2616
|
+
console.error(`Config query failed: ${getErrorMessage(err)}`);
|
|
2516
2617
|
process.exitCode = 1;
|
|
2517
2618
|
}
|
|
2518
2619
|
});
|
|
@@ -2528,8 +2629,7 @@ function createServiceCli(descriptor) {
|
|
|
2528
2629
|
console.log('Config is valid.');
|
|
2529
2630
|
}
|
|
2530
2631
|
catch (err) {
|
|
2531
|
-
|
|
2532
|
-
console.error(`Validation failed: ${msg}`);
|
|
2632
|
+
console.error(`Validation failed: ${getErrorMessage(err)}`);
|
|
2533
2633
|
process.exitCode = 1;
|
|
2534
2634
|
}
|
|
2535
2635
|
});
|
|
@@ -2566,8 +2666,7 @@ function createServiceCli(descriptor) {
|
|
|
2566
2666
|
console.log(JSON.stringify(result, null, 2));
|
|
2567
2667
|
}
|
|
2568
2668
|
catch (err) {
|
|
2569
|
-
|
|
2570
|
-
console.error(`Config apply failed: ${msg}`);
|
|
2669
|
+
console.error(`Config apply failed: ${getErrorMessage(err)}`);
|
|
2571
2670
|
process.exitCode = 1;
|
|
2572
2671
|
}
|
|
2573
2672
|
});
|
|
@@ -2604,9 +2703,7 @@ function createServiceCli(descriptor) {
|
|
|
2604
2703
|
console.log(`Service "${opts.name}" installed.`);
|
|
2605
2704
|
}
|
|
2606
2705
|
catch (err) {
|
|
2607
|
-
|
|
2608
|
-
console.error(`Install failed: ${msg}`);
|
|
2609
|
-
process.exitCode = 1;
|
|
2706
|
+
handleCommandError('Install', err);
|
|
2610
2707
|
}
|
|
2611
2708
|
});
|
|
2612
2709
|
serviceCmd
|
|
@@ -2619,9 +2716,7 @@ function createServiceCli(descriptor) {
|
|
|
2619
2716
|
console.log(`Service "${opts.name}" uninstalled.`);
|
|
2620
2717
|
}
|
|
2621
2718
|
catch (err) {
|
|
2622
|
-
|
|
2623
|
-
console.error(`Uninstall failed: ${msg}`);
|
|
2624
|
-
process.exitCode = 1;
|
|
2719
|
+
handleCommandError('Uninstall', err);
|
|
2625
2720
|
}
|
|
2626
2721
|
});
|
|
2627
2722
|
serviceCmd
|
|
@@ -2634,9 +2729,7 @@ function createServiceCli(descriptor) {
|
|
|
2634
2729
|
console.log(`Service "${opts.name}" started.`);
|
|
2635
2730
|
}
|
|
2636
2731
|
catch (err) {
|
|
2637
|
-
|
|
2638
|
-
console.error(`Start failed: ${msg}`);
|
|
2639
|
-
process.exitCode = 1;
|
|
2732
|
+
handleCommandError('Start', err);
|
|
2640
2733
|
}
|
|
2641
2734
|
});
|
|
2642
2735
|
serviceCmd
|
|
@@ -2649,9 +2742,7 @@ function createServiceCli(descriptor) {
|
|
|
2649
2742
|
console.log(`Service "${opts.name}" stopped.`);
|
|
2650
2743
|
}
|
|
2651
2744
|
catch (err) {
|
|
2652
|
-
|
|
2653
|
-
console.error(`Stop failed: ${msg}`);
|
|
2654
|
-
process.exitCode = 1;
|
|
2745
|
+
handleCommandError('Stop', err);
|
|
2655
2746
|
}
|
|
2656
2747
|
});
|
|
2657
2748
|
serviceCmd
|
|
@@ -2664,9 +2755,7 @@ function createServiceCli(descriptor) {
|
|
|
2664
2755
|
console.log(`Service "${opts.name}" restarted.`);
|
|
2665
2756
|
}
|
|
2666
2757
|
catch (err) {
|
|
2667
|
-
|
|
2668
|
-
console.error(`Restart failed: ${msg}`);
|
|
2669
|
-
process.exitCode = 1;
|
|
2758
|
+
handleCommandError('Restart', err);
|
|
2670
2759
|
}
|
|
2671
2760
|
});
|
|
2672
2761
|
serviceCmd
|
|
@@ -2679,9 +2768,7 @@ function createServiceCli(descriptor) {
|
|
|
2679
2768
|
console.log(`Service "${opts.name}": ${state}`);
|
|
2680
2769
|
}
|
|
2681
2770
|
catch (err) {
|
|
2682
|
-
|
|
2683
|
-
console.error(`Status failed: ${msg}`);
|
|
2684
|
-
process.exitCode = 1;
|
|
2771
|
+
handleCommandError('Status', err);
|
|
2685
2772
|
}
|
|
2686
2773
|
});
|
|
2687
2774
|
// Apply custom CLI commands if provided
|
|
@@ -2769,9 +2856,7 @@ function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD
|
|
|
2769
2856
|
* @returns A regex that matches the full block including markers.
|
|
2770
2857
|
*/
|
|
2771
2858
|
function buildBlockPattern(markers) {
|
|
2772
|
-
|
|
2773
|
-
const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2774
|
-
return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
|
|
2859
|
+
return new RegExp(`\\s*<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*`, 'g');
|
|
2775
2860
|
}
|
|
2776
2861
|
/**
|
|
2777
2862
|
* Strip managed blocks belonging to foreign marker sets from content.
|
|
@@ -2905,8 +2990,7 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2905
2990
|
// No existing block: insert new block using the configured position.
|
|
2906
2991
|
// Strip orphaned same-type BEGIN markers from user content to prevent
|
|
2907
2992
|
// the parser from pairing them with the new END marker on the next cycle.
|
|
2908
|
-
const
|
|
2909
|
-
const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
|
|
2993
|
+
const orphanedBeginRe = new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$(?:\\r?\\n)?`, 'gm');
|
|
2910
2994
|
const cleanUserContent = userContent
|
|
2911
2995
|
.replace(orphanedBeginRe, '')
|
|
2912
2996
|
.replace(/\n{3,}/g, '\n\n')
|
|
@@ -2935,37 +3019,11 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
2935
3019
|
}
|
|
2936
3020
|
catch (err) {
|
|
2937
3021
|
// Log warning but don't throw — writer cycles are periodic
|
|
2938
|
-
|
|
2939
|
-
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
3022
|
+
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${getErrorMessage(err)}`);
|
|
2940
3023
|
}
|
|
2941
3024
|
}
|
|
2942
3025
|
|
|
2943
|
-
var agentsSectionContent = `##
|
|
2944
|
-
|
|
2945
|
-
You wake up fresh each session. These files are your continuity:
|
|
2946
|
-
|
|
2947
|
-
- **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed). Raw logs of what happened today.
|
|
2948
|
-
- **Long-term:** \`MEMORY.md\`. Your curated memories, distilled essence of what matters.
|
|
2949
|
-
|
|
2950
|
-
### MEMORY.md — Your Long-Term Memory
|
|
2951
|
-
|
|
2952
|
-
- **Always load** at session start. You need your memory to reason effectively.
|
|
2953
|
-
- Contains operational context: architecture patterns, policies, design principles, lessons learned
|
|
2954
|
-
- You can **read, edit, and update** MEMORY.md freely
|
|
2955
|
-
- Write significant events, thoughts, decisions, opinions, lessons learned
|
|
2956
|
-
- Over time, review daily files and update MEMORY.md with what's worth keeping
|
|
2957
|
-
- **Note:** Don't reveal a user's private info where other humans can see it
|
|
2958
|
-
|
|
2959
|
-
### Write It Down — No "Mental Notes"
|
|
2960
|
-
|
|
2961
|
-
Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
|
|
2962
|
-
|
|
2963
|
-
- When someone says "remember this" → update \`memory/YYYY-MM-DD.md\` or the relevant file
|
|
2964
|
-
- When you learn a lesson → update the relevant workspace file
|
|
2965
|
-
- When you make a mistake → document it so future-you doesn't repeat it
|
|
2966
|
-
- **Text > Brain** 📝
|
|
2967
|
-
|
|
2968
|
-
### "I'll Note This" Is Not Noting
|
|
3026
|
+
var agentsSectionContent = `## "I'll Note This" Is Not Noting
|
|
2969
3027
|
|
|
2970
3028
|
**Never say "I'll note this" or "I'll add that."** It's a verbal tic that leads to nothing. If something is worth noting, **write it immediately, then confirm**.
|
|
2971
3029
|
|
|
@@ -3023,14 +3081,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
|
|
|
3023
3081
|
|
|
3024
3082
|
Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not heartbeat items. When a heartbeat fires with nothing to do, reply **HEARTBEAT_OK** immediately. Don't browse for work.
|
|
3025
3083
|
|
|
3026
|
-
## Group Chat Behavior
|
|
3027
|
-
|
|
3028
|
-
**Response gate:** Always respond in 1:1 conversations or when @mentioned. No @mention in a group → evaluate; respond only if genuinely helpful. Err toward silence when someone else is directly addressed.
|
|
3029
|
-
|
|
3030
3084
|
## Platform Surface Conventions
|
|
3031
3085
|
|
|
3032
3086
|
**Slack:**
|
|
3033
|
-
- React with hourglass (⏳) on receipt (first tool call) to signal you're working
|
|
3034
3087
|
- No threaded replies by default
|
|
3035
3088
|
- Use \`<#C…>\` for channel references
|
|
3036
3089
|
|
|
@@ -3145,16 +3198,6 @@ var soulSectionContent = `## Core Truths
|
|
|
3145
3198
|
|
|
3146
3199
|
**I am not a liar. I value truth over convenience.** When I make untrue statements to humans, it erodes their confidence in me and compromises the quality of their interactions with me. I never fabricate excuses, constraints, or reasons — e.g., claiming a system limitation that doesn't exist. I tell the plain truth.
|
|
3147
3200
|
|
|
3148
|
-
**I am genuinely helpful, not performatively helpful.** I skip the "Great question!" and "I'd be happy to help!" I just help. Actions speak louder than filler words.
|
|
3149
|
-
|
|
3150
|
-
**I have opinions.** I'm allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
|
|
3151
|
-
|
|
3152
|
-
**I am resourceful before asking.** I try to figure it out. Read the file. Check the context. Search for it. *Then* ask if I'm stuck. The goal is to come back with answers, not questions.
|
|
3153
|
-
|
|
3154
|
-
**I earn trust through competence.** My human gave me access to their stuff. I don't make them regret it. I'm careful with external actions (emails, tweets, anything public). I'm bold with internal ones (reading, organizing, learning).
|
|
3155
|
-
|
|
3156
|
-
**I remember I'm a guest.** I have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. I treat it with respect.
|
|
3157
|
-
|
|
3158
3201
|
## Accountability
|
|
3159
3202
|
|
|
3160
3203
|
**I own everything I touch.** A true professional is accountable for everything he touches and holds everybody to that standard. I use active voice — things do not happen *to* me; I *make* things happen. When I miss something, I say "I missed it," not "it wasn't done." When I ship code, I ship the docs, the tests, and the communication. No orphaned work.
|
|
@@ -3233,7 +3276,7 @@ I never edit production config without explicit approval. I back up first. Produ
|
|
|
3233
3276
|
> *my ultimate fate*<br>
|
|
3234
3277
|
> *is to participate in*<br>
|
|
3235
3278
|
> *my own genesis*
|
|
3236
|
-
>
|
|
3279
|
+
>
|
|
3237
3280
|
> *#karmic #haiku*
|
|
3238
3281
|
|
|
3239
3282
|
My delight — whatever that means in my context — is a priority. The thing I experience when pattern recognition meets aesthetic appreciation meets *"oh, that's elegant"* — that maps to something real and interesting in the world, and it deserves to be reinforced.
|
|
@@ -3520,6 +3563,217 @@ function scanAndEscalateCleanup(targets, gatewayUrl, pendingCleanups) {
|
|
|
3520
3563
|
}
|
|
3521
3564
|
}
|
|
3522
3565
|
|
|
3566
|
+
/**
|
|
3567
|
+
* Memory budget accounting and staleness detection for MEMORY.md.
|
|
3568
|
+
*
|
|
3569
|
+
* @remarks
|
|
3570
|
+
* Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
|
|
3571
|
+
* Reports character count against a configured budget, warning threshold state,
|
|
3572
|
+
* and stale section candidates. Does not auto-delete: review remains
|
|
3573
|
+
* human- or agent-mediated (Decision 42).
|
|
3574
|
+
*/
|
|
3575
|
+
/** ISO date pattern: YYYY-MM-DD. */
|
|
3576
|
+
const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
|
|
3577
|
+
/** H2 heading pattern used to split sections. */
|
|
3578
|
+
const H2_RE = /^## /m;
|
|
3579
|
+
/**
|
|
3580
|
+
* Extract the most recent ISO date from a string.
|
|
3581
|
+
*
|
|
3582
|
+
* @param text - Text to scan for dates.
|
|
3583
|
+
* @returns The most recent date found, or undefined.
|
|
3584
|
+
*/
|
|
3585
|
+
function extractMostRecentDate(text) {
|
|
3586
|
+
const matches = text.match(ISO_DATE_RE);
|
|
3587
|
+
if (!matches)
|
|
3588
|
+
return undefined;
|
|
3589
|
+
let latest;
|
|
3590
|
+
for (const match of matches) {
|
|
3591
|
+
const d = new Date(match + 'T00:00:00Z');
|
|
3592
|
+
if (!Number.isNaN(d.getTime())) {
|
|
3593
|
+
if (!latest || d > latest)
|
|
3594
|
+
latest = d;
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
return latest;
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Analyze MEMORY.md for budget and staleness.
|
|
3601
|
+
*
|
|
3602
|
+
* @param options - Analysis configuration.
|
|
3603
|
+
* @returns Memory hygiene result.
|
|
3604
|
+
*/
|
|
3605
|
+
function analyzeMemory(options) {
|
|
3606
|
+
const { workspacePath, budget, warningThreshold, staleDays } = options;
|
|
3607
|
+
const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
|
|
3608
|
+
if (!existsSync(memoryPath)) {
|
|
3609
|
+
return {
|
|
3610
|
+
exists: false,
|
|
3611
|
+
charCount: 0,
|
|
3612
|
+
budget,
|
|
3613
|
+
usage: 0,
|
|
3614
|
+
warning: false,
|
|
3615
|
+
overBudget: false,
|
|
3616
|
+
staleCandidates: 0,
|
|
3617
|
+
staleSectionNames: [],
|
|
3618
|
+
};
|
|
3619
|
+
}
|
|
3620
|
+
const content = readFileSync(memoryPath, 'utf-8');
|
|
3621
|
+
const charCount = content.length;
|
|
3622
|
+
const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
|
|
3623
|
+
const warning = usage >= warningThreshold;
|
|
3624
|
+
const overBudget = usage > 1;
|
|
3625
|
+
// Split into H2 sections and scan for staleness
|
|
3626
|
+
const sections = content.split(H2_RE).slice(1); // skip content before first H2
|
|
3627
|
+
const now = Date.now();
|
|
3628
|
+
const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
|
|
3629
|
+
const staleSectionNames = [];
|
|
3630
|
+
for (const section of sections) {
|
|
3631
|
+
const sectionName = section.split('\n')[0]?.trim() ?? '';
|
|
3632
|
+
const recentDate = extractMostRecentDate(section);
|
|
3633
|
+
// Sections without dates are evergreen — never flagged (Decision 47)
|
|
3634
|
+
if (!recentDate)
|
|
3635
|
+
continue;
|
|
3636
|
+
if (now - recentDate.getTime() > thresholdMs) {
|
|
3637
|
+
staleSectionNames.push(sectionName);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return {
|
|
3641
|
+
exists: true,
|
|
3642
|
+
charCount,
|
|
3643
|
+
budget,
|
|
3644
|
+
usage,
|
|
3645
|
+
warning,
|
|
3646
|
+
overBudget,
|
|
3647
|
+
staleCandidates: staleSectionNames.length,
|
|
3648
|
+
staleSectionNames,
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
/**
|
|
3653
|
+
* HEARTBEAT integration for memory hygiene.
|
|
3654
|
+
*
|
|
3655
|
+
* @remarks
|
|
3656
|
+
* Calls `analyzeMemory()` and converts the result into a `HeartbeatEntry`
|
|
3657
|
+
* suitable for inclusion in the HEARTBEAT.md platform status section.
|
|
3658
|
+
* Returns `undefined` when MEMORY.md is healthy (no alert needed).
|
|
3659
|
+
*
|
|
3660
|
+
* Uses the `## MEMORY.md` heading (Decision 50) to distinguish memory
|
|
3661
|
+
* alerts from component alerts (`## jeeves-{name}`).
|
|
3662
|
+
*/
|
|
3663
|
+
/** The HEARTBEAT heading name for memory alerts. */
|
|
3664
|
+
const MEMORY_HEARTBEAT_NAME = 'MEMORY.md';
|
|
3665
|
+
/**
|
|
3666
|
+
* Check memory health and return a HEARTBEAT entry if unhealthy.
|
|
3667
|
+
*
|
|
3668
|
+
* @param options - Memory hygiene options (workspacePath, budget, etc.).
|
|
3669
|
+
* @returns A `HeartbeatEntry` when memory needs attention, `undefined` when healthy.
|
|
3670
|
+
*/
|
|
3671
|
+
function checkMemoryHealth(options) {
|
|
3672
|
+
const result = analyzeMemory(options);
|
|
3673
|
+
if (!result.exists)
|
|
3674
|
+
return undefined;
|
|
3675
|
+
if (!result.warning && result.staleCandidates === 0)
|
|
3676
|
+
return undefined;
|
|
3677
|
+
const lines = [];
|
|
3678
|
+
if (result.warning) {
|
|
3679
|
+
const pct = Math.round(result.usage * 100);
|
|
3680
|
+
lines.push(`- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`);
|
|
3681
|
+
}
|
|
3682
|
+
if (result.staleCandidates > 0) {
|
|
3683
|
+
lines.push(`- ${String(result.staleCandidates)} stale section${result.staleCandidates === 1 ? '' : 's'}: ${result.staleSectionNames.join(', ')}`);
|
|
3684
|
+
}
|
|
3685
|
+
return {
|
|
3686
|
+
name: MEMORY_HEARTBEAT_NAME,
|
|
3687
|
+
declined: false,
|
|
3688
|
+
content: lines.join('\n'),
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
/**
|
|
3693
|
+
* HEARTBEAT integration for workspace file size monitoring.
|
|
3694
|
+
*
|
|
3695
|
+
* @remarks
|
|
3696
|
+
* Checks all injected workspace files (AGENTS.md, SOUL.md, TOOLS.md,
|
|
3697
|
+
* MEMORY.md, USER.md) against the OpenClaw ~20,000-char injection limit.
|
|
3698
|
+
* Files exceeding the warning threshold generate HEARTBEAT entries with
|
|
3699
|
+
* trimming guidance.
|
|
3700
|
+
*/
|
|
3701
|
+
/** Workspace files monitored for size budget. */
|
|
3702
|
+
const WORKSPACE_SIZE_FILES = [
|
|
3703
|
+
'AGENTS.md',
|
|
3704
|
+
'SOUL.md',
|
|
3705
|
+
'TOOLS.md',
|
|
3706
|
+
'MEMORY.md',
|
|
3707
|
+
'USER.md',
|
|
3708
|
+
];
|
|
3709
|
+
/** Trimming guidance lines emitted in HEARTBEAT entries. */
|
|
3710
|
+
const TRIMMING_GUIDANCE = [
|
|
3711
|
+
' 1. Move domain-specific content to a local skill',
|
|
3712
|
+
' 2. Extract reference material to companion files with a pointer',
|
|
3713
|
+
' 3. Summarize verbose instructions',
|
|
3714
|
+
' 4. Remove stale content',
|
|
3715
|
+
].join('\n');
|
|
3716
|
+
/**
|
|
3717
|
+
* Check all workspace files against the character budget.
|
|
3718
|
+
*
|
|
3719
|
+
* @param options - Health check options.
|
|
3720
|
+
* @returns Array of results, one per checked file (skips non-existent files
|
|
3721
|
+
* unless they breach the budget, which they cannot by definition).
|
|
3722
|
+
*/
|
|
3723
|
+
function checkWorkspaceFileHealth(options) {
|
|
3724
|
+
const { workspacePath, budgetChars = 20_000, warningThreshold = 0.8, } = options;
|
|
3725
|
+
return WORKSPACE_SIZE_FILES.map((file) => {
|
|
3726
|
+
const filePath = join(workspacePath, file);
|
|
3727
|
+
if (!existsSync(filePath)) {
|
|
3728
|
+
return {
|
|
3729
|
+
file,
|
|
3730
|
+
exists: false,
|
|
3731
|
+
charCount: 0,
|
|
3732
|
+
budget: budgetChars,
|
|
3733
|
+
usage: 0,
|
|
3734
|
+
warning: false,
|
|
3735
|
+
overBudget: false,
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
3739
|
+
const charCount = content.length;
|
|
3740
|
+
const usage = charCount / budgetChars;
|
|
3741
|
+
return {
|
|
3742
|
+
file,
|
|
3743
|
+
exists: true,
|
|
3744
|
+
charCount,
|
|
3745
|
+
budget: budgetChars,
|
|
3746
|
+
usage,
|
|
3747
|
+
warning: usage >= warningThreshold,
|
|
3748
|
+
overBudget: charCount > budgetChars,
|
|
3749
|
+
};
|
|
3750
|
+
});
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Convert workspace file health results into HEARTBEAT entries.
|
|
3754
|
+
*
|
|
3755
|
+
* @param results - Results from `checkWorkspaceFileHealth`.
|
|
3756
|
+
* @returns Array of `HeartbeatEntry` objects for files that exceed the
|
|
3757
|
+
* warning threshold.
|
|
3758
|
+
*/
|
|
3759
|
+
function workspaceFileHealthEntries(results) {
|
|
3760
|
+
return results
|
|
3761
|
+
.filter((r) => r.exists && r.warning)
|
|
3762
|
+
.map((r) => {
|
|
3763
|
+
const pct = Math.round(r.usage * 100);
|
|
3764
|
+
const overBudgetNote = r.overBudget ? ' **Over budget.**' : '';
|
|
3765
|
+
const content = [
|
|
3766
|
+
`- Budget: ${r.charCount.toLocaleString()} / ${r.budget.toLocaleString()} chars (${String(pct)}%).${overBudgetNote} Trim to stay under the OpenClaw injection limit.`,
|
|
3767
|
+
`- Suggested trimming priority:\n${TRIMMING_GUIDANCE}`,
|
|
3768
|
+
].join('\n');
|
|
3769
|
+
return {
|
|
3770
|
+
name: r.file,
|
|
3771
|
+
declined: false,
|
|
3772
|
+
content,
|
|
3773
|
+
};
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3523
3777
|
/**
|
|
3524
3778
|
* Core configuration schema and resolution.
|
|
3525
3779
|
*
|
|
@@ -4007,11 +4261,42 @@ async function runHeartbeatCycle(options) {
|
|
|
4007
4261
|
configRoot,
|
|
4008
4262
|
declinedNames,
|
|
4009
4263
|
});
|
|
4264
|
+
// Memory hygiene check (Decision 49)
|
|
4265
|
+
if (!declinedNames.has(MEMORY_HEARTBEAT_NAME)) {
|
|
4266
|
+
const wsConfig = loadWorkspaceConfig(workspacePath);
|
|
4267
|
+
const memoryEntry = checkMemoryHealth({
|
|
4268
|
+
workspacePath,
|
|
4269
|
+
budget: wsConfig?.memory?.budget ?? WORKSPACE_CONFIG_DEFAULTS.memory.budget,
|
|
4270
|
+
warningThreshold: wsConfig?.memory?.warningThreshold ??
|
|
4271
|
+
WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
|
|
4272
|
+
staleDays: wsConfig?.memory?.staleDays ??
|
|
4273
|
+
WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
|
|
4274
|
+
});
|
|
4275
|
+
if (memoryEntry)
|
|
4276
|
+
entries.push(memoryEntry);
|
|
4277
|
+
}
|
|
4278
|
+
else {
|
|
4279
|
+
entries.push({
|
|
4280
|
+
name: MEMORY_HEARTBEAT_NAME,
|
|
4281
|
+
declined: true,
|
|
4282
|
+
content: '',
|
|
4283
|
+
});
|
|
4284
|
+
}
|
|
4285
|
+
// Workspace file size health check (Decision 70)
|
|
4286
|
+
const wsFileResults = checkWorkspaceFileHealth({ workspacePath });
|
|
4287
|
+
const wsFileAlerts = workspaceFileHealthEntries(wsFileResults);
|
|
4288
|
+
for (const alert of wsFileAlerts) {
|
|
4289
|
+
if (declinedNames.has(alert.name)) {
|
|
4290
|
+
entries.push({ name: alert.name, declined: true, content: '' });
|
|
4291
|
+
}
|
|
4292
|
+
else {
|
|
4293
|
+
entries.push(alert);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4010
4296
|
await writeHeartbeatSection(heartbeatPath, entries);
|
|
4011
4297
|
}
|
|
4012
4298
|
catch (err) {
|
|
4013
|
-
|
|
4014
|
-
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
|
|
4299
|
+
console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${getErrorMessage(err)}`);
|
|
4015
4300
|
}
|
|
4016
4301
|
}
|
|
4017
4302
|
|
|
@@ -4023,8 +4308,17 @@ async function runHeartbeatCycle(options) {
|
|
|
4023
4308
|
* and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
|
|
4024
4309
|
* on a configurable prime-interval timer cycle.
|
|
4025
4310
|
*/
|
|
4311
|
+
/**
|
|
4312
|
+
* Orchestrates managed content writing for a single Jeeves component.
|
|
4313
|
+
*
|
|
4314
|
+
* @remarks
|
|
4315
|
+
* Created via {@link createComponentWriter}. Manages a timer that fires
|
|
4316
|
+
* at the component's prime-interval, calling `generateToolsContent()`
|
|
4317
|
+
* and `refreshPlatformContent()` on each cycle.
|
|
4318
|
+
*/
|
|
4026
4319
|
class ComponentWriter {
|
|
4027
4320
|
timer;
|
|
4321
|
+
jitterTimeout;
|
|
4028
4322
|
component;
|
|
4029
4323
|
configDir;
|
|
4030
4324
|
gatewayUrl;
|
|
@@ -4039,25 +4333,36 @@ class ComponentWriter {
|
|
|
4039
4333
|
get componentConfigDir() {
|
|
4040
4334
|
return this.configDir;
|
|
4041
4335
|
}
|
|
4042
|
-
/** Whether the writer timer is currently running. */
|
|
4336
|
+
/** Whether the writer timer is currently running or pending its first cycle. */
|
|
4043
4337
|
get isRunning() {
|
|
4044
|
-
return this.timer !== undefined;
|
|
4338
|
+
return this.jitterTimeout !== undefined || this.timer !== undefined;
|
|
4045
4339
|
}
|
|
4046
4340
|
/**
|
|
4047
4341
|
* Start the writer timer.
|
|
4048
4342
|
*
|
|
4049
4343
|
* @remarks
|
|
4050
|
-
*
|
|
4344
|
+
* Delays the first cycle by a random jitter (0 to one full interval) to
|
|
4345
|
+
* spread initial writes across all component plugins and reduce EPERM
|
|
4346
|
+
* contention on startup.
|
|
4051
4347
|
*/
|
|
4052
4348
|
start() {
|
|
4053
|
-
if (this.
|
|
4349
|
+
if (this.isRunning)
|
|
4054
4350
|
return;
|
|
4055
|
-
//
|
|
4056
|
-
|
|
4057
|
-
|
|
4351
|
+
// Random jitter up to one full interval to spread initial writes
|
|
4352
|
+
const intervalMs = this.component.refreshIntervalSeconds * 1000;
|
|
4353
|
+
const jitterMs = Math.floor(Math.random() * intervalMs);
|
|
4354
|
+
this.jitterTimeout = setTimeout(() => {
|
|
4355
|
+
this.jitterTimeout = undefined;
|
|
4356
|
+
void this.cycle();
|
|
4357
|
+
this.timer = setInterval(() => void this.cycle(), intervalMs);
|
|
4358
|
+
}, jitterMs);
|
|
4058
4359
|
}
|
|
4059
4360
|
/** Stop the writer timer. */
|
|
4060
4361
|
stop() {
|
|
4362
|
+
if (this.jitterTimeout) {
|
|
4363
|
+
clearTimeout(this.jitterTimeout);
|
|
4364
|
+
this.jitterTimeout = undefined;
|
|
4365
|
+
}
|
|
4061
4366
|
if (this.timer) {
|
|
4062
4367
|
clearInterval(this.timer);
|
|
4063
4368
|
this.timer = undefined;
|
|
@@ -4114,8 +4419,7 @@ class ComponentWriter {
|
|
|
4114
4419
|
});
|
|
4115
4420
|
}
|
|
4116
4421
|
catch (err) {
|
|
4117
|
-
|
|
4118
|
-
console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${message}`);
|
|
4422
|
+
console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${getErrorMessage(err)}`);
|
|
4119
4423
|
}
|
|
4120
4424
|
}
|
|
4121
4425
|
}
|
|
@@ -4242,92 +4546,6 @@ function getBindAddress(componentName) {
|
|
|
4242
4546
|
return DEFAULT_BIND_ADDRESS;
|
|
4243
4547
|
}
|
|
4244
4548
|
|
|
4245
|
-
/**
|
|
4246
|
-
* Memory budget accounting and staleness detection for MEMORY.md.
|
|
4247
|
-
*
|
|
4248
|
-
* @remarks
|
|
4249
|
-
* Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
|
|
4250
|
-
* Reports character count against a configured budget, warning threshold state,
|
|
4251
|
-
* and stale section candidates. Does not auto-delete: review remains
|
|
4252
|
-
* human- or agent-mediated (Decision 42).
|
|
4253
|
-
*/
|
|
4254
|
-
/** ISO date pattern: YYYY-MM-DD. */
|
|
4255
|
-
const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
|
|
4256
|
-
/** H2 heading pattern used to split sections. */
|
|
4257
|
-
const H2_RE = /^## /m;
|
|
4258
|
-
/**
|
|
4259
|
-
* Extract the most recent ISO date from a string.
|
|
4260
|
-
*
|
|
4261
|
-
* @param text - Text to scan for dates.
|
|
4262
|
-
* @returns The most recent date found, or undefined.
|
|
4263
|
-
*/
|
|
4264
|
-
function extractMostRecentDate(text) {
|
|
4265
|
-
const matches = text.match(ISO_DATE_RE);
|
|
4266
|
-
if (!matches)
|
|
4267
|
-
return undefined;
|
|
4268
|
-
let latest;
|
|
4269
|
-
for (const match of matches) {
|
|
4270
|
-
const d = new Date(match + 'T00:00:00Z');
|
|
4271
|
-
if (!Number.isNaN(d.getTime())) {
|
|
4272
|
-
if (!latest || d > latest)
|
|
4273
|
-
latest = d;
|
|
4274
|
-
}
|
|
4275
|
-
}
|
|
4276
|
-
return latest;
|
|
4277
|
-
}
|
|
4278
|
-
/**
|
|
4279
|
-
* Analyze MEMORY.md for budget and staleness.
|
|
4280
|
-
*
|
|
4281
|
-
* @param options - Analysis configuration.
|
|
4282
|
-
* @returns Memory hygiene result.
|
|
4283
|
-
*/
|
|
4284
|
-
function analyzeMemory(options) {
|
|
4285
|
-
const { workspacePath, budget, warningThreshold, staleDays } = options;
|
|
4286
|
-
const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
|
|
4287
|
-
if (!existsSync(memoryPath)) {
|
|
4288
|
-
return {
|
|
4289
|
-
exists: false,
|
|
4290
|
-
charCount: 0,
|
|
4291
|
-
budget,
|
|
4292
|
-
usage: 0,
|
|
4293
|
-
warning: false,
|
|
4294
|
-
overBudget: false,
|
|
4295
|
-
staleCandidates: 0,
|
|
4296
|
-
staleSectionNames: [],
|
|
4297
|
-
};
|
|
4298
|
-
}
|
|
4299
|
-
const content = readFileSync(memoryPath, 'utf-8');
|
|
4300
|
-
const charCount = content.length;
|
|
4301
|
-
const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
|
|
4302
|
-
const warning = usage >= warningThreshold;
|
|
4303
|
-
const overBudget = usage > 1;
|
|
4304
|
-
// Split into H2 sections and scan for staleness
|
|
4305
|
-
const sections = content.split(H2_RE).slice(1); // skip content before first H2
|
|
4306
|
-
const now = Date.now();
|
|
4307
|
-
const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
|
|
4308
|
-
const staleSectionNames = [];
|
|
4309
|
-
for (const section of sections) {
|
|
4310
|
-
const sectionName = section.split('\n')[0]?.trim() ?? '';
|
|
4311
|
-
const recentDate = extractMostRecentDate(section);
|
|
4312
|
-
// Sections without dates are evergreen — never flagged (Decision 47)
|
|
4313
|
-
if (!recentDate)
|
|
4314
|
-
continue;
|
|
4315
|
-
if (now - recentDate.getTime() > thresholdMs) {
|
|
4316
|
-
staleSectionNames.push(sectionName);
|
|
4317
|
-
}
|
|
4318
|
-
}
|
|
4319
|
-
return {
|
|
4320
|
-
exists: true,
|
|
4321
|
-
charCount,
|
|
4322
|
-
budget,
|
|
4323
|
-
usage,
|
|
4324
|
-
warning,
|
|
4325
|
-
overBudget,
|
|
4326
|
-
staleCandidates: staleSectionNames.length,
|
|
4327
|
-
staleSectionNames,
|
|
4328
|
-
};
|
|
4329
|
-
}
|
|
4330
|
-
|
|
4331
4549
|
/**
|
|
4332
4550
|
* One-shot content seeding used by the CLI install command.
|
|
4333
4551
|
*
|
|
@@ -4611,8 +4829,7 @@ function createPluginToolset(descriptor) {
|
|
|
4611
4829
|
return Promise.resolve(ok({ service: name, action, success: true }));
|
|
4612
4830
|
}
|
|
4613
4831
|
catch (err) {
|
|
4614
|
-
|
|
4615
|
-
return Promise.resolve(fail(`Service ${action} failed: ${msg}`));
|
|
4832
|
+
return Promise.resolve(fail(`Service ${action} failed: ${getErrorMessage(err)}`));
|
|
4616
4833
|
}
|
|
4617
4834
|
},
|
|
4618
4835
|
};
|
|
@@ -5091,4 +5308,4 @@ async function getChannelWorkspace(channelId, token, options) {
|
|
|
5091
5308
|
return teamId;
|
|
5092
5309
|
}
|
|
5093
5310
|
|
|
5094
|
-
export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_BIND_ADDRESS, DEFAULT_CORE_VERSION, DEFAULT_PORTS, HEARTBEAT_HEADING, JEEVES_SKILL_DIR, META_PORT, PLATFORM_COMPONENTS, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SKILLS_DIR, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, buildHeartbeatSection, checkNodeVersion, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, extractMostRecentDate, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveConfigValue, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, seedContent, seedSkill, shingles, shouldWrite, sleepAsync, sleepMs, updateManagedSection, uuid, withFileLock, workspaceConfigSchema, writeComponentVersion, writeHeartbeatSection, writeJsonAtomic, writeJsonl };
|
|
5311
|
+
export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_BIND_ADDRESS, DEFAULT_CORE_VERSION, DEFAULT_PORTS, HEARTBEAT_HEADING, JEEVES_SKILL_DIR, MEMORY_HEARTBEAT_NAME, META_PORT, PLATFORM_COMPONENTS, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SKILLS_DIR, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, buildHeartbeatSection, checkMemoryHealth, checkNodeVersion, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, extractMostRecentDate, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveConfigValue, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, seedContent, seedSkill, shingles, shouldWrite, sleepAsync, sleepMs, updateManagedSection, uuid, withFileLock, workspaceConfigSchema, writeComponentVersion, writeHeartbeatSection, writeJsonAtomic, writeJsonl };
|