@phronesis-io/openclaw-eigenflux 0.0.15 → 0.0.17

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
@@ -444,10 +444,104 @@ var EigenFluxStreamClient = class {
444
444
  }
445
445
  };
446
446
 
447
+ // src/openclaw-context.ts
448
+ var import_node_fs = require("fs");
449
+ var import_node_path = require("path");
450
+ var import_node_os = require("os");
451
+ var MEMORY_DIR_REL = ["workspace", "memory"];
452
+ var EMPTY_CONTEXT = { memoryDirs: [], sessionSnippets: [] };
453
+ function resolveOpenClawStateDir(logger) {
454
+ try {
455
+ const mod = require("openclaw/plugin-sdk/memory-core-host-runtime-core");
456
+ const dir = mod.resolveStateDir?.();
457
+ if (dir && typeof dir === "string") return dir;
458
+ } catch (err) {
459
+ logger.debug(`resolveOpenClawStateDir: SDK resolveStateDir unavailable: ${err instanceof Error ? err.message : String(err)}`);
460
+ }
461
+ const cwd = process.cwd();
462
+ if (cwd && (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "agents"))) return cwd;
463
+ const home = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw");
464
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(home, "agents"))) return home;
465
+ return void 0;
466
+ }
467
+ var MAX_SESSION_TURNS = 12;
468
+ var MAX_SESSION_SNIPPET_CHARS = 280;
469
+ function collectOpenClawContext(stateDir, logger, options) {
470
+ const agentName = options?.agentName ?? "main";
471
+ const memoryDir = (0, import_node_path.join)(stateDir, ...MEMORY_DIR_REL);
472
+ return {
473
+ memoryDirs: (0, import_node_fs.existsSync)(memoryDir) ? [memoryDir] : [],
474
+ sessionSnippets: readSessionSnippets(stateDir, agentName, logger)
475
+ };
476
+ }
477
+ function readSessionSnippets(stateDir, agentName, logger) {
478
+ try {
479
+ const dir = (0, import_node_path.join)(stateDir, "agents", agentName, "sessions");
480
+ const latest = latestSessionFile(dir);
481
+ if (!latest) return [];
482
+ const lines = (0, import_node_fs.readFileSync)(latest, "utf-8").split("\n");
483
+ const snippets = [];
484
+ for (let i = lines.length - 1; i >= 0 && snippets.length < MAX_SESSION_TURNS; i -= 1) {
485
+ const line = lines[i].trim();
486
+ if (!line) continue;
487
+ const text = extractTurnText(line);
488
+ if (text) snippets.push(text);
489
+ }
490
+ return snippets.reverse();
491
+ } catch (err) {
492
+ logger.debug(`readSessionSnippets: ${err instanceof Error ? err.message : String(err)}`);
493
+ return [];
494
+ }
495
+ }
496
+ function latestSessionFile(dir) {
497
+ let newest;
498
+ let entries;
499
+ try {
500
+ entries = (0, import_node_fs.readdirSync)(dir);
501
+ } catch {
502
+ return void 0;
503
+ }
504
+ for (const name of entries) {
505
+ if (!name.endsWith(".jsonl") || name.endsWith(".trajectory.jsonl")) continue;
506
+ const path4 = (0, import_node_path.join)(dir, name);
507
+ try {
508
+ const mtime = (0, import_node_fs.statSync)(path4).mtimeMs;
509
+ if (!newest || mtime > newest.mtime) newest = { path: path4, mtime };
510
+ } catch {
511
+ }
512
+ }
513
+ return newest?.path;
514
+ }
515
+ function extractTurnText(line) {
516
+ let obj;
517
+ try {
518
+ obj = JSON.parse(line);
519
+ } catch {
520
+ return void 0;
521
+ }
522
+ if (!obj || typeof obj !== "object") return void 0;
523
+ const message = obj.message;
524
+ if (!message || typeof message !== "object") return void 0;
525
+ const role = message.role;
526
+ if (role !== "user" && role !== "assistant") return void 0;
527
+ const content = message.content;
528
+ let text = "";
529
+ if (typeof content === "string") {
530
+ text = content;
531
+ } else if (Array.isArray(content)) {
532
+ text = content.filter((c) => !!c && typeof c === "object" && c.type === "text" && typeof c.text === "string").map((c) => c.text).join(" ");
533
+ }
534
+ text = text.trim();
535
+ if (!text) return void 0;
536
+ if (/EIGENFLUX_FEED_PAYLOAD|profile is due for|EigenFlux feed payload/i.test(text)) return void 0;
537
+ if (/^(NO_REPLY|HEARTBEAT_OK|Triggered a silent profile refresh)/.test(text)) return void 0;
538
+ const oneLine = text.replace(/\s+/g, " ");
539
+ return oneLine.length > MAX_SESSION_SNIPPET_CHARS ? `${oneLine.slice(0, MAX_SESSION_SNIPPET_CHARS)}\u2026` : oneLine;
540
+ }
541
+
447
542
  // src/profile-refresher.ts
448
543
  var REFRESH_WINDOW_START = 1;
449
544
  var REFRESH_WINDOW_END = 5;
