# 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]

### 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.
