@unblocklabs/unblock-memory 0.3.28 → 0.3.30

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.
@@ -0,0 +1,8 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ /** The OpenClaw agent database versions whose transcript layout we read. */
3
+ export declare function agentTranscriptSchemaVersion(db: DatabaseSync, errorMessage?: string): number;
4
+ export declare function assertAgentTranscriptIdentity(db: DatabaseSync, agentId: string, version: number, errorMessage?: string): void;
5
+ export declare function assertAgentTranscriptSchema(db: DatabaseSync, agentId: string, errorMessage?: string): void;
6
+ export declare const ACTIVE_EVENTS_FROM = "FROM session_transcript_active_events a\n JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq";
7
+ export declare const ACTIVE_EVENT_COUNT_SQL = "SELECT COUNT(*) n,COALESCE(SUM(length(e.event_json)),0) bytes\n FROM session_transcript_active_events a\n JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq WHERE a.session_id=?";
8
+ export declare const ACTIVE_EVENT_ROWS_SQL = "SELECT e.seq,e.event_json eventJson,e.created_at createdAt\n FROM session_transcript_active_events a\n JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq WHERE a.session_id=? ORDER BY a.active_position";
@@ -0,0 +1,29 @@
1
+ /** The OpenClaw agent database versions whose transcript layout we read. */
2
+ export function agentTranscriptSchemaVersion(db, errorMessage) {
3
+ const version = db.prepare("PRAGMA user_version").get()?.user_version;
4
+ if (typeof version !== "number" || ![17, 18, 19].includes(version)) {
5
+ throw new Error(errorMessage ?? "unsupported OpenClaw agent database schema: expected one of 17, 18, 19, " +
6
+ `found ${String(version ?? "unknown")}`);
7
+ }
8
+ return version;
9
+ }
10
+ export function assertAgentTranscriptIdentity(db, agentId, version, errorMessage) {
11
+ const fail = (message) => { throw new Error(errorMessage ?? message); };
12
+ const meta = db.prepare("SELECT role, schema_version AS schemaVersion, agent_id AS agentId " +
13
+ "FROM schema_meta WHERE meta_key = 'primary' LIMIT 1").get();
14
+ if (meta?.role !== "agent" || meta.schemaVersion !== version) {
15
+ fail("unsupported OpenClaw agent database primary schema metadata");
16
+ }
17
+ if (meta?.agentId !== agentId) {
18
+ fail(`OpenClaw agent database belongs to ${String(meta?.agentId)}, not ${agentId}`);
19
+ }
20
+ }
21
+ export function assertAgentTranscriptSchema(db, agentId, errorMessage) {
22
+ assertAgentTranscriptIdentity(db, agentId, agentTranscriptSchemaVersion(db, errorMessage), errorMessage);
23
+ }
24
+ export const ACTIVE_EVENTS_FROM = `FROM session_transcript_active_events a
25
+ JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq`;
26
+ export const ACTIVE_EVENT_COUNT_SQL = `SELECT COUNT(*) n,COALESCE(SUM(length(e.event_json)),0) bytes
27
+ ${ACTIVE_EVENTS_FROM} WHERE a.session_id=?`;
28
+ export const ACTIVE_EVENT_ROWS_SQL = `SELECT e.seq,e.event_json eventJson,e.created_at createdAt
29
+ ${ACTIVE_EVENTS_FROM} WHERE a.session_id=? ORDER BY a.active_position`;
@@ -0,0 +1 @@
1
+ export declare function dateOption(value: string | undefined, fallback: number): number;
@@ -0,0 +1,8 @@
1
+ export function dateOption(value, fallback) {
2
+ if (value === undefined)
3
+ return fallback;
4
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {
5
+ throw new Error("Dates must be valid YYYY-MM-DD UTC dates");
6
+ }
7
+ return Date.parse(value);
8
+ }
@@ -487,50 +487,43 @@ export class QmdMemoryManager {
487
487
  .filter((source) => selected.has(source.corpus))
488
488
  .map((source) => source.collection);
489
489
  }