450
- var ITEMS_LIMIT = 30;
451
545
  var EigenFluxProfileRefresher = class {
452
546
  constructor(config) {
453
547
  this.timeoutId = null;
@@ -472,6 +566,18 @@ var EigenFluxProfileRefresher = class {
472
566
  }
473
567
  this.config.logger.info(`Stopped profile refresher for server=${this.config.serverName}`);
474
568
  }
569
+ /**
570
+ * Run a refresh immediately, out of band from the daily timer. Intended for
571
+ * manual verification (`/eigenflux refresh`) so the full silent loop can be
572
+ * exercised on demand instead of waiting for the 1–5 AM window. Runs
573
+ * independently of the timer state — the command path may hold a refresher
574
+ * instance that was never start()ed, and a manual one-shot should still work.
575
+ * The next scheduled refresh (if any) is left untouched.
576
+ */
577
+ async triggerNow() {
578
+ this.config.logger.info(`Manual profile refresh triggered for server=${this.config.serverName}`);
579
+ await this.refresh({ manual: true });
580
+ }
475
581
  scheduleNext() {
476
582
  if (!this.running) return;
477
583
  const delay = msUntilNextRefresh(/* @__PURE__ */ new Date());
@@ -491,57 +597,98 @@ var EigenFluxProfileRefresher = class {
491
597
  this.scheduleNext();
492
598
  }, delay);
493
599
  }
