@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.
@@ -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, refinement_enabled, injection_enabled,
162
- last_seen_at, created_at, updated_at)
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', refinement_enabled = 0,
209
- injection_enabled = 0, updated_at = ? WHERE id = ?
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
- listRefinementCandidates(limit) {
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 people.* FROM people
300
- LEFT JOIN person_dossiers ON person_dossiers.person_id = people.id
301
- WHERE people.status = 'active'
302
- AND people.refinement_enabled = 1
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(bounded)
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
- setPolicies(personId, policies) {
326
+ setInjection(personId, enabled) {
316
327
  const now = new Date().toISOString();
317
328
  this.#db
318
- .prepare(`
319
- UPDATE people SET
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, input, reviewedAt = new Date().toISOString(), options = {}) {
334
+ replaceDossier(personId, reasonInput, input) {
330
335
  const dossier = Value.Parse(PERSON_DOSSIER_SCHEMA, input);
331
- if (dossier.blurb.length > this.#maxBlurbChars) {
332
- throw new Error(`dossier blurb must not exceed ${this.#maxBlurbChars} characters`);
333
- }
334
- const categories = dossier.sections.map((section) => section.category);
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
- if (options.requireRefinementEnabled && target.refinement_enabled !== 1) {
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 (current?.dossier_json === dossierJson) {
353
- this.#db
354
- .prepare("UPDATE person_dossiers SET reviewed_at = ? WHERE person_id = ?")
355
- .run(reviewedAt, personId);
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
- .run(personId, dossierJson, dossier.blurb, reviewedAt);
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: Value.Parse(PERSON_DOSSIER_SCHEMA, JSON.parse(row.dossier_json)),
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', refinement_enabled = 0,
401
- injection_enabled = 0, updated_at = ? WHERE id = ?
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', refinement_enabled = 0,
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 === 1)
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
- this.#db.exec(`
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
- refinement_enabled INTEGER NOT NULL DEFAULT 0 CHECK (refinement_enabled IN (0, 1)),
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
- PRAGMA user_version = 1;
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 { PeopleStores } from "./people-store.js";
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;
@@ -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("set_policy"),
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
- refinementEnabled: Type.Optional(Type.Boolean()),
49
- injectionEnabled: Type.Optional(Type.Boolean()),
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 || ctx.senderIsOwner !== true)
137
+ if (!active)
103
138
  return null;
104
139
  return {
105
140
  name: "memory_people_inspect",
106
141
  label: "Inspect People Memory",
107
- description: "Inspect one person by PeopleSQL personId or exact Slack identity, or list bounded actionable people todos.",
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: stores.get(active.agentId).listTodos(input.limit ?? 20),
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 || ctx.senderIsOwner !== true)
191
+ if (!active)
124
192
  return null;
125
193
  return {
126
194
  name: "memory_people_update",
127
195
  label: "Update People Memory",
128
- description: "Apply one validated people policy, company, todo, deletion, or restoration action.",
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 === "set_policy") {
136
- if (input.refinementEnabled === undefined && input.injectionEnabled === undefined) {
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 || ctx.senderIsOwner !== true)
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({
@@ -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, {