@remnic/plugin-openclaw 1.0.10 → 1.0.11

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.
Files changed (69) hide show
  1. package/dist/{calibration-674TDQNV.js → calibration-WCHOK6DX.js} +12 -4
  2. package/dist/capsule-cli-TFKLAG3S.js +329 -0
  3. package/dist/capsule-crypto-K3IRTKRH.js +17 -0
  4. package/dist/capsule-export-CVA3CKUQ.js +265 -0
  5. package/dist/capsule-import-CFX7BY5W.js +16 -0
  6. package/dist/capsule-merge-7RVOHJK3.js +189 -0
  7. package/dist/{causal-chain-OKDZSDEB.js → causal-chain-WYN5QOPS.js} +3 -2
  8. package/dist/{causal-consolidation-5BEXLQV5.js → causal-consolidation-JD6KJJH6.js} +16 -12
  9. package/dist/{causal-retrieval-3BKBXVXD.js → causal-retrieval-NZHQOZOE.js} +6 -5
  10. package/dist/{causal-trajectory-graph-RQIT37DN.js → causal-trajectory-graph-VBPE2WPM.js} +1 -1
  11. package/dist/chunk-37NKFWSO.js +233 -0
  12. package/dist/chunk-3G7FAF6S.js +60 -0
  13. package/dist/{chunk-Z7GRLVK3.js → chunk-3GUF7RQI.js} +235 -19
  14. package/dist/chunk-3I7RHWYT.js +214 -0
  15. package/dist/chunk-4G2XCSD2.js +186 -0
  16. package/dist/chunk-6IWEAUN6.js +148 -0
  17. package/dist/{chunk-LN5UZQVG.js → chunk-6UFI73TJ.js} +5 -3
  18. package/dist/chunk-7OQEPGQF.js +529 -0
  19. package/dist/chunk-B52XADV3.js +244 -0
  20. package/dist/chunk-BU5KJVWF.js +78 -0
  21. package/dist/chunk-CXM7EBAO.js +289 -0
  22. package/dist/chunk-ETJZRIAM.js +227 -0
  23. package/dist/chunk-FQRSVYY4.js +110 -0
  24. package/dist/chunk-HRGFO6AW.js +349 -0
  25. package/dist/chunk-I6B2W2IY.js +47 -0
  26. package/dist/chunk-JZBOXOUC.js +259 -0
  27. package/dist/chunk-K7EUBNDD.js +185 -0
  28. package/dist/chunk-L4PRBB2A.js +1860 -0
  29. package/dist/chunk-MBIFE6SA.js +250 -0
  30. package/dist/chunk-N7EOZY6F.js +400 -0
  31. package/dist/chunk-NKVIN6RD.js +118 -0
  32. package/dist/chunk-OEI7GLV2.js +17 -0
  33. package/dist/{chunk-S2ISS4AH.js → chunk-P3DIW2SD.js} +10 -10
  34. package/dist/{chunk-7TENHBV2.js → chunk-RQCTMECT.js} +10 -48
  35. package/dist/chunk-SSFTU6LP.js +182 -0
  36. package/dist/{chunk-BXTMZDRT.js → chunk-SVSQAG6M.js} +7 -5
  37. package/dist/chunk-TLVIQLB4.js +874 -0
  38. package/dist/{chunk-JJSNPSCD.js → chunk-TNH24SF6.js} +352 -50
  39. package/dist/chunk-TVKKIS53.js +720 -0
  40. package/dist/{chunk-YHH3SXKD.js → chunk-WPINX4MF.js} +1 -59
  41. package/dist/{chunk-HCFFXBLV.js → chunk-XMSDA5WA.js} +5 -1861
  42. package/dist/chunk-YGGGUTG3.js +125 -0
  43. package/dist/chunk-YGXXBRV7.js +10 -0
  44. package/dist/cipher-VHAFCG7Z.js +27 -0
  45. package/dist/dreams-ledger-3I52ISYR.js +285 -0
  46. package/dist/{engine-65C2J63X.js → engine-VMTFKFGO.js} +5 -2
  47. package/dist/{fallback-llm-LVK5PDIM.js → fallback-llm-WCWNGIQ3.js} +2 -1
  48. package/dist/first-start-migration-I24M2JEE.js +258 -0
  49. package/dist/forget-NI4RBDPB.js +68 -0
  50. package/dist/fs-utils-PZRI2HDZ.js +29 -0
  51. package/dist/graph-edge-decay-5CVKWBYH.js +203 -0
  52. package/dist/index.js +9775 -2900
  53. package/dist/kdf-H5B23ZM2.js +25 -0
  54. package/dist/memory-governance-DWGFV4FX.js +25 -0
  55. package/dist/metadata-JAGIWHEA.js +20 -0
  56. package/dist/migrate-from-identity-anchor-N3354WMP.js +7 -0
  57. package/dist/path-5LCUBAAZ.js +8 -0
  58. package/dist/peers-JF2I6RCR.js +43 -0
  59. package/dist/purge-XN2VSPZ2.js +204 -0
  60. package/dist/secure-store-FWJ7LBPH.js +149 -0
  61. package/dist/state-PVISYXRH.js +7 -0
  62. package/dist/state-store-LP5BO6SF.js +15 -0
  63. package/dist/{storage-DM4ZGOCN.js → storage-T2OGFUF4.js} +3 -1
  64. package/dist/tier-stats-IZNW66NC.js +147 -0
  65. package/dist/trace-NJESSGH7.js +289 -0
  66. package/dist/tui-MGK2LYJY.js +12 -0
  67. package/dist/types-H5R5D3WF.js +30 -0
  68. package/openclaw.plugin.json +519 -4
  69. package/package.json +1 -1
