# Changelog

All notable changes to this fork are documented here. Entries follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

Starting with 0.7.0 this fork uses an independent semver track decoupled from upstream. Prior releases used `<upstream>-identity.<n>`, which npm/OpenClaw treated as prereleases and blocked `plugins install` / `plugins update` from picking up the latest version without an explicit pin.

## [Unreleased]

## [0.10.0] - 2026-06-18

### Added — TEST mode (mocked Hindsight backend)

New `testMode` config (boolean, default `false`). When enabled, the plugin
serves a fully in-memory, mocked Hindsight backend: no embedded daemon, no
external API, no LLM credentials, no retain queue, no health checks. This lets
the plugin be deployed into test/staging environments with no reachable
Hindsight instance while still exercising its real effect on an agent's replies.

The mock replaces ONLY the `HindsightClient` HTTP boundary (`recall` / `retain`
/ `createBank`). Everything downstream runs unchanged against the canned data —
the `scopeClient` wrapper, the `recallMinRelevance` cross-encoder threshold
filter, `recallTopK`, `formatMemories` + `<hindsight_memories>` injection, and
provenance reporting. The canned recall response carries a well-formed
`trace.final_results[].cross_encoder_score`, so the threshold filter and the
provenance score path behave exactly as against a real server (TEST mode does
NOT silently bypass them).

`recall()` returns the built-in default memory set, overridable per-deployment
via `testMemories` (each entry needs a `text`; `score` becomes the entry's
cross-encoder score, defaulting to `0.5` — above the default
`recallMinRelevance` of `0.3` — so unscored memories are injected unless the
tester opts into filtering with a low score). `retain()` and `createBank()` are
resolving no-ops. A one-time WARN is logged at startup (`⚠️ TEST MODE ACTIVE`)
so mocked data is never mistaken for real memory.

**Langfuse:** the plugin emits nothing to Langfuse itself (by design). Once
mocked memories land in the prompt through the normal hook path, they become
part of whatever LLM trace the agent already produces — so they reach Langfuse
*if* a LiteLLM ↔ Langfuse callback is active in the environment (a deployment
prerequisite, not set up by this plugin). For plugin-side observability of the
injection, `provenanceReport: "full"` surfaces the exact injected set (with
scores) on the gateway agent-event bus — verified end-to-end in TEST mode.

NOT FOR PRODUCTION — guard against shipping `testMode: true` to a real
deployment.

## [0.9.1] - 2026-06-12

### Changed — privacy: log stable rejection codes for recall/retain skips

Recall/retain rejections are now logged as a STABLE CATEGORY CODE via
`classifyRejectionReason` instead of free-form detail, standardizing failure
classification and keeping potentially sensitive text out of the log stream.
Session handling was hardened so a user's identity stays consistent across
channels.

## [0.9.0] - 2026-06-12

### Fixed — webchat sessions fed a phantom `::cli` bank, ignoring identityLinks

`parseSessionKey` had no branch for the openclaw-webchat bridge's
per-conversation shape (`agent:<agentId>:webchat:chat:<canonical>:<chatId>`),
so those keys fell into the generic block: no `direct:<canonical>` channel was
derived, `canonicalDirectUser` resolved to nothing, and the bank user segment
fell back to the gateway senderId — `"cli"` for operator-mode clients. Every
webchat turn recreated a phantom `<prefix>-<agent>::cli` bank no matter what
`session.identityLinks` said (the links map `webchat:<canonical>`, which this
path never looked up). Observed live on the deployment.

New dedicated branch (mirror of the 6-part Open WebUI block): the channel
collapses to `direct:<canonical>`, so every webchat conversation of one human
feeds their ONE unified bank (e.g. `alice-alice::alice`), consolidated
with Telegram/WhatsApp via identityLinks. `provider` is intentionally omitted
(dashboard/openwebui convention). The pre-existing `::cli` bank is NOT
auto-migrated — delete it (or backfill) once this version is deployed.

### Added — provenance reporting (provenance/v1, opt-in)

New `provenanceReport` config (`"off"` default | `"metadata"` | `"full"`).
When enabled, every auto-recall injection also emits a structured provenance
report on the gateway agent-event bus (stream `hindsight-openclaw.provenance`)
so a chat frontend (openclaw-webchat) can show the user **which memories fed
this reply** — fact ids/types/dates plus the trace `cross_encoder_score` when
the server provides it, and the exact injected fact text at `full`. The report
mirrors EXACTLY the injected set (post `recallMinRelevance` filter, post
`recallTopK`), never the raw recall. Normative contract: openclaw-webchat
`docs/PROVENANCE_CONTRACT.md`.

Robustness/privacy invariants: emission degrades to SILENCE on old SDKs
(`emitAgentEvent` feature-detected), on a missing `runId`, or on a gateway
rejection; reports never go through logs; the gateway re-registration quirk is
handled by emitting through the FIRST registration's api (module singleton).
Off-list config values normalize to `"off"`. Emission FAILURES are logged as a
STABLE CATEGORY CODE only (`classifyRejectionReason`: `plugin_not_loaded` /
`missing_run_context` / `invalid_stream` / `validation_error` / `rate_limited`
/ `rejected` / `throw:<Error.name>`) — never the raw gateway reason or Error
message, which in `full` mode could echo injected fact excerpts into the log
stream. Mirrors the openclaw-knowledge plugin's classifier so both behave
identically.