490
+ async #updateAndEmbed(store, collection, force) {
491
+ const update = await store.update({ collections: collection ? [collection] : [] });
492
+ this.#cleanupRemovedDocuments?.(update.updated + update.removed);
493
+ const analysisStore = store;
494
+ const changed = update.indexed + update.updated + update.removed > 0 || update.needsEmbedding > 0;
495
+ if ((changed || force) && analysisStore.internal)
496
+ markMemoryAnalysisStale(analysisStore.internal.db);
497
+ const embed = await store.embed({
498
+ ...(collection ? { collection } : {}),
499
+ force,
500
+ chunkStrategy: "semantic",
501
+ });
502
+ this.#recordEmbedding(embed);
503
+ const chunksEmbedded = completedEmbeddingCount(embed);
504
+ if (!changed && !force && chunksEmbedded > 0 && analysisStore.internal) {
505
+ markMemoryAnalysisStale(analysisStore.internal.db);
506
+ }
507
+ return chunksEmbedded;
508
+ }
509
+ async #refreshIndexStatus(store) {
510
+ const status = await store.getStatus();
511
+ const collections = await store.listCollections();
512
+ this.#files = collections.reduce((total, collection) => total + collection.active_count, 0);
513
+ this.#dirty = status.needsEmbedding > 0;
514
+ }
490
515
  sync(params) {
491
516
  const run = async () => {
492
517
  const store = await this.#getStore();
493
518
  this.#dirty = true;
494
- const analysisStore = store;
495
519
  const collections = this.#qmdSources().filter((source) => source.kind !== "sessions");
496
- let analysisMarkedStale = false;
497
- const markAnalysisStale = () => {
498
- if (analysisMarkedStale || !analysisStore.internal)
499
- return;
500
- markMemoryAnalysisStale(analysisStore.internal.db);
501
- analysisMarkedStale = true;
502
- };
503
520
  if (collections.length === 0) {
504
- const update = await store.update({ collections: [] });
505
- this.#cleanupRemovedDocuments?.(update.updated + update.removed);
506
- if (update.indexed + update.updated + update.removed > 0 ||
507
- update.needsEmbedding > 0 || params?.force === true) {
508
- markAnalysisStale();
509
- }
510
- const embed = await store.embed({ force: params?.force, chunkStrategy: "semantic" });
511
- this.#recordEmbedding(embed);
512
- if (completedEmbeddingCount(embed) > 0)
513
- markAnalysisStale();
521
+ await this.#updateAndEmbed(store, undefined, params?.force);
514
522
  }
515
523
  for (const source of collections) {
516
- const update = await store.update({ collections: [source.collection] });
517
- this.#cleanupRemovedDocuments?.(update.updated + update.removed);
518
- const changed = update.indexed + update.updated + update.removed > 0 || update.needsEmbedding > 0;
519
- if (changed || params?.force === true)
520
- markAnalysisStale();
521
- const embed = await store.embed({
522
- collection: source.collection,
523
- force: params?.force,
524
- chunkStrategy: "semantic",
525
- });
526
- this.#recordEmbedding(embed);
527
- if (completedEmbeddingCount(embed) > 0)
528
- markAnalysisStale();
524
+ await this.#updateAndEmbed(store, source.collection, params?.force);
529
525
  }
530
- const status = await store.getStatus();
531
- const indexedCollections = await store.listCollections();
532
- this.#files = indexedCollections.reduce((total, collection) => total + collection.active_count, 0);
533
- this.#dirty = status.needsEmbedding > 0;
526
+ await this.#refreshIndexStatus(store);
534
527
  };
535
528
  return this.#enqueue(run);
536
529
  }
@@ -548,24 +541,7 @@ export class QmdMemoryManager {
548
541
  index: async () => {
549
542
  onPhase?.("indexing");
550
543
  const store = await this.#getStore();
551
- const update = await store.update({ collections: [sessions.collection] });
552
- this.#cleanupRemovedDocuments?.(update.updated + update.removed);
553
- const analysisStore = store;
554
- const invalidatesAnalysis = update.indexed + update.updated + update.removed > 0 ||
555
- update.needsEmbedding > 0;
556
- if (invalidatesAnalysis && analysisStore.internal) {
557
- markMemoryAnalysisStale(analysisStore.internal.db);
558
- }
559
- const embed = await store.embed({
560
- collection: sessions.collection,
561
- chunkStrategy: "semantic",
562
- });
563
- const chunksEmbedded = completedEmbeddingCount(embed);
564
- this.#recordEmbedding(embed);
565
- if (!invalidatesAnalysis && chunksEmbedded > 0 && analysisStore.internal) {
566
- markMemoryAnalysisStale(analysisStore.internal.db);
567
- }
568
- return chunksEmbedded;
544
+ return this.#updateAndEmbed(store, sessions.collection);
569
545
  },
570
546
  });
