@hyperspell/openclaw-hyperspell 0.17.1 → 0.19.0

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.
@@ -1,5 +1,8 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import { channelIdFromCtx } from "../lib/exclude-channels.js";
2
3
  import { getDefaultWriteScope, resolveRole, resolveUser, routeWrite, } from "../lib/sender.js";
4
+ import { resolveCurrentSessionId } from "../lib/session.js";
5
+ import { isMultiSpeaker } from "../lib/speaker-tracker.js";
3
6
  import { log } from "../logger.js";
4
7
  export function createRememberToolFactory(client, cfg) {
5
8
  const scopingEnabled = !!cfg.multiUser?.scoping;
@@ -21,6 +24,22 @@ export function createRememberToolFactory(client, cfg) {
21
24
  scope: Type.Optional(Type.String({ description: scopeDescription })),
22
25
  }),
23
26
  async execute(_toolCallId, params) {
27
+ // Decline silently-wrong writes in multi-speaker sessions with no multiUser
28
+ // config. Without per-sender routing the memory would land under cfg.userId
29
+ // regardless of who asked — contaminating the primary user's store with
30
+ // another person's data (attribution gap 1, issue #59 follow-up).
31
+ const sessionId = resolveCurrentSessionId(undefined, ctx);
32
+ if (isMultiSpeaker(sessionId, ctx?.is_group_chat === true) && !cfg.multiUser) {
33
+ log.warn("remember tool: declining write — multi-speaker session with no multiUser config; cannot attribute memory to current speaker");
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: "I can't store this memory right now — this is a multi-speaker session and without per-user memory configuration I have no way to attribute it to the right person. The memory would land in the primary user's store regardless of who asked. To fix this, add a `multiUser` config block to the plugin settings.",
39
+ },
40
+ ],
41
+ };
42
+ }
24
43
  const resolved = resolveUser(ctx, cfg);
25
44
  // Scope resolution: explicit param > role default > global default > "private"
26
45
  const scope = params.scope ??
@@ -67,12 +86,19 @@ export function createRememberToolFactory(client, cfg) {
67
86
  userId = resolved?.userId;
68
87
  }
69
88
  log.debug(`remember tool: "${params.text.slice(0, 50)}..." date=${params.date ?? "now"} userId=${userId} scope=${scope}`);
