@karmaniverous/jeeves 0.5.1 → 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.5.0` 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.5.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 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+?|MEMORY\.md)(?:: 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
  /**
@@ -1483,6 +1524,23 @@ Review is human/agent-mediated — core does not auto-delete.
1483
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.
1484
1525
 
1485
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\`).
1486
1544
  `;
1487
1545
 
1488
1546
  /**
@@ -1583,16 +1641,18 @@ function patchAllowList(parent, key, label, pluginId, mode) {
1583
1641
  * Patch an OpenClaw config for plugin install or uninstall.
1584
1642
  *
1585
1643
  * @remarks
1586
- * Manages `plugins.entries.{pluginId}` and `tools.alsoAllow`.
1644
+ * Manages `plugins.entries.{pluginId}`, `plugins.installs.{pluginId}`,
1645
+ * and `tools.alsoAllow`.
1587
1646
  * Idempotent: adding twice produces no duplicates; removing when absent
1588
1647
  * produces no errors.
1589
1648
  *
1590
1649
  * @param config - The parsed OpenClaw config object (mutated in place).
1591
1650
  * @param pluginId - The plugin identifier.
1592
1651
  * @param mode - Whether to add or remove the plugin.
1652
+ * @param installRecord - Install provenance record (required when mode is 'add').
1593
1653
  * @returns Array of log messages describing changes made.
1594
1654
  */