@@ -0,0 +1,227 @@
1
+ import {
2
+ parseExportBundle
3
+ } from "./chunk-K7EUBNDD.js";
4
+ import {
5
+ assertIsDirectoryNotSymlink,
6
+ assertRealpathInsideRoot,
7
+ fromPosixRelPath,
8
+ isPathInsideRoot,
9
+ sha256String
10
+ } from "./chunk-NKVIN6RD.js";
11
+ import {
12
+ decryptCapsuleFileInMemory,
13
+ isEncryptedCapsuleFile
14
+ } from "./chunk-SSFTU6LP.js";
15
+ import {
16
+ createVersion
17
+ } from "./chunk-6OJAU466.js";
18
+
19
+ // ../remnic-core/src/transfer/capsule-import.ts
20
+ import { lstat, mkdir, readFile, realpath, stat, writeFile } from "fs/promises";
21
+ import path from "path";
22
+ import { createHash, randomUUID } from "crypto";
23
+ import { gunzipSync } from "zlib";
24
+ async function importCapsule(opts) {
25
+ const archiveAbs = path.resolve(opts.archivePath);
26
+ const rootAbs = path.resolve(opts.root);
27
+ await assertIsDirectoryNotSymlink(rootAbs, "importCapsule", "root");
28
+ const mode = opts.mode ?? "skip";
29
+ if (mode !== "skip" && mode !== "overwrite" && mode !== "fork") {
30
+ throw new Error(
31
+ `importCapsule: unknown mode ${JSON.stringify(mode)}; expected "skip", "overwrite", or "fork"`
32
+ );
33
+ }
34
+ const encrypted = await isEncryptedCapsuleFile(archiveAbs);
35
+ let raw;
36
+ if (encrypted) {
37
+ if (!opts.memoryDir) {
38
+ throw new Error(
39
+ `importCapsule: archive is encrypted but 'memoryDir' was not provided. Pass the memory directory so the secure-store key can be retrieved, or run \`remnic secure-store unlock\` before importing.`
40
+ );
41
+ }
42
+ raw = await decryptCapsuleFileInMemory(archiveAbs, opts.memoryDir);
43
+ } else {
44
+ raw = await readFile(archiveAbs);
45
+ }
46
+ const json = gunzipSync(raw).toString("utf-8");
47
+ let parsedJson;
48
+ try {
49
+ parsedJson = JSON.parse(json);
50
+ } catch (cause) {
51
+ throw new Error(
52
+ `importCapsule: archive is not valid JSON after gunzip: ${archiveAbs}`,
53
+ { cause }
54
+ );
55
+ }
56
+ const parsed = parseExportBundle(parsedJson);
57
+ if (parsed.capsuleVersion !== 2) {
58
+ throw new Error(
59
+ "importCapsule: archive is V1; only V2 capsule archives are supported"
60
+ );
61
+ }
62
+ const bundle = parsed.bundle;
63
+ const manifest = bundle.manifest;
64
+ const capsule = manifest.capsule;
65
+ const manifestIndex = /* @__PURE__ */ new Map();
66
+ for (const f of manifest.files) {
67
+ manifestIndex.set(f.path, f);
68
+ }
69
+ if (manifestIndex.size !== manifest.files.length) {
70
+ throw new Error(
71
+ "importCapsule: manifest contains duplicate file paths"
72
+ );
73
+ }
74
+ const recordPaths = /* @__PURE__ */ new Set();
75
+ for (const rec of bundle.records) {
76
+ if (recordPaths.has(rec.path)) {
77
+ throw new Error(
78
+ `importCapsule: bundle contains duplicate record path: ${rec.path}`
79
+ );
80
+ }
81
+ recordPaths.add(rec.path);
82
+ }
83
+ const rootReal = await realpath(rootAbs).catch(() => rootAbs);
84
+ const seenTargetPaths = /* @__PURE__ */ new Map();
85
+ for (const rec of bundle.records) {
86
+ const entry = manifestIndex.get(rec.path);
87
+ if (!entry) {
88
+ throw new Error(
89
+ `importCapsule: archive checksum mismatch (record without manifest entry: ${rec.path})`
90
+ );
91
+ }
92
+ const { sha256, bytes } = sha256String(rec.content);
93
+ if (sha256 !== entry.sha256 || bytes !== entry.bytes) {
94
+ throw new Error(
95
+ `importCapsule: archive checksum mismatch for ${rec.path}: expected sha256=${entry.sha256} bytes=${entry.bytes}, got sha256=${sha256} bytes=${bytes}`
96
+ );
97
+ }
98
+ if (rec.path.includes("\\")) {
99
+ throw new Error(
100
+ `importCapsule: record path contains backslash separators (Windows-style paths are not allowed): ${rec.path}`
101
+ );
102
+ }
103
+ const posixNormalized = path.posix.normalize(rec.path);
104
+ if (rec.path.startsWith("/") || rec.path.split("/").some((seg) => seg === "..") || posixNormalized.startsWith("..") || posixNormalized.startsWith("/")) {
105
+ throw new Error(
106
+ `importCapsule: record path escapes target root: ${rec.path}`
107
+ );
108
+ }
109
+ const targetRel = computeTargetPath(rec.path, mode, capsule.id);
110
+ const targetAbs = path.join(rootReal, fromPosixRelPath(targetRel));
111
+ if (!isPathInsideRoot(rootReal, targetAbs)) {
112
+ throw new Error(
113
+ `importCapsule: record path escapes target root: ${rec.path}`
114
+ );
115
+ }
116
+ await assertRealpathInsideRoot(rootReal, targetAbs, rec.path, "importCapsule");
117
+ const targetLstat = await lstat(targetAbs).catch(() => null);
118
+ if (targetLstat !== null && targetLstat.isSymbolicLink()) {
119
+ throw new Error(
120
+ `importCapsule: record target is a symlink and cannot be written to safely: ${rec.path}`
121
+ );
122
+ }
123
+ const dedupKey = targetAbs.toLowerCase();
124
+ const firstSourcePath = seenTargetPaths.get(dedupKey);
125
+ if (firstSourcePath !== void 0) {
126
+ throw new Error(
127
+ `importCapsule: manifest contains two entries that resolve to the same target path: "${firstSourcePath}" and "${rec.path}" both map to "${targetRel}"`
128
+ );
129
+ }
130
+ seenTargetPaths.set(dedupKey, rec.path);
131
+ }
132
+ for (const f of manifest.files) {
133
+ if (!recordPaths.has(f.path)) {
134
+ throw new Error(
135
+ `importCapsule: archive checksum mismatch (manifest entry without record: ${f.path})`
136
+ );
137
+ }
138
+ }
139
+ const imported = [];
140
+ const skipped = [];
141
+ const sortedRecords = [...bundle.records].sort(
142
+ (a, b) => a.path.localeCompare(b.path)
143
+ );
144
+ for (const rec of sortedRecords) {
145
+ const targetRel = computeTargetPath(rec.path, mode, capsule.id);
146
+ const targetAbs = path.join(rootReal, fromPosixRelPath(targetRel));
147
+ const exists = await fileExistsAt(targetAbs);
148
+ if ((mode === "skip" || mode === "fork") && exists) {
149
+ skipped.push({ path: rec.path, reason: "exists" });
150
+ continue;
151
+ }
152
+ let snapshotted = false;
153
+ if (mode === "overwrite" && exists) {
154
+ if (opts.versioning && opts.versioning.enabled) {
155
+ const prior = await readFile(targetAbs, "utf-8").catch(() => "");
156
+ await createVersion(
157
+ targetAbs,
158
+ prior,
159
+ "manual",
160
+ opts.versioning,
161
+ opts.log,
162
+ `capsule-import: ${capsule.id}`,
163
+ rootReal
164
+ );
165
+ snapshotted = true;
166
+ }
167
+ }
168
+ let contentToWrite = rec.content;
169
+ let rewroteId = false;
170
+ if (mode === "fork") {
171
+ const forked = rewriteFrontmatterIdForFork(rec.content, capsule.id, opts.now);
172
+ contentToWrite = forked.content;
173
+ rewroteId = forked.rewrote;
174
+ }
175
+ await mkdir(path.dirname(targetAbs), { recursive: true });
176
+ await writeFile(targetAbs, contentToWrite, "utf-8");
177
+ imported.push({
178
+ sourcePath: rec.path,
179
+ targetPath: targetRel,
180
+ snapshotted,
181
+ rewroteId
182
+ });
183
+ }
184
+ skipped.sort((a, b) => a.path.localeCompare(b.path));
185
+ return { imported, skipped, manifest };
186
+ }
187
+ function computeTargetPath(sourcePosix, mode, capsuleId) {
188
+ if (mode === "fork") return `forks/${capsuleId}/${sourcePosix}`;
189
+ return sourcePosix;
190
+ }
191
+ async function fileExistsAt(absPath) {
192
+ const st = await stat(absPath).catch(() => null);
193
+ return st !== null && st.isFile();
194
+ }
195
+ function rewriteFrontmatterIdForFork(content, capsuleId, now) {
196
+ const crlfMatch = /^---(\r\n|\r|\n)/.exec(content);
197
+ const eol = crlfMatch ? crlfMatch[1] : "\n";
198
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(content);
199
+ if (!fmMatch) return { content, rewrote: false };
200
+ const fmBody = fmMatch[1];
201
+ const fmTrailer = fmMatch[2];
202
+ const fmStart = fmMatch.index;
203
+ const fmEnd = fmStart + fmMatch[0].length;
204
+ const idLineRe = /^id:[ \t]*([^\r\n]*)$/m;
205
+ const idMatch = idLineRe.exec(fmBody);
206
+ if (!idMatch) return { content, rewrote: false };
207
+ const newId = mintForkId(capsuleId, idMatch[1]?.trim() ?? "", now);
208
+ const replacedBody = fmBody.replace(idLineRe, `id: ${newId}`);
209
+ const prefix = content.slice(0, fmStart);
210
+ const tail = content.slice(fmEnd);
211
+ const rebuilt = `${prefix}---${eol}${replacedBody}${eol}---${fmTrailer}${tail}`;
212
+ return { content: rebuilt, rewrote: true };
213
+ }
214
+ function mintForkId(capsuleId, originalId, now) {
215
+ const base = originalId.length > 0 ? originalId : "fork";
216
+ if (now === void 0) {
217
+ const suffix2 = randomUUID().slice(0, 8);
218
+ return `${base}-fork-${capsuleId}-${suffix2}`;
219
+ }
220
+ const payload = JSON.stringify({ c: capsuleId, i: originalId, n: now });
221
+ const suffix = createHash("sha256").update(payload, "utf-8").digest("hex").slice(0, 8);
222
+ return `${base}-fork-${capsuleId}-${suffix}`;
223
+ }
224
+
225
+ export {
226
+ importCapsule
227
+ };
@@ -0,0 +1,110 @@
1
+ // ../remnic-core/src/routing/engine.ts
2
+ var DEFAULT_CATEGORIES = [
3
+ "fact",
4
+ "preference",
5
+ "correction",
6
+ "entity",
7
+ "decision",
8
+ "relationship",
9
+ "principle",
10
+ "commitment",
11
+ "moment",
12
+ "skill",
13
+ "rule",
14
+ "procedure",
15
+ "reasoning_trace"
16
+ ];
17
+ function normalizeNamespace(namespace) {
18
+ return namespace.trim();
19
+ }
20
+ function isLikelyUnsafeRegex(pattern) {
21
+ const value = pattern.trim();
22
+ if (value.length === 0) return true;
23
+ if (value.length > 120) return true;
24
+ if (/\\[1-9]/.test(value)) return true;
25
+ if (/\(\?<?[=!]/.test(value)) return true;
26
+ if (/\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)[+*{]/.test(value)) return true;
27
+ if (/(^|[^\\])[()|]/.test(value)) return true;
28
+ const quantifierCount = (value.match(/(^|[^\\])[*+?]/g)?.length ?? 0) + (value.match(/(^|[^\\])\{/g)?.length ?? 0);
29
+ if (quantifierCount > 1) return true;
30
+ return false;
31
+ }
32
+ function isSafeRouteNamespace(namespace) {
33
+ const value = normalizeNamespace(namespace);
34
+ if (value.length === 0) return false;
35
+ if (value === ".") return false;
36
+ if (value.includes("/") || value.includes("\\")) return false;
37
+ if (value.includes("..")) return false;
38
+ return /^[A-Za-z0-9._-]{1,64}$/.test(value);
39
+ }
40
+ function validateRouteTarget(target, options) {
41
+ if (!target || typeof target !== "object") {
42
+ return { ok: false, error: "target must be an object" };
43
+ }
44
+ const allowedCategories = new Set(options?.allowedCategories ?? DEFAULT_CATEGORIES);
45
+ const allowedNamespaces = options?.allowedNamespaces ? new Set(options.allowedNamespaces.map((v) => v.trim()).filter((v) => v.length > 0)) : null;
46
+ const normalized = {};
47
+ if (typeof target.category === "string") {
48
+ if (!allowedCategories.has(target.category)) {
49
+ return { ok: false, error: `invalid category: ${target.category}` };
50
+ }
51
+ normalized.category = target.category;
52
+ }
53
+ if (typeof target.namespace === "string") {
54
+ const namespace = normalizeNamespace(target.namespace);
55
+ if (!isSafeRouteNamespace(namespace)) {
56
+ return { ok: false, error: `invalid namespace: ${target.namespace}` };
57
+ }
58
+ if (allowedNamespaces && !allowedNamespaces.has(namespace)) {
59
+ return { ok: false, error: `namespace not allowed: ${namespace}` };
60
+ }
61
+ normalized.namespace = namespace;
62
+ }
63
+ if (!normalized.category && !normalized.namespace) {
64
+ return { ok: false, error: "target must include category or namespace" };
65
+ }
66
+ return { ok: true, target: normalized };
67
+ }
68
+ function doesRuleMatch(rule, text) {
69
+ if (!rule || typeof rule !== "object") return false;
70
+ if (rule.enabled === false) return false;
71
+ if (typeof rule.pattern !== "string") return false;
72
+ const pattern = rule.pattern.trim();
73
+ if (pattern.length === 0) return false;
74
+ if (rule.patternType === "keyword") {
75
+ return text.toLowerCase().includes(pattern.toLowerCase());
76
+ }
77
+ if (rule.patternType !== "regex") {
78
+ return false;
79
+ }
80
+ if (isLikelyUnsafeRegex(pattern)) {
81
+ return false;
82
+ }
83
+ try {
84
+ return new RegExp(pattern, "i").test(text);
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ function selectRouteRule(text, rules, options) {
90
+ const ranked = rules.map((rule, index) => ({ rule, index })).sort((a, b) => {
91
+ if (b.rule.priority !== a.rule.priority) return b.rule.priority - a.rule.priority;
92
+ return a.index - b.index;
93
+ });
94
+ for (const entry of ranked) {
95
+ if (!doesRuleMatch(entry.rule, text)) continue;
96
+ const validation = validateRouteTarget(entry.rule.target, options);
97
+ if (!validation.ok || !validation.target) continue;
98
+ return {
99
+ rule: entry.rule,
100
+ target: validation.target
101
+ };
102
+ }
103
+ return null;
104
+ }
105
+
106
+ export {
107
+ isSafeRouteNamespace,
108
+ validateRouteTarget,
109
+ selectRouteRule
110
+ };
@@ -0,0 +1,349 @@
1
+ import {
2
+ PEER_ID_PATTERN,
3
+ appendInteractionLog,
4
+ listPeers,
5
+ readPeerInteractionLog,
6
+ readPeerProfile,
7
+ writePeerProfile
8
+ } from "./chunk-TLVIQLB4.js";
9
+
10
+ // ../remnic-core/src/peers/profile-reasoner.ts
11
+ function buildPeerProfileReasonerPrompt(input) {
12
+ const existingFields = input.existingProfile ? Object.keys(input.existingProfile.fields) : [];
13
+ const logBlock = input.log.map((e) => {
14
+ const session = e.sessionId ? ` session=${e.sessionId}` : "";
15
+ return `- [${e.timestamp}] (${e.kind})${session} ${e.summary}`;
16
+ }).join("\n");
17
+ return [
18
+ `You are an async peer-profile reasoner. Your job is to read recent interaction-log entries for one peer and propose 0..${input.maxFields} profile-field updates.`,
19
+ "",
20
+ `Peer:`,
21
+ ` id: ${input.peer.id}`,
22
+ ` kind: ${input.peer.kind}`,
23
+ ` displayName: ${input.peer.displayName}`,
24
+ "",
25
+ `Existing profile field keys (preserve names when proposing updates that refine an existing field): ${existingFields.length > 0 ? existingFields.join(", ") : "(none yet)"}`,
26
+ "",
27
+ `Recent interaction log (oldest first):`,
28
+ logBlock.length > 0 ? logBlock : "(no entries)",
29
+ "",
30
+ `Output a single JSON object: {"proposals": [{"field": "<stable_key>", "value": "<markdown>", "signal": "<short_label>", "note": "<optional>", "sourceSessionId": "<optional>"}]}.`,
31
+ "",
32
+ `Rules:`,
33
+ `1. Only propose fields supported by evidence in the log. Do not invent.`,
34
+ `2. Keys are short snake_case (e.g. "communication_style", "tool_patterns").`,
35
+ `3. value is markdown. signal is a short label like "explicit_preference" or "topic_recurrence".`,
36
+ `4. Omit fields you can't justify. Empty proposals array is valid.`,
37
+ `5. Output JSON ONLY \u2014 no prose before or after.`
38
+ ].join("\n");
39
+ }
40
+ function parsePeerProfileReasonerResponse(raw) {
41
+ if (typeof raw !== "string" || raw.trim() === "") return [];
42
+ const trimmed = raw.trim();
43
+ const fenced = /^```(?:json)?\s*([\s\S]*?)```\s*$/u.exec(trimmed);
44
+ const payload = fenced ? fenced[1].trim() : trimmed;
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(payload);
48
+ } catch {
49
+ return [];
50
+ }
51
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
52
+ return [];
53
+ }
54
+ const obj = parsed;
55
+ if (!Array.isArray(obj.proposals)) return [];
56
+ const out = [];
57
+ const RESERVED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
58
+ for (const item of obj.proposals) {
59
+ if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
60
+ const r = item;
61
+ if (typeof r.field !== "string" || r.field.trim() === "") continue;
62
+ if (RESERVED_KEYS.has(r.field)) continue;
63
+ if (typeof r.value !== "string" || r.value.trim() === "") continue;
64
+ if (typeof r.signal !== "string" || r.signal.trim() === "") continue;
65
+ const proposal = {
66
+ field: r.field,
67
+ value: r.value,
68
+ signal: r.signal,
69
+ ...typeof r.note === "string" && r.note.length > 0 ? { note: r.note } : {},
70
+ ...typeof r.sourceSessionId === "string" && r.sourceSessionId.length > 0 ? { sourceSessionId: r.sourceSessionId } : {}
71
+ };
72
+ out.push(proposal);
73
+ }
74
+ return out;
75
+ }
76
+ var SYSTEM_MESSAGE = 'You are a peer-profile reasoner. Output ONLY a JSON object of the form {"proposals":[{"field":"...","value":"...","signal":"...","note":"...","sourceSessionId":"..."}]}. No prose, no fenced code block, no commentary.';
77
+ var RUN_MARKER_KIND = "peer_profile_reasoner_run";
78
+ function lastRunTimestamp(log) {
79
+ let latest;
80
+ for (const entry of log) {
81
+ if (entry.kind !== RUN_MARKER_KIND) continue;
82
+ if (latest === void 0 || entry.timestamp > latest) {
83
+ latest = entry.timestamp;
84
+ }
85
+ }
86
+ return latest;
87
+ }
88
+ function noopLogger() {
89
+ return { debug: () => {
90
+ }, info: () => {
91
+ }, warn: () => {
92
+ } };
93
+ }
94
+ async function runPeerProfileReasoner(options) {
95
+ const log = {
96
+ debug: options.log?.debug ?? noopLogger().debug,
97
+ info: options.log?.info ?? noopLogger().info,
98
+ warn: options.log?.warn ?? noopLogger().warn
99
+ };
100
+ const result = {
101
+ peersConsidered: 0,
102
+ peersProcessed: 0,
103
+ fieldsApplied: 0,
104
+ perPeer: []
105
+ };
106
+ if (options.enabled !== true) {
107
+ log.debug("[peer-profile-reasoner] disabled \u2014 no-op");
108
+ return result;
109
+ }
110
+ if (!options.llm) {
111
+ log.warn("[peer-profile-reasoner] no LLM client supplied \u2014 skipping run");
112
+ return result;
113
+ }
114
+ const minInteractions = Number.isFinite(options.minInteractions) ? Math.max(0, Math.floor(options.minInteractions)) : 0;
115
+ const maxFields = Number.isFinite(options.maxFieldsPerRun) ? Math.max(0, Math.floor(options.maxFieldsPerRun)) : 0;
116
+ if (maxFields === 0) {
117
+ log.debug("[peer-profile-reasoner] maxFieldsPerRun=0 \u2014 no-op");
118
+ return result;
119
+ }
120
+ const maxLogPerPeer = Number.isFinite(options.maxLogEntriesPerPeer ?? NaN) ? Math.max(1, Math.floor(options.maxLogEntriesPerPeer)) : 50;
121
+ const now = options.now ?? /* @__PURE__ */ new Date();
122
+ const nowIso = now.toISOString();
123
+ let peers;
124
+ try {
125
+ if (options.peerIds && options.peerIds.length > 0) {
126
+ const all = await listPeers(options.memoryDir);
127
+ const wanted = new Set(
128
+ options.peerIds.filter(
129
+ (id) => typeof id === "string" && PEER_ID_PATTERN.test(id)
130
+ )
131
+ );
132
+ peers = all.filter((p) => wanted.has(p.id));
133
+ } else {
134
+ peers = await listPeers(options.memoryDir);
135
+ }
136
+ } catch (err) {
137
+ log.warn(
138
+ `[peer-profile-reasoner] listPeers failed: ${err instanceof Error ? err.message : String(err)}`
139
+ );
140
+ return result;
141
+ }
142
+ result.peersConsidered = peers.length;
143
+ let fieldsAppliedTotal = 0;
144
+ for (const peer of peers) {
145
+ if (options.signal?.aborted) {
146
+ result.perPeer.push({
147
+ peerId: peer.id,
148
+ status: "skipped_aborted",
149
+ fieldsApplied: 0,
150
+ droppedDueToCap: 0,
151
+ fields: []
152
+ });
153
+ continue;
154
+ }
155
+ try {
156
+ const fullLog = await readPeerInteractionLog(
157
+ options.memoryDir,
158
+ peer.id
159
+ );
160
+ if (fullLog.length === 0) {
161
+ result.perPeer.push({
162
+ peerId: peer.id,
163
+ status: "skipped_no_log",
164
+ fieldsApplied: 0,
165
+ droppedDueToCap: 0,
166
+ fields: []
167
+ });
168
+ continue;
169
+ }
170
+ const lastRun = lastRunTimestamp(fullLog);
171
+ const sinceLastRunFull = lastRun ? fullLog.filter(
172
+ (e) => e.timestamp > lastRun && e.kind !== RUN_MARKER_KIND
173
+ ) : fullLog.filter((e) => e.kind !== RUN_MARKER_KIND);
174
+ if (sinceLastRunFull.length < minInteractions) {
175
+ result.perPeer.push({
176
+ peerId: peer.id,
177
+ status: "skipped_below_min_interactions",
178
+ fieldsApplied: 0,
179
+ droppedDueToCap: 0,
180
+ fields: []
181
+ });
182
+ continue;
183
+ }
184
+ const sinceLastRun = sinceLastRunFull.length > maxLogPerPeer ? sinceLastRunFull.slice(sinceLastRunFull.length - maxLogPerPeer) : sinceLastRunFull;
185
+ const existingProfile = await readPeerProfile(options.memoryDir, peer.id);
186
+ const remainingBudget = maxFields - fieldsAppliedTotal;
187
+ if (remainingBudget <= 0) {
188
+ result.perPeer.push({
189
+ peerId: peer.id,
190
+ status: "skipped_cap_reached",
191
+ fieldsApplied: 0,
192
+ droppedDueToCap: 0,
193
+ fields: []
194
+ });
195
+ continue;
196
+ }
197
+ const prompt = buildPeerProfileReasonerPrompt({
198
+ peer,
199
+ existingProfile,
200
+ log: sinceLastRun,
201
+ maxFields: remainingBudget
202
+ });
203
+ const messages = [
204
+ { role: "system", content: SYSTEM_MESSAGE },
205
+ { role: "user", content: prompt }
206
+ ];
207
+ let response;
208
+ try {
209
+ response = await options.llm.chatCompletion(messages, {
210
+ temperature: 0.2,
211
+ maxTokens: 1500
212
+ });
213
+ } catch (err) {
214
+ log.warn(
215
+ `[peer-profile-reasoner] LLM call failed for "${peer.id}": ${err instanceof Error ? err.message : String(err)}`
216
+ );
217
+ result.perPeer.push({
218
+ peerId: peer.id,
219
+ status: "skipped_llm_unavailable",
220
+ fieldsApplied: 0,
221
+ droppedDueToCap: 0,
222
+ fields: []
223
+ });
224
+ continue;
225
+ }
226
+ if (!response || typeof response.content !== "string") {
227
+ result.perPeer.push({
228
+ peerId: peer.id,
229
+ status: "skipped_llm_unavailable",
230
+ fieldsApplied: 0,
231
+ droppedDueToCap: 0,
232
+ fields: []
233
+ });
234
+ continue;
235
+ }
236
+ const proposals = parsePeerProfileReasonerResponse(response.content);
237
+ if (proposals.length === 0) {
238
+ result.perPeer.push({
239
+ peerId: peer.id,
240
+ status: "processed",
241
+ fieldsApplied: 0,
242
+ droppedDueToCap: 0,
243
+ fields: []
244
+ });
245
+ continue;
246
+ }
247
+ const sessionIdsInWindow = new Set(
248
+ sinceLastRun.map((e) => e.sessionId).filter((s) => typeof s === "string" && s.length > 0)
249
+ );
250
+ const baseFields = existingProfile ? { ...existingProfile.fields } : {};
251
+ const baseProvenance = {};
252
+ if (existingProfile) {
253
+ for (const [k, list] of Object.entries(existingProfile.provenance)) {
254
+ baseProvenance[k] = [...list];
255
+ }
256
+ }
257
+ const appliedFieldsForPeer = [];
258
+ let droppedDueToCap = 0;
259
+ let invalidProposalSeen = false;
260
+ for (const proposal of proposals) {
261
+ const candidateBudget = maxFields - fieldsAppliedTotal - appliedFieldsForPeer.length;
262
+ if (candidateBudget <= 0) {
263
+ droppedDueToCap += 1;
264
+ continue;
265
+ }
266
+ if (proposal.field === "__proto__" || proposal.field === "constructor" || proposal.field === "prototype") {
267
+ invalidProposalSeen = true;
268
+ continue;
269
+ }
270
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(proposal.field)) {
271
+ invalidProposalSeen = true;
272
+ continue;
273
+ }
274
+ baseFields[proposal.field] = proposal.value;
275
+ const sourceSessionId = proposal.sourceSessionId && sessionIdsInWindow.has(proposal.sourceSessionId) ? proposal.sourceSessionId : void 0;
276
+ const provEntry = {
277
+ observedAt: nowIso,
278
+ signal: proposal.signal,
279
+ ...sourceSessionId ? { sourceSessionId } : {},
280
+ ...proposal.note && proposal.note.length > 0 ? { note: proposal.note } : {}
281
+ };
282
+ const list = baseProvenance[proposal.field] ?? [];
283
+ list.push(provEntry);
284
+ baseProvenance[proposal.field] = list;
285
+ appliedFieldsForPeer.push(proposal.field);
286
+ }
287
+ if (appliedFieldsForPeer.length === 0) {
288
+ result.perPeer.push({
289
+ peerId: peer.id,
290
+ status: invalidProposalSeen ? "skipped_invalid_proposal" : droppedDueToCap > 0 ? "skipped_cap_reached" : "processed",
291
+ fieldsApplied: 0,
292
+ droppedDueToCap,
293
+ fields: []
294
+ });
295
+ continue;
296
+ }
297
+ const merged = {
298
+ peerId: peer.id,
299
+ updatedAt: nowIso,
300
+ fields: baseFields,
301
+ provenance: baseProvenance
302
+ };
303
+ await writePeerProfile(options.memoryDir, merged);
304
+ fieldsAppliedTotal += appliedFieldsForPeer.length;
305
+ const wantsMarker = options.appendRunMarkerToLog ?? true;
306
+ if (wantsMarker) {
307
+ try {
308
+ await appendInteractionLog(options.memoryDir, peer.id, {
309
+ timestamp: nowIso,
310
+ kind: RUN_MARKER_KIND,
311
+ summary: `applied ${appliedFieldsForPeer.length} field(s) via ${options.model ?? "unknown-model"}`
312
+ });
313
+ } catch (err) {
314
+ log.warn(
315
+ `[peer-profile-reasoner] run-marker append failed for "${peer.id}": ${err instanceof Error ? err.message : String(err)}`
316
+ );
317
+ }
318
+ }
319
+ result.perPeer.push({
320
+ peerId: peer.id,
321
+ status: "processed",
322
+ fieldsApplied: appliedFieldsForPeer.length,
323
+ droppedDueToCap,
324
+ fields: appliedFieldsForPeer
325
+ });
326
+ result.peersProcessed += 1;
327
+ result.fieldsApplied = fieldsAppliedTotal;
328
+ } catch (err) {
329
+ log.warn(
330
+ `[peer-profile-reasoner] error processing peer "${peer.id}": ${err instanceof Error ? err.message : String(err)}`
331
+ );
332
+ result.perPeer.push({
333
+ peerId: peer.id,
334
+ status: "error",
335
+ fieldsApplied: 0,
336
+ droppedDueToCap: 0,
337
+ fields: [],
338
+ error: err instanceof Error ? err.message : String(err)
339
+ });
340
+ }
341
+ }
342
+ return result;
343
+ }
344
+
345
+ export {
346
+ buildPeerProfileReasonerPrompt,
347
+ parsePeerProfileReasonerResponse,
348
+ runPeerProfileReasoner
349
+ };