@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/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, resolve, basename } from 'node:path';
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.4.7` placeholder is replaced by
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.4.7';
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 tempPath = join(dir, `.${String(Date.now())}.tmp`);
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
- try {
302
- renameSync(tempPath, filePath);
303
- }
304
- catch (err) {
310
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
305
311
  try {
306
- unlinkSync(tempPath);
312
+ renameSync(tempPath, filePath);
313
+ return;
307
314
  }
308
- catch {
309
- /* best-effort cleanup */
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
- const msg = err instanceof Error ? err.message : String(err);
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: ${message}` },
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: ${message}`,
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
- const message = err instanceof Error ? err.message : String(err);
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
- /** Built-in workspace config defaults. */
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
- const msg = err instanceof Error ? err.message : String(err);
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+?)(?:: declined)?$/gm;
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
- const message = err instanceof Error ? err.message : String(err);
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*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
1140
- endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
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}` and `tools.alsoAllow`.
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 messages = patchConfig(config, pluginId, 'add');
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
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 escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
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
- const message = err instanceof Error ? err.message : String(err);
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 = `## Memory Architecture
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
- const msg = err instanceof Error ? err.message : String(err);
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
- * Performs an immediate first write, then sets up the interval.
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.timer)
4349
+ if (this.isRunning)
4054
4350
  return;
4055
- // Fire immediately, then on interval
4056
- void this.cycle();
4057
- this.timer = setInterval(() => void this.cycle(), this.component.refreshIntervalSeconds * 1000);
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
- const message = err instanceof Error ? err.message : String(err);
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
- const msg = err instanceof Error ? err.message : String(err);
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 };