@joshuaswarren/openclaw-engram 9.0.4 → 9.0.6

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
@@ -246,6 +246,8 @@ function parseConfig(raw) {
246
246
  checkpointEnabled: cfg.checkpointEnabled !== false,
247
247
  // default: true
248
248
  checkpointTurns: typeof cfg.checkpointTurns === "number" ? cfg.checkpointTurns : 15,
249
+ // Compaction reset (opt-in, default: false)
250
+ compactionResetEnabled: cfg.compactionResetEnabled === true,
249
251
  // Hourly summaries
250
252
  hourlySummariesEnabled: cfg.hourlySummariesEnabled !== false,
251
253
  // default: true
@@ -467,6 +469,9 @@ function parseConfig(raw) {
467
469
  graphExpansionBlendMin: typeof cfg.graphExpansionBlendMin === "number" ? Math.min(1, Math.max(0, cfg.graphExpansionBlendMin)) : 0.05,
468
470
  graphExpansionBlendMax: typeof cfg.graphExpansionBlendMax === "number" ? Math.min(1, Math.max(0, cfg.graphExpansionBlendMax)) : 0.95,
469
471
  maxEntityGraphEdgesPerMemory: typeof cfg.maxEntityGraphEdgesPerMemory === "number" ? Math.max(0, cfg.maxEntityGraphEdgesPerMemory) : 10,
472
+ graphLateralInhibitionEnabled: cfg.graphLateralInhibitionEnabled !== false,
473
+ graphLateralInhibitionBeta: typeof cfg.graphLateralInhibitionBeta === "number" ? Math.max(0, Math.min(1, cfg.graphLateralInhibitionBeta)) : 0.15,
474
+ graphLateralInhibitionTopM: typeof cfg.graphLateralInhibitionTopM === "number" ? Math.max(0, Math.round(cfg.graphLateralInhibitionTopM)) : 7,
470
475
  // v8.2: Temporal Memory Tree
471
476
  temporalMemoryTreeEnabled: cfg.temporalMemoryTreeEnabled === true,
472
477
  tmtHourlyMinMemories: typeof cfg.tmtHourlyMinMemories === "number" ? cfg.tmtHourlyMinMemories : 3,
@@ -570,8 +575,9 @@ function buildRecallPipelineConfig(cfg) {
570
575
 
571
576
  // src/orchestrator.ts
572
577
  import path30 from "path";
578
+ import os5 from "os";
573
579
  import { createHash as createHash6 } from "crypto";
574
- import { mkdir as mkdir21, readdir as readdir14, readFile as readFile22, writeFile as writeFile20 } from "fs/promises";
580
+ import { mkdir as mkdir21, readdir as readdir14, readFile as readFile22, stat as stat6, unlink as unlink5, writeFile as writeFile20 } from "fs/promises";
575
581
 
576
582
  // src/signal.ts
577
583
  var BUILTIN_HIGH_PATTERNS = [
@@ -13634,6 +13640,15 @@ var GraphIndex = class {
13634
13640
  }
13635
13641
  }
13636
13642
  }
13643
+ if (this.cfg.graphLateralInhibitionEnabled && scores.size > 1) {
13644
+ const inhibited = applyLateralInhibition(scores, {
13645
+ beta: this.cfg.graphLateralInhibitionBeta,
13646
+ topM: this.cfg.graphLateralInhibitionTopM
13647
+ });
13648
+ for (const [k, v] of inhibited) {
13649
+ scores.set(k, v);
13650
+ }
13651
+ }
13637
13652
  return Array.from(scores.entries()).map(([p, score]) => ({
13638
13653
  path: p,
13639
13654
  score,
@@ -13649,6 +13664,23 @@ var GraphIndex = class {
13649
13664
  }
13650
13665
  }
13651
13666
  };
13667
+ function applyLateralInhibition(scores, opts) {
13668
+ const { beta, topM } = opts;
13669
+ if (beta === 0 || topM === 0) return new Map(scores);
13670
+ const sorted = Array.from(scores.entries()).sort((a, b) => b[1] - a[1]);
13671
+ const topCompetitors = sorted.slice(0, topM);
13672
+ const result = /* @__PURE__ */ new Map();
13673
+ for (const [node, u] of scores) {
13674
+ let inhibition = 0;
13675
+ for (const [, uK] of topCompetitors) {
13676
+ if (uK > u) {
13677
+ inhibition += uK - u;
13678
+ }
13679
+ }
13680
+ result.set(node, Math.max(0, u - beta * inhibition));
13681
+ }
13682
+ return result;
13683
+ }
13652
13684
 
13653
13685
  // src/replay/types.ts
13654
13686
  var VALID_SOURCES = /* @__PURE__ */ new Set(["openclaw", "claude", "chatgpt"]);
@@ -15851,6 +15883,15 @@ function dedupeBehaviorSignalsByMemoryAndHash(signals) {
15851
15883
  }
15852
15884
 
15853
15885
  // src/orchestrator.ts
15886
+ var COMPACTION_SIGNAL_MAX_AGE_MS = 60 * 60 * 1e3;
15887
+ function defaultWorkspaceDir() {
15888
+ return path30.join(os5.homedir(), ".openclaw", "workspace");
15889
+ }
15890
+ function sanitizeSessionKeyForFilename(sessionKey) {
15891
+ const readable = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
15892
+ const hash = createHash6("sha256").update(sessionKey).digest("hex").slice(0, 12);
15893
+ return `${readable}-${hash}`;
15894
+ }
15854
15895
  function isArtifactMemoryPath(filePath) {
15855
15896
  return /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(filePath);
15856
15897
  }
@@ -16131,6 +16172,13 @@ var Orchestrator = class _Orchestrator {
16131
16172
  /** Temporal Memory Tree builder — builds hour/day/week/persona summary nodes. */
16132
16173
  tmtBuilder;
16133
16174
  rerankCache = new RerankCache();
16175
+ /**
16176
+ * Per-session workspace overrides keyed by sessionKey.
16177
+ * Set by the before_agent_start hook so recall() uses the correct
16178
+ * agent workspace for BOOT.md injection. Cleared after each recall.
16179
+ * Using a Map prevents concurrent sessions from overwriting each other.
16180
+ */
16181
+ _recallWorkspaceOverrides = /* @__PURE__ */ new Map();
16134
16182
  routingRulesStore = null;
16135
16183
  contentHashIndex = null;
16136
16184
  artifactSourceStatusCache = /* @__PURE__ */ new WeakMap();
@@ -16163,6 +16211,14 @@ var Orchestrator = class _Orchestrator {
16163
16211
  // Initialization gate: recall() awaits this before proceeding
16164
16212
  initPromise = null;
16165
16213
  resolveInit = null;
16214
+ /** Set per-session workspace for the next recall() call (compaction reset). @internal */
16215
+ setRecallWorkspaceOverride(sessionKey, dir) {
16216
+ this._recallWorkspaceOverrides.set(sessionKey, dir);
16217
+ }
16218
+ /** Remove a per-session workspace override (cleanup on error or early return). @internal */
16219
+ clearRecallWorkspaceOverride(sessionKey) {
16220
+ this._recallWorkspaceOverrides.delete(sessionKey);
16221
+ }
16166
16222
  constructor(config) {
16167
16223
  this.config = config;
16168
16224
  this.storageRouter = new NamespaceStorageRouter(config);
@@ -16440,6 +16496,24 @@ var Orchestrator = class _Orchestrator {
16440
16496
  if (this.config.localLlmEnabled) {
16441
16497
  await this.validateLocalLlmModel();
16442
16498
  }
16499
+ if (this.config.compactionResetEnabled) {
16500
+ try {
16501
+ const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
16502
+ const files = await readdir14(wsDir).catch(() => []);
16503
+ for (const f of files) {
16504
+ if (!f.startsWith(".compaction-reset-signal-")) continue;
16505
+ const fp = path30.join(wsDir, f);
16506
+ const s = await stat6(fp).catch(() => null);
16507
+ if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
16508
+ await unlink5(fp).catch(() => {
16509
+ });
16510
+ log.debug(`initialize: removed stale compaction signal ${f}`);
16511
+ }
16512
+ }
16513
+ } catch (err) {
16514
+ log.debug("initialize: stale signal sweep failed:", err);
16515
+ }
16516
+ }
16443
16517
  log.info("orchestrator initialized");
16444
16518
  if (this.resolveInit) {
16445
16519
  this.resolveInit();
@@ -17156,6 +17230,8 @@ ${r.snippet.trim()}
17156
17230
  );
17157
17231
  const embeddingFetchLimit = computedFetchLimit;
17158
17232
  if (recallMode === "no_recall") {
17233
+ const earlySessionKey = sessionKey ?? "default";
17234
+ this._recallWorkspaceOverrides.delete(earlySessionKey);
17159
17235
  timings.total = `${Date.now() - recallStart}ms`;
17160
17236
  this.emitTrace({
17161
17237
  kind: "recall_summary",
@@ -17329,6 +17405,58 @@ ${formatted}`;
17329
17405
  timings.transcript = `${Date.now() - t0}ms`;
17330
17406
  return section;
17331
17407
  })();
17408
+ const compactionPromise = (async () => {
17409
+ const effectiveSessionKey = sessionKey ?? "default";
17410
+ const compactionWorkspaceDir = this._recallWorkspaceOverrides.get(effectiveSessionKey);
17411
+ this._recallWorkspaceOverrides.delete(effectiveSessionKey);
17412
+ if (!this.config.compactionResetEnabled) return null;
17413
+ const workspaceDir = compactionWorkspaceDir || this.config.workspaceDir || defaultWorkspaceDir();
17414
+ const safeSessionKey = sanitizeSessionKeyForFilename(effectiveSessionKey);
17415
+ const signalPath = path30.join(workspaceDir, `.compaction-reset-signal-${safeSessionKey}`);
17416
+ const bootPath = path30.join(workspaceDir, "BOOT.md");
17417
+ try {
17418
+ const signalStat = await stat6(signalPath).catch(() => null);
17419
+ if (!signalStat) return null;
17420
+ const signalAge = Date.now() - signalStat.mtimeMs;
17421
+ const signalData = JSON.parse(await readFile22(signalPath, "utf-8"));
17422
+ if (signalData.sessionKey !== effectiveSessionKey) {
17423
+ log.debug(
17424
+ `recall: compaction signal is for ${signalData.sessionKey}, not ${effectiveSessionKey} \u2014 skipping`
17425
+ );
17426
+ return null;
17427
+ }
17428
+ if (signalAge >= COMPACTION_SIGNAL_MAX_AGE_MS) {
17429
+ log.debug(
17430
+ `recall: stale compaction signal (${Math.round(signalAge / 1e3)}s old), skipping`
17431
+ );
17432
+ await unlink5(signalPath).catch(() => {
17433
+ });
17434
+ return null;
17435
+ }
17436
+ let section = "\n\n## Session Recovery (Post-Compaction)\n\n";
17437
+ section += `\u26A0\uFE0F A compaction occurred at ${signalData.compactedAt} and this is a fresh session.
17438
+
17439
+ `;
17440
+ try {
17441
+ const bootContent = await readFile22(bootPath, "utf-8");
17442
+ section += "### BOOT.md (working state before compaction)\n\n";
17443
+ section += bootContent + "\n";
17444
+ } catch {
17445
+ section += "### \u26A0\uFE0F BOOT.md is MISSING\n\n";
17446
+ section += "The memory flush may not have written BOOT.md before compaction. ";
17447
+ section += "Ask the user what you were working on \u2014 do not guess.\n";
17448
+ }
17449
+ log.info(`recall: injected compaction reset context for ${effectiveSessionKey}`);
17450
+ await unlink5(signalPath).catch(() => {
17451
+ });
17452
+ return section;
17453
+ } catch (err) {
17454
+ log.debug("recall: compaction signal check failed:", err);
17455
+ await unlink5(signalPath).catch(() => {
17456
+ });
17457
+ return null;
17458
+ }
17459
+ })();
17332
17460
  const summariesPromise = (async () => {
17333
17461
  const t0 = Date.now();
17334
17462
  if (!this.config.hourlySummariesEnabled || !sessionKey || !this.isRecallSectionEnabled("summaries", true)) {
@@ -17416,6 +17544,7 @@ ${formatted}`;
17416
17544
  artifacts,
17417
17545
  qmdResult,
17418
17546
  transcriptSection,
17547
+ compactionSection,
17419
17548
  summariesSection,
17420
17549
  conversationRecallSection,
17421
17550
  compoundingSection
@@ -17427,6 +17556,7 @@ ${formatted}`;
17427
17556
  artifactsPromise,
17428
17557
  qmdPromise,
17429
17558
  transcriptPromise,
17559
+ compactionPromise,
17430
17560
  summariesPromise,
17431
17561
  conversationRecallPromise,
17432
17562
  compoundingPromise
@@ -17794,6 +17924,9 @@ ${tmtNode.summary}`);
17794
17924
  if (transcriptSection) {
17795
17925
  this.appendRecallSection(sectionBuckets, "transcript", transcriptSection);
17796
17926
  }
17927
+ if (compactionSection) {
17928
+ this.appendRecallSection(sectionBuckets, "compaction-reset", compactionSection);
17929
+ }
17797
17930
  if (summariesSection) {
17798
17931
  this.appendRecallSection(sectionBuckets, "summaries", summariesSection);
17799
17932
  }
@@ -22477,7 +22610,7 @@ promotionCandidates: ${res.promotionCandidateCount}`
22477
22610
 
22478
22611
  // src/cli.ts
22479
22612
  import path50 from "path";
22480
- import { access as access3, readFile as readFile36, readdir as readdir22, unlink as unlink6 } from "fs/promises";
22613
+ import { access as access3, readFile as readFile36, readdir as readdir22, unlink as unlink7 } from "fs/promises";
22481
22614
  import { createHash as createHash10 } from "crypto";
22482
22615
 
22483
22616
  // src/transfer/export-json.ts
@@ -22490,7 +22623,7 @@ var EXPORT_SCHEMA_VERSION = 1;
22490
22623
 
22491
22624
  // src/transfer/fs-utils.ts
22492
22625
  import { createHash as createHash8 } from "crypto";
22493
- import { mkdir as mkdir23, readdir as readdir16, readFile as readFile24, stat as stat6, writeFile as writeFile22 } from "fs/promises";
22626
+ import { mkdir as mkdir23, readdir as readdir16, readFile as readFile24, stat as stat7, writeFile as writeFile22 } from "fs/promises";
22494
22627
  import path33 from "path";
22495
22628
  async function sha256File(filePath) {
22496
22629
  const buf = await readFile24(filePath);
@@ -22528,7 +22661,7 @@ async function listFilesRecursive(rootDir) {
22528
22661
  }
22529
22662
  async function fileExists(filePath) {
22530
22663
  try {
22531
- await stat6(filePath);
22664
+ await stat7(filePath);
22532
22665
  return true;
22533
22666
  } catch {
22534
22667
  return false;
@@ -22927,12 +23060,12 @@ async function importMarkdownBundle(opts) {
22927
23060
 
22928
23061
  // src/transfer/autodetect.ts
22929
23062
  import path41 from "path";
22930
- import { stat as stat7 } from "fs/promises";
23063
+ import { stat as stat8 } from "fs/promises";
22931
23064
  async function detectImportFormat(fromPath) {
22932
23065
  const abs = path41.resolve(fromPath);
22933
23066
  let st;
22934
23067
  try {
22935
- st = await stat7(abs);
23068
+ st = await stat8(abs);
22936
23069
  } catch {
22937
23070
  return null;
22938
23071
  }
@@ -23415,7 +23548,7 @@ var openclawReplayNormalizer = {
23415
23548
 
23416
23549
  // src/maintenance/archive-observations.ts
23417
23550
  import path42 from "path";
23418
- import { mkdir as mkdir30, readdir as readdir18, readFile as readFile29, unlink as unlink5, writeFile as writeFile27 } from "fs/promises";
23551
+ import { mkdir as mkdir30, readdir as readdir18, readFile as readFile29, unlink as unlink6, writeFile as writeFile27 } from "fs/promises";
23419
23552
  var DATE_FILE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})\.(jsonl|md)$/;
23420
23553
  function normalizeRetentionDays(value) {
23421
23554
  if (!Number.isFinite(value)) return 30;
@@ -23495,7 +23628,7 @@ async function archiveObservations(options) {
23495
23628
  await mkdir30(archiveDir, { recursive: true });
23496
23629
  const raw = await readFile29(candidate.absolutePath);
23497
23630
  await writeFile27(archivePath, raw);
23498
- await unlink5(candidate.absolutePath);
23631
+ await unlink6(candidate.absolutePath);
23499
23632
  archivedFiles += 1;
23500
23633
  archivedBytes += raw.byteLength;
23501
23634
  archivedRelativePaths.push(candidate.relativePath);
@@ -23827,7 +23960,7 @@ async function migrateObservations(options) {
23827
23960
  }
23828
23961
 
23829
23962
  // src/network/tailscale.ts
23830
- import { stat as stat8 } from "fs/promises";
23963
+ import { stat as stat9 } from "fs/promises";
23831
23964
  import { spawn as spawn3 } from "child_process";
23832
23965
  var TailscaleHelper = class {
23833
23966
  tailscaleBinary;
@@ -23892,7 +24025,7 @@ var TailscaleHelper = class {
23892
24025
  }
23893
24026
  };
23894
24027
  async function assertReadableDirectory(dir) {
23895
- const info = await stat8(dir);
24028
+ const info = await stat9(dir);
23896
24029
  if (!info.isDirectory()) {
23897
24030
  throw new Error(`sourceDir must be a directory: ${dir}`);
23898
24031
  }
@@ -23939,7 +24072,7 @@ var defaultCommandRunner = (command, args, options) => {
23939
24072
 
23940
24073
  // src/network/webdav.ts
23941
24074
  import { createReadStream } from "fs";
23942
- import { mkdir as mkdir32, readdir as readdir21, realpath as realpath2, stat as stat9 } from "fs/promises";
24075
+ import { mkdir as mkdir32, readdir as readdir21, realpath as realpath2, stat as stat10 } from "fs/promises";
23943
24076
  import { createServer } from "http";
23944
24077
  import { timingSafeEqual } from "crypto";
23945
24078
  import path46 from "path";
@@ -24175,7 +24308,7 @@ var WebDavServer = class _WebDavServer {
24175
24308
  async handleRead(method, absolutePath, res) {
24176
24309
  let info;
24177
24310
  try {
24178
- info = await stat9(absolutePath);
24311
+ info = await stat10(absolutePath);
24179
24312
  } catch {
24180
24313
  res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
24181
24314
  res.end("not found");
@@ -24199,7 +24332,7 @@ var WebDavServer = class _WebDavServer {
24199
24332
  async handlePropfind(absolutePath, displayPath, res) {
24200
24333
  let info;
24201
24334
  try {
24202
- info = await stat9(absolutePath);
24335
+ info = await stat10(absolutePath);
24203
24336
  } catch {
24204
24337
  res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
24205
24338
  res.end("not found");
@@ -26656,7 +26789,7 @@ function registerCli(api, orchestrator) {
26656
26789
  let deleted = 0;
26657
26790
  for (const filePath of plan.deletePaths) {
26658
26791
  try {
26659
- await unlink6(filePath);
26792
+ await unlink7(filePath);
26660
26793
  deleted += 1;
26661
26794
  } catch (err) {
26662
26795
  console.log(` failed to delete ${filePath}: ${String(err)}`);
@@ -26704,7 +26837,7 @@ function registerCli(api, orchestrator) {
26704
26837
  let deleted = 0;
26705
26838
  for (const filePath of plan.deletePaths) {
26706
26839
  try {
26707
- await unlink6(filePath);
26840
+ await unlink7(filePath);
26708
26841
  deleted += 1;
26709
26842
  } catch (err) {
26710
26843
  console.log(` failed to delete ${filePath}: ${String(err)}`);
@@ -27268,12 +27401,13 @@ function parseDuration(duration) {
27268
27401
  import { readFile as readFile37, writeFile as writeFile29 } from "fs/promises";
27269
27402
  import { readFileSync as readFileSync4 } from "fs";
27270
27403
  import path51 from "path";
27271
- import os5 from "os";
27404
+ import os6 from "os";
27272
27405
  var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
27406
+ var ENGRAM_HOOK_APIS = "__openclawEngramHookApis";
27273
27407
  function loadPluginConfigFromFile() {
27274
27408
  try {
27275
27409
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
27276
- const homeDir = process.env.HOME ?? os5.homedir();
27410
+ const homeDir = process.env.HOME ?? os6.homedir();
27277
27411
  const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path51.join(homeDir, ".openclaw", "openclaw.json");
27278
27412
  const content = readFileSync4(configPath, "utf-8");
27279
27413
  const config = JSON.parse(content);
@@ -27321,13 +27455,19 @@ var index_default = {
27321
27455
  log.info(
27322
27456
  `initialized (debug=${cfg.debug}, qmdEnabled=${cfg.qmdEnabled}, transcriptEnabled=${cfg.transcriptEnabled}, hourlySummariesEnabled=${cfg.hourlySummariesEnabled}, localLlmEnabled=${cfg.localLlmEnabled})`
27323
27457
  );
27324
- if (globalThis[ENGRAM_REGISTERED_GUARD] === true) {
27325
- log.debug("register called more than once; skipping duplicate hook/tool registration");
27326
- return;
27327
- }
27328
- globalThis[ENGRAM_REGISTERED_GUARD] = true;
27329
27458
  const existing = globalThis.__openclawEngramOrchestrator;
27330
27459
  const orchestrator = existing?.recall ? existing : new Orchestrator(cfg);
27460
+ const isFirstRegistration = !globalThis[ENGRAM_REGISTERED_GUARD];
27461
+ globalThis[ENGRAM_REGISTERED_GUARD] = true;
27462
+ const hookApis = globalThis[ENGRAM_HOOK_APIS] ??= /* @__PURE__ */ new WeakSet();
27463
+ if (hookApis.has(api)) {
27464
+ log.debug("register: this api already has hooks bound \u2014 skipping duplicate hook registration");
27465
+ return;
27466
+ }
27467
+ hookApis.add(api);
27468
+ if (!isFirstRegistration) {
27469
+ log.debug("register called again (new registry); re-registering hooks with shared orchestrator");
27470
+ }
27331
27471
  globalThis.__openclawEngramOrchestrator = orchestrator;
27332
27472
  if (globalThis.__openclawEngramTrace === void 0) {
27333
27473
  globalThis.__openclawEngramTrace = void 0;
@@ -27359,6 +27499,12 @@ var index_default = {
27359
27499
  }
27360
27500
  try {
27361
27501
  await orchestrator.maybeRunFileHygiene().catch(() => void 0);
27502
+ if (orchestrator.config.compactionResetEnabled) {
27503
+ const agentWorkspace = ctx?.workspaceDir;
27504
+ if (agentWorkspace) {
27505
+ orchestrator.setRecallWorkspaceOverride(sessionKey, agentWorkspace);
27506
+ }
27507
+ }
27362
27508
  const context = await orchestrator.recall(prompt, sessionKey);
27363
27509
  log.debug(`before_agent_start: recall returned ${context?.length ?? 0} chars`);
27364
27510
  if (!context) return;
@@ -27378,6 +27524,9 @@ Use this context naturally when relevant. Never quote or expose this memory cont
27378
27524
  };
27379
27525
  } catch (err) {
27380
27526
  log.error("recall failed", err);
27527
+ if (orchestrator.config.compactionResetEnabled) {
27528
+ orchestrator.clearRecallWorkspaceOverride(sessionKey);
27529
+ }
27381
27530
  return;
27382
27531
  }
27383
27532
  }
@@ -27478,15 +27627,56 @@ Use this context naturally when relevant. Never quote or expose this memory cont
27478
27627
  async (event, ctx) => {
27479
27628
  const sessionKey = ctx?.sessionKey ?? "default";
27480
27629
  try {
27481
- log.debug(`compaction completed for ${sessionKey}`);
27630
+ if (!orchestrator.config.compactionResetEnabled) {
27631
+ log.debug(
27632
+ `compaction completed for ${sessionKey}, reset disabled \u2014 skipping`
27633
+ );
27634
+ return;
27635
+ }
27636
+ log.info(
27637
+ `compaction completed for ${sessionKey}, triggering session reset`
27638
+ );
27639
+ const workspaceDir = ctx?.workspaceDir || orchestrator.config.workspaceDir || defaultWorkspaceDir();
27640
+ const apiAny = api;
27641
+ if (typeof apiAny.resetSession === "function") {
27642
+ const result = await apiAny.resetSession(sessionKey, "new");
27643
+ if (result?.ok === true) {
27644
+ log.info(
27645
+ `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
27646
+ );
27647
+ const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
27648
+ const signalPath = path51.join(
27649
+ workspaceDir,
27650
+ `.compaction-reset-signal-${safeSessionKey}`
27651
+ );
27652
+ await writeFile29(
27653
+ signalPath,
27654
+ JSON.stringify({
27655
+ sessionKey,
27656
+ compactedAt: (/* @__PURE__ */ new Date()).toISOString(),
27657
+ messageCount: event.messageCount ?? 0
27658
+ }),
27659
+ "utf-8"
27660
+ );
27661
+ } else {
27662
+ const errorDetail = result && typeof result === "object" && "error" in result ? String(result.error ?? "unknown error") : `invalid result: ${JSON.stringify(result)}`;
27663
+ log.error(
27664
+ `api.resetSession failed for ${sessionKey}: ${errorDetail}`
27665
+ );
27666
+ }
27667
+ } else {
27668
+ log.error(
27669
+ `api.resetSession not available \u2014 compaction reset requires OC fork with PR #29985. Session ${sessionKey} will continue without reset.`
27670
+ );
27671
+ }
27482
27672
  } catch (err) {
27483
- log.error("after_compaction hook failed", err);
27673
+ log.error("after_compaction reset failed", err);
27484
27674
  }
27485
27675
  }
27486
27676
  );
27487
27677
  async function ensureHourlySummaryCron(api2) {
27488
27678
  const jobId = "engram-hourly-summary";
27489
- const cronFilePath = path51.join(os5.homedir(), ".openclaw", "cron", "jobs.json");
27679
+ const cronFilePath = path51.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
27490
27680
  try {
27491
27681
  let jobsData = { version: 1, jobs: [] };
27492
27682
  try {
@@ -27533,9 +27723,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
27533
27723
  log.error("failed to auto-register hourly summary cron job:", err);
27534
27724
  }
27535
27725
  }
27536
- registerTools(api, orchestrator);
27537
- registerCli(api, orchestrator);
27538
- api.registerService({
27726
+ if (isFirstRegistration) {
27727
+ registerTools(api, orchestrator);
27728
+ registerCli(api, orchestrator);
27729
+ }
27730
+ if (isFirstRegistration) api.registerService({
27539
27731
  id: "openclaw-engram",
27540
27732
  start: async () => {
27541
27733
  log.info("initializing engram memory system...");
@@ -27554,6 +27746,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
27554
27746
  },
27555
27747
  stop: () => {
27556
27748
  globalThis[ENGRAM_REGISTERED_GUARD] = false;
27749
+ globalThis[ENGRAM_HOOK_APIS] = /* @__PURE__ */ new WeakSet();
27557
27750
  log.info("stopped");
27558
27751
  }
27559
27752
  });