@unblocklabs/unblock-memory 0.3.7 → 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 +63 -41
- package/dist/src/config.d.ts +0 -3
- package/dist/src/config.js +1 -11
- package/dist/src/people-hooks.js +65 -25
- package/dist/src/people-store.d.ts +35 -9
- package/dist/src/people-store.js +225 -65
- package/dist/src/people-tools.d.ts +1 -1
- package/dist/src/people-tools.js +96 -23
- package/dist/src/plugin.js +0 -2
- package/openclaw.plugin.json +3 -19
- package/package.json +1 -1
- package/skills/people-whisperer/SKILL.md +105 -0
- package/dist/src/people-cli.d.ts +0 -4
- package/dist/src/people-cli.js +0 -47
- package/dist/src/people-evidence.d.ts +0 -14
- package/dist/src/people-evidence.js +0 -93
- package/dist/src/people-refinement.d.ts +0 -72
- package/dist/src/people-refinement.js +0 -269
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";
|
|
@@ -40,12 +41,23 @@ const claimSchema = Type.Object({
|
|
|
40
41
|
}, { additionalProperties: false });
|
|
41
42
|
export const PERSON_DOSSIER_SCHEMA = Type.Object({
|
|
42
43
|
schemaVersion: Type.Literal(1),
|
|
43
|
-
blurb: Type.String({ minLength: 1 }),
|
|
44
|
+
blurb: Type.String({ minLength: 1, pattern: "\\S" }),
|
|
44
45
|
sections: Type.Array(Type.Object({
|
|
45
46
|
category: Type.Union(BASELINE_DOSSIER_CATEGORIES.map((category) => Type.Literal(category))),
|
|
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,
|
|
@@ -63,7 +81,6 @@ function person(row) {
|
|
|
63
81
|
preferredName: row.preferred_name,
|
|
64
82
|
status: row.status,
|
|
65
83
|
companyId: row.company_id,
|
|
66
|
-
refinementEnabled: row.refinement_enabled === 1,
|
|
67
84
|
injectionEnabled: row.injection_enabled === 1,
|
|
68
85
|
lastSeenAt: row.last_seen_at,
|
|
69
86
|
createdAt: row.created_at,
|
|
@@ -158,9 +175,8 @@ export class PeopleStore {
|
|
|
158
175
|
this.#db
|
|
159
176
|
.prepare(`
|
|
160
177
|
INSERT INTO people
|
|
161
|
-
(id, display_name, status,
|
|
162
|
-
|
|
163
|
-
VALUES (?, ?, 'active', 0, 0, ?, ?, ?)
|
|
178
|
+
(id, display_name, status, injection_enabled, last_seen_at, created_at, updated_at)
|
|
179
|
+
VALUES (?, ?, 'active', 1, ?, ?, ?)
|
|
164
180
|
`)
|
|
165
181
|
.run(personId, displayName, directorySync ? null : now, now, now);
|
|
166
182
|
this.#db
|
|
@@ -205,8 +221,8 @@ export class PeopleStore {
|
|
|
205
221
|
if (input.isDeactivated === true) {
|
|
206
222
|
this.#db
|
|
207
223
|
.prepare(`
|
|
208
|
-
UPDATE people SET status = 'unavailable',
|
|
209
|
-
|
|
224
|
+
UPDATE people SET status = 'unavailable', injection_enabled = 0,
|
|
225
|
+
updated_at = ? WHERE id = ?
|
|
210
226
|
`)
|
|
211
227
|
.run(now, personId);
|
|
212
228
|
}
|
|
@@ -292,71 +308,47 @@ export class PeopleStore {
|
|
|
292
308
|
throw error;
|
|
293
309
|
}
|
|
294
310
|
}
|
|
295
|
-
|
|
296
|
-
const bounded = Math.max(1, Math.min(100, Math.floor(limit)));
|
|
311
|
+
listActivePeople(limit = 50, offset = 0) {
|
|
297
312
|
return this.#db
|
|
298
313
|
.prepare(`
|
|
299
|
-
SELECT
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
AND people.last_seen_at IS NOT NULL
|
|
304
|
-
AND (person_dossiers.reviewed_at IS NULL OR people.last_seen_at > person_dossiers.reviewed_at)
|
|
305
|
-
ORDER BY people.last_seen_at DESC, people.id
|
|
306
|
-
LIMIT ?
|
|
314
|
+
SELECT * FROM people
|
|
315
|
+
WHERE status = 'active'
|
|
316
|
+
ORDER BY last_seen_at DESC, id
|
|
317
|
+
LIMIT ? OFFSET ?
|
|
307
318
|
`)
|
|
308
|
-
.all(
|
|
319
|
+
.all(limit, offset)
|
|
309
320
|
.map((row) => person(row));
|
|
310
321
|
}
|
|
311
322
|
findIdentity(provider, accountScope, externalId) {
|
|
312
323
|
const row = this.#identityRow(provider, accountScope, externalId);
|
|
313
324
|
return row ? identity(row) : undefined;
|
|
314
325
|
}
|
|
315
|
-
|
|
326
|
+
setInjection(personId, enabled) {
|
|
316
327
|
const now = new Date().toISOString();
|
|
317
328
|
this.#db
|
|
318
|
-
.prepare(
|
|
319
|
-
|
|
320
|
-
refinement_enabled = COALESCE(?, refinement_enabled),
|
|
321
|
-
injection_enabled = COALESCE(?, injection_enabled),
|
|
322
|
-
updated_at = ?
|
|
323
|
-
WHERE id = ?
|
|
324
|
-
`)
|
|
325
|
-
.run(policies.refinementEnabled === undefined ? null : Number(policies.refinementEnabled), policies.injectionEnabled === undefined ? null : Number(policies.injectionEnabled), now, personId);
|
|
329
|
+
.prepare("UPDATE people SET injection_enabled = ?, updated_at = ? WHERE id = ?")
|
|
330
|
+
.run(Number(enabled), now, personId);
|
|
326
331
|
const row = this.#db.prepare("SELECT * FROM people WHERE id = ?").get(personId);
|
|
327
332
|
return row ? person(row) : undefined;
|
|
328
333
|
}
|
|
329
|
-
replaceDossier(personId,
|
|
334
|
+
replaceDossier(personId, reasonInput, input) {
|
|
330
335
|
const dossier = Value.Parse(PERSON_DOSSIER_SCHEMA, input);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
if (new Set(categories).size !== categories.length) {
|
|
336
|
-
throw new Error("dossier sections must have unique categories");
|
|
337
|
-
}
|
|
336
|
+
this.#validateDossier(dossier);
|
|
337
|
+
const dossierJson = serializeDossier(dossier);
|
|
338
|
+
const reason = dossierReason(reasonInput);
|
|
339
|
+
const reviewedAt = new Date().toISOString();
|
|
338
340
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
339
341
|
try {
|
|
340
|
-
const target = this.#db
|
|
341
|
-
.prepare("SELECT refinement_enabled FROM people WHERE id = ?")
|
|
342
|
-
.get(personId);
|
|
342
|
+
const target = this.#db.prepare("SELECT id FROM people WHERE id = ?").get(personId);
|
|
343
343
|
if (!target)
|
|
344
344
|
throw new Error(`person not found: ${personId}`);
|
|
345
|
-
|
|
346
|
-
throw new Error("person is not enabled for refinement");
|
|
347
|
-
}
|
|
348
|
-
const dossierJson = JSON.stringify(dossier);
|
|
349
|
-
const current = this.#db
|
|
345
|
+
const existing = this.#db
|
|
350
346
|
.prepare("SELECT dossier_json FROM person_dossiers WHERE person_id = ?")
|
|
351
347
|
.get(personId);
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
this.#db
|
|
359
|
-
.prepare(`
|
|
348
|
+
if (existing)
|
|
349
|
+
parseDossierJson(existing.dossier_json);
|
|
350
|
+
this.#db
|
|
351
|
+
.prepare(`
|
|
360
352
|
INSERT INTO person_dossiers (person_id, dossier_json, blurb, reviewed_at)
|
|
361
353
|
VALUES (?, ?, ?, ?)
|
|
362
354
|
ON CONFLICT(person_id) DO UPDATE SET
|
|
@@ -364,8 +356,14 @@ export class PeopleStore {
|
|
|
364
356
|
blurb = excluded.blurb,
|
|
365
357
|
reviewed_at = excluded.reviewed_at
|
|
366
358
|
`)
|
|
367
|
-
|
|
368
|
-
|
|
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);
|
|
369
367
|
this.#db.exec("COMMIT");
|
|
370
368
|
return dossier;
|
|
371
369
|
}
|
|
@@ -374,17 +372,124 @@ export class PeopleStore {
|
|
|
374
372
|
throw error;
|
|
375
373
|
}
|
|
376
374
|
}
|
|
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
|
+
}
|
|
403
|
+
}
|
|
404
|
+
getWhisperReceipt(threadKey, personId) {
|
|
405
|
+
const row = this.#db
|
|
406
|
+
.prepare(`
|
|
407
|
+
SELECT run_id, contribution, injected_at FROM person_whisper_receipts
|
|
408
|
+
WHERE thread_key = ? AND person_id = ?
|
|
409
|
+
`)
|
|
410
|
+
.get(threadKey, personId);
|
|
411
|
+
return row
|
|
412
|
+
? { runId: row.run_id, contribution: row.contribution, injectedAt: row.injected_at }
|
|
413
|
+
: undefined;
|
|
414
|
+
}
|
|
415
|
+
recordWhisperReceipt(input) {
|
|
416
|
+
const threadKey = required(input.threadKey, "threadKey");
|
|
417
|
+
const runId = required(input.runId, "runId");
|
|
418
|
+
const contribution = required(input.contribution, "contribution");
|
|
419
|
+
const injectedAt = new Date().toISOString();
|
|
420
|
+
this.#db
|
|
421
|
+
.prepare(`
|
|
422
|
+
INSERT OR IGNORE INTO person_whisper_receipts
|
|
423
|
+
(thread_key, person_id, run_id, contribution, injected_at)
|
|
424
|
+
VALUES (?, ?, ?, ?, ?)
|
|
425
|
+
`)
|
|
426
|
+
.run(threadKey, input.personId, runId, contribution, injectedAt);
|
|
427
|
+
return this.getWhisperReceipt(threadKey, input.personId);
|
|
428
|
+
}
|
|
377
429
|
getDossier(personId) {
|
|
378
430
|
const row = this.#db
|
|
379
431
|
.prepare("SELECT dossier_json, reviewed_at FROM person_dossiers WHERE person_id = ?")
|
|
380
432
|
.get(personId);
|
|
381
433
|
return row
|
|
382
434
|
? {
|
|
383
|
-
dossier:
|
|
435
|
+
dossier: parseDossierJson(row.dossier_json),
|
|
384
436
|
reviewedAt: row.reviewed_at,
|
|
385
437
|
}
|
|
386
438
|
: undefined;
|
|
387
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
|
+
}
|
|
388
493
|
getDossierBlurb(personId) {
|
|
389
494
|
const row = this.#db
|
|
390
495
|
.prepare("SELECT blurb FROM person_dossiers WHERE person_id = ?")
|
|
@@ -397,8 +502,8 @@ export class PeopleStore {
|
|
|
397
502
|
try {
|
|
398
503
|
const changed = this.#db
|
|
399
504
|
.prepare(`
|
|
400
|
-
UPDATE people SET status = 'unavailable',
|
|
401
|
-
|
|
505
|
+
UPDATE people SET status = 'unavailable', injection_enabled = 0,
|
|
506
|
+
updated_at = ? WHERE id = ?
|
|
402
507
|
`)
|
|
403
508
|
.run(now, personId);
|
|
404
509
|
if (changed.changes === 0) {
|
|
@@ -425,8 +530,7 @@ export class PeopleStore {
|
|
|
425
530
|
try {
|
|
426
531
|
const changed = this.#db
|
|
427
532
|
.prepare(`
|
|
428
|
-
UPDATE people SET status = 'active',
|
|
429
|
-
injection_enabled = 0, updated_at = ?
|
|
533
|
+
UPDATE people SET status = 'active', injection_enabled = 0, updated_at = ?
|
|
430
534
|
WHERE id = ? AND status = 'unavailable'
|
|
431
535
|
`)
|
|
432
536
|
.run(now, personId);
|
|
@@ -568,16 +672,50 @@ export class PeopleStore {
|
|
|
568
672
|
`)
|
|
569
673
|
.get(provider, accountScope, externalId);
|
|
570
674
|
}
|
|
675
|
+
#validateDossier(dossier) {
|
|
676
|
+
if (dossier.blurb.length > this.#maxBlurbChars) {
|
|
677
|
+
throw new Error(`dossier blurb must not exceed ${this.#maxBlurbChars} characters`);
|
|
678
|
+
}
|
|
679
|
+
const categories = dossier.sections.map((section) => section.category);
|
|
680
|
+
if (new Set(categories).size !== categories.length) {
|
|
681
|
+
throw new Error("dossier sections must have unique categories");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
571
684
|
#migrate() {
|
|
572
685
|
const current = this.#db.prepare("PRAGMA user_version").get();
|
|
573
|
-
if (current.user_version ===
|
|
686
|
+
if (current.user_version === 4)
|
|
574
687
|
return;
|
|
575
|
-
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) {
|
|
576
692
|
throw new Error(`unsupported PeopleSQL schema version: ${current.user_version}`);
|
|
577
693
|
}
|
|
578
694
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
579
695
|
try {
|
|
580
|
-
|
|
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) {
|
|
702
|
+
this.#db.exec(`
|
|
703
|
+
DROP INDEX people_policy_seen;
|
|
704
|
+
ALTER TABLE people DROP COLUMN refinement_enabled;
|
|
705
|
+
UPDATE people SET injection_enabled = 1 WHERE status = 'active';
|
|
706
|
+
|
|
707
|
+
CREATE TABLE person_whisper_receipts (
|
|
708
|
+
thread_key TEXT NOT NULL,
|
|
709
|
+
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
710
|
+
run_id TEXT NOT NULL,
|
|
711
|
+
contribution TEXT NOT NULL,
|
|
712
|
+
injected_at TEXT NOT NULL,
|
|
713
|
+
PRIMARY KEY (thread_key, person_id)
|
|
714
|
+
) STRICT;
|
|
715
|
+
`);
|
|
716
|
+
}
|
|
717
|
+
else if (current.user_version === 0) {
|
|
718
|
+
this.#db.exec(`
|
|
581
719
|
CREATE TABLE companies (
|
|
582
720
|
id TEXT PRIMARY KEY,
|
|
583
721
|
name TEXT NOT NULL,
|
|
@@ -593,8 +731,7 @@ export class PeopleStore {
|
|
|
593
731
|
preferred_name TEXT,
|
|
594
732
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'unavailable', 'archived')),
|
|
595
733
|
company_id TEXT REFERENCES companies(id),
|
|
596
|
-
|
|
597
|
-
injection_enabled INTEGER NOT NULL DEFAULT 0 CHECK (injection_enabled IN (0, 1)),
|
|
734
|
+
injection_enabled INTEGER NOT NULL DEFAULT 1 CHECK (injection_enabled IN (0, 1)),
|
|
598
735
|
last_seen_at TEXT,
|
|
599
736
|
created_at TEXT NOT NULL,
|
|
600
737
|
updated_at TEXT NOT NULL
|
|
@@ -638,10 +775,33 @@ export class PeopleStore {
|
|
|
638
775
|
resolution_note TEXT
|
|
639
776
|
) STRICT;
|
|
640
777
|
|
|
778
|
+
CREATE TABLE person_whisper_receipts (
|
|
779
|
+
thread_key TEXT NOT NULL,
|
|
780
|
+
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
781
|
+
run_id TEXT NOT NULL,
|
|
782
|
+
contribution TEXT NOT NULL,
|
|
783
|
+
injected_at TEXT NOT NULL,
|
|
784
|
+
PRIMARY KEY (thread_key, person_id)
|
|
785
|
+
) STRICT;
|
|
786
|
+
|
|
641
787
|
CREATE INDEX people_status_seen ON people(status, last_seen_at);
|
|
642
|
-
CREATE INDEX people_policy_seen ON people(refinement_enabled, last_seen_at);
|
|
643
788
|
CREATE INDEX people_todos_status_seen ON people_todos(status, last_seen_at);
|
|
644
|
-
|
|
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;
|
|
645
805
|
`);
|
|
646
806
|
this.#db.exec("COMMIT");
|
|
647
807
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
2
|
import type { UnblockMemoryConfig } from "./config.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type PeopleStores } from "./people-store.js";
|
|
4
4
|
import { type SlackDirectoryReader } from "./slack-directory.js";
|
|
5
5
|
export declare function registerPeopleTools(api: OpenClawPluginApi, stores: PeopleStores, config: UnblockMemoryConfig["people"], directoryReader?: SlackDirectoryReader): void;
|
package/dist/src/people-tools.js
CHANGED
|
@@ -2,6 +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 { PERSON_DOSSIER_SCHEMA } from "./people-store.js";
|
|
5
6
|
import { createOpenClawSlackDirectory, syncSlackDirectory, } from "./slack-directory.js";
|
|
6
7
|
const nonEmpty = Type.String({ pattern: "\\S", maxLength: 1000 });
|
|
7
8
|
const inspectParameters = Type.Union([
|
|
@@ -32,6 +33,30 @@ const inspectParameters = Type.Union([
|
|
|
32
33
|
description: "Exact Slack identity for the person to inspect.",
|
|
33
34
|
}),
|
|
34
35
|
}, { additionalProperties: false }),
|
|
36
|
+
Type.Object({
|
|
37
|
+
view: Type.Literal("people"),
|
|
38
|
+
limit: Type.Optional(Type.Integer({
|
|
39
|
+
minimum: 1,
|
|
40
|
+
maximum: 100,
|
|
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.",
|
|
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,
|
|
59
|
+
}, { additionalProperties: false }),
|
|
35
60
|
Type.Object({
|
|
36
61
|
view: Type.Literal("todos"),
|
|
37
62
|
limit: Type.Optional(Type.Integer({
|
|
@@ -43,10 +68,20 @@ const inspectParameters = Type.Union([
|
|
|
43
68
|
]);
|
|
44
69
|
const updateParameters = Type.Union([
|
|
45
70
|
Type.Object({
|
|
46
|
-
action: Type.Literal("
|
|
71
|
+
action: Type.Literal("set_injection"),
|
|
72
|
+
personId: nonEmpty,
|
|
73
|
+
enabled: Type.Boolean(),
|
|
74
|
+
}, { additionalProperties: false }),
|
|
75
|
+
Type.Object({
|
|
76
|
+
action: Type.Literal("replace_dossier"),
|
|
47
77
|
personId: nonEmpty,
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
dossier: PERSON_DOSSIER_SCHEMA,
|
|
79
|
+
reason: nonEmpty,
|
|
80
|
+
}, { additionalProperties: false }),
|
|
81
|
+
Type.Object({
|
|
82
|
+
action: Type.Literal("delete_dossier"),
|
|
83
|
+
personId: nonEmpty,
|
|
84
|
+
reason: nonEmpty,
|
|
50
85
|
}, { additionalProperties: false }),
|
|
51
86
|
Type.Object({
|
|
52
87
|
action: Type.Literal("set_company"),
|
|
@@ -99,49 +134,91 @@ function personView(stores, agentId, selector, maxChars) {
|
|
|
99
134
|
}
|
|
100
135
|
function createInspectTool(stores, config, ctx) {
|
|
101
136
|
const active = context(ctx);
|
|
102
|
-
if (!active
|
|
137
|
+
if (!active)
|
|
103
138
|
return null;
|
|
104
139
|
return {
|
|
105
140
|
name: "memory_people_inspect",
|
|
106
141
|
label: "Inspect People Memory",
|
|
107
|
-
description: "
|
|
142
|
+
description: "List active people, inspect one person, read dossier change history, or list actionable people todos.",
|
|
108
143
|
parameters: inspectParameters,
|
|
109
144
|
async execute(_toolCallId, raw) {
|
|
110
145
|
const input = Value.Parse(inspectParameters, raw);
|
|
111
146
|
if (input.view === "person") {
|
|
112
147
|
return jsonResult(personView(stores, active.agentId, input, config.whisperer.maxChars));
|
|
113
148
|
}
|
|
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" });
|
|
181
|
+
}
|
|
114
182
|
return jsonResult({
|
|
115
183
|
status: "ok",
|
|
116
|
-
todos:
|
|
184
|
+
todos: store.listTodos(input.limit ?? 20),
|
|
117
185
|
});
|
|
118
186
|
},
|
|
119
187
|
};
|
|
120
188
|
}
|
|
121
189
|
function createUpdateTool(stores, ctx) {
|
|
122
190
|
const active = context(ctx);
|
|
123
|
-
if (!active
|
|
191
|
+
if (!active)
|
|
124
192
|
return null;
|
|
125
193
|
return {
|
|
126
194
|
name: "memory_people_update",
|
|
127
195
|
label: "Update People Memory",
|
|
128
|
-
description: "
|
|
196
|
+
description: "Update a dossier, one person's injection preference, company, todo, or person status.",
|
|
129
197
|
parameters: updateParameters,
|
|
130
198
|
async execute(_toolCallId, raw) {
|
|
131
|
-
if (ctx.senderIsOwner !== true)
|
|
132
|
-
return jsonResult({ status: "forbidden", error: "owner authorization required" });
|
|
133
199
|
const input = Value.Parse(updateParameters, raw);
|
|
134
200
|
const store = stores.get(active.agentId);
|
|
135
|
-
if (input.action === "
|
|
136
|
-
|
|
137
|
-
return jsonResult({
|
|
138
|
-
status: "invalid",
|
|
139
|
-
error: "set_policy requires at least one policy value",
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
const person = store.setPolicies(input.personId, input);
|
|
201
|
+
if (input.action === "set_injection") {
|
|
202
|
+
const person = store.setInjection(input.personId, input.enabled);
|
|
143
203
|
return jsonResult(person ? { status: "ok", person } : { status: "not_found" });
|
|
144
204
|
}
|
|
205
|
+
if (input.action === "replace_dossier") {
|
|
206
|
+
try {
|
|
207
|
+
const dossier = store.replaceDossier(input.personId, input.reason, input.dossier);
|
|
208
|
+
return jsonResult({ status: "ok", dossier });
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error instanceof Error && error.message.startsWith("person not found:")) {
|
|
212
|
+
return jsonResult({ status: "not_found" });
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (input.action === "delete_dossier") {
|
|
218
|
+
return jsonResult(store.deleteDossier(input.personId, input.reason)
|
|
219
|
+
? { status: "ok" }
|
|
220
|
+
: { status: "not_found" });
|
|
221
|
+
}
|
|
145
222
|
if (input.action === "set_company") {
|
|
146
223
|
const company = store.setCompany(input.personId, {
|
|
147
224
|
name: input.companyName,
|
|
@@ -165,7 +242,7 @@ function createUpdateTool(stores, ctx) {
|
|
|
165
242
|
}
|
|
166
243
|
function createSyncTool(stores, reader, ctx) {
|
|
167
244
|
const active = context(ctx);
|
|
168
|
-
if (!active
|
|
245
|
+
if (!active)
|
|
169
246
|
return null;
|
|
170
247
|
return {
|
|
171
248
|
name: "memory_people_sync",
|
|
@@ -174,8 +251,6 @@ function createSyncTool(stores, reader, ctx) {
|
|
|
174
251
|
parameters: syncParameters,
|
|
175
252
|
async execute(_toolCallId, raw) {
|
|
176
253
|
const input = Value.Parse(syncParameters, raw);
|
|
177
|
-
if (ctx.senderIsOwner !== true)
|
|
178
|
-
return jsonResult({ status: "forbidden", error: "owner authorization required" });
|
|
179
254
|
try {
|
|
180
255
|
return jsonResult(await syncSlackDirectory({
|
|
181
256
|
store: stores.get(active.agentId),
|
|
@@ -196,11 +271,9 @@ function createSyncTool(stores, reader, ctx) {
|
|
|
196
271
|
export function registerPeopleTools(api, stores, config, directoryReader) {
|
|
197
272
|
api.registerTool((ctx) => createInspectTool(stores, config, ctx), {
|
|
198
273
|
names: ["memory_people_inspect"],
|
|
199
|
-
optional: true,
|
|
200
274
|
});
|
|
201
275
|
api.registerTool((ctx) => createUpdateTool(stores, ctx), {
|
|
202
276
|
names: ["memory_people_update"],
|
|
203
|
-
optional: true,
|
|
204
277
|
});
|
|
205
278
|
api.registerTool((ctx) => createSyncTool(stores, directoryReader ??
|
|
206
279
|
createOpenClawSlackDirectory({
|
package/dist/src/plugin.js
CHANGED
|
@@ -2,7 +2,6 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
4
|
import { resolveConfig } from "./config.js";
|
|
5
|
-
import { registerPeopleCli } from "./people-cli.js";
|
|
6
5
|
import { registerPeopleHooks } from "./people-hooks.js";
|
|
7
6
|
import { PeopleStores } from "./people-store.js";
|
|
8
7
|
import { registerPeopleTools } from "./people-tools.js";
|
|
@@ -386,7 +385,6 @@ export function resolveFlushPlan(params = {}) {
|
|
|
386
385
|
}
|
|
387
386
|
export function registerUnblockMemory(api) {
|
|
388
387
|
const config = resolveConfig(api.pluginConfig);
|
|
389
|
-
registerPeopleCli(api, config.people);
|
|
390
388
|
if (api.registrationMode === "cli-metadata")
|
|
391
389
|
return;
|
|
392
390
|
const runtime = new QmdMemoryRuntime(config.corpora, {
|