renor-engine 0.0.0-stage → 1.0.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 (176) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +73 -0
  3. package/README.md +105 -2
  4. package/dist/BUILD.json +6 -0
  5. package/dist/api/apiAuth.js +43 -0
  6. package/dist/api/server.js +172 -0
  7. package/dist/buildIdentity.js +132 -0
  8. package/dist/cli-extractors.js +46 -0
  9. package/dist/cli.js +491 -0
  10. package/dist/config.js +65 -0
  11. package/dist/context/composer.js +320 -0
  12. package/dist/context/readHonesty.js +478 -0
  13. package/dist/context/rowStore.js +238 -0
  14. package/dist/conversation/formatter.js +81 -0
  15. package/dist/conversation/parser.js +189 -0
  16. package/dist/conversation/types.js +2 -0
  17. package/dist/conversation/writer.js +42 -0
  18. package/dist/db/client.js +147 -0
  19. package/dist/db/indexSalvage.js +81 -0
  20. package/dist/db/migrations.js +642 -0
  21. package/dist/db/readCache.js +109 -0
  22. package/dist/db/schema.js +5 -0
  23. package/dist/db/statements.js +89 -0
  24. package/dist/digests/digestCache.js +142 -0
  25. package/dist/digests/digests.js +597 -0
  26. package/dist/digests/worker.js +348 -0
  27. package/dist/embeddings/caching.js +337 -0
  28. package/dist/embeddings/coalescing.js +177 -0
  29. package/dist/embeddings/interactive.js +103 -0
  30. package/dist/embeddings/local.js +68 -0
  31. package/dist/embeddings/openai.js +105 -0
  32. package/dist/embeddings/rebuild.js +166 -0
  33. package/dist/embeddings/simdScan.js +258 -0
  34. package/dist/embeddings/space.js +21 -0
  35. package/dist/embeddings/types.js +14 -0
  36. package/dist/embeddings/vectorIndex.js +675 -0
  37. package/dist/embeddings/vertex.js +224 -0
  38. package/dist/eval/artifacts.js +42 -0
  39. package/dist/eval/cacheReport.js +139 -0
  40. package/dist/eval/comparison.js +182 -0
  41. package/dist/eval/context700-candidate.js +145 -0
  42. package/dist/eval/evalCache.js +525 -0
  43. package/dist/eval/hashEmbedding.js +35 -0
  44. package/dist/eval/longmemeval.js +56 -0
  45. package/dist/eval/metrics.js +181 -0
  46. package/dist/eval/renorbenchBaseline.js +486 -0
  47. package/dist/eval/renorbenchDocuments.js +134 -0
  48. package/dist/eval/renorbenchProduct.js +182 -0
  49. package/dist/eval/runLedger.js +279 -0
  50. package/dist/eval/runManifest.js +479 -0
  51. package/dist/eval/scoring.js +197 -0
  52. package/dist/evidence/attach.js +267 -0
  53. package/dist/evidence/background.js +147 -0
  54. package/dist/evidence/evidenceCache.js +182 -0
  55. package/dist/evidence/gate.js +37 -0
  56. package/dist/evidence/keyPool.js +85 -0
  57. package/dist/evidence/ollamaChat.js +84 -0
  58. package/dist/evidence/optin.js +165 -0
  59. package/dist/evidence/options.js +130 -0
  60. package/dist/evidence/secondPass.js +456 -0
  61. package/dist/extraction/classifyCapture.js +34 -0
  62. package/dist/extraction/decision-model.js +174 -0
  63. package/dist/extraction/eventExtractor.js +113 -0
  64. package/dist/extraction/extractionClient.js +122 -0
  65. package/dist/extraction/factExtractor.js +46 -0
  66. package/dist/extraction/factsWritePath.js +179 -0
  67. package/dist/extraction/preferenceExtractor.js +153 -0
  68. package/dist/extraction/secretGuard.js +176 -0
  69. package/dist/extraction/secretScanner.js +665 -0
  70. package/dist/extraction/typedFacts.js +224 -0
  71. package/dist/extraction/writePathAdjudication.js +110 -0
  72. package/dist/importing/apply.js +644 -0
  73. package/dist/importing/bundle-write.js +80 -0
  74. package/dist/importing/cli.js +407 -0
  75. package/dist/importing/existing.js +49 -0
  76. package/dist/importing/facts.js +123 -0
  77. package/dist/importing/forgetImported.js +79 -0
  78. package/dist/importing/hosted.js +467 -0
  79. package/dist/importing/index.js +29 -0
  80. package/dist/importing/journalLog.js +96 -0
  81. package/dist/importing/markdown.js +98 -0
  82. package/dist/importing/plan.js +206 -0
  83. package/dist/importing/secretScreen.js +113 -0
  84. package/dist/importing/serialize.js +188 -0
  85. package/dist/importing/source.js +463 -0
  86. package/dist/importing/types.js +6 -0
  87. package/dist/indexer/chunker.js +319 -0
  88. package/dist/indexer/dailyEntries.js +63 -0
  89. package/dist/indexer/hasher.js +10 -0
  90. package/dist/indexer/laneRuns.js +48 -0
  91. package/dist/indexer/o200k.js +481 -0
  92. package/dist/indexer/o200k.json +8 -0
  93. package/dist/indexer/outsideRoot.js +77 -0
  94. package/dist/indexer/parser.js +167 -0
  95. package/dist/indexer/pipeline.js +1168 -0
  96. package/dist/indexer/staleness.js +201 -0
  97. package/dist/indexer/tokenizer.js +77 -0
  98. package/dist/indexer/walker.js +184 -0
  99. package/dist/mcp/hive.js +88 -0
  100. package/dist/mcp/requestErrors.js +47 -0
  101. package/dist/mcp/server.js +396 -0
  102. package/dist/memory/agentLease.js +43 -0
  103. package/dist/memory/confinedWrite.js +315 -0
  104. package/dist/memory/controls.js +73 -0
  105. package/dist/memory/dailyNote.js +265 -0
  106. package/dist/memory/durableAppend.js +57 -0
  107. package/dist/memory/factConflicts.js +203 -0
  108. package/dist/memory/factTerms.js +321 -0
  109. package/dist/memory/hiveNameHeader.cjs +10 -0
  110. package/dist/memory/inventory.js +55 -0
  111. package/dist/memory/invisibleText.js +20 -0
  112. package/dist/memory/lifecycle.js +1756 -0
  113. package/dist/memory/limits.js +13 -0
  114. package/dist/memory/requests.js +183 -0
  115. package/dist/memory/sourceName.js +200 -0
  116. package/dist/memory/whatsNew.js +158 -0
  117. package/dist/memory/writer.js +175 -0
  118. package/dist/memory/writerConflicts.js +85 -0
  119. package/dist/memorymap/memoryMap.js +1568 -0
  120. package/dist/memorymap/replacedJournal.js +58 -0
  121. package/dist/memorymap/replacements.js +405 -0
  122. package/dist/mvp/activity.js +86 -0
  123. package/dist/mvp/agentMemory.js +1744 -0
  124. package/dist/mvp/auth.js +531 -0
  125. package/dist/mvp/cli.js +73 -0
  126. package/dist/mvp/client-store.js +462 -0
  127. package/dist/mvp/compileCache.js +249 -0
  128. package/dist/mvp/compileSeed.js +27 -0
  129. package/dist/mvp/config.js +288 -0
  130. package/dist/mvp/connectionStatus.js +80 -0
  131. package/dist/mvp/contextRelevance.js +413 -0
  132. package/dist/mvp/engine.js +1065 -0
  133. package/dist/mvp/exportAll.js +359 -0
  134. package/dist/mvp/factLanePack.js +77 -0
  135. package/dist/mvp/factMarkers.js +49 -0
  136. package/dist/mvp/factsLane.js +203 -0
  137. package/dist/mvp/hiddenSpans.js +585 -0
  138. package/dist/mvp/history.js +413 -0
  139. package/dist/mvp/hitDates.js +31 -0
  140. package/dist/mvp/hitFloor.js +36 -0
  141. package/dist/mvp/identity.js +18 -0
  142. package/dist/mvp/idle.js +113 -0
  143. package/dist/mvp/memoryDir.js +98 -0
  144. package/dist/mvp/posthogMcp.js +210 -0
  145. package/dist/mvp/readScope.js +104 -0
  146. package/dist/mvp/readUsefulness.js +222 -0
  147. package/dist/mvp/sanitize.js +48 -0
  148. package/dist/mvp/schemaRejections.js +91 -0
  149. package/dist/mvp/scope.js +127 -0
  150. package/dist/mvp/server.js +2089 -0
  151. package/dist/mvp/serverCard.js +29 -0
  152. package/dist/mvp/storedSize.js +139 -0
  153. package/dist/mvp/telemetry.js +441 -0
  154. package/dist/retrieval/adviceLines.js +195 -0
  155. package/dist/retrieval/adviceQuestion.js +44 -0
  156. package/dist/retrieval/conversationShape.js +44 -0
  157. package/dist/retrieval/dailyEntryTime.js +55 -0
  158. package/dist/retrieval/entityLookup.js +113 -0
  159. package/dist/retrieval/factsFirst.js +188 -0
  160. package/dist/retrieval/keyword.js +84 -0
  161. package/dist/retrieval/ledgerEventTime.js +51 -0
  162. package/dist/retrieval/ledgerLiveness.js +83 -0
  163. package/dist/retrieval/ledgerPresentation.js +161 -0
  164. package/dist/retrieval/noteIdentity.js +57 -0
  165. package/dist/retrieval/orchestrator.js +1086 -0
  166. package/dist/retrieval/queryHelpers.js +142 -0
  167. package/dist/retrieval/questionSpan.js +475 -0
  168. package/dist/retrieval/readerEvents.js +690 -0
  169. package/dist/retrieval/readerLayout.js +503 -0
  170. package/dist/retrieval/relativeTimeHints.js +113 -0
  171. package/dist/retrieval/semantic.js +67 -0
  172. package/dist/router/routerCache.js +106 -0
  173. package/dist/router/sessionRouter.js +726 -0
  174. package/dist/toolRefusal.js +120 -0
  175. package/dist/watcher/watcher.js +75 -0
  176. package/package.json +120 -4
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.chunkNote = chunkNote;
4
+ exports.chunkNoteCut = chunkNoteCut;
5
+ exports.chunkBodyFrom = chunkBodyFrom;
6
+ const crypto_1 = require("crypto");
7
+ const tokenizer_1 = require("./tokenizer");
8
+ const limits_1 = require("../memory/limits");
9
+ /** Token count for chunking. Special-token syntax (`<|im_start|>`, `<|endoftext|>`, …)
10
+ * can legitimately appear inside note CONTENT (e.g. imported ShareGPT histories);
11
+ * gpt-tokenizer rejects such text as chat protocol by default and would crash
12
+ * indexing. We count them as plain text instead of failing closed — that is what
13
+ * encodeTokens does, which is also why the tokenizer is loaded on first use
14
+ * rather than at import (see ./tokenizer).
15
+ *
16
+ * Counts are remembered by text. A note that is appended to is chunked whole again each time,
17
+ * and all of it but the tail is text already counted; counting was most of that pass. The memory
18
+ * kept is capped. A text of up to COUNTED_TEXT_MAX characters is kept by its text; a longer one, up to
19
+ * COUNTED_LONG_MAX, by its SHA-256, so a daily note's entries (up to a statement's 4,000 characters each) are
20
+ * counted once, not on every save, and the cache holds a digest for each, not the entry. Longer texts are counted
21
+ * every time.
22
+ *
23
+ * The key kept is never a slice of the note. A chunk's text is a slice, and a slice of a long string keeps the
24
+ * whole string alive: kept as it was, each text remembered held the note it was cut from (3.4 MB for a day's
25
+ * note), the cap counted only the slice, and a daily note's engine grew by the note's size with every save. */
26
+ const COUNTED_WEIGHT_LIMIT = 8_000_000;
27
+ const COUNTED_TEXT_MAX = 2_000;
28
+ const COUNTED_LONG_MAX = 100_000;
29
+ /** What a text kept by its digest counts against COUNTED_WEIGHT_LIMIT: the digest and the map's entry. */
30
+ const COUNTED_DIGEST_WEIGHT = 128;
31
+ const counted = new Map();
32
+ const countedLong = new Map();
33
+ let countedWeight = 0;
34
+ // A round trip through JSON builds a new string with the same code units, a lone surrogate included.
35
+ const owned = (s) => JSON.parse(JSON.stringify(s));
36
+ const tokens = (s) => {
37
+ if (s.length > COUNTED_LONG_MAX)
38
+ return (0, tokenizer_1.encodeTokens)(s);
39
+ const long = s.length > COUNTED_TEXT_MAX;
40
+ const memo = long ? countedLong : counted;
41
+ const key = long ? (0, crypto_1.createHash)('sha256').update(s, 'utf16le').digest('base64') : s;
42
+ const known = memo.get(key);
43
+ if (known !== undefined)
44
+ return known;
45
+ const n = (0, tokenizer_1.encodeTokens)(s);
46
+ const weight = long ? COUNTED_DIGEST_WEIGHT : s.length;
47
+ if (countedWeight + weight > COUNTED_WEIGHT_LIMIT) {
48
+ counted.clear();
49
+ countedLong.clear();
50
+ countedWeight = 0;
51
+ }
52
+ memo.set(long ? key : owned(s), n);
53
+ countedWeight += weight;
54
+ return n;
55
+ };
56
+ function chunkNote(note, options = {}) {
57
+ return chunkNoteCut(note, options).chunks;
58
+ }
59
+ /** chunkNote, and where its last section starts (see NoteCut). */
60
+ function chunkNoteCut(note, options = {}) {
61
+ const lines = note.body.split('\n');
62
+ const entries = entryRanges(note, lines);
63
+ if (entries)
64
+ return { chunks: chunkEntries(note, lines, entries, options) };
65
+ const cut = splitByHeadings(lines, 0, []);
66
+ const { chunks, lastFirst } = chunkSections(note.path, cut.sections, options);
67
+ return chunks.length > 0
68
+ ? { chunks, last: cut.last, lastFirst }
69
+ : {
70
+ chunks: [
71
+ {
72
+ notePath: note.path,
73
+ headingPath: '',
74
+ text: note.body.trim(),
75
+ startLine: 0,
76
+ endLine: lines.length,
77
+ tokenCount: tokens(note.body),
78
+ },
79
+ ],
80
+ };
81
+ }
82
+ /**
83
+ * The chunks chunkNote cuts from body line `from` on, in a note whose body lines before `from` are unchanged and
84
+ * whose last section, before, started at `from` with the heading stack `stack` (see LastSection): `lines` are the
85
+ * body's lines from `from` to its end. Every number in the result is a line of the whole body.
86
+ */
87
+ function chunkBodyFrom(notePath, lines, from, options = {}) {
88
+ const cut = splitByHeadings(lines, from.line, from.stack);
89
+ const { chunks, lastFirst } = chunkSections(notePath, cut.sections, options);
90
+ return { chunks, last: cut.last, lastFirst };
91
+ }
92
+ function chunkSections(notePath, sections, options) {
93
+ const max = options.maxTokens ?? 400;
94
+ const overlap = options.overlapTokens ?? 0;
95
+ const chunks = [];
96
+ let lastFirst = 0;
97
+ for (const sec of sections) {
98
+ lastFirst = chunks.length;
99
+ const text = sec.lines.join('\n').trim();
100
+ if (!text)
101
+ continue;
102
+ if (tokens(text) <= max) {
103
+ chunks.push({
104
+ notePath,
105
+ headingPath: sec.headingPath,
106
+ text,
107
+ startLine: sec.startLine,
108
+ endLine: sec.endLine,
109
+ tokenCount: tokens(text),
110
+ });
111
+ }
112
+ else {
113
+ chunks.push(...splitByParagraphs(text, sec.headingPath, notePath, sec.startLine, max, overlap));
114
+ }
115
+ }
116
+ return { chunks, lastFirst };
117
+ }
118
+ /**
119
+ * The sections of `lines`, numbered from line `offset` of the body, read with the heading stack `initial` in force
120
+ * before the first line; and where the last of them starts (see LastSection).
121
+ */
122
+ function splitByHeadings(lines, offset, initial) {
123
+ const sections = [];
124
+ let stack = initial;
125
+ let before = initial; // the stack in force before the heading that started the section being read
126
+ let buf = [];
127
+ let start = 0;
128
+ for (let i = 0; i < lines.length; i++) {
129
+ const h1 = lines[i].match(/^#\s+(.+)$/);
130
+ const h2 = !h1 ? lines[i].match(/^##\s+(.+)$/) : null;
131
+ const h3 = !h1 && !h2 ? lines[i].match(/^###\s+(.+)$/) : null;
132
+ if (h1 || h2 || h3) {
133
+ if (buf.length)
134
+ sections.push({ headingPath: stack.join(' > '), lines: buf, startLine: offset + start, endLine: offset + i });
135
+ before = stack;
136
+ if (h1)
137
+ stack = [h1[1].trim()];
138
+ else if (h2)
139
+ stack = [stack[0] ?? '', h2[1].trim()].filter(Boolean);
140
+ else if (h3)
141
+ stack = [stack[0] ?? '', stack[1] ?? '', h3[1].trim()].filter(Boolean);
142
+ buf = [lines[i]];
143
+ start = i;
144
+ }
145
+ else {
146
+ buf.push(lines[i]);
147
+ }
148
+ }
149
+ if (buf.length)
150
+ sections.push({ headingPath: stack.join(' > '), lines: buf, startLine: offset + start, endLine: offset + lines.length });
151
+ return { sections, last: { line: offset + start, stack: before } };
152
+ }
153
+ function splitByParagraphs(text, headingPath, notePath, base, max, overlapTokens = 0) {
154
+ const paras = text.split(/\n\n+/).flatMap(p => (p.length > ONE_CHUNK_CHARS
155
+ ? splitOversized(p, max).map((piece, i) => ({ text: piece, sep: i === 0 ? '\n\n' : '' }))
156
+ : [{ text: p, sep: '\n\n' }]));
157
+ const joined = (buf) => buf.map((q, i) => (i === 0 ? q.text : q.sep + q.text)).join('');
158
+ const chunks = [];
159
+ let buf = [];
160
+ let bt = 0;
161
+ let offset = base;
162
+ for (const para of paras) {
163
+ const p = para.text;
164
+ const pt = tokens(p);
165
+ if (bt + pt > max && buf.length > 0) {
166
+ const ct = joined(buf);
167
+ const lc = ct.split('\n').length;
168
+ chunks.push({ notePath, headingPath, text: ct, startLine: offset, endLine: offset + lc, tokenCount: tokens(ct) });
169
+ // The rest of a cut paragraph goes on from where this chunk stops, on the same line or the next, not a line below.
170
+ offset += para.sep === '' ? lc - 1 : lc;
171
+ // Implement overlap: carry over last paragraph(s) that fit within overlapTokens
172
+ buf = [];
173
+ bt = 0;
174
+ if (overlapTokens > 0) {
175
+ let overlapBuf = [];
176
+ let overlapBt = 0;
177
+ // Start from the end of the previous buffer and work backwards to find paragraphs that fit
178
+ const prevParas = ct.split('\n\n');
179
+ for (let i = prevParas.length - 1; i >= 0; i--) {
180
+ const prevPt = tokens(prevParas[i]);
181
+ if (overlapBt + prevPt <= overlapTokens) {
182
+ overlapBuf.unshift({ text: prevParas[i], sep: '\n\n' });
183
+ overlapBt += prevPt;
184
+ }
185
+ else {
186
+ break;
187
+ }
188
+ }
189
+ buf = overlapBuf;
190
+ bt = overlapBt;
191
+ // Adjust offset backwards to account for overlap content
192
+ if (overlapBuf.length > 0) {
193
+ const overlapLines = joined(overlapBuf).split('\n').length;
194
+ offset -= overlapLines;
195
+ }
196
+ }
197
+ }
198
+ buf.push(para);
199
+ bt += pt;
200
+ }
201
+ if (buf.length) {
202
+ const ct = joined(buf);
203
+ chunks.push({ notePath, headingPath, text: ct, startLine: offset, endLine: offset + ct.split('\n').length, tokenCount: tokens(ct) });
204
+ }
205
+ return chunks;
206
+ }
207
+ /** A line of only spaces, tabs and carriage returns is layout: the memory map's rule for where an entry ends. */
208
+ const BLANK_LINE_RE = /^[ \t\r]*$/;
209
+ /** How an entry's first line begins. The body is trimmed, so a last entry of nothing but spaces has lost the space after its source. */
210
+ const ENTRY_START_RE = /^- \[\d{2}:\d{2}:\d{2}\] \(/;
211
+ /** Does this text hold anything besides headings? A heading with nothing under it is a title, not a memory. */
212
+ const hasBody = (text) => text.split('\n').some(l => l.trim() !== '' && !/^#{1,6}\s/.test(l));
213
+ /**
214
+ * A daily note's entries as [start, end) line ranges of its body, or null when the note is cut by headings and
215
+ * paragraphs instead (it is not a daily note, or has no entry). An entry runs from the line that starts it to the
216
+ * next entry, less the blank lines that lay out the file; everything in it is the statement's, whatever it looks like.
217
+ */
218
+ function entryRanges(note, lines) {
219
+ const starts = note.entryStarts;
220
+ if (!starts || starts.length === 0)
221
+ return null;
222
+ // The starts were found on the stored text. If the body no longer agrees (a start that is not an entry's first
223
+ // line, or out of order), cut by headings rather than cut a statement in the wrong place.
224
+ let previous = -1;
225
+ for (const start of starts) {
226
+ if (!Number.isInteger(start) || start <= previous || start >= lines.length || !ENTRY_START_RE.test(lines[start]))
227
+ return null;
228
+ previous = start;
229
+ }
230
+ return starts.map((start, k) => {
231
+ let end = k + 1 < starts.length ? starts[k + 1] : lines.length;
232
+ while (end > start + 1 && BLANK_LINE_RE.test(lines[end - 1]))
233
+ end--;
234
+ return [start, end];
235
+ });
236
+ }
237
+ /**
238
+ * One chunk per entry of a daily note, so that recalling one memory returns that memory and not the day it was said
239
+ * on. An entry is cut whole whatever its words contain (headings, blank lines, `---`, a line that looks like another
240
+ * entry); one longer than a chunk is cut into paragraphs, but never together with another entry. The chunk is the
241
+ * entry as the memory map shows it, its time and source included, under the heading the note opens with
242
+ * (`2026-10-03`). What stands above the first entry is cut as any note is, minus a heading with nothing under it,
243
+ * since every entry already carries that heading in its path.
244
+ */
245
+ function chunkEntries(note, lines, entries, options) {
246
+ const max = options.maxTokens ?? 400;
247
+ const overlap = options.overlapTokens ?? 0;
248
+ const chunks = [];
249
+ const above = lines.slice(0, entries[0][0]);
250
+ let headingPath = '';
251
+ if (above.some(l => l.trim() !== '')) {
252
+ const { sections } = splitByHeadings(above, 0, []);
253
+ headingPath = sections[sections.length - 1].headingPath;
254
+ for (const chunk of chunkNote({ ...note, body: above.join('\n'), entryStarts: undefined }, options)) {
255
+ if (hasBody(chunk.text))
256
+ chunks.push(chunk);
257
+ }
258
+ }
259
+ for (const [start, end] of entries) {
260
+ const text = lines.slice(start, end).join('\n');
261
+ const tokenCount = tokens(text);
262
+ if (tokenCount <= max) {
263
+ chunks.push({ notePath: note.path, headingPath, text, startLine: start, endLine: end, tokenCount });
264
+ }
265
+ else {
266
+ chunks.push(...splitByParagraphs(text, headingPath, note.path, start, max, overlap));
267
+ }
268
+ }
269
+ return chunks;
270
+ }
271
+ /**
272
+ * A paragraph this short stays one chunk however many tokens it holds, as every paragraph did. A statement the engine
273
+ * saved is up to MAX_STATEMENT_LENGTH characters, stored with each character that needs it as a six-character \uXXXX
274
+ * escape, and it is one daily entry, one chunk and recalled as exactly its words (tests/mvp/statement-ledger.test.ts).
275
+ * A longer paragraph came from a pasted log, an import or a hostile file, and is cut at the chunk limit.
276
+ */
277
+ const ONE_CHUNK_CHARS = limits_1.MAX_STATEMENT_LENGTH * 8;
278
+ const PIECE_CHARS = 256;
279
+ /** The end of a piece that starts at `from` and wants to end at `to`: after the last space or newline in its back half, never inside a surrogate pair. */
280
+ function pieceEnd(p, from, to) {
281
+ if (to >= p.length)
282
+ return p.length;
283
+ const floor = from + ((to - from) >> 1);
284
+ for (let cut = to; cut > floor; cut--) {
285
+ if (p[cut] === ' ' || p[cut] === '\n')
286
+ return cut + 1;
287
+ }
288
+ const last = p.charCodeAt(to - 1);
289
+ return last >= 0xd800 && last < 0xdc00 && to - from > 1 ? to - 1 : to;
290
+ }
291
+ /**
292
+ * A paragraph over `max` tokens (one long line, a pasted log, a minified blob), cut at whitespace into pieces of at most
293
+ * `max` tokens. Only short pieces are counted, never the whole, and a piece that is still over `max` (CJK, emoji) is halved,
294
+ * so the cost is what the paragraph holds. The pieces are chunked like paragraphs.
295
+ */
296
+ function splitOversized(p, max) {
297
+ const out = [];
298
+ let buf = '';
299
+ let bt = 0;
300
+ for (let i = 0; i < p.length;) {
301
+ let j = pieceEnd(p, i, i + PIECE_CHARS);
302
+ let pt = tokens(p.slice(i, j));
303
+ while (pt > max && j - i > 2) {
304
+ j = pieceEnd(p, i, i + ((j - i) >> 1));
305
+ pt = tokens(p.slice(i, j));
306
+ }
307
+ if (bt + pt > max && buf) {
308
+ out.push(buf);
309
+ buf = '';
310
+ bt = 0;
311
+ }
312
+ buf += p.slice(i, j);
313
+ bt += pt;
314
+ i = j;
315
+ }
316
+ if (buf)
317
+ out.push(buf);
318
+ return out;
319
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DAILY_CHUNK_LAYOUT = void 0;
4
+ exports.noteContentHash = noteContentHash;
5
+ exports.dailyEntryStarts = dailyEntryStarts;
6
+ const hasher_1 = require("./hasher");
7
+ const dailyNote_1 = require("../memory/dailyNote");
8
+ /**
9
+ * How a daily note is cut into chunks, as a version: `entries-1` is one chunk per entry (see chunker.ts). A
10
+ * note's chunks are only as current as the layout they were cut in, so the layout is part of what the index
11
+ * remembers about a note (`noteContentHash`). Change how daily notes are cut and change this, and every daily note
12
+ * the index holds is cut again by the next ordinary index pass.
13
+ */
14
+ exports.DAILY_CHUNK_LAYOUT = 'entries-1';
15
+ /**
16
+ * What the index keeps to know a note is unchanged: the hash of its bytes and, for a daily note, of the layout its
17
+ * chunks are cut in. An index built before the layout existed holds a note under the hash of its bytes alone, so the
18
+ * next pass sees that note as changed, cuts it again and stores the new hash, once; nothing else about the note,
19
+ * its file or any other note moves.
20
+ */
21
+ function noteContentHash(filePath, content) {
22
+ return (0, hasher_1.hashContent)((0, dailyNote_1.isDailyNotePath)(filePath) ? `${exports.DAILY_CHUNK_LAYOUT}\u0000${content}` : content);
23
+ }
24
+ /** How many line breaks (`\n` alone, as every reader of this format splits) lie before `end` in `text`. */
25
+ function newlinesBefore(text, end) {
26
+ let n = 0;
27
+ for (let at = text.indexOf('\n'); at !== -1 && at < end; at = text.indexOf('\n', at + 1))
28
+ n++;
29
+ return n;
30
+ }
31
+ /**
32
+ * Where the entries of a daily note start, as indices into the lines of its parsed `body`; undefined when it has
33
+ * none (a file of free text under a `Daily/` folder, such as an imported journal, is cut like any other note).
34
+ *
35
+ * An entry starts at a line that begins at column 0 like `- [08:30:00] (claude) ...`. That is judged on the text as
36
+ * stored, not as read: a statement's later line that looks like an entry is kept behind a backslash in the file so
37
+ * it cannot forge one (dailyNote.ts), and once the words are restored it looks like an entry again. Cutting the
38
+ * restored text on that shape would split a statement in two; the stored text says which lines really start one.
39
+ * This is the same test the memory map uses, so a chunk is one of its entries.
40
+ *
41
+ * The restore changes lines in place, so line `i` of the stored text is line `i` of the decoded text; `rawBody` is
42
+ * what follows the frontmatter there and `body` is that, trimmed, which fixes the line the body begins on.
43
+ */
44
+ function dailyEntryStarts(stored, decoded, rawBody, body) {
45
+ if (!decoded.endsWith(rawBody))
46
+ return undefined;
47
+ const bodyAt = decoded.length - rawBody.length + (rawBody.length - rawBody.trimStart().length);
48
+ const first = newlinesBefore(decoded, bodyAt);
49
+ const bodyLines = body.split('\n');
50
+ const storedLines = stored.split('\n');
51
+ // Fail closed: if the restore ever changes the number of lines, the stored text no longer says which line is
52
+ // which, so say nothing about entries and let the note be cut by headings as before. (The chunker checks each
53
+ // start against the body once more.)
54
+ if (storedLines.length !== newlinesBefore(decoded, decoded.length) + 1)
55
+ return undefined;
56
+ const starts = [];
57
+ for (let i = 0; i < bodyLines.length; i++) {
58
+ const line = storedLines[first + i] ?? '';
59
+ if (line.startsWith('- [') && dailyNote_1.ENTRY_LINE_RE.test(line))
60
+ starts.push(i);
61
+ }
62
+ return starts.length > 0 ? starts : undefined;
63
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hashContent = hashContent;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ function hashContent(content) {
9
+ return crypto_1.default.createHash('sha256').update(content, 'utf8').digest('hex');
10
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.laneZeroYieldMarker = laneZeroYieldMarker;
4
+ exports.recordLaneRun = recordLaneRun;
5
+ function laneZeroYieldMarker(db, sourcePath, rowKind, sourceHash, extractionVersion) {
6
+ // The run fact alone is not enough: a concurrent writer for the SAME note
7
+ // (the watcher indexing while a CLI index runs) can have inserted a row since
8
+ // that run finished, and a live row must always outrank a marker. The row
9
+ // half is therefore re-checked live, in this same statement, so the decision
10
+ // is one atomic read of both halves. Extra extraction in that race is the
11
+ // safe direction; serving an empty lane is not.
12
+ const row = db.prepare(`SELECT max_rows_written FROM lane_runs
13
+ WHERE source_path = ? AND row_kind = ? AND source_hash = ? AND extraction_version = ?
14
+ AND max_rows_written = 0
15
+ AND NOT EXISTS (
16
+ SELECT 1 FROM structured_rows r
17
+ WHERE r.source_path = lane_runs.source_path AND r.row_kind = lane_runs.row_kind
18
+ AND r.source_hash = lane_runs.source_hash AND r.extraction_version = lane_runs.extraction_version
19
+ AND r.valid_to IS NULL
20
+ )
21
+ LIMIT 1`).get(sourcePath, rowKind, sourceHash, extractionVersion);
22
+ if (!row)
23
+ return undefined;
24
+ return { laneZeroYield: true };
25
+ }
26
+ /**
27
+ * Record a completed lane run. Called after the lane's extractor and its
28
+ * adjudicated writes returned without throwing — one row per (note, lane,
29
+ * content-version), refreshed in place on a repeat run.
30
+ *
31
+ * One statement, deliberately NOT wrapped in a transaction: the only value
32
+ * written is `rowsWritten`, an argument of this call, so there is no
33
+ * read-modify-write window to protect. (The MAX in the upsert is evaluated by
34
+ * SQLite inside the statement.) Wrapping it in a default (deferred)
35
+ * better-sqlite3 transaction would add a WAL snapshot-upgrade window
36
+ * (SQLITE_BUSY_SNAPSHOT) against the very cross-process writer this marker is
37
+ * defensive about — a failure introduced in the race instead of closed.
38
+ */
39
+ function recordLaneRun(db, sourcePath, rowKind, sourceHash, extractionVersion, rowsWritten) {
40
+ // Monotone on purpose: max(stored, this run). A zero can never erase a
41
+ // positive record for the same key, whichever run finishes last.
42
+ db.prepare(`INSERT INTO lane_runs
43
+ (source_path, row_kind, source_hash, extraction_version, max_rows_written, completed_at)
44
+ VALUES (?, ?, ?, ?, ?, ?)
45
+ ON CONFLICT(source_path, row_kind, source_hash, extraction_version) DO UPDATE SET
46
+ max_rows_written = max(lane_runs.max_rows_written, excluded.max_rows_written),
47
+ completed_at = excluded.completed_at`).run(sourcePath, rowKind, sourceHash, extractionVersion, rowsWritten, new Date().toISOString());
48
+ }