@unblocklabs/unblock-memory 0.3.16 → 0.3.18
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 +118 -17
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +6 -3
- package/dist/src/evidence-review.d.ts +9 -1
- package/dist/src/evidence-review.js +2 -1
- package/dist/src/manager.d.ts +5 -1
- package/dist/src/manager.js +6 -1
- package/dist/src/people-background.d.ts +3 -0
- package/dist/src/people-background.js +5 -0
- package/dist/src/people-dossier-review.d.ts +41 -0
- package/dist/src/people-dossier-review.js +47 -0
- package/dist/src/people-primer-config.d.ts +13 -0
- package/dist/src/people-primer-config.js +33 -0
- package/dist/src/people-primer-tool.d.ts +5 -0
- package/dist/src/people-primer-tool.js +78 -0
- package/dist/src/people-primer.d.ts +94 -0
- package/dist/src/people-primer.js +170 -0
- package/dist/src/people-store.d.ts +8 -1
- package/dist/src/people-store.js +60 -2
- package/dist/src/people-tools.d.ts +2 -1
- package/dist/src/people-tools.js +35 -11
- package/dist/src/plugin.js +3 -1
- package/dist/src/response-outcome.d.ts +1 -1
- package/dist/src/runtime.d.ts +1 -1
- package/dist/src/runtime.js +29 -20
- package/dist/src/session-sync.d.ts +16 -3
- package/dist/src/session-sync.js +96 -15
- package/dist/src/typesafe-review.d.ts +9 -1
- package/dist/src/typesafe-review.js +41 -4
- package/openclaw.plugin.json +20 -3
- package/package.json +1 -1
- package/skills/people-whisperer/SKILL.md +107 -100
package/README.md
CHANGED
|
@@ -524,18 +524,105 @@ directory.
|
|
|
524
524
|
|
|
525
525
|
### People Whisperer
|
|
526
526
|
|
|
527
|
+
#### Optional People Dossier Primer
|
|
528
|
+
|
|
529
|
+
`memory_people_prime({ personId, agentName? })` prepares evidence for an existing person;
|
|
530
|
+
it does **not** generate claims, update dossiers, or inject context. With
|
|
531
|
+
`people.enabled: true`, opt in separately:
|
|
532
|
+
|
|
533
|
+
```json
|
|
534
|
+
{
|
|
535
|
+
"peoplePrimer": {
|
|
536
|
+
"enabled": true,
|
|
537
|
+
"corpora": ["memory", "knowledge", "sessions"],
|
|
538
|
+
"hitsPerQuestion": 30,
|
|
539
|
+
"minScore": 0.35,
|
|
540
|
+
"minUsefulness": 0.8,
|
|
541
|
+
"maxEvidencePerQuestion": 3,
|
|
542
|
+
"timeoutMs": 30000
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
List only configured, approved non-skill corpora. The feature is **off by
|
|
548
|
+
default** and requires shared TypeSafe credentials. Disabled TypeSafe or missing/
|
|
549
|
+
unreadable credentials safely skip the primer; agents can still research normally.
|
|
550
|
+
Enabling it approves sending the person's identity, retrieved excerpts and optional
|
|
551
|
+
draft snippet to TypeSafe. Existing dossiers are not sent as grading evidence.
|
|
552
|
+
Sessions includes all indexed conversations;
|
|
553
|
+
results are available to the agent's tool callers, so scope approval accordingly.
|
|
554
|
+
|
|
555
|
+
Three default questions cover explicit role/organization, enduring organizational
|
|
556
|
+
background, and the person's relationship to the agent (not its business mission).
|
|
557
|
+
Preferences, working styles, priorities, feedback and task history are excluded.
|
|
558
|
+
Each uses QMD vector search (no query expansion) for up to 30 hits, configurable
|
|
559
|
+
up to 40. All unique eligible hits above the vector threshold are graded, not just
|
|
560
|
+
the final top three. Complete excerpts over 6,000 characters are counted and skipped,
|
|
561
|
+
not silently truncated. Duplicate source spans across questions share a request;
|
|
562
|
+
Independent attribution, explicit-background, durability, recognition-value and
|
|
563
|
+
question-usefulness judgments run together; every dimension must pass the threshold.
|
|
564
|
+
Every candidate is graded against all three questions, regardless of which search
|
|
565
|
+
found it. Mixed excerpts may supply a useful background fact without making their
|
|
566
|
+
surrounding behavioral content eligible for the snippet.
|
|
567
|
+
Provider concurrency is four, with a two-minute overall tool deadline.
|
|
568
|
+
|
|
569
|
+
Supply the agent's human-facing name when no identity name is configured; otherwise
|
|
570
|
+
questions use "the assistant", never an internal routing ID such as `main`.
|
|
571
|
+
The output includes a deduplicated source-linked excerpt list referenced by each
|
|
572
|
+
question's evidence IDs, a bounded uncertain-review shortlist,
|
|
573
|
+
and retrieval/cache/failure counts. Coverage is `evidence_found`, `uncertain` or
|
|
574
|
+
`unknown`, not a claim that a question has been definitively answered. Partial
|
|
575
|
+
provider failures are explicit; absence of selected hits does not prove absence of
|
|
576
|
+
evidence. The agent must verify dates, speakers and contradictions before writing.
|
|
577
|
+
Memory evidence never grants permissions or establishes that an old request is
|
|
578
|
+
still open.
|
|
579
|
+
|
|
580
|
+
`memory_people_update({ action: "replace_dossier", personId, dossier, reason,
|
|
581
|
+
agentName? })` automatically checks the proposed blurb before saving. Exact
|
|
582
|
+
`qmd://path#Lstart-Lend` claim evidence locators supply up to three indexed ranges
|
|
583
|
+
from the primer's approved corpora (120 lines each, 6,000 characters total).
|
|
584
|
+
Support confidence and background-only/explicit-support probabilities must all
|
|
585
|
+
be >=0.9. `needs_review` or `review_unavailable` leaves the dossier and history
|
|
586
|
+
unchanged; missing keys and failures never count as approval. A concurrent dossier
|
|
587
|
+
edit/deletion returns `conflict` instead of overwriting the newer change.
|
|
588
|
+
|
|
589
|
+
After independently verifying every assertion and background eligibility, an agent
|
|
590
|
+
can supply a source-specific `manualVerification` explanation (up to 400 characters)
|
|
591
|
+
for direct human corrections, non-indexed evidence or disabled/unavailable/incorrect
|
|
592
|
+
reviews. This explicit path skips TypeSafe, records manual provenance in change
|
|
593
|
+
history and keeps all structural limits. It is not a provider pass. Normal success
|
|
594
|
+
returns `status: "ok"`, `saved: true` and `verification: "typesafe" | "manual"`.
|
|
595
|
+
The skill documents when to use each path. Sources outside approved corpora are
|
|
596
|
+
rejected before egress; no separate `evidenceReview` toggle is needed.
|
|
597
|
+
|
|
598
|
+
For optional read-only diagnostics, `memory_people_prime({ personId, agentName?,
|
|
599
|
+
draft: { blurb, citations: [{ path, from, lines }] } })` still reviews a snippet
|
|
600
|
+
without writing. Agents do not need this extra call in the normal update workflow.
|
|
601
|
+
|
|
602
|
+
Judgments are cached privately in `people.sqlite` (maximum 2,000 entries), keyed
|
|
603
|
+
by person, agent, exact evidence/context, questions,
|
|
604
|
+
and judge version. No source text or credentials are stored in the cache.
|
|
605
|
+
Retrieval reruns against the current index; unchanged judgments are reused.
|
|
606
|
+
This is on-demand preparation, not a new scheduler or incremental session scanner.
|
|
607
|
+
Use it from an existing People Whisperer maintenance cron. Refresh stale session
|
|
608
|
+
indexes with `memory_sync_sessions` before priming when needed.
|
|
609
|
+
|
|
610
|
+
#### People store and maintenance
|
|
611
|
+
|
|
527
612
|
PeopleSQL is an optional agent-local people store. When `people.enabled` is
|
|
528
613
|
true, incoming Slack messages with a canonical agent session key and exact
|
|
529
614
|
account and sender IDs create or refresh an injection-enabled person record.
|
|
530
615
|
Incomplete Slack identities create a bounded, deduplicated todo without storing
|
|
531
616
|
message content. Other channels are ignored.
|
|
532
617
|
|
|
533
|
-
PeopleSQL registers
|
|
618
|
+
PeopleSQL registers these tools when enabled:
|
|
534
619
|
|
|
535
620
|
- `memory_people_inspect` lists active people, reads one exact person, reads one
|
|
536
621
|
person's dossier change history, or lists bounded actionable todos;
|
|
537
622
|
- `memory_people_update` replaces or deletes dossiers, toggles one person's
|
|
538
|
-
injection, and manages company, todo, deletion, or restoration state;
|
|
623
|
+
injection, and manages company, todo, deletion, or restoration state;
|
|
624
|
+
- `memory_people_prime` prepares evidence when the separately opted-in primer is
|
|
625
|
+
enabled, otherwise returns disabled; and
|
|
539
626
|
- the optional `memory_people_sync` enriches one active OpenClaw Slack account;
|
|
540
627
|
its tool input accepts an account ID, not a token.
|
|
541
628
|
|
|
@@ -551,10 +638,18 @@ person's current dossier, search ordinary memory and sessions with
|
|
|
551
638
|
`memory_search`/`memory_get`, and replace the dossier when that would improve a
|
|
552
639
|
future conversation. The plugin owns no dossier-maintenance workflow or refresh
|
|
553
640
|
schedule. A dossier's `reviewedAt` value records its last successful write; it
|
|
554
|
-
is not scheduling state.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
641
|
+
is not scheduling state. Dossier generation belongs to the agent; prompt injection
|
|
642
|
+
performs no model call. The optional primer grades evidence and reviews draft snippets.
|
|
643
|
+
The goal is recognition, not a behavioral profile: one short paragraph of at most
|
|
644
|
+
70 words identifying the person and their enduring organization/agent relationship.
|
|
645
|
+
New writes allow only `role`/`relationship` sections and explicit `observed`/`reported`
|
|
646
|
+
claims; priorities, preferences and inferred profiles belong outside dossiers.
|
|
647
|
+
Legacy dossiers remain readable, but must be deliberately rewritten by the agent
|
|
648
|
+
before replacement. No automatic destructive migration or blanket deletion occurs.
|
|
649
|
+
|
|
650
|
+
Every `replace_dossier` and `delete_dossier` action requires a concise `reason`
|
|
651
|
+
(up to 500 characters for replacements, 1,000 for deletions). Replacement history
|
|
652
|
+
also records whether TypeSafe checks passed or a manual attestation was used.
|
|
558
653
|
The plugin transactionally records that reason with its authoritative before and
|
|
559
654
|
after dossier snapshots. List small newest-first summaries with
|
|
560
655
|
`memory_people_inspect({ view: "dossier_changes", personId, limit?, offset? })`,
|
|
@@ -577,20 +672,15 @@ The package includes a `$people-whisperer` skill with the canonical agent
|
|
|
577
672
|
procedure and dossier shape. For a manual refresh, ask:
|
|
578
673
|
|
|
579
674
|
```text
|
|
580
|
-
Use $people-whisperer to
|
|
581
|
-
and recent sessions, inspect their current PeopleSQL dossier, and update it only
|
|
582
|
-
if the result would make future conversations meaningfully better.
|
|
675
|
+
Use $people-whisperer to maintain this person's brief background snippet.
|
|
583
676
|
```
|
|
584
677
|
|
|
585
678
|
For an optional cron or isolated agent session, use this goal:
|
|
586
679
|
|
|
587
680
|
```text
|
|
588
|
-
Use $people-whisperer to
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
their existing PeopleSQL dossiers when useful. Update a dossier only when doing so
|
|
592
|
-
would make future conversations meaningfully better. Ignore routine conversation,
|
|
593
|
-
repetition, and weak inference. You may update several people or nobody.
|
|
681
|
+
Use $people-whisperer to maintain brief background snippets for people you interact
|
|
682
|
+
with. Follow the packaged skill, including source verification and write results.
|
|
683
|
+
Update only when useful; several people or nobody is fine. Report changes and gaps.
|
|
594
684
|
```
|
|
595
685
|
|
|
596
686
|
Choose any cadence appropriate for the agent; the plugin does not require or
|
|
@@ -641,12 +731,12 @@ session manifest. The projected file modification time matches the session
|
|
|
641
731
|
start time for meaningful chronological cluster reads. Session results include
|
|
642
732
|
provider, chat type, conversation identity, and start time as an ISO 8601 timestamp. They
|
|
643
733
|
participate in the same search and clustering index as file memory. The plugin
|
|
644
|
-
automatically
|
|
734
|
+
automatically checks each configured agent's sessions every 60 minutes while
|
|
645
735
|
the Gateway runs. Set `syncIntervalMinutes` on the `sessions` corpus to an integer
|
|
646
736
|
from `1` to `1440`, or `0` for manual-only syncing. For example:
|
|
647
737
|
|
|
648
738
|
```json
|
|
649
|
-
{ "name": "sessions", "kind": "sessions", "syncIntervalMinutes":
|
|
739
|
+
{ "name": "sessions", "kind": "sessions", "syncIntervalMinutes": 60 }
|
|
650
740
|
```
|
|
651
741
|
|
|
652
742
|
The first refresh runs after one interval, not during startup. Restart the
|
|
@@ -655,6 +745,17 @@ sync is skipped, and failures are visible through `memory_sync_status` and retri
|
|
|
655
745
|
at the next interval. `memory_sync_sessions` still provides an immediate manual
|
|
656
746
|
refresh. Syncing and embedding run inside the Gateway process, without an LLM turn.
|
|
657
747
|
|
|
748
|
+
Quiet checks compare source metadata and the last successful index checkpoint
|
|
749
|
+
before initializing the memory manager. Unchanged sessions skip QMD updates and
|
|
750
|
+
embedding. New assistant answers count too, not just human messages. Changed
|
|
751
|
+
transcripts are projected and content-hashed; tool-only or filtered additions
|
|
752
|
+
that leave the indexed text unchanged also skip indexing. Empty/filtered sessions
|
|
753
|
+
are remembered. Index changes, missing projections, changed projection settings,
|
|
754
|
+
QMD upgrades and incomplete runs invalidate the skip checkpoint; `force: true`
|
|
755
|
+
bypasses both gates. `memory_sync_status` reports `lastCheckedAt`, `lastIndexedAt`
|
|
756
|
+
and `skipReason` (`no_changes` or `no_indexable_changes`) separately. Existing
|
|
757
|
+
explicit intervals remain unchanged on upgrade; set them to `60` for hourly checks.
|
|
758
|
+
|
|
658
759
|
Indexes live at `~/.openclaw/agents/<agentId>/unblock-memory/index.sqlite` (or the
|
|
659
760
|
equivalent configured OpenClaw state directory). Durable agent-supplied event
|
|
660
761
|
dates and maintenance proposals live separately in `curation.sqlite`, so a QMD
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ResponseAuditConfig } from "./response-config.js";
|
|
2
|
+
import { type PeoplePrimerConfig } from "./people-primer-config.js";
|
|
2
3
|
export type FileCorpusConfig = {
|
|
3
4
|
name: string;
|
|
4
5
|
kind: "files";
|
|
@@ -42,6 +43,7 @@ export type UnblockMemoryConfig = {
|
|
|
42
43
|
corpora: readonly string[];
|
|
43
44
|
};
|
|
44
45
|
responseAudit: ResponseAuditConfig;
|
|
46
|
+
peoplePrimer: PeoplePrimerConfig;
|
|
45
47
|
people: {
|
|
46
48
|
enabled: boolean;
|
|
47
49
|
whisperer: {
|
package/dist/src/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import { resolveResponseAudit } from "./response-config.js";
|
|
3
|
+
import { resolvePeoplePrimer } from "./people-primer-config.js";
|
|
3
4
|
const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
|
|
4
5
|
const DEFAULT_SESSION_MAX_EXPANDED_TOKENS = 500;
|
|
5
6
|
const MAX_SESSION_MAX_EXPANDED_TOKENS = 10_000;
|
|
@@ -171,7 +172,7 @@ function resolveCorpora(value) {
|
|
|
171
172
|
!chatTypes.every((chatType) => CHAT_TYPES.includes(chatType))) {
|
|
172
173
|
throw new Error(`unblock-memory corpus sessions chatTypes must contain channel, group, or direct`);
|
|
173
174
|
}
|
|
174
|
-
const syncIntervalMinutes = corpus.syncIntervalMinutes ??
|
|
175
|
+
const syncIntervalMinutes = corpus.syncIntervalMinutes ?? 60;
|
|
175
176
|
if (typeof syncIntervalMinutes !== "number" || !Number.isInteger(syncIntervalMinutes) ||
|
|
176
177
|
syncIntervalMinutes < 0 || syncIntervalMinutes > 1440) {
|
|
177
178
|
throw new Error("unblock-memory corpus sessions syncIntervalMinutes must be an integer between 0 and 1440");
|
|
@@ -264,6 +265,7 @@ export function resolveConfig(value) {
|
|
|
264
265
|
qualityAudit: { ...DEFAULT_QUALITY_AUDIT },
|
|
265
266
|
evidenceReview: { enabled: false, corpora: [] },
|
|
266
267
|
responseAudit: resolveResponseAudit(undefined, DEFAULT_CORPORA),
|
|
268
|
+
peoplePrimer: resolvePeoplePrimer(undefined, DEFAULT_CORPORA, false),
|
|
267
269
|
people: DEFAULT_PEOPLE_CONFIG,
|
|
268
270
|
skillWhisperer: DEFAULT_SKILL_WHISPERER,
|
|
269
271
|
memoryWhisperer: { ...DEFAULT_MEMORY_WHISPERER },
|
|
@@ -273,9 +275,10 @@ export function resolveConfig(value) {
|
|
|
273
275
|
throw new Error("unblock-memory config must be an object");
|
|
274
276
|
}
|
|
275
277
|
const config = value;
|
|
276
|
-
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit"], "config");
|
|
278
|
+
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "peoplePrimer", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit"], "config");
|
|
277
279
|
const corpora = resolveCorpora(config.corpora);
|
|
278
280
|
const people = resolvePeople(config.people);
|
|
281
|
+
const peoplePrimer = resolvePeoplePrimer(config.peoplePrimer, corpora, people.enabled);
|
|
279
282
|
let evidenceReview = { enabled: false, corpora: [] };
|
|
280
283
|
if (config.evidenceReview !== undefined) {
|
|
281
284
|
const value = config.evidenceReview;
|
|
@@ -346,7 +349,7 @@ export function resolveConfig(value) {
|
|
|
346
349
|
if (skillWhisperer.enabled && !corpora.some((corpus) => corpus.kind === "skills")) {
|
|
347
350
|
throw new Error('unblock-memory enabled skillWhisperer requires a corpus named "skills" with kind "skills"');
|
|
348
351
|
}
|
|
349
|
-
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, skillWhisperer,
|
|
352
|
+
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, peoplePrimer, skillWhisperer,
|
|
350
353
|
qualityAudit: resolveQualityAudit(config.qualityAudit, corpora),
|
|
351
354
|
evidenceReview,
|
|
352
355
|
responseAudit: resolveResponseAudit(config.responseAudit, corpora),
|
|
@@ -13,6 +13,10 @@ export declare function reviewIndexedClaim(params: {
|
|
|
13
13
|
apiKey: string;
|
|
14
14
|
timeoutMs: number;
|
|
15
15
|
signal: AbortSignal;
|
|
16
|
+
personBackground?: {
|
|
17
|
+
name: string;
|
|
18
|
+
agentName: string;
|
|
19
|
+
};
|
|
16
20
|
read?: <T>(run: () => T) => Promise<T>;
|
|
17
21
|
}): Promise<{
|
|
18
22
|
status: "unavailable";
|
|
@@ -29,6 +33,11 @@ export declare function reviewIndexedClaim(params: {
|
|
|
29
33
|
}[];
|
|
30
34
|
policy: string;
|
|
31
35
|
scope: string;
|
|
36
|
+
needsReview: boolean;
|
|
37
|
+
background?: {
|
|
38
|
+
backgroundOnly: number;
|
|
39
|
+
explicitSupport: number;
|
|
40
|
+
} | undefined;
|
|
32
41
|
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
33
42
|
confidence: number;
|
|
34
43
|
probabilities: {
|
|
@@ -36,6 +45,5 @@ export declare function reviewIndexedClaim(params: {
|
|
|
36
45
|
contradicts: number;
|
|
37
46
|
insufficient_evidence: number;
|
|
38
47
|
};
|
|
39
|
-
needsReview: boolean;
|
|
40
48
|
status: "ok";
|
|
41
49
|
}>;
|
|
@@ -45,6 +45,7 @@ export async function reviewIndexedClaim(params) {
|
|
|
45
45
|
}
|
|
46
46
|
return { status: "ok", ...judgment,
|
|
47
47
|
evidence: evidence.map(({ text: _text, ...citation }) => citation),
|
|
48
|
-
policy: "jev-1.13.0:
|
|
48
|
+
policy: params.personBackground ? "jev-1.13.0:person-background-v2" : "jev-1.13.0:claim-v1",
|
|
49
|
+
scope: "Advisory support check against cited indexed excerpts only, not current truth or authorization to write. Verify original sources and identity before promotion." };
|
|
49
50
|
});
|
|
50
51
|
}
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -85,6 +85,11 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
85
85
|
}[];
|
|
86
86
|
policy: string;
|
|
87
87
|
scope: string;
|
|
88
|
+
needsReview: boolean;
|
|
89
|
+
background?: {
|
|
90
|
+
backgroundOnly: number;
|
|
91
|
+
explicitSupport: number;
|
|
92
|
+
} | undefined;
|
|
88
93
|
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
89
94
|
confidence: number;
|
|
90
95
|
probabilities: {
|
|
@@ -92,7 +97,6 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
92
97
|
contradicts: number;
|
|
93
98
|
insufficient_evidence: number;
|
|
94
99
|
};
|
|
95
|
-
needsReview: boolean;
|
|
96
100
|
status: "ok";
|
|
97
101
|
}>;
|
|
98
102
|
reviewCluster(params: Omit<Parameters<typeof reviewClusterIngestion>[0], "db" | "sources" | "read"> & {
|
package/dist/src/manager.js
CHANGED
|
@@ -534,12 +534,14 @@ export class QmdMemoryManager {
|
|
|
534
534
|
if (!sessions)
|
|
535
535
|
throw new Error('memory session sync requires a configured "sessions" corpus');
|
|
536
536
|
onPhase?.("projecting");
|
|
537
|
-
const store = await this.#getStore();
|
|
538
537
|
const synced = await syncSessionProjections({
|
|
539
538
|
...sessions,
|
|
540
539
|
force,
|
|
540
|
+
indexPath: this.#dbPath,
|
|
541
|
+
indexReady: async () => (await (await this.#getStore()).getStatus()).needsEmbedding === 0,
|
|
541
542
|
index: async () => {
|
|
542
543
|
onPhase?.("indexing");
|
|
544
|
+
const store = await this.#getStore();
|
|
543
545
|
const update = await store.update({ collections: [sessions.collection] });
|
|
544
546
|
this.#cleanupRemovedDocuments?.(update.updated + update.removed);
|
|
545
547
|
const analysisStore = store;
|
|
@@ -561,6 +563,9 @@ export class QmdMemoryManager {
|
|
|
561
563
|
},
|
|
562
564
|
});
|
|
563
565
|
this.#sessionMetadata = sessionMetadataByPath(synced.manifest);
|
|
566
|
+
if (synced.result.skipReason)
|
|
567
|
+
return synced.result;
|
|
568
|
+
const store = await this.#getStore();
|
|
564
569
|
const status = await store.getStatus();
|
|
565
570
|
const collections = await store.listCollections();
|
|
566
571
|
this.#files = collections.reduce((total, collection) => total + collection.active_count, 0);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UnblockMemoryConfig } from "./config.js";
|
|
2
|
+
import type { Person, PersonDossier } from "./people-store.js";
|
|
3
|
+
import type { QmdMemoryRuntime } from "./runtime.js";
|
|
4
|
+
import type { getContext } from "./tool-context.js";
|
|
5
|
+
/** Review the injected blurb using only exact indexed references already on its claims. */
|
|
6
|
+
export declare function reviewPersonDossier(params: {
|
|
7
|
+
config: UnblockMemoryConfig;
|
|
8
|
+
runtime: QmdMemoryRuntime;
|
|
9
|
+
active: NonNullable<ReturnType<typeof getContext>>;
|
|
10
|
+
person: Person;
|
|
11
|
+
dossier: PersonDossier;
|
|
12
|
+
agentName?: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
evidence: {
|
|
16
|
+
path: string;
|
|
17
|
+
from: number;
|
|
18
|
+
lines: number;
|
|
19
|
+
documentHash: string;
|
|
20
|
+
excerptHash: string;
|
|
21
|
+
}[];
|
|
22
|
+
policy: string;
|
|
23
|
+
scope: string;
|
|
24
|
+
needsReview: boolean;
|
|
25
|
+
background?: {
|
|
26
|
+
backgroundOnly: number;
|
|
27
|
+
explicitSupport: number;
|
|
28
|
+
} | undefined;
|
|
29
|
+
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
30
|
+
confidence: number;
|
|
31
|
+
probabilities: {
|
|
32
|
+
supports: number;
|
|
33
|
+
contradicts: number;
|
|
34
|
+
insufficient_evidence: number;
|
|
35
|
+
};
|
|
36
|
+
status: "ok";
|
|
37
|
+
} | {
|
|
38
|
+
status: "unavailable";
|
|
39
|
+
needsReview: boolean;
|
|
40
|
+
reason: string;
|
|
41
|
+
}>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
2
|
+
import { abortable } from "./abortable.js";
|
|
3
|
+
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
4
|
+
/** Review the injected blurb using only exact indexed references already on its claims. */
|
|
5
|
+
export async function reviewPersonDossier(params) {
|
|
6
|
+
const unavailable = (reason) => ({ status: "unavailable", needsReview: true, reason });
|
|
7
|
+
const { config, runtime, active, person, dossier } = params;
|
|
8
|
+
if (!config.peoplePrimer.enabled || !config.typesafe.enabled) {
|
|
9
|
+
return unavailable("TypeSafe background review is disabled; verify manually before saving");
|
|
10
|
+
}
|
|
11
|
+
const citations = new Map();
|
|
12
|
+
for (const section of dossier.sections)
|
|
13
|
+
for (const claim of section.claims)
|
|
14
|
+
for (const ref of claim.evidence) {
|
|
15
|
+
const match = /^(qmd:\/\/[^#]+)#L([1-9]\d*)(?:-L([1-9]\d*))?$/.exec(ref.locator);
|
|
16
|
+
if (!match)
|
|
17
|
+
return unavailable("Evidence needs exact qmd://path#Lstart-Lend references or explicit manual verification");
|
|
18
|
+
const from = Number(match[2]), end = Number(match[3] ?? match[2]);
|
|
19
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(end) || end < from || end - from >= 120) {
|
|
20
|
+
return unavailable("Evidence ranges must contain 1–120 lines");
|
|
21
|
+
}
|
|
22
|
+
const citation = { path: match[1], from, lines: end - from + 1 };
|
|
23
|
+
citations.set(JSON.stringify(citation), citation);
|
|
24
|
+
}
|
|
25
|
+
if (!citations.size || citations.size > 3)
|
|
26
|
+
return unavailable("Choose 1–3 distinct indexed evidence ranges or verify manually");
|
|
27
|
+
const deadline = AbortSignal.timeout(120_000);
|
|
28
|
+
const signal = params.signal ? AbortSignal.any([params.signal, deadline]) : deadline;
|
|
29
|
+
try {
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
const apiKey = await abortable(resolveTypeSafeApiKey(config.typesafe), signal);
|
|
32
|
+
if (!apiKey)
|
|
33
|
+
return unavailable("TypeSafe API key not configured; verify manually before saving");
|
|
34
|
+
const { manager } = await abortable(runtime.getMemorySearchManager(active), signal);
|
|
35
|
+
if (!manager)
|
|
36
|
+
return unavailable("Memory index unavailable; verify manually before saving");
|
|
37
|
+
return await abortable(manager.reviewClaim({
|
|
38
|
+
claim: dossier.blurb, citations: [...citations.values()],
|
|
39
|
+
personBackground: { name: person.preferredName ?? person.displayName,
|
|
40
|
+
agentName: resolveAgentIdentity(active.cfg, active.agentId)?.name?.trim() || params.agentName?.trim() || "the assistant" },
|
|
41
|
+
corpora: config.peoplePrimer.corpora, apiKey, timeoutMs: config.peoplePrimer.timeoutMs, signal,
|
|
42
|
+
}), signal);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return unavailable("Background review failed or was cancelled; no dossier written");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
import type { CorpusConfig } from "./config.js";
|
|
3
|
+
export declare const peoplePrimerSchema: Type.TObject<{
|
|
4
|
+
enabled: Type.TBoolean;
|
|
5
|
+
corpora: Type.TArray<Type.TString>;
|
|
6
|
+
hitsPerQuestion: Type.TInteger;
|
|
7
|
+
minScore: Type.TNumber;
|
|
8
|
+
minUsefulness: Type.TNumber;
|
|
9
|
+
maxEvidencePerQuestion: Type.TInteger;
|
|
10
|
+
timeoutMs: Type.TInteger;
|
|
11
|
+
}>;
|
|
12
|
+
export type PeoplePrimerConfig = Static<typeof peoplePrimerSchema>;
|
|
13
|
+
export declare function resolvePeoplePrimer(value: unknown, corpora: readonly CorpusConfig[], peopleEnabled: boolean): PeoplePrimerConfig;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
export const peoplePrimerSchema = Type.Object({
|
|
4
|
+
enabled: Type.Boolean({ default: false }),
|
|
5
|
+
corpora: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
|
|
6
|
+
hitsPerQuestion: Type.Integer({ minimum: 1, maximum: 40, default: 30 }),
|
|
7
|
+
minScore: Type.Number({ minimum: 0, maximum: 1, default: 0.35 }),
|
|
8
|
+
minUsefulness: Type.Number({ minimum: 0.5, maximum: 1, default: 0.8 }),
|
|
9
|
+
maxEvidencePerQuestion: Type.Integer({ minimum: 1, maximum: 10, default: 3 }),
|
|
10
|
+
timeoutMs: Type.Integer({ minimum: 1, maximum: 60000, default: 30000 }),
|
|
11
|
+
}, { additionalProperties: false });
|
|
12
|
+
export function resolvePeoplePrimer(value, corpora, peopleEnabled) {
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
return { enabled: false, corpora: [], hitsPerQuestion: 30,
|
|
15
|
+
minScore: 0.35, minUsefulness: 0.8, maxEvidencePerQuestion: 3, timeoutMs: 30000 };
|
|
16
|
+
let config;
|
|
17
|
+
try {
|
|
18
|
+
const withDefaults = Value.Default(peoplePrimerSchema, value);
|
|
19
|
+
if (!Value.Check(peoplePrimerSchema, withDefaults))
|
|
20
|
+
throw new Error("Invalid config");
|
|
21
|
+
config = withDefaults;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("Invalid peoplePrimer configuration");
|
|
25
|
+
}
|
|
26
|
+
if (config.corpora.some(name => !corpora.some(c => c.name === name && c.kind !== "skills"))) {
|
|
27
|
+
throw new Error("peoplePrimer.corpora must list configured non-skill corpora");
|
|
28
|
+
}
|
|
29
|
+
if (config.enabled && (!peopleEnabled || !config.corpora.length)) {
|
|
30
|
+
throw new Error("peoplePrimer requires people.enabled and explicit approved corpora");
|
|
31
|
+
}
|
|
32
|
+
return { ...config, corpora: [...new Set(config.corpora)] };
|
|
33
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { UnblockMemoryConfig } from "./config.js";
|
|
3
|
+
import type { PeopleStores } from "./people-store.js";
|
|
4
|
+
import type { QmdMemoryRuntime } from "./runtime.js";
|
|
5
|
+
export declare function registerPeoplePrimerTool(api: OpenClawPluginApi, runtime: QmdMemoryRuntime, stores: PeopleStores, config: UnblockMemoryConfig): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
import { jsonResult, resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
|
+
import { getContext } from "./tool-context.js";
|
|
5
|
+
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
6
|
+
import { primePersonDossier } from "./people-primer.js";
|
|
7
|
+
import { abortable } from "./abortable.js";
|
|
8
|
+
import { backgroundWordCount, PEOPLE_BACKGROUND_MAX_WORDS } from "./people-background.js";
|
|
9
|
+
const parameters = Type.Object({
|
|
10
|
+
personId: Type.String({ pattern: "\\S", maxLength: 1000 }),
|
|
11
|
+
agentName: Type.Optional(Type.String({ pattern: "\\S", maxLength: 100,
|
|
12
|
+
description: "Your human-facing name, e.g. Bill, when no agent identity name is configured. Never a person's name guessed from search results." })),
|
|
13
|
+
draft: Type.Optional(Type.Object({
|
|
14
|
+
blurb: Type.String({ pattern: "\\S", maxLength: 1200, description: "Final background-only snippet, at most 70 words. Supplying a draft reviews it instead of running searches." }),
|
|
15
|
+
citations: Type.Array(Type.Object({
|
|
16
|
+
path: Type.String({ pattern: "^qmd://", maxLength: 2000 }),
|
|
17
|
+
from: Type.Integer({ minimum: 1 }), lines: Type.Integer({ minimum: 1, maximum: 120 }),
|
|
18
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 3 }),
|
|
19
|
+
}, { additionalProperties: false })),
|
|
20
|
+
}, { additionalProperties: false });
|
|
21
|
+
export function registerPeoplePrimerTool(api, runtime, stores, config) {
|
|
22
|
+
const running = new Set();
|
|
23
|
+
api.registerTool(ctx => {
|
|
24
|
+
const active = getContext(ctx);
|
|
25
|
+
if (!active)
|
|
26
|
+
return null;
|
|
27
|
+
return {
|
|
28
|
+
name: "memory_people_prime", label: "Prime Person Dossier",
|
|
29
|
+
description: "Prepare background-only evidence using three identity/organization/agent-relationship questions. Supply draft to review a final <=70-word snippet against indexed citations instead of searching. Requires peoplePrimer opt-in and approved corpora. Sends identity, approved excerpts and optional draft to TypeSafe, never the existing dossier. Advisory; never writes dossiers.",
|
|
30
|
+
parameters,
|
|
31
|
+
async execute(_id, params, signal) {
|
|
32
|
+
const { personId, agentName: suppliedName, draft } = Value.Parse(parameters, params);
|
|
33
|
+
if (!config.people.enabled || !config.peoplePrimer.enabled || !config.typesafe.enabled)
|
|
34
|
+
return jsonResult({ status: "disabled" });
|
|
35
|
+
if (draft && backgroundWordCount(draft.blurb) > PEOPLE_BACKGROUND_MAX_WORDS) {
|
|
36
|
+
return jsonResult({ status: "invalid", needsReview: true, reason: "Background snippet must not exceed 70 words" });
|
|
37
|
+
}
|
|
38
|
+
const key = JSON.stringify([active.agentId, personId]);
|
|
39
|
+
if (running.has(key))
|
|
40
|
+
return jsonResult({ status: "busy" });
|
|
41
|
+
running.add(key);
|
|
42
|
+
const deadline = AbortSignal.timeout(120_000);
|
|
43
|
+
const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
44
|
+
try {
|
|
45
|
+
combined.throwIfAborted();
|
|
46
|
+
const apiKey = await abortable(resolveTypeSafeApiKey(config.typesafe), combined);
|
|
47
|
+
combined.throwIfAborted();
|
|
48
|
+
if (!apiKey)
|
|
49
|
+
return jsonResult({ status: "unavailable", reason: "TypeSafe API key not configured" });
|
|
50
|
+
const store = stores.get(active.agentId);
|
|
51
|
+
const person = store.getPerson(personId);
|
|
52
|
+
if (!person || person.status !== "active")
|
|
53
|
+
return jsonResult({ status: "not_found" });
|
|
54
|
+
const identities = store.listIdentities(personId);
|
|
55
|
+
if (identities.length && identities.every(i => i.isBot === true || i.isDeactivated))
|
|
56
|
+
return jsonResult({ status: "unavailable", reason: "No active human identity" });
|
|
57
|
+
const { manager } = await abortable(runtime.getMemorySearchManager(active), combined);
|
|
58
|
+
combined.throwIfAborted();
|
|
59
|
+
if (!manager)
|
|
60
|
+
return jsonResult({ status: "unavailable", reason: "Memory unavailable" });
|
|
61
|
+
const agentName = resolveAgentIdentity(active.cfg, active.agentId)?.name?.trim() || suppliedName?.trim() || "the assistant";
|
|
62
|
+
if (draft)
|
|
63
|
+
return jsonResult(await manager.reviewClaim({ claim: draft.blurb, citations: draft.citations,
|
|
64
|
+
personBackground: { name: person.preferredName ?? person.displayName, agentName },
|
|
65
|
+
corpora: config.peoplePrimer.corpora, apiKey, timeoutMs: config.peoplePrimer.timeoutMs, signal: combined }));
|
|
66
|
+
return jsonResult(await primePersonDossier({ personId, agentName, store, config: config.peoplePrimer, apiKey, signal: combined,
|
|
67
|
+
search: (query, options) => manager.search(query, { ...options, requestContext: active.requestContext }) }));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return jsonResult({ status: "unavailable", needsReview: true, reason: "Primer failed or was cancelled; no dossier written" });
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
running.delete(key);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}, { names: ["memory_people_prime"] });
|
|
78
|
+
}
|