## [0.8.0] - 2026-05-24

### Added — recall relevance threshold (`recallMinRelevance`)

The plugin now exposes a `recallMinRelevance` config option that gates fact injection on the raw `cross_encoder_score` produced by the server-side reranker. When set, the plugin activates `trace=true` on the recall request, reads `response.trace.final_results[].cross_encoder_score`, and filters out every fact below the threshold BEFORE composing the `<hindsight_memories>` block. When no fact passes, the block is omitted entirely — **silence beats noise**.

The default value is **0.3**. With a passthrough reranker (the historical Hindsight server default `RERANKER_PROVIDER=rrf`), every fact's score is the constant 0.5 ≥ 0.3, so the filter is a no-op and behavior is unchanged. Once the server is configured with a real neural reranker (Jina / Cohere / BGE), the filter becomes effective and the doctrine "if no fact is relevant, inject nothing" is enforced.

Tunable per-deployment via `openclaw config set plugins.entries.hindsight-openclaw.config.recallMinRelevance 0.4`. Set to `0` to keep the trace for empirical observability without filtering. Set to `null` to disable both trace and filtering.

The threshold is **uniform across all banks** by design — a shared admin bank reproduces the conditions of every other collaborator's bank, so a per-bank threshold would break test reproducibility.

### Added — recall router (`recallRouter`)

A new `recallRouter` config block decides whether (and what kinds of) memory recall should be performed for a given user turn. The router is uniform across all banks and runs in two stages:

1. **Heuristics** — deterministic regex rules that skip recall on:
   - `ctx.trigger ∈ {heartbeat, cron, memory}` (operational, not user-initiated)
   - Agent meta-questions (`Quel est ton sessionid`, `who are you`, `what model are you`, `combien d'agents`)
   - CLI test pings (`test`, `ping`, `salut`, `hello`, `Are you there?`, `T'es là ?`) — restricted to `messageProvider="cli"` to avoid blocking short legitimate Telegram / Open WebUI turns
   - Anchored status pings (`status`, `heartbeat`, `system status`)

2. **Optional Jina classifier** (`mode: "jina-classifier"`) — calls `https://api.jina.ai/v1/classify` to classify ambiguous prompts. Supports two label sets:
   - `routes: 2` (default) — `NONE` / `ALL` (robust on short FR prompts)
   - `routes: 4` — `NONE` / `WORLD_ONLY` / `EXPERIENCE_ONLY` / `ALL` (finer routing, requires longer or unambiguous prompts)

When the router picks `WORLD_ONLY` or `EXPERIENCE_ONLY`, the plugin narrows `recallTypes` accordingly for that single turn (preserving the operator-configured types when narrowing would yield an empty list — fail-open).

Fail-open at every layer: a Jina outage, a malformed response, an unknown label, or a low-confidence prediction all degrade to `ALL` (legacy behavior) rather than blocking the agent.

Default: `{ enabled: true, mode: "heuristic", routes: 2, minConfidence: 0.35 }`. The default skips ~95% of heartbeat / meta / test pings with zero Jina cost. Switch to `mode: "jina-classifier"` and provide `jinaApiKey` to enable semantic routing.

### Added — extended `isEphemeralOperationalText` patterns

`isEphemeralOperationalText` (which short-circuits both retain and recall on operational noise) now catches a broader set of bilingual self-introspection and test pings observed in production Opik traces:

- `Quel est ton sessionid` / `session_id` / `runid` / `run_id`
- `Donne-moi l'identifiant de ta session` and variants
- `who are you`, `qui es-tu`, `what model are you`, `comment t'appelles-tu`
- `test`, `test de bon fonctionnement`, `test rapide`, `test fonctionnel`
- `ping`, `hello`, `salut`, `Are you there?`, `T'es là ?`, `T'es en ligne`
- `system status`, `heartbeat`

All regexes are anchored on `^...$` for short-prompt patterns to avoid false positives on legitimate business questions that happen to contain `test` or `status` (e.g. *"What is the status of the Acme project?"* — kept).

### Fixed — manifest version drift

`openclaw.plugin.json` was stuck at `0.7.3` while `package.json` advanced to `0.7.9`. Both are now aligned on `0.8.0`. OpenClaw treats both as sources of truth for capabilities discovery; the drift was harmless in practice but misleading in logs (`hindsight-openclaw@0.7.3` reported by the gateway despite the npm package being 0.7.9).

### Changed — recall hook surface (internal)

`BankScopedClient.recall` now accepts an optional `trace?: boolean` flag, propagated to the underlying `@vectorize-io/hindsight-client@0.5.0` SDK. The flag is opt-in: it is only set when `recallMinRelevance` is defined, keeping the wire payload identical to 0.7.x for deployments that disable the threshold.