571
547
  this.#sessionMetadata = sessionMetadataByPath(synced.manifest);
@@ -573,10 +549,7 @@ export class QmdMemoryManager {
573
549
  if (synced.result.skipReason)
574
550
  return synced.result;
575
551
  const store = await this.#getStore();
576
- const status = await store.getStatus();
577
- const collections = await store.listCollections();
578
- this.#files = collections.reduce((total, collection) => total + collection.active_count, 0);
579
- this.#dirty = status.needsEmbedding > 0;
552
+ await this.#refreshIndexStatus(store);
580
553
  return synced.result;
581
554
  });
582
555
  }
@@ -1,8 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { buildSkillWhispererQuery } from "./skill-whisperer.js";
3
2
  import { judgeTypeSafeMemories } from "./typesafe.js";
4
3
  import { resolveTypeSafeApiKey, TypeSafeRequestError } from "./typesafe-client.js";
5
- import { memoryConversation } from "./whisperer-context.js";
4
+ import { buildSkillWhispererQuery, memoryConversation } from "./whisperer-context.js";
6
5
  import { complementaryIndices, reviewMemoryRedundancy } from "./typesafe-review.js";
7
6
  import { MlxQueryGenerator, queryConversation } from "./mlx-query.js";
8
7
  import { judgeTrainingInput, TRAINING_GATE_THRESHOLD } from "./training-gate.js";
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { DatabaseSync } from "node:sqlite";
3
+ import { ACTIVE_EVENT_COUNT_SQL, ACTIVE_EVENT_ROWS_SQL, assertAgentTranscriptSchema } from "./agent-transcript.js";
3
4
  import { messageText } from "./whisperer-context.js";
4
5
  import { responseUserText } from "./response-text.js";
5
6
  export const RESPONSE_EXTRACTOR_VERSION = 5;
