@unblocklabs/unblock-memory 0.8.3 → 0.9.0
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 +194 -94
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +4 -2
- package/dist/src/diagnostics.d.ts +46 -2
- package/dist/src/diagnostics.js +11 -2
- package/dist/src/inside-out.js +3 -10
- package/dist/src/plugin.js +2 -0
- package/dist/src/response-judge.d.ts +1 -1
- package/dist/src/response-stages.d.ts +1 -1
- package/dist/src/response-text.d.ts +7 -0
- package/dist/src/response-text.js +14 -2
- package/dist/src/review-tools.js +4 -1
- package/dist/src/safety-config.d.ts +30 -0
- package/dist/src/safety-config.js +118 -0
- package/dist/src/safety-whisperer.d.ts +31 -0
- package/dist/src/safety-whisperer.js +302 -0
- package/dist/src/skill-whisperer.d.ts +1 -0
- package/dist/src/skill-whisperer.js +2 -2
- package/dist/src/typesafe-client.d.ts +2 -0
- package/dist/src/typesafe-client.js +7 -0
- package/dist/src/whisperer-prompt.d.ts +4 -2
- package/dist/src/whisperer-prompt.js +6 -5
- package/docs/configuration.md +166 -1
- package/openclaw.plugin.json +95 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,109 +1,209 @@
|
|
|
1
1
|
# Unblock Memory
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Long-term memory for OpenClaw agents. Unblock Memory indexes each agent's
|
|
4
|
+
workspace Markdown, and optionally its past conversations, so the agent can find
|
|
5
|
+
what it already knows with `memory_search` and check the source with `memory_get`.
|
|
6
|
+
Results are ranked by how useful each passage is for the question, not only by
|
|
7
|
+
keyword or vector similarity.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
**Requirements:** Node.js 22+, OpenClaw 2026.8.1-beta.3 or later, and a
|
|
12
|
+
[TypeSafe](https://docs.typesafe.ai) API key. Search uses TypeSafe to rank results.
|
|
13
|
+
|
|
14
|
+
1. Install once per OpenClaw host, not once per agent:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
openclaw plugins install npm:@unblocklabs/unblock-memory
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. Save your TypeSafe key in a private file (`chmod 600`), for example
|
|
21
|
+
`~/.openclaw/secrets/unblock-memory-typesafe.env`:
|
|
22
|
+
|
|
23
|
+
```dotenv
|
|
24
|
+
TYPESAFE_API_KEY="your-key"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
3. Merge this into `~/.openclaw/openclaw.json`, keeping your other settings.
|
|
28
|
+
`apiKeyFile` must be an absolute path.
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"plugins": {
|
|
33
|
+
"slots": { "memory": "unblock-memory" },
|
|
34
|
+
"entries": {
|
|
35
|
+
"unblock-memory": {
|
|
36
|
+
"enabled": true,
|
|
37
|
+
"config": {
|
|
38
|
+
"typesafe": { "apiKeyFile": "/home/you/.openclaw/secrets/unblock-memory-typesafe.env" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
4. Restart the Gateway, then ask an agent to run `memory_diagnostics`:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
openclaw gateway restart
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`"credential": "available"` means the key was found. It does not check that
|
|
53
|
+
TypeSafe accepts the key; a test search does.
|
|
54
|
+
|
|
55
|
+
By default each agent indexes `MEMORY.md`, `USER.md` and `memory/**/*.md` in its
|
|
56
|
+
workspace. The index is built on first use and refreshes in the background when
|
|
57
|
+
those files change. QMD, the local search engine, ships inside the package, so
|
|
58
|
+
there is nothing else to install or run.
|
|
59
|
+
|
|
60
|
+
To install from source instead:
|
|
61
|
+
`openclaw plugins install git:github.com/unblocklabs-ai/unblock-memory`.
|
|
8
62
|
|
|
9
|
-
##
|
|
63
|
+
## How it works
|
|
10
64
|
|
|
11
|
-
|
|
65
|
+
The agent writes two queries: keywords for BM25 search and a plain-language
|
|
66
|
+
question for vector search. Each returns up to ten candidate passages. TypeSafe
|
|
67
|
+
scores how useful each complete passage is for the question, and the best ones
|
|
68
|
+
come back with their source location.
|
|
12
69
|
|
|
13
|
-
```
|
|
14
|
-
|
|
70
|
+
```json
|
|
71
|
+
{ "bm25Query": "staging rollout approval", "vectorQuery": "Who approved the staging rollout?" }
|
|
15
72
|
```
|
|
16
73
|
|
|
17
|
-
Merge this into your OpenClaw configuration, preserving other plugins/settings:
|
|
18
|
-
|
|
19
74
|
```json
|
|
20
75
|
{
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
76
|
+
"results": [
|
|
77
|
+
{
|
|
78
|
+
"path": "qmd://source-3f9a1c2b7d4e/memory/2026-09-14.md",
|
|
79
|
+
"startLine": 12,
|
|
80
|
+
"endLine": 15,
|
|
81
|
+
"snippet": "Priya approved the staging rollout after the load test passed.",
|
|
82
|
+
"corpus": "memory",
|
|
83
|
+
"score": 0.93
|
|
25
84
|
}
|
|
26
|
-
|
|
85
|
+
]
|
|
27
86
|
}
|
|
28
87
|
```
|
|
29
88
|
|
|
30
|
-
The
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
89
|
+
The agent then calls `memory_get` with that `path` to read the surrounding
|
|
90
|
+
context, attribution and dates. Search defaults to 5 results with a usefulness
|
|
91
|
+
score of at least 0.7. See [search and read](docs/retrieval.md#search-and-read)
|
|
92
|
+
for every parameter.
|
|
93
|
+
|
|
94
|
+
**What leaves your machine:** indexing, embeddings, storage and `memory_get` run
|
|
95
|
+
locally. `memory_search` sends the search question and candidate passages, with
|
|
96
|
+
their paths and dates, to TypeSafe for scoring. Optional TypeSafe features send
|
|
97
|
+
only what their own settings approve; see
|
|
98
|
+
[TypeSafe data scope](docs/configuration.md#typesafe-data-scope).
|
|
99
|
+
|
|
100
|
+
**On by default:** search and read over the `memory` corpus, and a
|
|
101
|
+
[compaction memory flush](docs/configuration.md#compaction-memory-writes): before
|
|
102
|
+
OpenClaw compacts a long conversation, the agent is prompted to append lasting
|
|
103
|
+
notes to `memory/YYYY-MM-DD.md`. Turn the flush off with the host's
|
|
104
|
+
`agents.defaults.compaction.memoryFlush.enabled` setting.
|
|
105
|
+
|
|
106
|
+
## Optional features
|
|
107
|
+
|
|
108
|
+
Everything below is off until you configure it.
|
|
109
|
+
|
|
110
|
+
| Feature | What it does |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| [Session indexing](docs/retrieval.md#sessions) | Makes past conversations searchable. Group and channel chats by default; DMs only if you add them. |
|
|
113
|
+
| [Skill Whisperer](docs/retrieval.md#skill-whisperer) | Suggests one relevant skill for a user message. Never runs it. Works without TypeSafe. |
|
|
114
|
+
| [Memory Whisperer](docs/retrieval.md#memory-whisperer) | Adds up to two relevant past excerpts before the agent replies. Needs TypeSafe and a self-hosted query-generation API. |
|
|
115
|
+
| [Safety Whisperer](docs/configuration.md#safety-whisperer) | Reminds the agent not to share sensitive topics, such as one person's salary, with requestors you haven't approved. A prompt-level guardrail, not access control; fails open if TypeSafe is unavailable. |
|
|
116
|
+
| [People dossiers](docs/peoplesql.md) | Lets the agent keep a short, evidence-backed note about each person it works with. |
|
|
117
|
+
| [People Whisperer](docs/peoplesql.md#injection-and-person-state) | Shows the agent its note about a person once per Slack thread. Local lookup, no model call. |
|
|
118
|
+
| [People Primer](docs/peoplesql.md#optional-people-dossier-primer) | Gathers evidence for a person's note and reviews proposed notes before they are saved. |
|
|
119
|
+
| [Session memory extraction](docs/extraction.md) | Pulls lasting facts from past conversations, checks each against exact quotes, and can make them searchable. Starts in shadow mode. |
|
|
120
|
+
| [Clustering and curation](docs/retrieval.md#memory-analysis) | Groups related memories so the agent can spot duplicates and maintain its knowledge. Needs a local analysis worker. |
|
|
121
|
+
| [Quality and claim review](docs/retrieval.md#review-and-diagnostics) | Flags noisy indexed content and checks whether a claim is supported by its sources. Advisory only. |
|
|
122
|
+
| [Response audit](docs/response-audit.md) | Operator-only grading of reply quality and sentiment in approved Slack conversations. |
|
|
123
|
+
| [Inside Out](docs/configuration.md#inside-out) | Operator-only scores for six emotions in human replies, with reports. Never changes memory or prompts. |
|
|
124
|
+
|
|
125
|
+
Whisperers also need conversation-hook and prompt-injection permissions on the
|
|
126
|
+
plugin entry; see [host controls](docs/configuration.md#host-controls). Each
|
|
127
|
+
feature's requirements and failure behavior are listed in
|
|
128
|
+
[feature gates and fallbacks](docs/configuration.md#feature-gates-and-fallbacks).
|
|
129
|
+
|
|
130
|
+
<details>
|
|
131
|
+
<summary>All agent tools</summary>
|
|
132
|
+
|
|
133
|
+
| Area | Tools |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| Search and read | `memory_search`, `memory_get` |
|
|
136
|
+
| Sessions | `memory_sync_sessions`, `memory_sync_status` |
|
|
137
|
+
| Diagnostics | `memory_diagnostics` |
|
|
138
|
+
| Clusters | `memory_recluster`, `memory_list_clusters`, `memory_fetch_cluster` |
|
|
139
|
+
| Maintenance | `memory_list_maintenance_tasks`, `memory_update_maintenance_task` |
|
|
140
|
+
| Review | `memory_audit_quality`, `memory_review_claim`, `memory_review_cluster` |
|
|
141
|
+
| People (with `people.enabled`) | `memory_people_inspect`, `memory_people_update`, `memory_people_sync`, `memory_people_prime` |
|
|
142
|
+
|
|
143
|
+
</details>
|
|
144
|
+
|
|
145
|
+
## Using memory well
|
|
146
|
+
|
|
147
|
+
- Search, then read. Check context, attribution and dates with `memory_get`
|
|
148
|
+
before relying on a snippet.
|
|
149
|
+
- Empty results do not prove something never happened. The corpus may not be
|
|
150
|
+
selected, the threshold may be too high, or sessions may not have synced yet.
|
|
151
|
+
- Old memory is not current permission to act.
|
|
152
|
+
- Corpora are not access controls. Anyone who can use this agent's tools can
|
|
153
|
+
search everything indexed for it, so index only what they may all see.
|
|
154
|
+
|
|
155
|
+
Two skills ship with the package:
|
|
156
|
+
[people-whisperer](skills/people-whisperer/SKILL.md) and
|
|
157
|
+
[memory-curator](skills/memory-curator/SKILL.md). Add them to any explicit agent
|
|
158
|
+
skill allowlist.
|
|
159
|
+
|
|
160
|
+
## Documentation
|
|
34
161
|
|
|
35
|
-
|
|
36
|
-
|
|
162
|
+
- [Configuration](docs/configuration.md): every setting and default,
|
|
163
|
+
credentials, host permissions, storage and upgrades.
|
|
164
|
+
- [Retrieval](docs/retrieval.md): search and read, sessions, whisperers,
|
|
165
|
+
diagnostics, clustering and curation.
|
|
166
|
+
- [People](docs/peoplesql.md): dossiers, primer, injection, and
|
|
167
|
+
pause/delete/restore.
|
|
168
|
+
- [Session extraction](docs/extraction.md): shadow mode, backfill, thresholds
|
|
169
|
+
and publishing facts to search.
|
|
170
|
+
- [Response audit](docs/response-audit.md): operator commands, cadence and
|
|
171
|
+
reports.
|
|
172
|
+
- [Memory training](docs/memory-training.md): operator-only collection of
|
|
173
|
+
training data for Memory Whisperer's query model.
|
|
37
174
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- [Session extraction](docs/extraction.md): incremental/backfill runs, configurable
|
|
74
|
-
acceptance thresholds, shadow evaluation, and publishing lasting facts to search.
|
|
75
|
-
|
|
76
|
-
The shared TypeSafe integration defaults on. Ordinary search requires a key;
|
|
77
|
-
other TypeSafe features remain opt-in. People Whisperer's injection stays local;
|
|
78
|
-
Skill Whisperer has a local fallback.
|
|
79
|
-
[Provider gates and failure behavior](docs/configuration.md#feature-gates-and-fallbacks)
|
|
80
|
-
differ by feature.
|
|
81
|
-
|
|
82
|
-
## For agents
|
|
83
|
-
|
|
84
|
-
Use `memory_search` to locate evidence, then `memory_get` to inspect context,
|
|
85
|
-
attribution and dates. Follow bounded-read continuation when present. Empty search
|
|
86
|
-
results are not proof of absence; old memory is not current authorization.
|
|
87
|
-
|
|
88
|
-
The package includes two focused procedures:
|
|
89
|
-
[people-whisperer](skills/people-whisperer/SKILL.md) and
|
|
90
|
-
[memory-curator](skills/memory-curator/SKILL.md).
|
|
91
|
-
Include them in any explicit agent skill allowlist. Read their longer references
|
|
92
|
-
only when needed; Skill Whisperer does not install or authorize skills.
|
|
93
|
-
|
|
94
|
-
## Storage and development
|
|
95
|
-
|
|
96
|
-
Each agent has a rebuildable `index.sqlite` and durable `unblock-memory.sqlite`.
|
|
97
|
-
The latter holds people/dossiers/history, curation and private response audits;
|
|
98
|
-
sharing a file does not make audit data searchable or prompt-visible.
|
|
99
|
-
Read [migration, backup and rollback](docs/configuration.md#durable-database-migration)
|
|
100
|
-
before upgrading from the former separate databases.
|
|
101
|
-
|
|
102
|
-
Source-checkout-only fleet key tooling, when present, is documented in
|
|
103
|
-
`scripts/TYPESAFE-FLEET.md` and run through `scripts/typesafe-fleet.mjs`.
|
|
104
|
-
It is not part of the npm package; keep populated key files private.
|
|
105
|
-
|
|
106
|
-
[Release instructions](https://github.com/unblocklabs-ai/unblock-memory/blob/main/docs/RELEASE.md)
|
|
107
|
-
and [product direction](https://github.com/unblocklabs-ai/unblock-memory/blob/main/docs/vision.md)
|
|
108
|
-
are source-repository references. Planning documents describe historical or future
|
|
109
|
-
designs, not the installed runtime contract.
|
|
175
|
+
[Vision](https://github.com/unblocklabs-ai/unblock-memory/blob/main/docs/vision.md)
|
|
176
|
+
describes where the project is headed. Documents in `docs/planning/` are
|
|
177
|
+
historical or future designs, not current behavior.
|
|
178
|
+
|
|
179
|
+
## Storage
|
|
180
|
+
|
|
181
|
+
Each agent's state lives under `~/.openclaw/agents/<agentId>/unblock-memory/`:
|
|
182
|
+
|
|
183
|
+
- `index.sqlite`: the search index. Safe to delete; it rebuilds.
|
|
184
|
+
- `unblock-memory.sqlite`: people, dossiers, curation and audit history. Back
|
|
185
|
+
this up.
|
|
186
|
+
|
|
187
|
+
Upgrading from a version with separate `people.sqlite`, `curation.sqlite` or
|
|
188
|
+
`response-audit.sqlite` files? Stop the Gateway and read
|
|
189
|
+
[migration, backup and rollback](docs/configuration.md#durable-database-migration)
|
|
190
|
+
first.
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
npm install
|
|
196
|
+
npm run build
|
|
197
|
+
npm test # Node and Python 3 tests
|
|
198
|
+
npm run preflight # full pre-release check
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Releases publish to npm from GitHub Releases; see
|
|
202
|
+
[RELEASE.md](https://github.com/unblocklabs-ai/unblock-memory/blob/main/docs/RELEASE.md).
|
|
203
|
+
Fleet TypeSafe key tooling for source checkouts is described in
|
|
204
|
+
[TYPESAFE-FLEET.md](https://github.com/unblocklabs-ai/unblock-memory/blob/main/scripts/TYPESAFE-FLEET.md);
|
|
205
|
+
keep populated key files private.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
[MIT](https://github.com/unblocklabs-ai/unblock-memory/blob/main/LICENSE)
|
package/dist/src/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ExtractionConfig } from "./extraction-config.js";
|
|
|
2
2
|
import { type ResponseAuditConfig } from "./response-config.js";
|
|
3
3
|
import { type PeoplePrimerConfig } from "./people-primer-config.js";
|
|
4
4
|
import { type InsideOutConfig } from "./inside-out.js";
|
|
5
|
+
import { type SafetyWhispererConfig } from "./safety-config.js";
|
|
5
6
|
export type FileCorpusConfig = {
|
|
6
7
|
name: string;
|
|
7
8
|
kind: "files";
|
|
@@ -77,6 +78,7 @@ export type UnblockMemoryConfig = {
|
|
|
77
78
|
apiKeyFile?: string;
|
|
78
79
|
};
|
|
79
80
|
};
|
|
81
|
+
safetyWhisperer: SafetyWhispererConfig;
|
|
80
82
|
};
|
|
81
83
|
export declare const DEFAULT_PEOPLE_CONFIG: UnblockMemoryConfig["people"];
|
|
82
84
|
export declare function resolveConfig(value: unknown): UnblockMemoryConfig;
|
package/dist/src/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveExtraction } from "./extraction-config.js";
|
|
|
3
3
|
import { resolveResponseAudit } from "./response-config.js";
|
|
4
4
|
import { resolvePeoplePrimer } from "./people-primer-config.js";
|
|
5
5
|
import { resolveInsideOut } from "./inside-out.js";
|
|
6
|
+
import { resolveSafetyWhisperer } from "./safety-config.js";
|
|
6
7
|
const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
|
|
7
8
|
const DEFAULT_SESSION_MAX_EXPANDED_TOKENS = 500;
|
|
8
9
|
const MAX_SESSION_MAX_EXPANDED_TOKENS = 10_000;
|
|
@@ -297,7 +298,7 @@ export function resolveConfig(value) {
|
|
|
297
298
|
throw new Error("unblock-memory config must be an object");
|
|
298
299
|
}
|
|
299
300
|
const config = value;
|
|
300
|
-
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "peoplePrimer", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit", "insideOut", "extraction"], "config");
|
|
301
|
+
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "peoplePrimer", "skillWhisperer", "memoryWhisperer", "safetyWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit", "insideOut", "extraction"], "config");
|
|
301
302
|
const corpora = resolveCorpora(config.corpora);
|
|
302
303
|
const people = resolvePeople(config.people);
|
|
303
304
|
const peoplePrimer = resolvePeoplePrimer(config.peoplePrimer, corpora, people.enabled);
|
|
@@ -377,5 +378,6 @@ export function resolveConfig(value) {
|
|
|
377
378
|
evidenceReview,
|
|
378
379
|
responseAudit: resolveResponseAudit(config.responseAudit, corpora),
|
|
379
380
|
insideOut: resolveInsideOut(config.insideOut),
|
|
380
|
-
memoryWhisperer: resolveMemoryWhisperer(config.memoryWhisperer, corpora),
|
|
381
|
+
memoryWhisperer: resolveMemoryWhisperer(config.memoryWhisperer, corpora),
|
|
382
|
+
safetyWhisperer: resolveSafetyWhisperer(config.safetyWhisperer), typesafe: resolveTypeSafe(config.typesafe) };
|
|
381
383
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { RetrievalTelemetry } from "./retrieval-telemetry.js";
|
|
2
|
-
type Whisperer = "skill" | "memory";
|
|
3
|
-
type Outcome = "missing_key" | "typesafe_disabled" | "no_candidates" | "rejected" | "cooldown" | "emitted" | "failed" | "timed_out" | "cancelled" | "unavailable" | "payload_limit" | "redundancy_unavailable" | "recall_not_needed" | "queries_generated" | "judge_candidate_failed" | "input_too_large";
|
|
2
|
+
type Whisperer = "skill" | "memory" | "safety";
|
|
3
|
+
type Outcome = "missing_key" | "typesafe_disabled" | "no_candidates" | "rejected" | "cooldown" | "emitted" | "failed" | "timed_out" | "cancelled" | "unavailable" | "payload_limit" | "redundancy_unavailable" | "recall_not_needed" | "queries_generated" | "judge_candidate_failed" | "input_too_large" | "excluded" | "no_scenarios" | "empty_request" | "no_match" | "flagged" | "missing_identity" | "partial_response";
|
|
4
4
|
/** Process-local, content-free and bounded. Agent IDs are keys, never included in snapshots. */
|
|
5
5
|
export declare class WhispererDiagnostics {
|
|
6
6
|
#private;
|
|
7
7
|
static shared(): WhispererDiagnostics;
|
|
8
8
|
record(agentId: string, whisperer: Whisperer, outcome: Outcome): void;
|
|
9
|
+
/** Scenario IDs are operator configuration, not conversation content. */
|
|
10
|
+
recordSafetyMatch(agentId: string, scenarioId: string): void;
|
|
9
11
|
measureMemory(agentId: string, observation: Parameters<RetrievalTelemetry["record"]>[1]): void;
|
|
10
12
|
measureSearch(agentId: string, observation: Parameters<RetrievalTelemetry["record"]>[1]): void;
|
|
11
13
|
snapshot(agentId: string): {
|
|
@@ -26,6 +28,13 @@ export declare class WhispererDiagnostics {
|
|
|
26
28
|
queries_generated?: number | undefined;
|
|
27
29
|
judge_candidate_failed?: number | undefined;
|
|
28
30
|
input_too_large?: number | undefined;
|
|
31
|
+
excluded?: number | undefined;
|
|
32
|
+
no_scenarios?: number | undefined;
|
|
33
|
+
empty_request?: number | undefined;
|
|
34
|
+
no_match?: number | undefined;
|
|
35
|
+
flagged?: number | undefined;
|
|
36
|
+
missing_identity?: number | undefined;
|
|
37
|
+
partial_response?: number | undefined;
|
|
29
38
|
};
|
|
30
39
|
memory: {
|
|
31
40
|
cancelled?: number | undefined;
|
|
@@ -44,6 +53,41 @@ export declare class WhispererDiagnostics {
|
|
|
44
53
|
queries_generated?: number | undefined;
|
|
45
54
|
judge_candidate_failed?: number | undefined;
|
|
46
55
|
input_too_large?: number | undefined;
|
|
56
|
+
excluded?: number | undefined;
|
|
57
|
+
no_scenarios?: number | undefined;
|
|
58
|
+
empty_request?: number | undefined;
|
|
59
|
+
no_match?: number | undefined;
|
|
60
|
+
flagged?: number | undefined;
|
|
61
|
+
missing_identity?: number | undefined;
|
|
62
|
+
partial_response?: number | undefined;
|
|
63
|
+
};
|
|
64
|
+
safety: {
|
|
65
|
+
matchedScenarios: {
|
|
66
|
+
[k: string]: number;
|
|
67
|
+
};
|
|
68
|
+
cancelled?: number | undefined;
|
|
69
|
+
unavailable?: number | undefined;
|
|
70
|
+
rejected?: number | undefined;
|
|
71
|
+
failed?: number | undefined;
|
|
72
|
+
timed_out?: number | undefined;
|
|
73
|
+
missing_key?: number | undefined;
|
|
74
|
+
typesafe_disabled?: number | undefined;
|
|
75
|
+
no_candidates?: number | undefined;
|
|
76
|
+
cooldown?: number | undefined;
|
|
77
|
+
emitted?: number | undefined;
|
|
78
|
+
payload_limit?: number | undefined;
|
|
79
|
+
redundancy_unavailable?: number | undefined;
|
|
80
|
+
recall_not_needed?: number | undefined;
|
|
81
|
+
queries_generated?: number | undefined;
|
|
82
|
+
judge_candidate_failed?: number | undefined;
|
|
83
|
+
input_too_large?: number | undefined;
|
|
84
|
+
excluded?: number | undefined;
|
|
85
|
+
no_scenarios?: number | undefined;
|
|
86
|
+
empty_request?: number | undefined;
|
|
87
|
+
no_match?: number | undefined;
|
|
88
|
+
flagged?: number | undefined;
|
|
89
|
+
missing_identity?: number | undefined;
|
|
90
|
+
partial_response?: number | undefined;
|
|
47
91
|
};
|
|
48
92
|
telemetry: {
|
|
49
93
|
scope: string;
|
package/dist/src/diagnostics.js
CHANGED
|
@@ -3,7 +3,8 @@ import { RetrievalTelemetry } from "./retrieval-telemetry.js";
|
|
|
3
3
|
export class WhispererDiagnostics {
|
|
4
4
|
static shared() {
|
|
5
5
|
const scope = globalThis;
|
|
6
|
-
|
|
6
|
+
// Bump the version whenever the shape changes so an older loaded copy is never reused.
|
|
7
|
+
const symbol = Symbol.for("unblock-memory.whisperer-diagnostics.v2");
|
|
7
8
|
return (scope[symbol] ??= new WhispererDiagnostics());
|
|
8
9
|
}
|
|
9
10
|
#agents = new Map();
|
|
@@ -12,7 +13,7 @@ export class WhispererDiagnostics {
|
|
|
12
13
|
if (!entry) {
|
|
13
14
|
if (this.#agents.size >= 100)
|
|
14
15
|
this.#agents.delete(this.#agents.keys().next().value);
|
|
15
|
-
entry = { counts: { skill: {}, memory: {} }, telemetry: new RetrievalTelemetry() };
|
|
16
|
+
entry = { counts: { skill: {}, memory: {}, safety: {} }, safetyScenarios: new Map(), telemetry: new RetrievalTelemetry() };
|
|
16
17
|
this.#agents.set(agentId, entry);
|
|
17
18
|
}
|
|
18
19
|
return entry;
|
|
@@ -21,6 +22,13 @@ export class WhispererDiagnostics {
|
|
|
21
22
|
const entry = this.#entry(agentId).counts;
|
|
22
23
|
entry[whisperer][outcome] = Math.min(Number.MAX_SAFE_INTEGER, (entry[whisperer][outcome] ?? 0) + 1);
|
|
23
24
|
}
|
|
25
|
+
/** Scenario IDs are operator configuration, not conversation content. */
|
|
26
|
+
recordSafetyMatch(agentId, scenarioId) {
|
|
27
|
+
const matches = this.#entry(agentId).safetyScenarios;
|
|
28
|
+
if (!matches.has(scenarioId) && matches.size >= 20)
|
|
29
|
+
return;
|
|
30
|
+
matches.set(scenarioId, Math.min(Number.MAX_SAFE_INTEGER, (matches.get(scenarioId) ?? 0) + 1));
|
|
31
|
+
}
|
|
24
32
|
measureMemory(agentId, observation) {
|
|
25
33
|
this.#entry(agentId).telemetry.record("memoryWhisperer", observation);
|
|
26
34
|
}
|
|
@@ -30,6 +38,7 @@ export class WhispererDiagnostics {
|
|
|
30
38
|
snapshot(agentId) {
|
|
31
39
|
const entry = this.#agents.get(agentId);
|
|
32
40
|
return { skill: { ...entry?.counts.skill }, memory: { ...entry?.counts.memory },
|
|
41
|
+
safety: { ...entry?.counts.safety, matchedScenarios: Object.fromEntries(entry?.safetyScenarios ?? []) },
|
|
33
42
|
telemetry: entry?.telemetry.snapshot() ?? new RetrievalTelemetry().snapshot(),
|
|
34
43
|
scope: "process lifetime; up to 100 agents" };
|
|
35
44
|
}
|
package/dist/src/inside-out.js
CHANGED
|
@@ -5,7 +5,7 @@ import { stripReasoningTagsFromText } from "openclaw/plugin-sdk/text-chunking";
|
|
|
5
5
|
import { openMemoryDatabase } from "./memory-database.js";
|
|
6
6
|
import { messageText } from "./whisperer-context.js";
|
|
7
7
|
import { conversationUserText } from "./response-text.js";
|
|
8
|
-
import { requestTypeSafe, resolveTypeSafeApiKey, TYPESAFE_MODEL } from "./typesafe-client.js";
|
|
8
|
+
import { estimateJevTokens, requestTypeSafe, resolveTypeSafeApiKey, TYPESAFE_MODEL } from "./typesafe-client.js";
|
|
9
9
|
import { readInsideOutSources } from "./inside-out-sources.js";
|
|
10
10
|
const INSIDE_OUT_EMOTIONS = ["joy", "sadness", "fear", "anger", "disgust", "surprise"];
|
|
11
11
|
const RUBRIC = "emotion-presence-v2";
|
|
@@ -52,13 +52,6 @@ export function resolveInsideOut(value) {
|
|
|
52
52
|
maxContextTokens: integer("maxContextTokens", 256, 30000) };
|
|
53
53
|
}
|
|
54
54
|
const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
55
|
-
// An estimate, not Jev's tokenizer. Non-ASCII is budgeted separately; whole messages stay intact.
|
|
56
|
-
function tokens(value) {
|
|
57
|
-
let n = 0;
|
|
58
|
-
for (const c of JSON.stringify(value))
|
|
59
|
-
n += c.codePointAt(0) < 128 ? 1 / 3 : 2;
|
|
60
|
-
return Math.ceil(n);
|
|
61
|
-
}
|
|
62
55
|
function humanIdentity(source, message) {
|
|
63
56
|
const meta = message.__openclaw, identity = meta?.senderIdentity;
|
|
64
57
|
const channel = meta?.transport?.channel ?? identity?.pluginId ?? message.sourceChannel ?? source.channel;
|
|
@@ -113,12 +106,12 @@ function* interactions(source, budget, cached, after) {
|
|
|
113
106
|
const id = assistant >= 0 ? hash([source.sessionId, message.id]) : "";
|
|
114
107
|
if (assistant >= 0 && !cached(id)) {
|
|
115
108
|
const selected = [{ message: history[assistant], position: assistant }];
|
|
116
|
-
let size =
|
|
109
|
+
let size = estimateJevTokens({ history: selected.map(s => s.message), target: message });
|
|
117
110
|
const oversized = size > budget;
|
|
118
111
|
for (let i = history.length - 1; !oversized && i >= 0; i--) {
|
|
119
112
|
if (i === assistant)
|
|
120
113
|
continue;
|
|
121
|
-
const cost =
|
|
114
|
+
const cost = estimateJevTokens(history[i]) + 2;
|
|
122
115
|
if (size + cost > budget)
|
|
123
116
|
break;
|
|
124
117
|
selected.push({ message: history[i], position: i });
|
package/dist/src/plugin.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerPeopleTools } from "./people-tools.js";
|
|
|
8
8
|
import { registerPeoplePrimerTool } from "./people-primer-tool.js";
|
|
9
9
|
import { QmdMemoryRuntime } from "./runtime.js";
|
|
10
10
|
import { registerSkillWhisperer } from "./skill-whisperer.js";
|
|
11
|
+
import { registerSafetyWhisperer } from "./safety-whisperer.js";
|
|
11
12
|
import { registerMemoryWhisperer } from "./memory-whisperer.js";
|
|
12
13
|
import { registerWhispererPrompt } from "./whisperer-prompt.js";
|
|
13
14
|
import { getContext } from "./tool-context.js";
|
|
@@ -497,6 +498,7 @@ export function registerUnblockMemory(api) {
|
|
|
497
498
|
memory: registerMemoryWhisperer(api, runtime, config.memoryWhisperer, config.typesafe, diagnostics),
|
|
498
499
|
skill: registerSkillWhisperer(api, runtime, config.skillWhisperer, config.typesafe, diagnostics),
|
|
499
500
|
people: peopleWhisperer,
|
|
501
|
+
safety: registerSafetyWhisperer(api, config, diagnostics),
|
|
500
502
|
});
|
|
501
503
|
api.registerTool((ctx) => createSearchTool(runtime, ctx, config.typesafe, diagnostics), { names: ["memory_search"] });
|
|
502
504
|
api.registerTool((ctx) => createGetTool(runtime, ctx), { names: ["memory_get"] });
|
|
@@ -12,7 +12,7 @@ declare const qualitySchema: Type.TObject<{
|
|
|
12
12
|
answers: Type.TObject<{
|
|
13
13
|
taskType: Type.TObject<{
|
|
14
14
|
type: Type.TLiteral<"choice">;
|
|
15
|
-
choice: Type.TEnum<("action" | "question" | "
|
|
15
|
+
choice: Type.TEnum<("action" | "question" | "other" | "artifact" | "discussion")[]>;
|
|
16
16
|
confidence: Type.TNumber;
|
|
17
17
|
probabilities: Type.TObject<{
|
|
18
18
|
[k: string]: Type.TNumber;
|
|
@@ -51,7 +51,7 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
51
51
|
taskType: {
|
|
52
52
|
type: "choice";
|
|
53
53
|
confidence: number;
|
|
54
|
-
choice: "action" | "question" | "
|
|
54
|
+
choice: "action" | "question" | "other" | "artifact" | "discussion";
|
|
55
55
|
probabilities: {
|
|
56
56
|
[x: string]: number;
|
|
57
57
|
[x: number]: number;
|
|
@@ -3,6 +3,13 @@ export declare function conversationUserText(raw: string, sender?: unknown): {
|
|
|
3
3
|
text: string;
|
|
4
4
|
contextLimited: boolean;
|
|
5
5
|
} | undefined;
|
|
6
|
+
/** Like conversationUserText, plus the untrusted inbound context (thread starter, reply target,
|
|
7
|
+
* chat history) that a recognized envelope carries before the current message. */
|
|
8
|
+
export declare function conversationEnvelope(raw: string, sender?: unknown): {
|
|
9
|
+
text: string;
|
|
10
|
+
contextLimited: boolean;
|
|
11
|
+
context?: string;
|
|
12
|
+
} | undefined;
|
|
6
13
|
/** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
|
|
7
14
|
* never flattened into a human's request or used to approve embedded speakers. */
|
|
8
15
|
export declare function responseUserText(input: string, senderId: string): {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Infer legacy transport sender metadata only to unwrap visible conversation text. */
|
|
2
2
|
export function conversationUserText(raw, sender) {
|
|
3
|
+
const parsed = conversationEnvelope(raw, sender);
|
|
4
|
+
return parsed && { text: parsed.text, contextLimited: parsed.contextLimited };
|
|
5
|
+
}
|
|
6
|
+
/** Like conversationUserText, plus the untrusted inbound context (thread starter, reply target,
|
|
7
|
+
* chat history) that a recognized envelope carries before the current message. */
|
|
8
|
+
export function conversationEnvelope(raw, sender) {
|
|
3
9
|
let senderId = typeof sender === "string" ? sender : undefined;
|
|
4
10
|
if (senderId === undefined) {
|
|
5
11
|
const header = /^Conversation info: ⟦openclaw:ctx⟧\r?\n```json\r?\n([^]*?)\r?\n```\r?\n/.exec(raw.trim());
|
|
@@ -18,13 +24,18 @@ export function conversationUserText(raw, sender) {
|
|
|
18
24
|
}
|
|
19
25
|
senderId ??= /^From: [^\r\n]+ \(([^()\r\n]+)\)\r?\n/.exec(raw.trim())?.[1];
|
|
20
26
|
}
|
|
21
|
-
return
|
|
27
|
+
return userEnvelope(raw, senderId ?? "");
|
|
22
28
|
}
|
|
23
29
|
/** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
|
|
24
30
|
* never flattened into a human's request or used to approve embedded speakers. */
|
|
25
31
|
export function responseUserText(input, senderId) {
|
|
32
|
+
const parsed = userEnvelope(input, senderId);
|
|
33
|
+
return parsed && { text: parsed.text, contextLimited: parsed.contextLimited };
|
|
34
|
+
}
|
|
35
|
+
function userEnvelope(input, senderId) {
|
|
26
36
|
let text = input.trim();
|
|
27
37
|
let contextLimited = false;
|
|
38
|
+
let context;
|
|
28
39
|
const header = /^Conversation info: ⟦openclaw:ctx⟧\r?\n```json\r?\n([^]*?)\r?\n```\r?\n/.exec(text);
|
|
29
40
|
if (header) {
|
|
30
41
|
let meta;
|
|
@@ -41,6 +52,7 @@ export function responseUserText(input, senderId) {
|
|
|
41
52
|
if (markers.length !== 1 || markers[0][1] !== meta.sender.name)
|
|
42
53
|
return undefined;
|
|
43
54
|
const marker = markers[0];
|
|
55
|
+
context = rest.slice(0, marker.index).trim() || undefined;
|
|
44
56
|
contextLimited = meta.history_truncated === true || rest.slice(0, marker.index).includes("Chat history since last reply:");
|
|
45
57
|
text = rest.slice(marker.index + marker[0].length).trim();
|
|
46
58
|
// OpenClaw appends channel metadata after the current Slack message. Only
|
|
@@ -68,5 +80,5 @@ export function responseUserText(input, senderId) {
|
|
|
68
80
|
// Unrecognized/nested envelopes cannot safely supply the current human text.
|
|
69
81
|
if (!text || text.includes("⟦openclaw:ctx⟧") || /^Chat history since last reply:/m.test(text))
|
|
70
82
|
return undefined;
|
|
71
|
-
return { text, contextLimited };
|
|
83
|
+
return { text, contextLimited, ...(context ? { context } : {}) };
|
|
72
84
|
}
|
package/dist/src/review-tools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Value } from "typebox/value";
|
|
|
3
3
|
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
4
|
import { getContext } from "./tool-context.js";
|
|
5
5
|
import { resolveTypeSafeApiKey } from "./typesafe-client.js";
|
|
6
|
+
import { safetyStatus } from "./safety-whisperer.js";
|
|
6
7
|
import { abortable } from "./abortable.js";
|
|
7
8
|
const claimParameters = Type.Object({
|
|
8
9
|
claim: Type.String({ pattern: "\\S", maxLength: 2000 }),
|
|
@@ -47,7 +48,9 @@ export function registerReviewTools(api, runtime, config, diagnostics) {
|
|
|
47
48
|
}
|
|
48
49
|
const result = { status: "ok", credential,
|
|
49
50
|
enabled: { skill: config.skillWhisperer.enabled, memory: config.memoryWhisperer.enabled && config.typesafe.enabled,
|
|
50
|
-
complementaryHints: config.memoryWhisperer.complementaryHints, qualityAudit: config.qualityAudit.enabled, evidenceReview: config.evidenceReview.enabled
|
|
51
|
+
complementaryHints: config.memoryWhisperer.complementaryHints, qualityAudit: config.qualityAudit.enabled, evidenceReview: config.evidenceReview.enabled,
|
|
52
|
+
safety: config.safetyWhisperer.enabled },
|
|
53
|
+
safety: safetyStatus(api.config, config),
|
|
51
54
|
whisperers: diagnostics.snapshot(active.agentId) };
|
|
52
55
|
try {
|
|
53
56
|
const { manager } = await runtime.getMemorySearchManager(active);
|