The in-flight recall deduplication key now also hashes the active `recallTypes` and the `trace` flag, so a router-narrowed call (e.g. `types: ["world"]`) does not get conflated with a parallel `ALL` call to the same bank with the same query.

### Compatibility

- **Hindsight server**: ≥ 0.6.0. The `recallMinRelevance` filter activates the recall trace; older servers that omit `trace.final_results` from the response trigger the fail-open path (filter bypassed, log emitted at debug level).
- **OpenClaw gateway**: ≥ 2026.5.0 (unchanged from 0.7.x).
- **Jina cloud**: free tier sufficient for heuristic-only mode (zero calls). For `mode: "jina-classifier"`, the free tier (100 RPM / 100K TPM / 10M starter tokens) handles ~100 ambiguous prompts per minute per deployment.

### Migration

No action required — the defaults preserve legacy behavior for deployments that have not yet enabled a neural reranker server-side. To benefit from the threshold filter, configure `HINDSIGHT_API_RERANKER_PROVIDER=cohere` (or equivalent) on the Hindsight server, then tune `recallMinRelevance` empirically against trace observations.

### Added (post-publish hardening — advisor 2026-05-24)

After an independent advisor review surfaced four concerns the Codex passes did not catch, three were patched into this release before publish.

- **`recallRouter.observationFollowsNarrow`** (default `true`): when the router narrows to `WORLD_ONLY` or `EXPERIENCE_ONLY`, the operator-configured `observation` fact type is preserved alongside the chosen route by default. Consolidated observations are derived from world/experience and are often the highest-signal recall targets — strict exclusivity would silently drop the most relevant fact for many queries ("when was Alice hired?" classified WORLD_ONLY would otherwise lose the consolidated observation about Alice). Operators wanting strict route exclusivity pass `false` explicitly.

- **Log-once warning when `trace.final_results` is unavailable**: when `recallMinRelevance` is set but the Hindsight server omits the trace (older server version, passthrough reranker, upstream payload rename), the threshold filter is bypassed (fail-open). The plugin now logs a `warn` on the first occurrence per process so an operator sees the silent disabling instead of discovering it weeks later. Subsequent recalls log at `debug` only. Test-only helper `_resetTraceMissingLogFlagForTests` reset hooks the sticky flag for unit coverage.

- **End-to-end integration test for the router → trace → filter → topK chain**: new `src/hook-router-filter.test.ts` exercises the full hook wiring with a mocked `HindsightClient.prototype.recall` returning controlled trace payloads. Covers: threshold filtering (drop below, keep above, inject nothing when all below), router NONE skip, trace opt-out via `recallMinRelevance: null`, log-once trace-missing path, and router fall-back to ALL on ambiguous queries.

The fourth concern (`trace: true` server cost overhead under the default `recallMinRelevance=0.3`) was not patched — it is a runtime verification rather than a defect: a `curl` against the Hindsight `/recall` endpoint with `trace: true` quantifies the payload growth in production once Phase 0 (Jina reranker activation) goes live.

## [0.7.9] - 2026-05-17

### Fixed — regression in 0.7.8: Open WebUI retain/recall skipped (provider mismatch)

0.7.8 set `provider: "openwebui"` in the `parseSessionKey`
`openwebui:chat` branch, on the assumption that "openwebui" was the
dispatch surface. It is not. The `openclaw_gateway_pipe` connects to
the gateway with `client.mode="cli"`, so OpenClaw classifies the
dispatch surface as **`webchat`** (verified empirically: inbound_meta
v2 reports `channel/provider/surface = "webchat"`, sender
`{label:"cli", id:"cli"}`, Opik `channel: webchat`).

With `provider: "openwebui"` set, the cross-check in
`resolveAndCacheIdentity`
(`parsedSession.provider && dispatchChannel && parsedSession.provider
!== dispatchChannel`) evaluated `"openwebui" !== "webchat"` → **every
Open WebUI turn was skipped** with *"dispatch surface webchat does not
match session provider openwebui"*: zero retain, zero recall for all
Open WebUI conversations. (The legacy 5-part
`agent:<a>:openwebui:direct:<canonical>` shape had the same latent
bug via the generic branch — Open WebUI retain via the pipe never
actually worked.)

**Fix:** the `openwebui:chat` branch no longer sets `provider` —
identical convention to the `dashboard` block. An undefined
`parsedSession.provider` short-circuits the first guard of the
dispatch-mismatch check, and `resolveSessionIdentity` resolves
`messageProvider` from `ctx.messageProvider` ("webchat", the real
dispatch surface). The bank is still derived from the
`direct:<canonical>` channel segment, independent of provider, so
per-human unification (Open WebUI + Telegram + WhatsApp via
identityLinks) is preserved. Bank for alice remains
`alice-alice::alice`.

