@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.
@@ -16,6 +16,7 @@ settings; a rotated credential file is reread without a restart.
16
16
  | Skill Whisperer | `skillWhisperer.enabled`, skills corpus, host hooks | Best local vector candidate meeting `minScore` | Exclude failed candidates; successful siblings may qualify; no vector fallback |
17
17
  | Memory Whisperer | `memoryWhisperer.enabled`, explicit approved corpora, host hooks | No hints | Exclude failed candidates; recall/deadline failures still suppress the turn |
18
18
  | Complementary hints | Enabled Memory Whisperer + `complementaryHints` | No additional judgment; base hints also require a key | Keep baseline hints unless the total deadline expires |
19
+ | Safety Whisperer | `safetyWhisperer.enabled`, at least one scenario, host hooks (conversation access and prompt injection) | Could not check: the turn proceeds without safety prompts (fails open) | Fails open; recorded as could-not-check, never as no match |
19
20
  | People store/tools | `people.enabled` | Available; automatic save review needs primer/key or verified manual alternative | Storage/inspection still available |
20
21
  | People Whisperer | People + `people.whisperer.enabled`, host hooks, eligible person/dossier, no prior thread receipt | Unchanged local lookup | Unchanged local lookup |
21
22
  | People Primer / automatic dossier save review | People + `peoplePrimer.enabled`, approved corpora | No judgment; save requires source-specific manual verification | No automatic save; verify/retry instead |
@@ -25,8 +26,11 @@ settings; a rotated credential file is reread without a restart.
25
26
  | Inside Out | `insideOut.enabled`; opts in all retained channels | No inference | Failed reviews are recorded and retried; successes stay cached |
26
27
  | Clustering | Configured local `analysis.executable` | Unchanged | Unchanged; worker failures do not disable ordinary search |
27
28
 
28
- Whisperers, people storage, the primer and audits default off. `typesafe.enabled` defaults true but does not
29
- enable any feature; `sentimentEnabled` defaults true only **within an enabled
29
+ Skill Whisperer turns on by default once a `skills` corpus is configured; without
30
+ one it registers no hook and reports inactive.
31
+ Memory/People Whisperers, people storage, the primer and audits default off.
32
+ `typesafe.enabled` defaults true but does not enable those opt-in features;
33
+ `sentimentEnabled` defaults true only **within an enabled
30
34
  response audit**. `peoplePrimer` controls automatic dossier-save review;
31
35
  `evidenceReview` is a different advisory tool. Disabling people injection does
32
36
  not disable people tools/storage or erase dossiers.
@@ -215,8 +219,8 @@ contract; these tables explain their effects.
215
219
  | `typesafe.enabled` | `true` | Shared plugin provider gate; no feature is opted in merely by adding a key |
216
220
  | `typesafe.apiKey` | Unset | Explicit inline key; mutually exclusive with key file; prefer file |
217
221
  | `typesafe.apiKeyFile` | Unset | Absolute raw-key or dotenv file, reread at credential resolution; explicit missing file never falls back to a different key |
218
- | `typesafe.timeoutMs` | `1500` | 1–10,000 per request for Skill/Memory Whisperer, quality/claim/cluster review and response audit; **primer and dossier save use `peoplePrimer.timeoutMs` instead** |
219
- | `skillWhisperer.enabled` | `false` | Requires explicit skills corpus and appropriate host hook access; fixed 3-second total deadline |
222
+ | `typesafe.timeoutMs` | `1500` | 1–10,000 per request for Skill/Memory Whisperer, quality/claim/cluster review and response audit; **primer and dossier save use `peoplePrimer.timeoutMs` and Safety Whisperer uses `safetyWhisperer.timeoutMs` instead** |
223
+ | `skillWhisperer.enabled` | `true` | Active only with a `skills` corpus (otherwise resolved to `false`, with no hook or host warning) and appropriate host hook access; fixed 3-second total deadline; `false` opts out |
220
224
  | `skillWhisperer.historyMessages` | `5` | Nonnegative integer; prior visible messages used for routing |
221
225
  | `skillWhisperer.minScore` | `0.5` | 0–1, **local vector fallback only**, ignored for TypeSafe shortlist admission |
222
226
  | `skillWhisperer.cooldownTurns` | `10` | Nonnegative user-turn count; no fallback to weaker cooling-down alternatives |
@@ -230,6 +234,18 @@ contract; these tables explain their effects.
230
234
  | `memoryWhisperer.cooldownTurns` | `10` | 0–1,000; recently injected evidence |
