@hyperspell/openclaw-hyperspell 0.21.1 → 0.23.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.
@@ -89,12 +89,19 @@ function contentToText(content) {
89
89
  }
90
90
  return "";
91
91
  }
92
- function messagesToTranscript(messages) {
92
+ /**
93
+ * Only conversational turns carry relationship signal. `system` is scaffolding
94
+ * and `tool`/`toolResult` are machinery — a Discord send receipt says nothing
95
+ * about how a conversation felt, and letting one through means the extractor
96
+ * distills API JSON into an emotional register. Same rule as hot-buffer.ts.
97
+ */
98
+ const CONVERSATIONAL_ROLES = new Set(["user", "assistant"]);
99
+ export function messagesToTranscript(messages) {
93
100
  const lines = [];
94
101
  for (const m of messages) {
95
102
  if (!m.role || !m.content)
96
103
  continue;
97
- if (m.role === "system")
104
+ if (!CONVERSATIONAL_ROLES.has(m.role))
98
105
  continue;
99
106
  const raw = contentToText(m.content);
100
107
  if (!raw)
@@ -112,9 +119,13 @@ function messagesToTranscript(messages) {
112
119
  * rather than a distilled emotional register. Real summaries are second-person
113
120
  * prose ("Your relationship with this user…"); the placeholder echoes the input
114
121
  * conversation, which has role-prefixed lines.
122
+ *
123
+ * Covers the full role vocabulary, not just what messagesToTranscript writes
124
+ * today: rows stored before tool roles were excluded above are still fetched
125
+ * until they age out, and they lead with `toolResult:`.
115
126
  */
116
127
  export function looksLikeRawTranscript(summary) {
117
- return /(^|\n)\s*(user|assistant)\s*:/i.test(summary);
128
+ return /(^|\n)\s*(?:user|assistant|system|tool(?:result|use|call)?)\s*:/i.test(summary);
118
129
  }
119
130
  /** Compact relative time for the arc labels (e.g. "just now", "3h ago", "2d ago"). */
120
131
  function relativeWhen(iso) {
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { getWorkspaceDir } from "../config.js";
4
4
  import { channelIdFromCtx } from "../lib/exclude-channels.js";
5
+ import { HOT_BUFFER_SOURCE } from "../lib/filters.js";
5
6
  import { resolveUser } from "../lib/sender.js";
6
7
  import { cleanupSpeakerSession, isMultiSpeaker, recordSender, senderIdFromCtx, } from "../lib/speaker-tracker.js";
7
8
  import { log } from "../logger.js";
@@ -237,7 +238,7 @@ export function buildHotBufferHandler(client, cfg, opts) {
237
238
  // would have blocked.
238
239
  const channelId = channelIdFromCtx(ctx);
239
240
  const metadata = {
240
- openclaw_source: "hot_buffer",
241
+ openclaw_source: HOT_BUFFER_SOURCE,
241
242
  openclaw_session_id: sessionId,
242
243
  ...(channelId ? { openclaw_channel_id: channelId } : {}),
243
244
  };
@@ -1,4 +1,4 @@
1
- import { excludeFilterFor } from "../lib/filters.js";
1
+ import { excludeFilterFor, HOT_BUFFER_SOURCE } from "../lib/filters.js";
2
2
  import { resolveUser } from "../lib/sender.js";
3
3
  import { resolveCurrentSessionId } from "../lib/session.js";
4
4
  import { isMultiSpeaker } from "../lib/speaker-tracker.js";
@@ -144,13 +144,16 @@ async function fetchRecentTraces(client, cutoff, limit, userId) {
144
144
  * modern source of "recent interactions" (the agent_end-trace path only works
145
145
  * when auto-trace is on, which it usually isn't). The hot buffer writes one
146
146
  * session-grouped Resource per conversation: `resource_id` = the session id (a
147
- * UUID), untagged (no `openclaw_source`), with a generated title. We rely on
148
- * `listMemories` returning resources newest-first (verified live) and take the
149
- * newest `limit` conversation resources — their metadata carries no date to
147
+ * UUID), tagged `openclaw_source: "hot_buffer"`, with a generated title. We rely
148
+ * on `listMemories` returning resources newest-first (verified live) and take
149
+ * the newest `limit` conversation resources — their metadata carries no date to
150
150
  * filter on, so order is our recency signal.
151
151
  *
152
- * Excludes: tagged rows (memory_sync_section / command / agent_end), non-UUID
153
- * resources (synced docs), automated cron sessions, and untitled rows.
152
+ * Selects ON the hot-buffer tag. This used to skip any row carrying an
153
+ * `openclaw_source` at all, which was correct only while hot rows were untagged;
154
+ * backend #1921 (2026-07-02) started tagging them, so that exclusion began
155
+ * dropping every row it was meant to keep and the block silently vanished.
156
+ * Sibling rows (memory_sync_section / command / agent_end) fail the same check.
154
157
  */
155
158
  async function fetchRecentConversations(client, limit, userId) {
156
159
  const out = [];
@@ -166,7 +169,7 @@ async function fetchRecentConversations(client, limit, userId) {
166
169
  break;
167
170
  if (!UUID_RE.test(memory.resourceId))
168
171
  continue;
169
- if (memory.metadata?.openclaw_source)
172
+ if (memory.metadata?.openclaw_source !== HOT_BUFFER_SOURCE)
170
173
  continue;
171
174
  const title = memory.title ?? "";
172
175
  if (title.length === 0 || /^\[cron:/i.test(title))
package/dist/index.js CHANGED
@@ -144,6 +144,14 @@ export default {
144
144
  // Register exactly ONE of the two: cores between 2026-04 and 2026-07
145
145
  // accept both and would double-inject. Both hooks share the same
146
146
  // { prependContext } result contract and provide event.prompt.
147
+ //
148
+ // Do NOT rename this a third time. We were originally on
149
+ // `before_prompt_build`, moved to `before_agent_start`, and that is the
150
+ // only reason 2026.7.2 took injection down: `before_prompt_build` is
151
+ // still live in core's PROMPT_INJECTION_HOOK_NAMES and would have
152
+ // survived untouched. Staying on `agent_turn_prepare` (proven in
153
+ // production) — but the lesson is that "newer-sounding hook name" is not
154
+ // evidence of anything. Check core's hook table before moving again.
147
155
  const injectionHook = typeof api
148
156
  .enqueueNextTurnInjection === "function"
149
157
  ? "agent_turn_prepare"
@@ -10,6 +10,13 @@
10
10
  export const AGENT_END_SOURCE = "agent_end";
11
11
  /** Metadata tag on mood-weather roll records (see `recordMoodRoll` in hooks/mood-weather.ts). */
12
12
  export const MOOD_WEATHER_SOURCE = "mood_weather";
13
+ /**
14
+ * Metadata tag on hot-buffer conversation rows (written by hooks/hot-buffer.ts).
15
+ * Readers must select ON this value, never "any tag means not mine" — hot rows
16
+ * were untagged before backend #1921 (2026-07-02) and consumers written against
17
+ * that shape silently match nothing now.
18
+ */
19
+ export const HOT_BUFFER_SOURCE = "hot_buffer";
13
20
  /**
14
21
  * Memories produced by the auto-trace session-end hook are tagged in metadata
15
22
  * as `openclaw_source: "agent_end"` (see `sendTrace` in client.ts). Those should
@@ -2,7 +2,7 @@
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.21.0",
5
+ "version": "0.23.0",
6
6
  "kind": "memory",
7
7
  "contracts": {
8
8
  "tools": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspell/openclaw-hyperspell",
3
- "version": "0.21.1",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Hyperspell memory plugin",
6
6
  "license": "MIT",