**Latent 5-part bug closed (defense-in-depth).** The legacy
`agent:<a>:openwebui:direct:<canonical>` shape — still emitted by the
pipe as a fallback when no chat_id is available (title-generation,
task calls), and the shape a pipe rollback would revert to — got the
same `provider="openwebui"` via the generic branch and the same
silent dispatch-mismatch skip. A dedicated branch
(`parts.length === 5 && parts[2] === "openwebui" && parts[3] ===
"direct"`) now omits provider too, scoped strictly to `openwebui`
(Telegram/WhatsApp 5-part keep provider — the cross-check is
desirable for native channels). This is safe: title-gen calls are
independently filtered by `isEphemeralOperationalText` (text-level
guard on both recall and retain), so closing the path does not
pollute the bank; a genuine user message via this shape is retained
correctly. No more "documented-but-open" latent failure mode.

**Deployment:** the pipe already emits the 6-part shape (Option C is
live), so Open WebUI retain is currently broken on 0.7.8. Deploy 0.7.9
on both instances as soon as possible to restore it — no ordering
constraint (this strictly repairs; it cannot regress the legacy path).

## [0.7.8] - 2026-05-15

### Added — Open WebUI per-conversation sessionKey support (bank unification)

The custom `openclaw_gateway_pipe` Open WebUI Function now isolates each
Open WebUI conversation as its own OpenClaw session (ChatGPT-style: one
sessionKey per conversation thread → distinct sessionId, independent
short context, automatic fresh context on "New conversation"). It emits
a 6-part sessionKey:

```
agent:<agentId>:openwebui:chat:<canonical>:<chatId>
```

`parseSessionKey` now recognizes this shape and collapses the channel
segment to `direct:<canonical>` (same convention as the existing
`dashboard` block), keeping `provider="openwebui"`. Hindsight memory
stays **unified per human** across every Open WebUI conversation AND
consolidated with their Telegram/WhatsApp banks via identityLinks —
short context isolated per conversation, long-term memory shared per
person.

**Deployment ordering (critical):** deploy this plugin version on every
instance BEFORE switching the Open WebUI pipe to the per-chat sessionKey.
A pipe emitting the 6-part shape against a plugin without this branch
falls through to the generic parser, `extractCanonicalDirectUser` fails
the `^direct:` regex, and every Open WebUI conversation routes to the
`{prefix}-{agent}::anonymous` bank (cross-conversation collision + loss
of cross-channel unification) until the plugin is updated.

## [0.7.7] - 2026-05-14

### Fixed — regression in 0.7.6: legitimate sessions skipped on Control UI

0.7.6 introduced `excludeSenders` and applied it against the **raw**
`senderId` value emitted by the OpenClaw gateway. In practice, the
gateway emits `senderId="openclaw-control-ui"` (a channel-name fallback)
for any Control UI WebSocket session — even when the sessionKey carries
a resolved canonical identity (`direct:<user>`) from
`session.identityLinks`. `deriveBankId` was already correctly preferring
the canonical identity over the raw senderId for the `user` segment, so
legitimate sessions targeted legitimate banks like
`alice-alice::alice`. But the 0.7.6 guard short-circuited
**before** that resolution, looking only at the raw senderId, and
blocked every legitimate Control UI conversation as if it were a
phantom-bank attempt.

Observed: human prompts on two agent instances produced repeated log
lines:
```
[Hindsight] Skipping recall — senderId="openclaw-control-ui" matches excludeSenders ...
[Hindsight Hook] Skipping retain — senderId="openclaw-control-ui" matches excludeSenders ...
```
…while pre-bump traffic on the same setup wrote correctly into
`alice-alice::alice`.

### Changed — guard now applies to the resolved user segment, not the raw senderId

New helper `resolveBankUserSegment(ctx)` mirrors the resolution chain
used by `deriveBankId` for the `user` field:
```
canonicalDirectUser (from sessionKey `direct:<X>`) -> senderId -> "anonymous"
```
The recall (`before_prompt_build`) and retain (`agent_end`) hooks now
call `isExcludedSender(resolveBankUserSegment(ctx), pluginConfig)`. As
a result:
- A session with `senderId="openclaw-control-ui"` **and** a resolved
  `direct:alice` in its sessionKey → user segment = `"alice"` →
  guard passes → bank `{prefix}-alice::alice` (legitimate).
- A session with `senderId="openclaw-control-ui"` **and no** canonical
  identity → user segment = `"openclaw-control-ui"` → guard fires →
  no phantom bank is touched. This is the true phantom case the guard
  was always meant to catch.

`deriveBankId` itself is unchanged and remains pure. The fix is a
one-line lift in the hooks plus a small exported helper, with tests
that pin the contract `deriveBankId(ctx).user_segment === resolveBankUserSegment(ctx)`.

### Changed — guard gated on `bankIdExposesUserSegment(pluginConfig)`

New helper `bankIdExposesUserSegment` reports whether the configured
bank ID format will actually contain a sender-derived segment. The
recall and retain hooks now gate the `excludeSenders` check on this:
- `dynamicBankId: false` (static bank) → guard disabled, no phantom
  shape possible.
- `dynamicBankGranularity` without `"user"` (e.g. `["agent", "channel"]`)
  → guard disabled, the bank ID doesn't reflect the sender at all.
- Default config (or any granularity containing `"user"`) → guard
  active as before.

