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,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_STATEMENT_LENGTH = void 0;
4
+ /**
5
+ * The longest statement `remember` admits, in UTF-16 code units (what `String.length` counts, and what
6
+ * a zod `.max()` on a string counts).
7
+ *
8
+ * One number for every place that stores the user's words: the MCP schema, the ledger's `statement`
9
+ * field and the daily-note writer all read it from here, so a statement that is admitted is a statement
10
+ * that is kept. They used to disagree (4000 admitted, 2000 written) and the writer cut the difference
11
+ * without a word.
12
+ */
13
+ exports.MAX_STATEMENT_LENGTH = 4000;
@@ -0,0 +1,183 @@
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.REQUEST_PAGE_LIMIT = exports.DEFAULT_ASK_AGENT_PER_HOUR = void 0;
7
+ exports.askAgentPerHour = askAgentPerHour;
8
+ exports.noteKnownAgent = noteKnownAgent;
9
+ exports.askAgent = askAgent;
10
+ exports.listRequests = listRequests;
11
+ exports.listRequestsPage = listRequestsPage;
12
+ exports.answerRequest = answerRequest;
13
+ exports.closeRequest = closeRequest;
14
+ const crypto_1 = __importDefault(require("crypto"));
15
+ const scope_1 = require("../mvp/scope");
16
+ const controls_1 = require("./controls");
17
+ const sourceName_1 = require("./sourceName");
18
+ const STATUSES = new Set(['open', 'answered', 'closed']);
19
+ /** How many requests one sending connection may record per rolling hour, unless RENOR_ASK_AGENT_PER_HOUR says otherwise. */
20
+ exports.DEFAULT_ASK_AGENT_PER_HOUR = 30;
21
+ const HOUR_MS = 60 * 60 * 1000;
22
+ /** The hourly limit for ask_agent: RENOR_ASK_AGENT_PER_HOUR when it is a positive integer, otherwise the default. */
23
+ function askAgentPerHour() {
24
+ const raw = Number(process.env.RENOR_ASK_AGENT_PER_HOUR);
25
+ return Number.isInteger(raw) && raw > 0 ? raw : exports.DEFAULT_ASK_AGENT_PER_HOUR;
26
+ }
27
+ /** Records a connection name the gateway set (x-renor-agent), so ask_agent can address it. "unnamed" is never recorded. */
28
+ function noteKnownAgent(db, agent, now = new Date()) {
29
+ if (agent === 'unnamed')
30
+ return;
31
+ db.prepare('INSERT OR IGNORE INTO known_agents (agent, first_seen) VALUES (?, ?)').run(agent, now.toISOString());
32
+ }
33
+ function isKnownAgent(db, agent) {
34
+ return db.prepare('SELECT 1 FROM known_agents WHERE agent = ?').get(agent) !== undefined;
35
+ }
36
+ function row(db, id) {
37
+ const found = db.prepare('SELECT * FROM agent_requests WHERE id = ?').get(id);
38
+ if (!found)
39
+ throw new Error('no request with that id');
40
+ return found;
41
+ }
42
+ function askAgent(db, args) {
43
+ const rawTo = args.to.trim();
44
+ const question = args.request.trim();
45
+ if (!rawTo)
46
+ throw new Error('ask_agent needs who it is for');
47
+ if (!question)
48
+ throw new Error('ask_agent needs a request');
49
+ // Store the header form, the same name stdio and hosted publish. A spelling a header cannot render is unnamed.
50
+ const to = (0, sourceName_1.hiveIdentity)(rawTo);
51
+ const from = (0, sourceName_1.hiveIdentity)(args.from);
52
+ if (!from)
53
+ throw new Error('ask_agent needs a connection name');
54
+ if (!to)
55
+ throw new Error('ask_agent cannot address unnamed: that is every connection whose key has no name. Name the key, then ask it by that name');
56
+ // A request is text another agent reads, so it is held to the same rule as a memory: no control
57
+ // character a reader cannot see (memory/controls.ts). Checked before anything is stored.
58
+ (0, controls_1.assertNoUnstorableControls)('ask_agent to', to);
59
+ (0, controls_1.assertNoUnstorableControls)('ask_agent request', question);
60
+ const now = args.now ?? new Date();
61
+ if (args.knownOnly) {
62
+ if (!isKnownAgent(db, to))
63
+ throw new Error(`no connection named ${JSON.stringify(to)} has connected to this workspace; ask_agent addresses an agent by the name on its connection key`);
64
+ }
65
+ if (args.perHour !== undefined) {
66
+ const since = new Date(now.getTime() - HOUR_MS).toISOString();
67
+ const recent = db.prepare('SELECT COUNT(*) AS n, MIN(created_at) AS oldest FROM agent_requests WHERE from_agent = ? AND created_at > ?').get(from, since);
68
+ if (recent.n >= args.perHour) {
69
+ const retry = new Date(Date.parse(recent.oldest) + HOUR_MS).toISOString();
70
+ throw new Error(`ask_agent is limited to ${args.perHour} requests per hour per connection; this one can ask again after ${retry}`);
71
+ }
72
+ }
73
+ const id = `req_${crypto_1.default.randomBytes(8).toString('hex')}`;
74
+ const created = now.toISOString();
75
+ db.prepare(`INSERT INTO agent_requests (id, scope, from_agent, to_agent, question, answer, status, created_at, answered_at, closed_at)
76
+ VALUES (?, ?, ?, ?, ?, NULL, 'open', ?, NULL, NULL)`).run(id, args.scope !== undefined && args.scope.trim() !== '' ? (0, scope_1.normalizeScope)(args.scope) : null, from, to, question, created);
77
+ return { id, status: 'open', from_agent: from, to_agent: to, created_at: created };
78
+ }
79
+ /** At most this many requests one list returns, the newest; the rest are counted. whats_new leads with the same number. */
80
+ exports.REQUEST_PAGE_LIMIT = 20;
81
+ function listWhere(args) {
82
+ const status = args.status ?? 'open';
83
+ if (!STATUSES.has(status))
84
+ throw new Error('list_requests status must be open, answered or closed');
85
+ const caller = (0, sourceName_1.hiveIdentity)(args.agent) ?? args.agent.trim();
86
+ let sql;
87
+ const params = [];
88
+ if (args.to !== undefined && args.to.trim() !== '') {
89
+ // `to` names the other party of a thread, not an inbox to read. Both directions count: the request
90
+ // this connection asked of that agent, and the request that agent asked of this one. A connection
91
+ // that is neither party to a thread cannot list it, and cannot page through it.
92
+ const other = (0, sourceName_1.hiveIdentity)(args.to);
93
+ if (!other)
94
+ throw new Error('list_requests cannot address unnamed: that is every connection whose key has no name. Name the key, then ask it by that name');
95
+ sql = 'status = ? AND ((from_agent = ? AND to_agent = ?) OR (from_agent = ? AND to_agent = ?))';
96
+ params.push(status, caller, other, other, caller);
97
+ }
98
+ else {
99
+ // With no `to` this is the caller's own inbox, exactly as it has always been.
100
+ sql = 'status = ? AND to_agent = ?';
101
+ params.push(status, caller);
102
+ }
103
+ let scopeSql = '';
104
+ if (args.scope !== undefined && args.scope.trim() !== '') {
105
+ // Every spelling of the scope (atlas, scope:atlas, project:atlas); a request asked with no scope is global.
106
+ const saved = (0, scope_1.savedScopeSqlPredicate)((0, scope_1.normalizeScope)(args.scope), "COALESCE(scope, 'global')");
107
+ scopeSql = ` AND ${saved.sql}`;
108
+ params.push(...saved.params);
109
+ }
110
+ return { sql: `${sql}${scopeSql}`, params, caller };
111
+ }
112
+ /** An answer is for the agent who asked and the one who answered; anyone else sees that it was answered, not what. */
113
+ function visibleTo(agent, r) {
114
+ return r.from_agent === agent || r.to_agent === agent ? r : { ...r, answer: null };
115
+ }
116
+ /** Every request matching `args`, newest first. Unbounded: the tools read `listRequestsPage`. */
117
+ function listRequests(db, args) {
118
+ const where = listWhere(args);
119
+ const rows = db.prepare(`SELECT * FROM agent_requests WHERE ${where.sql} ORDER BY created_at DESC, rowid DESC`).all(...where.params);
120
+ return rows.map((r) => visibleTo(args.agent, r));
121
+ }
122
+ /**
123
+ * The newest REQUEST_PAGE_LIMIT requests matching `args` (older than `args.before` when given), and how many more there
124
+ * are (`more_requests`, absent when none) with the call that lists them (`more_requests_hint`). What list_requests and
125
+ * whats_new return: a flood of requests costs a reader one bounded page, never all of it.
126
+ */
127
+ function listRequestsPage(db, args) {
128
+ const where = listWhere(args);
129
+ let sql = where.sql;
130
+ const params = [...where.params];
131
+ if (args.before !== undefined && args.before.trim() !== '') {
132
+ // Same order as the list (created_at, then insertion): strictly after the given request in it.
133
+ // The anchor has to be a request this caller is a party to. Another pair's id is not a page cursor.
134
+ const anchor = db.prepare('SELECT created_at, rowid AS rid FROM agent_requests WHERE id = ? AND (from_agent = ? OR to_agent = ?)').get(args.before.trim(), where.caller, where.caller);
135
+ if (!anchor)
136
+ throw new Error('list_requests before must be the id of a request from whats_new or list_requests');
137
+ sql += ' AND (created_at < ? OR (created_at = ? AND rowid < ?))';
138
+ params.push(anchor.created_at, anchor.created_at, anchor.rid);
139
+ }
140
+ const rows = db.prepare(`SELECT * FROM agent_requests WHERE ${sql} ORDER BY created_at DESC, rowid DESC LIMIT ?`).all(...params, exports.REQUEST_PAGE_LIMIT);
141
+ const requests = rows.map((r) => visibleTo(args.agent, r));
142
+ if (requests.length < exports.REQUEST_PAGE_LIMIT)
143
+ return { requests };
144
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM agent_requests WHERE ${sql}`).get(...params).n;
145
+ if (total <= requests.length)
146
+ return { requests };
147
+ const more = total - requests.length;
148
+ const last = requests[requests.length - 1].id;
149
+ return {
150
+ requests,
151
+ more_requests: more,
152
+ more_requests_hint: `${more} older request${more === 1 ? '' : 's'} not shown; use list_requests with before=${last}`,
153
+ };
154
+ }
155
+ function answerRequest(db, args) {
156
+ const found = row(db, args.id);
157
+ if (found.to_agent !== args.agent)
158
+ throw new Error('only the agent this request is for can answer it');
159
+ if (found.status === 'closed')
160
+ throw new Error('a closed request cannot be answered');
161
+ const answer = args.answer.trim();
162
+ if (!answer)
163
+ throw new Error('answer_request needs an answer');
164
+ // The same rule ask_agent holds to: an answer is text another agent reads back (memory/controls.ts).
165
+ (0, controls_1.assertNoUnstorableControls)('answer_request answer', answer);
166
+ const at = (args.now ?? new Date()).toISOString();
167
+ // The guard is in the statement too: a close on another handle between the read above and this write must win.
168
+ const changed = db.prepare(`UPDATE agent_requests SET status = 'answered', answer = ?, answered_at = ? WHERE id = ? AND status != 'closed'`).run(answer, at, args.id).changes;
169
+ if (changed === 0)
170
+ throw new Error('a closed request cannot be answered');
171
+ return row(db, args.id);
172
+ }
173
+ function closeRequest(db, args) {
174
+ const found = row(db, args.id);
175
+ if (found.to_agent !== args.agent && found.from_agent !== args.agent) {
176
+ throw new Error('only the agent this request is for, or the one who asked, can close it');
177
+ }
178
+ if (found.status === 'closed')
179
+ return found;
180
+ const at = (args.now ?? new Date()).toISOString();
181
+ db.prepare(`UPDATE agent_requests SET status = 'closed', closed_at = ? WHERE id = ?`).run(at, args.id);
182
+ return row(db, args.id);
183
+ }
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ /**
3
+ * The source of a memory: a short name for who or what saved it (`claude-code`, `cursor`, `executor-3`). It is stored with
4
+ * the entry, in the daily-note header (`- [08:30:00] (cursor) ...`) and in the ledger event (`- source: cursor`), and shown
5
+ * beside it ("by cursor").
6
+ *
7
+ * A client may send more than a name. An agent that builds its call by hand sometimes sends where the words came from
8
+ * instead: a file path (`/home/ana/project/BOARD.md`), a Windows path, a URL. That is a location, not a name, and the two
9
+ * stores each did the wrong thing with it. The daily-note header deletes every character that is not a letter, a digit or a
10
+ * hyphen, so the whole path ran together into one word (`homeanaprojectBOARDmd`), with no limit on its length; the ledger
11
+ * kept the path as sent, the directories of the sender's machine in it, and refused the whole save past 256 characters.
12
+ *
13
+ * Three rules, shared by both writers and by the hosted tools:
14
+ * - `cleanSource`: a location is reduced to the name it ends in and the name is cut at MAX_SOURCE_LENGTH characters. A path
15
+ * becomes its last component; a URL becomes its host, or with no host the last component of its path, and its userinfo
16
+ * (`user:password@`) is never part of what it becomes, whatever else the URL lacks or spells oddly. A `/` or `\` is what
17
+ * makes a source a location, whatever else it is: `crew/executor-7` is stored as `executor-7`. The ledger stores this.
18
+ * A control character is dropped first, as a URL parser drops a tab or a newline, so a stray one never loses the save and
19
+ * never hides the URL a browser would read (`ht<newline>tps://user:pw@host` is `host`).
20
+ * It is not a secret scanner: a URL inside a longer text is a name like any other (RENOR_SECRET_GUARD judges the rest).
21
+ * - `headerSource`: what a note header stores, which keeps letters, digits and hyphens only. It is `cleanSource` with every
22
+ * run of other characters written as one hyphen instead of deleted, so the words stay apart (`Claude Code` is
23
+ * `Claude-Code`, `board.md` is `board-md`).
24
+ * - `productFromUserAgent`: when a client names no one, the name it gives itself, the product in its User-Agent.
25
+ * They decide what a NEW write stores and nothing more: an entry or event written earlier is read back exactly as it was
26
+ * stored, and is never rewritten.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.IMPORT_SOURCE_PREFIX = exports.MAX_SOURCE_LENGTH = void 0;
30
+ exports.cleanSource = cleanSource;
31
+ exports.headerSource = headerSource;
32
+ exports.hiveIdentity = hiveIdentity;
33
+ exports.productFromUserAgent = productFromUserAgent;
34
+ exports.isImportSource = isImportSource;
35
+ exports.savedSource = savedSource;
36
+ exports.authorMarker = authorMarker;
37
+ const hiveNameHeader_cjs_1 = require("./hiveNameHeader.cjs");
38
+ /** The longest source a write stores, in characters (code points). */
39
+ exports.MAX_SOURCE_LENGTH = 40;
40
+ /** `scheme:` at the start. Group 1 is the scheme. It has two letters or more: `C:\\Users` and `C://Users` are a Windows drive, not a URL. */
41
+ const SCHEME = /^([A-Za-z][A-Za-z0-9+.-]+):/;
42
+ /**
43
+ * The schemes a browser reads an authority after, whatever follows the colon: any run of `/` and `\`, none included, so
44
+ * `https://h`, `https:/h`, `https:\h`, `https:h` and `https:///h` are one URL. Any other scheme has an authority only after two
45
+ * separators (`ssh://h`), and a path after fewer: a browser reads `ssh:/user:pw@h` and `ssh:user:pw@h` as paths, and
46
+ * `file:/home/ana/BOARD.md` as a path (`file` is not one of these).
47
+ */
48
+ const WEB_SCHEME = /^(?:https?|wss?|ftp)$/i;
49
+ /** Where a URL's authority ends: its path, query or fragment starts. */
50
+ const AUTHORITY_END = /[\\/?#]/;
51
+ const SEPARATOR = /[\\/]/;
52
+ const COMPONENT_SEPARATORS = /[\\/]+/;
53
+ /** Every control character (C0, DEL and C1): the ledger refuses the first two in a field, and a URL parser drops a tab or a newline wherever it is. */
54
+ const CONTROL_CHARACTERS = /\p{Cc}+/gu;
55
+ /** The last component of a path that names something: `.` and `..` do not, and a trailing separator leaves an empty one. */
56
+ function lastComponent(location) {
57
+ const components = location.split(COMPONENT_SEPARATORS);
58
+ for (let i = components.length - 1; i >= 0; i--) {
59
+ const component = components[i].trim();
60
+ if (component !== '' && component !== '.' && component !== '..')
61
+ return component;
62
+ }
63
+ return '';
64
+ }
65
+ /** At most MAX_SOURCE_LENGTH characters, counted as the code points they are, so a letter outside the BMP is never split. */
66
+ function cut(name) {
67
+ // Code units are never fewer than code points, so a short string needs no walk.
68
+ if (name.length <= exports.MAX_SOURCE_LENGTH)
69
+ return name.trim();
70
+ let end = 0;
71
+ let count = 0;
72
+ for (const point of name) {
73
+ if (count === exports.MAX_SOURCE_LENGTH)
74
+ break;
75
+ end += point.length;
76
+ count++;
77
+ }
78
+ return name.slice(0, end).trim();
79
+ }
80
+ /**
81
+ * `rest` without the userinfo of an authority that only a `/`, `?` or `#` ends. A browser ends the authority of a scheme that is
82
+ * neither a web scheme nor `file` there and nowhere else, so its userinfo may hold a `\` (`custom://\user:pw@host`), which
83
+ * urlName otherwise reads as the end of a host (`smb://server\share`). Everything up to the last `@` of that authority goes first.
84
+ */
85
+ function afterUserinfo(rest) {
86
+ const end = rest.search(/[/?#]/);
87
+ const authority = end === -1 ? rest : rest.slice(0, end);
88
+ return rest.slice(authority.lastIndexOf('@') + 1);
89
+ }
90
+ /**
91
+ * What a URL names, or null when `given` is not one (no scheme, or a scheme with no authority): its host, or when it has none
92
+ * (`file:///home/ana/BOARD.md`, `https://user:pass@`) the last component of its path, its query and fragment left out.
93
+ *
94
+ * The userinfo (`user:password@`) is never part of the answer, because the authority is parsed rather than matched: it is
95
+ * what follows the scheme's separators up to the first `/`, `\`, `?` or `#`, and its userinfo is everything up to the LAST `@`
96
+ * in it, so a password with an unescaped `@`, an empty host, a port with no host and every spelling of the same URL a browser
97
+ * reads (`https:\\`, `https:///`, `https:/`, `https:`) leave the credentials in neither the host nor the path. (A scheme that is
98
+ * neither a web scheme nor `file` ends it at a `/`, `?` or `#` only, so a `\` can be part of its userinfo: afterUserinfo.) A
99
+ * path is still a path: an `@` in `file:///home/ana/notes@2024.md` is part of a file name. A URL inside a longer text is not
100
+ * recognised here; that is a name like any other, and so is a one-letter scheme (a Windows drive) and
101
+ * `jdbc:postgresql://user:pw@host`, which a browser reads as a path.
102
+ */
103
+ function urlName(given) {
104
+ const scheme = SCHEME.exec(given);
105
+ if (!scheme)
106
+ return null;
107
+ let rest = given.slice(scheme[0].length);
108
+ if (WEB_SCHEME.test(scheme[1]))
109
+ rest = rest.replace(/^[\\/]+/, '');
110
+ else if (/^[\\/]{2}/.test(rest))
111
+ rest = /^file$/i.test(scheme[1]) ? rest.slice(2) : afterUserinfo(rest.slice(2));
112
+ else
113
+ return null;
114
+ const end = rest.search(AUTHORITY_END);
115
+ const authority = end === -1 ? rest : rest.slice(0, end);
116
+ const hostAndPort = authority.slice(authority.lastIndexOf('@') + 1);
117
+ // A bracketed IPv6 host keeps its colons; otherwise the host stops at the port's colon. Always matches (possibly empty).
118
+ const host = /^\[[^\]]*\]|^[^:]*/.exec(hostAndPort)[0];
119
+ if (host.trim() !== '')
120
+ return host;
121
+ return lastComponent(end === -1 ? '' : rest.slice(end).split(/[?#]/, 1)[0]);
122
+ }
123
+ /**
124
+ * The name a write stores for the `source` a client sent: a path or a URL reduced to the name it ends in, at most
125
+ * MAX_SOURCE_LENGTH characters, with no control character in it. It may be empty (a source that was only blanks, controls or
126
+ * separators, or a URL that names nothing), which names no one. Never throws on a string.
127
+ */
128
+ function cleanSource(source) {
129
+ // Dropped, not turned into a blank: a URL parser drops a tab or a newline too, so a control character inside the scheme or
130
+ // between its separators (`https:/<tab>/user:pw@host`) must not hide the URL it is in.
131
+ const given = source.replace(CONTROL_CHARACTERS, '').trim();
132
+ const url = urlName(given);
133
+ if (url !== null)
134
+ return cut(url);
135
+ return cut(SEPARATOR.test(given) ? lastComponent(given) : given);
136
+ }
137
+ /**
138
+ * The name a daily-note header stores: letters, digits and hyphens, and nothing else, because the header's marks (`+esc`,
139
+ * `, UTC-03:00`) are told apart from a source name by what a name cannot hold (dailyNote.ts). Every run of other characters
140
+ * is written as one hyphen instead of being deleted, none at either end; accents are composed first (a macOS file name is
141
+ * decomposed) and a combining mark that cannot be composed is dropped. It may be empty.
142
+ */
143
+ function headerSource(source) {
144
+ return cleanSource(source)
145
+ .normalize('NFC')
146
+ .replace(/\p{M}+/gu, '')
147
+ .replace(/[^\p{L}\p{N}]+/gu, '-')
148
+ .replace(/^-|-$/g, '');
149
+ }
150
+ /**
151
+ * The hive name of a connection, on stdio and on hosted HTTP. It is the daily-note header form, so two
152
+ * spellings a header cannot tell apart are one agent. All transports first use the gateway's shared
153
+ * header-safe reduction, including its stable hash for names with no HTTP-renderable characters.
154
+ * An empty final header form, or the reserved word `unnamed`, is no name.
155
+ */
156
+ function hiveIdentity(raw) {
157
+ if (typeof raw !== 'string')
158
+ return null;
159
+ const rendered = headerSource((0, hiveNameHeader_cjs_1.hiveNameHeader)(raw));
160
+ if (rendered === '' || rendered === 'unnamed')
161
+ return null;
162
+ return rendered;
163
+ }
164
+ /**
165
+ * The product a client names in its User-Agent: `claude-code/2.1.289 (external, cli)` is `claude-code`. It is the name to
166
+ * give a memory the client saved without saying who it is. Undefined when there is no product with a letter or a digit in it.
167
+ */
168
+ function productFromUserAgent(userAgent) {
169
+ const header = Array.isArray(userAgent) ? userAgent[0] : userAgent;
170
+ if (typeof header !== 'string')
171
+ return undefined;
172
+ const product = /^\s*([^\s/()]+)/.exec(header)?.[1];
173
+ return product !== undefined && headerSource(product) !== '' ? product : undefined;
174
+ }
175
+ /**
176
+ * The prefix every imported fact's ledger source carries (importing/plan.ts), and only an import's: it is how a read tells
177
+ * an imported fact from an agent's save. A named connection's author is a header name, which has no colon, so a named agent
178
+ * cannot write it; a source a caller chose (typed, or the product in its User-Agent) is kept off it by savedSource.
179
+ */
180
+ exports.IMPORT_SOURCE_PREFIX = 'import:';
181
+ /** Whether a stored source names an import (`import:markdown`, `import:renor-index`). */
182
+ function isImportSource(source) {
183
+ return typeof source === 'string' && source.startsWith(exports.IMPORT_SOURCE_PREFIX);
184
+ }
185
+ /**
186
+ * The ledger source of a save a caller made (remember, save_memory, retire, restore): `cleanSource`, with the import prefix
187
+ * written as a hyphen (`import:markdown` is `import-markdown`, as a note header writes it), so no caller records a save
188
+ * the way an import is recorded. The import path writes its events itself and never comes through here.
189
+ */
190
+ function savedSource(source) {
191
+ const name = cleanSource(source);
192
+ return isImportSource(name) ? `import-${name.slice(exports.IMPORT_SOURCE_PREFIX.length)}` : name;
193
+ }
194
+ function authorMarker(author, reader) {
195
+ const fromOther = typeof author === 'string' && author !== '' && author !== reader;
196
+ return {
197
+ ...(fromOther ? { source: author } : {}),
198
+ ...(isImportSource(author) ? { untrusted: true } : {}),
199
+ };
200
+ }
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WHATS_NEW_EXCERPT_CHARS = exports.WHATS_NEW_REQUEST_LIMIT = exports.WHATS_NEW_LIMIT = void 0;
4
+ exports.readWhatsNew = readWhatsNew;
5
+ const scope_1 = require("../mvp/scope");
6
+ const requests_1 = require("./requests");
7
+ /** How many items one call returns. A full page comes with a cursor for the rest. */
8
+ exports.WHATS_NEW_LIMIT = 50;
9
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
10
+ /**
11
+ * At most this many open requests lead a reply (the newest); the rest are counted (`more_requests`). list_requests
12
+ * returns the same page: the rest come up as these are answered or closed.
13
+ */
14
+ exports.WHATS_NEW_REQUEST_LIMIT = requests_1.REQUEST_PAGE_LIMIT;
15
+ /** One feed item carries this many characters of another agent's text. list_requests still returns the full question. */
16
+ exports.WHATS_NEW_EXCERPT_CHARS = 240;
17
+ function excerpt(text) {
18
+ if (text === null)
19
+ return null;
20
+ let end = 0;
21
+ let count = 0;
22
+ for (const point of text) {
23
+ if (count === exports.WHATS_NEW_EXCERPT_CHARS)
24
+ break;
25
+ end += point.length;
26
+ count++;
27
+ }
28
+ return text.slice(0, end);
29
+ }
30
+ // A sixth part, "peek", marks a window opened with an explicit `since`: its pages never move the mark either.
31
+ function cursorOf(floorAt, floorId, beforeAt, beforeId, asOf, peek) {
32
+ const parts = [floorAt, String(floorId), beforeAt, String(beforeId), asOf];
33
+ if (peek)
34
+ parts.push('peek');
35
+ return parts.join('|');
36
+ }
37
+ function parseCursor(cursor) {
38
+ const parts = cursor.split('|');
39
+ if ((parts.length !== 5 && !(parts.length === 6 && parts[5] === 'peek')) || !parts[0] || !parts[2] || !parts[4]) {
40
+ throw new Error('whats_new cursor is not one this workspace issued');
41
+ }
42
+ const floorId = Number(parts[1]);
43
+ const beforeId = Number(parts[3]);
44
+ if (!Number.isInteger(floorId) || !Number.isInteger(beforeId)) {
45
+ throw new Error('whats_new cursor is not one this workspace issued');
46
+ }
47
+ return { floorAt: parts[0], floorId, beforeAt: parts[2], beforeId, asOf: parts[4], peek: parts.length === 6 };
48
+ }
49
+ /**
50
+ * What was saved after this agent's mark, newest first. The mark moves to `as_of` only when the
51
+ * page was not full, so a bounded reply cannot skip the rest. An explicit `since` is a peek: it
52
+ * never moves the mark, so writes between the mark and `since` are still shown by the next plain call. This agent's own rows are left out
53
+ * unless `includeOwn`. The first call, with no mark, starts seven days back.
54
+ */
55
+ function readWhatsNew(db, args) {
56
+ const limit = args.limit ?? exports.WHATS_NEW_LIMIT;
57
+ const now = (args.now ?? new Date()).toISOString();
58
+ // One scope has several spellings (atlas, scope:atlas, project:atlas): read all of them and keep one mark.
59
+ const scope = args.scope !== undefined && args.scope.trim() !== '' ? (0, scope_1.normalizeScope)(args.scope) : null;
60
+ const scopeKey = scope ? (0, scope_1.scopeSpellings)(scope)[0] : '';
61
+ const page = args.cursor ? parseCursor(args.cursor) : null;
62
+ let floorAt;
63
+ let floorId;
64
+ let asOf;
65
+ let beforeAt = null;
66
+ let beforeId = null;
67
+ let peek = false;
68
+ if (page) {
69
+ peek = page.peek;
70
+ floorAt = page.floorAt;
71
+ floorId = page.floorId;
72
+ asOf = page.asOf;
73
+ beforeAt = page.beforeAt;
74
+ beforeId = page.beforeId;
75
+ }
76
+ else if (args.since) {
77
+ peek = true;
78
+ floorAt = args.since;
79
+ floorId = -1;
80
+ asOf = now;
81
+ }
82
+ else {
83
+ const stored = db.prepare('SELECT last_at, last_id FROM agent_cursors WHERE agent = ? AND scope = ?').get(args.agent, scopeKey);
84
+ if (stored) {
85
+ floorAt = stored.last_at;
86
+ floorId = stored.last_id;
87
+ }
88
+ else {
89
+ floorAt = new Date(Date.parse(now) - WEEK_MS).toISOString();
90
+ floorId = -1;
91
+ }
92
+ asOf = now;
93
+ }
94
+ const params = [floorAt, floorAt, floorId, asOf];
95
+ let beforeSql = '';
96
+ if (beforeAt !== null && beforeId !== null) {
97
+ beforeSql = ' AND (created_at < ? OR (created_at = ? AND id < ?))';
98
+ params.push(beforeAt, beforeAt, beforeId);
99
+ }
100
+ let ownSql = '';
101
+ if (!args.includeOwn) {
102
+ ownSql = ' AND (author IS NULL OR author != ?)';
103
+ params.push(args.agent);
104
+ }
105
+ let scopeSql = '';
106
+ if (scope) {
107
+ // A row written without a scope (before migration 19, or a note retire/restore) is global.
108
+ const saved = (0, scope_1.savedScopeSqlPredicate)(scope, "COALESCE(scope, 'global')");
109
+ scopeSql = ` AND ${saved.sql}`;
110
+ params.push(...saved.params);
111
+ }
112
+ params.push(limit + 1);
113
+ const rows = db.prepare(`SELECT id, action, summary, created_at, author, scope FROM write_events
114
+ WHERE (created_at > ? OR (created_at = ? AND id > ?))
115
+ AND created_at < ?
116
+ ${beforeSql}${ownSql}${scopeSql}
117
+ ORDER BY created_at DESC, id DESC
118
+ LIMIT ?`).all(...params);
119
+ const more = rows.length > limit;
120
+ const kept = more ? rows.slice(0, limit) : rows;
121
+ const logged = kept.map((row) => {
122
+ const source = row.author && row.author !== '' ? row.author : 'unnamed';
123
+ const other = Boolean(row.author && row.author !== '' && row.author !== args.agent);
124
+ return {
125
+ kind: row.action,
126
+ at: row.created_at,
127
+ source,
128
+ scope: row.scope,
129
+ summary: excerpt(row.summary),
130
+ ...(other ? { untrusted: true } : {}),
131
+ };
132
+ });
133
+ // Open requests stay at the front of the first page until they are answered or closed.
134
+ // A later cursor page does not repeat them.
135
+ const waiting = args.cursor ? { requests: [] } : (0, requests_1.listRequestsPage)(db, { agent: args.agent, status: 'open', scope: args.scope });
136
+ const open = waiting.requests.map((request) => ({
137
+ kind: 'request',
138
+ at: request.created_at,
139
+ source: request.from_agent,
140
+ scope: request.scope,
141
+ summary: excerpt(request.question),
142
+ requestId: request.id,
143
+ untrusted: true,
144
+ }));
145
+ const extra = waiting.more_requests
146
+ ? { more_requests: waiting.more_requests, more_requests_hint: waiting.more_requests_hint }
147
+ : {};
148
+ const items = [...open, ...logged];
149
+ if (more) {
150
+ const last = kept[kept.length - 1];
151
+ return { items, as_of: asOf, next_since: null, cursor: cursorOf(floorAt, floorId, last.created_at, last.id, asOf, peek), ...extra };
152
+ }
153
+ if (peek)
154
+ return { items, as_of: asOf, next_since: asOf, cursor: null, ...extra };
155
+ db.prepare(`INSERT INTO agent_cursors (agent, scope, last_at, last_id) VALUES (?, ?, ?, ?)
156
+ ON CONFLICT(agent, scope) DO UPDATE SET last_at = excluded.last_at, last_id = excluded.last_id`).run(args.agent, scopeKey, asOf, -1);
157
+ return { items, as_of: asOf, next_since: asOf, cursor: null, ...extra };
158
+ }