negotium 0.2.17 → 0.2.19

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 (38) hide show
  1. package/dist/agent-helpers.js +41 -32
  2. package/dist/agent-helpers.js.map +10 -10
  3. package/dist/{chunk-5cpdbbeh.js → chunk-fc58cqry.js} +3 -1
  4. package/dist/{chunk-5cpdbbeh.js.map → chunk-fc58cqry.js.map} +3 -3
  5. package/dist/hosted-agent.js +4 -2
  6. package/dist/hosted-agent.js.map +4 -4
  7. package/dist/main.js +1377 -285
  8. package/dist/main.js.map +14 -13
  9. package/dist/mcp-factories.js +1497 -403
  10. package/dist/mcp-factories.js.map +14 -13
  11. package/dist/prompts.js +8 -8
  12. package/dist/prompts.js.map +3 -3
  13. package/dist/registry.js +3 -3
  14. package/dist/registry.js.map +2 -2
  15. package/dist/rollout.js +1 -1
  16. package/dist/runtime/src/agents/archiver.ts +1 -1
  17. package/dist/runtime/src/agents/idle-archiver.ts +3 -2
  18. package/dist/runtime/src/mcp/canonical-proxy-server.ts +31 -15
  19. package/dist/runtime/src/mcp/wiki-search-index.ts +359 -0
  20. package/dist/runtime/src/mcp/wiki-server.ts +1358 -155
  21. package/dist/runtime/src/platform/mcp-config.ts +2 -1
  22. package/dist/runtime/src/prompts/agents/wiki-archiver.md +73 -48
  23. package/dist/runtime/src/prompts/builders.ts +17 -34
  24. package/dist/runtime/src/runtime/turn-runner.ts +14 -17
  25. package/dist/runtime/src/storage/api-topics.ts +13 -4
  26. package/dist/runtime/src/topics/lifecycle.ts +3 -2
  27. package/dist/runtime/src/types/api.ts +6 -0
  28. package/dist/runtime/src/version.ts +1 -1
  29. package/dist/storage.js +12 -6
  30. package/dist/storage.js.map +3 -3
  31. package/dist/types/packages/core/src/agents/archiver.d.ts +1 -1
  32. package/dist/types/packages/core/src/mcp/wiki-search-index.d.ts +59 -0
  33. package/dist/types/packages/core/src/mcp/wiki-server.d.ts +4 -0
  34. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -4
  35. package/dist/types/packages/core/src/storage/api-topics.d.ts +1 -0
  36. package/dist/types/packages/core/src/types/api.d.ts +6 -0
  37. package/dist/types/packages/core/src/version.d.ts +1 -1
  38. package/package.json +1 -1
@@ -177,11 +177,12 @@ export function archiveActiveTopicForMemory(
177
177
  );
178
178
  return "below-threshold";
179
179
  }
180
- const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
180
+ const memoryTopic = topic.memoryKey ? topic : (getTopicMemoryOrigin(topicId) ?? topic);
181
+ const memoryTitle = memoryTopic.memoryKey?.trim() || memoryTopic.title;
181
182
  const launched = (options.launchArchiver ?? runArchiverTurn)({
182
183
  userId,
183
184
  topicId: memoryTopic.id,
184
- topicTitle: memoryTopic.title,
185
+ topicTitle: memoryTitle,
185
186
  archivePath: job.archivePath,
186
187
  ...(options.rawArchivePaths ? { rawArchivePaths: options.rawArchivePaths } : {}),
187
188
  messageCount: job.messageCount,
@@ -91,10 +91,27 @@ const wikiTools: Tool[] = [
91
91
  description: "Search the canonical workspace wiki.",
92
92
  inputSchema: {
93
93
  type: "object",
94
- properties: { question: { type: "string" } },
94
+ properties: {
95
+ question: { type: "string" },
96
+ kind: { type: "string", enum: ["all", "topic", "article", "summary"] },
97
+ limit: { type: "number", minimum: 1, maximum: 20 },
98
+ },
95
99
  required: ["question"],
96
100
  },
97
101
  },
102
+ {
103
+ name: "wiki_read",
104
+ description: "Read a canonical wiki result and optionally adopt a topic memory.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ kind: { type: "string", enum: ["topic", "article", "summary"] },
109
+ key: { type: "string" },
110
+ adopt: { type: "boolean" },
111
+ },
112
+ required: ["kind", "key"],
113
+ },
114
+ },
98
115
  {
99
116
  name: "wiki_topic_brief",
100
117
  description: "Read this canonical topic's brief.",
@@ -109,27 +126,26 @@ const wikiTools: Tool[] = [
109
126
  },
110
127
  },
