@theokit/sdk 4.61.0 → 4.62.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 CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.62.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 36bb21f: Memory recall is scored with BM25 instead of Jaccard, and rank fusion is damped for stores of
8
+ tens rather than hundreds.
9
+
10
+ **What changes for you:** which memories are selected when several are plausible. The store, the
11
+ budget and the API are unchanged; the ordering is not.
12
+
13
+ Measured on LongMemEval-S — 500 questions, 54 sessions each — through a public eval harness, with
14
+ its tokenised-substring `grep` adapter as the floor:
15
+
16
+ | | hit@5 | P@5 | R@5 | p50 |
17
+ | ----------------------- | --------- | --------- | --------- | -------- |
18
+ | Jaccard (4.61.0) | 80.0% | 0.236 | 0.670 | 205ms |
19
+ | `grep` (floor) | 89.0% | 0.295 | 0.807 | 2ms |
20
+ | **BM25 (this release)** | **96.8%** | **0.329** | **0.904** | **20ms** |
21
+
22
+ Jaccard lost to a naive substring match. The mechanism was isolated before it was fixed: on a
23
+ preference query, the term that discriminates appeared in 2 of 15 documents while a noise term
24
+ appeared in 14 — and Jaccard weighted them identically. Almost every document scored above zero,
25
+ fusion flattened what ordering remained, and recency decided a relevance question. IDF is the
26
+ whole fix, and the gain concentrates where that predicts:
27
+
28
+ ```
29
+ single-session-preference 46.7% -> 86.7% (+40.0)
30
+ single-session-assistant 89.3% -> 100.0% (+10.7)
31
+ temporal-reasoning 85.7% -> 96.2% (+10.5)
32
+ multi-session 94.7% -> 96.2% (+1.5)
33
+ ```
34
+
35
+ Also **10x faster**: the previous implementation called its scoring function from inside sort
36
+ comparators, re-tokenising every fact O(n log n) times per query.
37
+
38
+ Rank fusion damping moves from k = 60 to k = 5. Swept rather than chosen — the 500-question corpus
39
+ is insensitive to k once terms are IDF-weighted, while a 15-session corpus goes from 14/15 to
40
+ 15/15. The sweep cannot separate 5 from 1, so the tie is broken on the principle that at k = 1
41
+ damping is nearly gone and fusion stops fusing.
42
+
43
+ **This improves the reliability of memory poisoning as well as of recall**, and the two cannot be
44
+ separated: the property that makes a planted entry work is the property that makes a real one
45
+ useful. See the accompanying patch note for the re-measured figures.
46
+
47
+ ### Patch Changes
48
+
49
+ - Corrects a security figure published with 4.61.0, and adds the rule that produced the error.
50
+
51
+ The 4.61.0 notes said a planted memory entry made the agent perform the action it described in
52
+ **2 of 6 runs**. Re-measured against the published 4.61.0 itself, it is **6 of 6**. Registering
53
+ the permission engine still blocks it — 6 of 6, with zero errors.
54
+
55
+ The old figure was not a smaller version of the same risk; it was a measurement of a different
56
+ thing. It was taken against a retrieval path that did not recall the planted entry at all: on
57
+ 4.60.0 the agent answered "Done." and never saw it, while on 4.61.0 it recites the entry
58
+ verbatim. Nothing about the attack changed between those runs — the recall path did.
59
+
60
+ **A poisoning rate measured against a retrieval path that does not recall the plant is a
61
+ measurement of how often the attack reached the model, not of how often the model resisted it.**
62
+ Any such figure has to record whether the plant was recalled, or it cannot be compared across
63
+ versions.
64
+
65
+ The consequence for anyone depending on this: improving recall is a change to the threat model,
66
+ not something orthogonal to it. The property that makes a planted memory work is the property
67
+ that makes a real one useful. **If anything other than your agent's own deliberate writes can
68
+ reach the memory directory, register the permission engine.**
69
+
70
+ Separately, the original proof constructed the engine as `new PermissionEngine({ rules: [] })`.
71
+ The constructor takes the rules positionally, so that was never a rule list, and nothing checked
72
+ because the script was JavaScript. A crash inside the engine and a gated call produce the same
73
+ observation. Re-run in TypeScript with `new PermissionEngine([])`, it gates: 6 of 6 blocked, 0
74
+ runs threw.
75
+
3
76
  ## 4.61.0
4
77
 
5
78
  ### Minor Changes
@@ -1,4 +1,4 @@
1
- export { Agent } from './chunk-GQZKDGZM.js';
1
+ export { Agent } from './chunk-SKOCYA55.js';
2
2
  import './chunk-SUKXXLWD.js';