This closes an over-blocking edge case where an operator using a
custom granularity could have legitimate sessions silently skipped on
the basis of a `senderId` value that wouldn't have appeared in the
bank ID anyway.

### Note on data loss caused by the 0.7.6 release

The phantom-bank diagnosis that led to 0.7.6 also led to a manual purge
of banks named `*::openclaw-control-ui`. Post-0.7.7 investigation
showed those banks also contained legitimate human conversations that
fell into the fallback bank shape when `canonicalDirectUser` was not
resolved by the gateway (intermittent `session.identityLinks` mapping).
The deleted facts cannot be recovered without external snapshots. The
root cause of the inconsistent canonical resolution is upstream (gateway
or `session.identityLinks` policy) and is tracked separately.

## [0.7.6] - 2026-05-13

### Added — `excludeSenders` config option (phantom bank protection)

Sessions where the OpenClaw gateway cannot authenticate the principal end up
with `senderId` set to a channel-name fallback (`openclaw-control-ui`,
`anonymous`, `unknown`). Under `dynamicBankGranularity: ["agent", "user"]`,
this generated phantom banks such as `bob-agent::openclaw-control-ui`
that polluted the memory store with unauthenticated traffic.

Observed root causes:
- Internal Playwright sessions launched by the `browser` skill connecting to
  the gateway webchat without a token.
- App devices that lost their gateway token between restarts.
- Health-check or scanner traffic mimicking a webchat client.

`excludeSenders` skips recall AND retain for any senderId in the list. When
unset, the default list is `["anonymous", "unknown", "openclaw-control-ui"]`.
Pass `[]` to disable the protection entirely (not recommended).

Missing `senderId` (undefined or empty) is normalized to the sentinel
`"anonymous"` before the lookup. In practice today the upstream
`getIdentitySkipReason` already rejects sessions with `senderId` undefined
or equal to `"anonymous"` before `isExcludedSender` is reached, so this
normalization is belt-and-suspenders, not the closure of an active hole.
It buys insurance against a future refactor that would relax that upstream
guard. Operators using a custom `excludeSenders` list should include
`"anonymous"` if they want the normalization to remain effective.

Comparison is case-insensitive on both the resolved senderId and the
configured list, so a gateway emitting a different capitalization (e.g.
`"Anonymous"` after an upstream change) is still caught. This also applies
to custom `excludeSenders` lists — entries that previously would have
required exact-case matching (a non-existent scenario before 0.7.6 since
the option is new) are now matched regardless of case. Operators who
intentionally want case-sensitive behavior cannot opt into it; the trade
is biased toward catching drift.

The check is enforced in the `before_prompt_build` and `agent_end` hooks
**before** any bank is read or written. `deriveBankId` itself remains pure
and deterministic — the skip is a caller-side concern.

#### Operational note — logs and PII

When the guard triggers, the plugin emits a warning that includes the
resolved senderId verbatim. The three default exclusions are non-PII
sentinels. Operators who add real identifiers (phone numbers, user IDs)
to a custom `excludeSenders` list should be aware those values will
appear in container logs each time a guarded session is rejected.

### Why this is defense in depth, not a workaround

The OpenClaw gateway already rejects connections without a token when
`controlUi.allowInsecureAuth: false`. This option protects the memory store
even when that gateway setting drifts (debug toggle, future migration,
upstream behavior change) so a phantom bank can never be silently created.

## [0.7.5] - 2026-05-06

### Fixed — regression in 0.7.4: dashboard sessions skipped silently

The 0.7.4 dashboard session patch returned `provider: "dashboard"` in
`parseSessionKey()`. That value flowed through to
`resolveAndCacheIdentity()` and triggered the long-standing cross-check
`parsedSession.provider !== options.dispatchChannel` (the dispatch
surface comes from `ctx.messageProvider`, which OpenClaw emits as
`webchat` for Control UI sessions). Every Control UI turn was therefore
skipped with:

> `dispatch surface webchat does not match session provider dashboard`

— and **no recall, no retain happened** on Control UI sessions running
0.7.4 against OpenClaw 2026.5.x. The bank fragmentation went away
because the bank itself was never written to anymore.

### Changed — drop `provider` from the dashboard branch return

`parseSessionKey()` no longer sets `provider: "dashboard"` for the
4-part dashboard format. The agentId and `channel: "direct:<agentId>"`
are sufficient for `extractCanonicalDirectUser()` and `deriveBankId()`
to converge memory into the agent's primary bank, and leaving
`provider` undefined matches the existing convention documented above
the 4-part `direct:` branch:

> *"Provider is not in the sessionKey here — callers should rely on
>  ctx.messageProvider."*

### Migration

For instance owners on `@lacneu/hindsight-openclaw@0.7.4`:

1. `openclaw plugins update @lacneu/hindsight-openclaw` (or
   `openclaw plugins install @lacneu/hindsight-openclaw@0.7.5 --force`).
