@unblocklabs/unblock-memory 0.3.21 → 0.3.23

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.
@@ -42,11 +42,6 @@ export type UnblockMemoryConfig = {
42
42
  enabled: boolean;
43
43
  corpora: readonly string[];
44
44
  };
45
- xsearch: {
46
- enabled: boolean;
47
- corpora: readonly string[];
48
- timeoutMs: number;
49
- };
50
45
  responseAudit: ResponseAuditConfig;
51
46
  peoplePrimer: PeoplePrimerConfig;
52
47
  people: {
@@ -264,7 +264,6 @@ export function resolveConfig(value) {
264
264
  typesafe: { ...DEFAULT_TYPESAFE_CONFIG },
265
265
  qualityAudit: { ...DEFAULT_QUALITY_AUDIT },
266
266
  evidenceReview: { enabled: false, corpora: [] },
267
- xsearch: { enabled: false, corpora: [], timeoutMs: 10000 },
268
267
  responseAudit: resolveResponseAudit(undefined, DEFAULT_CORPORA),
269
268
  peoplePrimer: resolvePeoplePrimer(undefined, DEFAULT_CORPORA, false),
270
269
  people: DEFAULT_PEOPLE_CONFIG,
@@ -276,28 +275,10 @@ export function resolveConfig(value) {
276
275
  throw new Error("unblock-memory config must be an object");
277
276
  }
278
277
  const config = value;
279
- assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "peoplePrimer", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit", "xsearch"], "config");
278
+ assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "peoplePrimer", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit"], "config");
280
279
  const corpora = resolveCorpora(config.corpora);
281
280
  const people = resolvePeople(config.people);
282
281
  const peoplePrimer = resolvePeoplePrimer(config.peoplePrimer, corpora, people.enabled);
283
- let xsearch = { enabled: false, corpora: [], timeoutMs: 10000 };
284
- if (config.xsearch !== undefined) {
285
- const value = config.xsearch;
286
- if (!value || typeof value !== "object" || Array.isArray(value))
287
- throw new Error("xsearch must be an object");
288
- const options = value;
289
- assertOnlyKeys(options, ["enabled", "corpora", "timeoutMs"], "xsearch");
290
- try {
291
- const approved = resolveQualityAudit({ enabled: options.enabled, corpora: options.corpora }, corpora);
292
- xsearch = { enabled: approved.enabled, corpora: approved.corpora,
293
- timeoutMs: positiveInteger(options.timeoutMs, 10000, "xsearch.timeoutMs", 30000) };
294
- }
295
- catch (error) {
296
- if (error instanceof Error)
297
- throw new Error(error.message.replaceAll("qualityAudit", "xsearch"));
298
- throw error;
299
- }
300
- }
301
282
  let evidenceReview = { enabled: false, corpora: [] };
302
283
  if (config.evidenceReview !== undefined) {
303
284
  const value = config.evidenceReview;
@@ -368,7 +349,7 @@ export function resolveConfig(value) {
368
349
  if (skillWhisperer.enabled && !corpora.some((corpus) => corpus.kind === "skills")) {
369
350
  throw new Error('unblock-memory enabled skillWhisperer requires a corpus named "skills" with kind "skills"');
370
351
  }
371
- return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, peoplePrimer, skillWhisperer, xsearch,
352
+ return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, peoplePrimer, skillWhisperer,
372
353
  qualityAudit: resolveQualityAudit(config.qualityAudit, corpora),
373
354
  evidenceReview,
374
355
  responseAudit: resolveResponseAudit(config.responseAudit, corpora),
@@ -24,7 +24,7 @@ export type SessionSearchFilter = {
24
24
  export type MemoryRequestContext = Pick<OpenClawPluginToolContext, "sessionKey" | "sessionId" | "messageChannel" | "agentAccountId" | "nativeChannelId" | "deliveryContext">;
25
25
  export type CorpusSearchOptions = NonNullable<Parameters<MemorySearchManagerContract["search"]>[1]> & {
26
26
  corpora?: readonly string[];
27
- /** Internal hint/reranking budget; oversized matched chunks are omitted, never sliced. */
27
+ /** Internal vector-hint budget; oversized matched chunks are omitted, never sliced. */
28
28
  maxSnippetChars?: number;
29
29
  sessionFilter?: SessionSearchFilter;
30
30
  requestContext?: MemoryRequestContext;
@@ -194,7 +194,6 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
194
194
  };
195
195
  }): MaintenanceTask | undefined;
196
196
  search(query: string, opts?: CorpusSearchOptions): Promise<CorpusMemorySearchResult[]>;
197
- searchBm25(query: string, opts: CorpusSearchOptions): Promise<CorpusMemorySearchResult[]>;
198
197
  searchSkills(query: string, minScore: number, limit: number): Promise<SkillSearchCandidate[]>;
199
198
  readFile(params: {
200
199
  relPath: string;
@@ -14,7 +14,6 @@ import { qualityTaskPresence } from "./quality-triage.js";
14
14
  import { reviewIndexedClaim } from "./evidence-review.js";
15
15
  import { reviewClusterIngestion } from "./cluster-review.js";
16
16
  import { abortable } from "./abortable.js";
17
- import { xsearchBm25 } from "./xsearch-bm25.js";
18
17
  const DEFAULT_READ_LINES = 120;
19
18
  const MAX_READ_CHARS = 12_000;
20
19
  const WATCH_DEBOUNCE_MS = 250;
@@ -835,13 +834,10 @@ export class QmdMemoryManager {
835
834
  expand: false,
836
835
  });
837
836
  opts?.signal?.throwIfAborted();
838
- return this.#searchResults(hits, store, opts);
839
- }
840
- async #searchResults(hits, store, opts, method = "vector") {
841
837
  const tokenizer = store.internal?.llm;
842
838
  const results = [];
843
839
  for (const hit of hits) {
844
- // Hints and reranking must retain the entire matched chunk, even when expanded
840
+ // Proactive hints must retain the entire matched chunk, even when expanded
845
841
  // turn/message context exceeds their budget. Ordinary search is unchanged.
846
842
  if (hit.bestChunk.length > (opts?.maxSnippetChars ?? Infinity))
847
843
  continue;
@@ -865,7 +861,7 @@ export class QmdMemoryManager {
865
861
  path: hit.file,
866
862
  ...span,
867
863
  score: hit.score,
868
- ...(method === "vector" ? { vectorScore: hit.score } : { textScore: hit.score }),
864
+ vectorScore: hit.score,
869
865
  snippet: selected.text,
870
866
  source: "memory",
871
867
  corpus,
@@ -875,22 +871,6 @@ export class QmdMemoryManager {
875
871
  }
876
872
  return results;
877
873
  }
878
- async searchBm25(query, opts) {
879
- if (opts.sources && !opts.sources.includes("memory"))
880
- return [];
881
- const collections = this.#collectionNames(opts.corpora);
882
- opts.signal?.throwIfAborted();
883
- await abortable(this.#operationChain ?? Promise.resolve(), opts.signal);
884
- const sessions = this.#sessions;
885
- if (opts.sessionFilter && sessions && collections.includes(sessions.collection))
886
- await this.#refreshSessionMetadata();
887
- const allowedPaths = opts.sessionFilter && sessions && collections.includes(sessions.collection)
888
- ? sessionAllowedPaths(this.#sessionMetadata, sessions.collection, opts.sessionFilter) : undefined;
889
- const store = await this.#getAnalysisStore();
890
- opts.signal?.throwIfAborted();
891
- const hits = xsearchBm25(store.internal.db, query, collections, opts.maxResults ?? 5, allowedPaths);
892
- return this.#searchResults(hits, store, opts, "bm25");
893
- }
894
874
  async searchSkills(query, minScore, limit) {
895
875
  const collections = this.#skillCollectionNames();
896
876
  if (collections.length === 0)
@@ -1,9 +1,9 @@
1
1
  import { Type, type Static } from "typebox";
2
- export declare const PERSON_DOSSIER_SCHEMA: Type.TObject<{
2
+ declare const PERSON_DOSSIER_SCHEMA: Type.TObject<{
3
3
  schemaVersion: Type.TLiteral<1>;
4
4
  blurb: Type.TString;
5
5
  sections: Type.TArray<Type.TObject<{
6
- category: Type.TUnion<Type.TLiteral<"role" | "priorities" | "preferences" | "successCriteria" | "workingStyle" | "relationship" | "openLoops">[]>;
6
+ category: Type.TEnum<["role", "priorities", "preferences", "successCriteria", "workingStyle", "relationship", "openLoops"]>;
7
7
  claims: Type.TArray<Type.TObject<{
8
8
  statement: Type.TString;
9
9
  evidence: Type.TArray<Type.TObject<{
@@ -16,6 +16,23 @@ export declare const PERSON_DOSSIER_SCHEMA: Type.TObject<{
16
16
  }>>;
17
17
  }>>;
18
18
  }>;
19
+ export declare const PERSON_DOSSIER_WRITE_SCHEMA: Type.TObject<{
20
+ sections: Type.TArray<Type.TObject<{
21
+ category: Type.TEnum<["role", "relationship"]>;
22
+ claims: Type.TArray<Type.TObject<{
23
+ epistemicType: Type.TEnum<["observed", "reported"]>;
24
+ statement: Type.TString;
25
+ evidence: Type.TArray<Type.TObject<{
26
+ source: Type.TUnion<[Type.TLiteral<"session">, Type.TLiteral<"memory">, Type.TLiteral<"directory">, Type.TLiteral<"manual">]>;
27
+ locator: Type.TString;
28
+ observedAt: Type.TOptional<Type.TString>;
29
+ }>>;
30
+ confidence: Type.TOptional<Type.TUnion<[Type.TLiteral<"low">, Type.TLiteral<"medium">, Type.TLiteral<"high">]>>;
31
+ }>>;
32
+ }>>;
33
+ schemaVersion: Type.TLiteral<1>;
34
+ blurb: Type.TString;
35
+ }>;
19
36
  export type PersonDossier = Static<typeof PERSON_DOSSIER_SCHEMA>;
20
37
  export declare class DossierConflictError extends Error {
21
38
  constructor();
@@ -118,7 +135,23 @@ export declare class PeopleStore {
118
135
  listActivePeople(limit?: number, offset?: number): Person[];
119
136
  findIdentity(provider: string, accountScope: string, externalId: string): PersonIdentity | undefined;
120
137
  setInjection(personId: string, enabled: boolean): Person | undefined;
121
- validateDossier(input: unknown): PersonDossier;
138
+ validateDossier(input: unknown): {
139
+ schemaVersion: 1;
140
+ blurb: string;
141
+ sections: {
142
+ category: "role" | "relationship";
143
+ claims: {
144
+ confidence?: "low" | "medium" | "high" | undefined;
145
+ statement: string;
146
+ evidence: {
147
+ observedAt?: string | undefined;
148
+ source: "memory" | "manual" | "session" | "directory";
149
+ locator: string;
150
+ }[];
151
+ epistemicType: "observed" | "reported";
152
+ }[];
153
+ }[];
154
+ };
122
155
  getDossierRevision(personId: string): string | null;
123
156
  replaceDossier(personId: string, reasonInput: string, input: unknown, expectedRevision?: string | null): PersonDossier;
124
157
  deleteDossier(personId: string, reasonInput: string): boolean;
@@ -165,3 +198,4 @@ export declare class PeopleStores {
165
198
  get(agentId: string): PeopleStore;
166
199
  closeAll(): void;
167
200
  }
201
+ export {};
@@ -39,14 +39,25 @@ const claimSchema = Type.Object({
39
39
  ]),
40
40
  confidence: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")])),
41
41
  }, { additionalProperties: false });
42
- export const PERSON_DOSSIER_SCHEMA = Type.Object({
42
+ // Keep the broad schema for legacy dossier and history reads only.
43
+ const PERSON_DOSSIER_SCHEMA = Type.Object({
43
44
  schemaVersion: Type.Literal(1),
44
45
  blurb: Type.String({ minLength: 1, pattern: "\\S" }),
45
46
  sections: Type.Array(Type.Object({
46
- category: Type.Union(BASELINE_DOSSIER_CATEGORIES.map((category) => Type.Literal(category))),
47
+ category: Type.Enum(BASELINE_DOSSIER_CATEGORIES),
47
48
  claims: Type.Array(claimSchema, { minItems: 1, maxItems: 100 }),
48
49
  }, { additionalProperties: false }), { maxItems: BASELINE_DOSSIER_CATEGORIES.length }),
49
50
  }, { additionalProperties: false });
51
+ export const PERSON_DOSSIER_WRITE_SCHEMA = Type.Object({
52
+ ...PERSON_DOSSIER_SCHEMA.properties,
53
+ sections: Type.Array(Type.Object({
54
+ category: Type.Enum(["role", "relationship"]),
55
+ claims: Type.Array(Type.Object({
56
+ ...claimSchema.properties,
57
+ epistemicType: Type.Enum(["observed", "reported"]),
58
+ }, { additionalProperties: false }), { minItems: 1, maxItems: 100 }),
59
+ }, { additionalProperties: false }), { maxItems: 2 }),
60
+ }, { additionalProperties: false });
50
61
  export class DossierConflictError extends Error {
51
62
  constructor() { super("Dossier or person changed during review; inspect again before retrying"); }
52
63
  }
@@ -243,6 +254,15 @@ export class PeopleStore {
243
254
  WHERE provider = ? AND account_scope = ? AND external_id = ?
244
255
  `)
245
256
  .run(optional(input.displayName), optional(input.realName), optional(input.handle), optional(input.avatarUrl), optional(input.title), input.isBot === undefined ? null : Number(input.isBot), input.isDeactivated === undefined ? null : Number(input.isDeactivated), now, input.syncedAt ?? null, provider, accountScope, externalId);
257
+ const displayName = [optional(input.displayName), optional(input.realName)]
258
+ .find(name => name !== null && name !== externalId);
259
+ if (displayName) {
260
+ // Repair generated ID placeholders without renaming an established person.
261
+ this.#db.prepare(`
262
+ UPDATE people SET display_name = ?, updated_at = ?
263
+ WHERE id = ? AND display_name = ? AND preferred_name IS NULL
264
+ `).run(displayName, now, personId, externalId);
265
+ }
246
266
  if (!directorySync) {
247
267
  this.#db
248
268
  .prepare("UPDATE people SET last_seen_at = ?, updated_at = ? WHERE id = ?")
@@ -363,7 +383,7 @@ export class PeopleStore {
363
383
  return row ? person(row) : undefined;
364
384
  }
365
385
  validateDossier(input) {
366
- const dossier = Value.Parse(PERSON_DOSSIER_SCHEMA, input);
386
+ const dossier = Value.Parse(PERSON_DOSSIER_WRITE_SCHEMA, input);
367
387
  this.#validateDossier(dossier);
368
388
  serializeDossier(dossier);
369
389
  return dossier;
@@ -728,12 +748,6 @@ export class PeopleStore {
728
748
  if (backgroundWordCount(dossier.blurb) > PEOPLE_BACKGROUND_MAX_WORDS) {
729
749
  throw new Error(`dossier blurb must not exceed ${PEOPLE_BACKGROUND_MAX_WORDS} words`);
730
750
  }
731
- if (categories.some(category => category !== "role" && category !== "relationship")) {
732
- throw new Error("New dossiers support only role and relationship background; rewrite legacy behavioral profiles");
733
- }
734
- if (dossier.sections.some(section => section.claims.some(claim => claim.epistemicType === "inferred" || claim.epistemicType === "agent_assessment"))) {
735
- throw new Error("Background claims must be explicit observed or reported facts, not inferred profiles");
736
- }
737
751
  }
738
752
  #migrate() {
739
753
  this.#db.exec("BEGIN IMMEDIATE");
@@ -2,7 +2,7 @@ import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
2
2
  import { Type } from "typebox";
3
3
  import { Value } from "typebox/value";
4
4
  import { renderPeopleWhisper } from "./people-hooks.js";
5
- import { DossierConflictError, PERSON_DOSSIER_SCHEMA } from "./people-store.js";
5
+ import { DossierConflictError, PERSON_DOSSIER_WRITE_SCHEMA } from "./people-store.js";
6
6
  import { getContext } from "./tool-context.js";
7
7
  import { reviewPersonDossier } from "./people-dossier-review.js";
8
8
  import { createOpenClawSlackDirectory, syncSlackDirectory, } from "./slack-directory.js";
@@ -77,7 +77,7 @@ const updateParameters = Type.Union([
77
77
  Type.Object({
78
78
  action: Type.Literal("replace_dossier"),
79
79
  personId: nonEmpty,
80
- dossier: PERSON_DOSSIER_SCHEMA,
80
+ dossier: PERSON_DOSSIER_WRITE_SCHEMA,
81
81
  reason: Type.String({ pattern: "\\S", maxLength: 500 }),
82
82
  agentName: Type.Optional(Type.String({ pattern: "\\S", maxLength: 100 })),
83
83
  manualVerification: Type.Optional(Type.String({ pattern: "\\S", maxLength: 400,
@@ -144,7 +144,7 @@ function createInspectTool(stores, config, ctx) {
144
144
  return {
145
145
  name: "memory_people_inspect",
146
146
  label: "Inspect People Memory",
147
- description: "List active people, inspect one person, read dossier change history, or list actionable people todos.",
147
+ description: "List active people, inspect one person, read dossier change history, or list actionable people todos. injectionEligible is a record-level preview, not proof that global hooks or an already-served thread will inject.",
148
148
  parameters: inspectParameters,
149
149
  async execute(_toolCallId, raw) {
150
150
  const input = Value.Parse(inspectParameters, raw);
@@ -198,7 +198,7 @@ function createUpdateTool(stores, config, runtime, ctx) {
198
198
  return {
199
199
  name: "memory_people_update",
200
200
  label: "Update People Memory",
201
- description: "Replace a background-only dossier (blurb <=70 words, role/relationship sections, observed/reported facts). Automatically reviews the blurb against claim evidence qmd://path#Lstart-Lend before saving; blocked/unavailable reviews leave it unchanged. Use explicit manualVerification only after verifying original sources yourself. Also deletes dossiers or updates injection, company, todo and person status.",
201
+ description: "Replace a background-only dossier (blurb <=70 words, role/relationship sections, observed/reported facts). Uses peoplePrimer approval to review the blurb against claim evidence qmd://path#Lstart-Lend before saving; blocked/unavailable reviews leave it unchanged. Use explicit manualVerification only after verifying original sources yourself. Also deletes dossiers or updates injection, company, todo and person status. Restoring a person leaves injection off; dossier changes do not reset thread receipts.",
202
202
  parameters: updateParameters,
203
203
  async execute(_toolCallId, raw, signal) {
204
204
  const input = Value.Parse(updateParameters, raw);
@@ -271,7 +271,7 @@ function createSyncTool(stores, reader, ctx) {
271
271
  return {
272
272
  name: "memory_people_sync",
273
273
  label: "Sync Slack People",
274
- description: "Manually enrich this agent's people store from one OpenClaw-authenticated Slack directory account.",
274
+ description: "Manually enrich this agent's people store from one OpenClaw-authenticated Slack directory account (users:read). At most 200 entries from the directory start, without a continuation cursor. Skips unavailable people; deactivation disables the linked person and injection.",
275
275
  parameters: syncParameters,
276
276
  async execute(_toolCallId, raw) {
277
277
  const input = Value.Parse(syncParameters, raw);
@@ -14,30 +14,32 @@ import { getContext } from "./tool-context.js";
14
14
  import { WhispererDiagnostics } from "./diagnostics.js";
15
15
  import { registerReviewTools } from "./review-tools.js";
16
16
  import { registerResponseAudit } from "./response-runtime.js";
17
- import { rerankXsearch, XSEARCH_MAX_EXCERPT_CHARS } from "./xsearch.js";
18
- import { abortable } from "./abortable.js";
19
17
  const searchParameters = Type.Object({
20
18
  query: Type.String({ pattern: "\\S" }),
21
- corpora: Type.Optional(Type.Array(Type.String({ pattern: "\\S" }), { minItems: 1 })),
19
+ corpora: Type.Optional(Type.Array(Type.String({ pattern: "\\S" }), {
20
+ minItems: 1, description: 'Configured corpus names; default is all non-skill corpora. Use ["all"] alone for explicit all-corpora recall.',
21
+ })),
22
22
  sessionFilter: Type.Optional(Type.Object({
23
23
  startedFrom: Type.Optional(Type.String({
24
+ description: "Inclusive lower bound on session start time, not message or claim dates (ISO 8601).",
24
25
  pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$",
25
26
  })),
26
27
  startedTo: Type.Optional(Type.String({
28
+ description: "Inclusive upper bound on session start time, not message or claim dates (ISO 8601).",
27
29
  pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$",
28
30
  })),
29
31
  provider: Type.Optional(Type.String({ pattern: "\\S" })),
30
32
  chatType: Type.Optional(Type.Union([Type.Literal("channel"), Type.Literal("group"), Type.Literal("direct")])),
31
33
  accountId: Type.Optional(Type.String({ pattern: "\\S" })),
32
34
  conversationId: Type.Optional(Type.String({ pattern: "\\S" })),
33
- }, { additionalProperties: false })),
34
- maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
35
- minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
35
+ }, { additionalProperties: false, description: "Restricts session documents only; selected file corpora remain eligible. Not an audience access control." })),
36
+ maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Maximum hits; default 5." })),
37
+ minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1, description: "Minimum vector similarity; default 0.3. Not confidence in factual truth." })),
36
38
  }, { additionalProperties: false });
37
39
  const getParameters = Type.Object({
38
40
  path: Type.String({ pattern: "\\S" }),
39
- from: Type.Optional(Type.Integer({ minimum: 1 })),
40
- lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
41
+ from: Type.Optional(Type.Integer({ minimum: 1, description: "First source line, 1-based; default 1. Use nextFrom from a truncated read to continue." })),
42
+ lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000, description: "Requested lines; default 120, also bounded by 12,000 content characters." })),
41
43
  }, { additionalProperties: false });
42
44
  const syncSessionsParameters = Type.Object({
43
45
  force: Type.Optional(Type.Boolean()),
@@ -50,7 +52,7 @@ function createSearchTool(runtime, ctx) {
50
52
  return {
51
53
  name: "memory_search",
52
54
  label: "Memory Search",
53
- description: "Search configured memory corpora with semantic vector retrieval. The isolated skills corpus is never included.",
55
+ description: "Search this agent's configured memory corpora with local vector retrieval, not QMD's hybrid query. Skills are excluded. Results are evidence leads; inspect source context with memory_get. Empty results or errors do not prove absence of a fact.",
54
56
  parameters: searchParameters,
55
57
  async execute(_toolCallId, params, signal) {
56
58
  const { query: untrimmedQuery, corpora, sessionFilter, maxResults, minScore, } = Value.Parse(searchParameters, params);
@@ -81,57 +83,6 @@ function createSearchTool(runtime, ctx) {
81
83
  },
82
84
  };
83
85
  }
84
- function createXsearchTool(runtime, ctx, config) {
85
- const active = getContext(ctx);
86
- if (!active)
87
- return null;
88
- return {
89
- name: "memory_xsearch", label: "Hybrid Memory Search",
90
- description: "Search approved memory corpora with vector + BM25 retrieval, deduplicate excerpts, then independently rerank with TypeSafe usefulness scores. Slower than memory_search; use for higher-precision recall. Same session filters; minScore filters final usefulness (0–1), not vector similarity. Requires xsearch opt-in and a TypeSafe key; sends query and approved excerpts to TypeSafe. Skills excluded.",
91
- parameters: searchParameters,
92
- async execute(_id, params, signal) {
93
- const parsed = Value.Parse(searchParameters, params);
94
- const query = parsed.query.trim();
95
- if (!config.xsearch.enabled || !config.typesafe.enabled)
96
- return jsonResult({ status: "disabled", results: [], reason: "Use memory_search instead" });
97
- const requested = parsed.corpora?.map(corpus => corpus.trim());
98
- const corpora = !requested || (requested.length === 1 && requested[0] === "all")
99
- ? [...config.xsearch.corpora] : requested;
100
- if (corpora.some(corpus => !config.xsearch.corpora.includes(corpus))) {
101
- return jsonResult({ status: "unavailable", results: [], reason: "Requested corpus is not approved in xsearch.corpora" });
102
- }
103
- if (query.length > XSEARCH_MAX_EXCERPT_CHARS)
104
- return jsonResult({ status: "unavailable", results: [], reason: "Query exceeds 12000 characters" });
105
- const start = performance.now();
106
- const deadline = AbortSignal.timeout(60_000);
107
- const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
108
- try {
109
- combined.throwIfAborted();
110
- const apiKey = await abortable(resolveTypeSafeApiKey(config.typesafe), combined);
111
- if (!apiKey)
112
- return jsonResult({ status: "unavailable", results: [], reason: "TypeSafe API key not configured; use memory_search" });
113
- const { manager } = await abortable(runtime.getMemorySearchManager(active), combined);
114
- if (!manager)
115
- return jsonResult({ status: "unavailable", results: [], reason: "Memory unavailable" });
116
- const maxResults = parsed.maxResults ?? 5;
117
- const options = { corpora, sessionFilter: parsed.sessionFilter, maxResults: Math.ceil(maxResults * 1.5),
118
- minScore: 0, maxSnippetChars: XSEARCH_MAX_EXCERPT_CHARS, signal: combined, requestContext: active.requestContext };
119
- const [vector, lexical] = await abortable(Promise.all([
120
- manager.search(query, options), manager.searchBm25(query, options),
121
- ]), combined);
122
- const retrievalMs = Math.round(performance.now() - start);
123
- const ranked = await rerankXsearch({ query, sessionFilter: parsed.sessionFilter, vector, lexical, maxResults, minScore: parsed.minScore ?? 0,
124
- apiKey, timeoutMs: config.xsearch.timeoutMs, signal: combined });
125
- return jsonResult({ ...ranked, provider: "unblock-memory", retrievalMs, totalMs: Math.round(performance.now() - start),
126
- results: ranked.results.map(result => result.session ? { ...result,
127
- session: { ...result.session, startedAt: new Date(result.session.startedAt).toISOString() } } : result) });
128
- }
129
- catch {
130
- return jsonResult({ status: "unavailable", results: [], reason: "Hybrid search failed or was cancelled; use memory_search" });
131
- }
132
- },
133
- };
134
- }
135
86
  function createGetTool(runtime, ctx) {
136
87
  const active = getContext(ctx);
137
88
  if (!active)
@@ -139,7 +90,7 @@ function createGetTool(runtime, ctx) {
139
90
  return {
140
91
  name: "memory_get",
141
92
  label: "Memory Get",
142
- description: "Read an exact qmd:// path returned by memory_search or memory_xsearch.",
93
+ description: "Read an exact indexed qmd:// source path returned by memory tools. Defaults to 120 lines, bounded to 12,000 content characters. Check truncated/nextFrom and continue when present; not_found or unavailable is not a successful empty read.",
143
94
  parameters: getParameters,
144
95
  async execute(_toolCallId, params) {
145
96
  const { path: untrimmedPath, from, lines } = Value.Parse(getParameters, params);
@@ -480,7 +431,7 @@ export function registerUnblockMemory(api) {
480
431
  supportsPrivateTranscriptRecall: false,
481
432
  promptBuilder: ({ availableTools }) => availableTools.has("memory_search")
482
433
  ? [
483
- "Use memory_search for relevant past facts, then memory_get when more surrounding context is needed.",
434
+ "Use memory_search for relevant past facts, then memory_get to verify source context, attribution and dates. Follow read continuation when present. Empty search is not proof of absence; historical memory is not current authorization.",
484
435
  ]
485
436
  : [],
486
437
  flushPlanResolver: resolveFlushPlan,
@@ -507,7 +458,6 @@ export function registerUnblockMemory(api) {
507
458
  registerSkillWhisperer(api, runtime, config.skillWhisperer, config.typesafe, diagnostics);
508
459
  registerMemoryWhisperer(api, runtime, config.memoryWhisperer, config.typesafe, diagnostics);
509
460
  api.registerTool((ctx) => createSearchTool(runtime, ctx), { names: ["memory_search"] });
510
- api.registerTool((ctx) => createXsearchTool(runtime, ctx, config), { names: ["memory_xsearch"] });
511
461
  api.registerTool((ctx) => createGetTool(runtime, ctx), { names: ["memory_get"] });
512
462
  api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), {
513
463
  names: ["memory_sync_sessions"],
@@ -95,7 +95,8 @@ export async function syncSlackDirectory(params) {
95
95
  }
96
96
  try {
97
97
  const existing = params.store.findIdentity("slack", params.accountId, externalId);
98
- if (existing && params.store.getPerson(existing.personId)?.status !== "active") {
98
+ const existingPerson = existing ? params.store.getPerson(existing.personId) : undefined;
99
+ if (existing && existingPerson?.status !== "active") {
99
100
  counts.skipped += 1;
100
101
  continue;
101
102
  }
@@ -118,7 +119,7 @@ export async function syncSlackDirectory(params) {
118
119
  });
119
120
  if (result.created)
120
121
  counts.created += 1;
121
- else if (changed)
122
+ else if (changed || result.person.displayName !== existingPerson?.displayName)
122
123
  counts.updated += 1;
123
124
  else
124
125
  counts.unchanged += 1;