@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.
- package/CHANGELOG.md +104 -0
- package/README.md +1 -0
- package/dist/backend.d.ts +5 -0
- package/dist/errors.d.ts +2 -7
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/http.d.ts +2 -1
- package/dist/http.js +59 -4
- package/dist/http.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +19 -0
- package/dist/log.js +28 -0
- package/dist/log.js.map +1 -0
- package/dist/metrics.d.ts +22 -0
- package/dist/metrics.js +117 -0
- package/dist/metrics.js.map +1 -0
- package/dist/retrieval.d.ts +2 -1
- package/dist/retrieval.js +7 -2
- package/dist/retrieval.js.map +1 -1
- package/dist/service.d.ts +16 -0
- package/dist/service.js +61 -6
- package/dist/service.js.map +1 -1
- package/dist/store.js +17 -5
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +6 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/docs/architecture.md +4 -1
- package/docs/clients.md +3 -1
- package/docs/memory-model.md +9 -3
- package/docs/observability.md +120 -0
- package/docs/providers.md +2 -2
- package/docs/security.md +7 -3
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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.
|
|
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": {
|