@joshuaswarren/openclaw-engram 8.3.39 → 8.3.41

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
@@ -320,6 +320,8 @@ function parseConfig(raw) {
320
320
  autoPromoteToSharedEnabled: cfg.autoPromoteToSharedEnabled === true,
321
321
  autoPromoteToSharedCategories: Array.isArray(cfg.autoPromoteToSharedCategories) ? cfg.autoPromoteToSharedCategories.filter((c) => c === "correction" || c === "decision" || c === "preference") : ["correction", "decision", "preference"],
322
322
  autoPromoteMinConfidenceTier: cfg.autoPromoteMinConfidenceTier === "explicit" ? "explicit" : cfg.autoPromoteMinConfidenceTier === "implied" ? "implied" : "explicit",
323
+ routingRulesEnabled: cfg.routingRulesEnabled === true,
324
+ routingRulesStateFile: typeof cfg.routingRulesStateFile === "string" && cfg.routingRulesStateFile.trim().length > 0 ? cfg.routingRulesStateFile.trim() : "state/routing-rules.json",
323
325
  // v4.0 shared-context (default off)
324
326
  sharedContextEnabled: cfg.sharedContextEnabled === true,
325
327
  sharedContextDir: typeof cfg.sharedContextDir === "string" && cfg.sharedContextDir.length > 0 ? cfg.sharedContextDir : void 0,
@@ -17301,8 +17303,8 @@ mistakes: ${res.mistakesCount} patterns`
17301
17303
  }
17302
17304
 
17303
17305
  // src/cli.ts
17304
- import path39 from "path";
17305
- import { access as access2, readFile as readFile28, readdir as readdir17, unlink as unlink5 } from "fs/promises";
17306
+ import path40 from "path";
17307
+ import { access as access2, readFile as readFile29, readdir as readdir17, unlink as unlink5 } from "fs/promises";
17306
17308
 
17307
17309
  // src/transfer/export-json.ts
17308
17310
  import path27 from "path";
@@ -18181,8 +18183,8 @@ function gatherCandidates(input, warnings) {
18181
18183
  const record = rec;
18182
18184
  const content = typeof record.content === "string" ? record.content : null;
18183
18185
  if (!content) continue;
18184
- const path41 = typeof record.path === "string" ? record.path : "";
18185
- if (!path41.startsWith("transcripts/") && !path41.includes("/transcripts/")) continue;
18186
+ const path42 = typeof record.path === "string" ? record.path : "";
18187
+ if (!path42.startsWith("transcripts/") && !path42.includes("/transcripts/")) continue;
18186
18188
  rows.push(...parseJsonl(content, warnings));
18187
18189
  }
18188
18190
  return rows;
@@ -18650,6 +18652,359 @@ async function migrateObservations(options) {
18650
18652
  };
18651
18653
  }
18652
18654
 
18655
+ // src/routing/engine.ts
18656
+ var DEFAULT_CATEGORIES = [
18657
+ "fact",
18658
+ "preference",
18659
+ "correction",
18660
+ "entity",
18661
+ "decision",
18662
+ "relationship",
18663
+ "principle",
18664
+ "commitment",
18665
+ "moment",
18666
+ "skill"
18667
+ ];
18668
+ function normalizeNamespace(namespace) {
18669
+ return namespace.trim();
18670
+ }
18671
+ function isLikelyUnsafeRegex(pattern) {
18672
+ const value = pattern.trim();
18673
+ if (value.length === 0) return true;
18674
+ if (value.length > 120) return true;
18675
+ if (/\\[1-9]/.test(value)) return true;
18676
+ if (/\(\?<?[=!]/.test(value)) return true;
18677
+ if (/\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)[+*{]/.test(value)) return true;
18678
+ if (/(^|[^\\])[()|]/.test(value)) return true;
18679
+ const quantifierCount = (value.match(/(^|[^\\])[*+?]/g)?.length ?? 0) + (value.match(/(^|[^\\])\{/g)?.length ?? 0);
18680
+ if (quantifierCount > 1) return true;
18681
+ return false;
18682
+ }
18683
+ function isSafeRouteNamespace(namespace) {
18684
+ const value = normalizeNamespace(namespace);
18685
+ if (value.length === 0) return false;
18686
+ if (value === ".") return false;
18687
+ if (value.includes("/") || value.includes("\\")) return false;
18688
+ if (value.includes("..")) return false;
18689
+ return /^[A-Za-z0-9._-]{1,64}$/.test(value);
18690
+ }
18691
+ function validateRouteTarget(target, options) {
18692
+ if (!target || typeof target !== "object") {
18693
+ return { ok: false, error: "target must be an object" };
18694
+ }
18695
+ const allowedCategories = new Set(options?.allowedCategories ?? DEFAULT_CATEGORIES);
18696
+ const allowedNamespaces = options?.allowedNamespaces ? new Set(options.allowedNamespaces.map((v) => v.trim()).filter((v) => v.length > 0)) : null;
18697
+ const normalized = {};
18698
+ if (typeof target.category === "string") {
18699
+ if (!allowedCategories.has(target.category)) {
18700
+ return { ok: false, error: `invalid category: ${target.category}` };
18701
+ }
18702
+ normalized.category = target.category;
18703
+ }
18704
+ if (typeof target.namespace === "string") {
18705
+ const namespace = normalizeNamespace(target.namespace);
18706
+ if (!isSafeRouteNamespace(namespace)) {
18707
+ return { ok: false, error: `invalid namespace: ${target.namespace}` };
18708
+ }
18709
+ if (allowedNamespaces && !allowedNamespaces.has(namespace)) {
18710
+ return { ok: false, error: `namespace not allowed: ${namespace}` };
18711
+ }
18712
+ normalized.namespace = namespace;
18713
+ }
18714
+ if (!normalized.category && !normalized.namespace) {
18715
+ return { ok: false, error: "target must include category or namespace" };
18716
+ }
18717
+ return { ok: true, target: normalized };
18718
+ }
18719
+ function doesRuleMatch(rule, text) {
18720
+ if (!rule || typeof rule !== "object") return false;
18721
+ if (rule.enabled === false) return false;
18722
+ if (typeof rule.pattern !== "string") return false;
18723
+ const pattern = rule.pattern.trim();
18724
+ if (pattern.length === 0) return false;
18725
+ if (rule.patternType === "keyword") {
18726
+ return text.toLowerCase().includes(pattern.toLowerCase());
18727
+ }
18728
+ if (rule.patternType !== "regex") {
18729
+ return false;
18730
+ }
18731
+ if (isLikelyUnsafeRegex(pattern)) {
18732
+ return false;
18733
+ }
18734
+ try {
18735
+ return new RegExp(pattern, "i").test(text);
18736
+ } catch {
18737
+ return false;
18738
+ }
18739
+ }
18740
+ function selectRouteRule(text, rules, options) {
18741
+ const ranked = rules.map((rule, index) => ({ rule, index })).sort((a, b) => {
18742
+ if (b.rule.priority !== a.rule.priority) return b.rule.priority - a.rule.priority;
18743
+ return a.index - b.index;
18744
+ });
18745
+ for (const entry of ranked) {
18746
+ if (!doesRuleMatch(entry.rule, text)) continue;
18747
+ const validation = validateRouteTarget(entry.rule.target, options);
18748
+ if (!validation.ok || !validation.target) continue;
18749
+ return {
18750
+ rule: entry.rule,
18751
+ target: validation.target
18752
+ };
18753
+ }
18754
+ return null;
18755
+ }
18756
+
18757
+ // src/routing/store.ts
18758
+ import { lstat, mkdir as mkdir28, readFile as readFile28, realpath, rename as rename2, rm as rm4, stat as stat7, writeFile as writeFile25 } from "fs/promises";
18759
+ import path39 from "path";
18760
+ import { createHash as createHash7 } from "crypto";
18761
+ function defaultState() {
18762
+ return {
18763
+ version: 1,
18764
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
18765
+ rules: []
18766
+ };
18767
+ }
18768
+ function stableRuleId(rule) {
18769
+ const seed = JSON.stringify({
18770
+ patternType: rule.patternType,
18771
+ pattern: rule.pattern.trim(),
18772
+ priority: rule.priority,
18773
+ target: rule.target
18774
+ });
18775
+ return `route-${createHash7("sha256").update(seed).digest("hex").slice(0, 12)}`;
18776
+ }
18777
+ function resolveStatePath(memoryDir, stateFile) {
18778
+ const root = path39.resolve(memoryDir);
18779
+ const defaultPath = path39.join(root, "state", "routing-rules.json");
18780
+ if (path39.isAbsolute(stateFile)) {
18781
+ const absolute = path39.resolve(stateFile);
18782
+ return absolute.startsWith(root + path39.sep) ? absolute : defaultPath;
18783
+ }
18784
+ const resolved = path39.resolve(root, stateFile);
18785
+ return resolved.startsWith(root + path39.sep) ? resolved : defaultPath;
18786
+ }
18787
+ function normalizeRule(rule, options) {
18788
+ if (!rule || typeof rule !== "object") return null;
18789
+ if (rule.enabled === false) return null;
18790
+ if (rule.patternType !== "keyword" && rule.patternType !== "regex") return null;
18791
+ if (typeof rule.pattern !== "string" || rule.pattern.trim().length === 0) return null;
18792
+ if (typeof rule.priority !== "number" || !Number.isFinite(rule.priority)) return null;
18793
+ const targetValidation = validateRouteTarget(rule.target, options);
18794
+ if (!targetValidation.ok || !targetValidation.target) return null;
18795
+ const normalizedPriority = Math.trunc(rule.priority);
18796
+ const normalizedTarget = targetValidation.target;
18797
+ const id = typeof rule.id === "string" && rule.id.trim().length > 0 ? rule.id.trim() : stableRuleId({
18798
+ patternType: rule.patternType,
18799
+ pattern: rule.pattern.trim(),
18800
+ priority: normalizedPriority,
18801
+ target: normalizedTarget
18802
+ });
18803
+ return {
18804
+ id,
18805
+ patternType: rule.patternType,
18806
+ pattern: rule.pattern.trim(),
18807
+ priority: normalizedPriority,
18808
+ target: normalizedTarget,
18809
+ enabled: true
18810
+ };
18811
+ }
18812
+ var RoutingRulesStore = class {
18813
+ memoryRoot;
18814
+ statePath;
18815
+ lockPath;
18816
+ writeQueue = Promise.resolve();
18817
+ constructor(memoryDir, stateFile = "state/routing-rules.json") {
18818
+ this.memoryRoot = path39.resolve(memoryDir);
18819
+ this.statePath = resolveStatePath(memoryDir, stateFile);
18820
+ this.lockPath = `${this.statePath}.lock`;
18821
+ }
18822
+ async read(options) {
18823
+ try {
18824
+ const persisted = await this.readPersistedRules();
18825
+ return persisted.map((rule) => normalizeRule(rule, options)).filter((rule) => rule !== null);
18826
+ } catch {
18827
+ return [];
18828
+ }
18829
+ }
18830
+ async write(rules, options) {
18831
+ return this.withWriteLock(async () => this.writeNormalized(rules, options));
18832
+ }
18833
+ async upsert(rule, options) {
18834
+ return this.withWriteLock(async () => {
18835
+ const existing = await this.readPersistedRules();
18836
+ const normalized = normalizeRule(rule, options);
18837
+ if (!normalized) return existing;
18838
+ const next = existing.filter((entry) => entry.id !== normalized.id);
18839
+ next.push(normalized);
18840
+ return this.writeNormalized(next);
18841
+ });
18842
+ }
18843
+ async removeByPattern(pattern) {
18844
+ return this.withWriteLock(async () => {
18845
+ const trimmed = pattern.trim();
18846
+ const existing = await this.readPersistedRules();
18847
+ const next = existing.filter((entry) => entry.pattern !== trimmed);
18848
+ if (next.length === existing.length) return existing;
18849
+ return this.writeNormalized(next);
18850
+ });
18851
+ }
18852
+ async reset() {
18853
+ await this.withWriteLock(async () => {
18854
+ const payload = defaultState();
18855
+ await this.assertStatePathScoped();
18856
+ await writeFile25(this.statePath, JSON.stringify(payload, null, 2), "utf-8");
18857
+ });
18858
+ }
18859
+ dedupeById(rules) {
18860
+ const byId = /* @__PURE__ */ new Map();
18861
+ for (const rule of rules) {
18862
+ byId.set(rule.id, rule);
18863
+ }
18864
+ return Array.from(byId.values());
18865
+ }
18866
+ async readPersistedRules() {
18867
+ try {
18868
+ await this.assertStatePathScoped();
18869
+ const raw = await readFile28(this.statePath, "utf-8");
18870
+ const parsed = JSON.parse(raw);
18871
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.rules)) return [];
18872
+ const normalized = parsed.rules.map((rule) => normalizeRule(rule)).filter((rule) => rule !== null);
18873
+ return this.dedupeById(normalized);
18874
+ } catch {
18875
+ return [];
18876
+ }
18877
+ }
18878
+ async writeNormalized(rules, options) {
18879
+ const normalized = this.dedupeById(
18880
+ rules.map((rule) => normalizeRule(rule, options)).filter((rule) => rule !== null)
18881
+ );
18882
+ const payload = {
18883
+ version: 1,
18884
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
18885
+ rules: normalized
18886
+ };
18887
+ const tmpPath = `${this.statePath}.tmp-${process.pid}-${Date.now()}`;
18888
+ try {
18889
+ await this.assertStatePathScoped();
18890
+ await writeFile25(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
18891
+ await rename2(tmpPath, this.statePath);
18892
+ } catch (err) {
18893
+ log.debug(`routing rules write failed: ${err}`);
18894
+ throw err;
18895
+ } finally {
18896
+ await rm4(tmpPath, { force: true }).catch(() => {
18897
+ });
18898
+ }
18899
+ return normalized;
18900
+ }
18901
+ async withWriteLock(op) {
18902
+ const previous = this.writeQueue;
18903
+ let release = () => {
18904
+ };
18905
+ this.writeQueue = new Promise((resolve) => {
18906
+ release = resolve;
18907
+ });
18908
+ await previous;
18909
+ let unlock = null;
18910
+ try {
18911
+ unlock = await this.acquireFileLock();
18912
+ return await op();
18913
+ } finally {
18914
+ if (unlock) await unlock();
18915
+ release();
18916
+ }
18917
+ }
18918
+ async acquireFileLock() {
18919
+ const start = Date.now();
18920
+ const staleMs = 3e4;
18921
+ const timeoutMs = 5e3;
18922
+ let unexpectedLockError = null;
18923
+ await this.assertStatePathScoped();
18924
+ await mkdir28(path39.dirname(this.lockPath), { recursive: true });
18925
+ while (Date.now() - start < timeoutMs) {
18926
+ try {
18927
+ await mkdir28(this.lockPath);
18928
+ return async () => {
18929
+ try {
18930
+ await rm4(this.lockPath, { recursive: true, force: true });
18931
+ } catch {
18932
+ }
18933
+ };
18934
+ } catch (err) {
18935
+ const code = err.code;
18936
+ if (code !== "EEXIST") {
18937
+ unexpectedLockError = err;
18938
+ break;
18939
+ }
18940
+ try {
18941
+ const lockStat = await stat7(this.lockPath);
18942
+ if (Date.now() - lockStat.mtimeMs > staleMs) {
18943
+ await rm4(this.lockPath, { recursive: true, force: true });
18944
+ continue;
18945
+ }
18946
+ } catch {
18947
+ }
18948
+ await new Promise((resolve) => setTimeout(resolve, 25));
18949
+ }
18950
+ }
18951
+ if (unexpectedLockError) {
18952
+ throw unexpectedLockError;
18953
+ }
18954
+ throw new Error(`routing rules lock acquisition timed out after ${timeoutMs}ms`);
18955
+ }
18956
+ async assertStatePathScoped() {
18957
+ await mkdir28(this.memoryRoot, { recursive: true });
18958
+ const canonicalRoot = await realpath(this.memoryRoot);
18959
+ const canonicalParent = await this.canonicalizePathWithoutCreating(path39.dirname(this.statePath));
18960
+ const canonicalStatePath = path39.join(canonicalParent, path39.basename(this.statePath));
18961
+ if (!this.isPathInside(canonicalRoot, canonicalStatePath)) {
18962
+ throw new Error(`routing rules state path escaped memoryDir: ${canonicalStatePath}`);
18963
+ }
18964
+ await mkdir28(path39.dirname(this.statePath), { recursive: true });
18965
+ try {
18966
+ const stateStats = await lstat(this.statePath);
18967
+ if (stateStats.isSymbolicLink()) {
18968
+ const canonicalFile = await realpath(this.statePath);
18969
+ if (!this.isPathInside(canonicalRoot, canonicalFile)) {
18970
+ throw new Error(`routing rules state symlink escaped memoryDir: ${canonicalFile}`);
18971
+ }
18972
+ }
18973
+ } catch (err) {
18974
+ const code = err.code;
18975
+ if (code !== "ENOENT") {
18976
+ throw err;
18977
+ }
18978
+ }
18979
+ }
18980
+ isPathInside(root, candidate) {
18981
+ const normalizedRoot = path39.resolve(root);
18982
+ const normalizedCandidate = path39.resolve(candidate);
18983
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}${path39.sep}`);
18984
+ }
18985
+ async canonicalizePathWithoutCreating(targetPath) {
18986
+ const absoluteTarget = path39.resolve(targetPath);
18987
+ let probe = absoluteTarget;
18988
+ while (true) {
18989
+ try {
18990
+ const canonicalProbe = await realpath(probe);
18991
+ const remainder = path39.relative(probe, absoluteTarget);
18992
+ return path39.resolve(canonicalProbe, remainder);
18993
+ } catch (err) {
18994
+ const code = err.code;
18995
+ if (code !== "ENOENT") {
18996
+ throw err;
18997
+ }
18998
+ const parent = path39.dirname(probe);
18999
+ if (parent === probe) {
19000
+ return absoluteTarget;
19001
+ }
19002
+ probe = parent;
19003
+ }
19004
+ }
19005
+ }
19006
+ };
19007
+
18653
19008
  // src/cli.ts
