@earendil-works/pi-durable 1.3.0 → 1.5.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 (257) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +287 -82
  3. package/dist/codemode/codemode.d.ts +4 -4
  4. package/dist/codemode/codemode.d.ts.map +1 -1
  5. package/dist/codemode/codemode.js +21 -16
  6. package/dist/codemode/codemode.js.map +1 -1
  7. package/dist/codemode/models.d.ts.map +1 -1
  8. package/dist/codemode/models.js +6 -4
  9. package/dist/codemode/models.js.map +1 -1
  10. package/dist/documents.d.ts +1 -1
  11. package/dist/documents.d.ts.map +1 -1
  12. package/dist/documents.js.map +1 -1
  13. package/dist/env/index.d.ts +12 -10
  14. package/dist/env/index.d.ts.map +1 -1
  15. package/dist/env/index.js.map +1 -1
  16. package/dist/env/node-watch.d.ts +3 -3
  17. package/dist/env/node-watch.d.ts.map +1 -1
  18. package/dist/env/node-watch.js +18 -16
  19. package/dist/env/node-watch.js.map +1 -1
  20. package/dist/env/node.d.ts +2 -2
  21. package/dist/env/node.d.ts.map +1 -1
  22. package/dist/env/node.js +12 -4
  23. package/dist/env/node.js.map +1 -1
  24. package/dist/errors.d.ts +18 -2
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +26 -3
  27. package/dist/errors.js.map +1 -1
  28. package/dist/harness/agent.d.ts +13 -2
  29. package/dist/harness/agent.d.ts.map +1 -1
  30. package/dist/harness/agent.js +48 -6
  31. package/dist/harness/agent.js.map +1 -1
  32. package/dist/harness/compaction.d.ts +3 -1
  33. package/dist/harness/compaction.d.ts.map +1 -1
  34. package/dist/harness/compaction.js +38 -6
  35. package/dist/harness/compaction.js.map +1 -1
  36. package/dist/harness/context.d.ts +23 -7
  37. package/dist/harness/context.d.ts.map +1 -1
  38. package/dist/harness/context.js +71 -25
  39. package/dist/harness/context.js.map +1 -1
  40. package/dist/harness/define.d.ts +7 -3
  41. package/dist/harness/define.d.ts.map +1 -1
  42. package/dist/harness/define.js +4 -0
  43. package/dist/harness/define.js.map +1 -1
  44. package/dist/harness/generation.d.ts +38 -5
  45. package/dist/harness/generation.d.ts.map +1 -1
  46. package/dist/harness/generation.js +497 -108
  47. package/dist/harness/generation.js.map +1 -1
  48. package/dist/harness/harness.d.ts +7 -2
  49. package/dist/harness/harness.d.ts.map +1 -1
  50. package/dist/harness/harness.js +147 -57
  51. package/dist/harness/harness.js.map +1 -1
  52. package/dist/harness/inbox.d.ts +7 -3
  53. package/dist/harness/inbox.d.ts.map +1 -1
  54. package/dist/harness/inbox.js +7 -4
  55. package/dist/harness/inbox.js.map +1 -1
  56. package/dist/harness/json.d.ts +20 -4
  57. package/dist/harness/json.d.ts.map +1 -1
  58. package/dist/harness/json.js +23 -5
  59. package/dist/harness/json.js.map +1 -1
  60. package/dist/harness/live.d.ts +122 -16
  61. package/dist/harness/live.d.ts.map +1 -1
  62. package/dist/harness/live.js +93 -12
  63. package/dist/harness/live.js.map +1 -1
  64. package/dist/harness/partial-changes.d.ts +54 -0
  65. package/dist/harness/partial-changes.d.ts.map +1 -0
  66. package/dist/harness/partial-changes.js +162 -0
  67. package/dist/harness/partial-changes.js.map +1 -0
  68. package/dist/harness/prompt.d.ts +1 -1
  69. package/dist/harness/prompt.d.ts.map +1 -1
  70. package/dist/harness/prompt.js +1 -1
  71. package/dist/harness/prompt.js.map +1 -1
  72. package/dist/harness/registry-reader.d.ts +11 -0
  73. package/dist/harness/registry-reader.d.ts.map +1 -0
  74. package/dist/harness/registry-reader.js +25 -0
  75. package/dist/harness/registry-reader.js.map +1 -0
  76. package/dist/harness/scheduler.d.ts +86 -14
  77. package/dist/harness/scheduler.d.ts.map +1 -1
  78. package/dist/harness/scheduler.js +1459 -369
  79. package/dist/harness/scheduler.js.map +1 -1
  80. package/dist/harness/submissions.d.ts +23 -11
  81. package/dist/harness/submissions.d.ts.map +1 -1
  82. package/dist/harness/submissions.js +104 -36
  83. package/dist/harness/submissions.js.map +1 -1
  84. package/dist/harness/task-graph.d.ts +28 -8
  85. package/dist/harness/task-graph.d.ts.map +1 -1
  86. package/dist/harness/task-graph.js +85 -36
  87. package/dist/harness/task-graph.js.map +1 -1
  88. package/dist/harness/telemetry.d.ts +307 -0
  89. package/dist/harness/telemetry.d.ts.map +1 -0
  90. package/dist/harness/telemetry.js +136 -0
  91. package/dist/harness/telemetry.js.map +1 -0
  92. package/dist/harness/tool.d.ts +2 -0
  93. package/dist/harness/tool.d.ts.map +1 -1
  94. package/dist/harness/tool.js +223 -61
  95. package/dist/harness/tool.js.map +1 -1
  96. package/dist/harness/transcript.d.ts +72 -0
  97. package/dist/harness/transcript.d.ts.map +1 -0
  98. package/dist/harness/transcript.js +264 -0
  99. package/dist/harness/transcript.js.map +1 -0
  100. package/dist/harness/types.d.ts +302 -48
  101. package/dist/harness/types.d.ts.map +1 -1
  102. package/dist/harness/types.js.map +1 -1
  103. package/dist/harness/util.d.ts +7 -1
  104. package/dist/harness/util.d.ts.map +1 -1
  105. package/dist/harness/util.js +13 -0
  106. package/dist/harness/util.js.map +1 -1
  107. package/dist/harness/view.d.ts +43 -22
  108. package/dist/harness/view.d.ts.map +1 -1
  109. package/dist/harness/view.js +130 -71
  110. package/dist/harness/view.js.map +1 -1
  111. package/dist/index.d.ts +13 -9
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +7 -5
  114. package/dist/index.js.map +1 -1
  115. package/dist/search.d.ts +19 -3
  116. package/dist/search.d.ts.map +1 -1
  117. package/dist/search.js +107 -44
  118. package/dist/search.js.map +1 -1
  119. package/dist/session/cached-storage.d.ts +71 -0
  120. package/dist/session/cached-storage.d.ts.map +1 -0
  121. package/dist/session/cached-storage.js +269 -0
  122. package/dist/session/cached-storage.js.map +1 -0
  123. package/dist/session/conversation-tails.d.ts +33 -0
  124. package/dist/session/conversation-tails.d.ts.map +1 -0
  125. package/dist/session/conversation-tails.js +101 -0
  126. package/dist/session/conversation-tails.js.map +1 -0
  127. package/dist/session/keys.d.ts +68 -0
  128. package/dist/session/keys.d.ts.map +1 -0
  129. package/dist/session/keys.js +161 -0
  130. package/dist/session/keys.js.map +1 -0
  131. package/dist/session/session.d.ts +181 -29
  132. package/dist/session/session.d.ts.map +1 -1
  133. package/dist/session/session.js +1073 -183
  134. package/dist/session/session.js.map +1 -1
  135. package/dist/session/transaction.d.ts +86 -4
  136. package/dist/session/transaction.d.ts.map +1 -1
  137. package/dist/session/transaction.js +250 -38
  138. package/dist/session/transaction.js.map +1 -1
  139. package/dist/session/writer.d.ts +74 -0
  140. package/dist/session/writer.d.ts.map +1 -0
  141. package/dist/session/writer.js +294 -0
  142. package/dist/session/writer.js.map +1 -0
  143. package/dist/storage/indexeddb.d.ts +68 -0
  144. package/dist/storage/indexeddb.d.ts.map +1 -0
  145. package/dist/storage/indexeddb.js +881 -0
  146. package/dist/storage/indexeddb.js.map +1 -0
  147. package/dist/storage/jsonl/storage.d.ts +30 -2
  148. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  149. package/dist/storage/jsonl/storage.js +88 -24
  150. package/dist/storage/jsonl/storage.js.map +1 -1
  151. package/dist/storage/memory.d.ts +7 -2
  152. package/dist/storage/memory.d.ts.map +1 -1
  153. package/dist/storage/memory.js +29 -3
  154. package/dist/storage/memory.js.map +1 -1
  155. package/dist/storage/sqlite/cloudflare.d.ts +2 -51
  156. package/dist/storage/sqlite/cloudflare.d.ts.map +1 -1
  157. package/dist/storage/sqlite/cloudflare.js +2 -133
  158. package/dist/storage/sqlite/cloudflare.js.map +1 -1
  159. package/dist/storage/sqlite/database.d.ts +1 -36
  160. package/dist/storage/sqlite/database.d.ts.map +1 -1
  161. package/dist/storage/sqlite/database.js.map +1 -1
  162. package/dist/storage/sqlite/index.d.ts +1 -1
  163. package/dist/storage/sqlite/index.d.ts.map +1 -1
  164. package/dist/storage/sqlite/index.js.map +1 -1
  165. package/dist/storage/sqlite/migrations.d.ts +2 -5
  166. package/dist/storage/sqlite/migrations.d.ts.map +1 -1
  167. package/dist/storage/sqlite/migrations.js +28 -28
  168. package/dist/storage/sqlite/migrations.js.map +1 -1
  169. package/dist/storage/sqlite/node.d.ts +2 -37
  170. package/dist/storage/sqlite/node.d.ts.map +1 -1
  171. package/dist/storage/sqlite/node.js +3 -174
  172. package/dist/storage/sqlite/node.js.map +1 -1
  173. package/dist/storage/sqlite/schema.d.ts +1 -3
  174. package/dist/storage/sqlite/schema.d.ts.map +1 -1
  175. package/dist/storage/sqlite/schema.js +2 -9
  176. package/dist/storage/sqlite/schema.js.map +1 -1
  177. package/dist/storage/sqlite/storage.d.ts +15 -3
  178. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  179. package/dist/storage/sqlite/storage.js +110 -36
  180. package/dist/storage/sqlite/storage.js.map +1 -1
  181. package/dist/storage/store/commit.d.ts +33 -0
  182. package/dist/storage/store/commit.d.ts.map +1 -0
  183. package/dist/storage/store/commit.js +286 -0
  184. package/dist/storage/store/commit.js.map +1 -0
  185. package/dist/storage/store/documents.d.ts +43 -0
  186. package/dist/storage/store/documents.d.ts.map +1 -0
  187. package/dist/storage/store/documents.js +102 -0
  188. package/dist/storage/store/documents.js.map +1 -0
  189. package/dist/storage/store/index.d.ts +2 -0
  190. package/dist/storage/store/index.d.ts.map +1 -0
  191. package/dist/storage/store/index.js +2 -0
  192. package/dist/storage/store/index.js.map +1 -0
  193. package/dist/storage/store/queries.d.ts +49 -0
  194. package/dist/storage/store/queries.d.ts.map +1 -0
  195. package/dist/storage/store/queries.js +93 -0
  196. package/dist/storage/store/queries.js.map +1 -0
  197. package/dist/storage/store/storage.d.ts +59 -0
  198. package/dist/storage/store/storage.d.ts.map +1 -0
  199. package/dist/storage/store/storage.js +438 -0
  200. package/dist/storage/store/storage.js.map +1 -0
  201. package/dist/storage/store/tables.d.ts +54 -0
  202. package/dist/storage/store/tables.d.ts.map +1 -0
  203. package/dist/storage/store/tables.js +108 -0
  204. package/dist/storage/store/tables.js.map +1 -0
  205. package/dist/testing/commit-writes.d.ts +5 -0
  206. package/dist/testing/commit-writes.d.ts.map +1 -0
  207. package/dist/testing/commit-writes.js +12 -0
  208. package/dist/testing/commit-writes.js.map +1 -0
  209. package/dist/testing/env-conformance.js +7 -7
  210. package/dist/testing/env-conformance.js.map +1 -1
  211. package/dist/testing/index.d.ts +1 -0
  212. package/dist/testing/index.d.ts.map +1 -1
  213. package/dist/testing/index.js +1 -0
  214. package/dist/testing/index.js.map +1 -1
  215. package/dist/testing/storage-benchmark.d.ts.map +1 -1
  216. package/dist/testing/storage-benchmark.js +18 -17
  217. package/dist/testing/storage-benchmark.js.map +1 -1
  218. package/dist/testing/storage-conformance.d.ts.map +1 -1
  219. package/dist/testing/storage-conformance.js +369 -67
  220. package/dist/testing/storage-conformance.js.map +1 -1
  221. package/dist/timers.d.ts +21 -0
  222. package/dist/timers.d.ts.map +1 -0
  223. package/dist/timers.js +72 -0
  224. package/dist/timers.js.map +1 -0
  225. package/dist/tool-search/tool-search.d.ts +6 -12
  226. package/dist/tool-search/tool-search.d.ts.map +1 -1
  227. package/dist/tool-search/tool-search.js +7 -7
  228. package/dist/tool-search/tool-search.js.map +1 -1
  229. package/dist/tools/bash.d.ts +12 -12
  230. package/dist/tools/bash.d.ts.map +1 -1
  231. package/dist/tools/bash.js +16 -15
  232. package/dist/tools/bash.js.map +1 -1
  233. package/dist/tools/edit.d.ts +7 -7
  234. package/dist/tools/edit.d.ts.map +1 -1
  235. package/dist/tools/edit.js +9 -9
  236. package/dist/tools/edit.js.map +1 -1
  237. package/dist/tools/index.d.ts +32 -32
  238. package/dist/tools/read.d.ts +6 -6
  239. package/dist/tools/read.d.ts.map +1 -1
  240. package/dist/tools/read.js +19 -8
  241. package/dist/tools/read.js.map +1 -1
  242. package/dist/tools/write.d.ts +4 -4
  243. package/dist/tools/write.d.ts.map +1 -1
  244. package/dist/tools/write.js +5 -5
  245. package/dist/tools/write.js.map +1 -1
  246. package/dist/types.d.ts +331 -64
  247. package/dist/types.d.ts.map +1 -1
  248. package/dist/types.js.map +1 -1
  249. package/package.json +27 -10
  250. package/dist/harness/events.d.ts +0 -186
  251. package/dist/harness/events.d.ts.map +0 -1
  252. package/dist/harness/events.js +0 -373
  253. package/dist/harness/events.js.map +0 -1
  254. package/dist/session/observation.d.ts +0 -42
  255. package/dist/session/observation.d.ts.map +0 -1
  256. package/dist/session/observation.js +0 -281
  257. package/dist/session/observation.js.map +0 -1
