@unblocklabs/unblock-memory 0.8.3 → 0.10.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 CHANGED
@@ -1,109 +1,209 @@
1
1
  # Unblock Memory
2
2
 
3
- Workspace-native memory for OpenClaw, powered internally by `@unblocklabs/qmd`.
4
- It keeps one warm QMD store per agent and exposes `memory_search` and `memory_get`.
5
- Recall combines BM25 and vector candidates, then independently ranks complete
6
- passages with TypeSafe. The agent supplies both queries; no query generation or
7
- "do we need memory?" gate runs for explicit search. A TypeSafe key is required.
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
- ## Start here
63
+ ## How it works
10
64
 
11
- Install once per OpenClaw host, not once per agent:
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
- ```sh
14
- openclaw plugins install npm:@unblocklabs/unblock-memory
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
- "plugins": {
22
- "slots": { "memory": "unblock-memory" },
23
- "entries": {
24
- "unblock-memory": { "enabled": true, "config": {} }
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 default `memory` corpus contains `MEMORY.md`, `USER.md` and
31
- `memory/**/*.md`, relative to each agent's workspace. The first use builds its
32
- index; Markdown changes refresh it in the background. QMD is installed as a
33
- pinned dependency, not a separate service. A global QMD CLI is not required.
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. Turns on when you add a `skills` corpus; 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, compared with each person's own weekly baseline, 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
- Optional installation from source:
36
- `openclaw plugins install git:github.com/unblocklabs-ai/unblock-memory`.
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
- ## What does what?
39
-
40
- | Capability | What it does | Default / prerequisite |
41
- | --- | --- | --- |
42
- | [Search and reads](docs/retrieval.md#search-and-read) | BM25 + vector recall with TypeSafe ranking, then exact indexed reads | Search requires TypeSafe; reads stay local; files only unless more corpora are configured |
43
- | [Session indexing](docs/retrieval.md#sessions) | Makes past user/assistant exchanges searchable | Opt-in corpus; channel/group by default, DMs explicitly included |
44
- | [Skill Whisperer](docs/retrieval.md#skill-whisperer) | Suggests one relevant skill; never invokes it | Off; explicit skills corpus; TypeSafe optional |
45
- | [Memory Whisperer](docs/retrieval.md#memory-whisperer) | Injects up to two useful historical excerpts; query generation via resident API | Off; approved corpora + TypeSafe + query API credential |
46
- | [People dossiers](docs/peoplesql.md) | Stores agent-authored, evidence-backed recognition snippets | Off; `people.enabled` |
47
- | [People Primer](docs/peoplesql.md#optional-people-dossier-primer) | Selects evidence and reviews proposed blurbs; never generates/saves dossiers itself | Off; people + approved corpora + TypeSafe |
48
- | [People Whisperer](docs/peoplesql.md#injection-and-person-state) | Injects a saved blurb for an exact Slack identity | Off; separate global/per-person gates; no model call |
49
- | [Analysis and curation](docs/retrieval.md#memory-analysis) | Clusters existing vectors; agent maintains useful knowledge | Optional local analysis worker; no plugin-owned curation schedule |
50
- | [Quality and evidence review](docs/retrieval.md#review-and-diagnostics) | Advisory ingestion/claim checks and maintenance leads | Off; feature-specific corpora + TypeSafe |
51
- | [Response quality/sentiment](docs/response-audit.md) | Operator-only evaluation of approved Slack exchanges | Off; approved humans + TypeSafe; no memory/dossier updates |
52
- | [Inside Out](docs/configuration.md#inside-out) | Six emotion probabilities per human reply, with backfill and operator reports | Off; TypeSafe; channel agnostic; no memory or prompt changes |
53
- | [Session memory extraction](docs/extraction.md) | Luna proposes lasting facts; Jev verifies them; optional QMD search | Off; approved session types + TypeSafe + host LLM permission; shadow-only by default |
54
- | [Compaction memory flush](docs/configuration.md#compaction-memory-writes) | Supplies the host an append-only daily-memory write plan | Offered unless the host's memory-flush setting is false; separate from whisperers |
55
-
56
- **One agent search pipeline:** `memory_search` and Memory Whisperer share
57
- candidate discovery, passage rendering, deduplication and TypeSafe ranking.
58
- Whisperer additionally generates queries and applies its automatic recall/delivery policy.
59
-
60
- ## Configuration and operating guides
61
-
62
- - [Configuration](docs/configuration.md): every setting/default, feature dependencies,
63
- TypeSafe credentials/data scopes, host permissions and compaction.
64
- - [Retrieval](docs/retrieval.md): search/read workflow, sessions, whisperers,
65
- indexing, diagnostics, clustering and curation.
66
- - [People](docs/peoplesql.md): dossier workflow, primer/save review, injection and
67
- pause/delete/restore semantics.
68
- - [Response audit](docs/response-audit.md): operator commands, cadence, sentiment,
69
- evidence-linked reports and their limits.
70
- - [Memory training](docs/memory-training.md): resumable, operator-only conversation
71
- collection, TypeSafe recall gating, independent lex/vector Luna candidates, one
72
- score-feedback revision, and the runtime's shared passage grader for LFM targets.
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)
@@ -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;
@@ -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;
@@ -77,7 +78,7 @@ function resolveTypeSafe(value) {
77
78
  };
78
79
  }
79
80
  const DEFAULT_SKILL_WHISPERER = {
80
- enabled: false,
81
+ enabled: true,
81
82
  historyMessages: 5,
82
83
  minScore: 0.5,
83
84
  cooldownTurns: 10,
@@ -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);
@@ -344,7 +345,7 @@ export function resolveConfig(value) {
344
345
  }
345
346
  const value = config.skillWhisperer;
346
347
  assertOnlyKeys(value, ["enabled", "historyMessages", "minScore", "cooldownTurns"], "skillWhisperer");
347
- const enabled = value.enabled ?? false;
348
+ const enabled = value.enabled ?? DEFAULT_SKILL_WHISPERER.enabled;
348
349
  const historyMessages = value.historyMessages ?? 5;
349
350
  const minScore = value.minScore ?? 0.5;
350
351
  const cooldownTurns = value.cooldownTurns ?? 10;
@@ -368,8 +369,11 @@ export function resolveConfig(value) {
368
369
  }
369
370
  skillWhisperer = { enabled, historyMessages, minScore, cooldownTurns };
370
371
  }
372
+ // Default-on means "on once skills exist": without a skills corpus it could never
373
+ // hint, so it registers no prompt hook and reports inactive. Host-applied schema
374
+ // defaults are indistinguishable from an explicit true, so this never throws.
371
375
  if (skillWhisperer.enabled && !corpora.some((corpus) => corpus.kind === "skills")) {
372
- throw new Error('unblock-memory enabled skillWhisperer requires a corpus named "skills" with kind "skills"');
376
+ skillWhisperer = { ...skillWhisperer, enabled: false };
373
377
  }
374
378
  return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, peoplePrimer, skillWhisperer,
375
379
  extraction: resolveExtraction(config.extraction, corpora),
@@ -377,5 +381,6 @@ export function resolveConfig(value) {
377
381
  evidenceReview,
378
382
  responseAudit: resolveResponseAudit(config.responseAudit, corpora),
379
383
  insideOut: resolveInsideOut(config.insideOut),
380
- memoryWhisperer: resolveMemoryWhisperer(config.memoryWhisperer, corpora), typesafe: resolveTypeSafe(config.typesafe) };
384
+ memoryWhisperer: resolveMemoryWhisperer(config.memoryWhisperer, corpora),
385
+ safetyWhisperer: resolveSafetyWhisperer(config.safetyWhisperer), typesafe: resolveTypeSafe(config.typesafe) };
381
386
  }
@@ -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;
@@ -51,8 +95,8 @@ export declare class WhispererDiagnostics {
51
95
  [k: string]: {
52
96
  calls: number;
53
97
  outcomes: {
54
- ok?: number | undefined;
55
98
  cancelled?: number | undefined;
99
+ ok?: number | undefined;
56
100
  partial?: number | undefined;
57
101
  skipped?: number | undefined;
58
102
  failed?: number | undefined;
@@ -60,7 +104,7 @@ export declare class WhispererDiagnostics {
60
104
  timed_out?: number | undefined;
61
105
  };
62
106
  slowestRecent: {
63
- outcome: "ok" | "cancelled" | "partial" | "skipped" | "failed" | "empty" | "timed_out";
107
+ outcome: "cancelled" | "ok" | "partial" | "skipped" | "failed" | "empty" | "timed_out";
64
108
  setupMs?: number | undefined;
65
109
  queueWaitMs?: number | undefined;
66
110
  metadataMs?: number | undefined;
@@ -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
- const symbol = Symbol.for("unblock-memory.whisperer-diagnostics.v1");
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
  }
@@ -0,0 +1,120 @@
1
+ import type { DatabaseSync, SQLOutputValue } from "node:sqlite";
2
+ export declare const INSIDE_OUT_EMOTIONS: readonly ["joy", "sadness", "fear", "anger", "disgust", "surprise"];
3
+ /** Erased by an operator purge: never retried, linked, aggregated or compared. */
4
+ export declare const PURGED_GRADE = "Purged by operator";
5
+ type Emotion = typeof INSIDE_OUT_EMOTIONS[number];
6
+ type Row = Record<string, SQLOutputValue>;
7
+ type Statistics = {
8
+ count: number;
9
+ mean: number | null;
10
+ min: number | null;
11
+ p10: number | null;
12
+ median: number | null;
13
+ p90: number | null;
14
+ max: number | null;
15
+ distribution: [number, number][];
16
+ };
17
+ type Summary = Record<Emotion, Statistics>;
18
+ type Deviation = {
19
+ delta: number;
20
+ percentile: number;
21
+ unusualness: number;
22
+ };
23
+ type Comparison = {
24
+ status: "ok" | "insufficient_history";
25
+ count: number;
26
+ emotions: Record<Emotion, Deviation> | null;
27
+ };
28
+ type PersonalDeviation = {
29
+ version: string;
30
+ status: "ok" | "insufficient_history" | "unlinked_person" | "missing_time" | "future_time" | "grade_failed";
31
+ personId: string | null;
32
+ baselineId: string | null;
33
+ baselineEnd: number | null;
34
+ assessedAt: number;
35
+ minInteractions: number;
36
+ cumulative: Comparison | null;
37
+ weekly: Comparison | null;
38
+ };
39
+ /**
40
+ * One current, content-addressed snapshot per person, rubric, model and completed week.
41
+ * Reads finish before each batch commits, so the shared store is never held for a whole pass.
42
+ * No inference and no changes to raw grades.
43
+ */
44
+ export declare function refreshEmotionBaselines(db: DatabaseSync, options: {
45
+ minInteractions: number;
46
+ now?: number;
47
+ recompare?: boolean;
48
+ }): {
49
+ snapshots: number;
50
+ comparisons: number;
51
+ };
52
+ /** Prepared once per export, not per row. */
53
+ export declare function deviationReader(db: DatabaseSync): (row: Row, history?: boolean) => PersonalDeviation | {
54
+ status: string;
55
+ } | {
56
+ history: {
57
+ currentGrade: boolean;
58
+ version: string;
59
+ status: "ok" | "insufficient_history" | "unlinked_person" | "missing_time" | "future_time" | "grade_failed";
60
+ personId: string | null;
61
+ baselineId: string | null;
62
+ baselineEnd: number | null;
63
+ assessedAt: number;
64
+ minInteractions: number;
65
+ cumulative: Comparison | null;
66
+ weekly: Comparison | null;
67
+ }[];
68
+ version: string;
69
+ status: "ok" | "insufficient_history" | "unlinked_person" | "missing_time" | "future_time" | "grade_failed";
70
+ personId: string | null;
71
+ baselineId: string | null;
72
+ baselineEnd: number | null;
73
+ assessedAt: number;
74
+ minInteractions: number;
75
+ cumulative: Comparison | null;
76
+ weekly: Comparison | null;
77
+ } | {
78
+ history: {
79
+ currentGrade: boolean;
80
+ version: string;
81
+ status: "ok" | "insufficient_history" | "unlinked_person" | "missing_time" | "future_time" | "grade_failed";
82
+ personId: string | null;
83
+ baselineId: string | null;
84
+ baselineEnd: number | null;
85
+ assessedAt: number;
86
+ minInteractions: number;
87
+ cumulative: Comparison | null;
88
+ weekly: Comparison | null;
89
+ }[];
90
+ status: "not_compared";
91
+ };
92
+ /** Weekly snapshots, newest first; cumulative statistics are prefix sums of each population's stored weeks. */
93
+ export declare function emotionBaselineHistory(db: DatabaseSync, options?: {
94
+ personId?: string;
95
+ since?: number;
96
+ limit?: number;
97
+ }): {
98
+ id: string;
99
+ person_id: string;
100
+ rubric: string;
101
+ model: string;
102
+ version: string;
103
+ week_start: number;
104
+ week_end: number;
105
+ computed_at: number;
106
+ weekly: Summary;
107
+ cumulative: Summary;
108
+ }[];
109
+ /**
110
+ * Delete one person's baselines and assessment history. With `grades`, also erase their six
111
+ * probabilities and person link, keeping a tombstone so the reply is never sent to Jev again.
112
+ */
113
+ export declare function purgeEmotionProfile(db: DatabaseSync, personId: string, options?: {
114
+ grades?: boolean;
115
+ }): {
116
+ baselines: number;
117
+ assessments: number;
118
+ grades: number;
119
+ };
120
+ export {};