@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.
- package/README.md +86 -990
- package/dist/src/config.d.ts +0 -5
- package/dist/src/config.js +2 -21
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/manager.d.ts +0 -1
- package/dist/src/manager.js +2 -22
- package/dist/src/people-store.d.ts +37 -3
- package/dist/src/people-store.js +23 -9
- package/dist/src/people-tools.js +5 -5
- package/dist/src/plugin.js +13 -63
- package/dist/src/slack-directory.js +3 -2
- package/docs/configuration.md +380 -0
- package/docs/peoplesql.md +223 -0
- package/docs/response-audit.md +217 -0
- package/docs/retrieval.md +576 -0
- package/openclaw.plugin.json +7 -23
- package/package.json +6 -2
- package/skills/memory-curator/SKILL.md +5 -0
- package/skills/people-whisperer/SKILL.md +10 -0
- package/dist/src/xsearch-bm25.d.ts +0 -4
- package/dist/src/xsearch-bm25.js +0 -56
- package/dist/src/xsearch.d.ts +0 -62
- package/dist/src/xsearch.js +0 -124
package/dist/src/config.d.ts
CHANGED
|
@@ -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: {
|
package/dist/src/config.js
CHANGED
|
@@ -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"
|
|
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,
|
|
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),
|
package/dist/src/contracts.d.ts
CHANGED
|
@@ -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
|
|
27
|
+
/** Internal vector-hint budget; oversized matched chunks are omitted, never sliced. */
|
|
28
28
|
maxSnippetChars?: number;
|
|
29
29
|
sessionFilter?: SessionSearchFilter;
|
|
30
30
|
requestContext?: MemoryRequestContext;
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/manager.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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):
|
|
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 {};
|
package/dist/src/people-store.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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(
|
|
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");
|
package/dist/src/people-tools.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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).
|
|
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);
|
package/dist/src/plugin.js
CHANGED
|
@@ -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" }), {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|