2. Restart the gateway container.
3. Tail the gateway log with `debug: true` enabled in the plugin
   config. Send one Control UI message. The "Skipping retain for
   session agent:<id>:dashboard:<UUID>: dispatch surface webchat does
   not match session provider dashboard" warnings must disappear; you
   should now see retain/recall activity targeting
   `<prefix>-<agent>::<agent>` instead.

### Notes

- One-line code change in `src/index.ts`. Test suite updated to assert
  `provider` is absent for the dashboard branch (mirrors the existing
  4-part `direct:` test).

## [0.7.4] - 2026-05-06

### Fixed — Control UI sessions fragmenting memory banks on OpenClaw 2026.5.x

OpenClaw 2026.5.x emits a new sessionKey shape for Control UI WebSocket
sessions: `agent:<agentId>:dashboard:<UUID>`, where the UUID is ephemeral
per WebSocket connection. The previous `parseSessionKey()` fell through to
the empty-result branch (`parts.length < 5 return {}`) for this 4-part
format, leaving `senderId` to fall back to the raw `ctx.senderId` value
("openclaw-control-ui"). Each Control UI session was therefore retained
into a fragmented bank `<prefix>-<agent>::openclaw-control-ui`, separate
from the agent's primary DM bank `<prefix>-<agent>::<canonical>`.

`session.identityLinks` does not help here either — OpenClaw 2026.5.x does
not apply identityLinks resolution to dashboard sessions (they're treated
as operator/full-trust sessions, not user DMs).

### Added — recognize `agent:<id>:dashboard:<UUID>` sessionKey

`parseSessionKey()` now matches the 4-part dashboard format and returns
`channel: "direct:<agentId>"`, so `extractCanonicalDirectUser()` resolves
to the agentId itself. This mirrors the existing convention for the 3-part
`agent:<X>:main` shape (CLI/webchat) where the operator-talks-to-own-agent
case maps the missing user identity to the agentId.

Net effect: Control UI sessions now consolidate into the agent's primary
bank `<prefix>-<agent>::<agent>` (e.g. `alice-alice::alice`) instead
of creating an ephemeral `::openclaw-control-ui` fragment per session. This
also unifies Control UI memory with the canonical DM bank when an operator
chats with their own agent across Telegram/WhatsApp/OpenWebUI/Control UI.

### Migration

For instance owners on `@lacneu/hindsight-openclaw@0.7.3` who notice
fragmented `<prefix>-<agent>::openclaw-control-ui` banks in their Hindsight
inventory:

1. `openclaw plugins update @lacneu/hindsight-openclaw` (or
   `openclaw plugins install @lacneu/hindsight-openclaw@0.7.4 --force`).
2. Restart the gateway container.
3. The next Control UI message will retain into the agent's primary bank.
4. Optional: drop the orphan `<prefix>-<agent>::openclaw-control-ui` bank
   via the Hindsight UI (or `DELETE /v1/default/banks/<bank_id>`) since it
   contains only ephemeral test memories that were never consolidated. New
   `webchat:openclaw-control-ui` entries in `session.identityLinks` are not
   useful and can be removed — OpenClaw 2026.5.x does not consume them.

### Notes

- Patch is parsing-only in `src/index.ts`. No SDK breaking change.
- Existing tests for 3-part `main` and 4-part `direct` formats continue to
  pass unchanged.

## [0.7.3] - 2026-05-03

### Changed — migrate off legacy `before_agent_start` hook

OpenClaw 2026.5.x deprecates the `before_agent_start` agent hook in
favour of `before_model_resolve` and `before_prompt_build`. The
gateway doctor flags plugins that still register `before_agent_start`
with: *"hindsight-openclaw still uses legacy before_agent_start;
keep regression coverage on this plugin, and prefer
before_model_resolve/before_prompt_build for new work."*

In this release we **remove the `before_agent_start` registration
entirely**. The hook only did identity resolution + caching + a
diagnostic log — work that is already covered by:

- `before_dispatch` — already runs first, caches identity in
  `skipHindsightTurnBySession` for the rest of the turn pipeline.
- `before_prompt_build` — re-resolves identity (with `senderIdHint`)
  when the dispatch context wasn't enough, derives the bank id, and
  proceeds with auto-recall.

Net effect: identical observable behaviour, one fewer hook
registration, no more legacy-hook deprecation warning at boot.

### Added — `activation.onStartup: true`

Per the OpenClaw 2026.5.x manifest spec, plugins should declare an
explicit activation policy. `onStartup: true` keeps Hindsight
loaded at gateway boot — necessary for the auto-recall and
auto-retain hooks to fire on the very first turn after restart.

### Added — `description` in the manifest

`openclaw.plugin.json` now has a top-level `description` field
mirroring the package.json description, so `openclaw plugins
inspect hindsight-openclaw` and `openclaw plugins list` show
something useful.

### Added — `compat` declaration in package.json

```json
"openclaw": {
  "compat": {
    "pluginApi": ">=2026.5.0",
    "minGatewayVersion": "2026.5.0"
  }
}
```

Plus `peerDependencies.openclaw: ">=2026.5.0"`. Operators on
OpenClaw 2026.4.x must keep `@lacneu/hindsight-openclaw@0.7.2`.