18654
19009
  function rankCandidateForKeep(a, b) {
18655
19010
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -18699,6 +19054,37 @@ function planExactDuplicateDeletions(memories) {
18699
19054
  function planAggressiveDuplicateDeletions(memories) {
18700
19055
  return buildDedupePlan(memories, (memory) => normalizeAggressiveBody(memory.content));
18701
19056
  }
19057
+ function isRoutePatternType(value) {
19058
+ return value === "keyword" || value === "regex";
19059
+ }
19060
+ function parseRouteTargetCliArg(raw) {
19061
+ const trimmed = raw.trim();
19062
+ if (trimmed.length === 0) throw new Error("missing target");
19063
+ if (trimmed.startsWith("{")) {
19064
+ const parsed = JSON.parse(trimmed);
19065
+ if (!parsed || typeof parsed !== "object") throw new Error("invalid target JSON");
19066
+ return parsed;
19067
+ }
19068
+ const target = {};
19069
+ for (const token of trimmed.split(",")) {
19070
+ const part = token.trim();
19071
+ if (part.length === 0) continue;
19072
+ const normalized = part.replace(":", "=");
19073
+ const [rawKey, ...rawValueParts] = normalized.split("=");
19074
+ if (!rawKey || rawValueParts.length === 0) continue;
19075
+ const key = rawKey.trim().toLowerCase();
19076
+ const value = rawValueParts.join("=").trim();
19077
+ if (value.length === 0) continue;
19078
+ if (key === "category") {
19079
+ target.category = value;
19080
+ continue;
19081
+ }
19082
+ if (key === "namespace") {
19083
+ target.namespace = value;
19084
+ }
19085
+ }
19086
+ return target;
19087
+ }
18702
19088
  function normalizeNullableCliValue(value) {
18703
19089
  if (value === void 0) return void 0;
18704
19090
  const trimmed = value.trim();
@@ -18744,6 +19130,49 @@ async function runMigrateObservationsCliCommand(options) {
18744
19130
  now: options.now
18745
19131
  });
18746
19132
  }
19133
+ async function runRouteCliCommand(options) {
19134
+ const store = new RoutingRulesStore(options.memoryDir, options.stateFile);
19135
+ if (options.action === "list") {
19136
+ const rules = await store.read();
19137
+ return [...rules].sort((a, b) => {
19138
+ if (b.priority !== a.priority) return b.priority - a.priority;
19139
+ return a.pattern.localeCompare(b.pattern);
19140
+ });
19141
+ }
19142
+ if (options.action === "add") {
19143
+ const pattern = options.pattern?.trim();
19144
+ if (!pattern) throw new Error("missing pattern");
19145
+ if (!options.targetRaw || options.targetRaw.trim().length === 0) throw new Error("missing target");
19146
+ const patternType = options.patternType ?? "keyword";
19147
+ if (!isRoutePatternType(patternType)) throw new Error(`invalid route pattern type: ${patternType}`);
19148
+ const priority = options.priority ?? 0;
19149
+ if (!Number.isFinite(priority)) throw new Error("invalid priority");
19150
+ const target = parseRouteTargetCliArg(options.targetRaw);
19151
+ const validation = validateRouteTarget(target);
19152
+ if (!validation.ok || !validation.target) throw new Error(validation.error ?? "invalid target");
19153
+ const rule = {
19154
+ id: options.id?.trim() || "",
19155
+ patternType,
19156
+ pattern,
19157
+ priority: Math.trunc(priority),
19158
+ target: validation.target,
19159
+ enabled: true
19160
+ };
19161
+ return store.upsert(rule);
19162
+ }
19163
+ if (options.action === "remove") {
19164
+ const pattern = options.pattern?.trim();
19165
+ if (!pattern) throw new Error("missing pattern");
19166
+ return store.removeByPattern(pattern);
19167
+ }
19168
+ if (options.action === "test") {
19169
+ const text = options.text?.trim();
19170
+ if (!text) throw new Error("missing text");
19171
+ const rules = await store.read();
19172
+ return selectRouteRule(text, rules);
19173
+ }
19174
+ throw new Error(`unsupported route action: ${options.action}`);
19175
+ }
18747
19176
  async function runWorkTaskCliCommand(options) {
18748
19177
  const storage = new WorkStorage(options.memoryDir);
18749
19178
  if (options.action === "create") {
@@ -18881,7 +19310,7 @@ async function withTimeout(promise, timeoutMs, timeoutMessage) {
18881
19310
  }
18882
19311
  async function runReplayCliCommand(orchestrator, options) {
18883
19312
  const extractionIdleTimeoutMs = Number.isFinite(options.extractionIdleTimeoutMs) ? Math.max(1e3, Math.floor(options.extractionIdleTimeoutMs)) : 15 * 6e4;
18884
- const inputRaw = await readFile28(options.inputPath, "utf-8");
19313
+ const inputRaw = await readFile29(options.inputPath, "utf-8");
18885
19314
  const registry = buildReplayNormalizerRegistry([
18886
19315
  openclawReplayNormalizer,
18887
19316
  claudeReplayNormalizer,
@@ -18946,7 +19375,7 @@ async function runReplayCliCommand(orchestrator, options) {
18946
19375
  async function getPluginVersion() {
18947
19376
  try {
18948
19377
  const pkgPath = new URL("../package.json", import.meta.url);
18949
- const raw = await readFile28(pkgPath, "utf-8");
19378
+ const raw = await readFile29(pkgPath, "utf-8");
18950
19379
  const parsed = JSON.parse(raw);
18951
19380
  return parsed.version ?? "unknown";
18952
19381
  } catch {
@@ -18965,14 +19394,14 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace) {
18965
19394
  const ns = (namespace ?? "").trim();
18966
19395
  if (!ns) return orchestrator.config.memoryDir;
18967
19396
  if (!orchestrator.config.namespacesEnabled) return orchestrator.config.memoryDir;
18968
- const candidate = path39.join(orchestrator.config.memoryDir, "namespaces", ns);
19397
+ const candidate = path40.join(orchestrator.config.memoryDir, "namespaces", ns);
18969
19398
  if (ns === orchestrator.config.defaultNamespace) {
18970
19399
  return await exists2(candidate) ? candidate : orchestrator.config.memoryDir;
18971
19400
  }
18972
19401
  return candidate;
18973
19402
  }
18974
19403
  async function readAllMemoryFiles(memoryDir) {
18975
- const roots = [path39.join(memoryDir, "facts"), path39.join(memoryDir, "corrections")];
19404
+ const roots = [path40.join(memoryDir, "facts"), path40.join(memoryDir, "corrections")];
18976
19405
  const out = [];
18977
19406
  const walk = async (dir) => {
18978
19407
  let entries;
@@ -18983,14 +19412,14 @@ async function readAllMemoryFiles(memoryDir) {
18983
19412
  }
18984
19413
  for (const entry of entries) {
18985
19414
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
18986
- const fullPath = path39.join(dir, entryName);
19415
+ const fullPath = path40.join(dir, entryName);
18987
19416
  if (entry.isDirectory()) {
18988
19417
  await walk(fullPath);
18989
19418
  continue;
18990
19419
  }
18991
19420
  if (!entry.isFile() || !entryName.endsWith(".md")) continue;
18992
19421
  try {
18993
- const raw = await readFile28(fullPath, "utf-8");
19422
+ const raw = await readFile29(fullPath, "utf-8");
18994
19423
  const parsed = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
18995
19424
  if (!parsed) continue;
18996
19425
  const fmRaw = parsed[1];
@@ -19227,6 +19656,82 @@ function registerCli(api, orchestrator) {
19227
19656
  }
19228
19657
  console.log("OK");
19229
19658
  });
19659
+ const routeCmd = cmd.command("route").description("Manage custom memory routing rules");
19660
+ routeCmd.command("list").description("List configured routing rules").action(async () => {
19661
+ const rules = await runRouteCliCommand({
19662
+ action: "list",
19663
+ memoryDir: orchestrator.config.memoryDir,
19664
+ stateFile: orchestrator.config.routingRulesStateFile
19665
+ });
19666
+ if (rules.length === 0) {
19667
+ console.log("No routing rules configured.");
19668
+ return;
19669
+ }
19670
+ for (const rule of rules) {
19671
+ const targetParts = [
19672
+ rule.target.category ? `category=${rule.target.category}` : "",
19673
+ rule.target.namespace ? `namespace=${rule.target.namespace}` : ""
19674
+ ].filter((value) => value.length > 0);
19675
+ console.log(
19676
+ `${rule.id} type=${rule.patternType} priority=${rule.priority} pattern="${rule.pattern}" target=${targetParts.join(",")}`
19677
+ );
19678
+ }
19679
+ });
19680
+ routeCmd.command("add").description("Add or update a routing rule").argument("<pattern>", "Keyword or regex pattern").argument("<target>", "Target (JSON or category=<cat>,namespace=<ns>)").option("--type <type>", "Pattern type: keyword|regex", "keyword").option("--priority <n>", "Rule priority", "0").option("--id <id>", "Optional stable rule id").action(async (...args) => {
19681
+ const pattern = typeof args[0] === "string" ? args[0] : "";
19682
+ const targetRaw = typeof args[1] === "string" ? args[1] : "";
19683
+ const options = args[2] ?? {};
19684
+ const patternTypeRaw = typeof options.type === "string" ? options.type.trim().toLowerCase() : "keyword";
19685
+ if (!isRoutePatternType(patternTypeRaw)) {
19686
+ throw new Error(`invalid route pattern type: ${patternTypeRaw}`);
19687
+ }
19688
+ const priorityInput = String(options.priority ?? "0").trim();
19689
+ if (!/^-?\d+$/.test(priorityInput)) {
19690
+ throw new Error(`invalid route priority: ${priorityInput}`);
19691
+ }
19692
+ const priorityRaw = Number(priorityInput);
19693
+ const updated = await runRouteCliCommand({
19694
+ action: "add",
19695
+ memoryDir: orchestrator.config.memoryDir,
19696
+ stateFile: orchestrator.config.routingRulesStateFile,
19697
+ pattern,
19698
+ patternType: patternTypeRaw,
19699
+ priority: priorityRaw,
19700
+ targetRaw,
19701
+ id: typeof options.id === "string" ? options.id : void 0
19702
+ });
19703
+ console.log(`OK (${updated.length} rules)`);
19704
+ });
19705
+ routeCmd.command("remove").description("Remove routing rules by exact pattern").argument("<pattern>", "Pattern to remove").action(async (...args) => {
19706
+ const pattern = typeof args[0] === "string" ? args[0] : "";
19707
+ const next = await runRouteCliCommand({
19708
+ action: "remove",
19709
+ memoryDir: orchestrator.config.memoryDir,
19710
+ stateFile: orchestrator.config.routingRulesStateFile,
19711
+ pattern
19712
+ });
19713
+ console.log(`OK (${next.length} rules remain)`);
19714
+ });
19715
+ routeCmd.command("test").description("Test routing rule match for input text").argument("<text>", "Text to evaluate").action(async (...args) => {
19716
+ const text = typeof args[0] === "string" ? args[0] : "";
19717
+ const selection = await runRouteCliCommand({
19718
+ action: "test",
19719
+ memoryDir: orchestrator.config.memoryDir,
19720
+ stateFile: orchestrator.config.routingRulesStateFile,
19721
+ text
19722
+ });
19723
+ if (!selection) {
19724
+ console.log("No route match.");
19725
+ return;
19726
+ }
19727
+ const targetParts = [
19728
+ selection.target.category ? `category=${selection.target.category}` : "",
19729
+ selection.target.namespace ? `namespace=${selection.target.namespace}` : ""
19730
+ ].filter((value) => value.length > 0);
19731
+ console.log(
19732
+ `Matched ${selection.rule.id} type=${selection.rule.patternType} priority=${selection.rule.priority} target=${targetParts.join(",")}`
19733
+ );
19734
+ });
19230
19735
  cmd.command("archive-observations").description("Archive aged observation artifacts (dry-run by default)").option("--retention-days <n>", "Archive files older than N days", "30").option("--write", "Apply archive mutations (default: dry-run)").action(async (...args) => {
19231
19736
  const options = args[0] ?? {};
19232
19737
  const retentionDays = parseInt(String(options.retentionDays ?? "30"), 10);
@@ -19603,7 +20108,7 @@ function registerCli(api, orchestrator) {
19603
20108
  }
19604
20109
  });
19605
20110
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
19606
- const workspaceDir = path39.join(process.env.HOME ?? "~", ".openclaw", "workspace");
20111
+ const workspaceDir = path40.join(process.env.HOME ?? "~", ".openclaw", "workspace");
19607
20112
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
19608
20113
  if (!identity) {
19609
20114
  console.log("No identity file found.");
@@ -19826,8 +20331,8 @@ function registerCli(api, orchestrator) {
19826
20331
  const options = args[0] ?? {};
19827
20332
  const threadId = options.thread;
19828
20333
  const top = parseInt(options.top ?? "10", 10);
19829
- const memoryDir = path39.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
19830
- const threading = new ThreadingManager(path39.join(memoryDir, "threads"));
20334
+ const memoryDir = path40.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
20335
+ const threading = new ThreadingManager(path40.join(memoryDir, "threads"));
19831
20336
  if (threadId) {
19832
20337
  const thread = await threading.loadThread(threadId);
19833
20338
  if (!thread) {
@@ -20000,16 +20505,16 @@ function parseDuration(duration) {
20000
20505
  }
20001
20506
 
20002
20507
  // src/index.ts
20003
- import { readFile as readFile29, writeFile as writeFile25 } from "fs/promises";
20508
+ import { readFile as readFile30, writeFile as writeFile26 } from "fs/promises";
20004
20509
  import { readFileSync as readFileSync4 } from "fs";
20005
- import path40 from "path";
20510
+ import path41 from "path";
20006
20511
  import os5 from "os";
20007
20512
  var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
20008
20513
  function loadPluginConfigFromFile() {
20009
20514
  try {
20010
20515
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
20011
20516
  const homeDir = process.env.HOME ?? os5.homedir();
20012
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path40.join(homeDir, ".openclaw", "openclaw.json");
20517
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path41.join(homeDir, ".openclaw", "openclaw.json");
20013
20518
  const content = readFileSync4(configPath, "utf-8");
20014
20519
  const config = JSON.parse(content);
20015
20520
  const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
@@ -20217,11 +20722,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
20217
20722
  );
20218
20723
  async function ensureHourlySummaryCron(api2) {
20219
20724
  const jobId = "engram-hourly-summary";
20220
- const cronFilePath = path40.join(os5.homedir(), ".openclaw", "cron", "jobs.json");
20725
+ const cronFilePath = path41.join(os5.homedir(), ".openclaw", "cron", "jobs.json");
20221
20726
  try {
20222
20727
  let jobsData = { version: 1, jobs: [] };
20223
20728
  try {
20224
- const content = await readFile29(cronFilePath, "utf-8");
20729
+ const content = await readFile30(cronFilePath, "utf-8");
20225
20730
  jobsData = JSON.parse(content);
20226
20731
  } catch {
20227
20732
  }
@@ -20258,7 +20763,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
20258
20763
  state: {}
20259
20764
  };
20260
20765
  jobsData.jobs.push(newJob);
20261
- await writeFile25(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
20766
+ await writeFile26(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
20262
20767
  log.info("auto-registered hourly summary cron job");
20263
20768
  } catch (err) {
20264
20769
  log.error("failed to auto-register hourly summary cron job:", err);