package/dist/search.js CHANGED
@@ -2,6 +2,84 @@
2
2
  * BM25 tool search for code mode's `searchTools()` and the `tool_search` tool, a copy of the coding agent's tool search
3
3
  * ranking: durable keeps its own copy of what it needs from the coding agent.
4
4
  */
5
+ /**
6
+ * Okapi BM25 (k1 1.2, b 0.75) over `text`, with the name and the namespace as separate fields: the names of the best
7
+ * matches, best first; ties keep document order.
8
+ * - A tool that matches more of the query's terms, in any field, ranks above one that matches fewer, whatever their
9
+ * scores: for `screenshot window`, a tool whose description has both words beats `window` whose name has one.
10
+ * - A query term in the name scores more than any number of mentions in the text.
11
+ * - A tool whose whole local name (after the last `__`) is in the query gets a bonus worth one more match of every
12
+ * query term, so among tools matching as many terms, `peekaboo app` finds `mcp__peekaboo__app` over tools that
13
+ * only mention the words.
14
+ * - A query that is exactly a tool's name, or its name after a `__`, ranks that tool first, before coverage.
15
+ *
16
+ * Only `text` is length-normalized, so a long name or long namespace instructions do not lower a tool's score.
17
+ */
18
+ export function rankDocuments(query, documents, limit) {
19
+ const queryTerms = new Set(tokenize(query));
20
+ if (queryTerms.size === 0 || documents.length === 0 || limit <= 0)
21
+ return [];
22
+ const indexed = documents.map(indexDocument);
23
+ const averageLength = indexed.reduce((sum, document) => sum + document.textLength, 0) / indexed.length || 1;
24
+ const idf = new Map([...queryTerms].map((term) => {
25
+ const frequency = indexed.filter((document) => document.nameTerms.has(term) || document.textCounts.has(term) || document.namespaceTerms.has(term)).length;
26
+ return [term, Math.log(1 + (indexed.length - frequency + 0.5) / (frequency + 0.5))];
27
+ }));
28
+ const fullNameBonus = [...idf.values()].reduce((sum, value) => sum + value, 0);
29
+ const exactQuery = normalizeName(query);
30
+ const matches = [];
31
+ for (const document of indexed) {
32
+ const norm = K1 * (1 - B + (B * document.textLength) / averageLength);
33
+ let score = 0;
34
+ let covered = 0;
35
+ for (const term of queryTerms) {
36
+ const count = document.textCounts.get(term) ?? 0;
37
+ const weight = (document.nameTerms.has(term) ? NAME_WEIGHT : 0) +
38
+ (document.namespaceTerms.has(term) ? NAMESPACE_WEIGHT : 0) +
39
+ (count * (K1 + 1)) / (count + norm);
40
+ if (weight > 0)
41
+ covered++;
42
+ score += idf.get(term) * weight;
43
+ }
44
+ if (score === 0)
45
+ continue;
46
+ const local = document.localNameTerms;
47
+ if (local.length > 0 && local.every((term) => queryTerms.has(term)))
48
+ score += fullNameBonus;
49
+ matches.push({ name: document.name, score, covered, exact: isExactNameQuery(exactQuery, document.name) });
50
+ }
51
+ return matches
52
+ .sort((left, right) => Number(right.exact) - Number(left.exact) || right.covered - left.covered || right.score - left.score)
53
+ .slice(0, limit)
54
+ .map((match) => match.name);
55
+ }
56
+ const K1 = 1.2;
57
+ const B = 0.75;
58
+ /**
59
+ * A query term in the name counts this many times its IDF. The text part of a term is at most `K1 + 1` (2.2) times
60
+ * its IDF and about 1 for a single mention, so a name match beats any number of mentions in the text.
61
+ */
62
+ const NAME_WEIGHT = 3;
63
+ /**
64
+ * A query term in the namespace text counts this many times its IDF. Every tool of the namespace shares it, so it helps
65
+ * find the namespace but not the tool in it.
66
+ */
67
+ const NAMESPACE_WEIGHT = 0.5;
68
+ function indexDocument(document) {
69
+ const textCounts = new Map();
70
+ for (const term of tokenize(document.text))
71
+ textCounts.set(term, (textCounts.get(term) ?? 0) + 1);
72
+ const segments = document.name.split("__");
73
+ return {
74
+ name: document.name,
75
+ nameTerms: new Set(tokenize(document.name)),
76
+ localNameTerms: [...new Set(tokenize(segments[segments.length - 1]))],
77
+ textCounts,
78
+ textLength: [...textCounts.values()].reduce((sum, count) => sum + count, 0),
79
+ namespaceTerms: new Set(tokenize(document.namespaceText ?? "")),
80
+ };
81
+ }
82
+ const wordSegmenter = new Intl.Segmenter(undefined, { granularity: "word" });
5
83
  const STOP_WORDS = new Set([
6
84
  "a",
7
85
  "an",
@@ -25,6 +103,23 @@ const STOP_WORDS = new Set([
25
103
  "to",
26
104
  "with",
27
105
  ]);
106
+ /** Unicode word segmentation with identifier boundaries and English stop words/singular forms. */
107
+ export function tokenize(text) {
108
+ const normalized = text
109
+ .normalize("NFC")
110
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
111
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
112
+ .toLowerCase()
113
+ .replace(/[^\p{L}\p{M}\p{N}]+/gu, " ");
114
+ // The segmenter costs about 15 times a split. Plain ASCII splits into the same words at spaces, so only other
115
+ // text, such as Chinese written without spaces, goes through it.
116
+ const words = /^[a-z0-9 ]*$/.test(normalized)
117
+ ? normalized.split(" ")
118
+ : Array.from(wordSegmenter.segment(normalized), (segment) => (segment.isWordLike ? segment.segment : ""));
119
+ return words
120
+ .filter((word) => word.length > 0 && !STOP_WORDS.has(word))
121
+ .map((word) => (/^[a-z0-9]+$/.test(word) ? stem(word) : word));
122
+ }
28
123
  /** Naive singular form, so `issues` matches `issue` and `searches` matches `search`. */
29
124
  function stem(term) {
30
125
  if (term.length > 4 && term.endsWith("ies"))
@@ -35,51 +130,19 @@ function stem(term) {
35
130
  return term.slice(0, -1);
36
131
  return term;
37
132
  }
38
- /** Lowercase terms, split at camelCase boundaries and non-alphanumerics, without stop words. */
39
- export function tokenize(text) {
40
- return text
41
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
42
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
133
+ /** Lowercase with characters invalid in script identifiers as `_`, so `dev-radius` matches `dev_radius`. */
134
+ function normalizeName(name) {
135
+ return name
136
+ .trim()
43
137
  .toLowerCase()
44
- .split(/[^a-z0-9]+/)
45
- .filter((term) => term.length > 0 && !STOP_WORDS.has(term))
46
- .map(stem);
138
+ .replace(/[^a-z0-9_$]/g, "_");
47
139
  }
48
- /** Okapi BM25 (k1 1.2, b 0.75): the names of the best matches, best first; ties keep document order. */
49
- export function rankDocuments(query, documents, limit) {
50
- const k1 = 1.2;
51
- const b = 0.75;
52
- const queryTerms = [...new Set(tokenize(query))];
53
- if (queryTerms.length === 0 || documents.length === 0 || limit <= 0)
54
- return [];
55
- const termCounts = documents.map((document) => {
56
- const counts = new Map();
57
- for (const term of tokenize(document.text))
58
- counts.set(term, (counts.get(term) ?? 0) + 1);
59
- return counts;
60
- });
61
- const lengths = termCounts.map((counts) => [...counts.values()].reduce((sum, count) => sum + count, 0));
62
- const averageLength = lengths.reduce((sum, length) => sum + length, 0) / documents.length || 1;
63
- const idf = new Map(queryTerms.map((term) => {
64
- const frequency = termCounts.filter((counts) => counts.has(term)).length;
65
- return [term, Math.log(1 + (documents.length - frequency + 0.5) / (frequency + 0.5))];
66
- }));
67
- const matches = [];
68
- documents.forEach((document, index) => {
69
- let score = 0;
70
- for (const term of queryTerms) {
71
- const count = termCounts[index].get(term);
72
- if (!count)
73
- continue;
74
- const norm = k1 * (1 - b + (b * lengths[index]) / averageLength);
75
- score += (idf.get(term) ?? 0) * ((count * (k1 + 1)) / (count + norm));
76
- }
77
- if (score > 0)
78
- matches.push({ name: document.name, score });
79
- });
80
- return matches
81
- .sort((left, right) => right.score - left.score)
82
- .slice(0, limit)
83
- .map((match) => match.name);
140
+ /**
141
+ * Whether the whole query is the tool's name: its full name or a suffix after a `__` separator, so `thread_read` and
142
+ * `app__thread_read` both name `mcp__app__thread_read`.
143
+ */
144
+ function isExactNameQuery(query, name) {
145
+ const normalized = normalizeName(name);
146
+ return normalized === query || normalized.endsWith(`__${query}`);
84
147
  }
85
148
  //# sourceMappingURL=search.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC;IAC/C,GAAG;IACH,IAAI;IACJ,KAAK;IACL,KAAK;IACL,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,KAAK;IACL,MAAM;IACN,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,MAAM;IACN,KAAK;IACL,MAAM;IACN,IAAI;IACJ,MAAM;CACN,CAAC,CAAC;AAEH,wFAAwF;AACxF,SAAS,IAAI,CAAC,IAAY;IACzB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5E,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxF,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5F,OAAO,IAAI,CAAC;AACb,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,QAAQ,CAAC,IAAY;IACpC,OAAO,IAAI;SACT,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,uBAAuB,EAAE,OAAO,CAAC;SACzC,WAAW,EAAE;SACb,KAAK,CAAC,YAAY,CAAC;SACnB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;SAC1D,GAAG,CAAC,IAAI,CAAC,CAAC;AACb,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,SAAoC,EAAE,KAAa;IAC/F,MAAM,EAAE,GAAG,GAAG,CAAC;IACf,MAAM,CAAC,GAAG,IAAI,CAAC;IACf,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/E,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QAC7C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;QACzC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1F,OAAO,MAAM,CAAC;IACf,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACxG,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC;IAC/F,MAAM,GAAG,GAAG,IAAI,GAAG,CAClB,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACvB,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;QACzE,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAU,CAAC;IAChG,CAAC,CAAC,CACF,CAAC;IACF,MAAM,OAAO,GAAsC,EAAE,CAAC;IACtD,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE;QACrC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,CAAC,KAAK;gBAAE,SAAS;YACrB,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAE,CAAC,GAAG,aAAa,CAAC,CAAC;YAClE,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IACH,OAAO,OAAO;SACZ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;SAC/C,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;SACf,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC","sourcesContent":["/**\n * BM25 tool search for code mode's `searchTools()` and the `tool_search` tool, a copy of the coding agent's tool search\n * ranking: durable keeps its own copy of what it needs from the coding agent.\n */\n\nexport type SearchDocument = { readonly name: string; readonly text: string };\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n\t\"a\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"be\",\n\t\"by\",\n\t\"for\",\n\t\"from\",\n\t\"in\",\n\t\"is\",\n\t\"it\",\n\t\"of\",\n\t\"on\",\n\t\"or\",\n\t\"that\",\n\t\"the\",\n\t\"this\",\n\t\"to\",\n\t\"with\",\n]);\n\n/** Naive singular form, so `issues` matches `issue` and `searches` matches `search`. */\nfunction stem(term: string): string {\n\tif (term.length > 4 && term.endsWith(\"ies\")) return `${term.slice(0, -3)}y`;\n\tif (term.length > 4 && /(ches|shes|sses|xes|zes)$/.test(term)) return term.slice(0, -2);\n\tif (term.length > 3 && term.endsWith(\"s\") && !term.endsWith(\"ss\")) return term.slice(0, -1);\n\treturn term;\n}\n\n/** Lowercase terms, split at camelCase boundaries and non-alphanumerics, without stop words. */\nexport function tokenize(text: string): string[] {\n\treturn text\n\t\t.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n\t\t.replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n\t\t.toLowerCase()\n\t\t.split(/[^a-z0-9]+/)\n\t\t.filter((term) => term.length > 0 && !STOP_WORDS.has(term))\n\t\t.map(stem);\n}\n\n/** Okapi BM25 (k1 1.2, b 0.75): the names of the best matches, best first; ties keep document order. */\nexport function rankDocuments(query: string, documents: readonly SearchDocument[], limit: number): string[] {\n\tconst k1 = 1.2;\n\tconst b = 0.75;\n\tconst queryTerms = [...new Set(tokenize(query))];\n\tif (queryTerms.length === 0 || documents.length === 0 || limit <= 0) return [];\n\tconst termCounts = documents.map((document) => {\n\t\tconst counts = new Map<string, number>();\n\t\tfor (const term of tokenize(document.text)) counts.set(term, (counts.get(term) ?? 0) + 1);\n\t\treturn counts;\n\t});\n\tconst lengths = termCounts.map((counts) => [...counts.values()].reduce((sum, count) => sum + count, 0));\n\tconst averageLength = lengths.reduce((sum, length) => sum + length, 0) / documents.length || 1;\n\tconst idf = new Map(\n\t\tqueryTerms.map((term) => {\n\t\t\tconst frequency = termCounts.filter((counts) => counts.has(term)).length;\n\t\t\treturn [term, Math.log(1 + (documents.length - frequency + 0.5) / (frequency + 0.5))] as const;\n\t\t}),\n\t);\n\tconst matches: { name: string; score: number }[] = [];\n\tdocuments.forEach((document, index) => {\n\t\tlet score = 0;\n\t\tfor (const term of queryTerms) {\n\t\t\tconst count = termCounts[index]!.get(term);\n\t\t\tif (!count) continue;\n\t\t\tconst norm = k1 * (1 - b + (b * lengths[index]!) / averageLength);\n\t\t\tscore += (idf.get(term) ?? 0) * ((count * (k1 + 1)) / (count + norm));\n\t\t}\n\t\tif (score > 0) matches.push({ name: document.name, score });\n\t});\n\treturn matches\n\t\t.sort((left, right) => right.score - left.score)\n\t\t.slice(0, limit)\n\t\t.map((match) => match.name);\n}\n"]}
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,SAAoC,EAAE,KAAa;IAC/F,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5C,IAAI,UAAU,CAAC,IAAI,KAAK,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7E,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC7C,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,GAAG,GAAG,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IAC5G,MAAM,GAAG,GAAG,IAAI,GAAG,CAClB,CAAC,GAAG,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QAC5B,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAC/B,CAAC,QAAQ,EAAE,EAAE,CACZ,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CACnG,CAAC,MAAM,CAAC;QACT,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAU,CAAC;IAC9F,CAAC,CAAC,CACF,CAAC;IACF,MAAM,aAAa,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;IAC/E,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,OAAO,GAAuE,EAAE,CAAC;IACvF,KAAK,MAAM,QAAQ,IAAI,OAAO,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,CAAC;QACtE,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjD,MAAM,MAAM,GACX,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;gBAChD,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC1D,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;YACrC,IAAI,MAAM,GAAG,CAAC;gBAAE,OAAO,EAAE,CAAC;YAC1B,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAE,GAAG,MAAM,CAAC;QAClC,CAAC;QACD,IAAI,KAAK,KAAK,CAAC;YAAE,SAAS;QAC1B,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC;QACtC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAAE,KAAK,IAAI,aAAa,CAAC;QAC5F,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,CAAC,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3G,CAAC;IACD,OAAO,OAAO;SACZ,IAAI,CACJ,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CACrG;SACA,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;SACf,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,EAAE,GAAG,GAAG,CAAC;AAEf,MAAM,CAAC,GAAG,IAAI,CAAC;AAEf;;;GAGG;AACH,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB;;;GAGG;AACH,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAc7B,SAAS,aAAa,CAAC,QAAwB;IAC9C,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAClG,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO;QACN,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,SAAS,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3C,cAAc,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,CAAC,CAAC;QACtE,UAAU;QACV,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC;QAC3E,cAAc,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;KAC/D,CAAC;AACH,CAAC;AAED,MAAM,aAAa,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC;AAE7E,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC;IAC/C,GAAG;IACH,IAAI;IACJ,KAAK;IACL,KAAK;IACL,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,KAAK;IACL,MAAM;IACN,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,MAAM;IACN,KAAK;IACL,MAAM;IACN,IAAI;IACJ,MAAM;CACN,CAAC,CAAC;AAEH,kGAAkG;AAClG,MAAM,UAAU,QAAQ,CAAC,IAAY;IACpC,MAAM,UAAU,GAAG,IAAI;SACrB,SAAS,CAAC,KAAK,CAAC;SAChB,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,uBAAuB,EAAE,OAAO,CAAC;SACzC,WAAW,EAAE;SACb,OAAO,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;IACxC,8GAA8G;IAC9G,iEAAiE;IACjE,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC;QAC5C,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC;QACvB,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3G,OAAO,KAAK;SACV,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;SAC1D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,wFAAwF;AACxF,SAAS,IAAI,CAAC,IAAY;IACzB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5E,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxF,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5F,OAAO,IAAI,CAAC;AACb,CAAC;AAED,4GAA4G;AAC5G,SAAS,aAAa,CAAC,IAAY;IAClC,OAAO,IAAI;SACT,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,KAAa,EAAE,IAAY;IACpD,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,UAAU,KAAK,KAAK,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;AAClE,CAAC","sourcesContent":["/**\n * BM25 tool search for code mode's `searchTools()` and the `tool_search` tool, a copy of the coding agent's tool search\n * ranking: durable keeps its own copy of what it needs from the coding agent.\n */\n\n/** A tool as the ranker sees it: its name, its text, and the text of its namespace. */\nexport type SearchDocument = {\n\treadonly name: string;\n\t/** Description and parameter names and descriptions; not the name, which the ranker reads from `name`. */\n\treadonly text: string;\n\t/** Name, description, and instructions of the tool's namespace. */\n\treadonly namespaceText?: string;\n};\n\n/**\n * Okapi BM25 (k1 1.2, b 0.75) over `text`, with the name and the namespace as separate fields: the names of the best\n * matches, best first; ties keep document order.\n * - A tool that matches more of the query's terms, in any field, ranks above one that matches fewer, whatever their\n * scores: for `screenshot window`, a tool whose description has both words beats `window` whose name has one.\n * - A query term in the name scores more than any number of mentions in the text.\n * - A tool whose whole local name (after the last `__`) is in the query gets a bonus worth one more match of every\n * query term, so among tools matching as many terms, `peekaboo app` finds `mcp__peekaboo__app` over tools that\n * only mention the words.\n * - A query that is exactly a tool's name, or its name after a `__`, ranks that tool first, before coverage.\n *\n * Only `text` is length-normalized, so a long name or long namespace instructions do not lower a tool's score.\n */\nexport function rankDocuments(query: string, documents: readonly SearchDocument[], limit: number): string[] {\n\tconst queryTerms = new Set(tokenize(query));\n\tif (queryTerms.size === 0 || documents.length === 0 || limit <= 0) return [];\n\tconst indexed = documents.map(indexDocument);\n\tconst averageLength = indexed.reduce((sum, document) => sum + document.textLength, 0) / indexed.length || 1;\n\tconst idf = new Map(\n\t\t[...queryTerms].map((term) => {\n\t\t\tconst frequency = indexed.filter(\n\t\t\t\t(document) =>\n\t\t\t\t\tdocument.nameTerms.has(term) || document.textCounts.has(term) || document.namespaceTerms.has(term),\n\t\t\t).length;\n\t\t\treturn [term, Math.log(1 + (indexed.length - frequency + 0.5) / (frequency + 0.5))] as const;\n\t\t}),\n\t);\n\tconst fullNameBonus = [...idf.values()].reduce((sum, value) => sum + value, 0);\n\tconst exactQuery = normalizeName(query);\n\tconst matches: { name: string; score: number; covered: number; exact: boolean }[] = [];\n\tfor (const document of indexed) {\n\t\tconst norm = K1 * (1 - B + (B * document.textLength) / averageLength);\n\t\tlet score = 0;\n\t\tlet covered = 0;\n\t\tfor (const term of queryTerms) {\n\t\t\tconst count = document.textCounts.get(term) ?? 0;\n\t\t\tconst weight =\n\t\t\t\t(document.nameTerms.has(term) ? NAME_WEIGHT : 0) +\n\t\t\t\t(document.namespaceTerms.has(term) ? NAMESPACE_WEIGHT : 0) +\n\t\t\t\t(count * (K1 + 1)) / (count + norm);\n\t\t\tif (weight > 0) covered++;\n\t\t\tscore += idf.get(term)! * weight;\n\t\t}\n\t\tif (score === 0) continue;\n\t\tconst local = document.localNameTerms;\n\t\tif (local.length > 0 && local.every((term) => queryTerms.has(term))) score += fullNameBonus;\n\t\tmatches.push({ name: document.name, score, covered, exact: isExactNameQuery(exactQuery, document.name) });\n\t}\n\treturn matches\n\t\t.sort(\n\t\t\t(left, right) =>\n\t\t\t\tNumber(right.exact) - Number(left.exact) || right.covered - left.covered || right.score - left.score,\n\t\t)\n\t\t.slice(0, limit)\n\t\t.map((match) => match.name);\n}\n\nconst K1 = 1.2;\n\nconst B = 0.75;\n\n/**\n * A query term in the name counts this many times its IDF. The text part of a term is at most `K1 + 1` (2.2) times\n * its IDF and about 1 for a single mention, so a name match beats any number of mentions in the text.\n */\nconst NAME_WEIGHT = 3;\n\n/**\n * A query term in the namespace text counts this many times its IDF. Every tool of the namespace shares it, so it helps\n * find the namespace but not the tool in it.\n */\nconst NAMESPACE_WEIGHT = 0.5;\n\n/** A document split into the fields the ranker weighs differently. */\ntype IndexedDocument = {\n\tname: string;\n\t/** Terms of the name: `mcp`, `app`, `thread`, `read` for `mcp__app__thread_read`. */\n\tnameTerms: ReadonlySet<string>;\n\t/** Terms of the name after its last `__`, the part models say: `thread`, `read`. */\n\tlocalNameTerms: readonly string[];\n\ttextCounts: ReadonlyMap<string, number>;\n\ttextLength: number;\n\tnamespaceTerms: ReadonlySet<string>;\n};\n\nfunction indexDocument(document: SearchDocument): IndexedDocument {\n\tconst textCounts = new Map<string, number>();\n\tfor (const term of tokenize(document.text)) textCounts.set(term, (textCounts.get(term) ?? 0) + 1);\n\tconst segments = document.name.split(\"__\");\n\treturn {\n\t\tname: document.name,\n\t\tnameTerms: new Set(tokenize(document.name)),\n\t\tlocalNameTerms: [...new Set(tokenize(segments[segments.length - 1]!))],\n\t\ttextCounts,\n\t\ttextLength: [...textCounts.values()].reduce((sum, count) => sum + count, 0),\n\t\tnamespaceTerms: new Set(tokenize(document.namespaceText ?? \"\")),\n\t};\n}\n\nconst wordSegmenter = new Intl.Segmenter(undefined, { granularity: \"word\" });\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n\t\"a\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"be\",\n\t\"by\",\n\t\"for\",\n\t\"from\",\n\t\"in\",\n\t\"is\",\n\t\"it\",\n\t\"of\",\n\t\"on\",\n\t\"or\",\n\t\"that\",\n\t\"the\",\n\t\"this\",\n\t\"to\",\n\t\"with\",\n]);\n\n/** Unicode word segmentation with identifier boundaries and English stop words/singular forms. */\nexport function tokenize(text: string): string[] {\n\tconst normalized = text\n\t\t.normalize(\"NFC\")\n\t\t.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n\t\t.replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n\t\t.toLowerCase()\n\t\t.replace(/[^\\p{L}\\p{M}\\p{N}]+/gu, \" \");\n\t// The segmenter costs about 15 times a split. Plain ASCII splits into the same words at spaces, so only other\n\t// text, such as Chinese written without spaces, goes through it.\n\tconst words = /^[a-z0-9 ]*$/.test(normalized)\n\t\t? normalized.split(\" \")\n\t\t: Array.from(wordSegmenter.segment(normalized), (segment) => (segment.isWordLike ? segment.segment : \"\"));\n\treturn words\n\t\t.filter((word) => word.length > 0 && !STOP_WORDS.has(word))\n\t\t.map((word) => (/^[a-z0-9]+$/.test(word) ? stem(word) : word));\n}\n\n/** Naive singular form, so `issues` matches `issue` and `searches` matches `search`. */\nfunction stem(term: string): string {\n\tif (term.length > 4 && term.endsWith(\"ies\")) return `${term.slice(0, -3)}y`;\n\tif (term.length > 4 && /(ches|shes|sses|xes|zes)$/.test(term)) return term.slice(0, -2);\n\tif (term.length > 3 && term.endsWith(\"s\") && !term.endsWith(\"ss\")) return term.slice(0, -1);\n\treturn term;\n}\n\n/** Lowercase with characters invalid in script identifiers as `_`, so `dev-radius` matches `dev_radius`. */\nfunction normalizeName(name: string): string {\n\treturn name\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9_$]/g, \"_\");\n}\n\n/**\n * Whether the whole query is the tool's name: its full name or a suffix after a `__` separator, so `thread_read` and\n * `app__thread_read` both name `mcp__app__thread_read`.\n */\nfunction isExactNameQuery(query: string, name: string): boolean {\n\tconst normalized = normalizeName(name);\n\treturn normalized === query || normalized.endsWith(`__${query}`);\n}\n"]}
@@ -0,0 +1,71 @@
1
+ import type { Context, JsonValue } from "@earendil-works/chord";
2
+ import type { CommitResult, ConversationId, ConversationQuery, ConversationRecord, Cursor, DocumentAddress, DocumentId, DocumentPoint, DocumentQuery, DocumentRecord, EntryId, EntryQuery, EntryRecord, Id, Page, Seq, Storage, StorageWrite, StoredDocument, SubmissionId, SubmissionQuery, SubmissionRecord, TaskId, TaskQuery, TaskRecord } from "../types.ts";
3
+ type AnyTaskRecord = TaskRecord<JsonValue, JsonValue, JsonValue>;
4
+ type StoredEntry = {
5
+ readonly entry: EntryRecord;
6
+ readonly commitSeq: Seq;
7
+ };
8
+ /**
9
+ * Storage that serves hot record reads from memory: conversations, tasks, and written submissions, each table bounded
10
+ * by a count, the least recently used record leaving first, except live tasks, which are kept until they end, and the
11
+ * ended tasks a task waits on, kept until it moved on from its wait: a task's record is read again at each of its state
12
+ * changes, a waiter reads the outcomes of what it waited on once it resumes (`runtime.outcomes()`), and both are
13
+ * bounded by live work, which the scheduler keeps in memory too. Without that, a burst of more tasks than the count
14
+ * would make each of those reads a Storage read on the line. Every commit goes through it, and one process owns a
15
+ * Storage, so it applies each batch as it is published and never goes stale: `commit()`'s batches when the Session publishes
16
+ * them (`published()`), not at their durability receipts. A read
17
+ * result is kept only when no commit published since the read started wrote its record, as the read may not observe
18
+ * that commit; versions are kept only for records with a read in flight. Cached values are frozen and shared. Entries,
19
+ * scans, and documents pass through: the Session keeps documents and the newest entry per conversation itself, and
20
+ * context ranges hold the entries a run reads.
21
+ *
22
+ * Not for a read-only Session: another process writes its Storage.
23
+ */
24
+ export declare class CachedStorage implements Storage {
25
+ #private;
26
+ constructor(storage: Storage);
27
+ /** The backend's own scope for a publication (`Storage.publishing`), when it has one. */
28
+ get publishing(): Storage["publishing"];
29
+ /** Pass the batches through; the Session applies each one with `published()` when it publishes it. */
30
+ commit(batches: readonly (readonly StorageWrite[])[], receive: (index: number, seq: Seq) => void, context: Context): Promise<CommitResult>;
31
+ /**
32
+ * Apply a batch `commit()` stored, when the Session publishes it. Its records are frozen in place and kept as
33
+ * they are, no copies: the Session's components that follow the publication, such as the scheduler's live tasks,
34
+ * hold the same objects, so a live task's record is in memory once.
35
+ */
36
+ published(writes: readonly StorageWrite[]): void;
37
+ mintId<I extends Id<string>>(): Promise<I>;
38
+ conversation(id: ConversationId, context: Context): Promise<ConversationRecord | undefined>;
39
+ scanConversations(query: ConversationQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<ConversationRecord, Cursor>>;
40
+ entry(id: EntryId, context: Context): Promise<StoredEntry | undefined>;
41
+ entry(conversationId: ConversationId, id: EntryId, context: Context): Promise<StoredEntry | undefined>;
42
+ findLatestHeadMarker(conversationId: ConversationId, atOrBeforeEntryId: EntryId | undefined, context: Context): Promise<(EntryRecord & {
43
+ readonly head: EntryId;
44
+ }) | undefined>;
45
+ scanEntries(query: EntryQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<EntryRecord, Cursor>>;
46
+ task(id: TaskId, context: Context): Promise<AnyTaskRecord | undefined>;
47
+ scanTasks(query: TaskQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<AnyTaskRecord, Cursor>>;
48
+ /** Cached as written only: this process writes every submission it settles, and a read one is usually settled. */
49
+ submission(id: SubmissionId, context: Context): Promise<SubmissionRecord | undefined>;
50
+ scanSubmissions(query: SubmissionQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<SubmissionRecord, Cursor>>;
51
+ submissionByRequest(conversationId: ConversationId, requestId: string, context: Context): Promise<SubmissionRecord | undefined>;
52
+ findDocument(address: DocumentAddress, at: DocumentPoint, context: Context): Promise<DocumentRecord | undefined>;
53
+ document(id: DocumentId, at: DocumentPoint, context: Context): Promise<StoredDocument | undefined>;
54
+ scanDocuments(query: DocumentQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<DocumentRecord, Cursor>>;
55
+ close(context: Context): Promise<void>;
56
+ /** Records kept per table, for tests that bound memory. */
57
+ get sizes(): {
58
+ readonly conversations: number;
59
+ readonly tasks: number;
60
+ readonly liveTasks: number;
61
+ readonly awaited: number;
62
+ readonly waits: number;
63
+ readonly submissions: number;
64
+ };
65
+ /** Records with a read in flight, for tests that bound memory. */
66
+ get readsInFlight(): number;
67
+ }
68
+ /** `storage`'s publication scope (`Storage.publishing`), for a wrapper to expose as its own; none when it has none. */
69
+ export declare function scopeOf(storage: Storage): Storage["publishing"];
70
+ export {};
71
+ //# sourceMappingURL=cached-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cached-storage.d.ts","sourceRoot":"","sources":["../../src/session/cached-storage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Storage that serves hot record reads from memory: conversations, tasks, and written submissions, each table bounded
3
+ * by a count, the least recently used record leaving first, except live tasks, which are kept until they end, and the
4
+ * ended tasks a task waits on, kept until it moved on from its wait: a task's record is read again at each of its state
5
+ * changes, a waiter reads the outcomes of what it waited on once it resumes (`runtime.outcomes()`), and both are
6
+ * bounded by live work, which the scheduler keeps in memory too. Without that, a burst of more tasks than the count
7
+ * would make each of those reads a Storage read on the line. Every commit goes through it, and one process owns a
8
+ * Storage, so it applies each batch as it is published and never goes stale: `commit()`'s batches when the Session publishes
9
+ * them (`published()`), not at their durability receipts. A read
10
+ * result is kept only when no commit published since the read started wrote its record, as the read may not observe
11
+ * that commit; versions are kept only for records with a read in flight. Cached values are frozen and shared. Entries,
12
+ * scans, and documents pass through: the Session keeps documents and the newest entry per conversation itself, and
13
+ * context ranges hold the entries a run reads.
14
+ *
15
+ * Not for a read-only Session: another process writes its Storage.
16
+ */
17
+ export class CachedStorage {
18
+ #storage;
19
+ #conversations = new Lru(4096);
20
+ /** Live (non-terminal) task records, until their task ends. */
21
+ #liveTasks = new Map();
22
+ /**
23
+ * Tasks some task waits on, or waited on until its wait ended and it has not moved on since: how many such waits name
24
+ * each, and its ended record once it ended.
25
+ */
26
+ #awaited = new Map();
27
+ /** Per task that waits, or left its wait and has not changed since: what it waited on, and whether it left. */
28
+ #waits = new Map();
29
+ // Ended task records carry inputs and outcomes, such as a tool call's arguments, so fewer are kept.
30
+ #tasks = new Lru(256);
31
+ #submissions = new Lru(256);
32
+ /** Per record with a read in flight: the reads, and how many published commits wrote it since the first began. */
33
+ #reads = new Map();
34
+ constructor(storage) {
35
+ this.#storage = storage;
36
+ }
37
+ /** The backend's own scope for a publication (`Storage.publishing`), when it has one. */
38
+ get publishing() {
39
+ return scopeOf(this.#storage);
40
+ }
41
+ /** Pass the batches through; the Session applies each one with `published()` when it publishes it. */
42
+ commit(batches, receive, context) {
43
+ return this.#storage.commit(batches, receive, context);
44
+ }
45
+ /**
46
+ * Apply a batch `commit()` stored, when the Session publishes it. Its records are frozen in place and kept as
47
+ * they are, no copies: the Session's components that follow the publication, such as the scheduler's live tasks,
48
+ * hold the same objects, so a live task's record is in memory once.
49
+ */
50
+ published(writes) {
51
+ for (const write of writes) {
52
+ switch (write.type) {
53
+ case "conversation":
54
+ this.#wrote(write.value.id);
55
+ this.#conversations.set(write.value.id, freeze(write.value));
56
+ break;
57
+ case "task":
58
+ this.#wrote(write.value.id);
59
+ this.#followWait(write.value);
60
+ this.#keepTask(freeze(write.value));
61
+ break;
62
+ case "submission":
63
+ this.#wrote(write.value.id);
64
+ this.#submissions.set(write.value.id, freeze(write.value));
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ mintId() {
70
+ return this.#storage.mintId();
71
+ }
72
+ conversation(id, context) {
73
+ const cached = this.#conversations.get(id);
74
+ if (cached !== undefined)
75
+ return Promise.resolve(cached);
76
+ return this.#keep(id, () => this.#storage.conversation(id, context), (record) => this.#conversations.set(id, record));
77
+ }
78
+ scanConversations(query, limit, cursor, context) {
79
+ return this.#storage.scanConversations(query, limit, cursor, context);
80
+ }
81
+ entry(first, second, third) {
82
+ if (third === undefined)
83
+ return this.#storage.entry(first, second);
84
+ return this.#storage.entry(first, second, third);
85
+ }
86
+ findLatestHeadMarker(conversationId, atOrBeforeEntryId, context) {
87
+ return this.#storage.findLatestHeadMarker(conversationId, atOrBeforeEntryId, context);
88
+ }
89
+ scanEntries(query, limit, cursor, context) {
90
+ return this.#storage.scanEntries(query, limit, cursor, context);
91
+ }
92
+ task(id, context) {
93
+ const cached = this.#liveTasks.get(id) ?? this.#awaited.get(id)?.record ?? this.#tasks.get(id);
94
+ if (cached !== undefined)
95
+ return Promise.resolve(cached);
96
+ return this.#keep(id, () => this.#storage.task(id, context), (record) => this.#keepTask(record));
97
+ }
98
+ scanTasks(query, limit, cursor, context) {
99
+ return this.#storage.scanTasks(query, limit, cursor, context);
100
+ }
101
+ /** Cached as written only: this process writes every submission it settles, and a read one is usually settled. */
102
+ submission(id, context) {
103
+ const cached = this.#submissions.get(id);
104
+ return cached === undefined ? this.#storage.submission(id, context) : Promise.resolve(cached);
105
+ }
106
+ scanSubmissions(query, limit, cursor, context) {
107
+ return this.#storage.scanSubmissions(query, limit, cursor, context);
108
+ }
109
+ submissionByRequest(conversationId, requestId, context) {
110
+ return this.#storage.submissionByRequest(conversationId, requestId, context);
111
+ }
112
+ findDocument(address, at, context) {
113
+ return this.#storage.findDocument(address, at, context);
114
+ }
115
+ document(id, at, context) {
116
+ return this.#storage.document(id, at, context);
117
+ }
118
+ scanDocuments(query, limit, cursor, context) {
119
+ return this.#storage.scanDocuments(query, limit, cursor, context);
120
+ }
121
+ close(context) {
122
+ return this.#storage.close(context);
123
+ }
124
+ /** Records kept per table, for tests that bound memory. */
125
+ get sizes() {
126
+ return {
127
+ conversations: this.#conversations.size,
128
+ tasks: this.#tasks.size,
129
+ liveTasks: this.#liveTasks.size,
130
+ awaited: this.#awaited.size,
131
+ waits: this.#waits.size,
132
+ submissions: this.#submissions.size,
133
+ };
134
+ }
135
+ /** Keep a frozen task record: a live one until its task ends, an ended one in the bounded table. */
136
+ #keepTask(record) {
137
+ if (record.state.status === "terminal") {
138
+ this.#liveTasks.delete(record.id);
139
+ const awaited = this.#awaited.get(record.id);
140
+ if (awaited !== undefined)
141
+ awaited.record = record;
142
+ else
143
+ this.#tasks.set(record.id, record);
144
+ return;
145
+ }
146
+ this.#tasks.delete(record.id);
147
+ this.#liveTasks.set(record.id, record);
148
+ }
149
+ /**
150
+ * Follow a task's waits from its written record: a wait holds the ended records of its `on` until the task, after
151
+ * leaving the wait, changes again (or ends), as the phase that resumes reads their outcomes before its next commit.
152
+ */
153
+ #followWait(record) {
154
+ const wait = this.#waits.get(record.id);
155
+ if (record.state.status === "waiting") {
156
+ if (wait !== undefined)
157
+ this.#release(wait.on);
158
+ const on = record.state.on;
159
+ this.#waits.set(record.id, { on, left: false });
160
+ for (const id of on) {
161
+ let awaited = this.#awaited.get(id);
162
+ if (awaited === undefined) {
163
+ // One that ended already moves over from the bounded table.
164
+ const ended = this.#tasks.get(id);
165
+ this.#tasks.delete(id);
166
+ awaited = ended === undefined ? { count: 0 } : { count: 0, record: ended };
167
+ this.#awaited.set(id, awaited);
168
+ }
169
+ awaited.count++;
170
+ }
171
+ return;
172
+ }
173
+ if (wait === undefined)
174
+ return;
175
+ if (!wait.left && record.state.status !== "terminal") {
176
+ wait.left = true;
177
+ return;
178
+ }
179
+ this.#waits.delete(record.id);
180
+ this.#release(wait.on);
181
+ }
182
+ /** A wait on `on` is over: what no other wait names goes back to the bounded table. */
183
+ #release(on) {
184
+ for (const id of on) {
185
+ const awaited = this.#awaited.get(id);
186
+ if (--awaited.count > 0)
187
+ continue;
188
+ this.#awaited.delete(id);
189
+ if (awaited.record !== undefined)
190
+ this.#tasks.set(id, awaited.record);
191
+ }
192
+ }
193
+ /** A published commit wrote record `id`: a read of it in flight may have missed that. */
194
+ #wrote(id) {
195
+ const reads = this.#reads.get(id);
196
+ if (reads !== undefined)
197
+ reads.version++;
198
+ }
199
+ /** Resolve with a read's result, frozen; keep a found record unless a commit published since wrote it. */
200
+ async #keep(id, read, keep) {
201
+ let reads = this.#reads.get(id);
202
+ if (reads === undefined) {
203
+ reads = { count: 0, version: 0 };
204
+ this.#reads.set(id, reads);
205
+ }
206
+ reads.count++;
207
+ const version = reads.version;
208
+ let value;
209
+ try {
210
+ value = await read();
211
+ }
212
+ finally {
213
+ if (--reads.count === 0)
214
+ this.#reads.delete(id);
215
+ }
216
+ if (value === undefined)
217
+ return undefined;
218
+ freeze(value);
219
+ if (reads.version === version)
220
+ keep(value);
221
+ return value;
222
+ }
223
+ /** Records with a read in flight, for tests that bound memory. */
224
+ get readsInFlight() {
225
+ return this.#reads.size;
226
+ }
227
+ }
228
+ /** `storage`'s publication scope (`Storage.publishing`), for a wrapper to expose as its own; none when it has none. */
229
+ export function scopeOf(storage) {
230
+ if (storage.publishing === undefined)
231
+ return undefined;
232
+ return (publish, context) => storage.publishing(publish, context);
233
+ }
234
+ /** A Map bounded to `capacity` entries that drops the least recently used one. */
235
+ class Lru {
236
+ #map = new Map();
237
+ #capacity;
238
+ constructor(capacity) {
239
+ this.#capacity = capacity;
240
+ }
241
+ get(key) {
242
+ const value = this.#map.get(key);
243
+ if (value === undefined)
244
+ return undefined;
245
+ this.#map.delete(key);
246
+ this.#map.set(key, value);
247
+ return value;
248
+ }
249
+ get size() {
250
+ return this.#map.size;
251
+ }
252
+ delete(key) {
253
+ this.#map.delete(key);
254
+ }
255
+ set(key, value) {
256
+ this.#map.delete(key);
257
+ this.#map.set(key, value);
258
+ if (this.#map.size > this.#capacity)
259
+ this.#map.delete(this.#map.keys().next().value);
260
+ }
261
+ }
262
+ function freeze(value) {
263
+ if (value === null || typeof value !== "object" || Object.isFrozen(value))
264
+ return value;
265
+ for (const child of Object.values(value))
266
+ freeze(child);
267
+ return Object.freeze(value);
268
+ }
269
+ //# sourceMappingURL=cached-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cached-storage.js","sourceRoot":"","sources":["../../src/session/cached-storage.ts"],"names":[],"mappings":"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","sourcesContent":["// Write-through cache of hot Storage record reads for the Session that owns the Storage.\nimport type { Context, JsonValue } from \"@earendil-works/chord\";\nimport type {\n\tCommitResult,\n\tConversationId,\n\tConversationQuery,\n\tConversationRecord,\n\tCursor,\n\tDocumentAddress,\n\tDocumentId,\n\tDocumentPoint,\n\tDocumentQuery,\n\tDocumentRecord,\n\tEntryId,\n\tEntryQuery,\n\tEntryRecord,\n\tId,\n\tPage,\n\tSeq,\n\tStorage,\n\tStorageWrite,\n\tStoredDocument,\n\tSubmissionId,\n\tSubmissionQuery,\n\tSubmissionRecord,\n\tTaskId,\n\tTaskQuery,\n\tTaskRecord,\n} from \"../types.ts\";\n\ntype AnyTaskRecord = TaskRecord<JsonValue, JsonValue, JsonValue>;\ntype StoredEntry = { readonly entry: EntryRecord; readonly commitSeq: Seq };\n\n/**\n * Storage that serves hot record reads from memory: conversations, tasks, and written submissions, each table bounded\n * by a count, the least recently used record leaving first, except live tasks, which are kept until they end, and the\n * ended tasks a task waits on, kept until it moved on from its wait: a task's record is read again at each of its state\n * changes, a waiter reads the outcomes of what it waited on once it resumes (`runtime.outcomes()`), and both are\n * bounded by live work, which the scheduler keeps in memory too. Without that, a burst of more tasks than the count\n * would make each of those reads a Storage read on the line. Every commit goes through it, and one process owns a\n * Storage, so it applies each batch as it is published and never goes stale: `commit()`'s batches when the Session publishes\n * them (`published()`), not at their durability receipts. A read\n * result is kept only when no commit published since the read started wrote its record, as the read may not observe\n * that commit; versions are kept only for records with a read in flight. Cached values are frozen and shared. Entries,\n * scans, and documents pass through: the Session keeps documents and the newest entry per conversation itself, and\n * context ranges hold the entries a run reads.\n *\n * Not for a read-only Session: another process writes its Storage.\n */\nexport class CachedStorage implements Storage {\n\treadonly #storage: Storage;\n\treadonly #conversations = new Lru<ConversationId, ConversationRecord>(4096);\n\t/** Live (non-terminal) task records, until their task ends. */\n\treadonly #liveTasks = new Map<TaskId, AnyTaskRecord>();\n\t/**\n\t * Tasks some task waits on, or waited on until its wait ended and it has not moved on since: how many such waits name\n\t * each, and its ended record once it ended.\n\t */\n\treadonly #awaited = new Map<TaskId, { count: number; record?: AnyTaskRecord }>();\n\t/** Per task that waits, or left its wait and has not changed since: what it waited on, and whether it left. */\n\treadonly #waits = new Map<TaskId, { readonly on: readonly TaskId[]; left: boolean }>();\n\t// Ended task records carry inputs and outcomes, such as a tool call's arguments, so fewer are kept.\n\treadonly #tasks = new Lru<TaskId, AnyTaskRecord>(256);\n\treadonly #submissions = new Lru<SubmissionId, SubmissionRecord>(256);\n\t/** Per record with a read in flight: the reads, and how many published commits wrote it since the first began. */\n\treadonly #reads = new Map<Id<string>, { count: number; version: number }>();\n\n\tconstructor(storage: Storage) {\n\t\tthis.#storage = storage;\n\t}\n\n\t/** The backend's own scope for a publication (`Storage.publishing`), when it has one. */\n\tget publishing(): Storage[\"publishing\"] {\n\t\treturn scopeOf(this.#storage);\n\t}\n\n\t/** Pass the batches through; the Session applies each one with `published()` when it publishes it. */\n\tcommit(\n\t\tbatches: readonly (readonly StorageWrite[])[],\n\t\treceive: (index: number, seq: Seq) => void,\n\t\tcontext: Context,\n\t): Promise<CommitResult> {\n\t\treturn this.#storage.commit(batches, receive, context);\n\t}\n\n\t/**\n\t * Apply a batch `commit()` stored, when the Session publishes it. Its records are frozen in place and kept as\n\t * they are, no copies: the Session's components that follow the publication, such as the scheduler's live tasks,\n\t * hold the same objects, so a live task's record is in memory once.\n\t */\n\tpublished(writes: readonly StorageWrite[]): void {\n\t\tfor (const write of writes) {\n\t\t\tswitch (write.type) {\n\t\t\t\tcase \"conversation\":\n\t\t\t\t\tthis.#wrote(write.value.id);\n\t\t\t\t\tthis.#conversations.set(write.value.id, freeze(write.value));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"task\":\n\t\t\t\t\tthis.#wrote(write.value.id);\n\t\t\t\t\tthis.#followWait(write.value);\n\t\t\t\t\tthis.#keepTask(freeze(write.value));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"submission\":\n\t\t\t\t\tthis.#wrote(write.value.id);\n\t\t\t\t\tthis.#submissions.set(write.value.id, freeze(write.value));\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tmintId<I extends Id<string>>(): Promise<I> {\n\t\treturn this.#storage.mintId<I>();\n\t}\n\n\tconversation(id: ConversationId, context: Context): Promise<ConversationRecord | undefined> {\n\t\tconst cached = this.#conversations.get(id);\n\t\tif (cached !== undefined) return Promise.resolve(cached);\n\t\treturn this.#keep(\n\t\t\tid,\n\t\t\t() => this.#storage.conversation(id, context),\n\t\t\t(record) => this.#conversations.set(id, record),\n\t\t);\n\t}\n\n\tscanConversations(\n\t\tquery: ConversationQuery,\n\t\tlimit: number,\n\t\tcursor: Cursor | undefined,\n\t\tcontext: Context,\n\t): Promise<Page<ConversationRecord, Cursor>> {\n\t\treturn this.#storage.scanConversations(query, limit, cursor, context);\n\t}\n\n\tentry(id: EntryId, context: Context): Promise<StoredEntry | undefined>;\n\tentry(conversationId: ConversationId, id: EntryId, context: Context): Promise<StoredEntry | undefined>;\n\tentry(\n\t\tfirst: EntryId | ConversationId,\n\t\tsecond: EntryId | Context,\n\t\tthird?: Context,\n\t): Promise<StoredEntry | undefined> {\n\t\tif (third === undefined) return this.#storage.entry(first as EntryId, second as Context);\n\t\treturn this.#storage.entry(first as ConversationId, second as EntryId, third);\n\t}\n\n\tfindLatestHeadMarker(\n\t\tconversationId: ConversationId,\n\t\tatOrBeforeEntryId: EntryId | undefined,\n\t\tcontext: Context,\n\t): Promise<(EntryRecord & { readonly head: EntryId }) | undefined> {\n\t\treturn this.#storage.findLatestHeadMarker(conversationId, atOrBeforeEntryId, context);\n\t}\n\n\tscanEntries(\n\t\tquery: EntryQuery,\n\t\tlimit: number,\n\t\tcursor: Cursor | undefined,\n\t\tcontext: Context,\n\t): Promise<Page<EntryRecord, Cursor>> {\n\t\treturn this.#storage.scanEntries(query, limit, cursor, context);\n\t}\n\n\ttask(id: TaskId, context: Context): Promise<AnyTaskRecord | undefined> {\n\t\tconst cached = this.#liveTasks.get(id) ?? this.#awaited.get(id)?.record ?? this.#tasks.get(id);\n\t\tif (cached !== undefined) return Promise.resolve(cached);\n\t\treturn this.#keep(\n\t\t\tid,\n\t\t\t() => this.#storage.task(id, context),\n\t\t\t(record) => this.#keepTask(record),\n\t\t);\n\t}\n\n\tscanTasks(\n\t\tquery: TaskQuery,\n\t\tlimit: number,\n\t\tcursor: Cursor | undefined,\n\t\tcontext: Context,\n\t): Promise<Page<AnyTaskRecord, Cursor>> {\n\t\treturn this.#storage.scanTasks(query, limit, cursor, context);\n\t}\n\n\t/** Cached as written only: this process writes every submission it settles, and a read one is usually settled. */\n\tsubmission(id: SubmissionId, context: Context): Promise<SubmissionRecord | undefined> {\n\t\tconst cached = this.#submissions.get(id);\n\t\treturn cached === undefined ? this.#storage.submission(id, context) : Promise.resolve(cached);\n\t}\n\n\tscanSubmissions(\n\t\tquery: SubmissionQuery,\n\t\tlimit: number,\n\t\tcursor: Cursor | undefined,\n\t\tcontext: Context,\n\t): Promise<Page<SubmissionRecord, Cursor>> {\n\t\treturn this.#storage.scanSubmissions(query, limit, cursor, context);\n\t}\n\n\tsubmissionByRequest(\n\t\tconversationId: ConversationId,\n\t\trequestId: string,\n\t\tcontext: Context,\n\t): Promise<SubmissionRecord | undefined> {\n\t\treturn this.#storage.submissionByRequest(conversationId, requestId, context);\n\t}\n\n\tfindDocument(address: DocumentAddress, at: DocumentPoint, context: Context): Promise<DocumentRecord | undefined> {\n\t\treturn this.#storage.findDocument(address, at, context);\n\t}\n\n\tdocument(id: DocumentId, at: DocumentPoint, context: Context): Promise<StoredDocument | undefined> {\n\t\treturn this.#storage.document(id, at, context);\n\t}\n\n\tscanDocuments(\n\t\tquery: DocumentQuery,\n\t\tlimit: number,\n\t\tcursor: Cursor | undefined,\n\t\tcontext: Context,\n\t): Promise<Page<DocumentRecord, Cursor>> {\n\t\treturn this.#storage.scanDocuments(query, limit, cursor, context);\n\t}\n\n\tclose(context: Context): Promise<void> {\n\t\treturn this.#storage.close(context);\n\t}\n\n\t/** Records kept per table, for tests that bound memory. */\n\tget sizes(): {\n\t\treadonly conversations: number;\n\t\treadonly tasks: number;\n\t\treadonly liveTasks: number;\n\t\treadonly awaited: number;\n\t\treadonly waits: number;\n\t\treadonly submissions: number;\n\t} {\n\t\treturn {\n\t\t\tconversations: this.#conversations.size,\n\t\t\ttasks: this.#tasks.size,\n\t\t\tliveTasks: this.#liveTasks.size,\n\t\t\tawaited: this.#awaited.size,\n\t\t\twaits: this.#waits.size,\n\t\t\tsubmissions: this.#submissions.size,\n\t\t};\n\t}\n\n\t/** Keep a frozen task record: a live one until its task ends, an ended one in the bounded table. */\n\t#keepTask(record: AnyTaskRecord): void {\n\t\tif (record.state.status === \"terminal\") {\n\t\t\tthis.#liveTasks.delete(record.id);\n\t\t\tconst awaited = this.#awaited.get(record.id);\n\t\t\tif (awaited !== undefined) awaited.record = record;\n\t\t\telse this.#tasks.set(record.id, record);\n\t\t\treturn;\n\t\t}\n\t\tthis.#tasks.delete(record.id);\n\t\tthis.#liveTasks.set(record.id, record);\n\t}\n\n\t/**\n\t * Follow a task's waits from its written record: a wait holds the ended records of its `on` until the task, after\n\t * leaving the wait, changes again (or ends), as the phase that resumes reads their outcomes before its next commit.\n\t */\n\t#followWait(record: AnyTaskRecord): void {\n\t\tconst wait = this.#waits.get(record.id);\n\t\tif (record.state.status === \"waiting\") {\n\t\t\tif (wait !== undefined) this.#release(wait.on);\n\t\t\tconst on = record.state.on;\n\t\t\tthis.#waits.set(record.id, { on, left: false });\n\t\t\tfor (const id of on) {\n\t\t\t\tlet awaited = this.#awaited.get(id);\n\t\t\t\tif (awaited === undefined) {\n\t\t\t\t\t// One that ended already moves over from the bounded table.\n\t\t\t\t\tconst ended = this.#tasks.get(id);\n\t\t\t\t\tthis.#tasks.delete(id);\n\t\t\t\t\tawaited = ended === undefined ? { count: 0 } : { count: 0, record: ended };\n\t\t\t\t\tthis.#awaited.set(id, awaited);\n\t\t\t\t}\n\t\t\t\tawaited.count++;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (wait === undefined) return;\n\t\tif (!wait.left && record.state.status !== \"terminal\") {\n\t\t\twait.left = true;\n\t\t\treturn;\n\t\t}\n\t\tthis.#waits.delete(record.id);\n\t\tthis.#release(wait.on);\n\t}\n\n\t/** A wait on `on` is over: what no other wait names goes back to the bounded table. */\n\t#release(on: readonly TaskId[]): void {\n\t\tfor (const id of on) {\n\t\t\tconst awaited = this.#awaited.get(id)!;\n\t\t\tif (--awaited.count > 0) continue;\n\t\t\tthis.#awaited.delete(id);\n\t\t\tif (awaited.record !== undefined) this.#tasks.set(id, awaited.record);\n\t\t}\n\t}\n\n\t/** A published commit wrote record `id`: a read of it in flight may have missed that. */\n\t#wrote(id: Id<string>): void {\n\t\tconst reads = this.#reads.get(id);\n\t\tif (reads !== undefined) reads.version++;\n\t}\n\n\t/** Resolve with a read's result, frozen; keep a found record unless a commit published since wrote it. */\n\tasync #keep<T extends object>(\n\t\tid: Id<string>,\n\t\tread: () => Promise<T | undefined>,\n\t\tkeep: (value: T) => void,\n\t): Promise<T | undefined> {\n\t\tlet reads = this.#reads.get(id);\n\t\tif (reads === undefined) {\n\t\t\treads = { count: 0, version: 0 };\n\t\t\tthis.#reads.set(id, reads);\n\t\t}\n\t\treads.count++;\n\t\tconst version = reads.version;\n\t\tlet value: T | undefined;\n\t\ttry {\n\t\t\tvalue = await read();\n\t\t} finally {\n\t\t\tif (--reads.count === 0) this.#reads.delete(id);\n\t\t}\n\t\tif (value === undefined) return undefined;\n\t\tfreeze(value);\n\t\tif (reads.version === version) keep(value);\n\t\treturn value;\n\t}\n\n\t/** Records with a read in flight, for tests that bound memory. */\n\tget readsInFlight(): number {\n\t\treturn this.#reads.size;\n\t}\n}\n\n/** `storage`'s publication scope (`Storage.publishing`), for a wrapper to expose as its own; none when it has none. */\nexport function scopeOf(storage: Storage): Storage[\"publishing\"] {\n\tif (storage.publishing === undefined) return undefined;\n\treturn (publish, context) => storage.publishing!(publish, context);\n}\n\n/** A Map bounded to `capacity` entries that drops the least recently used one. */\nclass Lru<K, V> {\n\treadonly #map = new Map<K, V>();\n\treadonly #capacity: number;\n\n\tconstructor(capacity: number) {\n\t\tthis.#capacity = capacity;\n\t}\n\n\tget(key: K): V | undefined {\n\t\tconst value = this.#map.get(key);\n\t\tif (value === undefined) return undefined;\n\t\tthis.#map.delete(key);\n\t\tthis.#map.set(key, value);\n\t\treturn value;\n\t}\n\n\tget size(): number {\n\t\treturn this.#map.size;\n\t}\n\n\tdelete(key: K): void {\n\t\tthis.#map.delete(key);\n\t}\n\n\tset(key: K, value: V): void {\n\t\tthis.#map.delete(key);\n\t\tthis.#map.set(key, value);\n\t\tif (this.#map.size > this.#capacity) this.#map.delete(this.#map.keys().next().value!);\n\t}\n}\n\nfunction freeze<T>(value: T): T {\n\tif (value === null || typeof value !== \"object\" || Object.isFrozen(value)) return value;\n\tfor (const child of Object.values(value)) freeze(child);\n\treturn Object.freeze(value);\n}\n"]}