111
128
  {
112
- name: "save_wiki_entry",
113
- description: "Save a summary to the canonical workspace wiki.",
114
- inputSchema: {
115
- type: "object",
116
- properties: { content: { type: "string" } },
117
- required: ["content"],
118
- },
119
- },
120
- {
121
- name: "save_topic_brief",
129
+ name: "wiki_write",
122
130
  description:
123
- "Write the accumulated persona/topic brief to the canonical workspace wiki (call after save_wiki_entry).",
131
+ "Write a canonical wiki document and its catalog row together (summary, article, or topic brief).",
124
132
  inputSchema: {
125
133
  type: "object",
126
- properties: { content: { type: "string" } },
127
- required: ["content"],
134
+ properties: {
135
+ kind: { type: "string", enum: ["summary", "article", "topic"] },
136
+ content: { type: "string" },
137
+ description: { type: "string" },
138
+ topic: { type: "string" },
139
+ slug: { type: "string" },
140
+ section: { type: "string" },
141
+ date: { type: "string" },
142
+ },
143
+ required: ["kind", "content", "description"],
128
144
  },
129
145
  },
130
146
  {
131
147
  name: "index_upsert",
132
- description: "Upsert a canonical wiki index entry.",
148
+ description: "Correct the description or section of an existing canonical wiki entry.",
133
149
  inputSchema: {
134
150
  type: "object",
135
151
  properties: {
@@ -0,0 +1,359 @@
1
+ import { chmodSync, existsSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { Database } from "#storage/sqlite";
3
+
4
+ const SEARCH_INDEX_SCHEMA_VERSION = 3;
5
+
6
+ export type IndexedWikiKind = "article" | "summary";
7
+
8
+ export interface WikiSearchIndexSource {
9
+ kind: IndexedWikiKind;
10
+ key: string;
11
+ path: string;
12
+ mtimeMs: number;
13
+ size: number;
14
+ }
15
+
16
+ export interface PreparedWikiSearchDocument {
17
+ title: string;
18
+ text: string;
19
+ tokens: string[];
20
+ date?: string;
21
+ family?: string;
22
+ }
23
+
24
+ export interface WikiSearchIndexDocument extends PreparedWikiSearchDocument {
25
+ id: string;
26
+ kind: IndexedWikiKind;
27
+ key: string;
28
+ path: string;
29
+ }
30
+
31
+ export interface WikiSearchIndexMetadata {
32
+ id: string;
33
+ kind: IndexedWikiKind;
34
+ key: string;
35
+ title: string;
36
+ date?: string;
37
+ }
38
+
39
+ export interface WikiSummaryTemporalQuery {
40
+ year?: string;
41
+ month?: string;
42
+ direction: "latest" | "oldest";
43
+ limit: number;
44
+ }
45
+
46
+ export interface WikiSearchIndexSyncResult {
47
+ added: number;
48
+ updated: number;
49
+ removed: number;
50
+ }
51
+
52
+ interface StoredSourceRow {
53
+ id: string;
54
+ path: string;
55
+ mtime_ms: number;
56
+ size: number;
57
+ }
58
+
59
+ interface StoredDocumentRow {
60
+ id: string;
61
+ kind: IndexedWikiKind;
62
+ key: string;
63
+ path: string;
64
+ title: string;
65
+ text: string;
66
+ date: string | null;
67
+ }
68
+
69
+ interface StoredMetadataRow {
70
+ id: string;
71
+ kind: IndexedWikiKind;
72
+ key: string;
73
+ title: string;
74
+ date: string | null;
75
+ }
76
+
77
+ function documentId(source: Pick<WikiSearchIndexSource, "kind" | "key">): string {
78
+ return `${source.kind}:${source.key}`;
79
+ }
80
+
81
+ function ensureSchema(database: InstanceType<typeof Database>): void {
82
+ const version = database
83
+ .query<{ user_version: number }, []>("PRAGMA user_version")
84
+ .get()?.user_version;
85
+ if (version !== 0 && version !== SEARCH_INDEX_SCHEMA_VERSION) {
86
+ throw new Error(`Unsupported wiki search index schema: ${version}`);
87
+ }
88
+ database.exec(`
89
+ CREATE TABLE IF NOT EXISTS wiki_search_documents (
90
+ id TEXT PRIMARY KEY,
91
+ kind TEXT NOT NULL,
92
+ key TEXT NOT NULL,
93
+ path TEXT NOT NULL,
94
+ mtime_ms REAL NOT NULL,
95
+ size INTEGER NOT NULL,
96
+ title TEXT NOT NULL,
97
+ text TEXT NOT NULL,
98
+ date TEXT,
99
+ family TEXT
100
+ );
101
+ CREATE UNIQUE INDEX IF NOT EXISTS wiki_search_documents_kind_key
102
+ ON wiki_search_documents(kind, key);
103
+ CREATE INDEX IF NOT EXISTS wiki_search_documents_kind_date
104
+ ON wiki_search_documents(kind, date);
105
+ CREATE INDEX IF NOT EXISTS wiki_search_documents_kind_family_date
106
+ ON wiki_search_documents(kind, family, date);
107
+ CREATE VIRTUAL TABLE IF NOT EXISTS wiki_search_fts USING fts5(
108
+ document_id UNINDEXED,
109
+ kind UNINDEXED,
110
+ tokens,
111
+ tokenize = 'unicode61'
112
+ );
113
+ PRAGMA user_version = ${SEARCH_INDEX_SCHEMA_VERSION};
114
+ `);
115
+ }
116
+
117
+ export class WikiSearchIndex {
118
+ readonly #database: InstanceType<typeof Database>;
119
+
120
+ constructor(readonly path: string) {
121
+ this.#database = new Database(path, { create: true });
122
+ try {
123
+ this.#database.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = DELETE;");
124
+ ensureSchema(this.#database);
125
+ chmodSync(path, 0o600);
126
+ } catch (error) {
127
+ this.#database.close();
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ close(): void {
133
+ this.#database.close();
134
+ }
135
+
136
+ /**
137
+ * Record one document that was just written.
138
+ *
139
+ * The write path already holds the document text, so the index is maintained
140
+ * incrementally and never needs to scan the content directories. `sync` stays
141
+ * for the explicit backfill of documents that predate the write tool.
142
+ */
143
+ upsert(source: WikiSearchIndexSource, document: PreparedWikiSearchDocument): void {
144
+ const id = documentId(source);
145
+ const apply = this.#database.transaction(() => {
146
+ this.#database.query("DELETE FROM wiki_search_fts WHERE document_id = ?").run(id);
147
+ this.#database.query("DELETE FROM wiki_search_documents WHERE id = ?").run(id);
148
+ this.#database
149
+ .query(`
150
+ INSERT INTO wiki_search_documents
151
+ (id, kind, key, path, mtime_ms, size, title, text, date, family)
152
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
153
+ `)
154
+ .run(
155
+ id,
156
+ source.kind,
157
+ source.key,
158
+ source.path,
159
+ source.mtimeMs,
160
+ source.size,
161
+ document.title,
162
+ document.text,
163
+ document.date ?? null,
164
+ document.family ?? null,
165
+ );
166
+ this.#database
167
+ .query("INSERT INTO wiki_search_fts(document_id, kind, tokens) VALUES (?, ?, ?)")
168
+ .run(id, source.kind, document.tokens.join(" "));
169
+ });
170
+ apply.immediate();
171
+ }
172
+
173
+ sync(
174
+ sources: WikiSearchIndexSource[],
175
+ prepare: (source: WikiSearchIndexSource, text: string) => PreparedWikiSearchDocument,
176
+ ): WikiSearchIndexSyncResult {
177
+ const result: WikiSearchIndexSyncResult = { added: 0, updated: 0, removed: 0 };
178
+ const existingRows = this.#database
179
+ .query<StoredSourceRow, []>(
180
+ "SELECT id, path, mtime_ms, size FROM wiki_search_documents ORDER BY id",
181
+ )
182
+ .all();
183
+ const existing = new Map(existingRows.map((row) => [row.id, row]));
184
+ const present = new Set<string>();
185
+ const changed: Array<{
186
+ source: WikiSearchIndexSource;
187
+ document: PreparedWikiSearchDocument;
188
+ added: boolean;
189
+ }> = [];
190
+
191
+ for (const source of sources) {
192
+ const id = documentId(source);
193
+ present.add(id);
194
+ const stored = existing.get(id);
195
+ if (
196
+ stored &&
197
+ stored.path === source.path &&
198
+ stored.mtime_ms === source.mtimeMs &&
199
+ stored.size === source.size
200
+ ) {
201
+ continue;
202
+ }
203
+ try {
204
+ const text = readFileSync(source.path, "utf-8");
205
+ changed.push({ source, document: prepare(source, text), added: !stored });
206
+ } catch {
207
+ // Keep the previous indexed copy, if any. A later sync can retry after
208
+ // a concurrent writer finishes or permissions are restored.
209
+ }
210
+ }
211
+ const removed = existingRows.filter((row) => !present.has(row.id));
212
+
213
+ const apply = this.#database.transaction(() => {
214
+ const deleteTerms = this.#database.query("DELETE FROM wiki_search_fts WHERE document_id = ?");
215
+ const deleteDocument = this.#database.query("DELETE FROM wiki_search_documents WHERE id = ?");
216
+ const insertDocument = this.#database.query(`
217
+ INSERT INTO wiki_search_documents
218
+ (id, kind, key, path, mtime_ms, size, title, text, date, family)
219
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
220
+ `);
221
+ const insertTerms = this.#database.query(`
222
+ INSERT INTO wiki_search_fts(document_id, kind, tokens)
223
+ VALUES (?, ?, ?)
224
+ `);
225
+
226
+ for (const row of removed) {
227
+ deleteTerms.run(row.id);
228
+ deleteDocument.run(row.id);
229
+ result.removed += 1;
230
+ }
231
+ for (const { source, document, added } of changed) {
232
+ const id = documentId(source);
233
+ deleteTerms.run(id);
234
+ deleteDocument.run(id);
235
+ insertDocument.run(
236
+ id,
237
+ source.kind,
238
+ source.key,
239
+ source.path,
240
+ source.mtimeMs,
241
+ source.size,
242
+ document.title,
243
+ document.text,
244
+ document.date ?? null,
245
+ document.family ?? null,
246
+ );
247
+ insertTerms.run(id, source.kind, document.tokens.join(" "));
248
+ if (added) result.added += 1;
249
+ else result.updated += 1;
250
+ }
251
+ });
252
+ apply.immediate();
253
+ return result;
254
+ }
255
+
256
+ metadata(kinds: IndexedWikiKind[]): WikiSearchIndexMetadata[] {
257
+ if (kinds.length === 0) return [];
258
+ const placeholders = kinds.map(() => "?").join(", ");
259
+ const rows = this.#database
260
+ .query<StoredMetadataRow, string[]>(
261
+ `SELECT id, kind, key, title, date
262
+ FROM wiki_search_documents
263
+ WHERE kind IN (${placeholders})`,
264
+ )
265
+ .all(...kinds);
266
+ return rows.map((row) => ({
267
+ id: row.id,
268
+ kind: row.kind,
269
+ key: row.key,
270
+ title: row.title,
271
+ ...(row.date ? { date: row.date } : {}),
272
+ }));
273
+ }
274
+
275
+ summaryMetadataByDate(query: WikiSummaryTemporalQuery): WikiSearchIndexMetadata[] {
276
+ const conditions = ["kind = 'summary'", "date IS NOT NULL"];
277
+ const parameters: Array<string | number> = [];
278
+ if (query.year && query.month) {
279
+ const month = Number(query.month);
280
+ const nextYear = month === 12 ? String(Number(query.year) + 1) : query.year;
281
+ const nextMonth = String(month === 12 ? 1 : month + 1).padStart(2, "0");
282
+ conditions.push("date >= ?", "date < ?");
283
+ parameters.push(`${query.year}-${query.month}-01`, `${nextYear}-${nextMonth}-01`);
284
+ } else if (query.year) {
285
+ conditions.push("date >= ?", "date < ?");
286
+ parameters.push(`${query.year}-01-01`, `${Number(query.year) + 1}-01-01`);
287
+ } else if (query.month) {
288
+ conditions.push("substr(date, 6, 2) = ?");
289
+ parameters.push(query.month);
290
+ }
291
+ parameters.push(query.limit);
292
+ const rows = this.#database
293
+ .query<StoredMetadataRow, Array<string | number>>(
294
+ `SELECT id, kind, key, title, date
295
+ FROM wiki_search_documents
296
+ WHERE ${conditions.join(" AND ")}
297
+ ORDER BY date ${query.direction === "oldest" ? "ASC" : "DESC"}, key ASC
298
+ LIMIT ?`,
299
+ )
300
+ .all(...parameters);
301
+ return rows.map((row) => ({
302
+ id: row.id,
303
+ kind: row.kind,
304
+ key: row.key,
305
+ title: row.title,
306
+ ...(row.date ? { date: row.date } : {}),
307
+ }));
308
+ }
309
+
310
+ matchingDocumentIds(kinds: IndexedWikiKind[], terms: string[]): Set<string> {
311
+ if (kinds.length === 0 || terms.length === 0) return new Set();
312
+ const kindPlaceholders = kinds.map(() => "?").join(", ");
313
+ const match = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
314
+ const rows = this.#database
315
+ .query<{ document_id: string }, string[]>(
316
+ `SELECT DISTINCT document_id
317
+ FROM wiki_search_fts
318
+ WHERE wiki_search_fts MATCH ? AND kind IN (${kindPlaceholders})`,
319
+ )
320
+ .all(match, ...kinds);
321
+ return new Set(rows.map((row) => row.document_id));
322
+ }
323
+
324
+ documents(ids: Set<string>): WikiSearchIndexDocument[] {
325
+ if (ids.size === 0) return [];
326
+ const values = [...ids];
327
+ const rows: StoredDocumentRow[] = [];
328
+ for (let offset = 0; offset < values.length; offset += 500) {
329
+ const chunk = values.slice(offset, offset + 500);
330
+ const placeholders = chunk.map(() => "?").join(", ");
331
+ rows.push(
332
+ ...this.#database
333
+ .query<StoredDocumentRow, string[]>(
334
+ `SELECT id, kind, key, path, title, text, date
335
+ FROM wiki_search_documents
336
+ WHERE id IN (${placeholders})`,
337
+ )
338
+ .all(...chunk),
339
+ );
340
+ }
341
+ return rows.map((row) => ({
342
+ id: row.id,
343
+ kind: row.kind,
344
+ key: row.key,
345
+ path: row.path,
346
+ title: row.title,
347
+ text: row.text,
348
+ tokens: [],
349
+ ...(row.date ? { date: row.date } : {}),
350
+ }));
351
+ }
352
+ }
353
+
354
+ export function resetCorruptWikiSearchIndex(path: string): void {
355
+ for (const candidate of [path, `${path}-journal`, `${path}-wal`, `${path}-shm`]) {
356
+ if (!existsSync(candidate)) continue;
357
+ unlinkSync(candidate);
358
+ }
359
+ }