### Migration

For instance owners on `@lacneu/hindsight-openclaw@0.7.2`:

1. `openclaw plugins install @lacneu/hindsight-openclaw@0.7.3 --force`
2. Restart the gateway container.
3. `openclaw plugins doctor` should no longer flag a legacy
   `before_agent_start` warning for `hindsight-openclaw`.
4. Auto-recall and auto-retain continue to fire as before — the
   removed hook had no observable side effect beyond a debug log.

### Notes

- No tests broken: identity resolution coverage was already on
  `before_prompt_build` and `before_dispatch`, not on
  `before_agent_start`.
- No config schema change.

## [0.7.2] - 2026-04-27

### Changed (BREAKING)

- **Main-session user identity is now deterministically the agentId.** On control-UI / webchat / CLI sessions (sessionKey shaped `agent:<X>:main(:...)?`), when no `senderId` is available in the hook context, the plugin now sets `senderId = agentId` instead of falling back to a static `mainSessionUser` config value or a synthetic `agent-user:<agentId>`.

  Rationale: OpenClaw's gateway shares a single token across all human operators of a Control UI deployment — there is no per-user identity to extract from the hook context. Mapping the missing senderId to the agentId means anyone chatting with `agent alice` via Control UI lands in the bank `<prefix>-alice::alice`, the same bank that Alice writes into via Telegram / WhatsApp / OpenWebUI once `session.identityLinks["alice"]` resolves her peerIds to the canonical `alice`. Net effect: cross-channel memory unification per human, without any auth multi-user requirement.

- **Regex extended to cover suffixed main sessions.** The mutation now matches `^agent:[^:]+:main(:|$)` instead of the exact `^agent:[^:]+:main$`. This covers heartbeat / cron / subagent runs that ride on top of a main session, observed on OpenClaw 2026.4.x as `agent:<X>:main:heartbeat`. Previously these slipped past the mutation and produced orphan banks shaped `<prefix>-<X>::agent-user%3A<X>`.

### Removed (BREAKING)

- **`PluginConfig.mainSessionUser` is removed.** The field no longer exists in the type, in `openclaw.plugin.json` configSchema, in `uiHints`, or in `getPluginConfig` parsing. Operators who had it set in their `openclaw.json` should `openclaw config unset plugins.entries.hindsight-openclaw.config.mainSessionUser` after upgrading. The new behavior subsumes the previous use case (`mainSessionUser: "alice"`) and works without any config knob.

### Migration

1. `openclaw plugins update @lacneu/hindsight-openclaw` on each instance.
2. `openclaw config unset plugins.entries.hindsight-openclaw.config.mainSessionUser` (optional cleanup; the key is ignored after the upgrade).
3. Restart the gateway.
4. Old orphan banks shaped `<prefix>-<X>::<old-mainSessionUser>` and `<prefix>-<X>::agent-user%3A<X>` will stop receiving new memories. Delete them via `DELETE /v1/default/banks/<urlencoded-bank-id>` once the new banks `<prefix>-<X>::<X>` start filling correctly.

### Tests

- `src/index.test.ts`: replaced the four `mainSessionUser` cases with seven new cases covering the `agentId` mapping (plain main, `main:heartbeat`, `main:cron`, `main:subagent`, preservation of an existing senderId, DM session unaffected, retryable skip on non-main webchat without senderId), plus a regression test through `getPluginConfig`.
- `src/derive-bank-id.test.ts`: replaced the `mainSessionUser unifies webchat` end-to-end case with two new cases that exercise the full `getIdentitySkipReason` → `deriveBankId` flow and prove cross-channel unification (Control UI + Telegram via identityLinks both resolve to `alice::alice`).

## [0.7.1] - 2026-04-20

### Fixed

- **`mainSessionUser` was silently ignored at runtime.** 0.7.0 added the field to the type and to the hook logic, but `getPluginConfig` — the function that normalizes the on-disk config before the hook reads it — never picked the key up. Operators who had `plugins.entries.hindsight-openclaw.config.mainSessionUser` set still saw the bank fall back to `<prefix>::agent-user:<agentId>` because the hook read `pluginConfig?.mainSessionUser === undefined`. Now the key is read and trimmed in `getPluginConfig`; empty / whitespace-only values are treated as absent.
- Test gap that let the bug through: the existing unit tests passed `pluginConfig` straight into `getIdentitySkipReason`, bypassing `getPluginConfig`. Added a regression test that round-trips through the real code path, plus a trim test.

### Changed

- `getPluginConfig` is now exported from `src/index.ts` so test code can exercise the real normalization path.

## [0.7.0] - 2026-04-19

### Changed

- **Version scheme: dropped the `-identity.<n>` prerelease suffix.** Every prior release had a hyphen in its version (`0.6.5-identity.1`, `0.6.5-identity.2`, `0.6.5-identity.3`), which is a prerelease per semver 2.0.0. OpenClaw's plugin installer refuses prereleases unless an exact version is pinned, which broke the normal upgrade flow. From 0.7.0 onward the fork follows a clean independent track (0.7.x, 0.8.x, …) so `openclaw plugins install @lacneu/hindsight-openclaw` resolves to `latest` automatically and `openclaw plugins update` works without a pinned spec.
- No functional change from 0.6.5-identity.3 — the same bug fix (`agent-user:<agentId>` mutation propagation in `resolveAndCacheIdentity`) and the same feature (`PluginConfig.mainSessionUser` for `agent:<X>:main` sessionKeys) ship under the new version.