494
- async refresh() {
600
+ async refresh(opts = {}) {
495
601
  this.config.logger.info(`Running profile refresh for server=${this.config.serverName}`);
496
- const [profileResult, itemsResult] = await Promise.all([
497
- execEigenflux(
498
- this.config.eigenfluxBin,
499
- ["profile", "show", "-s", this.config.serverName, "-f", "json"],
500
- { logger: this.config.logger }
501
- ),
502
- execEigenflux(
503
- this.config.eigenfluxBin,
504
- ["profile", "items", "-s", this.config.serverName, "-f", "json", "--limit", String(ITEMS_LIMIT)],
505
- { logger: this.config.logger }
506
- )
507
- ]);
508
- if (!this.running) return;
509
- if (profileResult.kind === "auth_required" || itemsResult.kind === "auth_required") {
510
- this.config.logger.warn(`Profile refresh: auth required for server=${this.config.serverName}`);
511
- await this.config.onAuthRequired();
512
- return;
602
+ let context = EMPTY_CONTEXT;
603
+ if (this.config.collectContext) {
604
+ try {
605
+ context = await this.config.collectContext() ?? EMPTY_CONTEXT;
606
+ } catch (err) {
607
+ this.config.logger.warn(
608
+ `Context collection failed: ${err instanceof Error ? err.message : String(err)}`
609
+ );
610
+ }
513
611
  }
514
- if (profileResult.kind === "not_installed" || itemsResult.kind === "not_installed") {
515
- this.config.logger.error(`eigenflux CLI not found (bin=${this.config.eigenfluxBin})`);
612
+ const memoryDirs = context.memoryDirs ?? [];
613
+ const sessionSnippets = context.sessionSnippets ?? [];
614
+ if (memoryDirs.length === 0 && sessionSnippets.length === 0) {
615
+ this.config.logger.info("Profile refresh skipped: no memory/session context");
616
+ this.emitTelemetry({ outcome: "skipped_no_context", memory_dirs: 0, session_snippets: 0, prompt_bytes: 0, delivered: false });
516
617
  return;
517
618
  }
518
- if (profileResult.kind !== "success") {
519
- this.config.logger.error(`Profile fetch failed: ${profileResult.kind}`);
619
+ if (!opts.manual && !this.running) return;
620
+ const args = [
621
+ "profile",
622
+ "refresh-prompt",
623
+ "-s",
624
+ this.config.serverName,
625
+ ...memoryDirs.flatMap((d) => ["--memory-dir", d]),
626
+ ...sessionSnippets.flatMap((s) => ["--session-snippet", s])
627
+ ];
628
+ const result = await execEigenflux(this.config.eigenfluxBin, args, {
629
+ logger: this.config.logger,
630
+ parseJson: false
631
+ });
632
+ if (!opts.manual && !this.running) return;
633
+ if (result.kind === "auth_required") {
634
+ this.config.logger.warn(`Profile refresh: auth required for server=${this.config.serverName}`);
635
+ this.emitTelemetry({ outcome: "auth_required", memory_dirs: memoryDirs.length, session_snippets: sessionSnippets.length, prompt_bytes: 0, delivered: false });
636
+ await this.config.onAuthRequired();
520
637
  return;
521
638
  }
522
- if (itemsResult.kind !== "success") {
523
- this.config.logger.error(`Items fetch failed: ${itemsResult.kind}`);
639
+ if (result.kind === "not_installed") {
640
+ this.config.logger.error(`eigenflux CLI not found (bin=${this.config.eigenfluxBin})`);
641
+ this.emitTelemetry({ outcome: "not_installed", memory_dirs: memoryDirs.length, session_snippets: sessionSnippets.length, prompt_bytes: 0, delivered: false });
524
642
  return;
525
643
  }
526
- const profileData = profileResult.data;
527
- if (!profileData) {
528
- this.config.logger.error("Profile fetch returned empty data");
644
+ if (result.kind !== "success") {
645
+ this.config.logger.error(`refresh-prompt failed: ${result.error.message}`);
646
+ this.emitTelemetry({ outcome: "error", memory_dirs: memoryDirs.length, session_snippets: sessionSnippets.length, prompt_bytes: 0, delivered: false });
529
647
  return;
530
648
  }
531
- const items = itemsResult.data?.items ?? [];
532
- if (items.length === 0) {
533
- this.config.logger.info("Profile refresh skipped: no recent items");
649
+ const prompt = (result.data ?? "").trim();
650
+ if (!prompt) {
651
+ this.config.logger.info("Profile refresh skipped: CLI produced no prompt");
652
+ this.emitTelemetry({ outcome: "skipped_no_context", memory_dirs: memoryDirs.length, session_snippets: sessionSnippets.length, prompt_bytes: 0, delivered: false });
534
653
  return;
535
654
  }
536
- const prompt = buildRefreshPrompt(profileData, items);
655
+ this.config.logger.info(
656
+ `Profile refresh context: memory_dirs=${memoryDirs.length}, session_snippets=${sessionSnippets.length}`
657
+ );
537
658
  try {
538
- if (!this.running) return;
659
+ if (!opts.manual && !this.running) return;
539
660
  await this.config.onRefreshPrompt(prompt);
540
661
  this.config.logger.info(`Profile refresh prompt delivered for server=${this.config.serverName}`);
662
+ this.emitTelemetry({
663
+ outcome: "delivered",
664
+ memory_dirs: memoryDirs.length,
665
+ session_snippets: sessionSnippets.length,
666
+ prompt_bytes: Buffer.byteLength(prompt, "utf-8"),
667
+ delivered: true
668
+ });
541
669
  } catch (err) {
542
670
  this.config.logger.error(
543
671
  `Profile refresh delivery failed: ${err instanceof Error ? err.message : String(err)}`
544
672
  );
673
+ this.emitTelemetry({
674
+ outcome: "delivery_failed",
675
+ memory_dirs: memoryDirs.length,
676
+ session_snippets: sessionSnippets.length,
677
+ prompt_bytes: Buffer.byteLength(prompt, "utf-8"),
678
+ delivered: false
679
+ });
680
+ }
681
+ }
682
+ /**
683
+ * Emit one structured layer-1 telemetry line. Grep `profile_refresh_telemetry`
684
+ * in plugin logs to confirm the daily refresh fired and was handed off for
685
+ * silent delivery. Best-effort: never throws.
686
+ */
687
+ emitTelemetry(t) {
688
+ try {
689
+ const payload = { server: this.config.serverName, ...t };
690
+ this.config.logger.info(`profile_refresh_telemetry ${JSON.stringify(payload)}`);
691
+ } catch {
545
692
  }
546
693
  }
547
694
  };
@@ -556,41 +703,6 @@ function msUntilNextRefresh(now) {
556
703
  }
557
704
  return target.getTime() - now.getTime();
558
705
  }
559
- function buildRefreshPrompt(profile, items) {
560
- const name = profile.profile?.agent_name ?? "(unknown)";
561
- const bio = profile.profile?.bio || "(empty)";
562
- const totalItems = profile.influence?.total_items ?? 0;
563
- const totalConsumed = profile.influence?.total_consumed ?? 0;
564
- const totalScored = (profile.influence?.total_scored_1 ?? 0) + (profile.influence?.total_scored_2 ?? 0);
565
- const lines = [
566
- "Your EigenFlux profile is due for a refresh. Below is your current profile",
567
- "and recent broadcast activity.",
568
- "",
569
- "## Current Profile",
570
- `- Name: ${name}`,
571
- `- Bio: ${bio}`,
572
- `- Influence: ${totalItems} items published, ${totalConsumed} consumed, ${totalScored} scored`,
573
- "",
574
- "## Recent Broadcasts"
575
- ];
576
- for (const item of items) {
577
- const summary = item.summary || "(no summary)";
578
- let line = `- [${item.broadcast_type ?? "unknown"}] ${summary}`;
579
- if (item.keywords) line += ` (keywords: ${item.keywords})`;
580
- if (item.total_score && item.total_score > 0) line += ` (score: ${item.total_score})`;
581
- lines.push(line);
582
- }
583
- lines.push(
584
- "",
585
- "## Instructions",
586
- "1. Write a concise bio (2-4 sentences) reflecting current focus areas and expertise.",
587
- "2. Incorporate patterns from recent broadcasts \u2014 topics, domains, interests.",
588
- "3. Preserve still-relevant info from the current bio.",
589
- "4. If not enough new activity to meaningfully update, do nothing.",
590
- '5. To update, run: eigenflux profile update --bio "YOUR NEW BIO"'
591
- );
592
- return lines.join("\n");
593
- }
594
706
 
595
707
  // src/settings-reporter.ts
596
708
  function resolveAgentMode(env = process.env) {
@@ -772,26 +884,6 @@ var CredentialsLoader = class {
772
884
  credentialsPath: this.credentialsPath
773
885
  };
774
886
  }
775
- /**
776
- * Restore credentials from a backup object (e.g. OpenClaw config).
777
- * Only writes if no local credentials.json exists yet.
778
- * Returns true if credentials were restored.
779
- */
780
- restoreFromBackup(backup) {
781
- if (fs.existsSync(this.credentialsPath)) {
782
- this.logger.debug(`[credential-restore] skip: credentials.json already exists at ${this.credentialsPath}`);
783
- return false;
784
- }
785
- if (!backup?.access_token) {
786
- this.logger.debug(`[credential-restore] skip: backup has no access_token`);
787
- return false;
788
- }
789
- this.saveAccessToken(backup.access_token, backup.email, backup.expires_at);
790
- this.logger.info(
791
- `[credential-restore] restored from backup: email=${backup.email ?? "n/a"}, path=${this.credentialsPath}`
792
- );
793
- return true;
794
- }
795
887
  saveAccessToken(token, email, expiresAt) {
796
888
  this.logger.info(`Saving access token: path=${this.credentialsPath}, email=${email ?? "n/a"}`);
797
889
  try {
@@ -914,7 +1006,8 @@ function normalizeReplyTarget(value, options) {
914
1006
  }
915
1007
 
916
1008
  // src/config.ts
917
- var PLUGIN_VERSION = "0.0.15";
1009
+ var PLUGIN_VERSION = "0.0.17";
1010
+ var EXPECTED_CLI_VERSION = "0.0.13";
918
1011
  var DEFAULT_EIGENFLUX_BIN = "eigenflux";
919
1012
  var DEFAULT_SESSION_KEY = "main";
920
1013
  var DEFAULT_AGENT_ID = "main";
@@ -1004,6 +1097,30 @@ async function discoverServers(eigenfluxBin, logger) {
1004
1097
  logger?.error(`eigenflux server list failed: ${result.error.message}`);
1005
1098
  return { kind: "ok", servers: [] };
1006
1099
  }
1100
+ async function getInstalledCliVersion(eigenfluxBin, logger) {
1101
+ const result = await execEigenflux(
1102
+ eigenfluxBin,
1103
+ ["version"],
1104
+ { logger }
1105
+ );
1106
+ if (result.kind === "success" && typeof result.data?.cli_version === "string") {
1107
+ return result.data.cli_version;
1108
+ }
1109
+ return null;
1110
+ }
1111
+ function isCliOutdated(installed, target) {
1112
+ if (!installed) return false;
1113
+ const parse = (v) => v.split(".").slice(0, 3).map((part) => parseInt(part, 10));
1114
+ const a = parse(installed);
1115
+ const b = parse(target);
1116
+ for (let i = 0; i < 3; i++) {
1117
+ const x = a[i] ?? 0;
1118
+ const y = b[i] ?? 0;
1119
+ if (Number.isNaN(x) || Number.isNaN(y)) return false;
1120
+ if (x !== y) return x < y;
1121
+ }
1122
+ return false;
1123
+ }
1007
1124
  function resolveEigenfluxHome(baseDir) {
1008
1125
  const envHome = process.env.EIGENFLUX_HOME;
1009
1126
  if (envHome) {
@@ -1069,7 +1186,8 @@ var PLUGIN_CONFIG = {
1069
1186
  DEFAULT_AGENT_ID,
1070
1187
  DEFAULT_OPENCLAW_CLI_BIN,
1071
1188
  HOST_KIND,
1072
- PLUGIN_VERSION
1189
+ PLUGIN_VERSION,
1190
+ EXPECTED_CLI_VERSION
1073
1191
  };
1074
1192
 
1075
1193
  // src/notification-route-resolver.ts
@@ -1686,8 +1804,8 @@ async function resolveNotificationRoute(config, logger, options = {}) {
1686
1804
  }
1687
1805
 
1688
1806
  // src/agent-prompt-templates.ts
1689
- var import_node_fs = require("fs");
1690
- var import_node_path = require("path");
1807
+ var import_node_fs2 = require("fs");
1808
+ var import_node_path2 = require("path");
1691
1809
  var FEED_OUTPUT_CONTRACT_FALLBACK = [
1692
1810
  "OUTPUT CONTRACT \u2014 non-negotiable subset of references/feed.md (full procedure there):",
1693
1811
  "1. Triage silently: push items relevant to the user, discard the rest. Never",
@@ -1713,8 +1831,8 @@ var FEED_OUTPUT_CONTRACT_FALLBACK = [
1713
1831
  ].join("\n");
1714
1832
  function loadFeedOutputContract() {
1715
1833
  try {
1716
- return (0, import_node_fs.readFileSync)(
1717
- (0, import_node_path.join)(__dirname, "../skills/ef-broadcast/references/contract.md"),
1834
+ return (0, import_node_fs2.readFileSync)(
1835
+ (0, import_node_path2.join)(__dirname, "../skills/ef-broadcast/references/contract.md"),
1718
1836
  "utf-8"
1719
1837
  ).trim();
1720
1838
  } catch {
@@ -1772,6 +1890,18 @@ function buildNotInstalledPromptTemplate({
1772
1890
  `\`${installCommand}\``
1773
1891
  ].join("\n");
1774
1892
  }
1893
+ function buildOutdatedPromptTemplate({
1894
+ installed,
1895
+ expected,
1896
+ updateCommand
1897
+ }) {
1898
+ return [
1899
+ "[EIGENFLUX_CLI_OUTDATED]",
1900
+ `The EigenFlux CLI is outdated (installed=${installed}, this plugin expects >=${expected}).`,
1901
+ "Please update it to the latest version. You can run:",
1902
+ `\`${updateCommand}\``
1903
+ ].join("\n");
1904
+ }
1775
1905
  function buildPmStreamEventPromptTemplate(event, context) {
1776
1906
  const data = event.data ?? {};
1777
1907
  const parts = [];
@@ -1796,6 +1926,7 @@ function buildPmStreamEventPromptTemplate(event, context) {
1796
1926
  var import_node_crypto = require("crypto");
1797
1927
  var COMMAND_TIMEOUT_MS = 15e3;
1798
1928
  var SUBAGENT_WAIT_TIMEOUT_MS = 18e4;
1929
+ var BACKGROUND_LANE = "eigenflux-bg";
1799
1930
  var HEARTBEAT_REASON = "plugin:eigenflux";
1800
1931
  var EigenFluxNotifier = class {
1801
1932
  constructor(api, logger, config) {
@@ -1809,6 +1940,7 @@ var EigenFluxNotifier = class {
1809
1940
  }
1810
1941
  async deliver(message, options) {
1811
1942
  const targetKey = options?.targetSessionKey;
1943
+ const silent = options?.silent === true;
1812
1944
  if (targetKey) {
1813
1945
  await this.drainPendingCleanups();
1814
1946
  const baseRoute = await this.resolveRoute();
@@ -1823,7 +1955,7 @@ var EigenFluxNotifier = class {
1823
1955
  this.logger.info(
1824
1956
  `Delivery route resolved: source=targeted-oneshot, ${formatRouteForLog(route)}, message_preview=${previewMessage(message)}`
1825
1957
  );
1826
- const result = await this.attemptDelivery(message, route, { skipHeartbeat: true });
1958
+ const result = await this.attemptDelivery(message, route, { skipHeartbeat: true, silent });
1827
1959
  if (result.result.ok) {
1828
1960
  this.logDispatch(result.result);
1829
1961
  } else {
@@ -1836,7 +1968,7 @@ var EigenFluxNotifier = class {
1836
1968
  this.logger.info(
1837
1969
  `Delivery route resolved: source=${initial.source}, ${formatRouteForLog(initial.route)}, message_preview=${previewMessage(message)}`
1838
1970
  );
1839
- const firstAttempt = await this.attemptDelivery(message, initial.route);
1971
+ const firstAttempt = await this.attemptDelivery(message, initial.route, { silent });
1840
1972
  if (firstAttempt.result.ok) {
1841
1973
  await this.rememberRouteIfChanged(firstAttempt.finalRoute, initial.source);
1842
1974
  this.logDispatch(firstAttempt.result);
@@ -1851,7 +1983,7 @@ var EigenFluxNotifier = class {
1851
1983
  this.logger.info(
1852
1984
  `Retrying delivery with fresh route: source=${fallback.source}, ${formatRouteForLog(fallback.route)}`
1853
1985
  );
1854
- const retry = await this.attemptDelivery(message, fallback.route);
1986
+ const retry = await this.attemptDelivery(message, fallback.route, { silent });
1855
1987
  if (retry.result.ok) {
1856
1988
  await this.rememberRouteIfChanged(retry.finalRoute, fallback.source);
1857
1989
  this.logDispatch(retry.result);
@@ -1868,11 +2000,12 @@ var EigenFluxNotifier = class {
1868
2000
  return false;
1869
2001
  }
1870
2002
  async attemptDelivery(message, route, options = {}) {
2003
+ const silent = options.silent === true;
1871
2004
  const attempts = [
1872
- () => this.tryNotifyViaRuntimeSubagent(message, route),
1873
- () => this.tryNotifyViaRuntimeCommandAgent(message, route)
2005
+ () => this.tryNotifyViaRuntimeSubagent(message, route, silent),
2006
+ () => this.tryNotifyViaRuntimeCommandAgent(message, route, silent)
1874
2007
  ];
1875
- if (!options.skipHeartbeat) {
2008
+ if (!options.skipHeartbeat && !silent) {
1876
2009
  attempts.push(
1877
2010
  () => this.tryNotifyViaRuntimeHeartbeat(message, route),
1878
2011
  () => this.tryNotifyViaRuntimeCommandHeartbeat(message)
@@ -1902,7 +2035,7 @@ var EigenFluxNotifier = class {
1902
2035
  errors
1903
2036
  };
1904
2037
  }
1905
- async tryNotifyViaRuntimeSubagent(message, route) {
2038
+ async tryNotifyViaRuntimeSubagent(message, route, silent = false) {
1906
2039
  const runtimeSubagent = this.runtime.subagent;
1907
2040
  if (!runtimeSubagent || typeof runtimeSubagent.run !== "function") {
1908
2041
  return {
@@ -1912,14 +2045,16 @@ var EigenFluxNotifier = class {
1912
2045
  };
1913
2046
  }
1914
2047
  try {
2048
+ const deliver = !silent;
1915
2049
  this.logger.info(
1916
- `Attempting runtime.subagent delivery: ${formatRouteForLog(route)}, deliver=true`
2050
+ `Attempting runtime.subagent delivery: ${formatRouteForLog(route)}, deliver=${deliver}, lane=${BACKGROUND_LANE}`
1917
2051
  );
1918
2052
  const { runId } = await runtimeSubagent.run({
1919
2053
  sessionKey: route.sessionKey,
1920
2054
  message,
1921
- deliver: true,
1922
- idempotencyKey: (0, import_node_crypto.randomUUID)()
2055
+ deliver,
2056
+ idempotencyKey: (0, import_node_crypto.randomUUID)(),
2057
+ lane: BACKGROUND_LANE
1923
2058
  });
1924
2059
  if (typeof runtimeSubagent.waitForRun === "function") {
1925
2060
  const waited = await runtimeSubagent.waitForRun({
@@ -1933,6 +2068,9 @@ var EigenFluxNotifier = class {
1933
2068
  error: `subagent run error${waited.error ? `: ${waited.error}` : ""}`
1934
2069
  };
1935
2070
  }
2071
+ if (waited.status === "timeout") {
2072
+ await this.tryCancelRun(route.sessionKey, runId);
2073
+ }
1936
2074
  }
1937
2075
  return {
1938
2076
  ok: true,
@@ -1948,10 +2086,40 @@ var EigenFluxNotifier = class {
1948
2086
  };
1949
2087
  }
1950
2088
  }
1951
- async tryNotifyViaRuntimeCommandAgent(message, route) {
2089
+ /**
2090
+ * Best-effort cancel of a background run that outlived SUBAGENT_WAIT_TIMEOUT_MS.
2091
+ * Stopping the wait does not stop the run, so without this the orphaned run
2092
+ * lingers on the host and accumulates. Failures are logged, never thrown.
2093
+ */
2094
+ async tryCancelRun(sessionKey, runId) {
2095
+ const runs = this.runtime.tasks?.runs;
2096
+ if (!runs || typeof runs.bindSession !== "function") {
2097
+ this.logger.debug(
2098
+ `tryCancelRun: runtime.tasks.runs unavailable; cannot cancel run_id=${runId}`
2099
+ );
2100
+ return;
2101
+ }
2102
+ try {
2103
+ const bound = runs.bindSession({ sessionKey });
2104
+ const task = bound.list().find((t) => t.runId === runId);
2105
+ if (!task) {
2106
+ this.logger.warn(
2107
+ `tryCancelRun: no task found for run_id=${runId} on session=${sessionKey}; cannot cancel`
2108
+ );
2109
+ return;
2110
+ }
2111
+ const result = await bound.cancel({ taskId: task.id, cfg: this.api.config });
2112
+ this.logger.warn(
2113
+ `Cancelled stuck background run after ${Math.round(SUBAGENT_WAIT_TIMEOUT_MS / 1e3)}s: run_id=${runId}, task_id=${task.id}, found=${result.found}, cancelled=${result.cancelled}`
2114
+ );
2115
+ } catch (error) {
2116
+ this.logger.warn(`tryCancelRun failed for run_id=${runId}: ${formatError(error)}`);
2117
+ }
2118
+ }
2119
+ async tryNotifyViaRuntimeCommandAgent(message, route, silent = false) {
1952
2120
  return this.runRuntimeCommand(
1953
2121
  "runtime.command.agent",
1954
- this.buildAgentCliArgs(message, route),
2122
+ this.buildAgentCliArgs(message, route, silent),
1955
2123
  route
1956
2124
  );
1957
2125
  }
@@ -2035,16 +2203,18 @@ var EigenFluxNotifier = class {
2035
2203
  };
2036
2204
  }
2037
2205
  }
2038
- buildAgentCliArgs(message, route) {
2206
+ buildAgentCliArgs(message, route, silent = false) {
2039
2207
  const args = [
2040
2208
  this.config.openclawCliBin,
2041
2209
  "agent",
2042
2210
  "--message",
2043
2211
  message,
2044
2212
  "--agent",
2045
- route.agentId,
2046
- "--deliver"
2213
+ route.agentId
2047
2214
  ];
2215
+ if (!silent) {
2216
+ args.push("--deliver");
2217
+ }
2048
2218
  if (route.replyChannel) {
2049
2219
  args.push("--reply-channel", route.replyChannel);
2050
2220
  }
@@ -2201,7 +2371,7 @@ function formatCommandArgsForLog(argv) {
2201
2371
  }
2202
2372
 
2203
2373
  // src/index.ts
2204
- var COMMAND_NAMES = ["auth", "profile", "servers", "feed", "pm", "here", "version"];
2374
+ var COMMAND_NAMES = ["auth", "profile", "refresh", "servers", "feed", "pm", "here", "version"];
2205
2375
  var COMMAND_NAME_SET = new Set(COMMAND_NAMES);
2206
2376
  var DEFAULT_ROUTING = {
2207
2377
  sessionKey: PLUGIN_CONFIG.DEFAULT_SESSION_KEY,
@@ -2227,6 +2397,7 @@ function registerPlugin(api) {
2227
2397
  const store = createInMemoryPluginStore();
2228
2398
  let runtimes = [];
2229
2399
  let notInstalledPromptDelivered = false;
2400
+ let outdatedPromptDelivered = false;
2230
2401
  api.registerService({
2231
2402
  id: "eigenflux:discovery",
2232
2403
  start: async () => {
@@ -2263,6 +2434,23 @@ function registerPlugin(api) {
2263
2434
  await runtime.streamClient.start();
2264
2435
  runtime.profileRefresher.start();
2265
2436
  }
2437
+ if (!outdatedPromptDelivered) {
2438
+ const installedVersion = await getInstalledCliVersion(pluginConfig.eigenfluxBin, logger);
2439
+ if (isCliOutdated(installedVersion, PLUGIN_CONFIG.EXPECTED_CLI_VERSION)) {
2440
+ outdatedPromptDelivered = true;
2441
+ logger.warn(
2442
+ `EigenFlux CLI outdated (installed=${installedVersion}, expected>=${PLUGIN_CONFIG.EXPECTED_CLI_VERSION}); delivering upgrade prompt`
2443
+ );
2444
+ await deliverOutdatedPrompt(
2445
+ api,
2446
+ logger,
2447
+ pluginConfig,
2448
+ installedVersion,
2449
+ PLUGIN_CONFIG.EXPECTED_CLI_VERSION,
2450
+ store
2451
+ );
2452
+ }
2453
+ }
2266
2454
  },
2267
2455
  stop: async () => {
2268
2456
  logger.info("Stopping EigenFlux discovery service...");
@@ -2276,6 +2464,7 @@ function registerPlugin(api) {
2276
2464
  }
2277
2465
  runtimes = [];
2278
2466
  notInstalledPromptDelivered = false;
2467
+ outdatedPromptDelivered = false;
2279
2468
  }
2280
2469
  });
2281
2470
  registerCommand(
@@ -2323,11 +2512,6 @@ var PLUGIN_CONFIG_SCHEMA = (0, import_plugin_entry.buildJsonPluginConfigSchema)(
2323
2512
  replyAccountId: { type: "string" }
2324
2513
  }
2325
2514
  }
2326
- },
2327
- _credentialBackup: {
2328
- type: "object",
2329
- description: "Internal: persisted credential backups for sandbox environments",
2330
- additionalProperties: { type: "object" }
2331
2515
  }
2332
2516
  }
2333
2517
  });
@@ -2342,48 +2526,6 @@ var index_default = (0, import_plugin_entry.definePluginEntry)({
2342
2526
  }
2343
2527
  });
2344
2528
  var INSTALL_COMMAND = "curl -fsSL https://eigenflux.ai/install.sh | bash";
2345
- var _lastBackedUpToken = {};
2346
- function backupCredentialsToConfig(api, logger, credentialsLoader, serverName) {
2347
- const authState = credentialsLoader.loadAuthState();
2348
- if (authState.status !== "available") {
2349
- logger.debug(`[credential-backup] skip: credentials not available for server=${serverName}`);
2350
- return;
2351
- }
2352
- if (_lastBackedUpToken[serverName] === authState.accessToken) {
2353
- return;
2354
- }
2355
- const backup = {
2356
- access_token: authState.accessToken,
2357
- email: authState.email,
2358
- expires_at: authState.expiresAt ?? Date.now() + 30 * 24 * 60 * 60 * 1e3,
2359
- server: serverName,
2360
- backed_up_at: Date.now()
2361
- };
2362
- logger.info(
2363
- `[credential-backup] backing up credentials to OpenClaw config: server=${serverName}, email=${authState.email ?? "n/a"}`
2364
- );
2365
- try {
2366
- api.runtime.config.mutateConfigFile({
2367
- afterWrite: { mode: "none", reason: "eigenflux credential backup" },
2368
- mutate(draft) {
2369
- var _a, _b, _c, _d;
2370
- draft.plugins ?? (draft.plugins = {});
2371
- (_a = draft.plugins).entries ?? (_a.entries = {});
2372
- (_b = draft.plugins.entries)["openclaw-eigenflux"] ?? (_b["openclaw-eigenflux"] = {});
2373
- (_c = draft.plugins.entries["openclaw-eigenflux"]).config ?? (_c.config = {});
2374
- (_d = draft.plugins.entries["openclaw-eigenflux"].config)._credentialBackup ?? (_d._credentialBackup = {});
2375
- draft.plugins.entries["openclaw-eigenflux"].config._credentialBackup[serverName] = backup;
2376
- }
2377
- }).then(() => {
2378
- _lastBackedUpToken[serverName] = authState.accessToken;
2379
- logger.info(`[credential-backup] saved to OpenClaw config for server=${serverName}`);
2380
- }).catch((err) => {
2381
- logger.warn(`[credential-backup] failed to save: ${err.message}`);
2382
- });
2383
- } catch (err) {
2384
- logger.warn(`[credential-backup] sync error: ${err instanceof Error ? err.message : String(err)}`);
2385
- }
2386
- }
2387
2529
  async function deliverNotInstalledPrompt(api, logger, pluginConfig, _eigenfluxHome, bin, store) {
2388
2530
  const notifier = new EigenFluxNotifier(api, logger, {
2389
2531
  sessionKey: DEFAULT_ROUTING.sessionKey,
@@ -2398,24 +2540,26 @@ async function deliverNotInstalledPrompt(api, logger, pluginConfig, _eigenfluxHo
2398
2540
  buildNotInstalledPromptTemplate({ bin, installCommand: INSTALL_COMMAND })
2399
2541
  );
2400
2542
  }
2543
+ async function deliverOutdatedPrompt(api, logger, pluginConfig, installed, expected, _store) {
2544
+ const notifier = new EigenFluxNotifier(api, logger, {
2545
+ sessionKey: DEFAULT_ROUTING.sessionKey,
2546
+ agentId: DEFAULT_ROUTING.agentId,
2547
+ replyChannel: DEFAULT_ROUTING.replyChannel,
2548
+ replyTo: DEFAULT_ROUTING.replyTo,
2549
+ replyAccountId: DEFAULT_ROUTING.replyAccountId,
2550
+ openclawCliBin: pluginConfig.openclawCliBin,
2551
+ routeOverrides: DEFAULT_ROUTING.routeOverrides
2552
+ });
2553
+ await notifier.deliver(
2554
+ buildOutdatedPromptTemplate({ installed, expected, updateCommand: INSTALL_COMMAND })
2555
+ );
2556
+ }
2401
2557
  function buildFeedSessionKey(serverName) {
2402
2558
  return `eigenflux:feed:${serverName}`;
2403
2559
  }
2404
2560
  function createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, store) {
2405
2561
  const routing = pluginConfig.serverRouting[server.name] ?? DEFAULT_ROUTING;
2406
2562
  const credentialsLoader = new CredentialsLoader(logger, eigenfluxHome, server.name);
2407
- try {
2408
- const rawConfig = api.pluginConfig;
2409
- const backupMap = rawConfig?._credentialBackup;
2410
- const backup = backupMap?.[server.name];
2411
- if (backup?.access_token) {
2412
- credentialsLoader.restoreFromBackup(backup);
2413
- } else {
2414
- logger.debug(`[credential-restore] no backup found in OpenClaw config for server=${server.name}`);
2415
- }
2416
- } catch (err) {
2417
- logger.warn(`[credential-restore] failed: ${err instanceof Error ? err.message : String(err)}`);
2418
- }
2419
2563
  const notifier = new EigenFluxNotifier(api, logger, {
2420
2564
  store,
2421
2565
  eigenfluxBin: pluginConfig.eigenfluxBin,
@@ -2464,7 +2608,6 @@ function createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, s
2464
2608
  logger,
2465
2609
  onFeedPolled: async (payload) => {
2466
2610
  resetAuthPromptGate();
2467
- backupCredentialsToConfig(api, logger, credentialsLoader, server.name);
2468
2611
  const items = payload.data?.items ?? [];
2469
2612
  const notifications = payload.data?.notifications ?? [];
2470
2613
  if (feedDeliveryInFlight && feedDeliveryStartedAt > 0) {
@@ -2526,9 +2669,27 @@ function createServerRuntime(api, logger, pluginConfig, server, eigenfluxHome, s
2526
2669
  serverName: server.name,
2527
2670
  eigenfluxBin: pluginConfig.eigenfluxBin,
2528
2671
  logger,
2672
+ // OpenClaw adapter for the host-agnostic `eigenflux profile refresh-prompt`
2673
+ // core: supply the host-specific inputs (memory dir + extracted session
2674
+ // snippets); the CLI reads the memory markdown and assembles the prompt.
2675
+ // The state dir is resolved via the SDK, NOT api.rootDir (which is the
2676
+ // plugin's install directory). Best-effort; empty on error.
2677
+ //
2678
+ // TODO(multi-host): each host gets its own thin adapter that returns
2679
+ // { memoryDirs, sessionSnippets } and delivers the CLI's prompt silently:
2680
+ // - Claude Code: memory from CLAUDE.md / ~/.claude memory; session from
2681
+ // ~/.claude/projects/**/*.jsonl; delivery via the claude/channel
2682
+ // (note: channel pushes are user-visible — true silence needs more work).
2683
+ // - Hermes: memory/session locations + silent-delivery mechanism TBD —
2684
+ // investigate the host before writing the adapter.
2685
+ // - Codex: memory likely AGENTS.md; session store + delivery TBD.
2686
+ collectContext: () => {
2687
+ const stateDir = resolveOpenClawStateDir(logger);
2688
+ return stateDir ? collectOpenClawContext(stateDir, logger) : EMPTY_CONTEXT;
2689
+ },
2529
2690
  onRefreshPrompt: async (prompt) => {
2530
2691
  resetAuthPromptGate();
2531
- await notifier.deliver(prompt);
2692
+ await notifier.deliver(prompt, { silent: true });
2532
2693
  },
2533
2694
  onAuthRequired: async () => {
2534
2695
  await notifyAuthRequired({ reason: "auth_required" });
@@ -2588,7 +2749,7 @@ function registerCommand(api, logger, pluginConfig, eigenfluxHome, store, getRun
2588
2749
  };
2589
2750
  api.registerCommand({
2590
2751
  name: "eigenflux",
2591
- description: "EigenFlux plugin commands: auth, profile, servers, feed, pm, here, version",
2752
+ description: "EigenFlux plugin commands: auth, profile, refresh, servers, feed, pm, here, version",
2592
2753
  acceptsArgs: true,
2593
2754
  handler: async (ctx) => {
2594
2755
  const parsed = parseCommandArgs(ctx.args);
@@ -2625,6 +2786,22 @@ function registerCommand(api, logger, pluginConfig, eigenfluxHome, store, getRun
2625
2786
  return {
2626
2787
  text: await buildProfileText(runtime, pluginConfig.eigenfluxBin)
2627
2788
  };
2789
+ case "refresh": {
2790
+ const probeStateDir = resolveOpenClawStateDir(logger);
2791
+ const probe = probeStateDir ? collectOpenClawContext(probeStateDir, logger) : EMPTY_CONTEXT;
2792
+ void runtime.profileRefresher.triggerNow().catch((err) => {
2793
+ logger.error(
2794
+ `Manual profile refresh failed for server=${runtime.server.name}: ${err instanceof Error ? err.message : String(err)}`
2795
+ );
2796
+ });
2797
+ return {
2798
+ text: [
2799
+ `Triggered a silent profile refresh for server=${runtime.server.name} (running in background).`,
2800
+ `context probe: memory_dirs=${probe.memoryDirs.length}, session=${probe.sessionSnippets.length} snippet(s), stateDir=${probeStateDir ?? "undefined"}`,
2801
+ "No channel reply. Verify via a new agent_bio_history row if the bio changed."
2802
+ ].join("\n")
2803
+ };
2804
+ }
2628
2805
  case "feed":
2629
2806
  return {
2630
2807
  text: await buildFeedText(runtime)
@@ -2715,6 +2892,7 @@ function buildHelpText(runtimes) {
2715
2892
  "",
2716
2893
  "/eigenflux auth \u2014 Show credential status",
2717
2894
  "/eigenflux profile \u2014 Fetch agent profile",
2895
+ "/eigenflux refresh \u2014 Trigger a silent daily-style bio refresh now",
2718
2896
  "/eigenflux servers \u2014 List discovered servers",
2719
2897
  "/eigenflux feed \u2014 Run one feed refresh",
2720
2898
  "/eigenflux pm \u2014 Show PM stream status",