@unblocklabs/unblock-memory 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ index. It does not re-embed memory, copy vectors, or create another database.
13
13
  From npm:
14
14
 
15
15
  ```bash
16
- openclaw plugins install @unblocklabs/unblock-memory
16
+ openclaw plugins install npm:@unblocklabs/unblock-memory
17
17
  ```
18
18
 
19
19
  Or directly from GitHub:
@@ -22,10 +22,15 @@ Or directly from GitHub:
22
22
  openclaw plugins install git:github.com/unblocklabs-ai/unblock-memory
23
23
  ```
24
24
 
25
+ Install the plugin once on each OpenClaw host, not once per agent. It installs
26
+ its pinned `@unblocklabs/qmd` runtime dependency automatically, so QMD does not
27
+ need to be installed separately. Each agent gets its own QMD index when it first
28
+ uses memory.
29
+
25
30
  ## Configuration
26
31
 
27
- Select the plugin as the memory provider and list any exact Markdown files,
28
- directories, or globs to index:
32
+ Select the plugin as the memory provider and group exact Markdown files,
33
+ directories, or globs into named corpora:
29
34
 
30
35
  ```json5
31
36
  {
@@ -34,14 +39,26 @@ directories, or globs to index:
34
39
  entries: {
35
40
  "unblock-memory": {
36
41
  config: {
37
- paths: [
38
- "MEMORY.md",
39
- "USER.md",
40
- "memory/**/*.md",
41
- "/absolute/shared/**/*.md",
42
+ corpora: [
43
+ {
44
+ name: "memory",
45
+ kind: "files",
46
+ paths: ["MEMORY.md", "USER.md", "memory/**/*.md"],
47
+ },
48
+ {
49
+ name: "projects",
50
+ kind: "files",
51
+ paths: ["/absolute/shared/**/*.md"],
52
+ },
53
+ {
54
+ name: "sessions",
55
+ kind: "sessions",
56
+ chatTypes: ["channel", "group"],
57
+ },
42
58
  ],
59
+ // Optional: omit unless the local analysis worker is installed.
43
60
  analysis: {
44
- executable: "/absolute/path/to/unblock-memory-analysis",
61
+ executable: "/absolute/path/to/unblock-cluster/bin/unblock-memory-analysis",
45
62
  },
46
63
  },
47
64
  },
@@ -51,9 +68,47 @@ directories, or globs to index:
51
68
  ```
52
69
 
53
70
  Relative entries resolve from each agent workspace. Absolute paths and `~/`
54
- paths are supported. A directory means recursive Markdown. When `paths` is
55
- omitted, the defaults are `MEMORY.md`, `USER.md`, and `memory/**/*.md`; an
56
- explicit array replaces those defaults.
71
+ paths are supported. A directory means recursive Markdown. When `corpora` is
72
+ omitted, the plugin creates a `memory` corpus containing `MEMORY.md`, `USER.md`,
73
+ and `memory/**/*.md`. Explicit configuration must include exactly one `memory`
74
+ corpus; other unique names may be added for custom material.
75
+
76
+ `memory_search` searches every configured corpus by default. Pass
77
+ `corpora: ["projects"]` to search selected corpora or `corpora: ["all"]` to
78
+ request all of them explicitly. Search results include their corpus name and
79
+ remain readable by passing the returned `qmd://` path to `memory_get`.
80
+
81
+ Use `sessionFilter` to restrict session results by metadata while leaving file
82
+ corpora searchable. Supported fields are `startedFrom` and `startedTo`
83
+ (inclusive ISO 8601 timestamps), `provider`, `chatType`, `accountId`, and
84
+ `conversationId`:
85
+
86
+ ```json
87
+ {
88
+ "query": "deployment decision",
89
+ "sessionFilter": {
90
+ "startedFrom": "2026-08-01T00:00:00Z",
91
+ "provider": "slack",
92
+ "chatType": "channel"
93
+ }
94
+ }
95
+ ```
96
+
97
+ Provider matching is case-normalized; `chatType` uses the lowercase values
98
+ shown in the configuration example. Account and conversation IDs are trimmed
99
+ and matched exactly. When only `sessions` is selected and no sessions match,
100
+ search returns no results. With other corpora selected, their results remain
101
+ eligible.
102
+
103
+ The optional `sessions` corpus reads the current agent's normal OpenClaw SQLite
104
+ store and indexes its active user/assistant transcript branch. It defaults to
105
+ channel and group conversations; add `direct` explicitly to include DMs. Run
106
+ `memory_sync_sessions` to refresh it. Projections are private derived Markdown
107
+ under the agent's `unblock-memory/sessions` state directory and can be rebuilt
108
+ from OpenClaw at any time. Session results include provider, chat type,
109
+ conversation identity, and start time. They participate in the same search and
110
+ clustering index as file memory. This phase does not sync sessions at startup or
111
+ on a schedule; refreshes are manual through `memory_sync_sessions`.
57
112
 
58
113
  Indexes live at `~/.openclaw/agents/<agentId>/unblock-memory/index.sqlite` (or the
59
114
  equivalent configured OpenClaw state directory). The first lookup builds the
@@ -62,12 +117,30 @@ refresh.
62
117
 
63
118
  ## Memory analysis
64
119
 
65
- Analysis is opt-in and requires the separately installed local analysis worker.
66
- Set `analysis.executable` to its absolute path. The plugin invokes that file
67
- directly with `--db <the agent's known index path>` and, when requested,
68
- `--config-json <validated clustering options>`. Agents cannot choose a database,
120
+ Analysis is opt-in. Core indexing, `memory_search`, and `memory_get` need only
121
+ Unblock Memory and its automatically installed QMD dependency. To enable
122
+ clustering, install the
123
+ [`unblock-cluster`](https://github.com/unblocklabs-ai/unblock-cluster) worker once
124
+ on the same host:
125
+
126
+ ```bash
127
+ git clone https://github.com/unblocklabs-ai/unblock-cluster.git
128
+ cd unblock-cluster
129
+ python3 -m venv .venv
130
+ .venv/bin/python -m pip install -r requirements.txt
131
+ ```
132
+
133
+ Set `analysis.executable` to the absolute path of
134
+ `bin/unblock-memory-analysis` in that checkout. One worker installation can
135
+ serve every agent on the host. The plugin invokes it directly with
136
+ `--db <the agent's known index path>` and, when requested, a validated
137
+ `--config-json <clustering options>` payload. Agents cannot choose a database,
69
138
  executable, shell command, or arbitrary arguments.
70
139
 
140
+ Without the worker, `memory_list_clusters` reports that memory has not been
141
+ analyzed and `memory_recluster` reports that analysis is unavailable. Ordinary
142
+ memory search and reads continue to work.
143
+
71
144
  The analysis worker reads QMD's existing semantic vectors and writes only
72
145
  derived results into three namespaced tables in that same `index.sqlite`:
73
146
 
@@ -100,6 +173,5 @@ A failed rebuild leaves the stale result intact, while a successful rebuild
100
173
  atomically replaces it. Analysis is never scheduled automatically. If the worker
101
174
  is absent or fails, `memory_search` and `memory_get` continue to work.
102
175
 
103
- Session transcripts are intentionally out of scope for this first version. Existing
104
- `unblock-qmd` indexes are derived caches and may be left in place; Unblock Memory
105
- rebuilds its new index from the configured workspace Markdown.
176
+ Existing `unblock-qmd` indexes are derived caches and may be left in place;
177
+ Unblock Memory rebuilds its own index from configured corpora.
@@ -1,8 +1,4 @@
1
1
  import type { QMDStore } from "@unblocklabs/qmd";
2
- export declare const DEFAULT_CLUSTER_LIMIT = 20;
3
- export declare const MAX_CLUSTER_LIMIT = 50;
4
- export declare const DEFAULT_MEMBER_LIMIT = 20;
5
- export declare const MAX_MEMBER_LIMIT = 50;
6
2
  type AnalysisDatabase = QMDStore["internal"]["db"];
7
3
  export type MemoryReclusterOptions = {
8
4
  space?: {
@@ -26,7 +22,7 @@ export type AnalysisRunner = (params: {
26
22
  options?: MemoryReclusterOptions;
27
23
  signal?: AbortSignal;
28
24
  }) => Promise<void>;
29
- export type MemoryAnalysisMember = {
25
+ type MemoryAnalysisMember = {
30
26
  hash: string;
31
27
  seq: number;
32
28
  probability: number;
@@ -52,7 +48,7 @@ export type MemoryAnalysisSummary = {
52
48
  stale: boolean;
53
49
  staleSince: string | null;
54
50
  };
55
- export type MemoryClusterSummary = {
51
+ type MemoryClusterSummary = {
56
52
  clusterId: string;
57
53
  size: number;
58
54
  availableSize: number;
@@ -1,9 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- export const DEFAULT_CLUSTER_LIMIT = 20;
4
- export const MAX_CLUSTER_LIMIT = 50;
5
- export const DEFAULT_MEMBER_LIMIT = 20;
6
- export const MAX_MEMBER_LIMIT = 50;
3
+ const DEFAULT_CLUSTER_LIMIT = 20;
4
+ const MAX_CLUSTER_LIMIT = 50;
5
+ const DEFAULT_MEMBER_LIMIT = 20;
6
+ const MAX_MEMBER_LIMIT = 50;
7
7
  const MAX_EXCERPT_BYTES = 2_000;
8
8
  const MAX_TOTAL_EXCERPT_BYTES = 12_000;
9
9
  const MAX_ALIASES_PER_MEMBER = 5;
@@ -47,6 +47,19 @@ export function ensureMemoryAnalysisSchema(db) {
47
47
 
48
48
  CREATE INDEX IF NOT EXISTS idx_memory_analysis_memberships_cluster
49
49
  ON memory_analysis_memberships(run_id, cluster_id, representative_rank);
50
+
51
+ CREATE VIEW IF NOT EXISTS memory_analysis_available_memberships AS
52
+ SELECT
53
+ m.run_id, m.hash, m.seq, m.cluster_id, m.probability, m.outlier_score,
54
+ m.x, m.y, m.representative_rank, cv.pos, cv.chunk_len, c.doc
55
+ FROM memory_analysis_memberships m
56
+ JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
57
+ JOIN content c ON c.hash = m.hash
58
+ WHERE EXISTS (
59
+ SELECT 1
60
+ FROM documents d
61
+ WHERE d.hash = m.hash AND d.active = 1
62
+ );
50
63
  `);
51
64
  }
52
65
  export function markMemoryAnalysisStale(db) {
@@ -137,10 +150,7 @@ export function latestAnalysisRunId(db) {
137
150
  function count(db, sql, runId) {
138
151
  return db.prepare(sql).get(runId)?.count ?? 0;
139
152
  }
140
- export function readAnalysisSummary(db) {
141
- const run = latestRun(db);
142
- if (!run)
143
- return undefined;
153
+ function analysisSummary(db, run) {
144
154
  const clusters = count(db, "SELECT COUNT(*) AS count FROM memory_analysis_clusters WHERE run_id = ?", run.id);
145
155
  const members = count(db, "SELECT COUNT(*) AS count FROM memory_analysis_memberships WHERE run_id = ?", run.id);
146
156
  const expectedNonNoise = count(db, "SELECT COALESCE(SUM(size), 0) AS count FROM memory_analysis_clusters WHERE run_id = ?", run.id);
@@ -171,6 +181,14 @@ export function readAnalysisSummary(db) {
171
181
  staleSince: run.stale_at,
172
182
  };
173
183
  }
184
+ export function readAnalysisSummary(db) {
185
+ const run = latestRun(db);
186
+ return run ? analysisSummary(db, run) : undefined;
187
+ }
188
+ function latestValidRun(db) {
189
+ const run = latestRun(db);
190
+ return run && analysisSummary(db, run) ? run : undefined;
191
+ }
174
192
  function sourcePaths(db, hash, limit) {
175
193
  if (limit <= 0)
176
194
  return [];
@@ -202,16 +220,9 @@ function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTE
202
220
  const rows = db.prepare(`
203
221
  SELECT
204
222
  m.hash, m.seq, m.probability, m.outlier_score, m.x, m.y,
205
- m.representative_rank, cv.pos, cv.chunk_len, c.doc
206
- FROM memory_analysis_memberships m
207
- JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
208
- JOIN content c ON c.hash = m.hash
223
+ m.representative_rank, m.pos, m.chunk_len, m.doc
224
+ FROM memory_analysis_available_memberships m
209
225
  WHERE m.run_id = ? AND m.cluster_id = ?
210
- AND EXISTS (
211
- SELECT 1
212
- FROM documents d
213
- WHERE d.hash = m.hash AND d.active = 1
214
- )
215
226
  ORDER BY ${noiseOrder}
216
227
  LIMIT ?
217
228
  `).all(runId, clusterId, limit);
@@ -240,15 +251,8 @@ function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTE
240
251
  function availableSize(db, runId, clusterId) {
241
252
  return db.prepare(`
242
253
  SELECT COUNT(*) AS count
243
- FROM memory_analysis_memberships m
244
- JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
245
- JOIN content c ON c.hash = m.hash
246
- WHERE m.run_id = ? AND m.cluster_id = ?
247
- AND EXISTS (
248
- SELECT 1
249
- FROM documents d
250
- WHERE d.hash = m.hash AND d.active = 1
251
- )
254
+ FROM memory_analysis_available_memberships
255
+ WHERE run_id = ? AND cluster_id = ?
252
256
  `).get(runId, clusterId)?.count ?? 0;
253
257
  }
254
258
  function readMetadata(run) {
@@ -295,22 +299,20 @@ function noiseRow(db, runId) {
295
299
  SELECT
296
300
  -1 AS cluster_id,
297
301
  COUNT(*) AS size,
298
- COUNT(CASE WHEN cv.hash IS NOT NULL AND c.hash IS NOT NULL AND EXISTS (
299
- SELECT 1
300
- FROM documents d
301
- WHERE d.hash = m.hash AND d.active = 1
302
- ) THEN 1 END) AS available_size,
302
+ (
303
+ SELECT COUNT(*)
304
+ FROM memory_analysis_available_memberships available
305
+ WHERE available.run_id = ? AND available.cluster_id = -1
306
+ ) AS available_size,
303
307
  COALESCE(AVG(m.probability), 0) AS mean_probability
304
308
  FROM memory_analysis_memberships m
305
- LEFT JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
306
- LEFT JOIN content c ON c.hash = m.hash
307
309
  WHERE m.run_id = ? AND m.cluster_id = -1
308
310
  HAVING COUNT(*) > 0
309
- `).get(runId);
311
+ `).get(runId, runId);
310
312
  }
311
313
  export function readClusters(db, requestedLimit = DEFAULT_CLUSTER_LIMIT) {
312
- const run = latestRun(db);
313
- if (!run || !readAnalysisSummary(db)) {
314
+ const run = latestValidRun(db);
315
+ if (!run) {
314
316
  return { status: "not_analyzed", ...readMetadata(), clusters: [], noise: null };
315
317
  }
316
318
  const limit = Math.max(1, Math.min(MAX_CLUSTER_LIMIT, Math.floor(requestedLimit)));
@@ -318,19 +320,14 @@ export function readClusters(db, requestedLimit = DEFAULT_CLUSTER_LIMIT) {
318
320
  SELECT
319
321
  c.cluster_id,
320
322
  c.size,
321
- COUNT(CASE WHEN cv.hash IS NOT NULL AND content_row.hash IS NOT NULL AND EXISTS (
322
- SELECT 1
323
- FROM documents d
324
- WHERE d.hash = m.hash AND d.active = 1
325
- ) THEN 1 END) AS available_size,
323
+ (
324
+ SELECT COUNT(*)
325
+ FROM memory_analysis_available_memberships available
326
+ WHERE available.run_id = c.run_id AND available.cluster_id = c.cluster_id
327
+ ) AS available_size,
326
328
  c.mean_probability
327
329
  FROM memory_analysis_clusters c
328
- LEFT JOIN memory_analysis_memberships m
329
- ON m.run_id = c.run_id AND m.cluster_id = c.cluster_id
330
- LEFT JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
331
- LEFT JOIN content content_row ON content_row.hash = m.hash
332
330
  WHERE c.run_id = ?
333
- GROUP BY c.cluster_id, c.size, c.mean_probability
334
331
  ORDER BY c.size DESC, c.cluster_id
335
332
  LIMIT ?
336
333
  `).all(run.id, limit);
@@ -366,8 +363,8 @@ function resolveClusterId(db, runId, reference) {
366
363
  return clusterIds.find((row) => clusterReference(runId, row.cluster_id) === reference)?.cluster_id;
367
364
  }
368
365
  export function readCluster(db, clusterReferenceId, requestedLimit = DEFAULT_MEMBER_LIMIT) {
369
- const run = latestRun(db);
370
- if (!run || !readAnalysisSummary(db)) {
366
+ const run = latestValidRun(db);
367
+ if (!run) {
371
368
  return { status: "not_analyzed", ...readMetadata() };
372
369
  }
373
370
  const metadata = readMetadata(run);
@@ -1,8 +1,22 @@
1
- export declare const DEFAULT_PATHS: readonly ["MEMORY.md", "USER.md", "memory/**/*.md"];
2
- export type UnblockMemoryConfig = {
1
+ export type FileCorpusConfig = {
2
+ name: string;
3
+ kind: "files";
3
4
  paths: readonly string[];
5
+ };
6
+ declare const CHAT_TYPES: readonly ["channel", "group", "direct"];
7
+ export type ChatType = typeof CHAT_TYPES[number];
8
+ type SessionCorpusConfig = {
9
+ name: "sessions";
10
+ kind: "sessions";
11
+ chatTypes: readonly ChatType[];
12
+ };
13
+ export type CorpusConfig = FileCorpusConfig | SessionCorpusConfig;
14
+ export declare const DEFAULT_CORPORA: readonly FileCorpusConfig[];
15
+ export type UnblockMemoryConfig = {
16
+ corpora: readonly CorpusConfig[];
4
17
  analysis: {
5
18
  executable?: string;
6
19
  };
7
20
  };
8
21
  export declare function resolveConfig(value: unknown): UnblockMemoryConfig;
22
+ export {};
@@ -1,27 +1,90 @@
1
1
  import { isAbsolute } from "node:path";
2
- export const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
3
- export function resolveConfig(value) {
4
- if (!value || typeof value !== "object") {
5
- return { paths: DEFAULT_PATHS, analysis: {} };
2
+ const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
3
+ const CHAT_TYPES = ["channel", "group", "direct"];
4
+ export const DEFAULT_CORPORA = [{
5
+ name: "memory",
6
+ kind: "files",
7
+ paths: DEFAULT_PATHS,
8
+ }];
9
+ function assertOnlyKeys(value, allowed, label) {
10
+ const unknown = Object.keys(value).find((key) => !allowed.includes(key));
11
+ if (unknown)
12
+ throw new Error(`unblock-memory ${label} has unknown property: ${unknown}`);
13
+ }
14
+ function resolveCorpora(value) {
15
+ if (value === undefined)
16
+ return DEFAULT_CORPORA;
17
+ if (!Array.isArray(value) || value.length === 0) {
18
+ throw new Error("unblock-memory corpora must be a non-empty array");
6
19
  }
7
- const config = value;
8
- let paths = DEFAULT_PATHS;
9
- if (config.paths !== undefined) {
10
- if (!Array.isArray(config.paths) || !config.paths.every((entry) => typeof entry === "string" && entry.trim())) {
11
- throw new Error("unblock-memory paths must be an array of non-empty strings");
20
+ const names = new Set();
21
+ const corpora = value.map((entry, index) => {
22
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
23
+ throw new Error(`unblock-memory corpora[${index}] must be an object`);
24
+ }
25
+ const corpus = entry;
26
+ if (typeof corpus.name !== "string" || !corpus.name.trim()) {
27
+ throw new Error(`unblock-memory corpora[${index}].name must be a non-empty string`);
28
+ }
29
+ const name = corpus.name.trim();
30
+ if (name === "all") {
31
+ throw new Error(`unblock-memory corpus name is reserved: ${name}`);
32
+ }
33
+ if (names.has(name))
34
+ throw new Error(`unblock-memory corpus names must be unique: ${name}`);
35
+ names.add(name);
36
+ if (corpus.kind === "sessions") {
37
+ assertOnlyKeys(corpus, ["name", "kind", "chatTypes"], `corpora[${index}]`);
38
+ if (name !== "sessions") {
39
+ throw new Error('unblock-memory session corpus must be named "sessions"');
40
+ }
41
+ const chatTypes = corpus.chatTypes ?? ["channel", "group"];
42
+ if (!Array.isArray(chatTypes) || chatTypes.length === 0 ||
43
+ !chatTypes.every((chatType) => CHAT_TYPES.includes(chatType))) {
44
+ throw new Error(`unblock-memory corpus sessions chatTypes must contain channel, group, or direct`);
45
+ }
46
+ return { name: "sessions", kind: "sessions", chatTypes: [...new Set(chatTypes)] };
47
+ }
48
+ assertOnlyKeys(corpus, ["name", "kind", "paths"], `corpora[${index}]`);
49
+ if (name === "sessions") {
50
+ throw new Error('unblock-memory corpus named "sessions" must have kind "sessions"');
12
51
  }
13
- paths = config.paths.map((entry) => entry.trim());
52
+ if (corpus.kind !== "files") {
53
+ throw new Error(`unblock-memory corpus ${name} must have kind "files" or "sessions"`);
54
+ }
55
+ if (!Array.isArray(corpus.paths) || corpus.paths.length === 0 ||
56
+ !corpus.paths.every((path) => typeof path === "string" && path.trim())) {
57
+ throw new Error(`unblock-memory corpus ${name} paths must be a non-empty array of non-empty strings`);
58
+ }
59
+ return { name, kind: "files", paths: corpus.paths.map((path) => path.trim()) };
60
+ });
61
+ if (corpora.filter((corpus) => corpus.name === "memory").length !== 1) {
62
+ throw new Error('unblock-memory corpora must contain exactly one corpus named "memory"');
14
63
  }
64
+ return corpora;
65
+ }
66
+ export function resolveConfig(value) {
67
+ if (value === undefined || value === null) {
68
+ return { corpora: DEFAULT_CORPORA, analysis: {} };
69
+ }
70
+ if (typeof value !== "object" || Array.isArray(value)) {
71
+ throw new Error("unblock-memory config must be an object");
72
+ }
73
+ const config = value;
74
+ assertOnlyKeys(config, ["corpora", "analysis"], "config");
75
+ const corpora = resolveCorpora(config.corpora);
15
76
  if (config.analysis === undefined)
16
- return { paths, analysis: {} };
17
- if (!config.analysis || typeof config.analysis !== "object") {
77
+ return { corpora, analysis: {} };
78
+ if (!config.analysis || typeof config.analysis !== "object" || Array.isArray(config.analysis)) {
18
79
  throw new Error("unblock-memory analysis must be an object");
19
80
  }
20
- const configured = config.analysis.executable;
81
+ const analysis = config.analysis;
82
+ assertOnlyKeys(analysis, ["executable"], "analysis");
83
+ const configured = analysis.executable;
21
84
  if (configured === undefined)
22
- return { paths, analysis: {} };
85
+ return { corpora, analysis: {} };
23
86
  if (typeof configured !== "string" || !configured.trim() || !isAbsolute(configured.trim())) {
24
87
  throw new Error("unblock-memory analysis.executable must be an absolute non-empty path");
25
88
  }
26
- return { paths, analysis: { executable: configured.trim() } };
89
+ return { corpora, analysis: { executable: configured.trim() } };
27
90
  }
@@ -1,9 +1,27 @@
1
1
  import type { MemoryPluginCapability } from "openclaw/plugin-sdk/memory-host-core";
2
+ import type { SessionMetadata } from "./session-projector.js";
3
+ import type { ChatType } from "./config.js";
2
4
  export type MemoryPluginRuntimeContract = NonNullable<MemoryPluginCapability["runtime"]>;
3
5
  type ManagerLookup = Awaited<ReturnType<MemoryPluginRuntimeContract["getMemorySearchManager"]>>;
4
6
  export type MemorySearchManagerContract = NonNullable<ManagerLookup["manager"]>;
5
7
  export type MemoryProviderStatus = ReturnType<MemorySearchManagerContract["status"]>;
6
8
  export type MemorySearchResult = Awaited<ReturnType<MemorySearchManagerContract["search"]>>[number];
9
+ export type CorpusMemorySearchResult = MemorySearchResult & {
10
+ corpus: string;
11
+ session?: SessionMetadata;
12
+ };
13
+ export type SessionSearchFilter = {
14
+ startedFrom?: string;
15
+ startedTo?: string;
16
+ provider?: string;
17
+ chatType?: ChatType;
18
+ accountId?: string;
19
+ conversationId?: string;
20
+ };
21
+ export type CorpusSearchOptions = NonNullable<Parameters<MemorySearchManagerContract["search"]>[1]> & {
22
+ corpora?: readonly string[];
23
+ sessionFilter?: SessionSearchFilter;
24
+ };
7
25
  export type MemoryEmbeddingProbeResult = Awaited<ReturnType<MemorySearchManagerContract["probeEmbeddingAvailability"]>>;
8
26
  export type MemorySyncParams = Parameters<NonNullable<MemorySearchManagerContract["sync"]>>[0];
9
27
  export type MemoryReadResult = {
@@ -1,8 +1,20 @@
1
1
  import type { QMDStore } from "@unblocklabs/qmd";
2
2
  import { type AnalysisRunner, type MemoryAnalysisSummary, type MemoryClusterDetail, type MemoryClusterList, type MemoryReclusterOptions } from "./analysis.js";
3
- import type { MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemorySearchManagerContract, MemorySearchResult, MemorySyncParams } from "./contracts.js";
3
+ import type { CorpusMemorySearchResult, CorpusSearchOptions, MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
4
+ import type { ChatType } from "./config.js";
5
+ import { type SessionSyncResult } from "./session-sync.js";
4
6
  import { type ResolvedSource } from "./sources.js";
5
7
  export type ManagerStore = Pick<QMDStore, "update" | "embed" | "getStatus" | "listCollections" | "searchLex" | "vsearch" | "get" | "getDocumentBody" | "close">;
8
+ export type ManagerSessionConfig = {
9
+ agentId: string;
10
+ agentName: string;
11
+ chatTypes: readonly ChatType[];
12
+ collection: string;
13
+ databasePath: string;
14
+ manifestPath: string;
15
+ outputDir: string;
16
+ timezone: string;
17
+ };
6
18
  export declare function enableSecureDelete(store: QMDStore): void;
7
19
  export declare function cleanupRemovedDocuments(store: QMDStore, changedDocuments?: number): number;
8
20
  export declare function pruneStaleCollections(store: QMDStore, configuredCollections: ReadonlySet<string>): Promise<number>;
@@ -21,22 +33,18 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
21
33
  storeFactory?: () => Promise<ManagerStore>;
22
34
  analysisExecutable?: string;
23
35
  analysisRunner?: AnalysisRunner;
36
+ sessions?: ManagerSessionConfig;
24
37
  });
25
38
  start(): Promise<void>;
26
39
  sync(params?: MemorySyncParams): Promise<void>;
40
+ syncSessions(force?: boolean): Promise<SessionSyncResult>;
27
41
  recluster(options?: MemoryReclusterOptions, signal?: AbortSignal): Promise<MemoryAnalysisSummary>;
28
42
  listClusters(limit?: number): Promise<MemoryClusterList>;
29
43
  fetchCluster(params: {
30
44
  clusterId: string;
31
45
  topK?: number;
32
46
  }): Promise<MemoryClusterDetail>;
33
- search(query: string, opts?: {
34
- maxResults?: number;
35
- minScore?: number;
36
- lexicalOnly?: boolean;
37
- sources?: Array<"memory" | "sessions">;
38
- signal?: AbortSignal;
39
- }): Promise<MemorySearchResult[]>;
47
+ search(query: string, opts?: CorpusSearchOptions): Promise<CorpusMemorySearchResult[]>;
40
48
  readFile(params: {
41
49
  relPath: string;
42
50
  from?: number;