@tenex-chat/backend 0.9.5 → 0.9.7

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 (148) hide show
  1. package/README.md +5 -1
  2. package/dist/daemon-wrapper.cjs +34 -0
  3. package/dist/index.js +59268 -0
  4. package/dist/wrapper.js +171 -0
  5. package/package.json +19 -27
  6. package/src/agents/AgentRegistry.ts +9 -7
  7. package/src/agents/AgentStorage.ts +24 -1
  8. package/src/agents/agent-installer.ts +6 -0
  9. package/src/agents/agent-loader.ts +7 -2
  10. package/src/agents/constants.ts +10 -2
  11. package/src/agents/execution/AgentExecutor.ts +35 -6
  12. package/src/agents/execution/StreamCallbacks.ts +53 -13
  13. package/src/agents/execution/StreamExecutionHandler.ts +110 -16
  14. package/src/agents/execution/StreamSetup.ts +19 -9
  15. package/src/agents/execution/ToolEventHandlers.ts +112 -0
  16. package/src/agents/role-categories.ts +53 -0
  17. package/src/agents/types/runtime.ts +7 -0
  18. package/src/agents/types/storage.ts +7 -0
  19. package/src/commands/agent/import/openclaw-distiller.ts +63 -7
  20. package/src/commands/agent/import/openclaw-reader.ts +54 -0
  21. package/src/commands/agent/import/openclaw.ts +120 -29
  22. package/src/commands/agent/index.ts +83 -2
  23. package/src/commands/setup/display.ts +123 -0
  24. package/src/commands/setup/embed.ts +13 -13
  25. package/src/commands/setup/global-system-prompt.ts +15 -17
  26. package/src/commands/setup/image.ts +17 -20
  27. package/src/commands/setup/interactive.ts +37 -20
  28. package/src/commands/setup/llm.ts +12 -7
  29. package/src/commands/setup/onboarding.ts +1580 -248
  30. package/src/commands/setup/providers.ts +3 -3
  31. package/src/conversations/ConversationStore.ts +23 -2
  32. package/src/conversations/MessageBuilder.ts +51 -73
  33. package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
  34. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
  35. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
  36. package/src/conversations/services/ConversationSummarizer.ts +1 -2
  37. package/src/conversations/types.ts +11 -0
  38. package/src/daemon/Daemon.ts +78 -57
  39. package/src/daemon/ProjectRuntime.ts +6 -12
  40. package/src/daemon/SubscriptionManager.ts +13 -0
  41. package/src/daemon/index.ts +0 -1
  42. package/src/event-handler/index.ts +1 -0
  43. package/src/index.ts +20 -1
  44. package/src/llm/ChunkHandler.ts +1 -1
  45. package/src/llm/FinishHandler.ts +28 -4
  46. package/src/llm/LLMConfigEditor.ts +218 -106
  47. package/src/llm/index.ts +0 -4
  48. package/src/llm/meta/MetaModelResolver.ts +3 -18
  49. package/src/llm/middleware/message-sanitizer.ts +153 -0
  50. package/src/llm/providers/ollama-models.ts +0 -38
  51. package/src/llm/service.ts +50 -15
  52. package/src/llm/types.ts +0 -12
  53. package/src/llm/utils/ConfigurationManager.ts +88 -465
  54. package/src/llm/utils/ConfigurationTester.ts +42 -185
  55. package/src/llm/utils/ModelSelector.ts +156 -92
  56. package/src/llm/utils/ProviderConfigUI.ts +10 -141
  57. package/src/llm/utils/models-dev-cache.ts +102 -23
  58. package/src/llm/utils/provider-select-prompt.ts +284 -0
  59. package/src/llm/utils/provider-setup.ts +81 -34
  60. package/src/llm/utils/variant-list-prompt.ts +361 -0
  61. package/src/nostr/AgentEventDecoder.ts +1 -0
  62. package/src/nostr/AgentEventEncoder.ts +37 -0
  63. package/src/nostr/AgentProfilePublisher.ts +13 -0
  64. package/src/nostr/AgentPublisher.ts +26 -0
  65. package/src/nostr/kinds.ts +1 -0
  66. package/src/nostr/ndkClient.ts +4 -1
  67. package/src/nostr/types.ts +12 -0
  68. package/src/prompts/fragments/25-rag-instructions.ts +22 -21
  69. package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
  70. package/src/prompts/fragments/index.ts +2 -0
  71. package/src/prompts/utils/systemPromptBuilder.ts +18 -28
  72. package/src/services/AgentDefinitionMonitor.ts +8 -0
  73. package/src/services/ConfigService.ts +34 -0
  74. package/src/services/PubkeyService.ts +7 -1
  75. package/src/services/compression/CompressionService.ts +133 -74
  76. package/src/services/compression/compression-utils.ts +110 -19
  77. package/src/services/config/types.ts +0 -6
  78. package/src/services/dispatch/AgentDispatchService.ts +79 -0
  79. package/src/services/intervention/InterventionService.ts +78 -5
  80. package/src/services/nip46/Nip46SigningService.ts +30 -1
  81. package/src/services/projects/ProjectContext.ts +8 -6
  82. package/src/services/rag/RAGCollectionRegistry.ts +199 -0
  83. package/src/services/rag/RAGDatabaseService.ts +2 -7
  84. package/src/services/rag/RAGOperations.ts +25 -45
  85. package/src/services/rag/RAGService.ts +0 -31
  86. package/src/services/rag/RagSubscriptionService.ts +71 -122
  87. package/src/services/rag/rag-utils.ts +13 -0
  88. package/src/services/ral/RALRegistry.ts +25 -184
  89. package/src/services/reports/ReportEmbeddingService.ts +63 -113
  90. package/src/services/search/UnifiedSearchService.ts +115 -4
  91. package/src/services/search/index.ts +1 -0
  92. package/src/services/search/projectFilter.ts +20 -4
  93. package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
  94. package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
  95. package/src/services/search/providers/LessonSearchProvider.ts +1 -8
  96. package/src/services/search/providers/ReportSearchProvider.ts +1 -0
  97. package/src/services/search/types.ts +24 -3
  98. package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
  99. package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
  100. package/src/telemetry/setup.ts +2 -13
  101. package/src/tools/implementations/ask.ts +3 -3
  102. package/src/tools/implementations/conversation_get.ts +28 -268
  103. package/src/tools/implementations/fs_grep.ts +6 -6
  104. package/src/tools/implementations/fs_read.ts +2 -0
  105. package/src/tools/implementations/fs_write.ts +2 -0
  106. package/src/tools/implementations/learn.ts +38 -50
  107. package/src/tools/implementations/rag_add_documents.ts +6 -4
  108. package/src/tools/implementations/rag_create_collection.ts +37 -4
  109. package/src/tools/implementations/rag_delete_collection.ts +9 -0
  110. package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
  111. package/src/tools/registry.ts +7 -8
  112. package/src/tools/types.ts +11 -2
  113. package/src/tools/utils/transcript-args.ts +13 -0
  114. package/src/utils/cli-theme.ts +13 -0
  115. package/src/utils/logger.ts +55 -0
  116. package/src/utils/metadataKeys.ts +17 -0
  117. package/src/utils/sqlEscaping.ts +39 -0
  118. package/src/wrapper.ts +7 -3
  119. package/dist/src/index.js +0 -46790
  120. package/dist/tenex-backend-wrapper.cjs +0 -3
  121. package/src/agents/execution/constants.ts +0 -16
  122. package/src/agents/execution/index.ts +0 -3
  123. package/src/agents/index.ts +0 -4
  124. package/src/commands/agent.ts +0 -235
  125. package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
  126. package/src/conversations/formatters/index.ts +0 -9
  127. package/src/conversations/index.ts +0 -2
  128. package/src/conversations/utils/content-utils.ts +0 -69
  129. package/src/daemon/UnixSocketTransport.ts +0 -318
  130. package/src/event-handler/newConversation.ts +0 -165
  131. package/src/events/NDKProjectStatus.ts +0 -384
  132. package/src/events/index.ts +0 -4
  133. package/src/lib/json-parser.ts +0 -30
  134. package/src/llm/RecordingState.ts +0 -37
  135. package/src/llm/StreamPublisher.ts +0 -40
  136. package/src/llm/middleware/flight-recorder.ts +0 -188
  137. package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
  138. package/src/nostr/constants.ts +0 -38
  139. package/src/prompts/core/index.ts +0 -3
  140. package/src/prompts/index.ts +0 -21
  141. package/src/services/image/index.ts +0 -12
  142. package/src/services/status/index.ts +0 -11
  143. package/src/telemetry/diagnostics.ts +0 -27
  144. package/src/tools/implementations/rag_query.ts +0 -107
  145. package/src/types/index.ts +0 -46
  146. package/src/utils/agentFetcher.ts +0 -107
  147. package/src/utils/conversation-utils.ts +0 -1
  148. package/src/utils/process.ts +0 -49