89
+ // Tag with the originating conversation so purge-channel can find this
90
+ // memory if the channel is quarantined later (the tool is already
91
+ // suppressed in channels that are quarantined NOW).
92
+ const channelId = channelIdFromCtx(ctx);
70
93
  try {
71
94
  await client.addMemory(params.text, {
72
95
  title: params.title,
73
96
  date: params.date,
74
97
  collection,
75
- metadata: { source: "openclaw_tool" },
98
+ metadata: {
99
+ source: "openclaw_tool",
100
+ ...(channelId ? { openclaw_channel_id: channelId } : {}),
101
+ },
76
102
  userId,
77
103
  scope: scopingEnabled ? scope : undefined,
78
104
  });
@@ -2,6 +2,8 @@ import { Type } from "@sinclair/typebox";
2
2
  import { mergeWithExclude } from "../lib/filters.js";
3
3
  import { classifySearchError, logSearchError, searchErrorToolText, } from "../lib/search-error.js";
4
4
  import { buildScopeFilter, getCanReadScopes, resolveUser } from "../lib/sender.js";
5
+ import { resolveCurrentSessionId } from "../lib/session.js";
6
+ import { isMultiSpeaker } from "../lib/speaker-tracker.js";
5
7
  import { log } from "../logger.js";
6
8
  export function createSearchToolFactory(client, cfg) {
7
9
  const scopingEnabled = !!cfg.multiUser?.scoping;
@@ -27,6 +29,15 @@ export function createSearchToolFactory(client, cfg) {
27
29
  const limit = params.limit ?? 5;
28
30
  const resolved = resolveUser(ctx, cfg);
29
31
  const userId = params.userId ?? resolved?.userId;
32
+ // Warn when searching in a multi-speaker session with no multiUser config:
33
+ // results reflect the primary user's full store, not the current speaker's
34
+ // personal space. The search proceeds but the result carries the caveat so
35
+ // the agent can qualify its answer (attribution gap 1, issue #59 follow-up).
36
+ const sessionId = resolveCurrentSessionId(undefined, ctx);
37
+ const multiSpeakerNoConfig = isMultiSpeaker(sessionId, ctx?.is_group_chat === true) && !cfg.multiUser;
38
+ if (multiSpeakerNoConfig) {
39
+ log.warn("search tool: multi-speaker session with no multiUser config — results reflect primary user's full store, not current speaker's personal space");
40
+ }
30
41
  // Build scope filter: intersect requested scope (if any) with caller's canRead
31
42
  let filter;
32
43
  if (scopingEnabled) {
@@ -70,7 +81,10 @@ export function createSearchToolFactory(client, cfg) {
70
81
  return `${i + 1}. Source: ${doc.source}\n Title: ${title}\n Summary: ${summary}\n Relevance: ${relevance}`;
71
82
  })
72
83
  .join("\n\n");
73
- const text = `Found ${documents.length} memories:\n\n${formattedDocs}`;
84
+ const caveat = multiSpeakerNoConfig
85
+ ? "\n\n⚠️ Note: This is a multi-speaker session without per-user memory config. These results reflect the primary user's store, not the current speaker's personal space. Use these results with that limitation in mind."
86
+ : "";
87
+ const text = `Found ${documents.length} memories:\n\n${formattedDocs}${caveat}`;
74
88
  return {
75
89
  content: [{ type: "text", text }],
76
90
  details: { count: documents.length, documents },
@@ -2,12 +2,13 @@
2
2
  "id": "openclaw-hyperspell",
3
3
  "name": "Hyperspell",
4
4
  "description": "Context and memory for your AI agents — search across Notion, Slack, Google Drive, and more",
5
- "version": "0.13.0",
5
+ "version": "0.19.0",
6
6
  "kind": "memory",
7
7
  "contracts": {
8
8
  "tools": [
9
9
  "hyperspell_search",
10
10
  "hyperspell_remember",
11
+ "hyperspell_emotional_arc",
11
12
  "hyperspell_network_scan",
12
13
  "hyperspell_network_write",
13
14
  "hyperspell_network_complete"
@@ -46,6 +47,11 @@
46
47
  "help": "Maintain emotional continuity across sessions — remembers not just what happened, but how it felt",
47
48
  "advanced": true
48
49
  },
50
+ "excludeChannels": {
51
+ "label": "Excluded Channels",
52
+ "help": "Conversation/channel ids fully quarantined from memory: no context injection, no memory writes, no memory tools in those sessions. Threads inside an excluded channel inherit the quarantine. Forward-only; already-synced content is not removed (see purge-channel CLI command).",
53
+ "advanced": true
54
+ },
49
55
  "startupOrientation": {
50
56
  "label": "Startup Orientation",
51
57
  "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.",
@@ -133,6 +139,15 @@
133
139
  "emotionalContext": {
134
140
  "type": "boolean"
135
141
  },
142
+ "moodWeatherChance": {
143
+ "type": "number",
144
+ "minimum": 0,
145
+ "maximum": 1
146
+ },
147
+ "excludeChannels": {
148
+ "type": "array",
149
+ "items": { "type": "string" }
150
+ },
136
151
  "relationshipId": {
137
152
  "type": "string"
138
153
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspell/openclaw-hyperspell",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Hyperspell memory plugin",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "check-types": "tsc --noEmit",
39
39
  "lint": "bunx @biomejs/biome ci .",
40
40
  "lint:fix": "bunx @biomejs/biome check --write .",
41
- "test": "node --test --experimental-strip-types client.test.ts lib/sender.test.ts lib/filters.test.ts lib/search-error.test.ts lib/session.test.ts lib/ranking.test.ts tools/search.test.ts config.test.ts sync/markdown.test.ts hooks/auto-trace.test.ts hooks/auto-context.test.ts hooks/hot-buffer.test.ts hooks/emotional-state.test.ts hooks/startup-orientation.test.ts"
41
+ "test": "node --test --experimental-strip-types client.test.ts lib/sender.test.ts lib/filters.test.ts lib/search-error.test.ts lib/session.test.ts lib/exclude-channels.test.ts lib/ranking.test.ts lib/eval-matchers.test.ts tools/search.test.ts tools/emotional-arc.test.ts commands/preview.test.ts commands/purge-channel.test.ts config.test.ts sync/markdown.test.ts hooks/auto-trace.test.ts hooks/auto-context.test.ts hooks/hot-buffer.test.ts hooks/emotional-state.test.ts hooks/startup-orientation.test.ts hooks/mood-weather.test.ts eval/eval.test.ts"
42
42
  },
43
43
  "openclaw": {
44
44
  "extensions": [