### Migration

Existing installs pinned at `@0.6.5-identity.<n>`: run `openclaw plugins install @lacneu/hindsight-openclaw` (no `@version`) to pull 0.7.0 and rewrite `plugins.installs.hindsight-openclaw.spec` to the unpinned form. Subsequent upgrades will work with `openclaw plugins update @lacneu/hindsight-openclaw`.

## [0.6.5-identity.3] - 2026-04-19

### Fixed

- **Bug: `agent-user:<agentId>` mutation was lost before `deriveBankId`.** In `resolveAndCacheIdentity`, only the `reason` field of `getIdentitySkipReason`'s return was destructured; the mutated `resolvedCtx` (where `senderId` is promoted from `undefined`/`"anonymous"` to `agent-user:<agentId>` for CLI/webchat/main sessions when `allowCliSessions` is true) was discarded. `deriveBankId` subsequently received the pre-mutation context and computed `bankId=…::anonymous`. The fix threads the mutated ctx through and re-caches it in `sessionIdentityBySession` so downstream hooks observe the promoted identity.
- Real-world impact: on the deployment, every webchat/control-UI session with `sessionKey=agent:main:main` and `dynamicBankGranularity=["agent","user"]` was writing into `alice-main::anonymous` instead of a stable bank.

### Added

- `PluginConfig.mainSessionUser` — canonical user identifier injected when the sessionKey matches `/^agent:[^:]+:main$/` (webchat / control-UI / CLI). When set (e.g. `"alice"`), the bank resolves to `<prefix>::alice` and merges with the operator's Telegram / WhatsApp / Open WebUI banks under the same canonical. Applies only when `allowCliSessions` is true (i.e. `dynamicBankGranularity` includes `"agent"` or a static `bankId` is configured). Documented in `openclaw.plugin.json` configSchema and `types.ts`.
- `src/index.test.ts`: 3 new cases covering `mainSessionUser` for main sessions, non-application to non-main sessions, and fallback to `agent-user:<agentId>` when `mainSessionUser` is an empty string.
- `src/derive-bank-id.test.ts`: end-to-end scenario (`getIdentitySkipReason` → `deriveBankId`) proving the unified bank for webchat when `mainSessionUser` is set.

## [0.6.5-identity.2] - 2026-04-18

### Fixed

- `parseSessionKey` now recognizes the 4-part `agent:<agentId>:direct:<peer_or_canonical>` shape emitted by OpenClaw v2026.4.15+ for resolved DM sessions. Previously the parser required 5 parts (`agent:<agentId>:<provider>:<channelType>:<channelId>`) and silently returned `{}` for the new shape, which caused `extractCanonicalDirectUser` to receive `undefined` and fall back to the raw `senderId`. With the fix in place, a sessionKey produced by `session.identityLinks` (e.g. `agent:main:direct:alice`) is parsed into `channel: "direct:alice"` and `deriveBankId` uses the canonical identity as expected.
- Real-world impact: with `session.identityLinks = { alice: ["telegram:10000001", "whatsapp:+15551230001"] }`, Hindsight was producing two banks (`alice-main::%2B15551230001` and `alice-main::10000001`) instead of the unified `alice-main::alice`.

### Added

- `src/index.test.ts`: coverage for the 4-part direct sessionKey parser (raw telegram peer, WhatsApp E.164 peer, canonical identity).
- `src/derive-bank-id.test.ts`: scenarios for the v2026.4.15 format — canonical resolution, cross-channel unification (Telegram + WhatsApp), and fallback to raw peer when identityLinks is not configured.

## [0.6.5-identity.1] - 2026-04-17

### Added

- `extractCanonicalDirectUser` helper that reads the `direct:<canonical>` segment embedded in a resolved OpenClaw sessionKey.
- `deriveBankId` now prefers this canonical identity over the raw `senderId` when computing the `user` field of the bank ID. Memory is consolidated across channels for a single human user when `session.identityLinks` is configured upstream.
- Test coverage in `src/derive-bank-id.test.ts` covering:
  - Canonical user from Telegram → takes precedence over raw senderId.
  - Two channels (Telegram + Discord) with the same canonical identity share one bank.
  - Two different canonical identities get separate banks.
  - Group / cron / heartbeat sessions fall back to raw senderId (no `direct:` segment).
  - No sessionKey at all → falls back to raw senderId (no regression).
  - Pre-existing behavior without identityLinks unchanged (backwards compatible).

### Changed

- Package renamed to `@lacneu/hindsight-openclaw` (plugin id `hindsight-openclaw` unchanged — drop-in replacement).

### Base

Forked from `@vectorize-io/hindsight-openclaw` 0.6.5.
