@xdarkicex/openclaw-memory-libravdb 1.3.5
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/README.md +46 -0
- package/docs/README.md +14 -0
- package/docs/architecture-decisions/README.md +6 -0
- package/docs/architecture-decisions/adr-001-onnx-over-ollama.md +21 -0
- package/docs/architecture-decisions/adr-002-libravdb-over-lancedb.md +19 -0
- package/docs/architecture-decisions/adr-003-convex-gating-over-threshold.md +27 -0
- package/docs/architecture-decisions/adr-004-sidecar-over-native-ts.md +21 -0
- package/docs/architecture.md +188 -0
- package/docs/contributing.md +76 -0
- package/docs/dependencies.md +38 -0
- package/docs/embedding-profiles.md +42 -0
- package/docs/gating.md +329 -0
- package/docs/implementation.md +381 -0
- package/docs/installation.md +272 -0
- package/docs/mathematics.md +695 -0
- package/docs/models.md +63 -0
- package/docs/problem.md +64 -0
- package/docs/security.md +86 -0
- package/openclaw.plugin.json +84 -0
- package/package.json +41 -0
- package/scripts/build-sidecar.sh +30 -0
- package/scripts/postinstall.js +169 -0
- package/scripts/setup.sh +20 -0
- package/scripts/setup.ts +505 -0
- package/scripts/sidecar-release.d.ts +4 -0
- package/scripts/sidecar-release.js +17 -0
- package/sidecar/cmd/inspect_onnx/main.go +105 -0
- package/sidecar/compact/gate.go +273 -0
- package/sidecar/compact/gate_test.go +85 -0
- package/sidecar/compact/summarize.go +345 -0
- package/sidecar/compact/summarize_test.go +319 -0
- package/sidecar/compact/tokens.go +11 -0
- package/sidecar/config/config.go +119 -0
- package/sidecar/config/config_test.go +75 -0
- package/sidecar/embed/engine.go +696 -0
- package/sidecar/embed/engine_test.go +349 -0
- package/sidecar/embed/matryoshka.go +93 -0
- package/sidecar/embed/matryoshka_test.go +150 -0
- package/sidecar/embed/onnx_local.go +319 -0
- package/sidecar/embed/onnx_local_test.go +159 -0
- package/sidecar/embed/profile_contract_test.go +71 -0
- package/sidecar/embed/profile_eval_test.go +923 -0
- package/sidecar/embed/profiles.go +39 -0
- package/sidecar/go.mod +21 -0
- package/sidecar/go.sum +30 -0
- package/sidecar/health/check.go +33 -0
- package/sidecar/health/check_test.go +55 -0
- package/sidecar/main.go +151 -0
- package/sidecar/model/encoder.go +222 -0
- package/sidecar/model/registry.go +262 -0
- package/sidecar/model/registry_test.go +102 -0
- package/sidecar/model/seq2seq.go +133 -0
- package/sidecar/server/rpc.go +343 -0
- package/sidecar/server/rpc_test.go +350 -0
- package/sidecar/server/transport.go +160 -0
- package/sidecar/store/libravdb.go +676 -0
- package/sidecar/store/libravdb_test.go +472 -0
- package/sidecar/summarize/engine.go +360 -0
- package/sidecar/summarize/engine_test.go +148 -0
- package/sidecar/summarize/onnx_local.go +494 -0
- package/sidecar/summarize/onnx_local_test.go +48 -0
- package/sidecar/summarize/profiles.go +52 -0
- package/sidecar/summarize/tokenizer.go +13 -0
- package/sidecar/summarize/tokenizer_hf.go +76 -0
- package/sidecar/summarize/util.go +13 -0
- package/src/cli.ts +205 -0
- package/src/context-engine.ts +195 -0
- package/src/index.ts +27 -0
- package/src/memory-provider.ts +24 -0
- package/src/openclaw-plugin-sdk.d.ts +53 -0
- package/src/plugin-runtime.ts +67 -0
- package/src/recall-cache.ts +34 -0
- package/src/recall-utils.ts +22 -0
- package/src/rpc.ts +84 -0
- package/src/scoring.ts +58 -0
- package/src/sidecar.ts +506 -0
- package/src/tokens.ts +36 -0
- package/src/types.ts +146 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tests.json +12 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
package server
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/compact"
|
|
10
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/embed"
|
|
11
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/health"
|
|
12
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/store"
|
|
13
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/summarize"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
type HandlerFn func(context.Context, any) (any, error)
|
|
17
|
+
|
|
18
|
+
type Server struct {
|
|
19
|
+
Embedder embed.Embedder
|
|
20
|
+
Extractive summarize.Summarizer
|
|
21
|
+
Abstractive summarize.Summarizer
|
|
22
|
+
Store *store.Store
|
|
23
|
+
Gating compact.GatingConfig
|
|
24
|
+
methods map[string]HandlerFn
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func New(embedder embed.Embedder, extractive summarize.Summarizer, abstractive summarize.Summarizer, st *store.Store, gating compact.GatingConfig) *Server {
|
|
28
|
+
s := &Server{
|
|
29
|
+
Embedder: embedder,
|
|
30
|
+
Extractive: extractive,
|
|
31
|
+
Abstractive: abstractive,
|
|
32
|
+
Store: st,
|
|
33
|
+
Gating: gating,
|
|
34
|
+
}
|
|
35
|
+
s.methods = map[string]HandlerFn{
|
|
36
|
+
"health": s.handleHealth,
|
|
37
|
+
"status": s.handleStatus,
|
|
38
|
+
"ensure_collections": s.handleEnsureCollections,
|
|
39
|
+
"insert_text": s.handleInsertText,
|
|
40
|
+
"gating_scalar": s.handleGatingScalar,
|
|
41
|
+
"search_text": s.handleSearchText,
|
|
42
|
+
"list_by_meta": s.handleListByMeta,
|
|
43
|
+
"export_memory": s.handleExportMemory,
|
|
44
|
+
"flush_namespace": s.handleFlushNamespace,
|
|
45
|
+
"delete": s.handleDelete,
|
|
46
|
+
"delete_batch": s.handleDeleteBatch,
|
|
47
|
+
"compact_session": s.handleCompact,
|
|
48
|
+
"flush": s.handleFlush,
|
|
49
|
+
}
|
|
50
|
+
return s
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func (s *Server) Call(ctx context.Context, method string, params any) (any, error) {
|
|
54
|
+
handler, ok := s.methods[method]
|
|
55
|
+
if !ok {
|
|
56
|
+
return nil, fmt.Errorf("unknown method: %s", method)
|
|
57
|
+
}
|
|
58
|
+
return handler(ctx, params)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type ensureCollectionsParams struct {
|
|
62
|
+
Collections []string `json:"collections"`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type insertTextParams struct {
|
|
66
|
+
Collection string `json:"collection"`
|
|
67
|
+
ID string `json:"id"`
|
|
68
|
+
Text string `json:"text"`
|
|
69
|
+
Metadata map[string]any `json:"metadata"`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type searchTextParams struct {
|
|
73
|
+
Collection string `json:"collection"`
|
|
74
|
+
Text string `json:"text"`
|
|
75
|
+
K int `json:"k"`
|
|
76
|
+
ExcludeIDs []string `json:"excludeIds"`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type listByMetaParams struct {
|
|
80
|
+
Collection string `json:"collection"`
|
|
81
|
+
Key string `json:"key"`
|
|
82
|
+
Value string `json:"value"`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type deleteParams struct {
|
|
86
|
+
Collection string `json:"collection"`
|
|
87
|
+
ID string `json:"id"`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type deleteBatchParams struct {
|
|
91
|
+
Collection string `json:"collection"`
|
|
92
|
+
IDs []string `json:"ids"`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type compactParams struct {
|
|
96
|
+
SessionID string `json:"sessionId"`
|
|
97
|
+
Force bool `json:"force"`
|
|
98
|
+
TargetSize int `json:"targetSize,omitempty"`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type searchTextResult struct {
|
|
102
|
+
Results []store.SearchResult `json:"results"`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type gatingScalarParams struct {
|
|
106
|
+
UserID string `json:"userId"`
|
|
107
|
+
Text string `json:"text"`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type flushNamespaceParams struct {
|
|
111
|
+
UserID string `json:"userId"`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type memoryStatus struct {
|
|
115
|
+
OK bool `json:"ok"`
|
|
116
|
+
Message string `json:"message"`
|
|
117
|
+
TurnCount int `json:"turnCount"`
|
|
118
|
+
MemoryCount int `json:"memoryCount"`
|
|
119
|
+
GatingThreshold float64 `json:"gatingThreshold"`
|
|
120
|
+
AbstractiveReady bool `json:"abstractiveReady"`
|
|
121
|
+
EmbeddingProfile string `json:"embeddingProfile"`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type exportMemoryRecord struct {
|
|
125
|
+
Collection string `json:"collection"`
|
|
126
|
+
ID string `json:"id"`
|
|
127
|
+
Text string `json:"text"`
|
|
128
|
+
Metadata map[string]any `json:"metadata"`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type exportMemoryResult struct {
|
|
132
|
+
Records []exportMemoryRecord `json:"records"`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func (s *Server) handleHealth(_ context.Context, _ any) (any, error) {
|
|
136
|
+
return health.Check(s.Embedder, s.Store), nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func (s *Server) handleStatus(_ context.Context, _ any) (any, error) {
|
|
140
|
+
base := health.Check(s.Embedder, s.Store)
|
|
141
|
+
status := memoryStatus{
|
|
142
|
+
OK: base.OK,
|
|
143
|
+
Message: base.Message,
|
|
144
|
+
TurnCount: s.Store.CountByPrefix("turns:"),
|
|
145
|
+
MemoryCount: s.Store.CountByPrefix("user:"),
|
|
146
|
+
GatingThreshold: s.Gating.Threshold,
|
|
147
|
+
AbstractiveReady: s.Abstractive != nil && s.Abstractive.Ready(),
|
|
148
|
+
EmbeddingProfile: firstNonEmpty(s.Embedder.Profile().Family, s.Embedder.Profile().Backend, "unknown"),
|
|
149
|
+
}
|
|
150
|
+
return status, nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func (s *Server) handleEnsureCollections(ctx context.Context, raw any) (any, error) {
|
|
154
|
+
var params ensureCollectionsParams
|
|
155
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
156
|
+
return nil, err
|
|
157
|
+
}
|
|
158
|
+
for _, collection := range params.Collections {
|
|
159
|
+
if err := s.Store.EnsureCollection(ctx, collection); err != nil {
|
|
160
|
+
return nil, err
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return map[string]any{"ok": true}, nil
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func (s *Server) handleInsertText(ctx context.Context, raw any) (any, error) {
|
|
167
|
+
var params insertTextParams
|
|
168
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
169
|
+
return nil, err
|
|
170
|
+
}
|
|
171
|
+
if err := s.Store.InsertText(ctx, params.Collection, params.ID, params.Text, params.Metadata); err != nil {
|
|
172
|
+
return nil, err
|
|
173
|
+
}
|
|
174
|
+
return map[string]any{"ok": true}, nil
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func (s *Server) handleSearchText(ctx context.Context, raw any) (any, error) {
|
|
178
|
+
var params searchTextParams
|
|
179
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
180
|
+
return nil, err
|
|
181
|
+
}
|
|
182
|
+
results, err := s.Store.SearchText(ctx, params.Collection, params.Text, params.K, params.ExcludeIDs)
|
|
183
|
+
if err != nil {
|
|
184
|
+
return nil, err
|
|
185
|
+
}
|
|
186
|
+
return searchTextResult{Results: results}, nil
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func (s *Server) handleGatingScalar(ctx context.Context, raw any) (any, error) {
|
|
190
|
+
var params gatingScalarParams
|
|
191
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
192
|
+
return nil, err
|
|
193
|
+
}
|
|
194
|
+
turnHits, err := s.Store.SearchText(ctx, "turns:"+params.UserID, params.Text, 10, nil)
|
|
195
|
+
if err != nil {
|
|
196
|
+
return nil, err
|
|
197
|
+
}
|
|
198
|
+
memHits, err := s.Store.SearchText(ctx, "user:"+params.UserID, params.Text, 5, nil)
|
|
199
|
+
if err != nil {
|
|
200
|
+
return nil, err
|
|
201
|
+
}
|
|
202
|
+
return compact.ComputeGating(turnHits, memHits, params.Text, s.Gating), nil
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func (s *Server) handleListByMeta(ctx context.Context, raw any) (any, error) {
|
|
206
|
+
var params listByMetaParams
|
|
207
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
208
|
+
return nil, err
|
|
209
|
+
}
|
|
210
|
+
results, err := s.Store.ListByMeta(ctx, params.Collection, params.Key, params.Value)
|
|
211
|
+
if err != nil {
|
|
212
|
+
return nil, err
|
|
213
|
+
}
|
|
214
|
+
return searchTextResult{Results: results}, nil
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func (s *Server) handleExportMemory(ctx context.Context, raw any) (any, error) {
|
|
218
|
+
var params flushNamespaceParams
|
|
219
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
220
|
+
return nil, err
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
prefix := "user:"
|
|
224
|
+
if params.UserID != "" {
|
|
225
|
+
prefix = "user:" + params.UserID
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
collections := s.Store.CollectionNames()
|
|
229
|
+
records := make([]exportMemoryRecord, 0)
|
|
230
|
+
for _, collection := range collections {
|
|
231
|
+
if collection == storeDirtyCollectionName() || collection == "" {
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if collection != prefix && !hasTierPrefix(collection, prefix) {
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
items, err := s.Store.ListCollection(ctx, collection)
|
|
238
|
+
if err != nil {
|
|
239
|
+
return nil, err
|
|
240
|
+
}
|
|
241
|
+
for _, item := range items {
|
|
242
|
+
records = append(records, exportMemoryRecord{
|
|
243
|
+
Collection: collection,
|
|
244
|
+
ID: item.ID,
|
|
245
|
+
Text: item.Text,
|
|
246
|
+
Metadata: item.Metadata,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return exportMemoryResult{Records: records}, nil
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func (s *Server) handleFlushNamespace(ctx context.Context, raw any) (any, error) {
|
|
255
|
+
var params flushNamespaceParams
|
|
256
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
257
|
+
return nil, err
|
|
258
|
+
}
|
|
259
|
+
if params.UserID == "" {
|
|
260
|
+
return nil, fmt.Errorf("userId is required")
|
|
261
|
+
}
|
|
262
|
+
if err := s.Store.DeleteCollectionsByPrefix(ctx, "user:"+params.UserID); err != nil {
|
|
263
|
+
return nil, err
|
|
264
|
+
}
|
|
265
|
+
return map[string]any{"ok": true}, nil
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func (s *Server) handleDelete(ctx context.Context, raw any) (any, error) {
|
|
269
|
+
var params deleteParams
|
|
270
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
271
|
+
return nil, err
|
|
272
|
+
}
|
|
273
|
+
if err := s.Store.Delete(ctx, params.Collection, params.ID); err != nil {
|
|
274
|
+
return nil, err
|
|
275
|
+
}
|
|
276
|
+
return map[string]any{"ok": true}, nil
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
func (s *Server) handleDeleteBatch(ctx context.Context, raw any) (any, error) {
|
|
280
|
+
var params deleteBatchParams
|
|
281
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
282
|
+
return nil, err
|
|
283
|
+
}
|
|
284
|
+
if err := s.Store.DeleteBatch(ctx, params.Collection, params.IDs); err != nil {
|
|
285
|
+
return nil, err
|
|
286
|
+
}
|
|
287
|
+
return map[string]any{"ok": true}, nil
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
func (s *Server) handleCompact(ctx context.Context, raw any) (any, error) {
|
|
291
|
+
var params compactParams
|
|
292
|
+
if err := decode(raw, ¶ms); err != nil {
|
|
293
|
+
return nil, err
|
|
294
|
+
}
|
|
295
|
+
result, err := compact.CompactSession(
|
|
296
|
+
ctx,
|
|
297
|
+
s.Store,
|
|
298
|
+
s.Extractive,
|
|
299
|
+
s.Abstractive,
|
|
300
|
+
params.SessionID,
|
|
301
|
+
params.Force,
|
|
302
|
+
params.TargetSize,
|
|
303
|
+
)
|
|
304
|
+
if err != nil {
|
|
305
|
+
return nil, err
|
|
306
|
+
}
|
|
307
|
+
return result, nil
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
func (s *Server) handleFlush(ctx context.Context, _ any) (any, error) {
|
|
311
|
+
if err := s.Store.Flush(ctx); err != nil {
|
|
312
|
+
return nil, err
|
|
313
|
+
}
|
|
314
|
+
return map[string]any{"ok": true}, nil
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
func decode(raw any, target any) error {
|
|
318
|
+
if raw == nil {
|
|
319
|
+
return nil
|
|
320
|
+
}
|
|
321
|
+
data, err := json.Marshal(raw)
|
|
322
|
+
if err != nil {
|
|
323
|
+
return err
|
|
324
|
+
}
|
|
325
|
+
return json.Unmarshal(data, target)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func hasTierPrefix(collection, prefix string) bool {
|
|
329
|
+
return collection == prefix || strings.HasPrefix(collection, prefix+":")
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
func storeDirtyCollectionName() string {
|
|
333
|
+
return "_tier_dirty"
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
func firstNonEmpty(values ...string) string {
|
|
337
|
+
for _, value := range values {
|
|
338
|
+
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
339
|
+
return trimmed
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return ""
|
|
343
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
package server
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"math"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/compact"
|
|
9
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/embed"
|
|
10
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/health"
|
|
11
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/store"
|
|
12
|
+
"github.com/xDarkicex/openclaw-memory-libravdb/sidecar/summarize"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type fakeEmbedder struct{}
|
|
16
|
+
|
|
17
|
+
func (fakeEmbedder) EmbedDocument(_ context.Context, text string) ([]float32, error) {
|
|
18
|
+
switch text {
|
|
19
|
+
case "alpha":
|
|
20
|
+
return []float32{1, 0}, nil
|
|
21
|
+
case "query-alpha":
|
|
22
|
+
return []float32{1, 0}, nil
|
|
23
|
+
case "gate-query":
|
|
24
|
+
return []float32{1, 0}, nil
|
|
25
|
+
case "turn-match":
|
|
26
|
+
return []float32{1, 0}, nil
|
|
27
|
+
case "memory-match":
|
|
28
|
+
return []float32{1, 0}, nil
|
|
29
|
+
case "I prefer switching /src/context-engine.ts after fixed ERR_TIMEOUT in func ComputeGating() on 2026-03-29.":
|
|
30
|
+
return []float32{1, 0}, nil
|
|
31
|
+
default:
|
|
32
|
+
return []float32{0, 1}, nil
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func (fakeEmbedder) EmbedQuery(_ context.Context, text string) ([]float32, error) {
|
|
37
|
+
return fakeEmbedder{}.EmbedDocument(context.Background(), text)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func (fakeEmbedder) Dimensions() int { return 2 }
|
|
41
|
+
func (fakeEmbedder) Profile() embed.Profile {
|
|
42
|
+
return embed.Profile{
|
|
43
|
+
Backend: "test",
|
|
44
|
+
Family: "test",
|
|
45
|
+
Dimensions: 2,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
func (fakeEmbedder) Ready() bool { return true }
|
|
49
|
+
func (fakeEmbedder) Reason() string { return "" }
|
|
50
|
+
func (fakeEmbedder) Mode() string { return "primary" }
|
|
51
|
+
|
|
52
|
+
func TestRPCInsertSearchAndDelete(t *testing.T) {
|
|
53
|
+
ctx := context.Background()
|
|
54
|
+
st, err := store.Open("test-path", fakeEmbedder{})
|
|
55
|
+
if err != nil {
|
|
56
|
+
t.Fatalf("store.Open() error = %v", err)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
srv := New(fakeEmbedder{}, nil, nil, st, compact.DefaultGatingConfig())
|
|
60
|
+
|
|
61
|
+
if _, err := srv.Call(ctx, "insert_text", map[string]any{
|
|
62
|
+
"collection": "session:test",
|
|
63
|
+
"id": "a",
|
|
64
|
+
"text": "alpha",
|
|
65
|
+
"metadata": map[string]any{"type": "turn"},
|
|
66
|
+
}); err != nil {
|
|
67
|
+
t.Fatalf("insert_text error = %v", err)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
got, err := srv.Call(ctx, "search_text", map[string]any{
|
|
71
|
+
"collection": "session:test",
|
|
72
|
+
"text": "query-alpha",
|
|
73
|
+
"k": 5,
|
|
74
|
+
})
|
|
75
|
+
if err != nil {
|
|
76
|
+
t.Fatalf("search_text error = %v", err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
search, ok := got.(searchTextResult)
|
|
80
|
+
if !ok {
|
|
81
|
+
t.Fatalf("expected searchTextResult, got %T", got)
|
|
82
|
+
}
|
|
83
|
+
if len(search.Results) != 1 || search.Results[0].ID != "a" {
|
|
84
|
+
t.Fatalf("unexpected search results: %+v", search.Results)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if _, err := srv.Call(ctx, "delete", map[string]any{
|
|
88
|
+
"collection": "session:test",
|
|
89
|
+
"id": "a",
|
|
90
|
+
}); err != nil {
|
|
91
|
+
t.Fatalf("delete error = %v", err)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func TestRPCHealthAndListByMeta(t *testing.T) {
|
|
96
|
+
ctx := context.Background()
|
|
97
|
+
st, err := store.Open("test-path", fakeEmbedder{})
|
|
98
|
+
if err != nil {
|
|
99
|
+
t.Fatalf("store.Open() error = %v", err)
|
|
100
|
+
}
|
|
101
|
+
srv := New(fakeEmbedder{}, nil, nil, st, compact.DefaultGatingConfig())
|
|
102
|
+
|
|
103
|
+
if _, err := srv.Call(ctx, "insert_text", map[string]any{
|
|
104
|
+
"collection": "global",
|
|
105
|
+
"id": "g1",
|
|
106
|
+
"text": "alpha",
|
|
107
|
+
"metadata": map[string]any{"source": "spec"},
|
|
108
|
+
}); err != nil {
|
|
109
|
+
t.Fatalf("insert_text error = %v", err)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
got, err := srv.Call(ctx, "list_by_meta", map[string]any{
|
|
113
|
+
"collection": "global",
|
|
114
|
+
"key": "source",
|
|
115
|
+
"value": "spec",
|
|
116
|
+
})
|
|
117
|
+
if err != nil {
|
|
118
|
+
t.Fatalf("list_by_meta error = %v", err)
|
|
119
|
+
}
|
|
120
|
+
listed := got.(searchTextResult)
|
|
121
|
+
if len(listed.Results) != 1 || listed.Results[0].ID != "g1" {
|
|
122
|
+
t.Fatalf("unexpected list_by_meta results: %+v", listed.Results)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
gotHealth, err := srv.Call(ctx, "health", nil)
|
|
126
|
+
if err != nil {
|
|
127
|
+
t.Fatalf("health error = %v", err)
|
|
128
|
+
}
|
|
129
|
+
status, ok := gotHealth.(health.Status)
|
|
130
|
+
if !ok {
|
|
131
|
+
t.Fatalf("expected health.Status, got %T", gotHealth)
|
|
132
|
+
}
|
|
133
|
+
if !status.OK {
|
|
134
|
+
t.Fatalf("expected healthy response, got %+v", gotHealth)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func TestRPCUnknownMethodErrors(t *testing.T) {
|
|
139
|
+
ctx := context.Background()
|
|
140
|
+
st, err := store.Open("test-path", fakeEmbedder{})
|
|
141
|
+
if err != nil {
|
|
142
|
+
t.Fatalf("store.Open() error = %v", err)
|
|
143
|
+
}
|
|
144
|
+
srv := New(fakeEmbedder{}, nil, nil, st, compact.DefaultGatingConfig())
|
|
145
|
+
|
|
146
|
+
if _, err := srv.Call(ctx, "does_not_exist", nil); err == nil {
|
|
147
|
+
t.Fatalf("expected unknown method to error")
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func TestRPCMalformedParamsError(t *testing.T) {
|
|
152
|
+
ctx := context.Background()
|
|
153
|
+
st, err := store.Open("test-path", fakeEmbedder{})
|
|
154
|
+
if err != nil {
|
|
155
|
+
t.Fatalf("store.Open() error = %v", err)
|
|
156
|
+
}
|
|
157
|
+
srv := New(fakeEmbedder{}, nil, nil, st, compact.DefaultGatingConfig())
|
|
158
|
+
|
|
159
|
+
if _, err := srv.Call(ctx, "insert_text", "not-an-object"); err == nil {
|
|
160
|
+
t.Fatalf("expected malformed params to error")
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func TestRPCCompactReturnsStructuredResult(t *testing.T) {
|
|
165
|
+
ctx := context.Background()
|
|
166
|
+
st, err := store.Open("test-path", fakeEmbedder{})
|
|
167
|
+
if err != nil {
|
|
168
|
+
t.Fatalf("store.Open() error = %v", err)
|
|
169
|
+
}
|
|
170
|
+
srv := New(fakeEmbedder{}, summarize.NewExtractive(fakeEmbedder{}, "extractive"), nil, st, compact.DefaultGatingConfig())
|
|
171
|
+
|
|
172
|
+
if _, err := srv.Call(ctx, "insert_text", map[string]any{
|
|
173
|
+
"collection": "session:test",
|
|
174
|
+
"id": "a",
|
|
175
|
+
"text": "alpha",
|
|
176
|
+
"metadata": map[string]any{"type": "turn", "sessionId": "test", "ts": int64(10)},
|
|
177
|
+
}); err != nil {
|
|
178
|
+
t.Fatalf("insert_text(a) error = %v", err)
|
|
179
|
+
}
|
|
180
|
+
if _, err := srv.Call(ctx, "insert_text", map[string]any{
|
|
181
|
+
"collection": "session:test",
|
|
182
|
+
"id": "b",
|
|
183
|
+
"text": "alpha",
|
|
184
|
+
"metadata": map[string]any{"type": "turn", "sessionId": "test", "ts": int64(20)},
|
|
185
|
+
}); err != nil {
|
|
186
|
+
t.Fatalf("insert_text(b) error = %v", err)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
got, err := srv.Call(ctx, "compact_session", map[string]any{
|
|
190
|
+
"sessionId": "test",
|
|
191
|
+
"force": true,
|
|
192
|
+
"targetSize": 20,
|
|
193
|
+
})
|
|
194
|
+
if err != nil {
|
|
195
|
+
t.Fatalf("compact_session error = %v", err)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
result, ok := got.(compact.Result)
|
|
199
|
+
if !ok {
|
|
200
|
+
t.Fatalf("expected compact.Result, got %T", got)
|
|
201
|
+
}
|
|
202
|
+
if !result.DidCompact || result.ClustersFormed != 1 || result.TurnsRemoved != 2 {
|
|
203
|
+
t.Fatalf("unexpected compact result: %+v", result)
|
|
204
|
+
}
|
|
205
|
+
if result.SummaryMethod == "" {
|
|
206
|
+
t.Fatalf("expected summary method in compact result: %+v", result)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func TestRPCGatingScalarReturnsDecomposedSignals(t *testing.T) {
|
|
211
|
+
ctx := context.Background()
|
|
212
|
+
st, err := store.Open("test-path", fakeEmbedder{})
|
|
213
|
+
if err != nil {
|
|
214
|
+
t.Fatalf("store.Open() error = %v", err)
|
|
215
|
+
}
|
|
216
|
+
srv := New(fakeEmbedder{}, nil, nil, st, compact.DefaultGatingConfig())
|
|
217
|
+
|
|
218
|
+
for i := 0; i < 5; i++ {
|
|
219
|
+
if err := st.InsertText(ctx, "turns:u1", string(rune('a'+i)), "turn-match", map[string]any{"type": "turn"}); err != nil {
|
|
220
|
+
t.Fatalf("turn insert %d error = %v", i, err)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for i := 0; i < 2; i++ {
|
|
224
|
+
if err := st.InsertText(ctx, "user:u1", string(rune('k'+i)), "memory-match", map[string]any{"type": "turn", "userId": "u1"}); err != nil {
|
|
225
|
+
t.Fatalf("memory insert %d error = %v", i, err)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
got, err := srv.Call(ctx, "gating_scalar", map[string]any{
|
|
230
|
+
"userId": "u1",
|
|
231
|
+
"text": "I prefer switching /src/context-engine.ts after fixed ERR_TIMEOUT in func ComputeGating() on 2026-03-29.",
|
|
232
|
+
})
|
|
233
|
+
if err != nil {
|
|
234
|
+
t.Fatalf("gating_scalar error = %v", err)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
signals, ok := got.(compact.GatingSignals)
|
|
238
|
+
if !ok {
|
|
239
|
+
t.Fatalf("expected compact.GatingSignals, got %T", got)
|
|
240
|
+
}
|
|
241
|
+
if signals.InputFreq != 1.0 {
|
|
242
|
+
t.Fatalf("InputFreq = %v, want 1.0", signals.InputFreq)
|
|
243
|
+
}
|
|
244
|
+
if signals.MemSaturation != (2.0 / 3.0) {
|
|
245
|
+
t.Fatalf("MemSaturation = %v, want %v", signals.MemSaturation, 2.0/3.0)
|
|
246
|
+
}
|
|
247
|
+
if signals.H != 0.0 {
|
|
248
|
+
t.Fatalf("H = %v, want 0.0", signals.H)
|
|
249
|
+
}
|
|
250
|
+
if signals.D <= 0.0 {
|
|
251
|
+
t.Fatalf("D = %v, want positive conversational structure", signals.D)
|
|
252
|
+
}
|
|
253
|
+
if math.Abs(signals.R-(1.0/3.0)) > 1e-12 {
|
|
254
|
+
t.Fatalf("R = %v, want %v", signals.R, 1.0/3.0)
|
|
255
|
+
}
|
|
256
|
+
if signals.T < 0.5 {
|
|
257
|
+
t.Fatalf("T = %v, want technical mixture weight above 0.5", signals.T)
|
|
258
|
+
}
|
|
259
|
+
if signals.P <= 0.0 || signals.A <= 0.0 || signals.Dtech <= 0.0 {
|
|
260
|
+
t.Fatalf("expected positive technical signals, got %+v", signals)
|
|
261
|
+
}
|
|
262
|
+
if signals.G < signals.Gconv || signals.G > signals.Gtech {
|
|
263
|
+
t.Fatalf("expected convex blend bounded by sub-formulas, got %+v", signals)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func TestRPCStatusReportsCountsAndThreshold(t *testing.T) {
|
|
268
|
+
ctx := context.Background()
|
|
269
|
+
st, err := store.Open("test-path", fakeEmbedder{})
|
|
270
|
+
if err != nil {
|
|
271
|
+
t.Fatalf("store.Open() error = %v", err)
|
|
272
|
+
}
|
|
273
|
+
cfg := compact.DefaultGatingConfig()
|
|
274
|
+
cfg.Threshold = 0.42
|
|
275
|
+
srv := New(fakeEmbedder{}, nil, nil, st, cfg)
|
|
276
|
+
|
|
277
|
+
if err := st.InsertText(ctx, "turns:u1", "t1", "turn-match", map[string]any{"type": "turn"}); err != nil {
|
|
278
|
+
t.Fatalf("turn insert error = %v", err)
|
|
279
|
+
}
|
|
280
|
+
if err := st.InsertText(ctx, "user:u1", "m1", "memory-match", map[string]any{"type": "turn"}); err != nil {
|
|
281
|
+
t.Fatalf("memory insert error = %v", err)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
got, err := srv.Call(ctx, "status", nil)
|
|
285
|
+
if err != nil {
|
|
286
|
+
t.Fatalf("status error = %v", err)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
status, ok := got.(memoryStatus)
|
|
290
|
+
if !ok {
|
|
291
|
+
t.Fatalf("expected memoryStatus, got %T", got)
|
|
292
|
+
}
|
|
293
|
+
if !status.OK {
|
|
294
|
+
t.Fatalf("expected healthy status, got %+v", status)
|
|
295
|
+
}
|
|
296
|
+
if status.TurnCount != 1 || status.MemoryCount != 1 {
|
|
297
|
+
t.Fatalf("unexpected counts: %+v", status)
|
|
298
|
+
}
|
|
299
|
+
if status.GatingThreshold != 0.42 {
|
|
300
|
+
t.Fatalf("GatingThreshold = %v, want 0.42", status.GatingThreshold)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func TestRPCExportMemoryAndFlushNamespace(t *testing.T) {
|
|
305
|
+
ctx := context.Background()
|
|
306
|
+
st, err := store.Open("test-path", fakeEmbedder{})
|
|
307
|
+
if err != nil {
|
|
308
|
+
t.Fatalf("store.Open() error = %v", err)
|
|
309
|
+
}
|
|
310
|
+
srv := New(fakeEmbedder{}, nil, nil, st, compact.DefaultGatingConfig())
|
|
311
|
+
|
|
312
|
+
if err := st.InsertText(ctx, "user:u1", "a", "memory-match", map[string]any{"userId": "u1"}); err != nil {
|
|
313
|
+
t.Fatalf("u1 insert error = %v", err)
|
|
314
|
+
}
|
|
315
|
+
if err := st.InsertText(ctx, "user:u2", "b", "memory-match", map[string]any{"userId": "u2"}); err != nil {
|
|
316
|
+
t.Fatalf("u2 insert error = %v", err)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
exportedRaw, err := srv.Call(ctx, "export_memory", map[string]any{"userId": "u1"})
|
|
320
|
+
if err != nil {
|
|
321
|
+
t.Fatalf("export_memory error = %v", err)
|
|
322
|
+
}
|
|
323
|
+
exported, ok := exportedRaw.(exportMemoryResult)
|
|
324
|
+
if !ok {
|
|
325
|
+
t.Fatalf("expected exportMemoryResult, got %T", exportedRaw)
|
|
326
|
+
}
|
|
327
|
+
if len(exported.Records) != 1 || exported.Records[0].Collection != "user:u1" || exported.Records[0].ID != "a" {
|
|
328
|
+
t.Fatalf("unexpected export records: %+v", exported.Records)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if _, err := srv.Call(ctx, "flush_namespace", map[string]any{"userId": "u1"}); err != nil {
|
|
332
|
+
t.Fatalf("flush_namespace error = %v", err)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
u1, err := st.ListCollection(ctx, "user:u1")
|
|
336
|
+
if err != nil {
|
|
337
|
+
t.Fatalf("ListCollection(user:u1) error = %v", err)
|
|
338
|
+
}
|
|
339
|
+
if len(u1) != 0 {
|
|
340
|
+
t.Fatalf("expected user:u1 to be empty after flush, got %+v", u1)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
u2, err := st.ListCollection(ctx, "user:u2")
|
|
344
|
+
if err != nil {
|
|
345
|
+
t.Fatalf("ListCollection(user:u2) error = %v", err)
|
|
346
|
+
}
|
|
347
|
+
if len(u2) != 1 || u2[0].ID != "b" {
|
|
348
|
+
t.Fatalf("expected user:u2 to remain intact, got %+v", u2)
|
|
349
|
+
}
|
|
350
|
+
}
|