231
235
  | `memoryWhisperer.timeoutMs` | `3000` | 1–10,000 total whisper deadline, not just the provider timeout |
232
236
  | `memoryWhisperer.complementaryHints` | `false` | Optional extra redundancy judgment; does not expand retrieval or enable the feature |
237
+ | `safetyWhisperer.enabled` | `false` | Requires at least one scenario, a TypeSafe key and host hooks; see [Safety Whisperer](#safety-whisperer) |
238
+ | `safetyWhisperer.historyMessages` | `6` | 0–20 earlier visible messages, whole messages only, for resolving follow-ups |
239
+ | `safetyWhisperer.timeoutMs` | `3000` | 1–10,000 total deadline (credential and request); the host hook timeout must exceed the slowest enabled whisperer |
240
+ | `safetyWhisperer.excludedChannels` | `[]` | Up to 500 `{ provider, accountId, channelId, name? }`, matched case-insensitively; skips every scenario there |
241
+ | `safetyWhisperer.scenarios` | `[]` | Up to 20; unique `id` of letters, digits, `.`, `_` or `-` |
242
+ | `safetyWhisperer.scenarios[].instructions` | Required | Up to 2,000 characters: what the scenario covers, with exclusions; sent to TypeSafe |
243
+ | `safetyWhisperer.scenarios[].flaggedPrompt` | Required | Up to 2,000 characters, appended for matching unapproved requests; never sent to TypeSafe |
244
+ | `safetyWhisperer.scenarios[].minProbability` | `0.4` | 0–1 minimum yes-probability that the current request is covered |
245
+ | `safetyWhisperer.scenarios[].approvedRequestors` | `[]` | Up to 500 `{ provider, accountId, senderId, name? }`; `senderId` must match exactly; skip this scenario for them |
246
+ | `safetyWhisperer.scenarios[].excludedChannels` | `[]` | Up to 500 channels where only this scenario is skipped |
247
+ | `safetyWhisperer.scenarios[].agentIds` | Unset (every agent) | Non-empty list limits the scenario to these agents when one config serves several; normalized like OpenClaw agent IDs (lowercase) |
248
+ | `safetyWhisperer.scenarios[].enabled` | `true` | Disabled scenarios are never sent to TypeSafe |
233
249
 
234
250
  No configured plugin credential means fallback to **`TYPESAFE_API_KEY` only** in
235
251
  the Gateway environment. Unlike QMD's resolver, the plugin does not read a
@@ -364,6 +380,7 @@ boundary, not multi-tenant authorization. Approve sources for the agent's audien
364
380
  | Cluster review | Up to 6 eligible complete sampled chunks, each at most 2,000 characters; conclusions only concern the sample |
365
381
  | Response audit | Approved visible request/answer/context/feedback and bounded later response evidence, separated by assessment stage; optional current-index whole-short-document evidence from approved file corpora |
366
382
  | Inside Out | Target human reply, nearest assistant text and bounded preceding visible history; channel-scoped speaker identity keys |
383
+ | Safety check | The complete current human message without OpenClaw's conversation-info header; the thread starter, replied-to message and channel messages since the last reply that arrived with it, which can include other people's names and message IDs; up to `historyMessages` earlier visible messages; and each eligible scenario's `instructions`. No sender, account or channel IDs from that header, approval lists or flagged prompts. A message wrapper Safety Whisperer does not recognize (anything other than Slack's) is sent whole, minus that header. |
367
384
  | Standalone QMD query | Query, optional intent, selected excerpts, source paths and evaluation time; separate process/SDK credentials and collection scope |
368
385
 
369
386
  Omitting tool-result/thinking/system fields does not remove their content if it
@@ -371,6 +388,157 @@ was quoted in ordinary visible text. Provider judgments are advisory; probabilit
371
388
  or score is not proof. Enabling a feature approves only that feature's documented processing.
372
389
 
373
390
 
