@hyperspell/openclaw-hyperspell 0.17.0 → 0.17.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/config.js +1 -0
- package/dist/hooks/auto-context.js +26 -32
- package/dist/lib/ranking.js +24 -0
- package/dist/tools/search.js +1 -1
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -72,6 +72,7 @@ function parseRanking(raw) {
|
|
|
72
72
|
? r.storyTerms.map((t) => String(t)).filter((t) => t.length > 0)
|
|
73
73
|
: DEFAULT_RANKING.storyTerms,
|
|
74
74
|
candidateMultiplier: Math.max(1, num(r.candidateMultiplier, DEFAULT_RANKING.candidateMultiplier)),
|
|
75
|
+
chatterQuota: Math.max(0, num(r.chatterQuota, DEFAULT_RANKING.chatterQuota)),
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
function parseSources(raw) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildScopeFilter, getCanReadScopes, resolveUser, } from "../lib/sender.js";
|
|
2
2
|
import { excludeFilterFor, mergeWithExclude } from "../lib/filters.js";
|
|
3
|
-
import { rerank } from "../lib/ranking.js";
|
|
3
|
+
import { rerank, selectRanked } from "../lib/ranking.js";
|
|
4
4
|
import { classifySearchError, logSearchError } from "../lib/search-error.js";
|
|
5
5
|
import { resolveCurrentSessionId } from "../lib/session.js";
|
|
6
6
|
import { log } from "../logger.js";
|
|
@@ -53,18 +53,14 @@ function formatHighlightBullets(results, maxResults, threshold) {
|
|
|
53
53
|
return sections.join("\n\n");
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* the lower of (threshold, the result's own base relevance), so we don't then
|
|
61
|
-
* hide the very lines that define a boosted-but-quiet memory.
|
|
56
|
+
* Format already-SELECTED composite-ranked results (threshold + chatter quota
|
|
57
|
+
* applied upstream by selectRanked). Highlights are floored at the lower of
|
|
58
|
+
* (threshold, the result's own base relevance), so we don't hide the very lines
|
|
59
|
+
* that define a boosted-but-quiet memory.
|
|
62
60
|
*/
|
|
63
|
-
function
|
|
61
|
+
function formatSelected(selected, threshold) {
|
|
64
62
|
const sections = [];
|
|
65
|
-
for (const r of
|
|
66
|
-
if (r._composite < threshold)
|
|
67
|
-
continue;
|
|
63
|
+
for (const r of selected) {
|
|
68
64
|
const hiFloor = Math.min(threshold, r._base);
|
|
69
65
|
const chosen = [...r.highlights]
|
|
70
66
|
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
@@ -77,8 +73,6 @@ function formatRankedBullets(ranked, maxResults, threshold) {
|
|
|
77
73
|
.map((h) => `- ${h.text.replace(/\n/g, " ")} [${Math.round((h.score ?? 0) * 100)}%]`)
|
|
78
74
|
.join("\n");
|
|
79
75
|
sections.push(`### ${title} (resource_id: ${r.resourceId}, source: ${r.source})\n\n${bullets}`);
|
|
80
|
-
if (sections.length >= maxResults)
|
|
81
|
-
break;
|
|
82
76
|
}
|
|
83
77
|
if (sections.length === 0)
|
|
84
78
|
return null;
|
|
@@ -86,18 +80,15 @@ function formatRankedBullets(ranked, maxResults, threshold) {
|
|
|
86
80
|
}
|
|
87
81
|
const INTRO = "The following is surfaced from the user's memory and connected sources, including past conversations. Reference it as recalled context, only when relevant to the conversation.";
|
|
88
82
|
const DISCLAIMER = "Draw on it when relevant — including indirect connections — but don't force it into every response or make assumptions beyond what's stated.";
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
/** Wrap
|
|
95
|
-
*
|
|
83
|
+
// A short, CONDITIONAL reminder appended only to a real memory block (not a
|
|
84
|
+
// standing every-turn imperative — that reads as ambient framing and trains
|
|
85
|
+
// search-as-ritual; the agent's own instructions carry the standing rule). The
|
|
86
|
+
// point here is just: what surfaced is a passive match, not all of memory.
|
|
87
|
+
const SEARCH_REMINDER = "This is a passive match, not all of memory — if the answer turns on a specific past decision, promise, name, or something recorded, search for it directly before concluding, and say so plainly if it isn't there.";
|
|
88
|
+
/** Wrap a real memory block. No memory → no injection (caller returns nothing);
|
|
89
|
+
* the standing search rule lives in the agent's own instructions, not here. */
|
|
96
90
|
function wrapContext(memorySection) {
|
|
97
|
-
|
|
98
|
-
return `<hyperspell-context>\n${INTRO}\n\n${memorySection}\n\n${DISCLAIMER}\n\n${SEARCH_DIRECTIVE}\n</hyperspell-context>`;
|
|
99
|
-
}
|
|
100
|
-
return `<hyperspell-context>\n${SEARCH_DIRECTIVE}\n</hyperspell-context>`;
|
|
91
|
+
return `<hyperspell-context>\n${INTRO}\n\n${memorySection}\n\n${DISCLAIMER}\n\n${SEARCH_REMINDER}\n</hyperspell-context>`;
|
|
101
92
|
}
|
|
102
93
|
/**
|
|
103
94
|
* Drop results belonging to the CURRENT session. The hot buffer writes the live
|
|
@@ -146,11 +137,13 @@ export function buildAutoContextHandler(client, cfg) {
|
|
|
146
137
|
let formatted;
|
|
147
138
|
if (ranking.enabled) {
|
|
148
139
|
const ranked = rerank(results, ranking);
|
|
149
|
-
|
|
140
|
+
// Threshold + chatter quota applied here, so a high-similarity echo can
|
|
141
|
+
// inform but never flood (the quota bounds count; the penalty bounds rank).
|
|
142
|
+
const selected = selectRanked(ranked, cfg.maxResults, cfg.relevanceThreshold, ranking.chatterQuota);
|
|
143
|
+
formatted = formatSelected(selected, cfg.relevanceThreshold);
|
|
150
144
|
if (formatted) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
log.debug(`auto-context: injecting (ranked) ${JSON.stringify(tally)} from ${results.length} candidates`);
|
|
145
|
+
const tally = selected.reduce((acc, r) => ((acc[r._kind] = (acc[r._kind] ?? 0) + 1), acc), {});
|
|
146
|
+
log.debug(`auto-context: injecting (ranked) ${JSON.stringify(tally)} from ${results.length} candidates (chatter cap ${ranking.chatterQuota})`);
|
|
154
147
|
}
|
|
155
148
|
}
|
|
156
149
|
else {
|
|
@@ -158,11 +151,12 @@ export function buildAutoContextHandler(client, cfg) {
|
|
|
158
151
|
if (formatted)
|
|
159
152
|
log.debug(`auto-context: injecting ${results.length} memories`);
|
|
160
153
|
}
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
154
|
+
// No memory cleared the bar → no injection. The standing "search before you
|
|
155
|
+
// conclude" rule lives in the agent's own instructions, not an ambient
|
|
156
|
+
// every-turn banner (which reads as framing and trains search-as-ritual).
|
|
164
157
|
if (!formatted) {
|
|
165
|
-
log.debug("auto-context:
|
|
158
|
+
log.debug("auto-context: no relevant memories found");
|
|
159
|
+
return;
|
|
166
160
|
}
|
|
167
161
|
return { prependContext: wrapContext(formatted) };
|
|
168
162
|
}
|
package/dist/lib/ranking.js
CHANGED
|
@@ -5,6 +5,7 @@ export const DEFAULT_RANKING = {
|
|
|
5
5
|
storyBoost: 0.15,
|
|
6
6
|
storyTerms: [],
|
|
7
7
|
candidateMultiplier: 3,
|
|
8
|
+
chatterQuota: 2,
|
|
8
9
|
};
|
|
9
10
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
11
|
/** Highest available relevance for a result (doc score or its best highlight). */
|
|
@@ -61,3 +62,26 @@ export function rerank(results, w) {
|
|
|
61
62
|
})
|
|
62
63
|
.sort((a, b) => b._composite - a._composite);
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Choose which ranked results to inject: keep those clearing `threshold` on
|
|
67
|
+
* their composite, cap CHATTER at `chatterQuota` regardless of score (so a
|
|
68
|
+
* high-similarity echo can inform but never flood — the penalty bounds rank,
|
|
69
|
+
* the quota bounds count), and stop at `maxResults`.
|
|
70
|
+
*/
|
|
71
|
+
export function selectRanked(ranked, maxResults, threshold, chatterQuota) {
|
|
72
|
+
const out = [];
|
|
73
|
+
let chatter = 0;
|
|
74
|
+
for (const r of ranked) {
|
|
75
|
+
if (r._composite < threshold)
|
|
76
|
+
continue;
|
|
77
|
+
if (r._kind === "chatter") {
|
|
78
|
+
if (chatter >= chatterQuota)
|
|
79
|
+
continue;
|
|
80
|
+
chatter++;
|
|
81
|
+
}
|
|
82
|
+
out.push(r);
|
|
83
|
+
if (out.length >= maxResults)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
package/dist/tools/search.js
CHANGED
|
@@ -12,7 +12,7 @@ export function createSearchToolFactory(client, cfg) {
|
|
|
12
12
|
return (ctx) => ({
|
|
13
13
|
name: "hyperspell_search",
|
|
14
14
|
label: "Memory Search",
|
|
15
|
-
description: "Search the user's long-term memory and connected sources: saved memories and notes; past conversations — including earlier or parallel sessions you have no transcript for; and connected sources (Notion, Slack, Gmail, Google Drive, etc.). Use this
|
|
15
|
+
description: "Search the user's long-term memory and connected sources: saved memories and notes; past conversations — including earlier or parallel sessions you have no transcript for; and connected sources (Notion, Slack, Gmail, Google Drive, etc.). Use this proactively, not only when asked — but on content, not as ritual: before answering anything that touches the user's history, a past decision, a name, a promise, or something you might have recorded, search FIRST with a specific query and look. Don't answer from impression when you can check. If a search comes back empty, say so plainly — never fill the gap with something invented.",
|
|
16
16
|
parameters: Type.Object({
|
|
17
17
|
query: Type.String({ description: "Search query" }),
|
|
18
18
|
limit: Type.Optional(Type.Number({ description: "Max results (default: 5)" })),
|
package/openclaw.plugin.json
CHANGED
|
@@ -164,7 +164,8 @@
|
|
|
164
164
|
"chatterPenalty": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
165
165
|
"storyBoost": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
166
166
|
"storyTerms": { "type": "array", "items": { "type": "string" } },
|
|
167
|
-
"candidateMultiplier": { "type": "number", "minimum": 1, "maximum": 10 }
|
|
167
|
+
"candidateMultiplier": { "type": "number", "minimum": 1, "maximum": 10 },
|
|
168
|
+
"chatterQuota": { "type": "number", "minimum": 0, "maximum": 20 }
|
|
168
169
|
}
|
|
169
170
|
},
|
|
170
171
|
"debug": {
|