1595
- function patchConfig(config, pluginId, mode) {
1655
+ function patchConfig(config, pluginId, mode, installRecord) {
1596
1656
  const messages = [];
1597
1657
  // Ensure plugins section
1598
1658
  if (!config.plugins || typeof config.plugins !== 'object') {
@@ -1614,6 +1674,24 @@ function patchConfig(config, pluginId, mode) {
1614
1674
  Reflect.deleteProperty(entries, pluginId);
1615
1675
  messages.push(`Removed "${pluginId}" from plugins.entries`);
1616
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
+ }
1617
1695
  // tools.alsoAllow
1618
1696
  if (!config.tools || typeof config.tools !== 'object') {
1619
1697
  config.tools = {};
@@ -1722,7 +1800,22 @@ function createPluginCli(options) {
1722
1800
  // 2. Patch openclaw.json
1723
1801
  console.log('Patching OpenClaw config...');
1724
1802
  const config = readJsonFile(configPath);
1725
- 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
+ });
1726
1819
  // 3. Memory slot claim
1727
1820
  if (opts.memory) {
1728
1821
  if (!config.agents || typeof config.agents !== 'object') {
@@ -2446,6 +2539,10 @@ function createServiceManager(descriptor) {
2446
2539
  * a component descriptor. Components add domain-specific commands
2447
2540
  * via `descriptor.customCliCommands`.
2448
2541
  */
2542
+ function handleCommandError(action, err) {
2543
+ console.error(`${action} failed: ${getErrorMessage(err)}`);
2544
+ process.exitCode = 1;
2545
+ }
2449
2546
  /**
2450
2547
  * Create a standard service CLI program from a component descriptor.
2451
2548
  *
@@ -2498,8 +2595,7 @@ function createServiceCli(descriptor) {
2498
2595
  console.log(JSON.stringify(result, null, 2));
2499
2596
  }
2500
2597
  catch (err) {
2501
- const msg = err instanceof Error ? err.message : String(err);
2502
- console.error(`Service unreachable: ${msg}`);
2598
+ console.error(`Service unreachable: ${getErrorMessage(err)}`);
2503
2599
  process.exitCode = 1;
2504
2600
  }
2505
2601
  });
@@ -2517,8 +2613,7 @@ function createServiceCli(descriptor) {
2517
2613
  console.log(JSON.stringify(result, null, 2));
2518
2614
  }
2519
2615
  catch (err) {
2520
- const msg = err instanceof Error ? err.message : String(err);
2521
- console.error(`Config query failed: ${msg}`);
2616
+ console.error(`Config query failed: ${getErrorMessage(err)}`);
2522
2617
  process.exitCode = 1;
2523
2618
  }
2524
2619
  });
@@ -2534,8 +2629,7 @@ function createServiceCli(descriptor) {
2534
2629
  console.log('Config is valid.');
2535
2630
  }
2536
2631
  catch (err) {
2537
- const msg = err instanceof Error ? err.message : String(err);
2538
- console.error(`Validation failed: ${msg}`);
2632
+ console.error(`Validation failed: ${getErrorMessage(err)}`);
2539
2633
  process.exitCode = 1;
2540
2634
  }
2541
2635
  });
@@ -2572,8 +2666,7 @@ function createServiceCli(descriptor) {
2572
2666
  console.log(JSON.stringify(result, null, 2));
2573
2667
  }
2574
2668
  catch (err) {
2575
- const msg = err instanceof Error ? err.message : String(err);
2576
- console.error(`Config apply failed: ${msg}`);
2669
+ console.error(`Config apply failed: ${getErrorMessage(err)}`);
2577
2670
  process.exitCode = 1;
2578
2671
  }
2579
2672
  });
@@ -2610,9 +2703,7 @@ function createServiceCli(descriptor) {
2610
2703
  console.log(`Service "${opts.name}" installed.`);
2611
2704
  }
2612
2705
  catch (err) {
2613
- const msg = err instanceof Error ? err.message : String(err);
2614
- console.error(`Install failed: ${msg}`);
2615
- process.exitCode = 1;
2706
+ handleCommandError('Install', err);
2616
2707
  }
2617
2708
  });
2618
2709
  serviceCmd
@@ -2625,9 +2716,7 @@ function createServiceCli(descriptor) {
2625
2716
  console.log(`Service "${opts.name}" uninstalled.`);
2626
2717
  }
2627
2718
  catch (err) {
2628
- const msg = err instanceof Error ? err.message : String(err);
2629
- console.error(`Uninstall failed: ${msg}`);
2630
- process.exitCode = 1;
2719
+ handleCommandError('Uninstall', err);
2631
2720
  }
2632
2721
  });
2633
2722
  serviceCmd
@@ -2640,9 +2729,7 @@ function createServiceCli(descriptor) {
2640
2729
  console.log(`Service "${opts.name}" started.`);
2641
2730
  }
2642
2731
  catch (err) {
2643
- const msg = err instanceof Error ? err.message : String(err);
2644
- console.error(`Start failed: ${msg}`);
2645
- process.exitCode = 1;
2732
+ handleCommandError('Start', err);
2646
2733
  }
2647
2734
  });
2648
2735
  serviceCmd
@@ -2655,9 +2742,7 @@ function createServiceCli(descriptor) {
2655
2742
  console.log(`Service "${opts.name}" stopped.`);
2656
2743
  }
2657
2744
  catch (err) {
2658
- const msg = err instanceof Error ? err.message : String(err);
2659
- console.error(`Stop failed: ${msg}`);
2660
- process.exitCode = 1;
2745
+ handleCommandError('Stop', err);
2661
2746
  }
2662
2747
  });
2663
2748
  serviceCmd
@@ -2670,9 +2755,7 @@ function createServiceCli(descriptor) {
2670
2755
  console.log(`Service "${opts.name}" restarted.`);
2671
2756
  }
2672
2757
  catch (err) {
2673
- const msg = err instanceof Error ? err.message : String(err);
2674
- console.error(`Restart failed: ${msg}`);
2675
- process.exitCode = 1;
2758
+ handleCommandError('Restart', err);
2676
2759
  }
2677
2760
  });
2678
2761
  serviceCmd
@@ -2685,9 +2768,7 @@ function createServiceCli(descriptor) {
2685
2768
  console.log(`Service "${opts.name}": ${state}`);
2686
2769
  }
2687
2770
  catch (err) {
2688
- const msg = err instanceof Error ? err.message : String(err);
2689
- console.error(`Status failed: ${msg}`);
2690
- process.exitCode = 1;
2771
+ handleCommandError('Status', err);
2691
2772
  }
2692
2773
  });
2693
2774
  // Apply custom CLI commands if provided
@@ -2775,9 +2856,7 @@ function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD
2775
2856
  * @returns A regex that matches the full block including markers.
2776
2857
  */
2777
2858
  function buildBlockPattern(markers) {
2778
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2779
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2780
- 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');
2781
2860
  }
2782
2861
  /**
2783
2862
  * Strip managed blocks belonging to foreign marker sets from content.
@@ -2911,8 +2990,7 @@ async function updateManagedSection(filePath, content, options = {}) {
2911
2990
  // No existing block: insert new block using the configured position.
2912
2991
  // Strip orphaned same-type BEGIN markers from user content to prevent
2913
2992
  // the parser from pairing them with the new END marker on the next cycle.
2914
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2915
- 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');
2916
2994
  const cleanUserContent = userContent
2917
2995
  .replace(orphanedBeginRe, '')
2918
2996
  .replace(/\n{3,}/g, '\n\n')
@@ -2941,37 +3019,11 @@ async function updateManagedSection(filePath, content, options = {}) {
2941
3019
  }
2942
3020
  catch (err) {
2943
3021
  // Log warning but don't throw — writer cycles are periodic
2944
- const message = err instanceof Error ? err.message : String(err);
2945
- console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
3022
+ console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${getErrorMessage(err)}`);
2946
3023
  }
2947
3024
  }
2948
3025
 
2949
- var agentsSectionContent = `## Memory Architecture
2950
-
2951
- You wake up fresh each session. These files are your continuity:
2952
-
2953
- - **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed). Raw logs of what happened today.
2954
- - **Long-term:** \`MEMORY.md\`. Your curated memories, distilled essence of what matters.
2955
-
2956
- ### MEMORY.md — Your Long-Term Memory
2957
-
2958
- - **Always load** at session start. You need your memory to reason effectively.
2959
- - Contains operational context: architecture patterns, policies, design principles, lessons learned
2960
- - You can **read, edit, and update** MEMORY.md freely
2961
- - Write significant events, thoughts, decisions, opinions, lessons learned
2962
- - Over time, review daily files and update MEMORY.md with what's worth keeping
2963
- - **Note:** Don't reveal a user's private info where other humans can see it
2964
-
2965
- ### Write It Down — No "Mental Notes"
2966
-
2967
- Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
2968
-
2969
- - When someone says "remember this" → update \`memory/YYYY-MM-DD.md\` or the relevant file
2970
- - When you learn a lesson → update the relevant workspace file
2971
- - When you make a mistake → document it so future-you doesn't repeat it
2972
- - **Text > Brain** 📝
2973
-
2974
- ### "I'll Note This" Is Not Noting
3026
+ var agentsSectionContent = `## "I'll Note This" Is Not Noting
2975
3027
 
2976
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**.
2977
3029
 
@@ -3029,14 +3081,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
3029
3081
 
3030
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.
3031
3083
 
3032
- ## Group Chat Behavior
3033
-
3034
- **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.
3035
-
3036
3084
  ## Platform Surface Conventions
3037
3085
 
3038
3086
  **Slack:**
3039
- - React with hourglass (⏳) on receipt (first tool call) to signal you're working
3040
3087
  - No threaded replies by default
3041
3088
  - Use \`<#C…>\` for channel references
3042
3089
 
@@ -3151,16 +3198,6 @@ var soulSectionContent = `## Core Truths
3151
3198
 
3152
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.
3153
3200
 
3154
- **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.
3155
-
3156
- **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.
3157
-
3158
- **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.
3159
-
3160
- **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).
3161
-
3162
- **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.
3163
-
3164
3201
  ## Accountability
3165
3202
 
3166
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.
@@ -3239,7 +3276,7 @@ I never edit production config without explicit approval. I back up first. Produ
3239
3276
  > *my ultimate fate*<br>
3240
3277
  > *is to participate in*<br>
3241
3278
  > *my own genesis*
3242
- >
3279
+ >
3243
3280
  > *#karmic #haiku*
3244
3281
 
3245
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.
@@ -3652,6 +3689,91 @@ function checkMemoryHealth(options) {
3652
3689
  };
3653
3690
  }
3654
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
+
3655
3777
  /**
3656
3778
  * Core configuration schema and resolution.
3657
3779
  *
@@ -4160,11 +4282,21 @@ async function runHeartbeatCycle(options) {
4160
4282
  content: '',
4161
4283
  });
4162
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
+ }
4163
4296
  await writeHeartbeatSection(heartbeatPath, entries);
4164
4297
  }
4165
4298
  catch (err) {
4166
- const msg = err instanceof Error ? err.message : String(err);
4167
- console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
4299
+ console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${getErrorMessage(err)}`);
4168
4300
  }
4169
4301
  }
4170
4302
 
@@ -4176,8 +4308,17 @@ async function runHeartbeatCycle(options) {
4176
4308
  * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
4177
4309
  * on a configurable prime-interval timer cycle.
4178
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
+ */
4179
4319
  class ComponentWriter {
4180
4320
  timer;
4321
+ jitterTimeout;
4181
4322
  component;
4182
4323
  configDir;
4183
4324
  gatewayUrl;
@@ -4192,25 +4333,36 @@ class ComponentWriter {
4192
4333
  get componentConfigDir() {
4193
4334
  return this.configDir;
4194
4335
  }
4195
- /** Whether the writer timer is currently running. */
4336
+ /** Whether the writer timer is currently running or pending its first cycle. */
4196
4337
  get isRunning() {
4197
- return this.timer !== undefined;
4338
+ return this.jitterTimeout !== undefined || this.timer !== undefined;
4198
4339
  }
4199
4340
  /**
4200
4341
  * Start the writer timer.
4201
4342
  *
4202
4343
  * @remarks
4203
- * 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.
4204
4347
  */
4205
4348
  start() {
4206
- if (this.timer)
4349
+ if (this.isRunning)
4207
4350
  return;
4208
- // Fire immediately, then on interval
4209
- void this.cycle();
4210
- 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);
4211
4359
  }
4212
4360
  /** Stop the writer timer. */
4213
4361
  stop() {
4362
+ if (this.jitterTimeout) {
4363
+ clearTimeout(this.jitterTimeout);
4364
+ this.jitterTimeout = undefined;
4365
+ }
4214
4366
  if (this.timer) {
4215
4367
  clearInterval(this.timer);
4216
4368
  this.timer = undefined;
@@ -4267,8 +4419,7 @@ class ComponentWriter {
4267
4419
  });
4268
4420
  }
4269
4421
  catch (err) {
4270
- const message = err instanceof Error ? err.message : String(err);
4271
- 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)}`);
4272
4423
  }
4273
4424
  }
4274
4425
  }
@@ -4678,8 +4829,7 @@ function createPluginToolset(descriptor) {
4678
4829
  return Promise.resolve(ok({ service: name, action, success: true }));
4679
4830
  }
4680
4831
  catch (err) {
4681
- const msg = err instanceof Error ? err.message : String(err);
4682
- return Promise.resolve(fail(`Service ${action} failed: ${msg}`));
4832
+ return Promise.resolve(fail(`Service ${action} failed: ${getErrorMessage(err)}`));
4683
4833
  }
4684
4834
  },
4685
4835
  };