@lacneu/openclaw-knowledge 3.1.2 → 3.2.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +264 -1
  2. package/README.md +131 -0
  3. package/dist/config.d.ts +4 -0
  4. package/dist/config.js +26 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts +25 -4
  7. package/dist/index.js +295 -46
  8. package/dist/index.js.map +1 -1
  9. package/dist/jina/classifier.d.ts +55 -0
  10. package/dist/jina/classifier.js +170 -0
  11. package/dist/jina/classifier.js.map +1 -0
  12. package/dist/jina/client.d.ts +30 -0
  13. package/dist/jina/client.js +131 -0
  14. package/dist/jina/client.js.map +1 -0
  15. package/dist/jina/errors.d.ts +42 -0
  16. package/dist/jina/errors.js +113 -0
  17. package/dist/jina/errors.js.map +1 -0
  18. package/dist/jina/reranker.d.ts +34 -0
  19. package/dist/jina/reranker.js +95 -0
  20. package/dist/jina/reranker.js.map +1 -0
  21. package/dist/jina/types.d.ts +78 -0
  22. package/dist/jina/types.js +12 -0
  23. package/dist/jina/types.js.map +1 -0
  24. package/dist/pgvector.d.ts +29 -0
  25. package/dist/pgvector.js +68 -0
  26. package/dist/pgvector.js.map +1 -1
  27. package/dist/router/heuristic.d.ts +29 -0
  28. package/dist/router/heuristic.js +104 -0
  29. package/dist/router/heuristic.js.map +1 -0
  30. package/dist/router/index.d.ts +33 -0
  31. package/dist/router/index.js +94 -0
  32. package/dist/router/index.js.map +1 -0
  33. package/dist/router/labels.d.ts +33 -0
  34. package/dist/router/labels.js +67 -0
  35. package/dist/router/labels.js.map +1 -0
  36. package/dist/router/types.d.ts +23 -0
  37. package/dist/router/types.js +7 -0
  38. package/dist/router/types.js.map +1 -0
  39. package/dist/tracing/events.d.ts +83 -0
  40. package/dist/tracing/events.js +86 -0
  41. package/dist/tracing/events.js.map +1 -0
  42. package/dist/types.d.ts +57 -0
  43. package/openclaw.plugin.json +97 -4
  44. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,268 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.2.0] - 2026-05-23
