@unblocklabs/unblock-memory 0.3.8 → 0.3.9
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 +49 -11
- package/dist/src/config.js +1 -13
- package/dist/src/people-hooks.js +9 -6
- package/dist/src/people-store.d.ts +19 -4
- package/dist/src/people-store.js +153 -66
- package/dist/src/people-tools.js +66 -39
- package/openclaw.plugin.json +1 -9
- package/package.json +1 -1
- package/skills/people-whisperer/SKILL.md +105 -0
- package/dist/src/people-evidence.d.ts +0 -21
- package/dist/src/people-evidence.js +0 -126
- package/dist/src/people-refinement.d.ts +0 -14
- package/dist/src/people-refinement.js +0 -30
package/README.md
CHANGED
|
@@ -145,11 +145,10 @@ message content. Other channels are ignored.
|
|
|
145
145
|
|
|
146
146
|
PeopleSQL registers three tools when enabled:
|
|
147
147
|
|
|
148
|
-
- `memory_people_inspect` reads one exact person,
|
|
149
|
-
|
|
150
|
-
- `memory_people_update` replaces or deletes dossiers,
|
|
151
|
-
|
|
152
|
-
restoration state; and
|
|
148
|
+
- `memory_people_inspect` lists active people, reads one exact person, reads one
|
|
149
|
+
person's dossier change history, or lists bounded actionable todos;
|
|
150
|
+
- `memory_people_update` replaces or deletes dossiers, toggles one person's
|
|
151
|
+
injection, and manages company, todo, deletion, or restoration state; and
|
|
153
152
|
- the optional `memory_people_sync` enriches one active OpenClaw Slack account;
|
|
154
153
|
its tool input accepts an account ID, not a token.
|
|
155
154
|
|
|
@@ -160,12 +159,24 @@ may need to be allowed explicitly. The sync is bounded to
|
|
|
160
159
|
keeps only normalized ID, name, handle, and avatar fields. Slack requires the
|
|
161
160
|
`users:read` scope.
|
|
162
161
|
|
|
163
|
-
The agent owns dossier generation and refresh.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
The agent owns dossier generation and refresh. It can list people, inspect one
|
|
163
|
+
person's current dossier, search ordinary memory and sessions with
|
|
164
|
+
`memory_search`/`memory_get`, and replace the dossier when that would improve a
|
|
165
|
+
future conversation. The plugin owns no dossier-maintenance workflow or refresh
|
|
166
|
+
schedule. A dossier's `reviewedAt` value records its last successful write; it
|
|
167
|
+
is not scheduling state. The plugin performs no model call for dossier
|
|
168
|
+
maintenance or prompt injection.
|
|
169
|
+
|
|
170
|
+
Every `replace_dossier` and `delete_dossier` action requires a concise `reason`.
|
|
171
|
+
The plugin transactionally records that reason with its authoritative before and
|
|
172
|
+
after dossier snapshots. List small newest-first summaries with
|
|
173
|
+
`memory_people_inspect({ view: "dossier_changes", personId, limit?, offset? })`,
|
|
174
|
+
then fetch one exact diff with
|
|
175
|
+
`memory_people_inspect({ view: "dossier_change", personId, changeId })`. List
|
|
176
|
+
responses include `nextOffset`, so all history remains reachable without loading
|
|
177
|
+
many dossiers into one tool result. Because the injected snippet is the dossier's
|
|
178
|
+
`blurb`, its changes are included in the same history. A complete new serialized
|
|
179
|
+
dossier is capped at 64 KiB; larger legacy dossiers remain readable and repairable.
|
|
169
180
|
|
|
170
181
|
Set `people.whisperer.enabled` to inject context. For each exact Slack sender,
|
|
171
182
|
the plugin prepends that person's stored dossier blurb, bounded by `maxChars`,
|
|
@@ -175,6 +186,33 @@ Unthreaded DMs use their OpenClaw session as the conversational scope. Unknown,
|
|
|
175
186
|
unavailable, disabled, or dossierless people produce no context. Injection
|
|
176
187
|
remains subject to OpenClaw's `allowPromptInjection` policy.
|
|
177
188
|
|
|
189
|
+
The package includes a `$people-whisperer` skill with the canonical agent
|
|
190
|
+
procedure and dossier shape. For a manual refresh, ask:
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
Use $people-whisperer to improve your understanding of this person. Search memory
|
|
194
|
+
and recent sessions, inspect their current PeopleSQL dossier, and update it only
|
|
195
|
+
if the result would make future conversations meaningfully better.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For an optional cron or isolated agent session, use this goal:
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
Use $people-whisperer to improve your understanding of people you interact with.
|
|
202
|
+
|
|
203
|
+
Search recent sessions and memory for meaningful information about people. Inspect
|
|
204
|
+
their existing PeopleSQL dossiers when useful. Update a dossier only when doing so
|
|
205
|
+
would make future conversations meaningfully better. Ignore routine conversation,
|
|
206
|
+
repetition, and weak inference. You may update several people or nobody.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Choose any cadence appropriate for the agent; the plugin does not require or
|
|
210
|
+
track one. If session transcripts are a source, configure a `sessions` corpus
|
|
211
|
+
(including `direct` when DMs matter) and refresh it with
|
|
212
|
+
`memory_sync_sessions`. Ordinary `memory_search` calls accept targeted queries,
|
|
213
|
+
corpora, session metadata filters, score thresholds, and up to 20 results per
|
|
214
|
+
call; People Whisperer itself imposes no evidence-window limit.
|
|
215
|
+
|
|
178
216
|
Use `sessionFilter` to restrict session results by metadata while leaving file
|
|
179
217
|
corpora searchable. Supported fields are `startedFrom` and `startedTo`
|
|
180
218
|
(inclusive ISO 8601 timestamps), `provider`, `chatType`, `accountId`, and
|
package/dist/src/config.js
CHANGED
|
@@ -114,22 +114,10 @@ function resolvePeople(value) {
|
|
|
114
114
|
throw new Error("unblock-memory people must be an object");
|
|
115
115
|
}
|
|
116
116
|
const people = value;
|
|
117
|
-
assertOnlyKeys(people, ["enabled", "
|
|
117
|
+
assertOnlyKeys(people, ["enabled", "whisperer", "todos"], "people");
|
|
118
118
|
const enabled = people.enabled ?? false;
|
|
119
119
|
if (typeof enabled !== "boolean")
|
|
120
120
|
throw new Error("unblock-memory people.enabled must be a boolean");
|
|
121
|
-
// Accepted only so existing installations can upgrade without first rewriting config.
|
|
122
|
-
const legacyRefinement = people.refinement;
|
|
123
|
-
if (legacyRefinement !== undefined) {
|
|
124
|
-
if (!legacyRefinement ||
|
|
125
|
-
typeof legacyRefinement !== "object" ||
|
|
126
|
-
Array.isArray(legacyRefinement)) {
|
|
127
|
-
throw new Error("unblock-memory people.refinement must be an object");
|
|
128
|
-
}
|
|
129
|
-
const legacy = legacyRefinement;
|
|
130
|
-
assertOnlyKeys(legacy, ["maxPeoplePerRun"], "people.refinement");
|
|
131
|
-
positiveInteger(legacy.maxPeoplePerRun, 10, "people.refinement.maxPeoplePerRun", 50);
|
|
132
|
-
}
|
|
133
121
|
const whisperer = people.whisperer ?? {};
|
|
134
122
|
if (!whisperer || typeof whisperer !== "object" || Array.isArray(whisperer)) {
|
|
135
123
|
throw new Error("unblock-memory people.whisperer must be an object");
|
package/dist/src/people-hooks.js
CHANGED
|
@@ -45,11 +45,11 @@ export function registerPeopleHooks(api, stores, config) {
|
|
|
45
45
|
if (threadKey) {
|
|
46
46
|
const identityKey = promptIdentityKey(sessionKey, accountScope, externalId);
|
|
47
47
|
if (runId) {
|
|
48
|
-
pendingThreadByIdentity.delete(identityKey);
|
|
49
48
|
threadByRun.set(runId, threadKey);
|
|
50
49
|
}
|
|
51
50
|
else {
|
|
52
|
-
pendingThreadByIdentity.
|
|
51
|
+
const pendingThreadKey = pendingThreadByIdentity.get(identityKey);
|
|
52
|
+
pendingThreadByIdentity.set(identityKey, pendingThreadKey === undefined || pendingThreadKey === threadKey ? threadKey : null);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -94,12 +94,15 @@ export function registerPeopleHooks(api, stores, config) {
|
|
|
94
94
|
if (!sessionKey || !parsed || !accountScope || !externalId || !runId)
|
|
95
95
|
return;
|
|
96
96
|
const identityKey = promptIdentityKey(sessionKey, accountScope, externalId);
|
|
97
|
-
const
|
|
98
|
-
const
|
|
97
|
+
const mappedThreadKey = threadByRun.get(runId);
|
|
98
|
+
const pendingThreadKey = mappedThreadKey ? undefined : pendingThreadByIdentity.get(identityKey);
|
|
99
|
+
if (!mappedThreadKey && pendingThreadByIdentity.has(identityKey)) {
|
|
100
|
+
pendingThreadByIdentity.delete(identityKey);
|
|
101
|
+
}
|
|
102
|
+
const threadKey = mappedThreadKey ?? pendingThreadKey ?? undefined;
|
|
99
103
|
if (!threadKey)
|
|
100
104
|
return;
|
|
101
|
-
if (pendingThreadKey) {
|
|
102
|
-
pendingThreadByIdentity.delete(identityKey);
|
|
105
|
+
if (typeof pendingThreadKey === "string") {
|
|
103
106
|
threadByRun.set(runId, pendingThreadKey);
|
|
104
107
|
}
|
|
105
108
|
try {
|
|
@@ -17,6 +17,19 @@ export declare const PERSON_DOSSIER_SCHEMA: Type.TObject<{
|
|
|
17
17
|
}>>;
|
|
18
18
|
}>;
|
|
19
19
|
export type PersonDossier = Static<typeof PERSON_DOSSIER_SCHEMA>;
|
|
20
|
+
export type PersonDossierChange = {
|
|
21
|
+
id: string;
|
|
22
|
+
personId: string;
|
|
23
|
+
action: "replace" | "delete";
|
|
24
|
+
beforeDossier: PersonDossier | null;
|
|
25
|
+
afterDossier: PersonDossier | null;
|
|
26
|
+
reason: string;
|
|
27
|
+
changedAt: string;
|
|
28
|
+
};
|
|
29
|
+
export type PersonDossierChangeSummary = Omit<PersonDossierChange, "beforeDossier" | "afterDossier"> & {
|
|
30
|
+
beforeDossierBytes: number | null;
|
|
31
|
+
afterDossierBytes: number | null;
|
|
32
|
+
};
|
|
20
33
|
export type Person = {
|
|
21
34
|
id: string;
|
|
22
35
|
displayName: string;
|
|
@@ -97,12 +110,11 @@ export declare class PeopleStore {
|
|
|
97
110
|
name: string;
|
|
98
111
|
primaryDomain?: string;
|
|
99
112
|
}): Company | undefined;
|
|
100
|
-
listActivePeople(): Person[];
|
|
113
|
+
listActivePeople(limit?: number, offset?: number): Person[];
|
|
101
114
|
findIdentity(provider: string, accountScope: string, externalId: string): PersonIdentity | undefined;
|
|
102
115
|
setInjection(personId: string, enabled: boolean): Person | undefined;
|
|
103
|
-
replaceDossier(personId: string,
|
|
104
|
-
deleteDossier(personId: string): boolean;
|
|
105
|
-
listProcessedEvidenceLocators(personId: string, source: "session"): Set<string>;
|
|
116
|
+
replaceDossier(personId: string, reasonInput: string, input: unknown): PersonDossier;
|
|
117
|
+
deleteDossier(personId: string, reasonInput: string): boolean;
|
|
106
118
|
getWhisperReceipt(threadKey: string, personId: string): {
|
|
107
119
|
runId: string;
|
|
108
120
|
contribution: string;
|
|
@@ -122,6 +134,9 @@ export declare class PeopleStore {
|
|
|
122
134
|
dossier: PersonDossier;
|
|
123
135
|
reviewedAt: string;
|
|
124
136
|
} | undefined;
|
|
137
|
+
getDossierReviewedAt(personId: string): string | undefined;
|
|
138
|
+
listDossierChanges(personId: string, limit?: number, offset?: number): PersonDossierChangeSummary[];
|
|
139
|
+
getDossierChange(personId: string, changeId: string): PersonDossierChange | undefined;
|
|
125
140
|
getDossierBlurb(personId: string): string | undefined;
|
|
126
141
|
softDeletePerson(personId: string): Person | undefined;
|
|
127
142
|
restorePerson(personId: string): Person | undefined;
|
package/dist/src/people-store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
3
4
|
import { join, dirname } from "node:path";
|
|
@@ -46,6 +47,17 @@ export const PERSON_DOSSIER_SCHEMA = Type.Object({
|
|
|
46
47
|
claims: Type.Array(claimSchema, { minItems: 1, maxItems: 100 }),
|
|
47
48
|
}, { additionalProperties: false }), { maxItems: BASELINE_DOSSIER_CATEGORIES.length }),
|
|
48
49
|
}, { additionalProperties: false });
|
|
50
|
+
const MAX_DOSSIER_JSON_BYTES = 64 * 1024;
|
|
51
|
+
function serializeDossier(dossier) {
|
|
52
|
+
const json = JSON.stringify(dossier);
|
|
53
|
+
if (Buffer.byteLength(json, "utf8") > MAX_DOSSIER_JSON_BYTES) {
|
|
54
|
+
throw new Error(`dossier must serialize to at most ${MAX_DOSSIER_JSON_BYTES} bytes`);
|
|
55
|
+
}
|
|
56
|
+
return json;
|
|
57
|
+
}
|
|
58
|
+
function parseDossierJson(json) {
|
|
59
|
+
return Value.Parse(PERSON_DOSSIER_SCHEMA, JSON.parse(json));
|
|
60
|
+
}
|
|
49
61
|
const OVERFLOW_KEY = "__people_todo_overflow__";
|
|
50
62
|
function required(value, label) {
|
|
51
63
|
const normalized = value.trim();
|
|
@@ -56,6 +68,12 @@ function required(value, label) {
|
|
|
56
68
|
function optional(value) {
|
|
57
69
|
return value?.trim() || null;
|
|
58
70
|
}
|
|
71
|
+
function dossierReason(value) {
|
|
72
|
+
const reason = required(value, "reason");
|
|
73
|
+
if (reason.length > 1000)
|
|
74
|
+
throw new Error("reason must not exceed 1000 characters");
|
|
75
|
+
return reason;
|
|
76
|
+
}
|
|
59
77
|
function person(row) {
|
|
60
78
|
return {
|
|
61
79
|
id: row.id,
|
|
@@ -290,14 +308,15 @@ export class PeopleStore {
|
|
|
290
308
|
throw error;
|
|
291
309
|
}
|
|
292
310
|
}
|
|
293
|
-
listActivePeople() {
|
|
311
|
+
listActivePeople(limit = 50, offset = 0) {
|
|
294
312
|
return this.#db
|
|
295
313
|
.prepare(`
|
|
296
314
|
SELECT * FROM people
|
|
297
315
|
WHERE status = 'active'
|
|
298
316
|
ORDER BY last_seen_at DESC, id
|
|
317
|
+
LIMIT ? OFFSET ?
|
|
299
318
|
`)
|
|
300
|
-
.all()
|
|
319
|
+
.all(limit, offset)
|
|
301
320
|
.map((row) => person(row));
|
|
302
321
|
}
|
|
303
322
|
findIdentity(provider, accountScope, externalId) {
|
|
@@ -312,20 +331,24 @@ export class PeopleStore {
|
|
|
312
331
|
const row = this.#db.prepare("SELECT * FROM people WHERE id = ?").get(personId);
|
|
313
332
|
return row ? person(row) : undefined;
|
|
314
333
|
}
|
|
315
|
-
replaceDossier(personId,
|
|
316
|
-
const dossier =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
334
|
+
replaceDossier(personId, reasonInput, input) {
|
|
335
|
+
const dossier = Value.Parse(PERSON_DOSSIER_SCHEMA, input);
|
|
336
|
+
this.#validateDossier(dossier);
|
|
337
|
+
const dossierJson = serializeDossier(dossier);
|
|
338
|
+
const reason = dossierReason(reasonInput);
|
|
320
339
|
const reviewedAt = new Date().toISOString();
|
|
321
340
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
322
341
|
try {
|
|
323
342
|
const target = this.#db.prepare("SELECT id FROM people WHERE id = ?").get(personId);
|
|
324
343
|
if (!target)
|
|
325
344
|
throw new Error(`person not found: ${personId}`);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
345
|
+
const existing = this.#db
|
|
346
|
+
.prepare("SELECT dossier_json FROM person_dossiers WHERE person_id = ?")
|
|
347
|
+
.get(personId);
|
|
348
|
+
if (existing)
|
|
349
|
+
parseDossierJson(existing.dossier_json);
|
|
350
|
+
this.#db
|
|
351
|
+
.prepare(`
|
|
329
352
|
INSERT INTO person_dossiers (person_id, dossier_json, blurb, reviewed_at)
|
|
330
353
|
VALUES (?, ?, ?, ?)
|
|
331
354
|
ON CONFLICT(person_id) DO UPDATE SET
|
|
@@ -333,36 +356,50 @@ export class PeopleStore {
|
|
|
333
356
|
blurb = excluded.blurb,
|
|
334
357
|
reviewed_at = excluded.reviewed_at
|
|
335
358
|
`)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
consume.run(personId, locator, reviewedAt);
|
|
345
|
-
}
|
|
359
|
+
.run(personId, dossierJson, dossier.blurb, reviewedAt);
|
|
360
|
+
this.#db
|
|
361
|
+
.prepare(`
|
|
362
|
+
INSERT INTO person_dossier_changes
|
|
363
|
+
(id, person_id, action, before_dossier_json, after_dossier_json, reason, changed_at)
|
|
364
|
+
VALUES (?, ?, 'replace', ?, ?, ?, ?)
|
|
365
|
+
`)
|
|
366
|
+
.run(randomUUID(), personId, existing?.dossier_json ?? null, dossierJson, reason, reviewedAt);
|
|
346
367
|
this.#db.exec("COMMIT");
|
|
347
|
-
return dossier
|
|
368
|
+
return dossier;
|
|
348
369
|
}
|
|
349
370
|
catch (error) {
|
|
350
371
|
this.#db.exec("ROLLBACK");
|
|
351
372
|
throw error;
|
|
352
373
|
}
|
|
353
374
|
}
|
|
354
|
-
deleteDossier(personId) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
375
|
+
deleteDossier(personId, reasonInput) {
|
|
376
|
+
const reason = dossierReason(reasonInput);
|
|
377
|
+
const changedAt = new Date().toISOString();
|
|
378
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
379
|
+
try {
|
|
380
|
+
const existing = this.#db
|
|
381
|
+
.prepare("SELECT dossier_json FROM person_dossiers WHERE person_id = ?")
|
|
382
|
+
.get(personId);
|
|
383
|
+
if (!existing) {
|
|
384
|
+
this.#db.exec("COMMIT");
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
parseDossierJson(existing.dossier_json);
|
|
388
|
+
this.#db.prepare("DELETE FROM person_dossiers WHERE person_id = ?").run(personId);
|
|
389
|
+
this.#db
|
|
390
|
+
.prepare(`
|
|
391
|
+
INSERT INTO person_dossier_changes
|
|
392
|
+
(id, person_id, action, before_dossier_json, after_dossier_json, reason, changed_at)
|
|
393
|
+
VALUES (?, ?, 'delete', ?, NULL, ?, ?)
|
|
394
|
+
`)
|
|
395
|
+
.run(randomUUID(), personId, existing.dossier_json, reason, changedAt);
|
|
396
|
+
this.#db.exec("COMMIT");
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
this.#db.exec("ROLLBACK");
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
366
403
|
}
|
|
367
404
|
getWhisperReceipt(threadKey, personId) {
|
|
368
405
|
const row = this.#db
|
|
@@ -395,11 +432,64 @@ export class PeopleStore {
|
|
|
395
432
|
.get(personId);
|
|
396
433
|
return row
|
|
397
434
|
? {
|
|
398
|
-
dossier:
|
|
435
|
+
dossier: parseDossierJson(row.dossier_json),
|
|
399
436
|
reviewedAt: row.reviewed_at,
|
|
400
437
|
}
|
|
401
438
|
: undefined;
|
|
402
439
|
}
|
|
440
|
+
getDossierReviewedAt(personId) {
|
|
441
|
+
const row = this.#db
|
|
442
|
+
.prepare("SELECT reviewed_at FROM person_dossiers WHERE person_id = ?")
|
|
443
|
+
.get(personId);
|
|
444
|
+
return row?.reviewed_at;
|
|
445
|
+
}
|
|
446
|
+
listDossierChanges(personId, limit = 20, offset = 0) {
|
|
447
|
+
return this.#db
|
|
448
|
+
.prepare(`
|
|
449
|
+
SELECT id, person_id, action, reason, changed_at,
|
|
450
|
+
CASE WHEN before_dossier_json IS NULL THEN NULL
|
|
451
|
+
ELSE length(CAST(before_dossier_json AS BLOB)) END AS before_dossier_bytes,
|
|
452
|
+
CASE WHEN after_dossier_json IS NULL THEN NULL
|
|
453
|
+
ELSE length(CAST(after_dossier_json AS BLOB)) END AS after_dossier_bytes
|
|
454
|
+
FROM person_dossier_changes
|
|
455
|
+
WHERE person_id = ?
|
|
456
|
+
ORDER BY changed_at DESC, rowid DESC
|
|
457
|
+
LIMIT ? OFFSET ?
|
|
458
|
+
`)
|
|
459
|
+
.all(personId, limit, offset)
|
|
460
|
+
.map((value) => {
|
|
461
|
+
const row = value;
|
|
462
|
+
return {
|
|
463
|
+
id: row.id,
|
|
464
|
+
personId: row.person_id,
|
|
465
|
+
action: row.action,
|
|
466
|
+
beforeDossierBytes: row.before_dossier_bytes,
|
|
467
|
+
afterDossierBytes: row.after_dossier_bytes,
|
|
468
|
+
reason: row.reason,
|
|
469
|
+
changedAt: row.changed_at,
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
getDossierChange(personId, changeId) {
|
|
474
|
+
const row = this.#db
|
|
475
|
+
.prepare("SELECT * FROM person_dossier_changes WHERE person_id = ? AND id = ?")
|
|
476
|
+
.get(personId, changeId);
|
|
477
|
+
return row
|
|
478
|
+
? {
|
|
479
|
+
id: row.id,
|
|
480
|
+
personId: row.person_id,
|
|
481
|
+
action: row.action,
|
|
482
|
+
beforeDossier: row.before_dossier_json === null
|
|
483
|
+
? null
|
|
484
|
+
: parseDossierJson(row.before_dossier_json),
|
|
485
|
+
afterDossier: row.after_dossier_json === null
|
|
486
|
+
? null
|
|
487
|
+
: parseDossierJson(row.after_dossier_json),
|
|
488
|
+
reason: row.reason,
|
|
489
|
+
changedAt: row.changed_at,
|
|
490
|
+
}
|
|
491
|
+
: undefined;
|
|
492
|
+
}
|
|
403
493
|
getDossierBlurb(personId) {
|
|
404
494
|
const row = this.#db
|
|
405
495
|
.prepare("SELECT blurb FROM person_dossiers WHERE person_id = ?")
|
|
@@ -591,36 +681,29 @@ export class PeopleStore {
|
|
|
591
681
|
throw new Error("dossier sections must have unique categories");
|
|
592
682
|
}
|
|
593
683
|
}
|
|
594
|
-
#validateEvidenceLocators(locators) {
|
|
595
|
-
const unique = [...new Set(locators.map((locator) => locator.trim()))];
|
|
596
|
-
const invalid = unique.find((locator) => !/^session:.+:event:\d+$/.test(locator));
|
|
597
|
-
if (invalid !== undefined)
|
|
598
|
-
throw new Error(`invalid session evidence locator: ${invalid}`);
|
|
599
|
-
return unique;
|
|
600
|
-
}
|
|
601
684
|
#migrate() {
|
|
602
685
|
const current = this.#db.prepare("PRAGMA user_version").get();
|
|
603
|
-
if (current.user_version ===
|
|
686
|
+
if (current.user_version === 4)
|
|
604
687
|
return;
|
|
605
|
-
if (current.user_version !== 0 &&
|
|
688
|
+
if (current.user_version !== 0 &&
|
|
689
|
+
current.user_version !== 1 &&
|
|
690
|
+
current.user_version !== 2 &&
|
|
691
|
+
current.user_version !== 3) {
|
|
606
692
|
throw new Error(`unsupported PeopleSQL schema version: ${current.user_version}`);
|
|
607
693
|
}
|
|
608
694
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
609
695
|
try {
|
|
610
|
-
if (current.user_version ===
|
|
696
|
+
if (current.user_version === 2) {
|
|
697
|
+
this.#db.exec(`
|
|
698
|
+
DROP TABLE person_evidence_receipts;
|
|
699
|
+
`);
|
|
700
|
+
}
|
|
701
|
+
else if (current.user_version === 1) {
|
|
611
702
|
this.#db.exec(`
|
|
612
703
|
DROP INDEX people_policy_seen;
|
|
613
704
|
ALTER TABLE people DROP COLUMN refinement_enabled;
|
|
614
705
|
UPDATE people SET injection_enabled = 1 WHERE status = 'active';
|
|
615
706
|
|
|
616
|
-
CREATE TABLE person_evidence_receipts (
|
|
617
|
-
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
618
|
-
source TEXT NOT NULL,
|
|
619
|
-
locator TEXT NOT NULL,
|
|
620
|
-
processed_at TEXT NOT NULL,
|
|
621
|
-
PRIMARY KEY (person_id, source, locator)
|
|
622
|
-
) STRICT;
|
|
623
|
-
|
|
624
707
|
CREATE TABLE person_whisper_receipts (
|
|
625
708
|
thread_key TEXT NOT NULL,
|
|
626
709
|
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
@@ -629,13 +712,10 @@ export class PeopleStore {
|
|
|
629
712
|
injected_at TEXT NOT NULL,
|
|
630
713
|
PRIMARY KEY (thread_key, person_id)
|
|
631
714
|
) STRICT;
|
|
632
|
-
|
|
633
|
-
PRAGMA user_version = 2;
|
|
634
715
|
`);
|
|
635
|
-
this.#db.exec("COMMIT");
|
|
636
|
-
return;
|
|
637
716
|
}
|
|
638
|
-
|
|
717
|
+
else if (current.user_version === 0) {
|
|
718
|
+
this.#db.exec(`
|
|
639
719
|
CREATE TABLE companies (
|
|
640
720
|
id TEXT PRIMARY KEY,
|
|
641
721
|
name TEXT NOT NULL,
|
|
@@ -695,14 +775,6 @@ export class PeopleStore {
|
|
|
695
775
|
resolution_note TEXT
|
|
696
776
|
) STRICT;
|
|
697
777
|
|
|
698
|
-
CREATE TABLE person_evidence_receipts (
|
|
699
|
-
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
700
|
-
source TEXT NOT NULL,
|
|
701
|
-
locator TEXT NOT NULL,
|
|
702
|
-
processed_at TEXT NOT NULL,
|
|
703
|
-
PRIMARY KEY (person_id, source, locator)
|
|
704
|
-
) STRICT;
|
|
705
|
-
|
|
706
778
|
CREATE TABLE person_whisper_receipts (
|
|
707
779
|
thread_key TEXT NOT NULL,
|
|
708
780
|
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
@@ -714,7 +786,22 @@ export class PeopleStore {
|
|
|
714
786
|
|
|
715
787
|
CREATE INDEX people_status_seen ON people(status, last_seen_at);
|
|
716
788
|
CREATE INDEX people_todos_status_seen ON people_todos(status, last_seen_at);
|
|
717
|
-
|
|
789
|
+
`);
|
|
790
|
+
}
|
|
791
|
+
this.#db.exec(`
|
|
792
|
+
CREATE TABLE person_dossier_changes (
|
|
793
|
+
id TEXT PRIMARY KEY,
|
|
794
|
+
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
795
|
+
action TEXT NOT NULL CHECK (action IN ('replace', 'delete')),
|
|
796
|
+
before_dossier_json TEXT,
|
|
797
|
+
after_dossier_json TEXT,
|
|
798
|
+
reason TEXT NOT NULL,
|
|
799
|
+
changed_at TEXT NOT NULL
|
|
800
|
+
) STRICT;
|
|
801
|
+
|
|
802
|
+
CREATE INDEX person_dossier_changes_person_changed
|
|
803
|
+
ON person_dossier_changes(person_id, changed_at DESC);
|
|
804
|
+
PRAGMA user_version = 4;
|
|
718
805
|
`);
|
|
719
806
|
this.#db.exec("COMMIT");
|
|
720
807
|
}
|
package/dist/src/people-tools.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
2
|
-
import { resolveAgentDir } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
3
2
|
import { Type } from "typebox";
|
|
4
3
|
import { Value } from "typebox/value";
|
|
5
4
|
import { renderPeopleWhisper } from "./people-hooks.js";
|
|
6
|
-
import { nextPeopleRefinement } from "./people-refinement.js";
|
|
7
5
|
import { PERSON_DOSSIER_SCHEMA } from "./people-store.js";
|
|
8
6
|
import { createOpenClawSlackDirectory, syncSlackDirectory, } from "./slack-directory.js";
|
|
9
7
|
const nonEmpty = Type.String({ pattern: "\\S", maxLength: 1000 });
|
|
@@ -36,16 +34,36 @@ const inspectParameters = Type.Union([
|
|
|
36
34
|
}),
|
|
37
35
|
}, { additionalProperties: false }),
|
|
38
36
|
Type.Object({
|
|
39
|
-
view: Type.Literal("
|
|
37
|
+
view: Type.Literal("people"),
|
|
40
38
|
limit: Type.Optional(Type.Integer({
|
|
41
39
|
minimum: 1,
|
|
42
40
|
maximum: 100,
|
|
43
|
-
description: "Maximum
|
|
41
|
+
description: "Maximum active people to return.",
|
|
42
|
+
})),
|
|
43
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "People to skip." })),
|
|
44
|
+
}, { additionalProperties: false }),
|
|
45
|
+
Type.Object({
|
|
46
|
+
view: Type.Literal("dossier_changes"),
|
|
47
|
+
personId: nonEmpty,
|
|
48
|
+
limit: Type.Optional(Type.Integer({
|
|
49
|
+
minimum: 1,
|
|
50
|
+
maximum: 100,
|
|
51
|
+
description: "Maximum dossier change summaries to return, newest first.",
|
|
44
52
|
})),
|
|
53
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "Changes to skip." })),
|
|
54
|
+
}, { additionalProperties: false }),
|
|
55
|
+
Type.Object({
|
|
56
|
+
view: Type.Literal("dossier_change"),
|
|
57
|
+
personId: nonEmpty,
|
|
58
|
+
changeId: nonEmpty,
|
|
45
59
|
}, { additionalProperties: false }),
|
|
46
60
|
Type.Object({
|
|
47
|
-
view: Type.Literal("
|
|
48
|
-
|
|
61
|
+
view: Type.Literal("todos"),
|
|
62
|
+
limit: Type.Optional(Type.Integer({
|
|
63
|
+
minimum: 1,
|
|
64
|
+
maximum: 100,
|
|
65
|
+
description: "Maximum actionable todos to return.",
|
|
66
|
+
})),
|
|
49
67
|
}, { additionalProperties: false }),
|
|
50
68
|
]);
|
|
51
69
|
const updateParameters = Type.Union([
|
|
@@ -57,16 +75,13 @@ const updateParameters = Type.Union([
|
|
|
57
75
|
Type.Object({
|
|
58
76
|
action: Type.Literal("replace_dossier"),
|
|
59
77
|
personId: nonEmpty,
|
|
60
|
-
dossier:
|
|
61
|
-
|
|
62
|
-
minItems: 1,
|
|
63
|
-
maxItems: 50,
|
|
64
|
-
uniqueItems: true,
|
|
65
|
-
})),
|
|
78
|
+
dossier: PERSON_DOSSIER_SCHEMA,
|
|
79
|
+
reason: nonEmpty,
|
|
66
80
|
}, { additionalProperties: false }),
|
|
67
81
|
Type.Object({
|
|
68
82
|
action: Type.Literal("delete_dossier"),
|
|
69
83
|
personId: nonEmpty,
|
|
84
|
+
reason: nonEmpty,
|
|
70
85
|
}, { additionalProperties: false }),
|
|
71
86
|
Type.Object({
|
|
72
87
|
action: Type.Literal("set_company"),
|
|
@@ -94,7 +109,7 @@ const syncParameters = Type.Object({
|
|
|
94
109
|
}, { additionalProperties: false });
|
|
95
110
|
function context(ctx) {
|
|
96
111
|
const cfg = ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
|
|
97
|
-
return cfg && ctx.agentId ? { agentId: ctx.agentId
|
|
112
|
+
return cfg && ctx.agentId ? { agentId: ctx.agentId } : undefined;
|
|
98
113
|
}
|
|
99
114
|
function personView(stores, agentId, selector, maxChars) {
|
|
100
115
|
const store = stores.get(agentId);
|
|
@@ -124,33 +139,49 @@ function createInspectTool(stores, config, ctx) {
|
|
|
124
139
|
return {
|
|
125
140
|
name: "memory_people_inspect",
|
|
126
141
|
label: "Inspect People Memory",
|
|
127
|
-
description: "
|
|
142
|
+
description: "List active people, inspect one person, read dossier change history, or list actionable people todos.",
|
|
128
143
|
parameters: inspectParameters,
|
|
129
144
|
async execute(_toolCallId, raw) {
|
|
130
145
|
const input = Value.Parse(inspectParameters, raw);
|
|
131
146
|
if (input.view === "person") {
|
|
132
147
|
return jsonResult(personView(stores, active.agentId, input, config.whisperer.maxChars));
|
|
133
148
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
const store = stores.get(active.agentId);
|
|
150
|
+
if (input.view === "people") {
|
|
151
|
+
const limit = input.limit ?? 50;
|
|
152
|
+
const offset = input.offset ?? 0;
|
|
153
|
+
const people = store.listActivePeople(limit, offset).map((person) => {
|
|
154
|
+
const dossierReviewedAt = store.getDossierReviewedAt(person.id);
|
|
155
|
+
return {
|
|
156
|
+
person,
|
|
157
|
+
identities: store.listIdentities(person.id),
|
|
158
|
+
hasDossier: dossierReviewedAt !== undefined,
|
|
159
|
+
dossierReviewedAt: dossierReviewedAt ?? null,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
return jsonResult({
|
|
163
|
+
status: "ok",
|
|
164
|
+
people,
|
|
165
|
+
nextOffset: people.length === limit ? offset + people.length : null,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (input.view === "dossier_changes") {
|
|
169
|
+
const limit = input.limit ?? 20;
|
|
170
|
+
const offset = input.offset ?? 0;
|
|
171
|
+
const changes = store.listDossierChanges(input.personId, limit, offset);
|
|
172
|
+
return jsonResult({
|
|
173
|
+
status: "ok",
|
|
174
|
+
changes,
|
|
175
|
+
nextOffset: changes.length === limit ? offset + changes.length : null,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (input.view === "dossier_change") {
|
|
179
|
+
const change = store.getDossierChange(input.personId, input.changeId);
|
|
180
|
+
return jsonResult(change ? { status: "ok", change } : { status: "not_found" });
|
|
150
181
|
}
|
|
151
182
|
return jsonResult({
|
|
152
183
|
status: "ok",
|
|
153
|
-
todos:
|
|
184
|
+
todos: store.listTodos(input.limit ?? 20),
|
|
154
185
|
});
|
|
155
186
|
},
|
|
156
187
|
};
|
|
@@ -172,14 +203,8 @@ function createUpdateTool(stores, ctx) {
|
|
|
172
203
|
return jsonResult(person ? { status: "ok", person } : { status: "not_found" });
|
|
173
204
|
}
|
|
174
205
|
if (input.action === "replace_dossier") {
|
|
175
|
-
if (input.dossier === undefined && input.consumedEvidenceLocators === undefined) {
|
|
176
|
-
return jsonResult({
|
|
177
|
-
status: "invalid",
|
|
178
|
-
error: "replace_dossier requires a dossier or consumed evidence locators",
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
206
|
try {
|
|
182
|
-
const dossier = store.replaceDossier(input.personId, input.
|
|
207
|
+
const dossier = store.replaceDossier(input.personId, input.reason, input.dossier);
|
|
183
208
|
return jsonResult({ status: "ok", dossier });
|
|
184
209
|
}
|
|
185
210
|
catch (error) {
|
|
@@ -190,7 +215,9 @@ function createUpdateTool(stores, ctx) {
|
|
|
190
215
|
}
|
|
191
216
|
}
|
|
192
217
|
if (input.action === "delete_dossier") {
|
|
193
|
-
return jsonResult(store.deleteDossier(input.personId
|
|
218
|
+
return jsonResult(store.deleteDossier(input.personId, input.reason)
|
|
219
|
+
? { status: "ok" }
|
|
220
|
+
: { status: "not_found" });
|
|
194
221
|
}
|
|
195
222
|
if (input.action === "set_company") {
|
|
196
223
|
const company = store.setCompany(input.personId, {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.9",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|
|
@@ -138,14 +138,6 @@
|
|
|
138
138
|
"additionalProperties": false,
|
|
139
139
|
"properties": {
|
|
140
140
|
"enabled": { "type": "boolean", "default": false },
|
|
141
|
-
"refinement": {
|
|
142
|
-
"type": "object",
|
|
143
|
-
"description": "Deprecated compatibility field; accepted but ignored.",
|
|
144
|
-
"additionalProperties": false,
|
|
145
|
-
"properties": {
|
|
146
|
-
"maxPeoplePerRun": { "type": "integer", "minimum": 1, "maximum": 50 }
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
141
|
"whisperer": {
|
|
150
142
|
"type": "object",
|
|
151
143
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: people-whisperer
|
|
3
|
+
description: Maintain useful PeopleSQL dossiers from ordinary memory and session evidence so future conversations start with accurate person context.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# People Whisperer
|
|
7
|
+
|
|
8
|
+
Improve the agent's durable understanding of people it interacts with. Prefer no
|
|
9
|
+
write over routine, repetitive, weakly inferred, or already captured information.
|
|
10
|
+
The goal is a useful future conversation, not processing every interaction.
|
|
11
|
+
|
|
12
|
+
## Choose and inspect
|
|
13
|
+
|
|
14
|
+
- For a named or current person, call `memory_people_inspect` with
|
|
15
|
+
`view: "person"` and their `personId` or exact Slack identity.
|
|
16
|
+
- For autonomous maintenance, call `memory_people_inspect` with
|
|
17
|
+
`view: "people"` and an optional `limit`. Use the returned identity,
|
|
18
|
+
`lastSeenAt`, dossier presence, and dossier `reviewedAt` only as context for
|
|
19
|
+
your judgment. `reviewedAt` is the last dossier write, not a due date.
|
|
20
|
+
- Do not assume every listed person needs work. You may update several people or
|
|
21
|
+
nobody.
|
|
22
|
+
|
|
23
|
+
## Investigate
|
|
24
|
+
|
|
25
|
+
1. Read the current dossier when one exists.
|
|
26
|
+
2. Search for meaningful information with `memory_search`. Use targeted queries,
|
|
27
|
+
relevant corpora, and session metadata filters rather than treating a fixed
|
|
28
|
+
recent-message window as the person's history.
|
|
29
|
+
3. Follow useful `qmd://` results with `memory_get`. If recent OpenClaw sessions
|
|
30
|
+
are not indexed, use `memory_sync_sessions` and check `memory_sync_status`
|
|
31
|
+
before searching again.
|
|
32
|
+
4. Prefer direct statements, repeated behavior, decisions, feedback, and
|
|
33
|
+
outcomes. Distinguish observation, reported information, inference, and agent
|
|
34
|
+
assessment. Do not promote small talk or one ambiguous exchange into a durable
|
|
35
|
+
claim.
|
|
36
|
+
5. Preserve still-useful existing claims. Dossier replacement is complete, not
|
|
37
|
+
a patch.
|
|
38
|
+
|
|
39
|
+
Ordinary `memory_search` supports multiple targeted calls and up to 20 results
|
|
40
|
+
per call. People Whisperer does not impose its own result window or require the
|
|
41
|
+
agent to acknowledge what it inspected.
|
|
42
|
+
|
|
43
|
+
## Write only when useful
|
|
44
|
+
|
|
45
|
+
Call `memory_people_update` with `action: "replace_dossier"`, the `personId`, a
|
|
46
|
+
concise `reason` for the change, and a complete dossier. The plugin records the
|
|
47
|
+
reason and exact before/after snapshots transactionally. Keep the complete dossier
|
|
48
|
+
under the plugin's 64 KiB serialized limit:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"action": "replace_dossier",
|
|
53
|
+
"personId": "PeopleSQL person ID",
|
|
54
|
+
"reason": "Added a durable preference supported by recent sessions.",
|
|
55
|
+
"dossier": {
|
|
56
|
+
"schemaVersion": 1,
|
|
57
|
+
"blurb": "Concise context worth having before the next conversation.",
|
|
58
|
+
"sections": [
|
|
59
|
+
{
|
|
60
|
+
"category": "preferences",
|
|
61
|
+
"claims": [
|
|
62
|
+
{
|
|
63
|
+
"statement": "A durable, specific claim.",
|
|
64
|
+
"evidence": [
|
|
65
|
+
{
|
|
66
|
+
"source": "session",
|
|
67
|
+
"locator": "qmd://path-returned-by-memory-search",
|
|
68
|
+
"observedAt": "2026-08-31T12:00:00Z"
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"epistemicType": "observed",
|
|
72
|
+
"confidence": "high"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Allowed section categories are `role`, `priorities`, `preferences`,
|
|
82
|
+
`successCriteria`, `workingStyle`, `relationship`, and `openLoops`. Evidence
|
|
83
|
+
sources are `session`, `memory`, `directory`, or `manual`; `observedAt` and
|
|
84
|
+
`confidence` are optional. Epistemic types are `observed`, `reported`,
|
|
85
|
+
`inferred`, or `agent_assessment`.
|
|
86
|
+
|
|
87
|
+
Make the blurb immediately useful, concise, and honest about uncertainty. Do not
|
|
88
|
+
stuff it with biography or raw evidence. Claim evidence references are
|
|
89
|
+
provenance, not work receipts.
|
|
90
|
+
|
|
91
|
+
Use `delete_dossier` when the current dossier is too unreliable to inject and
|
|
92
|
+
cannot be responsibly repaired; deletion also requires a concise `reason`. Use
|
|
93
|
+
`memory_people_inspect` with `view: "dossier_changes"`, the `personId`, and
|
|
94
|
+
optional `limit`/`offset` to list small newest-first history summaries. Follow a
|
|
95
|
+
summary with `view: "dossier_change"`, the `personId`, and its `changeId` only
|
|
96
|
+
when you need the exact before/after dossier and blurb. Follow `nextOffset` to page.
|
|
97
|
+
Use `set_injection` to disable or re-enable
|
|
98
|
+
whispers for one person without deleting their dossier. Company, todo, and
|
|
99
|
+
person-status actions are available for the corresponding data changes.
|
|
100
|
+
|
|
101
|
+
## Finish
|
|
102
|
+
|
|
103
|
+
Report whom you investigated, which memory or sessions informed any write, what
|
|
104
|
+
changed, and why skipped people did not need an update. Do not manufacture a
|
|
105
|
+
write to show activity.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export type PersonSessionEvidence = {
|
|
2
|
-
source: "session";
|
|
3
|
-
locator: string;
|
|
4
|
-
observedAt: string;
|
|
5
|
-
text: string;
|
|
6
|
-
context: Array<{
|
|
7
|
-
locator: string;
|
|
8
|
-
role: string;
|
|
9
|
-
text: string;
|
|
10
|
-
senderId?: string;
|
|
11
|
-
}>;
|
|
12
|
-
};
|
|
13
|
-
export declare function readPersonSessionEvidence(params: {
|
|
14
|
-
databasePath: string;
|
|
15
|
-
agentId: string;
|
|
16
|
-
accountScope: string;
|
|
17
|
-
externalId: string;
|
|
18
|
-
limit?: number;
|
|
19
|
-
maxMessageChars?: number;
|
|
20
|
-
excludeLocators?: ReadonlySet<string>;
|
|
21
|
-
}): PersonSessionEvidence[];
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
function record(value) {
|
|
3
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
4
|
-
? value
|
|
5
|
-
: undefined;
|
|
6
|
-
}
|
|
7
|
-
function messageText(value) {
|
|
8
|
-
if (typeof value === "string")
|
|
9
|
-
return value.trim() || undefined;
|
|
10
|
-
if (!Array.isArray(value))
|
|
11
|
-
return undefined;
|
|
12
|
-
const text = value
|
|
13
|
-
.flatMap((part) => {
|
|
14
|
-
const block = record(part);
|
|
15
|
-
return block?.type === "text" && typeof block.text === "string" ? [block.text] : [];
|
|
16
|
-
})
|
|
17
|
-
.join("\n")
|
|
18
|
-
.trim();
|
|
19
|
-
return text || undefined;
|
|
20
|
-
}
|
|
21
|
-
function evidenceTimestamp(event, fallback) {
|
|
22
|
-
const raw = event.timestamp;
|
|
23
|
-
const milliseconds = typeof raw === "number" && Number.isFinite(raw)
|
|
24
|
-
? raw
|
|
25
|
-
: typeof raw === "string"
|
|
26
|
-
? Date.parse(raw)
|
|
27
|
-
: fallback;
|
|
28
|
-
return new Date(Number.isFinite(milliseconds) ? milliseconds : fallback).toISOString();
|
|
29
|
-
}
|
|
30
|
-
export function readPersonSessionEvidence(params) {
|
|
31
|
-
const limit = Math.max(1, Math.min(50, Math.floor(params.limit ?? 20)));
|
|
32
|
-
const maxMessageChars = Math.max(1, Math.min(4000, Math.floor(params.maxMessageChars ?? 2000)));
|
|
33
|
-
const db = new DatabaseSync(params.databasePath, { readOnly: true });
|
|
34
|
-
try {
|
|
35
|
-
db.exec("PRAGMA query_only = ON; PRAGMA busy_timeout = 5000; BEGIN");
|
|
36
|
-
const version = db.prepare("PRAGMA user_version").get();
|
|
37
|
-
const meta = db
|
|
38
|
-
.prepare("SELECT schema_version, agent_id FROM schema_meta WHERE meta_key = 'primary'")
|
|
39
|
-
.get();
|
|
40
|
-
if (version?.user_version !== 17 ||
|
|
41
|
-
meta?.schema_version !== 17 ||
|
|
42
|
-
meta.agent_id !== params.agentId) {
|
|
43
|
-
throw new Error("unsupported or mismatched OpenClaw agent database");
|
|
44
|
-
}
|
|
45
|
-
const rows = db.prepare(`
|
|
46
|
-
SELECT events.session_id, active.active_position, active.event_seq,
|
|
47
|
-
events.event_json, events.created_at
|
|
48
|
-
FROM session_transcript_active_events AS active
|
|
49
|
-
JOIN transcript_events AS events
|
|
50
|
-
ON events.session_id = active.session_id AND events.seq = active.event_seq
|
|
51
|
-
JOIN session_windows AS sessions ON sessions.session_id = active.session_id
|
|
52
|
-
LEFT JOIN conversations ON conversations.conversation_id = sessions.primary_conversation_id
|
|
53
|
-
WHERE active.message_position IS NOT NULL
|
|
54
|
-
AND COALESCE(sessions.channel, conversations.channel) = 'slack'
|
|
55
|
-
AND COALESCE(sessions.account_id, conversations.account_id) = ?
|
|
56
|
-
AND json_extract(events.event_json, '$.type') = 'message'
|
|
57
|
-
AND json_extract(events.event_json, '$.message.role') = 'user'
|
|
58
|
-
AND json_extract(events.event_json, '$.message.__openclaw.senderId') = ?
|
|
59
|
-
ORDER BY events.created_at DESC, events.session_id, active.active_position DESC
|
|
60
|
-
`);
|
|
61
|
-
const contextStatement = db.prepare(`
|
|
62
|
-
SELECT active.event_seq, events.event_json
|
|
63
|
-
FROM session_transcript_active_events AS active
|
|
64
|
-
JOIN transcript_events AS events
|
|
65
|
-
ON events.session_id = active.session_id AND events.seq = active.event_seq
|
|
66
|
-
WHERE active.session_id = ?
|
|
67
|
-
AND active.message_position IS NOT NULL
|
|
68
|
-
AND active.active_position BETWEEN ? AND ?
|
|
69
|
-
AND json_extract(events.event_json, '$.type') = 'message'
|
|
70
|
-
ORDER BY active.active_position
|
|
71
|
-
`);
|
|
72
|
-
const evidence = [];
|
|
73
|
-
for (const row of rows.iterate(params.accountScope, params.externalId)) {
|
|
74
|
-
const locator = `session:${row.session_id}:event:${row.event_seq}`;
|
|
75
|
-
if (params.excludeLocators?.has(locator))
|
|
76
|
-
continue;
|
|
77
|
-
const event = record(JSON.parse(row.event_json));
|
|
78
|
-
const message = record(event?.message);
|
|
79
|
-
const text = messageText(message?.content);
|
|
80
|
-
if (!event || !message || !text)
|
|
81
|
-
continue;
|
|
82
|
-
const context = contextStatement
|
|
83
|
-
.all(row.session_id, row.active_position - 1, row.active_position + 2)
|
|
84
|
-
.flatMap((contextRow) => {
|
|
85
|
-
const candidate = contextRow;
|
|
86
|
-
const contextEvent = record(JSON.parse(candidate.event_json));
|
|
87
|
-
const contextMessage = record(contextEvent?.message);
|
|
88
|
-
const contextText = messageText(contextMessage?.content);
|
|
89
|
-
if (!contextMessage || !contextText || typeof contextMessage.role !== "string")
|
|
90
|
-
return [];
|
|
91
|
-
const metadata = record(contextMessage.__openclaw);
|
|
92
|
-
return [
|
|
93
|
-
{
|
|
94
|
-
locator: `session:${row.session_id}:event:${candidate.event_seq}`,
|
|
95
|
-
role: contextMessage.role,
|
|
96
|
-
text: contextText.slice(0, maxMessageChars),
|
|
97
|
-
...(typeof metadata?.senderId === "string" ? { senderId: metadata.senderId } : {}),
|
|
98
|
-
},
|
|
99
|
-
];
|
|
100
|
-
});
|
|
101
|
-
evidence.push({
|
|
102
|
-
source: "session",
|
|
103
|
-
locator,
|
|
104
|
-
observedAt: evidenceTimestamp(event, row.created_at),
|
|
105
|
-
text: text.slice(0, maxMessageChars),
|
|
106
|
-
context,
|
|
107
|
-
});
|
|
108
|
-
if (evidence.length === limit)
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
db.exec("COMMIT");
|
|
112
|
-
return evidence;
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
try {
|
|
116
|
-
db.exec("ROLLBACK");
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// The read transaction may not have started if opening the schema failed.
|
|
120
|
-
}
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
finally {
|
|
124
|
-
db.close();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type PersonSessionEvidence } from "./people-evidence.js";
|
|
2
|
-
import type { PeopleStore, Person, PersonDossier, PersonIdentity } from "./people-store.js";
|
|
3
|
-
export type PeopleRefinementPacket = {
|
|
4
|
-
person: Person;
|
|
5
|
-
identities: PersonIdentity[];
|
|
6
|
-
currentDossier?: PersonDossier;
|
|
7
|
-
evidence: PersonSessionEvidence[];
|
|
8
|
-
};
|
|
9
|
-
export declare function nextPeopleRefinement(params: {
|
|
10
|
-
store: PeopleStore;
|
|
11
|
-
agentId: string;
|
|
12
|
-
agentDatabasePath: string;
|
|
13
|
-
evidenceLimit?: number;
|
|
14
|
-
}): PeopleRefinementPacket | undefined;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { readPersonSessionEvidence } from "./people-evidence.js";
|
|
2
|
-
export function nextPeopleRefinement(params) {
|
|
3
|
-
const evidenceLimit = Math.max(1, Math.min(50, Math.floor(params.evidenceLimit ?? 20)));
|
|
4
|
-
for (const person of params.store.listActivePeople()) {
|
|
5
|
-
const identities = params.store.listIdentities(person.id);
|
|
6
|
-
const processed = params.store.listProcessedEvidenceLocators(person.id, "session");
|
|
7
|
-
const evidence = identities
|
|
8
|
-
.filter((identity) => identity.provider === "slack")
|
|
9
|
-
.flatMap((identity) => readPersonSessionEvidence({
|
|
10
|
-
databasePath: params.agentDatabasePath,
|
|
11
|
-
agentId: params.agentId,
|
|
12
|
-
accountScope: identity.accountScope,
|
|
13
|
-
externalId: identity.externalId,
|
|
14
|
-
limit: evidenceLimit,
|
|
15
|
-
excludeLocators: processed,
|
|
16
|
-
}))
|
|
17
|
-
.filter((entry, index, all) => all.findIndex((candidate) => candidate.locator === entry.locator) === index)
|
|
18
|
-
.sort((left, right) => right.observedAt.localeCompare(left.observedAt))
|
|
19
|
-
.slice(0, evidenceLimit);
|
|
20
|
-
if (evidence.length === 0)
|
|
21
|
-
continue;
|
|
22
|
-
return {
|
|
23
|
-
person,
|
|
24
|
-
identities,
|
|
25
|
-
currentDossier: params.store.getDossier(person.id)?.dossier,
|
|
26
|
-
evidence,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|