@@ -182,11 +183,7 @@ export class ResponseTranscriptReader {
182
183
  this.#db = new DatabaseSync(path, { readOnly: true });
183
184
  try {
184
185
  this.#db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000");
185
- const version = this.#db.prepare("PRAGMA user_version").get()?.user_version;
186
- const meta = this.#db.prepare("SELECT role,agent_id,schema_version FROM schema_meta WHERE meta_key='primary'").get();
187
- if (![17, 18, 19].includes(Number(version)) || meta?.role !== "agent" || meta.agent_id !== agentId || meta.schema_version !== version) {
188
- throw new Error("Unsupported response-audit transcript schema or agent");
189
- }
186
+ assertAgentTranscriptSchema(this.#db, agentId, "Unsupported response-audit transcript schema or agent");
190
187
  }
191
188
  catch (error) {
192
189
  this.#db.close();
@@ -219,14 +216,10 @@ export class ResponseTranscriptReader {
219
216
  return null;
220
217
  const session = { sessionId, accountId: String(window.accountId),
221
218
  conversationId: String(window.conversationId), chatType: String(window.chatType) };
222
- const count = this.#db.prepare(`SELECT COUNT(*) n,COALESCE(SUM(length(e.event_json)),0) bytes
223
- FROM session_transcript_active_events a JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq
224
- WHERE a.session_id=?`).get(session.sessionId);
219
+ const count = this.#db.prepare(ACTIVE_EVENT_COUNT_SQL).get(session.sessionId);
225
220
  if (Number(count.n) > MAX_EVENTS || Number(count.bytes) > MAX_SESSION_BYTES)
226
221
  return undefined;
227
- const rows = this.#db.prepare(`SELECT e.seq,e.event_json eventJson,e.created_at createdAt
228
- FROM session_transcript_active_events a JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq
229
- WHERE a.session_id=? ORDER BY a.active_position`).all(session.sessionId);
222
+ const rows = this.#db.prepare(ACTIVE_EVENT_ROWS_SQL).all(session.sessionId);
230
223
  // Exact active content catches in-place edits and branch changes even when writer
231
224
  // watermarks are absent. Raw text is never retained in the checkpoint database.
232
225
  const revision = hash([RESPONSE_EXTRACTOR_VERSION, session, config.historyMessages, [...config.senderIds].sort(), rows]);
@@ -6,14 +6,7 @@ import { normalizeAgentIdStrict } from "openclaw/plugin-sdk/routing";
6
6
  import { resolveSources } from "./sources.js";
7
7
  import { auditResponses, responseCohort } from "./response-audit.js";
8
8
  import { ResponseAuditStore } from "./response-store.js";
9
- function dateOption(value, fallback) {
10
- if (value === undefined)
11
- return fallback;
12
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {
13
- throw new Error("Dates must be valid YYYY-MM-DD UTC dates");
14
- }
15
- return Date.parse(value);
16
- }
9
+ import { dateOption } from "./date-option.js";
17
10
  export function registerResponseAudit(api, config) {
18
11
  const options = (cfg, id) => {
19
12
  const normalized = normalizeAgentIdStrict(id);
@@ -3,10 +3,10 @@ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
3
3
  import { chmod, mkdir, readFile, rename, unlink, utimes, writeFile } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { DatabaseSync } from "node:sqlite";
6
+ import { ACTIVE_EVENTS_FROM, agentTranscriptSchemaVersion, assertAgentTranscriptIdentity } from "./agent-transcript.js";
6
7
  import { projectSessionDocument, sessionDocumentPath, } from "./session-projector.js";
7
8
  const MANIFEST_VERSION = 1;
8
9
  export const PROJECTOR_VERSION = 7;
9
- const SUPPORTED_SCHEMA_VERSIONS = new Set([17, 18, 19]);
10
10
  // Source lives in src/, published code in dist/src/. Read our own pinned dependency
11
11
  // metadata, not QMD internals (which may also be substituted by runtime inspectors).
12
12
  const sourcePackage = new URL("../package.json", import.meta.url);
@@ -76,12 +76,7 @@ function projectionPath(outputDir, documentPath) {
76
76
  return target;
77
77
  }
78
78
  function assertSchema(db, expectedAgentId) {
79
- const pragma = db.prepare("PRAGMA user_version").get();
80
- const schemaVersion = pragma?.user_version;
81
- if (typeof schemaVersion !== "number" || !SUPPORTED_SCHEMA_VERSIONS.has(schemaVersion)) {
82
- throw new Error("unsupported OpenClaw agent database schema: expected one of 17, 18, 19, " +
83
- `found ${String(schemaVersion ?? "unknown")}`);
84
- }
79
+ const version = agentTranscriptSchemaVersion(db);
85
80
  for (const [table, required] of Object.entries(REQUIRED_COLUMNS)) {
86
81
  const columns = new Set(db.prepare(`PRAGMA table_info(${table})`).all()
87
82
  .map((column) => column.name));
@@ -89,14 +84,7 @@ function assertSchema(db, expectedAgentId) {
89
84
  if (missing)
90
85
  throw new Error(`unsupported OpenClaw agent database: missing ${table}.${missing}`);
91
86
  }
92
- const meta = db.prepare("SELECT role, schema_version AS schemaVersion, agent_id AS agentId " +
93
- "FROM schema_meta WHERE meta_key = 'primary' LIMIT 1").get();
94
- if (meta?.role !== "agent" || meta.schemaVersion !== schemaVersion) {
95
- throw new Error("unsupported OpenClaw agent database primary schema metadata");
96
- }
97
- if (meta.agentId !== expectedAgentId) {
98
- throw new Error(`OpenClaw agent database belongs to ${String(meta.agentId)}, not ${expectedAgentId}`);
99
- }
87
+ assertAgentTranscriptIdentity(db, expectedAgentId, version);
100
88
  }
101
89
  function readSnapshot(params) {
102
90
  const db = new DatabaseSync(params.databasePath, { readOnly: true });
@@ -129,13 +117,11 @@ function readSnapshot(params) {
129
117
  ORDER BY window.created_at, window.session_id
130
118
  `).all(...params.chatTypes);
131
119
  const readEvents = db.prepare(`
132
- SELECT active.session_id AS sessionId, event.event_json AS eventJson,
133
- event.created_at AS createdAt
134
- FROM session_transcript_active_events AS active
135
- JOIN transcript_events AS event
136
- ON event.session_id = active.session_id AND event.seq = active.event_seq
137
- WHERE active.session_id = ?
138
- ORDER BY active.active_position
120
+ SELECT a.session_id AS sessionId, e.event_json AS eventJson,
121
+ e.created_at AS createdAt
122
+ ${ACTIVE_EVENTS_FROM}
123
+ WHERE a.session_id = ?
124
+ ORDER BY a.active_position
139
125
  `);
140
126
  const events = new Map();
141
127
  const changed = new Set();
@@ -12,6 +12,5 @@ type SkillWhispererRuntime = {
12
12
  agentId: string;
13
13
  }, path: string): string | undefined;
14
14
  };
15
- export declare function buildSkillWhispererQuery(prompt: string, messages: readonly unknown[], historyMessages: number): string;
16
15
  export declare function registerSkillWhisperer(api: OpenClawPluginApi, runtime: SkillWhispererRuntime, config: UnblockMemoryConfig["skillWhisperer"], typesafe: UnblockMemoryConfig["typesafe"], diagnostics?: WhispererDiagnostics): Parameters<typeof api.on<"before_prompt_build">>[1] | undefined;
17
16
  export {};
@@ -1,21 +1,13 @@
1
1
  import { basename } from "node:path";
2
2
  import { selectTypeSafeSkill } from "./typesafe.js";
3
3
  import { resolveTypeSafeApiKey, TypeSafeRequestError } from "./typesafe-client.js";
4
- import { messageText } from "./whisperer-context.js";
4
+ import { buildSkillWhispererQuery, messageText } from "./whisperer-context.js";
5
5
  const CANDIDATE_LIMIT = 10;
6
6
  const MAX_QUERY_CHARS = 12_000;
7
7
  const TYPESAFE_CANDIDATE_LIMIT = 3;
8
8
  function isRecord(value) {
9
9
  return value !== null && typeof value === "object" && !Array.isArray(value);
10
10
  }
11
- export function buildSkillWhispererQuery(prompt, messages, historyMessages) {
12
- const availableHistory = messages.flatMap((message) => {
13
- const parsed = messageText(message);
14
- return parsed ? [`${parsed.role}: ${parsed.text}`] : [];
15
- });
16
- const history = historyMessages === 0 ? [] : availableHistory.slice(-historyMessages);
17
- return [...history, `user: ${prompt.trim()}`].join("\n\n").slice(-MAX_QUERY_CHARS);
18
- }
19
11
  function typeSafeConversation(prompt, messages, historyMessages) {
20
12
  const currentRequest = prompt.trim().slice(-MAX_QUERY_CHARS);
21
13
  let remaining = MAX_QUERY_CHARS - currentRequest.length;
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { DatabaseSync } from "node:sqlite";
3
+ import { ACTIVE_EVENT_COUNT_SQL, ACTIVE_EVENT_ROWS_SQL, assertAgentTranscriptSchema } from "./agent-transcript.js";
3
4
  import { messageText } from "./whisperer-context.js";
4
5
  import { conversationUserText } from "./response-text.js";
5
6
  // Identical serialized inputs keep their checkpoints when eligibility broadens.
@@ -133,11 +134,7 @@ export class TrainingTranscriptReader {
133
134
  this.#db = new DatabaseSync(path, { readOnly: true });
134
135
  try {
135
136
  this.#db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000");
136
- const version = this.#db.prepare("PRAGMA user_version").get()?.user_version;
137
- const meta = this.#db.prepare("SELECT role,agent_id,schema_version FROM schema_meta WHERE meta_key='primary'").get();
138
- if (![17, 18, 19].includes(Number(version)) || meta?.role !== "agent" || meta.agent_id !== agentId || meta.schema_version !== version) {
139
- throw new Error("Unsupported training transcript schema or agent");
140
- }
137
+ assertAgentTranscriptSchema(this.#db, agentId, "Unsupported training transcript schema or agent");
141
138
  const columns = this.#db.prepare("PRAGMA table_info(session_windows)").all().map(c => c.name);
142
139
  this.#lineage = ["parent_session_key", "spawned_by", "plugin_owner_id", "hook_external_content_source"].every(c => columns.includes(c));
143
140
  }
@@ -160,14 +157,10 @@ export class TrainingTranscriptReader {
160
157
  /:(?:cron|subagent|heartbeat|hook)(?::|$)/i.test(String(session.session_key)) ||
161
158
  session.parent_session_key || session.spawned_by || session.plugin_owner_id || session.hook_external_content_source)
162
159
  return null;
163
- const size = this.#db.prepare(`SELECT COUNT(*) n,COALESCE(SUM(length(e.event_json)),0) bytes
164
- FROM session_transcript_active_events a JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq
165
- WHERE a.session_id=?`).get(sessionId);
160
+ const size = this.#db.prepare(ACTIVE_EVENT_COUNT_SQL).get(sessionId);
166
161
  if (Number(size.n) > 50_000 || Number(size.bytes) > 32_000_000)
167
162
  return { oversized: true };
168
- const rows = this.#db.prepare(`SELECT e.seq,e.event_json eventJson,e.created_at createdAt
169
- FROM session_transcript_active_events a JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq
170
- WHERE a.session_id=? ORDER BY a.active_position`).iterate(sessionId);
163
+ const rows = this.#db.prepare(ACTIVE_EVENT_ROWS_SQL).iterate(sessionId);
171
164
  return trainingExamples(rows);
172
165
  }
173
166
  finally {
@@ -7,14 +7,7 @@ import { TrainingStore } from "./training-store.js";
7
7
  import { TRAINING_GATE_THRESHOLD } from "./training-gate.js";
8
8
  import { collectTraining, runTraining } from "./training.js";
9
9
  import { generateTrainingQueries, evaluateTrainingQueries, exportQueryTraining, TRAINING_EVALUATION_CONCURRENCY } from "./training-queries.js";
10
- function dateOption(value, fallback) {
11
- if (value === undefined)
12
- return fallback;
13
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {
14
- throw new Error("Dates must be valid YYYY-MM-DD UTC dates");
15
- }
16
- return Date.parse(value);
17
- }
10
+ import { dateOption } from "./date-option.js";
18
11
  function thresholdOption(value) {
19
12
  const result = Number(value);
20
13
  if (!value.trim() || !Number.isFinite(result) || result < 0 || result > 1)
@@ -3,6 +3,7 @@ export declare function messageText(message: unknown): {
3
3
  role: "user" | "assistant";
4
4
  text: string;
5
5
  } | undefined;
6
+ export declare function buildSkillWhispererQuery(prompt: string, messages: readonly unknown[], historyMessages: number): string;
6
7
  export declare function memoryConversation(prompt: string, messages: readonly unknown[]): {
7
8
  currentRequest: string;
8
9
  history: {
@@ -1,3 +1,4 @@
1
+ const MAX_SKILL_QUERY_CHARS = 12_000;
1
2
  /** Only visible user/assistant text; never system, tool, image, or thinking blocks. */
2
3
  export function messageText(message) {
3
4
  if (!message || typeof message !== "object" || !("role" in message) || !("content" in message) ||
@@ -10,6 +11,14 @@ export function messageText(message) {
10
11
  }).join("\n").trim() : "";
11
12
  return text ? { role: message.role, text } : undefined;
12
13
  }
14
+ export function buildSkillWhispererQuery(prompt, messages, historyMessages) {
15
+ const availableHistory = messages.flatMap(message => {
16
+ const parsed = messageText(message);
17
+ return parsed ? [`${parsed.role}: ${parsed.text}`] : [];
18
+ });
19
+ const history = historyMessages === 0 ? [] : availableHistory.slice(-historyMessages);
20
+ return [...history, `user: ${prompt.trim()}`].join("\n\n").slice(-MAX_SKILL_QUERY_CHARS);
21
+ }
13
22
  export function memoryConversation(prompt, messages) {
14
23
  const currentRequest = prompt.trim().slice(-16_000);
15
24
  let remaining = 16_000 - currentRequest.length;
@@ -83,7 +83,7 @@ unknown/future dates. Never infer dates from quoted headings. Copy existing vect
83
83
  only for complete chunks within the safe prefix; BM25 sees that same prefix.
84
84
  Validate returned passages/dates again. No reembedding or temporary transcripts.
85
85
 
86
- QMD 2.10.1 cannot independently set per-method depth through its public search API.
86
+ QMD cannot independently set per-method depth through its public search API.
87
87
  A small discovery adapter retains its tokenization, FTS-highlight chunk selection,
88
88
  source-aware dedup and installed chunk helpers, but requests ten per method and
89
89
  omits query-conditioned scoring. It does not rewrite QMD. Its existing
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "unblock-memory",
3
3
  "name": "Unblock Memory",
4
- "version": "0.3.28",
4
+ "version": "0.3.30",
5
5
  "description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
6
6
  "kind": "memory",
7
7
  "activation": { "onStartup": true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/unblock-memory",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  "preflight": "npm run knip && npm run build && npm run typecheck && npm test && npm run plugin:inspect && npm run plugin:inspect:runtime && npm pack --dry-run"
43
43
  },
44
44
  "dependencies": {
45
- "@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.10.1/unblocklabs-qmd-2.10.1.tgz",
45
+ "@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.10.2/unblocklabs-qmd-2.10.2.tgz",
46
46
  "chokidar": "5.0.0",
47
47
  "picomatch": "^4.0.5",
48
48
  "typebox": "1.3.6"