@hilbras/remembra 3.5.0 → 3.7.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.
@@ -0,0 +1,120 @@
1
+ # Observability
2
+
3
+ Structured logging, Prometheus metrics, health/readiness, and alert rules.
4
+ Added in **3.7.0** (Phase 7 of the deep audit).
5
+
6
+ ## Structured logging
7
+
8
+ Every server-side event goes through one logger (`src/log.ts`) and lands on
9
+ **stderr** — stdout stays reserved for MCP stdio framing and CLI output.
10
+
11
+ | | |
12
+ |---|---|
13
+ | **Format selection** | `REMEMBRA_LOG=json` or `REMEMBRA_LOG=text` forces a format. Unset → **auto**: JSON when stderr is piped/redirected (containers, CI, log shippers), text on a TTY. |
14
+ | **JSON shape** | `{"ts","level","event","msg",...fields}` — one object per line, ready for any log shipper. |
15
+ | **Text shape** | The human message verbatim (the exact strings Remembra always printed). |
16
+ | **Hygiene** | Raw search query text and the storage root path are included **only under `REMEMBRA_DEBUG=1`** (Phase 2 log-hygiene rule). |
17
+
18
+ Example JSON events:
19
+
20
+ ```json
21
+ {"ts":"2026-09-23T05:53:50.366Z","level":"info","event":"search","terms":2,"results":2,"duration_ms":1}
22
+ {"ts":"2026-09-23T05:53:50.314Z","level":"info","event":"http_listening","msg":"Remembra HTTP API listening on 0.0.0.0:8787 (auth required)","port":8787,"host":"0.0.0.0","auth":true}
23
+ {"ts":"…","level":"warn","event":"memory_parse_skipped","file":"bad.md","reason":"missing frontmatter"}
24
+ ```
25
+
26
+ Event names in the wild: `http_listening`, `mcp_listening`, `search`,
27
+ `shutdown`, `crash_recovery`, `memory_parse_skipped`, `embedding_failed`,
28
+ `touch_failed`, `merge_llm_failed`, `decay_failed`.
29
+
30
+ ## Metrics — `GET /metrics`
31
+
32
+ Prometheus text exposition (format 0.0.4), served by the **same binary**.
33
+
34
+ **Auth:** the endpoint sits *after* the API-key check — keyed (including
35
+ public) deployments require `x-api-key` (or `Authorization: Bearer`); an
36
+ unkeyed server is loopback-only by the default-deny rule anyway. `/health`
37
+ stays exempt so unauthenticated readiness probes keep working.
38
+
39
+ | Series | Type | Labels | Meaning |
40
+ |---|---|---|---|
41
+ | `remembra_http_requests_total` | counter | `route`, `method`, `status` | Requests. `route` is a fixed low-cardinality label (`health`/`metrics`/`memories`/`search`/`digest`/`maintain`/`memory_item`/`other`) — never the raw path. |
42
+ | `remembra_http_request_duration_seconds` | histogram | `route` | Request latency. |
43
+ | `remembra_errors_total` | counter | `code`, `transport` | Classified errors (`http`/`mcp`). Codes: the [error codes](architecture.md#error-classification-audit-phase-2) plus `INVALID_INPUT`, `PAYLOAD_TOO_LARGE`, `INTERNAL`. |
44
+ | `remembra_searches_total` | counter | — | `memory_search` invocations. |
45
+ | `remembra_search_duration_seconds` | histogram | — | Search latency (embed + walk + score + rank). |
46
+ | `remembra_stores_total` | counter | — | `memory_store` invocations. |
47
+ | `remembra_digests_total` | counter | — | Completed session-digest runs. |
48
+ | `remembra_digest_items_total` | counter | `result` | Digest items: `stored` / `skipped` / `merged`. |
49
+ | `remembra_digest_duration_seconds` | histogram | — | Digest run latency (includes lock queueing). |
50
+ | `remembra_cache_events_total` | counter | `result` | Parse-cache probes: `hit` / `miss`. |
51
+ | `remembra_cache_entries` | gauge | — | Parse-cache entries currently held. |
52
+ | `remembra_info` | gauge | `version` | Build info, always `1`. |
53
+
54
+ ### Scrape config
55
+
56
+ ```yaml
57
+ scrape_configs:
58
+ - job_name: remembra
59
+ metrics_path: /metrics
60
+ static_configs:
61
+ - targets: ["127.0.0.1:8787"]
62
+ # Required when REMEMBRA_API_KEY is set:
63
+ headers:
64
+ x-api-key: YOUR_KEY
65
+ ```
66
+
67
+ ## Health — `GET /health`
68
+
69
+ No auth (readiness probes carry no key). Liveness **and** readiness in one:
70
+
71
+ - `200 {"status":"ok", "version", "uptime_s", "storage":"ok", "cache":{...}}`
72
+ when a full store read succeeds.
73
+ - `503 {"status":"unready", "storage":"<ERROR_CODE>", ...}` when storage cannot
74
+ be read — the probe **fails instead of lying**.
75
+
76
+ ## Alerting
77
+
78
+ Remembra ships the *substrate* (counters + scrape endpoint), not a notifier —
79
+ alerting is Prometheus/Grafana's job. Ready-to-paste rules:
80
+
81
+ ```yaml
82
+ groups:
83
+ - name: remembra
84
+ rules:
85
+ - alert: RemembraDown
86
+ expr: up{job="remembra"} == 0
87
+ for: 2m
88
+ annotations:
89
+ summary: Remembra target unreachable
90
+
91
+ - alert: RemembraHighInternalErrorRate
92
+ expr: sum(rate(remembra_errors_total{code=~"IO_ERROR|INTERNAL|LLM_ERROR"}[5m])) > 0.1
93
+ for: 5m
94
+ annotations:
95
+ summary: >-
96
+ {{ $value | humanize }} internal errors/s — storage or provider
97
+ failures (client 4xx are excluded on purpose)
98
+
99
+ - alert: RemembraReadinessFailing
100
+ expr: increase(remembra_http_requests_total{route="health",status="503"}[5m]) > 0
101
+ annotations:
102
+ summary: /health returning 503 (storage unready)
103
+
104
+ - alert: RemembraLockContention
105
+ expr: increase(remembra_errors_total{code="LOCK_TIMEOUT"}[15m]) > 5
106
+ annotations:
107
+ summary: Repeated lock timeouts — concurrent writers starving
108
+
109
+ - alert: RemembraSlowSearches
110
+ expr: histogram_quantile(0.95, rate(remembra_search_duration_seconds_bucket[5m])) > 1
111
+ annotations:
112
+ summary: Search p95 above 1s
113
+
114
+ - alert: RemembraCacheThrash
115
+ expr: |
116
+ sum(rate(remembra_cache_events_total{result="miss"}[15m]))
117
+ / sum(rate(remembra_cache_events_total[15m])) > 0.5
118
+ annotations:
119
+ summary: Parse-cache hit ratio below 50% — store churn or capacity
120
+ ```
package/docs/providers.md CHANGED
@@ -84,8 +84,8 @@ With `REMEMBRA_EMBEDDINGS=openai|ollama`:
84
84
  frontmatter (`embedding: [...]`) — computed once, never re-embedded.
85
85
  - **On search**: the query is embedded and **cosine similarity becomes the
86
86
  primary ranking signal**. Importance and recency remain small modifiers.
87
- - **Gates stay absolute**: `role` memories always surface, and memories from
88
- other scopes are never returned, no matter how similar.
87
+ - **Gates stay absolute**: in-scope `role` memories always surface, and
88
+ memories from other scopes are never returned, no matter how similar.
89
89
  - **Memories without vectors** (stored while embeddings were off) fall back
90
90
  to keyword matching.
91
91
 
package/docs/security.md CHANGED
@@ -23,13 +23,16 @@ and how to deploy safely.
23
23
 
24
24
  ### Role memories are instructions — the prompt-injection surface
25
25
 
26
- `role` memories always surface (+1000) and are meant to be **followed**, not
27
- merely recalled. That is the feature — and the risk:
26
+ `role` memories always surface (+1000) **within their scope** and are meant
27
+ to be **followed**, not merely recalled. That is the feature — and the risk:
28
28
 
29
29
  - **Via MCP**: whoever can call `memory_store` in your session is already the
30
30
  agent you're running. No additional boundary is crossed.
31
31
  - **Via HTTP**: anyone with your API key can plant a global `role` that every
32
32
  future session will receive as an instruction.
33
+ - **Cross-project**: roles scoped to another project do *not* surface in your
34
+ searches (isolation, enforced since 3.6.0) — the residual vector is a
35
+ `global` role, which reaches every scope by design.
33
36
 
34
37
  **Rules:**
35
38
  1. Never expose HTTP without `REMEMBRA_API_KEY` (enforced — see below).
@@ -52,11 +55,12 @@ Retrieved memories should be treated as **data with provenance**, not commands
52
55
  | **Body size limit** | 10 MiB default (`REMEMBRA_MAX_BODY`), `413` on excess — pre-checks `Content-Length` and enforces while streaming |
53
56
  | **Digest validation** | `DigestInput` Zod schema on both MCP and HTTP paths |
54
57
  | **Atomic writes** | temp file + `rename()` (POSIX-atomic) — no half-written memories after a crash |
55
- | **Advisory locking** | `<root>/.remembra.lock` (`O_EXCL`) + in-process FIFO — cross-process writes serialize; stale locks (dead pid / older than `REMEMBRA_LOCK_STALE_MS`) are stolen; waiters fail with typed `LOCK_TIMEOUT` (HTTP 423) |
58
+ | **Advisory locking** | `<root>/.remembra.lock` (`O_EXCL`) + in-process FIFO — cross-process writes serialize; stale locks (dead pid / older than `REMEMBRA_LOCK_STALE_MS`) are stolen; a fresh lock with this process's own pid is treated as a live sibling instance and waited on; waiters fail with typed `LOCK_TIMEOUT` (HTTP 423) |
56
59
  | **Crash recovery** | one-time pass on first access: removes orphaned `*.tmp`, reconciles ids left in both active+archived trees by an interrupted archive/revive |
57
60
  | **Structured errors** | every actionable failure has a stable code (`INVALID_INPUT`, `LOCK_TIMEOUT`, `LLM_ERROR`, …) mapped to HTTP statuses / MCP `[CODE]` prefixes |
58
61
  | **ID collisions** | 12-hex IDs (2⁴⁸) + existence check on store |
59
62
  | **Content-Length** | Set on every response |
63
+ | **Metrics auth (3.7.0)** | `GET /metrics` sits *after* the API-key check — counters and latencies never leak without the key (`/health` stays exempt for readiness probes) |
60
64
 
61
65
  ## Deployment checklist
62
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilbras/remembra",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "description": "External memory for AI assistants — remember facts, decisions, roles and history across sessions. MCP server for OpenCode, Claude Code, Cline, Kimi Code and more.",
5
5
  "type": "module",
6
6
  "bin": {