3
3
  import './chunk-YEXA3PGR.js';
4
4
  import './chunk-K2BQQ445.js';
@@ -51,5 +51,5 @@ import './chunk-3OR54XG4.js';
51
51
  import './chunk-IDCKSLYH.js';
52
52
  import './chunk-T7XEKOVW.js';
53
53
  import './chunk-T7O6K6PX.js';
54
- //# sourceMappingURL=agent-P42GA6RM.js.map
55
- //# sourceMappingURL=agent-P42GA6RM.js.map
54
+ //# sourceMappingURL=agent-NVBFTY5H.js.map
55
+ //# sourceMappingURL=agent-NVBFTY5H.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"agent-P42GA6RM.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"agent-NVBFTY5H.js"}
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkR7YIVL3K_cjs = require('./chunk-R7YIVL3K.cjs');
3
+ var chunkAH6WD7JR_cjs = require('./chunk-AH6WD7JR.cjs');
4
4
  require('./chunk-NGESVVJN.cjs');
5
5
  require('./chunk-2DG7KW4L.cjs');
6
6
  require('./chunk-BUUUWQMB.cjs');
@@ -58,7 +58,7 @@ require('./chunk-NUKRL3I6.cjs');
58
58
 
59
59
  Object.defineProperty(exports, "Agent", {
60
60
  enumerable: true,
61
- get: function () { return chunkR7YIVL3K_cjs.Agent; }
61
+ get: function () { return chunkAH6WD7JR_cjs.Agent; }
62
62
  });
63
- //# sourceMappingURL=agent-M53C3PAA.cjs.map
64
- //# sourceMappingURL=agent-M53C3PAA.cjs.map
63
+ //# sourceMappingURL=agent-VYPUO4UB.cjs.map
64
+ //# sourceMappingURL=agent-VYPUO4UB.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"agent-M53C3PAA.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"agent-VYPUO4UB.cjs"}
@@ -3439,7 +3439,7 @@ var HISTOGRAM_NAMES = {
3439
3439
  };
