@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 +524 -19
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +10 -0
- package/package.json +1 -1
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
|
|
17305
|
-
import { access as access2, readFile as
|
|
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
|
|
18185
|
-
if (!
|
|
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
|
|
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
|
|
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 =
|
|
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 = [
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
19830
|
-
const threading = new ThreadingManager(
|
|
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
|
|
20508
|
+
import { readFile as readFile30, writeFile as writeFile26 } from "fs/promises";
|
|
20004
20509
|
import { readFileSync as readFileSync4 } from "fs";
|
|
20005
|
-
import
|
|
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 :
|
|
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 =
|
|
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
|
|
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
|
|
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);
|