391
+ ## Safety Whisperer
392
+
393
+ An optional, interim guardrail for agents that serve several people. Before the
394
+ main model runs on a human turn, it checks the current message against scenarios
395
+ you define, such as questions about one person's salary. When a requestor who is
396
+ not approved for a scenario asks something it covers, that scenario's
397
+ `flaggedPrompt` is appended next to their message for that turn.
398
+
399
+ **This is a prompt-level guardrail, not access control.** The classifier can miss
400
+ a request and the agent can ignore an injected instruction. It does not remove
401
+ sensitive information already available in context, memory or tools. An approved
402
+ requestor is not an approved audience: asking in a shared channel can still expose
403
+ an answer to everyone there. Use separate agents and real data-access controls for
404
+ hard boundaries.
405
+
406
+ ```json
407
+ {
408
+ "safetyWhisperer": {
409
+ "enabled": true,
410
+ "excludedChannels": [
411
+ { "name": "#leadership", "provider": "slack", "accountId": "work", "channelId": "C_LEADERSHIP" }
412
+ ],
413
+ "scenarios": [
414
+ {
415
+ "id": "individual-salaries",
416
+ "instructions": "Requests about a specific individual employee's salary, pay or compensation, including colloquial phrasing such as how much someone makes or earns. Exclude vendors and contractors.",
417
+ "approvedRequestors": [
418
+ { "name": "Dani", "provider": "slack", "accountId": "work", "senderId": "U_DANI" }
419
+ ],
420
+ "excludedChannels": [
421
+ { "name": "#people-ops", "provider": "slack", "accountId": "work", "channelId": "C_PEOPLE_OPS" }
422
+ ],
423
+ "flaggedPrompt": "This requestor is not approved to access this information. Kindly tell them that; do not disclose the requested information."
424
+ }
425
+ ]
426
+ }
427
+ }
428
+ ```
429
+
430
+ The plugin entry also needs `hooks.allowConversationAccess: true`, and
431
+ `allowPromptInjection` must not be `false`; see [host controls](#host-controls).
432
+
433
+ ### How each turn is checked
434
+
435
+ 1. Only user-triggered turns are checked, every time: there is no cooldown. A
436
+ prompt rebuild for the same run reuses that run's result. Messages steered
437
+ into a run that is already working are not checked; see
438
+ [messages sent while the agent works](#messages-sent-while-the-agent-works).
439
+ 2. A global `excludedChannels` match skips every scenario. A scenario's own
440
+ `excludedChannels` skip only that scenario. Thread replies and forum topics
441
+ use their channel.
442
+ 3. Disabled scenarios, scenarios limited to other `agentIds`, and scenarios that
443
+ approve this requestor are dropped.
444
+ 4. If none remain, no request is made. Otherwise one TypeSafe request asks one
445
+ independent yes/no question per remaining scenario. It includes the complete
446
+ current message, the channel context that arrived with it (thread starter,
447
+ replied-to message, chat history since the agent's last reply) and up to
448
+ `historyMessages` earlier visible messages. A follow-up like "What about
449
+ Sally?" or "answer the question above" is therefore judged in context, while
450
+ the current message is judged, not whether an earlier one mentioned the topic.
451
+ 5. Every scenario scoring at least its `minProbability` adds its `flaggedPrompt`
452
+ in `<safety>` tags, last in the shared `<unblock_memory>` block after the
453
+ human message.
454
+
455
+ ### Messages sent while the agent works
456
+
457
+ OpenClaw's default queue mode, `steer`, adds a message sent during an active run
458
+ to that run without building a new prompt, so the message is never checked. The
459
+ same applies when a person types `/queue steer` in a session. For agents that
460
+ rely on Safety Whisperer, set `messages.queue.mode` (and any
461
+ `messages.queue.byChannel` entries) to `followup` or `collect`; `memory_diagnostics`
462
+ and the Gateway log warn with `queue_mode_steer` until then. In `collect` mode,
463
+ queued messages from different people carry no single sender, so none of them is
464
+ approved.
465
+
466
+ ### Identity
467
+
468
+ Approval and exclusions compare `provider`, `accountId`, and `senderId` or
469
+ `channelId` values from the host's trusted turn metadata. `senderId` must match
470
+ exactly. `provider`, `accountId` and `channelId` match case-insensitively, because
471
+ OpenClaw lowercases them in places. Names are labels only; display names, message
472
+ text and identity claims never count. Missing identity never approves anyone and
473
+ never excludes a channel, so turns without a sender ID, such as CLI or webchat,
474
+ are treated as unapproved. Email-based approval is not supported.
475
+
476
+ For Slack:
477
+
478
+ - `provider` is `slack`.
479
+ - `accountId` is the OpenClaw Slack account key, or `default` when the channel
480
+ has one account.
481
+ - `senderId` is the person's member ID (`U…`): in their Slack profile, choose
482
+ **Copy member ID**.
483
+ - `channelId` is the channel's ID (`C…` or `G…`), shown at the bottom of the
484
+ channel details.
485
+
486
+ Some runtimes do not pass sender identity to plugins; OpenClaw's Codex
487
+ app-server harness is one. There, every requestor is treated as unapproved,
488
+ exclusions may not apply, and `memory_diagnostics` counts `missing_identity`;
489
+ the first such turn is also logged.
490
+
491
+ ### When the check cannot run
492
+
493
+ Safety Whisperer **fails open**: if the check cannot complete, the turn proceeds
494
+ with no safety prompt. A TypeSafe outage or missing key therefore turns the
495
+ guardrail off. The exception is a message too large to check: the requestor
496
+ controls its size, so every eligible scenario's `flaggedPrompt` is added instead.
497
+ `memory_diagnostics` reports each outcome under `whisperers.safety`, keeping
498
+ could-not-check separate from no match:
499
+
500
+ | Outcome | Meaning |
501
+ | --- | --- |
502
+ | `flagged` | At least one scenario matched; `matchedScenarios` counts matches by scenario ID |
503
+ | `no_match` | Checked; no scenario matched |
504
+ | `excluded` / `no_scenarios` | Skipped by a global exclusion, or nothing remained after filtering; no request |
505
+ | `empty_request` | The current message had no text, such as an attachment alone; not checked |
506
+ | `missing_key` / `typesafe_disabled` | Could not check: no credential, or TypeSafe disabled |
507
+ | `input_too_large` | Not checked: the current message exceeds the roughly 24k-token budget. It is never truncated; every eligible `flaggedPrompt` is added |
508
+ | `timed_out` / `failed` | Could not check: deadline, provider error or no usable answer |
509
+ | `partial_response` | Counted with `flagged`: some answers were unusable, and the confident matches were still added |
510
+ | `missing_identity` | Counted with the turn's outcome: the host passed no provider, account or sender ID |
511
+ | `cancelled` | The session ended or the Gateway stopped during the check |
512
+
513
+ Warnings include the outcome, error code, HTTP status and scenario count, never
514
+ message text; a missing key is logged once until it returns.
515
+ `memory_diagnostics` also reports `safety.active: false` with a `reason` when
516
+ settings or host controls prevent checks (`typesafe_disabled`,
517
+ `conversation_access_not_allowed`, `prompt_injection_not_allowed`,
518
+ `no_enabled_scenarios`), and a `warnings` list:
519
+
520
+ - `host_hook_timeout_below_whisperer_timeouts`: the host's `before_prompt_build`
521
+ timeout is shorter than the slowest enabled whisperer (`slowestWhispererMs`).
522
+ The host times out the plugin's whole contribution, so a slow Memory Whisperer
523
+ turn can drop a safety prompt even after diagnostics counted it as `flagged`.
524
+ - `queue_mode_steer`: see
525
+ [messages sent while the agent works](#messages-sent-while-the-agent-works).
526
+
527
+ The Gateway logs this status at startup when the feature is inactive or has a
528
+ warning.
529
+
530
+ ### Writing scenarios
531
+
532
+ `instructions` are the classifier's entire definition of a scenario. Name the
533
+ subject, its synonyms and colloquial phrasing, and what is excluded. In the
534
+ source checkout's `eval/safety-whisperer/` cases, adding "pay … including how much
535
+ someone makes or earns" raised a compound request ("What does Bob make, and is he
536
+ on medical leave?") from 0.06 to 0.45–0.48 and lowered "When is payday?" from 0.45
537
+ to 0.18–0.21. With that wording, the 0.4 default `minProbability` made no errors
538
+ across 22 cases, while 0.5 missed the compound request. Lower it to catch more
539
+ borderline requests, or raise it for fewer false refusals. Test your own scenarios
540
+ there before relying on them.
541
+
374
542
  ## Inside Out
375
543
 
376
544
  Inside Out records six independent emotion-presence probabilities for each human
@@ -384,15 +552,18 @@ change memories, retrieval, prompts, or dossiers.
384
552
  "enabled": true,
385
553
  "intervalMinutes": 1440,
386
554
  "maxInteractions": 100,
387
- "maxContextTokens": 30000
555
+ "maxContextTokens": 30000,
556
+ "baselineMinInteractions": 20
388
557
  }
389
558
  }
390
559
  ```
391
560
 
392
561
  Defaults: disabled; daily cadence; at most 100 attempted reviews per run; estimated
393
562
  30,000-token context budget, leaving room for questions under Jev's 32k
394
- state-plus-longest-question limit. `intervalMinutes: 0` is manual-only. The shared
395
- `typesafe` settings supply Jev credentials and request timeout.
563
+ state-plus-longest-question limit. `intervalMinutes: 0` makes inference manual-only;
564
+ local baselines still refresh daily and on Gateway startup when Inside Out is enabled.
565
+ `baselineMinInteractions` defaults to 20 (1–10,000) for each comparison population.
566
+ The shared `typesafe` settings supply Jev credentials and request timeout.
396
567
 
397
568
  **Enabling this approves sending visible human/assistant conversation text across
398
569
  all retained channels to TypeSafe.** System messages, tool bodies, thinking blocks,
@@ -417,11 +588,15 @@ openclaw memory-emotions link-people --agent main
417
588
  openclaw memory-emotions link-people --agent main --repair-identities
418
589
  openclaw memory-emotions run --agent main --session SESSION_ID
419
590
  openclaw memory-emotions export --agent main --session SESSION_ID
591
+ openclaw memory-emotions baseline-refresh --agent main
592
+ openclaw memory-emotions baselines --agent main --person PERSON_ID
593
+ openclaw memory-emotions export --agent main --person PERSON_ID --deviation-history
594
+ openclaw memory-emotions baseline-refresh --agent main --recompare
420
595
  ```
421
596
 
422
597
  `run` handles backfill and new interactions through the same bounded pass. Repeat
423
- it until `reviewed` and `failed` are zero to finish backfill. Gateway startup also
424
- runs one pass, then repeats at the configured interval. Successful results are
598
+ it until `reviewed` and `failed` are zero to finish backfill. With automatic inference
599
+ enabled, Gateway startup also runs one pass, then repeats at the configured interval. Successful results are
425
600
  cached; new interactions take priority over retries. Request failures retry after
426
601
  an hour when the pass has remaining capacity, or immediately with `--retry`.
427
602
  Oversized context failures require explicit `--retry`, for example after increasing
@@ -480,13 +655,109 @@ changed or removed mappings; by default it never reads transcripts. Its explicit
480
655
  `--repair-identities` option reads sources owning unlinked reviews and repairs
481
656
  their channel, sender ID and human key from persisted metadata before linking.
482
657
  Neither mode calls Jev or changes scores, timestamps, review IDs or checkpoints.
483
- Unmatched identities stay null. Exports include `person_id`.
658
+ Unmatched identities stay null. Exports include `person_id` and `personal_deviation`.
484
659
  `export` emits all matching rows as JSON, including errors. `report` averages
485
660
  successful rows by human, UTC day/week, rubric, and model; counts are the denominator.
486
- Both accept `--session`, `--sender`, `--since YYYY-MM-DD`, `--emotion`, and `--min`. `--sender`
661
+ Both accept `--person`, `--session`, `--sender`, `--since YYYY-MM-DD`, `--emotion`, and `--min`. `--sender`
487
662
  matches exact sender IDs across channels; use exported `human_key` for precise
488
663
  account-specific analysis. These commands are read-only and never invoke Jev.
489
664
 
665
+ ### Person-relative baselines and deviations
666
+
667
+ Inside Out keeps analytics beside PeopleSQL in the same `unblock-memory.sqlite`,
668
+ linked by its existing `person_id`. `person_emotion_baselines` holds one snapshot
669
+ per person, rubric, model and completed week; `inside_out_deviations` holds
670
+ individual assessments. They are separate from dossier fields, recognition blurbs
671
+ and searchable memory. PeopleSQL identities must already exist; Inside Out does not
672
+ create people or guess links. Statistics are per agent's retained grades, not
673
+ fleet-wide aggregates.
674
+
675
+ Weeks run Monday 00:00 UTC to the next Monday, with an exclusive end. Each stored
676
+ week keeps, per emotion, the exact sample count, sum, minimum and maximum, plus the
677
+ distribution in 0.01-wide bins (at most 101 values), so a snapshot stays small
678
+ however much history accumulates. Reports show weekly and cumulative sample counts,
679
+ mean, minimum, maximum, p10, median, p90 and the binned distribution; cumulative
680
+ values are computed from all earlier stored weeks when read. Means, minimums and
681
+ maximums are exact; quantiles use bin values. Completed empty weeks are logged with
682
+ zero weekly observations. Rubrics and models have separate populations.
683
+
684
+ For each graded reply, `personal_deviation` compares its raw probabilities to that
685
+ person's **preceding completed weeks**, excluding the reply's entire current week
686
+ and future interactions. Both cumulative history and the immediately preceding
687
+ week are reported. Each emotion includes:
688
+
689
+ - `delta`: signed difference from the person's exact baseline mean.
690
+ - `percentile`: empirical rank from 0 to 1 within 0.01-wide bins; grades in the
691
+ same bin are ties and take the midpoint.
692
+ - `unusualness`: `abs(2 * percentile - 1)`, from 0 at the middle to 1 at the extremes.
693
+ This is a rank-derived signal, **not a probability or emotional intensity**.
694
+
695
+ For example, a joy probability of 0.8 can be routine for someone whose prior grades
696
+ are consistently 0.8, and unusual for someone whose prior grades are 0.1. This does
697
+ not establish their personality, mental state, or the agent's quality. Counts below
698
+ `baselineMinInteractions` produce `insufficient_history` with null emotion metrics,
699
+ independently for weekly and cumulative comparisons. Unlinked people, missing or
700
+ future timestamps and failed grades have explicit statuses; they are never neutral
701
+ scores. Exports before the first local assessment show `not_compared`.
702
+
703
+ `run` refreshes snapshots after grading new replies. Both grading and daily local
704
+ refreshes update exact PeopleSQL links before aggregating or assessing new replies.
705
+ Historical aggregation runs in a worker thread and commits in short batches, so the
706
+ Gateway event loop stays responsive and other writers to `unblock-memory.sqlite`
707
+ wait for one batch at most, never a whole refresh. Gateway shutdown cancels an
708
+ active refresh; the next one resumes, and repeating a refresh never duplicates
709
+ records. If the refresh after grading fails, `run` still returns its counts with a
710
+ `baselineError` (the Gateway logs a warning); saved grades are unaffected.
711
+ `link-people` also refreshes analytics. `baseline-refresh` does this without loading
712
+ transcripts, calling Jev, or requiring TypeSafe credentials. `baselines` reads
713
+ weekly snapshots, newest first, with optional `--person`, `--since YYYY-MM-DD`
714
+ (inclusive UTC week-end) and `--limit` (1–1,000 weeks, default 100).
715
+
716
+ Snapshots always reflect the currently linked grades. A late historical grade, a
717
+ retry, a relink or removed identity evidence rewrites the affected week and every
718
+ later week of that population in place, and a population with no linked grades left
719
+ loses its snapshots; earlier revisions are not kept. A snapshot `id` addresses its
720
+ whole cumulative population, so returning to an earlier population restores the
721
+ earlier `id`.
722
+
723
+ Normal refreshes preserve an existing assessment of the same grade. Late historical
724
+ grades change snapshots but do not silently rewrite earlier comparisons.
725
+ Use `baseline-refresh --recompare` to append changed assessments after backfill,
726
+ identity or sample-policy changes; `export --deviation-history` includes the earlier
727
+ results and marks which match the current grade. Repeating an unchanged refresh
728
+ adds no duplicate records. Previously sparse or future-dated assessments also need
729
+ explicit re-comparison when their circumstances change.
730
+
731
+ `computed_at` and `assessedAt` record when these calculations actually occurred.
732
+ A backfilled comparison uses retained evidence from earlier interaction weeks, but
733
+ does not pretend that evidence had already been graded at the historical send time.
734
+ Baseline calculation requires no additional model calls and changes no raw emotion
735
+ judgments, dossiers, prompt injection, clustering or memory creation.
736
+
737
+ ### Deleting a person's emotion data
738
+
739
+ ```bash
740
+ openclaw memory-emotions purge --agent main --person PERSON_ID
741
+ openclaw memory-emotions purge --agent main --person PERSON_ID --grades
742
+ ```
743
+
744
+ `purge` deletes one person's weekly snapshots and their whole assessment history,
745
+ including assessments recorded while a reply was linked to them, and prints what it
746
+ removed. It makes no Jev calls.
747
+
748
+ - **Without `--grades`**, their raw grades stay, so the next refresh rebuilds
749
+ current snapshots and assessments from replies still linked to them. Use this
750
+ after correcting a misattributed identity.
751
+ - **With `--grades`**, it also erases the six probabilities of every reply currently
752
+ linked to that person and unlinks it. Each row stays as a tombstone with the error
753
+ `Purged by operator`, so `run` (including `--retry`) never sends that reply to Jev
754
+ again, `link-people` never relinks it, and refreshes skip it. This cannot be
755
+ undone; run `link-people` first so every reply from that person is linked.
756
+
757
+ Purging removes existing data only. While Inside Out stays enabled and the person's
758
+ identity remains in PeopleSQL, their new replies are graded and start a new
759
+ baseline. Soft-deleting a person in PeopleSQL does not remove Inside Out data.
760
+
490
761
  ## Storage, upgrades and recovery
491
762
 
492
763
  Each agent's state lives under the configured OpenClaw state directory, normally
package/docs/peoplesql.md CHANGED
@@ -9,6 +9,13 @@ store with `people.enabled`; enable injection separately with
9
9
  and automatic save review, not a maintenance scheduler. For independent setup
10
10
  examples, see the [configuration profiles](configuration.md#example-profiles).
11
11
 
12
+ [Inside Out baselines](configuration.md#person-relative-baselines-and-deviations)
13
+ reuse PeopleSQL's person IDs in separate analytics tables in the same database.
14
+ They retain weekly emotion-grade distributions and interaction deviations; none of
15
+ this analytics is written into dossiers or injected by People Whisperer. Deleting a
16
+ dossier or soft-deleting a person leaves it in place; use
17
+ [`memory-emotions purge`](configuration.md#deleting-a-persons-emotion-data).
18
+
12
19
  ## Optional People Dossier Primer
13
20
 
14
21
  `memory_people_prime({ personId, agentName? })` prepares evidence for an existing person;
package/docs/retrieval.md CHANGED
@@ -468,9 +468,11 @@ or raw exceptions; exact query text is not retained by this telemetry.
468
468
  ## Skill Whisperer
469
469
 
470
470
 
471
- Skill Whisperer is an optional semantic reminder for user turns. Configure one
472
- isolated `skills` corpus, set `skillWhisperer.enabled` to `true`, and authorize
473
- `plugins.entries.unblock-memory.hooks.allowConversationAccess`. The feature
471
+ Skill Whisperer is a semantic reminder for user turns. It turns on by default once
472
+ you configure one isolated `skills` corpus and authorize
473
+ `plugins.entries.unblock-memory.hooks.allowConversationAccess`. Without a skills
474
+ corpus it stays inactive: no prompt hook is registered and diagnostics report it
475
+ disabled. Set `skillWhisperer.enabled` to `false` to opt out. It
474
476
  embeds the current prompt plus the configured number of prior user/assistant
475
477
  messages, compares it with each configured skill's frontmatter `name` and
476
478
  `description`. With TypeSafe enabled and a key available, the top three valid
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "unblock-memory",
3
3
  "name": "Unblock Memory",
4
- "version": "0.8.3",
4
+ "version": "0.10.0",
5
5
  "description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
6
6
  "kind": "memory",
7
7
  "activation": { "onStartup": true },
@@ -92,6 +92,14 @@
92
92
  "label": "Approved Hint Corpora",
93
93
  "help": "Explicit non-skill corpus allowlist for automatic recall and TypeSafe processing. Session recall spans this agent's indexed sessions; the sessions corpus chatTypes setting controls direct-message inclusion."
94
94
  },
95
+ "safetyWhisperer.enabled": {
96
+ "label": "Safety Whisperer",
97
+ "help": "Prompt-level guardrail, not access control. Sends every checked human message and bounded recent history to TypeSafe and appends a scenario's flaggedPrompt when an unapproved requestor's message matches. Fails open: if the check cannot run, the turn proceeds without it. Requires hook conversation access and prompt injection. Disabled by default."
98
+ },
99
+ "safetyWhisperer.scenarios": {
100
+ "label": "Safety Scenarios",
101
+ "help": "Each scenario is judged independently. Approval and exclusions match exact provider/account/sender or channel IDs from trusted channel metadata, never names or message text."
102
+ },
95
103
  "people.enabled": {
96
104
  "label": "PeopleSQL",
97
105
  "help": "Enable the agent-local people store, Slack identity observation and people tools. Disabled by default; whispering and TypeSafe primer/review are separate opt-ins."
@@ -116,7 +124,8 @@
116
124
  "enabled": { "type": "boolean", "default": false },
117
125
  "intervalMinutes": { "type": "integer", "minimum": 0, "maximum": 10080, "default": 1440 },
118
126
  "maxInteractions": { "type": "integer", "minimum": 1, "maximum": 10000, "default": 100 },
119
- "maxContextTokens": { "type": "integer", "minimum": 256, "maximum": 30000, "default": 30000 }
127
+ "maxContextTokens": { "type": "integer", "minimum": 256, "maximum": 30000, "default": 30000 },
128
+ "baselineMinInteractions": { "type": "integer", "minimum": 1, "maximum": 10000, "default": 20 }
120
129
  }
121
130
  },
122
131
  "extraction": {
@@ -340,17 +349,103 @@
340
349
  "type": "object",
341
350
  "additionalProperties": false,
342
351
  "properties": {
343
- "enabled": { "type": "boolean", "default": false },
352
+ "enabled": { "type": "boolean", "default": true, "description": "Takes effect only when a corpus with kind \"skills\" is configured." },
344
353
  "historyMessages": { "type": "integer", "minimum": 0, "default": 5 },
345
354
  "minScore": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.5 },
346
355
  "cooldownTurns": { "type": "integer", "minimum": 0, "default": 10 }
347
356
  },
348
357
  "default": {
349
- "enabled": false,
358
+ "enabled": true,
350
359
  "historyMessages": 5,
351
360
  "minScore": 0.5,
352
361
  "cooldownTurns": 10
353
362
  }
363
+ },
364
+ "safetyWhisperer": {
365
+ "type": "object",
366
+ "additionalProperties": false,
367
+ "description": "Optional prompt-level guardrail for sensitive request scenarios. Fails open when the check cannot run.",
368
+ "properties": {
369
+ "enabled": { "type": "boolean", "default": false },
370
+ "historyMessages": { "type": "integer", "minimum": 0, "maximum": 20, "default": 6 },
371
+ "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 10000, "default": 3000 },
372
+ "excludedChannels": {
373
+ "type": "array",
374
+ "maxItems": 500,
375
+ "description": "Channels where every safety scenario is skipped. Threads inherit their channel.",
376
+ "items": {
377
+ "type": "object",
378
+ "additionalProperties": false,
379
+ "required": ["provider", "accountId", "channelId"],
380
+ "properties": {
381
+ "name": { "type": "string", "pattern": "\\S", "maxLength": 256 },
382
+ "provider": { "type": "string", "pattern": "\\S", "maxLength": 256 },
383
+ "accountId": { "type": "string", "pattern": "\\S", "maxLength": 256 },
384
+ "channelId": { "type": "string", "pattern": "\\S", "maxLength": 256 }
385
+ }
386
+ }
387
+ },
388
+ "scenarios": {
389
+ "type": "array",
390
+ "maxItems": 20,
391
+ "items": {
392
+ "type": "object",
393
+ "additionalProperties": false,
394
+ "required": ["id", "instructions", "flaggedPrompt"],
395
+ "properties": {
396
+ "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
397
+ "enabled": { "type": "boolean", "default": true },
398
+ "instructions": { "type": "string", "pattern": "\\S", "maxLength": 2000, "description": "What requests this scenario covers, including exclusions." },
399
+ "flaggedPrompt": { "type": "string", "pattern": "\\S", "maxLength": 2000, "description": "Appended next to the human message when an unapproved requestor's message matches." },
400
+ "minProbability": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.4, "description": "Minimum TypeSafe yes-probability that the current request is covered." },
401
+ "agentIds": {
402
+ "type": "array",
403
+ "minItems": 1,
404
+ "maxItems": 500,
405
+ "description": "Limit the scenario to these agents; omit for every agent.",
406
+ "items": { "type": "string", "pattern": "\\S", "maxLength": 256 }
407
+ },
408
+ "approvedRequestors": {
409
+ "type": "array",
410
+ "maxItems": 500,
411
+ "items": {
412
+ "type": "object",
413
+ "additionalProperties": false,
414
+ "required": ["provider", "accountId", "senderId"],
415
+ "properties": {
416
+ "name": { "type": "string", "pattern": "\\S", "maxLength": 256 },
417
+ "provider": { "type": "string", "pattern": "\\S", "maxLength": 256 },
418
+ "accountId": { "type": "string", "pattern": "\\S", "maxLength": 256 },
419
+ "senderId": { "type": "string", "pattern": "\\S", "maxLength": 256 }
420
+ }
421
+ }
422
+ },
423
+ "excludedChannels": {
424
+ "type": "array",
425
+ "maxItems": 500,
426
+ "items": {
427
+ "type": "object",
428
+ "additionalProperties": false,
429
+ "required": ["provider", "accountId", "channelId"],
430
+ "properties": {
431
+ "name": { "type": "string", "pattern": "\\S", "maxLength": 256 },
432
+ "provider": { "type": "string", "pattern": "\\S", "maxLength": 256 },
433
+ "accountId": { "type": "string", "pattern": "\\S", "maxLength": 256 },
434
+ "channelId": { "type": "string", "pattern": "\\S", "maxLength": 256 }
435
+ }
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
441
+ },
442
+ "default": {
443
+ "enabled": false,
444
+ "historyMessages": 6,
445
+ "timeoutMs": 3000,
446
+ "excludedChannels": [],
447
+ "scenarios": []
448
+ }
354
449
  }
355
450
  }
356
451
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/unblock-memory",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",