@@ -0,0 +1,199 @@
1
+ /**
2
+ * RAGCollectionRegistry - Tracks collection metadata for scope-aware search.
3
+ *
4
+ * Stores scope, project, and agent metadata for each RAG collection as a
5
+ * JSON sidecar file alongside the LanceDB data directory. This enables
6
+ * rag_search() to automatically include only relevant collections based on
7
+ * the agent's context (global, project, personal).
8
+ *
9
+ * IMPORTANT: This is NOT access control. No requests are ever denied.
10
+ * Scoping only determines default visibility when rag_search() runs
11
+ * without an explicit `collections` parameter.
12
+ */
13
+
14
+ import { logger } from "@/utils/logger";
15
+ import { getLanceDBDataDir } from "./rag-utils";
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+
19
+ /** Scope types for RAG collections */
20
+ export type CollectionScope = "global" | "project" | "personal";
21
+
22
+ /** Metadata stored for each registered collection */
23
+ export interface CollectionMetadata {
24
+ /** Scope determines default visibility in rag_search() */
25
+ scope: CollectionScope;
26
+
27
+ /** Project ID (NIP-33 address) that created the collection */
28
+ projectId?: string;
29
+
30
+ /** Agent pubkey that created the collection */
31
+ agentPubkey?: string;
32
+
33
+ /** When the collection was first registered */
34
+ createdAt: number;
35
+ }
36
+
37
+ /** Shape of the sidecar JSON file */
38
+ interface RegistryData {
39
+ /** Schema version for future migrations */
40
+ version: 1;
41
+
42
+ /** Map of collection name → metadata */
43
+ collections: Record<string, CollectionMetadata>;
44
+ }
45
+
46
+ const REGISTRY_FILENAME = "collection-registry.json";
47
+
48
+ export class RAGCollectionRegistry {
49
+ private static instance: RAGCollectionRegistry | null = null;
50
+ private data: RegistryData;
51
+ private readonly filePath: string;
52
+
53
+ private constructor() {
54
+ this.filePath = path.join(getLanceDBDataDir(), REGISTRY_FILENAME);
55
+ this.data = this.load();
56
+ }
57
+
58
+ public static getInstance(): RAGCollectionRegistry {
59
+ if (!RAGCollectionRegistry.instance) {
60
+ RAGCollectionRegistry.instance = new RAGCollectionRegistry();
61
+ }
62
+ return RAGCollectionRegistry.instance;
63
+ }
64
+
65
+ /**
66
+ * Register a collection with scope metadata.
67
+ * Overwrites existing metadata for the same collection name.
68
+ */
69
+ public register(
70
+ name: string,
71
+ metadata: Omit<CollectionMetadata, "createdAt">
72
+ ): void {
73
+ const existing = this.data.collections[name];
74
+ this.data.collections[name] = {
75
+ ...metadata,
76
+ createdAt: existing?.createdAt ?? Date.now(),
77
+ };
78
+ this.save();
79
+ logger.debug(`[RAGCollectionRegistry] Registered collection '${name}'`, {
80
+ scope: metadata.scope,
81
+ projectId: metadata.projectId,
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Get metadata for a specific collection.
87
+ * Returns undefined for unregistered (legacy) collections.
88
+ */
89
+ public get(name: string): CollectionMetadata | undefined {
90
+ return this.data.collections[name];
91
+ }
92
+
93
+ /**
94
+ * Remove a collection from the registry.
95
+ */
96
+ public unregister(name: string): void {
97
+ if (this.data.collections[name]) {
98
+ delete this.data.collections[name];
99
+ this.save();
100
+ logger.debug(`[RAGCollectionRegistry] Unregistered collection '${name}'`);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get all collections that match the given context.
106
+ *
107
+ * Matching rules:
108
+ * - `global` collections: always included
109
+ * - `project` collections: included when projectId matches
110
+ * - `personal` collections: included when agentPubkey matches
111
+ * - Unregistered (legacy) collections: treated as global
112
+ *
113
+ * @param allCollections - All known collection names (from RAGService.listCollections)
114
+ * @param projectId - Current project ID
115
+ * @param agentPubkey - Current agent's pubkey
116
+ * @returns Collection names that match the context
117
+ */
118
+ public getMatchingCollections(
119
+ allCollections: string[],
120
+ projectId: string,
121
+ agentPubkey?: string
122
+ ): string[] {
123
+ return allCollections.filter((name) => {
124
+ const metadata = this.data.collections[name];
125
+
126
+ // Unregistered (legacy) collections are treated as global
127
+ if (!metadata) return true;
128
+
129
+ switch (metadata.scope) {
130
+ case "global":
131
+ return true;
132
+ case "project":
133
+ return metadata.projectId === projectId;
134
+ case "personal":
135
+ return metadata.agentPubkey === agentPubkey;
136
+ default:
137
+ // Unknown scope — treat as global for safety
138
+ return true;
139
+ }
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Get all registered collection metadata.
145
+ * Returns a deep copy to prevent callers from mutating internal state.
146
+ */
147
+ public getAll(): Record<string, CollectionMetadata> {
148
+ return JSON.parse(JSON.stringify(this.data.collections));
149
+ }
150
+
151
+ /**
152
+ * Load registry from the sidecar JSON file.
153
+ * Returns empty registry if file doesn't exist or is corrupted.
154
+ */
155
+ private load(): RegistryData {
156
+ try {
157
+ if (fs.existsSync(this.filePath)) {
158
+ const raw = fs.readFileSync(this.filePath, "utf-8");
159
+ const parsed = JSON.parse(raw) as RegistryData;
160
+ if (parsed.version === 1 && parsed.collections) {
161
+ return parsed;
162
+ }
163
+ logger.warn("[RAGCollectionRegistry] Unknown registry version, starting fresh");
164
+ }
165
+ } catch (error) {
166
+ const message = error instanceof Error ? error.message : String(error);
167
+ logger.warn("[RAGCollectionRegistry] Failed to load registry, starting fresh", {
168
+ error: message,
169
+ });
170
+ }
171
+ return { version: 1, collections: {} };
172
+ }
173
+
174
+ /**
175
+ * Persist registry to the sidecar JSON file.
176
+ */
177
+ private save(): void {
178
+ try {
179
+ // Ensure directory exists
180
+ const dir = path.dirname(this.filePath);
181
+ if (!fs.existsSync(dir)) {
182
+ fs.mkdirSync(dir, { recursive: true });
183
+ }
184
+ fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), "utf-8");
185
+ } catch (error) {
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ logger.error("[RAGCollectionRegistry] Failed to save registry", {
188
+ error: message,
189
+ });
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Reset singleton (for testing).
195
+ */
196
+ public static resetInstance(): void {
197
+ RAGCollectionRegistry.instance = null;
198
+ }
199
+ }
@@ -1,7 +1,6 @@
1
- import { config } from "@/services/ConfigService";
2
- import * as path from "node:path";
3
1
  import { handleError } from "@/utils/error-handler";
4
2
  import { logger } from "@/utils/logger";
3
+ import { getLanceDBDataDir } from "./rag-utils";
5
4
  import { type Connection, type Table, connect } from "@lancedb/lancedb";
6
5
 
7
6
  /**
@@ -27,11 +26,7 @@ export class RAGDatabaseService {
27
26
  private tableCache: Map<string, Table> = new Map();
28
27
 
29
28
  constructor(dataDir?: string) {
30
- // Use provided directory or environment variable, fallback to global location
31
- this.dataDir =
32
- dataDir ||
33
- process.env.LANCEDB_DATA_DIR ||
34
- path.join(config.getConfigPath("data"), "lancedb");
29
+ this.dataDir = dataDir || getLanceDBDataDir();
35
30
 
36
31
  logger.debug(`RAGDatabaseService initialized with data directory: ${this.dataDir}`);
37
32
  }
@@ -1,5 +1,7 @@
1
1
  import type { DocumentMetadata, LanceDBResult, LanceDBStoredDocument } from "@/services/rag/rag-utils";
2
2
  import { calculateRelevanceScore, mapLanceResultToDocument } from "@/services/rag/rag-utils";
3
+ import { AGENT_PUBKEY_KEYS } from "@/utils/metadataKeys";
4
+ import { SQL_LIKE_ESCAPE_CLAUSE, escapeSqlLikeValue } from "@/utils/sqlEscaping";
3
5
  import { handleError } from "@/utils/error-handler";
4
6
  import { logger } from "@/utils/logger";
5
7
  import type { Table, VectorQuery } from "@lancedb/lancedb";
@@ -469,33 +471,13 @@ export class RAGOperations {
469
471
  * @param agentPubkey Optional agent pubkey to count their contributions
470
472
  * @returns Object with total count and optional agent-specific count
471
473
  */
472
- /**
473
- * Escape a string for use in SQL LIKE pattern.
474
- * Escapes: single quotes ('), double quotes ("), backslashes (\), and LIKE wildcards (%, _).
475
- *
476
- * IMPORTANT: DataFusion (used by LanceDB) has NO default escape character.
477
- * The backslash escapes here only work when paired with ESCAPE '\\' clause.
478
- * See: https://github.com/apache/datafusion/issues/13291
479
- *
480
- * Note: Agent pubkeys are hex strings (0-9, a-f) so most escaping isn't strictly needed,
481
- * but we escape properly for defense-in-depth and to handle any future metadata fields.
482
- */
483
- private escapeSqlLikeValue(value: string): string {
484
- return value
485
- .replace(/\\/g, "\\\\") // Escape backslashes first
486
- .replace(/'/g, "''") // SQL standard: escape single quote by doubling
487
- .replace(/"/g, '\\"') // Escape double quotes
488
- .replace(/%/g, "\\%") // Escape LIKE wildcard %
489
- .replace(/_/g, "\\_"); // Escape LIKE wildcard _
490
- }
491
-
492
474
  async getCollectionStats(
493
475
  collectionName: string,
494
476
  agentPubkey?: string
495
477
  ): Promise<{ totalCount: number; agentCount?: number }> {
496
- const table = await this.dbManager.getTable(collectionName);
497
-
498
478
  try {
479
+ const table = await this.dbManager.getTable(collectionName);
480
+
499
481
  // Get total document count
500
482
  const totalCount = await table.countRows();
501
483
 
@@ -503,11 +485,11 @@ export class RAGOperations {
503
485
  // The metadata field is stored as JSON string, so we use LIKE for matching
504
486
  let agentCount: number | undefined;
505
487
  if (agentPubkey) {
506
- // SQL filter for JSON string field containing agent_pubkey
507
- // Format: metadata LIKE '%"agent_pubkey":"<pubkey>"%' ESCAPE '\\'
508
- // ESCAPE clause is required because DataFusion has no default escape character
509
- const escapedPubkey = this.escapeSqlLikeValue(agentPubkey);
510
- const filter = `metadata LIKE '%"agent_pubkey":"${escapedPubkey}"%' ESCAPE '\\\\'`;
488
+ const escaped = escapeSqlLikeValue(agentPubkey);
489
+ const clauses = AGENT_PUBKEY_KEYS
490
+ .map((key) => `metadata LIKE '%"${key}":"${escaped}"%' ${SQL_LIKE_ESCAPE_CLAUSE}`)
491
+ .join(" OR ");
492
+ const filter = `(${clauses})`;
511
493
  agentCount = await table.countRows(filter);
512
494
  }
513
495
 
@@ -532,28 +514,26 @@ export class RAGOperations {
532
514
  ): Promise<Array<{ name: string; agentDocCount: number; totalDocCount: number }>> {
533
515
  const collections = await this.listCollections();
534
516
 
535
- const stats = await Promise.all(
517
+ const results = await Promise.allSettled(
536
518
  collections.map(async (name) => {
537
- try {
538
- const { totalCount, agentCount } = await this.getCollectionStats(name, agentPubkey);
539
- return {
540
- name,
541
- agentDocCount: agentCount ?? 0,
542
- totalDocCount: totalCount,
543
- };
544
- } catch (error) {
545
- // Log but don't fail - return zero counts for problematic collections
546
- logger.warn(`Failed to get stats for collection '${name}':`, error);
547
- return {
548
- name,
549
- agentDocCount: 0,
550
- totalDocCount: 0,
551
- };
552
- }
519
+ const { totalCount, agentCount } = await this.getCollectionStats(name, agentPubkey);
520
+ return {
521
+ name,
522
+ agentDocCount: agentCount ?? 0,
523
+ totalDocCount: totalCount,
524
+ };
553
525
  })
554
526
  );
555
527
 
556
- return stats;
528
+ return results
529
+ .filter((r): r is PromiseFulfilledResult<{ name: string; agentDocCount: number; totalDocCount: number }> => {
530
+ if (r.status === "rejected") {
531
+ logger.warn(`Failed to get stats for collection: ${r.reason}`);
532
+ return false;
533
+ }
534
+ return true;
535
+ })
536
+ .map(r => r.value);
557
537
  }
558
538
 
559
539
  /**
@@ -5,36 +5,6 @@ import { RAGDatabaseService } from "./RAGDatabaseService";
5
5
  import { RAGOperations } from "./RAGOperations";
6
6
  import type { BulkUpsertResult, LanceDBSchema, RAGCollection, RAGDocument, RAGQueryResult } from "./RAGOperations";
7
7
 
8
- /**
9
- * Lightweight check for RAG collections without full service initialization.
10
- * Returns true if any RAG collections exist in the database.
11
- *
12
- * This is designed for use in system prompt building where we want to avoid
13
- * initializing the embedding provider (which can be expensive) if RAG isn't used.
14
- */
15
- export async function hasRagCollections(): Promise<boolean> {
16
- let tempDbService: RAGDatabaseService | null = null;
17
- try {
18
- // Use a temporary DB service just for listing - avoids embedding provider init
19
- tempDbService = new RAGDatabaseService();
20
- const tables = await tempDbService.listTables();
21
- return tables.length > 0;
22
- } catch (error) {
23
- // Database doesn't exist or can't connect - no collections
24
- logger.debug("RAG database not available for collection check:", error);
25
- return false;
26
- } finally {
27
- // Ensure cleanup even on listTables() errors; log but don't override result
28
- if (tempDbService) {
29
- try {
30
- await tempDbService.close();
31
- } catch (closeError) {
32
- logger.debug("Failed to close temporary RAG database service:", closeError);
33
- }
34
- }
35
- }
36
- }
37
-
38
8
  /**
39
9
  * Facade for RAG functionality
40
10
  * Coordinates between database management and operations
@@ -237,4 +207,3 @@ export class RAGService {
237
207
  export type { BulkUpsertResult, RAGDocument, RAGCollection, RAGQueryResult } from "./RAGOperations";
238
208
  export { RAGValidationError, RAGOperationError } from "./RAGOperations";
239
209
  export { RAGDatabaseError } from "./RAGDatabaseService";
240
- // hasRagCollections is exported at module level (above class)
@@ -197,72 +197,42 @@ export class RagSubscriptionService {
197
197
  });
198
198
  };
199
199
 
200
+ // Validate that resourceUri is a proper URI format
200
201
  try {
201
- // Validate that resourceUri is a proper URI format
202
- try {
203
- new URL(subscription.resourceUri);
204
- } catch {
205
- throw new Error(
206
- `Invalid resourceUri: "${subscription.resourceUri}". Resource URI must be a valid URI format (e.g., "nostr://feed/pubkey/kinds", "file:///path/to/file"). This appears to be a tool name or invalid format. If you're using a resource template, you must first expand it with parameters to get the actual URI.`
207
- );
208
- }
202
+ new URL(subscription.resourceUri);
203
+ } catch {
204
+ throw new Error(
205
+ `Invalid resourceUri: "${subscription.resourceUri}". Resource URI must be a valid URI format (e.g., "nostr://feed/pubkey/kinds", "file:///path/to/file"). This appears to be a tool name or invalid format. If you're using a resource template, you must first expand it with parameters to get the actual URI.`
206
+ );
207
+ }
209
208
 
210
- // Get mcpManager from project context
211
- const projectCtx = getProjectContext();
212
- const mcpManager = projectCtx.mcpManager;
213
- if (!mcpManager) {
214
- throw new Error("MCPManager not available in project context");
215
- }
209
+ // Get mcpManager from project context
210
+ const projectCtx = getProjectContext();
211
+ const mcpManager = projectCtx.mcpManager;
212
+ if (!mcpManager) {
213
+ throw new Error("MCPManager not available in project context");
214
+ }
216
215
 
217
- // Try to subscribe to resource updates
218
- let subscriptionSupported = true;
219
- try {
220
- // CRITICAL: Register handler FIRST, then subscribe
221
- const removeHandler = mcpManager.addResourceNotificationHandler(subscription.mcpServerId, listener);
216
+ // CRITICAL: Register handler FIRST, then subscribe
217
+ const removeHandler = mcpManager.addResourceNotificationHandler(subscription.mcpServerId, listener);
222
218
 
223
- // Subscribe to resource updates
224
- await mcpManager.subscribeToResource(
225
- subscription.mcpServerId,
226
- subscription.resourceUri
227
- );
228
-
229
- // Store listener reference and removal function for cleanup
230
- this.resourceListeners.set(
231
- listenerKey,
232
- listener as unknown as (notification: Notification) => void
233
- );
234
- this.handlerRemovers.set(listenerKey, removeHandler);
219
+ // Subscribe to resource updates — if server doesn't support subscriptions, let it throw
220
+ await mcpManager.subscribeToResource(
221
+ subscription.mcpServerId,
222
+ subscription.resourceUri
223
+ );
235
224
 
236
- logger.info(
237
- `RAG subscription '${subscription.subscriptionId}' active with push notifications. ` +
238
- `Listening for updates from ${subscription.mcpServerId}:${subscription.resourceUri}`
239
- );
240
- } catch (error) {
241
- if (
242
- error instanceof Error &&
243
- error.message.includes("does not support resource subscriptions")
244
- ) {
245
- // Server doesn't support subscriptions, gracefully degrade to polling
246
- subscriptionSupported = false;
247
- logger.warn(
248
- `Server '${subscription.mcpServerId}' does not support resource subscriptions. ` +
249
- `Subscription '${subscription.subscriptionId}' will use polling mode. ` +
250
- `Call pollResource() manually or set up a polling interval.`
251
- );
252
- // Don't exit - gracefully degrade to polling mode
253
- } else {
254
- throw error;
255
- }
256
- }
225
+ // Store listener reference and removal function for cleanup
226
+ this.resourceListeners.set(
227
+ listenerKey,
228
+ listener as unknown as (notification: Notification) => void
229
+ );
230
+ this.handlerRemovers.set(listenerKey, removeHandler);
257
231
 
258
- if (!subscriptionSupported) {
259
- subscription.lastError = "Server does not support subscriptions - use polling mode";
260
- }
261
- } catch (error) {
262
- subscription.status = SubscriptionStatus.ERROR;
263
- subscription.lastError = error instanceof Error ? error.message : "Unknown error";
264
- throw error;
265
- }
232
+ logger.info(
233
+ `RAG subscription '${subscription.subscriptionId}' active with push notifications. ` +
234
+ `Listening for updates from ${subscription.mcpServerId}:${subscription.resourceUri}`
235
+ );
266
236
  }
267
237
 
268
238
  /**
@@ -272,75 +242,54 @@ export class RagSubscriptionService {
272
242
  subscription: RagSubscription,
273
243
  notification: Notification
274
244
  ): Promise<void> {
275
- try {
276
- // Extract content from notification
277
- const content = this.extractContentFromNotification(notification);
245
+ const content = this.extractContentFromNotification(notification);
278
246
 
279
- if (!content) {
280
- logger.warn(
281
- `Received empty update for subscription '${subscription.subscriptionId}'`
282
- );
283
- return;
284
- }
247
+ if (!content) {
248
+ throw new Error(
249
+ `Received empty update for subscription '${subscription.subscriptionId}'`
250
+ );
251
+ }
285
252
 
286
- // Build metadata with provenance fields for filtering at query time
287
- const metadata: DocumentMetadata = {
288
- // Auto-inject provenance for filtering at query time
289
- agent_pubkey: subscription.agentPubkey,
290
- // Subscription-specific metadata
291
- subscriptionId: subscription.subscriptionId,
292
- mcpServerId: subscription.mcpServerId,
293
- resourceUri: subscription.resourceUri,
294
- timestamp: Date.now(),
295
- };
296
-
297
- // Add project_id if available from project context (uses NIP-33 address format)
298
- if (isProjectContextInitialized()) {
299
- try {
300
- const projectCtx = getProjectContext();
301
- const projectId = projectCtx.project.tagId();
302
- if (projectId) {
303
- metadata.project_id = projectId;
304
- }
305
- } catch (error) {
306
- logger.debug("Project context error during RAG subscription update", { error });
307
- }
253
+ // Build metadata with provenance fields for filtering at query time
254
+ const metadata: DocumentMetadata = {
255
+ agent_pubkey: subscription.agentPubkey,
256
+ subscriptionId: subscription.subscriptionId,
257
+ mcpServerId: subscription.mcpServerId,
258
+ resourceUri: subscription.resourceUri,
259
+ timestamp: Date.now(),
260
+ };
261
+
262
+ // Add project_id if available from project context (uses NIP-33 address format)
263
+ if (isProjectContextInitialized()) {
264
+ const projectCtx = getProjectContext();
265
+ const projectId = projectCtx.project.tagId();
266
+ if (projectId) {
267
+ metadata.project_id = projectId;
308
268
  }
269
+ }
309
270
 
310
- // Add document to RAG collection
311
- await this.ragService.addDocuments(subscription.ragCollection, [
312
- {
313
- content,
314
- metadata,
315
- source: `${subscription.mcpServerId}:${subscription.resourceUri}`,
316
- timestamp: Date.now(),
317
- },
318
- ]);
319
-
320
- // Update subscription metrics
321
- subscription.documentsProcessed++;
322
- subscription.lastDocumentIngested = content.substring(0, 200); // Store snippet
323
- subscription.updatedAt = Date.now();
324
- subscription.status = SubscriptionStatus.RUNNING;
325
- subscription.lastError = undefined;
271
+ // Add document to RAG collection
272
+ await this.ragService.addDocuments(subscription.ragCollection, [
273
+ {
274
+ content,
275
+ metadata,
276
+ source: `${subscription.mcpServerId}:${subscription.resourceUri}`,
277
+ timestamp: Date.now(),
278
+ },
279
+ ]);
326
280
 
327
- await this.saveSubscriptions();
281
+ // Update subscription metrics
282
+ subscription.documentsProcessed++;
283
+ subscription.lastDocumentIngested = content.substring(0, 200);
284
+ subscription.updatedAt = Date.now();
285
+ subscription.status = SubscriptionStatus.RUNNING;
286
+ subscription.lastError = undefined;
328
287
 
329
- logger.debug(
330
- `Processed update for subscription '${subscription.subscriptionId}', total documents: ${subscription.documentsProcessed}`
331
- );
332
- } catch (error) {
333
- subscription.status = SubscriptionStatus.ERROR;
334
- subscription.lastError = error instanceof Error ? error.message : "Unknown error";
335
- subscription.updatedAt = Date.now();
336
- await this.saveSubscriptions();
288
+ await this.saveSubscriptions();
337
289
 
338
- handleError(
339
- error,
340
- `Failed to process update for subscription '${subscription.subscriptionId}'`,
341
- { logLevel: "error" }
342
- );
343
- }
290
+ logger.debug(
291
+ `Processed update for subscription '${subscription.subscriptionId}', total documents: ${subscription.documentsProcessed}`
292
+ );
344
293
  }
345
294
 
346
295
  /**
@@ -1,4 +1,17 @@
1
+ import { config } from "@/services/ConfigService";
1
2
  import { logger } from "@/utils/logger";
3
+ import * as path from "node:path";
4
+
5
+ /**
6
+ * Resolve the LanceDB data directory path.
7
+ * Centralizes env/config fallback logic used by multiple RAG services.
8
+ */
9
+ export function getLanceDBDataDir(): string {
10
+ return (
11
+ process.env.LANCEDB_DATA_DIR ||
12
+ path.join(config.getConfigPath("data"), "lancedb")
13
+ );
14
+ }
2
15
 
3
16
  /**
4
17
  * JSON-serializable primitive value.