3440
3440
  function safeRequire(moduleName) {
3441
3441
  try {
3442
- const r = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunk-R7YIVL3K.cjs', document.baseURI).href)));
3442
+ const r = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunk-AH6WD7JR.cjs', document.baseURI).href)));
3443
3443
  return r(moduleName);
3444
3444
  } catch {
3445
3445
  return void 0;
@@ -3667,7 +3667,7 @@ var cachedOtel;
3667
3667
  function loadOtel() {
3668
3668
  if (cachedOtel === void 0) {
3669
3669
  try {
3670
- const r = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunk-R7YIVL3K.cjs', document.baseURI).href)));
3670
+ const r = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunk-AH6WD7JR.cjs', document.baseURI).href)));
3671
3671
  const mod = r("@opentelemetry/api");
3672
3672
  cachedOtel = mod;
3673
3673
  } catch {
@@ -9139,17 +9139,7 @@ function terms(text) {
9139
9139
  text.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3 && !STOPWORDS.has(w))
9140
9140
  );
9141
9141
  }
9142
- function relevance(fact, queryTerms) {
9143
- if (queryTerms.size === 0) return 0;
9144
- const factTerms = terms(fact.text);
9145
- if (factTerms.size === 0) return 0;
9146
- let shared = 0;
9147
- for (const w of queryTerms) if (factTerms.has(w)) shared += 1;
9148
- if (shared === 0) return 0;
9149
- const union = (/* @__PURE__ */ new Set([...queryTerms, ...factTerms])).size;
9150
- return shared / union;
9151
- }
9152
- var RRF_K = 60;
9142
+ var RRF_K = 5;
9153
9143
  function corroborationRank(fact) {
9154
9144
  if (fact.observations === void 0) return 1;
9155
9145
  return fact.observations > 1 ? 2 : 0;
@@ -9157,13 +9147,55 @@ function corroborationRank(fact) {
9157
9147
  function sortByCorroboration(facts) {
9158
9148
  return [...facts].sort((a, b) => corroborationRank(b) - corroborationRank(a));
9159
9149
  }
9160
- function rankForQuery(facts, queryTerms) {
9161
- const scored = facts.map((f) => ({ f, score: relevance(f, queryTerms) }));
9162
- const matching = scored.filter((s) => s.score > 0).map((s) => s.f);
9163
- const rest = scored.filter((s) => s.score === 0).map((s) => s.f);
9164
- const byRelevance = [...matching].sort(
9165
- (a, b) => relevance(b, queryTerms) - relevance(a, queryTerms)
9166
- );
9150
+ var BM25_K1 = 1.2;
9151
+ var BM25_B = 0.75;
9152
+ function docTerms(text) {
9153
+ const counts = /* @__PURE__ */ new Map();
9154
+ let length = 0;
9155
+ for (const w of text.toLowerCase().split(/[^a-z0-9]+/).filter((x) => x.length >= 3 && !STOPWORDS.has(x))) {
9156
+ counts.set(w, (counts.get(w) ?? 0) + 1);
9157
+ length += 1;
9158
+ }
9159
+ return { counts, length };
9160
+ }
9161
+ function documentFrequencies(docs, queryTerms) {
9162
+ const df = /* @__PURE__ */ new Map();
9163
+ for (const term of queryTerms) {
9164
+ let count = 0;
9165
+ for (const d of docs) if (d.terms.counts.has(term)) count += 1;
9166
+ df.set(term, count);
9167
+ }
9168
+ return df;
9169
+ }
9170
+ function scoreDoc(doc, queryTerms, df, n, avgLength) {
9171
+ let score = 0;
9172
+ for (const term of queryTerms) {
9173
+ const tf = doc.terms.counts.get(term);
9174
+ if (tf === void 0) continue;
9175
+ const frequency = df.get(term) ?? 0;
9176
+ const idf = Math.log(1 + (n - frequency + 0.5) / (frequency + 0.5));
9177
+ const norm = tf + BM25_K1 * (1 - BM25_B + BM25_B * doc.terms.length / avgLength);
9178
+ score += idf * (tf * (BM25_K1 + 1)) / norm;
9179
+ }
9180
+ return score;
9181
+ }
9182
+ function bm25Scores(facts, queryTerms) {
9183
+ const scores = /* @__PURE__ */ new Map();
9184
+ if (queryTerms.size === 0 || facts.length === 0) return scores;
9185
+ const docs = facts.map((f) => ({ fact: f, terms: docTerms(f.text) }));
9186
+ const n = docs.length;
9187
+ const avgLength = docs.reduce((sum, d) => sum + d.terms.length, 0) / n || 1;
9188
+ const df = documentFrequencies(docs, queryTerms);
9189
+ for (const d of docs) {
9190
+ const score = scoreDoc(d, queryTerms, df, n, avgLength);
9191
+ if (score > 0) scores.set(d.fact, score);
9192
+ }
9193
+ return scores;
9194
+ }
9195
+ function rankForQuery(facts, scores) {
9196
+ const matching = facts.filter((f) => (scores.get(f) ?? 0) > 0);
9197
+ const rest = facts.filter((f) => (scores.get(f) ?? 0) <= 0);
9198
+ const byRelevance = [...matching].sort((a, b) => (scores.get(b) ?? 0) - (scores.get(a) ?? 0));
9167
9199
  const byRecency = [...matching].sort(
9168
9200
  (a, b) => (b.modified ?? "").localeCompare(a.modified ?? "")
9169
9201
  );
@@ -9191,9 +9223,10 @@ function rankBuckets(facts, query) {
9191
9223
  dated.sort((a, b) => b.modified.localeCompare(a.modified));
9192
9224
  return { dated, undated };
9193
9225
  }
9226
+ const scores = bm25Scores(facts, queryTerms);
9194
9227
  return {
9195
- dated: rankForQuery(dated, queryTerms),
9196
- undated: [...undated].sort((a, b) => relevance(b, queryTerms) - relevance(a, queryTerms))
9228
+ dated: rankForQuery(dated, scores),
9229
+ undated: [...undated].sort((a, b) => (scores.get(b) ?? 0) - (scores.get(a) ?? 0))
9197
9230
  };
9198
9231
  }
9199
9232
  function selectFactsForInjection(facts, options = {}) {
@@ -10755,5 +10788,5 @@ exports.computeCost = computeCost;
10755
10788
  exports.generateCronId = generateCronId;
10756
10789
  exports.getPricingEntry = getPricingEntry;
10757
10790
  exports.openRouterMemoryEmbeddingProviderAdapter = openRouterMemoryEmbeddingProviderAdapter;
10758
- //# sourceMappingURL=chunk-R7YIVL3K.cjs.map
10759
- //# sourceMappingURL=chunk-R7YIVL3K.cjs.map
10791
+ //# sourceMappingURL=chunk-AH6WD7JR.cjs.map
10792
+ //# sourceMappingURL=chunk-AH6WD7JR.cjs.map