@hyperspell/openclaw-hyperspell 0.13.0 → 0.13.1
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/commands/setup.js +0 -1
- package/dist/config.js +1 -1
- package/dist/hooks/auto-trace.js +2 -0
- package/dist/hooks/startup-orientation.js +108 -28
- package/openclaw.plugin.json +15 -0
- package/package.json +1 -1
package/dist/commands/setup.js
CHANGED
package/dist/config.js
CHANGED
|
@@ -25,6 +25,7 @@ const ALLOWED_KEYS = [
|
|
|
25
25
|
"debug",
|
|
26
26
|
"knowledgeGraph",
|
|
27
27
|
"multiUser",
|
|
28
|
+
"dreaming",
|
|
28
29
|
];
|
|
29
30
|
const VALID_SOURCES = [
|
|
30
31
|
"reddit",
|
|
@@ -252,7 +253,6 @@ export function parseConfig(raw) {
|
|
|
252
253
|
recentDays: soRaw.recentDays ?? 7,
|
|
253
254
|
recentLimit: soRaw.recentLimit ?? 5,
|
|
254
255
|
loopsLimit: soRaw.loopsLimit ?? 3,
|
|
255
|
-
recentQuery: soRaw.recentQuery ?? "conversation session interaction",
|
|
256
256
|
loopsQuery: soRaw.loopsQuery ??
|
|
257
257
|
"open tasks pending questions unfinished promised need to follow up",
|
|
258
258
|
},
|
package/dist/hooks/auto-trace.js
CHANGED
|
@@ -13,6 +13,8 @@ export function sanitizeTraceText(input) {
|
|
|
13
13
|
let out = input;
|
|
14
14
|
out = out.replace(/<hyperspell-context>[\s\S]*?<\/hyperspell-context>\n?/g, "");
|
|
15
15
|
out = out.replace(/<hyperspell-emotional-context>[\s\S]*?<\/hyperspell-emotional-context>\n?/g, "");
|
|
16
|
+
out = out.replace(/<hyperspell-recent-interactions>[\s\S]*?<\/hyperspell-recent-interactions>\n?/g, "");
|
|
17
|
+
out = out.replace(/<hyperspell-unfinished-loops>[\s\S]*?<\/hyperspell-unfinished-loops>\n?/g, "");
|
|
16
18
|
out = out.replace(/Sender \(untrusted metadata\):\s*```json[\s\S]*?```\n?/g, "");
|
|
17
19
|
out = out.replace(/\[Bootstrap pending\][\s\S]*?(?=\n{2,}|\nSystem:|\nSender|$)/g, "");
|
|
18
20
|
out = out.replace(/\[Startup context loaded by runtime\][\s\S]*?(?:\n\n|$)/g, "");
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { resolveUser } from "../lib/sender.js";
|
|
2
2
|
import { log } from "../logger.js";
|
|
3
|
-
const
|
|
3
|
+
const MAX_ATTEMPTS = 2;
|
|
4
|
+
const RECENT_BUFFER_LIMIT = 100;
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Sessions where the orientation block was already injected (or where we
|
|
7
|
+
* deliberately decided not to inject — unknown sender, exhausted retries).
|
|
8
|
+
* Re-checked at the top of every turn; on hit, we skip.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle mirrors the emotional-context hook: `after_compaction` clears
|
|
11
|
+
* (the original injection may have been trimmed out of history) and
|
|
12
|
+
* `session_end` cleans up to keep both structures bounded.
|
|
11
13
|
*/
|
|
12
14
|
const injectedSessions = new Set();
|
|
15
|
+
/**
|
|
16
|
+
* Counts attempts for sessions where every call has failed so far. We only
|
|
17
|
+
* count failures here, so a session that succeeds on retry will never appear.
|
|
18
|
+
* Capped at MAX_ATTEMPTS — past that we give up and add to injectedSessions.
|
|
19
|
+
*/
|
|
20
|
+
const failedAttempts = new Map();
|
|
13
21
|
function formatRelativeTime(iso) {
|
|
14
22
|
if (!iso)
|
|
15
23
|
return "";
|
|
@@ -36,7 +44,7 @@ function formatRecentInteractions(results) {
|
|
|
36
44
|
return null;
|
|
37
45
|
const lines = results.map((r) => {
|
|
38
46
|
const when = formatRelativeTime(r.createdAt);
|
|
39
|
-
const title = r.title
|
|
47
|
+
const title = r.title || `[${r.source}]`;
|
|
40
48
|
const prefix = when ? `[${when}] ` : "";
|
|
41
49
|
const top = r.highlights[0]?.text?.replace(/\n/g, " ").slice(0, 140);
|
|
42
50
|
const tail = top ? ` — ${top}` : "";
|
|
@@ -50,7 +58,7 @@ function formatUnfinishedLoops(results) {
|
|
|
50
58
|
const top = r.highlights[0];
|
|
51
59
|
if (!top)
|
|
52
60
|
continue;
|
|
53
|
-
const title = r.title
|
|
61
|
+
const title = r.title || `[${r.source}]`;
|
|
54
62
|
bullets.push(`- ${title}: ${top.text.replace(/\n/g, " ")}`);
|
|
55
63
|
}
|
|
56
64
|
return bullets.length > 0 ? bullets.join("\n") : null;
|
|
@@ -58,7 +66,7 @@ function formatUnfinishedLoops(results) {
|
|
|
58
66
|
function isoDaysAgo(days) {
|
|
59
67
|
const d = new Date();
|
|
60
68
|
d.setUTCDate(d.getUTCDate() - days);
|
|
61
|
-
return d
|
|
69
|
+
return d;
|
|
62
70
|
}
|
|
63
71
|
/**
|
|
64
72
|
* Resolve the userId to use for personal searches. In multi-user mode, skip
|
|
@@ -74,12 +82,73 @@ function personalUserId(cfg, ctx) {
|
|
|
74
82
|
return { skip: true };
|
|
75
83
|
return { skip: false, userId: resolved.userId };
|
|
76
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Pull recent agent_end traces via listMemories, sorted chronologically.
|
|
87
|
+
*
|
|
88
|
+
* Why list, not search: a date-window + relevance search ranks results by
|
|
89
|
+
* lexical similarity to a generic query, which is approximately random
|
|
90
|
+
* within a 7-day slice and can easily exclude yesterday in favor of a
|
|
91
|
+
* 5-day-old session. listMemories gives us true chronological recall.
|
|
92
|
+
*
|
|
93
|
+
* The SDK's list endpoint doesn't expose date or metadata filters in our
|
|
94
|
+
* wrapper, so we filter client-side. Buffer is capped at
|
|
95
|
+
* RECENT_BUFFER_LIMIT to bound wire cost; if a user has more than that
|
|
96
|
+
* many traces in the cutoff window we'll still get the newest ones,
|
|
97
|
+
* since the underlying API returns recency-ordered pages.
|
|
98
|
+
*/
|
|
99
|
+
async function fetchRecentTraces(client, cutoff, limit, userId) {
|
|
100
|
+
const buffer = [];
|
|
101
|
+
let scanned = 0;
|
|
102
|
+
const cutoffMs = cutoff.getTime();
|
|
103
|
+
for await (const memory of client.listMemories({
|
|
104
|
+
source: "trace",
|
|
105
|
+
userId,
|
|
106
|
+
pageSize: 50,
|
|
107
|
+
})) {
|
|
108
|
+
scanned++;
|
|
109
|
+
if (scanned > RECENT_BUFFER_LIMIT)
|
|
110
|
+
break;
|
|
111
|
+
const meta = memory.metadata;
|
|
112
|
+
if (meta.openclaw_source !== "agent_end")
|
|
113
|
+
continue;
|
|
114
|
+
const createdRaw = meta.created_at;
|
|
115
|
+
if (typeof createdRaw !== "string")
|
|
116
|
+
continue;
|
|
117
|
+
const createdMs = new Date(createdRaw).getTime();
|
|
118
|
+
if (Number.isNaN(createdMs) || createdMs < cutoffMs)
|
|
119
|
+
continue;
|
|
120
|
+
buffer.push({
|
|
121
|
+
resourceId: memory.resourceId,
|
|
122
|
+
title: memory.title,
|
|
123
|
+
source: memory.source,
|
|
124
|
+
score: null,
|
|
125
|
+
url: null,
|
|
126
|
+
createdAt: createdRaw,
|
|
127
|
+
highlights: [],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
buffer.sort((a, b) => {
|
|
131
|
+
const at = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
132
|
+
const bt = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
133
|
+
return bt - at;
|
|
134
|
+
});
|
|
135
|
+
return buffer.slice(0, limit);
|
|
136
|
+
}
|
|
77
137
|
export function buildStartupOrientationHandler(client, cfg) {
|
|
78
138
|
const so = cfg.startupOrientation;
|
|
79
139
|
return async (_event, ctx) => {
|
|
80
140
|
const sessionKey = ctx?.sessionKey;
|
|
81
141
|
if (sessionKey && injectedSessions.has(sessionKey))
|
|
82
142
|
return;
|
|
143
|
+
const tries = (sessionKey && failedAttempts.get(sessionKey)) || 0;
|
|
144
|
+
if (tries >= MAX_ATTEMPTS) {
|
|
145
|
+
log.debug(`startup-orientation: giving up after ${tries} failed attempt(s)`);
|
|
146
|
+
if (sessionKey) {
|
|
147
|
+
injectedSessions.add(sessionKey);
|
|
148
|
+
failedAttempts.delete(sessionKey);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
83
152
|
const { skip, userId } = personalUserId(cfg, ctx);
|
|
84
153
|
if (skip) {
|
|
85
154
|
log.debug("startup-orientation: skipping — unknown sender in multi-user mode");
|
|
@@ -87,31 +156,36 @@ export function buildStartupOrientationHandler(client, cfg) {
|
|
|
87
156
|
injectedSessions.add(sessionKey);
|
|
88
157
|
return;
|
|
89
158
|
}
|
|
90
|
-
const
|
|
159
|
+
const cutoff = isoDaysAgo(so.recentDays);
|
|
91
160
|
const [recentSettled, loopsSettled] = await Promise.allSettled([
|
|
92
|
-
client
|
|
93
|
-
limit: so.recentLimit,
|
|
94
|
-
after,
|
|
95
|
-
filter: RECENT_FILTER,
|
|
96
|
-
userId,
|
|
97
|
-
}),
|
|
161
|
+
fetchRecentTraces(client, cutoff, so.recentLimit, userId),
|
|
98
162
|
client.search(so.loopsQuery, {
|
|
99
163
|
limit: so.loopsLimit,
|
|
100
164
|
userId,
|
|
101
165
|
}),
|
|
102
166
|
]);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
167
|
+
const recentOk = recentSettled.status === "fulfilled";
|
|
168
|
+
const loopsOk = loopsSettled.status === "fulfilled";
|
|
169
|
+
const recent = recentOk ? recentSettled.value : [];
|
|
170
|
+
const loops = loopsOk ? loopsSettled.value : [];
|
|
171
|
+
if (!recentOk) {
|
|
172
|
+
log.error("startup-orientation: recent listMemories failed", recentSettled.reason);
|
|
106
173
|
}
|
|
107
|
-
|
|
108
|
-
if (loopsSettled.status === "rejected") {
|
|
174
|
+
if (!loopsOk) {
|
|
109
175
|
log.error("startup-orientation: loops search failed", loopsSettled.reason);
|
|
110
176
|
}
|
|
177
|
+
if (!recentOk && !loopsOk) {
|
|
178
|
+
if (sessionKey)
|
|
179
|
+
failedAttempts.set(sessionKey, tries + 1);
|
|
180
|
+
log.debug(`startup-orientation: both calls failed (attempt ${tries + 1}/${MAX_ATTEMPTS}); will retry next turn`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (sessionKey) {
|
|
184
|
+
injectedSessions.add(sessionKey);
|
|
185
|
+
failedAttempts.delete(sessionKey);
|
|
186
|
+
}
|
|
111
187
|
const recentBody = formatRecentInteractions(recent);
|
|
112
188
|
const loopsBody = formatUnfinishedLoops(loops);
|
|
113
|
-
if (sessionKey)
|
|
114
|
-
injectedSessions.add(sessionKey);
|
|
115
189
|
if (!recentBody && !loopsBody) {
|
|
116
190
|
log.debug("startup-orientation: nothing to inject");
|
|
117
191
|
return;
|
|
@@ -120,7 +194,7 @@ export function buildStartupOrientationHandler(client, cfg) {
|
|
|
120
194
|
if (recentBody) {
|
|
121
195
|
blocks.push([
|
|
122
196
|
"<hyperspell-recent-interactions>",
|
|
123
|
-
`Your last ${so.recentDays} days of conversations with this user, most-
|
|
197
|
+
`Your last ${so.recentDays} days of conversations with this user, most-recent-first. Use for situational continuity — don't quote verbatim.`,
|
|
124
198
|
"",
|
|
125
199
|
recentBody,
|
|
126
200
|
"</hyperspell-recent-interactions>",
|
|
@@ -142,7 +216,11 @@ export function buildStartupOrientationHandler(client, cfg) {
|
|
|
142
216
|
export function buildStartupOrientationCompactionHandler() {
|
|
143
217
|
return async (_event, ctx) => {
|
|
144
218
|
const sessionKey = ctx?.sessionKey;
|
|
145
|
-
if (sessionKey
|
|
219
|
+
if (!sessionKey)
|
|
220
|
+
return;
|
|
221
|
+
const dropped = injectedSessions.delete(sessionKey);
|
|
222
|
+
failedAttempts.delete(sessionKey);
|
|
223
|
+
if (dropped) {
|
|
146
224
|
log.debug(`startup-orientation: cache cleared after compaction (session=${sessionKey})`);
|
|
147
225
|
}
|
|
148
226
|
};
|
|
@@ -150,7 +228,9 @@ export function buildStartupOrientationCompactionHandler() {
|
|
|
150
228
|
export function buildStartupOrientationSessionCleanupHandler() {
|
|
151
229
|
return async (_event, ctx) => {
|
|
152
230
|
const sessionKey = ctx?.sessionKey;
|
|
153
|
-
if (sessionKey)
|
|
154
|
-
|
|
231
|
+
if (!sessionKey)
|
|
232
|
+
return;
|
|
233
|
+
injectedSessions.delete(sessionKey);
|
|
234
|
+
failedAttempts.delete(sessionKey);
|
|
155
235
|
};
|
|
156
236
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
"help": "Maintain emotional continuity across sessions — remembers not just what happened, but how it felt",
|
|
42
42
|
"advanced": true
|
|
43
43
|
},
|
|
44
|
+
"startupOrientation": {
|
|
45
|
+
"label": "Startup Orientation",
|
|
46
|
+
"help": "Inject recent-interactions and unfinished-loops blocks once per session at startup. Costs two extra calls + ~500–800 tokens of injection per session; off by default.",
|
|
47
|
+
"advanced": true
|
|
48
|
+
},
|
|
44
49
|
"relationshipId": {
|
|
45
50
|
"label": "Relationship ID",
|
|
46
51
|
"placeholder": "partner-anna",
|
|
@@ -117,6 +122,16 @@
|
|
|
117
122
|
"relationshipId": {
|
|
118
123
|
"type": "string"
|
|
119
124
|
},
|
|
125
|
+
"startupOrientation": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"properties": {
|
|
128
|
+
"enabled": { "type": "boolean" },
|
|
129
|
+
"recentDays": { "type": "number", "minimum": 1, "maximum": 90 },
|
|
130
|
+
"recentLimit": { "type": "number", "minimum": 1, "maximum": 20 },
|
|
131
|
+
"loopsLimit": { "type": "number", "minimum": 1, "maximum": 20 },
|
|
132
|
+
"loopsQuery": { "type": "string" }
|
|
133
|
+
}
|
|
134
|
+
},
|
|
120
135
|
"sources": {
|
|
121
136
|
"type": "string"
|
|
122
137
|
},
|