11
+
12
+ ### Added — Jina-powered router (`jina.router.*`)
13
+
14
+ The plugin can now skip retrieval entirely when the user turn is clearly
15
+ not a knowledge-base question. Two operational sources of waste are
16
+ eliminated:
17
+
18
+ - **Heartbeats**, **cron**, and **memory** triggers (from
19
+ `PluginHookAgentContext.trigger`) — gated deterministically, zero
20
+ Jina cost, zero ambiguity. In observed traces these accounted for the
21
+ majority of pgvector / LightRAG / Jina rerank calls.
22
+ - **Meta-agent questions** ("what is your session id", "combien d'agents
23
+ dans cette instance") that the knowledge base can never answer.
24
+ - **CLI test pings** (`isCli && /^test|ping|hello|salut.*$/i`) when the
25
+ sender is the local CLI harness.
26
+
27
+ Two modes:
28
+
29
+ - `heuristic` (default) — zero-cost regex + trigger rules only. Safe to
30
+ enable as a first step; never crashes, never consumes Jina tokens.
31
+ - `jina-classifier` — same heuristics first, then Jina `/v1/classify`
32
+ for ambiguous queries. Supports both **zero-shot** (built-in labels)
33
+ and **few-shot** (operator-trained `classifierId`). The plugin does
34
+ NOT implement `/v1/train` — training is an out-of-band step.
35
+
36
+ The router is **fail-open** by contract: any Jina outage falls back to
37
+ `ALL` (the pre-3.2.0 behavior) and never blocks the agent.
38
+
39
+ ### Added — Jina-powered pgvector reranker (`jina.pgvectorReranker.*`)
40
+
41
+ After the cosine-similarity recall stage, results may optionally be
42
+ re-ordered by a Jina cross-encoder. This dramatically improves precision
43
+ on noisy candidate sets (the 0.36–0.41 cosine scores we observed in
44
+ production were below the practical relevance floor).
45
+
46
+ - Default model: `jina-reranker-v2-base-multilingual` (recommended for
47
+ French content; v3 is English-biased).
48
+ - Hard-coded `return_documents: false` for token economy (the plugin
49
+ already owns the source rows; only `(index, score)` is needed back).
50
+ - Hard-coded `truncate: true` so over-long chunks get clipped rather
51
+ than failing the whole batch.
52
+ - Independent cooldown counter — a Jina rerank outage does NOT trip the
53
+ router cooldown, and vice versa.
54
+ - At init, warns if `topK < pgvectorRerankerTopN × 2` (not enough recall
55
+ for the reranker to meaningfully change ordering).
56
+
57
+ ### Added — structured event tracing (`[knowledge.event]`)
58
+
59
+ Every router decision, source execution, reranker run, and cooldown
60
+ transition emits a single-line JSON event through `logger.info`, prefixed
61
+ with `[knowledge.event] `. Operators can scrape these lines into Opik,
62
+ LangFuse, or any OTLP collector without the plugin needing to depend on
63
+ a specific tracing SDK (preserves the single-`pg`-dep promise).
64
+
65
+ ### Added — modular Jina client (`src/jina/`)
66
+
67
+ A small, dependency-free HTTP client (`client.ts`) used by the classifier
68
+ and reranker. Features:
69
+
70
+ - Bearer-only auth — the API key never appears in the URL or in error
71
+ messages.
72
+ - AbortController-based 8 s timeout per request, composable with a
73
+ caller-supplied `AbortSignal`.
74
+ - Defensive JSON parsing — CDN HTML 5xx pages don't crash the plugin.
75
+ - Typed errors: `JinaAuthError` / `JinaRateLimitError` / `JinaApiError`
76
+ / `JinaNetworkError` (all extending `JinaError`).
77
+ - Error bodies truncated to 200 chars in messages, matching the existing
78
+ pattern in `embeddings.ts` / `lightrag.ts`.
79
+
80
+ ### Changed — hook handler now reads `PluginHookAgentContext`
81
+
82
+ `createBeforePromptBuildHandler` returns a handler with the canonical
83
+ SDK signature `(event, ctx?)`. `ctx.trigger` is consumed by the router
84
+ gate; other ctx fields are ignored. The change is backward-compatible —
85
+ calling the handler with no `ctx` argument keeps the pre-3.2.0 behavior.
86
+
87
+ ### Changed — three independent cooldown counters
88
+
89
+ The pre-existing 3-errors → 5-min cooldown remains shared between
90
+ pgvector and LightRAG (the "global" scope). Router and pgvector reranker
91
+ each get their own counter so a Jina outage on one path cannot stop the
92
+ other. All three are reset to zero on the first success after expiry.
93
+
94
+ ### Migration
95
+
96
+ Pre-3.2.0 configs continue to work identically — every new feature
97
+ defaults to OFF and requires explicit opt-in via the `jina.*` block.
98
+ The `jina` block as a whole is optional; omit it to keep current
99
+ behavior.
100
+
101
+ To enable the router (recommended starting point):
102
+
103
+ ```yaml
104
+ plugins:
105
+ openclaw-knowledge:
106
+ config:
107
+ jina:
108
+ apiKey: ${JINA_API_KEY}
109
+ router:
110
+ enabled: true
111
+ mode: heuristic # safe default, no Jina calls
112
+ ```
113
+
114
+ > **Operational note — `isCli` heuristic.** The CLI-trivial skip rule
115
+ > fires only when `ctx.messageProvider === "cli"`. The exact value the
116
+ > OpenClaw SDK populates depends on the channel that initiated the
117
+ > turn. Before enabling the router in production, log `ctx.messageProvider`
118
+ > for one CLI turn and verify it matches `"cli"`. If your gateway uses
119
+ > a different identifier, the safest path is to leave this branch
120
+ > dormant — meta-agent regex and trigger gating already cover the most
121
+ > wasteful traffic (heartbeats, "what is your session id?").
122
+
123
+ To enable the pgvector reranker:
124
+
125
+ ```yaml
126
+ jina:
127
+ apiKey: ${JINA_API_KEY}
128
+ pgvectorReranker:
129
+ enabled: true
130
+ # model: jina-reranker-v2-base-multilingual (default)
131
+ # topN: 5 (default)
132
+ topK: 20 # recommended ≥ rerankerTopN × 2
133
+ ```
134
+
135
+ ### Fixed during code review
136
+
137
+ Eleven pre-release issues flagged across six Codex adversarial review
138
+ passes (2026-05-23):
139
+
140
+ - **Label/Route name mismatch.** The classifier labels used the literal
141
+ `"NO_RETRIEVAL"` while the `Route` type and `isKnownRoute()` only
142
+ accepted `"NONE"`. Effect: every no-retrieval prediction silently fell
143
+ back to `ALL`, defeating the whole point of the router for the most
144
+ important class. Few-shot classifiers trained against `"NONE"` were
145
+ also unreachable. Renamed `ROUTE_NO_RETRIEVAL` → `ROUTE_NONE` with the
146
+ literal value `"NONE"`. Operators training a few-shot classifier must
147
+ use the four canonical names `NONE`, `PGVECTOR_ONLY`, `LIGHTRAG_ONLY`,
148
+ `ALL`. Pinned by a regression test.
149
+
150
+ - **Exclusive route on single-source deployment dropped retrieval.**
151
+ In a pgvector-only deployment (no LightRAG), a router decision of
152
+ `LIGHTRAG_ONLY` produced zero tasks and a silent context drop, even
153
+ though pgvector was available. Added `projectRouteOnEnabledSources()`
154
+ that falls back from `PGVECTOR_ONLY`/`LIGHTRAG_ONLY` to the available
155
+ source when the target is disabled (and to `NONE` when neither is
156
+ available). `ALL` and `NONE` remain pass-through. The router event
157
+ emitted to the log now reflects the EFFECTIVE (projected) route, not
158
+ the abstract decision. Covered by 7 unit tests + 1 e2e regression
159
+ test.
160
+
161
+ - **Jina client timeout did not cover the body read.** The internal
162
+ `clearTimeout` ran in a `finally` immediately after `fetch()` returned,
163
+ BEFORE `resp.text()`. If an upstream proxy delivered headers fast but
164
+ stalled the body stream, the response could hang indefinitely (only
165
+ the SDK's outer timeout would eventually catch it — far too long for
166
+ `before_prompt_build`). Refactored `postJson` so the single
167
+ `clearTimeout` + signal-detach happen in an outer `finally` that wraps
168
+ the entire request-AND-body cycle. Added a dedicated regression test
169
+ using a `ReadableStream` body that only completes on abort.
170
+
171
+ - **README documented the wrong no-retrieval label.** The "Adaptive
172
+ router" section listed `NO_RETRIEVAL` as one of the four routes, but
173
+ the code accepts only `NONE`. Operators following the doc to train a
174
+ few-shot classifier would have produced an unusable classifier.
175
+ Corrected to `NONE` and added an explicit reminder that few-shot
176
+ classifiers MUST be trained against the four canonical names.
177
+
178
+ - **Privacy: query preview removed from debug logs.** The original
179
+ `emitQueryPreview` logged the first 80 chars of every user query when
180
+ `logger.debug` was active. In Ataraxis-style deployments those queries
181
+ routinely carry PHI / client content / occasionally secrets, so even a
182
+ truncated preview was a leak vector. Replaced by `emitQueryFingerprint`
183
+ which logs a non-reversible SHA-256 prefix (`fp=<12 hex chars>`) and
184
+ the integer length only. Operators still get turn-level correlation
185
+ across the router event, source events, and downstream Opik traces,
186
+ without any portion of the underlying text appearing in logs.
187
+ Regression test asserts that **every word of a sensitive query is
188
+ absent** from the emitted log line.
189
+
190
+ - **Router heuristic falsely classified business questions as meta.**
191
+ The "status" trigger pattern matched any prompt ending with the word
192
+ "status", so `what is the ACME project status?` was being routed to
193
+ `NONE` (no retrieval). Anchored the relevant patterns with `^`/`$`
194
+ so only whole-prompt pings (`status?`, `system status?`, `are you
195
+ there?`) trigger the meta-skip. Business questions that happen to
196
+ mention the words remain on the retrieval path. Pinned by 4 new
197
+ regression tests.
198
+
199
+ - **Privacy: Jina error bodies could echo PHI into error logs.**
200
+ `JinaApiError.message` includes the first 200 chars of the upstream
201
+ response body. On `/v1/rerank` failures, that body can echo the user
202
+ query or a document chunk back. The previous
203
+ `logger.error(\`...— \${err.message}\`)` re-published this content in
204
+ plain text. Added `summarizeJinaError()` that returns only the error
205
+ class and HTTP status code (e.g. `JinaApiError(status=503)`), and
206
+ used it everywhere the hook handler logs an error. Body content
207
+ never reaches the log. Covered by 7 new unit tests.
208
+
209
+ - **Pgvector reranker lost the first turn after cooldown expiry.**
210
+ `maybeResetCooldown` was called AFTER the `rerankerActive` check, so
211
+ the first turn after the 5-min window expired still ran cosine-only,
212
+ even though the operator's log said `resuming`. Moved the reset
213
+ before the check. The behavior is now documented inline and the
214
+ ordering invariant is preserved by source comments — full e2e
215
+ coverage requires a pg.Pool seam that doesn't exist yet.
216
+
217
+ - **Router cooldown re-enabled retrieval for heartbeats during a Jina
218
+ outage.** `runRouterWithCooldown` used to short-circuit straight to
219
+ `ALL` once the classifier circuit opened, bypassing the zero-cost
220
+ heuristic layer entirely. Result: during a 5-minute Jina outage,
221
+ every heartbeat / cron / meta-question would resume calling
222
+ pgvector + LightRAG — the exact waste the router was designed to
223
+ block. Fixed by DOWNGRADING the router mode to `"heuristic"` during
224
+ cooldown rather than short-circuiting, so heartbeat / trigger /
225
+ meta-regex / CLI rules still apply. Pinned by an end-to-end
226
+ regression test that: trips 3 classifier errors, then sends a
227
+ heartbeat turn and asserts ZERO fetch calls (no Gemini embed, no
228
+ LightRAG, no Jina). The error counter is also no longer reset by
229
+ successful heuristic-only turns during cooldown — that would have
230
+ prematurely declared the classifier healthy.
231
+
232
+ - **Privacy: query fingerprint hash was dictionary-recoverable on
233
+ short prompts.** `emitQueryFingerprint` emitted the first 12 hex
234
+ chars of `SHA-256(query)` as a debug-only "non-reversible"
235
+ correlation key. On low-entropy prompts (the hook accepts queries as
236
+ short as 3 chars), the hash is brute-forceable offline against a
237
+ dictionary of likely prompts — so the "privacy invariant" was leaky
238
+ in exactly the deployments where it mattered most. Removed the hash
239
+ entirely. Replaced by `emitTurnMetadata(logger, ctx.runId, query.length)`
240
+ which emits the SDK's non-query-derived `runId` and a length count
241
+ only. Pinned by a regression test that asserts no query word AND no
242
+ long hex token appears in the debug line. Operators who want
243
+ cross-turn content correlation must instrument at the SDK layer with
244
+ their own keyed scheme (HMAC + deployment secret); the plugin will
245
+ not do it.
246
+
247
+ - **Telemetry: `rawCount` was post-rerank, hiding recall vs pruning.**
248
+ When the pgvector reranker was active, `PgvectorEvent.rawCount`
249
+ reflected the post-rerank truncated size (`topN`), not the number of
250
+ candidates pgvector actually returned. Operators relying on the
251
+ event to monitor recall would see the wrong value. Fixed by carrying
252
+ the pre-rerank count as a dedicated `rawCount: number` field on the
253
+ internal `PgvectorSourceResult`, captured BEFORE `rerankPgvectorResults`
254
+ runs. `rerankedCount` continues to reflect the post-truncation final
255
+ size, so operators can compute pruning = `rawCount − rerankedCount`.
256
+
257
+ ### Test coverage
258
+
259
+ - 56 pre-existing tests preserved (no behavioral regression on legacy
260
+ paths).
261
+ - 133 new tests covering: Jina client error mapping (incl. body-stall
262
+ abort regression), `summarizeJinaError` privacy contract, classifier
263
+ defensive parsing across 4 known response shapes, reranker defensive
264
+ parsing, router heuristics across triggers / meta-regex / CLI / keyword
265
+ fast-paths (incl. business-status false-positive regression), router
266
+ orchestration in all fail-open scenarios incl. heuristic preservation
267
+ during classifier cooldown, route projection onto enabled sources,
268
+ pgvector reranker integration, turn-metadata tracing via SDK runId
269
+ (regression-pinned: no query content, no hash, in logs).
270
+ - Total: 189 tests, all green.
271
+
10
272
  ### TODO — full migration to `kind: "context-engine"` (deferred)
11
273
 
12
274
  The OpenClaw doctor classifies the current `before_prompt_build`-only
@@ -208,7 +470,8 @@ For instance owners on `@lacneu/openclaw-knowledge@3.1.0` or `3.1.1`:
208
470
  - Release workflow: creates GitHub Release with tarball on tag push.
209
471
  - Architecture, lifecycle, and sequence diagrams in `schemas/`.
210
472
 
211
- [Unreleased]: https://github.com/OlivierNeu/openclaw-knowledge-plugin/compare/v3.1.0...HEAD
473
+ [Unreleased]: https://github.com/OlivierNeu/openclaw-knowledge-plugin/compare/v3.2.0...HEAD
474
+ [3.2.0]: https://github.com/OlivierNeu/openclaw-knowledge-plugin/compare/v3.1.2...v3.2.0
212
475
  [3.1.0]: https://github.com/OlivierNeu/openclaw-knowledge-plugin/compare/v1.2.0...v3.1.0
213
476
  [1.2.0]: https://github.com/OlivierNeu/openclaw-knowledge-plugin/compare/v1.1.2...v1.2.0
214
477
  [1.1.2]: https://github.com/OlivierNeu/openclaw-knowledge-plugin/compare/v1.1.1...v1.1.2
package/README.md CHANGED
@@ -183,6 +183,14 @@ openclaw gateway restart
183
183
  | `lightragQueryMode` | string | `"hybrid"` | Query mode: `naive`, `local`, `global`, `hybrid` |
184
184
  | `lightragMaxChars` | number | `4000` | Character budget for LightRAG context |
185
185
  | `lightragEnabled` | boolean | `true` if `lightragUrl` set | Disable LightRAG while keeping pgvector |
186
+ | **Jina integration (optional, v3.2.0+)** | | | |
187
+ | `jina.apiKey` | string | — | Jina API key shared by router & reranker (supports `${ENV_VAR}`) |
188
+ | `jina.router.enabled` | boolean | `false` | Adaptive routing (skip irrelevant retrievals) |
189
+ | `jina.router.mode` | string | `"heuristic"` | `heuristic` (zero-cost) or `jina-classifier` (heuristic + Jina fallback) |
190
+ | `jina.router.classifierId` | string | — | Optional pre-trained few-shot classifier ID |
191
+ | `jina.pgvectorReranker.enabled` | boolean | `false` | Cross-encoder re-ordering of pgvector results |
192
+ | `jina.pgvectorReranker.model` | string | `"jina-reranker-v2-base-multilingual"` | Reranker model |
193
+ | `jina.pgvectorReranker.topN` | number | `5` | Max results returned after rerank |
186
194
 
187
195
  ### LightRAG query modes
188
196
 
@@ -195,6 +203,129 @@ openclaw gateway restart
195
203
 
196
204
  ---
197
205
 
206
+ ## Jina integration (v3.2.0+)
207
+
208
+ The plugin can optionally call the [Jina AI](https://jina.ai/) cloud API
209
+ to make two improvements:
210
+
211
+ ### Adaptive router — skip retrieval when it can't help
212
+
213
+ By default the plugin queries every configured source on every turn.
214
+ That's wasteful on heartbeats, cron-driven turns, and meta-questions
215
+ ("what is your session id?") that no knowledge base can answer.
216
+
217
+ Enabling the router introduces a gating step before the sources are
218
+ called:
219
+
220
+ 1. **Zero-cost heuristics first.** Skips on `PluginHookAgentContext.trigger ∈ {heartbeat, cron, memory}`, on
221
+ meta-agent regex matches, and on CLI test pings from
222
+ `messageProvider="cli"`. Keyword fast-paths route obvious factual
223
+ lookups to pgvector and obvious multi-hop questions to LightRAG.
224
+ 2. **Jina Classifier fallback** (only in `mode: "jina-classifier"`).
225
+ When the heuristics are ambiguous, the plugin calls
226
+ `POST /v1/classify` to pick one of four routes:
227
+ `NONE`, `PGVECTOR_ONLY`, `LIGHTRAG_ONLY`, or `ALL`. **Few-shot
228
+ classifiers MUST be trained against these exact canonical names** —
229
+ any other label is silently rejected and falls back to `ALL`.
230
+ Supports **zero-shot** (built-in labels, no training required) and
231
+ **few-shot** (pre-trained classifier_id, ~50 tokens per call vs ~200
232
+ for zero-shot).
233
+ 3. **Fail-open.** Any Jina outage degrades silently to `ALL` — the
234
+ pre-3.2.0 behavior. Routing never blocks the agent.
235
+
236
+ Enable in `openclaw.json`:
237
+
238
+ ```json
239
+ "jina": {
240
+ "apiKey": "${JINA_API_KEY}",
241
+ "router": {
242
+ "enabled": true,
243
+ "mode": "heuristic"
244
+ }
245
+ }
246
+ ```
247
+
248
+ Then switch to `mode: "jina-classifier"` once you're comfortable.
249
+
250
+ ### Pgvector reranker — re-order vector results by relevance
251
+
252
+ Vector cosine similarity is great recall but mediocre precision: the
253
+ top-K candidates are often noisy. A cross-encoder reranker re-scores
254
+ each (query, candidate) pair as a pair, which is much more accurate
255
+ than independent embeddings — at the cost of one Jina call per turn.
256
+
257
+ Enable in `openclaw.json`:
258
+
259
+ ```json
260
+ "jina": {
261
+ "apiKey": "${JINA_API_KEY}",
262
+ "pgvectorReranker": {
263
+ "enabled": true
264
+ }
265
+ },
266
+ "topK": 20
267
+ ```
268
+
269
+ Recommended `topK ≥ topN × 2` so the reranker has room to re-order.
270
+ The plugin warns at init if the ratio is too tight.
271
+
272
+ **Model default:** `jina-reranker-v2-base-multilingual` — best for
273
+ French content. v3 is larger (131K context) but English-biased. Switch
274
+ via `jina.pgvectorReranker.model`.
275
+
276
+ ### Observability
277
+
278
+ Every router decision, source execution, and cooldown transition emits
279
+ a structured event line:
280
+
281
+ ```
282
+ [knowledge.event] {"type":"router","route":"PGVECTOR_ONLY","reason":"heuristic_keyword","score":null,"queryLength":42,"trigger":"user"}
283
+ [knowledge.event] {"type":"pgvector","collections":["knowledge_olivier"],"rawCount":5,"rerankedCount":5,"topScore":0.78,"durationMs":124}
284
+ [knowledge.event] {"type":"cooldown","scope":"router","consecutiveErrors":3}
285
+ ```
286
+
287
+ These lines can be scraped by Opik, LangFuse, or any OTLP collector
288
+ without the plugin depending on a specific tracing SDK.
289
+
290
+ #### Privacy invariant
291
+
292
+ The plugin **never** logs any portion of the raw user query, any
293
+ retrieved chunk text, any hash of them, or any other potentially-PII
294
+ payload. When `logger.debug` is enabled, an extra correlation line
295
+ carries the SDK-provided turn identifier only:
296
+
297
+ ```
298
+ [knowledge.event] turn.metadata runId=01HF... qlen=42
299
+ ```
300
+
301
+ `runId` comes from `PluginHookAgentContext.runId` (the OpenClaw SDK's
302
+ non-query-derived turn identifier). `qlen` is just a character count.
303
+ The plugin deliberately does NOT publish any hash of the query — a
304
+ deterministic SHA-256 prefix of a 3–10 character prompt is
305
+ dictionary-recoverable offline, which would defeat the invariant on
306
+ exactly the deployments where it matters most (PHI / regulated
307
+ content).
308
+
309
+ Operators who need CONTENT correlation across turns (e.g. "this user
310
+ asked the same question twice") must instrument at the SDK layer with
311
+ a keyed HMAC and a deployment-side secret; the plugin will not do it
312
+ for them.
313
+
314
+ ### Cooldown isolation
315
+
316
+ The pre-existing 3-errors → 5-min cooldown is now split into three
317
+ independent counters:
318
+
319
+ | Scope | Triggers cooldown |
320
+ |-------|-------------------|
321
+ | `global` | Both pgvector AND LightRAG fail in the same turn |
322
+ | `router` | Repeated Jina classifier errors |
323
+ | `pgvector_reranker` | Repeated Jina rerank errors |
324
+
325
+ A Jina outage on one path no longer affects the others.
326
+
327
+ ---
328
+
198
329
  ## Data model
199
330
 
200
331
  ### pgvector: `knowledge_vectors` table
package/dist/config.d.ts CHANGED
@@ -10,5 +10,9 @@ export declare function resolveEnv<T>(value: T): T;
10
10
  * Apply defaults and env substitution to the raw plugin config. A source is
11
11
  * enabled when its credentials are present, unless the user explicitly toggles
12
12
  * `pgvectorEnabled`/`lightragEnabled` off.
13
+ *
14
+ * Jina-derived features (router + pgvector reranker) follow the same
15
+ * "default to off" discipline: nothing activates without an explicit opt-in,
16
+ * so pre-3.2.0 configs continue to work identically.
13
17
  */
14
18
  export declare function resolveConfig(cfg?: KnowledgePluginConfig): ResolvedKnowledgeConfig;
package/dist/config.js CHANGED
@@ -22,16 +22,28 @@ const DEFAULT_SCORE_THRESHOLD = 0.3;
22
22
  const DEFAULT_MAX_INJECT_CHARS = 4000;
23
23
  const DEFAULT_LIGHTRAG_MODE = "hybrid";
24
24
  const DEFAULT_LIGHTRAG_MAX_CHARS = 4000;
25
+ const DEFAULT_ROUTER_MODE = "heuristic";
26
+ const DEFAULT_RERANKER_MODEL = "jina-reranker-v2-base-multilingual";
27
+ const DEFAULT_RERANKER_TOP_N = 5;
25
28
  /**
26
29
  * Apply defaults and env substitution to the raw plugin config. A source is
27
30
  * enabled when its credentials are present, unless the user explicitly toggles
28
31
  * `pgvectorEnabled`/`lightragEnabled` off.
32
+ *
33
+ * Jina-derived features (router + pgvector reranker) follow the same
34
+ * "default to off" discipline: nothing activates without an explicit opt-in,
35
+ * so pre-3.2.0 configs continue to work identically.
29
36
  */
30
37
  export function resolveConfig(cfg = {}) {
31
38
  const geminiApiKey = resolveEnv(cfg.geminiApiKey ?? "");
32
39
  const postgresUrl = resolveEnv(cfg.postgresUrl ?? DEFAULT_POSTGRES_URL);
33
40
  const lightragUrl = resolveEnv(cfg.lightragUrl ?? "");
34
41
  const lightragApiKey = resolveEnv(cfg.lightragApiKey ?? "");
42
+ const jina = (cfg.jina ?? {});
43
+ const router = (jina.router ?? {});
44
+ const reranker = (jina.pgvectorReranker ?? {});
45
+ const jinaApiKey = resolveEnv(jina.apiKey ?? "");
46
+ const routerClassifierId = resolveEnv(router.classifierId ?? "");
35
47
  return {
36
48
  enabled: cfg.enabled !== false,
37
49
  geminiApiKey,
@@ -46,6 +58,20 @@ export function resolveConfig(cfg = {}) {
46
58
  lightragQueryMode: cfg.lightragQueryMode ?? DEFAULT_LIGHTRAG_MODE,
47
59
  lightragMaxChars: cfg.lightragMaxChars ?? DEFAULT_LIGHTRAG_MAX_CHARS,
48
60
  lightragEnabled: cfg.lightragEnabled !== false && Boolean(lightragUrl),
61
+ // Jina shared key (used by router and/or reranker)
62
+ jinaApiKey,
63
+ // Router — disabled by default, even with a Jina key present, so
64
+ // operators must opt in explicitly. "heuristic" mode is the safest
65
+ // entry point: zero cost, deterministic.
66
+ routerEnabled: router.enabled === true,
67
+ routerMode: router.mode ?? DEFAULT_ROUTER_MODE,
68
+ routerClassifierId,
69
+ // Pgvector reranker — disabled by default. Requires both the toggle
70
+ // and a Jina key to actually activate at runtime (the handler checks
71
+ // this combination before calling).
72
+ pgvectorRerankerEnabled: reranker.enabled === true && Boolean(jinaApiKey),
73
+ pgvectorRerankerModel: reranker.model ?? DEFAULT_RERANKER_MODEL,
74
+ pgvectorRerankerTopN: reranker.topN ?? DEFAULT_RERANKER_TOP_N,
49
75
  };
50
76
  }
51
77
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,EAAE;AACF,2EAA2E;AAC3E,6DAA6D;AAQ7D;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAI,KAAQ;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE;QACvD,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACjC,CAAC,CAAiB,CAAC;AACrB,CAAC;AAED,MAAM,oBAAoB,GAAG,kDAAkD,CAAC;AAChF,MAAM,mBAAmB,GAAG,CAAC,mBAAmB,CAAC,CAAC;AAClD,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,uBAAuB,GAAG,GAAG,CAAC;AACpC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AACtC,MAAM,qBAAqB,GAAsB,QAAQ,CAAC;AAC1D,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAExC;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,MAA6B,EAAE;IAE/B,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC,WAAW,IAAI,oBAAoB,CAAC,CAAC;IACxE,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,cAAc,GAAG,UAAU,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC;IAE5D,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,KAAK;QAC9B,YAAY;QACZ,WAAW;QACX,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,mBAAmB;QACnD,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,aAAa;QAC/B,cAAc,EAAE,GAAG,CAAC,cAAc,IAAI,uBAAuB;QAC7D,cAAc,EAAE,GAAG,CAAC,cAAc,IAAI,wBAAwB;QAC9D,eAAe,EAAE,GAAG,CAAC,eAAe,KAAK,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC;QACvE,WAAW;QACX,cAAc;QACd,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,IAAI,qBAAqB;QACjE,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,IAAI,0BAA0B;QACpE,eAAe,EAAE,GAAG,CAAC,eAAe,KAAK,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;KACvE,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { OpenClawPluginApi, PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
2
- import type { BeforePromptBuildEvent, BeforePromptBuildResult, PgPoolLike, ResolvedKnowledgeConfig } from "./types.js";
2
+ import type { Route } from "./router/types.js";
3
+ import type { BeforePromptBuildEvent, BeforePromptBuildResult, PgPoolLike, PluginHookAgentContext, ResolvedKnowledgeConfig } from "./types.js";
3
4
  export { resolveEnv, resolveConfig } from "./config.js";
4
5
  export { embedQuery } from "./embeddings.js";
5
- export { searchCollection, formatPgvectorResults } from "./pgvector.js";
6
+ export { searchCollection, formatPgvectorResults, rerankPgvectorResults, } from "./pgvector.js";
6
7
  export { queryLightRAG, truncateLightRAG, formatLightRAGResults } from "./lightrag.js";
7
- export type { BeforePromptBuildEvent, BeforePromptBuildResult, KnowledgePluginConfig, LightRAGQueryMode, PgPoolLike, PgvectorResult, PgvectorRow, PromptContentPart, PromptMessage, ResolvedKnowledgeConfig, } from "./types.js";
8
+ export { decideRoute } from "./router/index.js";
9
+ export type { BeforePromptBuildEvent, BeforePromptBuildResult, JinaPluginConfig, KnowledgePluginConfig, LightRAGQueryMode, PgPoolLike, PgvectorResult, PgvectorRerankerPluginConfig, PgvectorRow, PluginHookAgentContext, PromptContentPart, PromptMessage, ResolvedKnowledgeConfig, RouterPluginConfig, } from "./types.js";
8
10
  interface HookHandlerDeps {
9
11
  config: ResolvedKnowledgeConfig;
10
12
  pool: PgPoolLike | null;
@@ -14,7 +16,26 @@ interface HookHandlerDeps {
14
16
  * Build the `before_prompt_build` handler bound to a specific plugin state.
15
17
  * Kept as a pure factory so the handler can be unit-tested with fake deps.
16
18
  */
17
- export declare function createBeforePromptBuildHandler(deps: HookHandlerDeps): (event: BeforePromptBuildEvent) => Promise<BeforePromptBuildResult | undefined>;
19
+ export declare function createBeforePromptBuildHandler(deps: HookHandlerDeps): (event: BeforePromptBuildEvent, ctx?: PluginHookAgentContext) => Promise<BeforePromptBuildResult | undefined>;
20
+ /**
21
+ * Project a router decision onto the set of sources that are actually
22
+ * enabled in this deployment. This prevents "silent empty retrieval"
23
+ * when, for example, a pgvector-only deployment is told to use
24
+ * `LIGHTRAG_ONLY` for a multi-hop question — without this projection the
25
+ * task list would be empty and the agent would lose context that
26
+ * pgvector could have provided.
27
+ *
28
+ * Rules:
29
+ * - `NONE` → `NONE` (the router deliberately wants no retrieval).
30
+ * - `ALL` → `ALL` (downstream `shouldUseX` already skips disabled sources).
31
+ * - `PGVECTOR_ONLY` + pgvector disabled:
32
+ * - LightRAG available → `LIGHTRAG_ONLY` (best effort)
33
+ * - neither available → `NONE` (caller short-circuits)
34
+ * - `LIGHTRAG_ONLY` + LightRAG disabled: symmetric.
35
+ *
36
+ * Exported for unit testing.
37
+ */
38
+ export declare function projectRouteOnEnabledSources(route: Route, pgvectorEnabled: boolean, lightragEnabled: boolean): Route;
18
39
  /**
19
40
  * Register the plugin against a minimal shape-compatible subset of the
20
41
  * OpenClaw plugin API. Returns nothing; side effects are setting a hook and