@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.
Files changed (80) hide show
  1. package/README.md +46 -0
  2. package/docs/README.md +14 -0
  3. package/docs/architecture-decisions/README.md +6 -0
  4. package/docs/architecture-decisions/adr-001-onnx-over-ollama.md +21 -0
  5. package/docs/architecture-decisions/adr-002-libravdb-over-lancedb.md +19 -0
  6. package/docs/architecture-decisions/adr-003-convex-gating-over-threshold.md +27 -0
  7. package/docs/architecture-decisions/adr-004-sidecar-over-native-ts.md +21 -0
  8. package/docs/architecture.md +188 -0
  9. package/docs/contributing.md +76 -0
  10. package/docs/dependencies.md +38 -0
  11. package/docs/embedding-profiles.md +42 -0
  12. package/docs/gating.md +329 -0
  13. package/docs/implementation.md +381 -0
  14. package/docs/installation.md +272 -0
  15. package/docs/mathematics.md +695 -0
  16. package/docs/models.md +63 -0
  17. package/docs/problem.md +64 -0
  18. package/docs/security.md +86 -0
  19. package/openclaw.plugin.json +84 -0
  20. package/package.json +41 -0
  21. package/scripts/build-sidecar.sh +30 -0
  22. package/scripts/postinstall.js +169 -0
  23. package/scripts/setup.sh +20 -0
  24. package/scripts/setup.ts +505 -0
  25. package/scripts/sidecar-release.d.ts +4 -0
  26. package/scripts/sidecar-release.js +17 -0
  27. package/sidecar/cmd/inspect_onnx/main.go +105 -0
  28. package/sidecar/compact/gate.go +273 -0
  29. package/sidecar/compact/gate_test.go +85 -0
  30. package/sidecar/compact/summarize.go +345 -0
  31. package/sidecar/compact/summarize_test.go +319 -0
  32. package/sidecar/compact/tokens.go +11 -0
  33. package/sidecar/config/config.go +119 -0
  34. package/sidecar/config/config_test.go +75 -0
  35. package/sidecar/embed/engine.go +696 -0
  36. package/sidecar/embed/engine_test.go +349 -0
  37. package/sidecar/embed/matryoshka.go +93 -0
  38. package/sidecar/embed/matryoshka_test.go +150 -0
  39. package/sidecar/embed/onnx_local.go +319 -0
  40. package/sidecar/embed/onnx_local_test.go +159 -0
  41. package/sidecar/embed/profile_contract_test.go +71 -0
  42. package/sidecar/embed/profile_eval_test.go +923 -0
  43. package/sidecar/embed/profiles.go +39 -0
  44. package/sidecar/go.mod +21 -0
  45. package/sidecar/go.sum +30 -0
  46. package/sidecar/health/check.go +33 -0
  47. package/sidecar/health/check_test.go +55 -0
  48. package/sidecar/main.go +151 -0
  49. package/sidecar/model/encoder.go +222 -0
  50. package/sidecar/model/registry.go +262 -0
  51. package/sidecar/model/registry_test.go +102 -0
  52. package/sidecar/model/seq2seq.go +133 -0
  53. package/sidecar/server/rpc.go +343 -0
  54. package/sidecar/server/rpc_test.go +350 -0
  55. package/sidecar/server/transport.go +160 -0
  56. package/sidecar/store/libravdb.go +676 -0
  57. package/sidecar/store/libravdb_test.go +472 -0
  58. package/sidecar/summarize/engine.go +360 -0
  59. package/sidecar/summarize/engine_test.go +148 -0
  60. package/sidecar/summarize/onnx_local.go +494 -0
  61. package/sidecar/summarize/onnx_local_test.go +48 -0
  62. package/sidecar/summarize/profiles.go +52 -0
  63. package/sidecar/summarize/tokenizer.go +13 -0
  64. package/sidecar/summarize/tokenizer_hf.go +76 -0
  65. package/sidecar/summarize/util.go +13 -0
  66. package/src/cli.ts +205 -0
  67. package/src/context-engine.ts +195 -0
  68. package/src/index.ts +27 -0
  69. package/src/memory-provider.ts +24 -0
  70. package/src/openclaw-plugin-sdk.d.ts +53 -0
  71. package/src/plugin-runtime.ts +67 -0
  72. package/src/recall-cache.ts +34 -0
  73. package/src/recall-utils.ts +22 -0
  74. package/src/rpc.ts +84 -0
  75. package/src/scoring.ts +58 -0
  76. package/src/sidecar.ts +506 -0
  77. package/src/tokens.ts +36 -0
  78. package/src/types.ts +146 -0
  79. package/tsconfig.json +20 -0
  80. 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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
+ }