@tenex-chat/backend 0.9.4 → 0.9.6

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 +47 -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 -46778
  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 -215
  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
@@ -1,15 +1,28 @@
1
1
  /**
2
2
  * Centralized SQL project filter for RAG queries.
3
3
  *
4
- * Shared utility for the `metadata LIKE '%"projectId":"..."'` SQL prefilter pattern
5
- * used across all project-scoped RAG collections (reports, conversations, lessons).
4
+ * Shared utility for project-scoped metadata filtering across all RAG collections
5
+ * (reports, conversations, lessons, and generic collections).
6
6
  *
7
7
  * Applied DURING vector search (prefilter) to ensure proper project isolation.
8
+ *
9
+ * Matches both "projectId" (canonical, used by specialized services) and
10
+ * "project_id" (legacy, used by older rag_add_documents ingestion).
8
11
  */
9
12
 
13
+ import { PROJECT_ID_KEYS } from "@/utils/metadataKeys";
14
+ import { SQL_LIKE_ESCAPE_CLAUSE, escapeSqlLikeValue } from "@/utils/sqlEscaping";
15
+
10
16
  /**
11
17
  * Build a SQL prefilter string for project isolation in LanceDB queries.
12
18
  *
19
+ * Matches documents where metadata contains EITHER:
20
+ * - "projectId":"<id>" (canonical camelCase, used by specialized services)
21
+ * - "project_id":"<id>" (legacy snake_case, used by older rag_add_documents)
22
+ *
23
+ * Uses proper SQL LIKE escaping so that project IDs containing wildcards
24
+ * (%, _) or quotes don't broaden or break the filter.
25
+ *
13
26
  * @param projectId - The project ID to filter by. Pass 'ALL' or undefined to skip filtering.
14
27
  * @returns SQL filter string or undefined if no filtering needed.
15
28
  */
@@ -17,6 +30,9 @@ export function buildProjectFilter(projectId?: string): string | undefined {
17
30
  if (!projectId || projectId.toLowerCase() === "all") {
18
31
  return undefined;
19
32
  }
20
- const escapedProjectId = projectId.replace(/'/g, "''");
21
- return `metadata LIKE '%"projectId":"${escapedProjectId}"%'`;
33
+ const escaped = escapeSqlLikeValue(projectId);
34
+ const clauses = PROJECT_ID_KEYS
35
+ .map((key) => `metadata LIKE '%"${key}":"${escaped}"%' ${SQL_LIKE_ESCAPE_CLAUSE}`)
36
+ .join(" OR ");
37
+ return `(${clauses})`;
22
38
  }
@@ -10,6 +10,7 @@ import type { SearchProvider, SearchResult } from "../types";
10
10
 
11
11
  export class ConversationSearchProvider implements SearchProvider {
12
12
  readonly name = "conversations";
13
+ readonly collectionName = "conversation_embeddings";
13
14
  readonly description = "Past conversation threads and discussions";
14
15
 
15
16
  async search(
@@ -0,0 +1,81 @@
1
+ /**
2
+ * GenericCollectionSearchProvider - Search provider for any RAG collection.
3
+ *
4
+ * Created dynamically for RAG collections that don't have a dedicated
5
+ * specialized provider. Queries via RAGService with basic project-scoped
6
+ * filtering.
7
+ */
8
+
9
+ import { logger } from "@/utils/logger";
10
+ import { RAGService, type RAGQueryResult } from "@/services/rag/RAGService";
11
+ import { buildProjectFilter } from "../projectFilter";
12
+ import type { SearchProvider, SearchResult } from "../types";
13
+
14
+ export class GenericCollectionSearchProvider implements SearchProvider {
15
+ readonly name: string;
16
+ readonly description: string;
17
+
18
+ /** The actual RAG collection name to query */
19
+ readonly collectionName: string;
20
+
21
+ constructor(collectionName: string) {
22
+ this.collectionName = collectionName;
23
+ this.name = collectionName;
24
+ this.description = `RAG collection: ${collectionName}`;
25
+ }
26
+
27
+ async search(
28
+ query: string,
29
+ projectId: string,
30
+ limit: number,
31
+ minScore: number
32
+ ): Promise<SearchResult[]> {
33
+ const ragService = RAGService.getInstance();
34
+
35
+ const filter = buildProjectFilter(projectId);
36
+
37
+ const results = await ragService.queryWithFilter(
38
+ this.collectionName,
39
+ query,
40
+ limit * 2, // Request more to account for minScore filtering
41
+ filter
42
+ );
43
+
44
+ logger.debug(`[GenericSearchProvider:${this.collectionName}] Search complete`, {
45
+ query,
46
+ projectId,
47
+ rawResults: results.length,
48
+ });
49
+
50
+ const filtered = results
51
+ .filter((result: RAGQueryResult) => result.score >= minScore && !!result.document.id)
52
+ .slice(0, limit)
53
+ .map((result: RAGQueryResult) => this.transformResult(result, projectId));
54
+
55
+ if (filtered.length < results.length) {
56
+ const dropped = results.length - filtered.length;
57
+ logger.debug(`[GenericSearchProvider:${this.collectionName}] Dropped ${dropped} result(s) (low score or missing ID)`);
58
+ }
59
+
60
+ return filtered;
61
+ }
62
+
63
+ private transformResult(result: RAGQueryResult, fallbackProjectId: string): SearchResult {
64
+ const metadata = result.document.metadata || {};
65
+
66
+ return {
67
+ source: this.collectionName,
68
+ id: result.document.id || "",
69
+ projectId: String(metadata.projectId || fallbackProjectId),
70
+ relevanceScore: result.score,
71
+ title: String(metadata.title || result.document.id || ""),
72
+ summary: result.document.content?.substring(0, 200) || "",
73
+ createdAt: metadata.timestamp ? Number(metadata.timestamp) : undefined,
74
+ author: metadata.agentPubkey ? String(metadata.agentPubkey) : undefined,
75
+ authorName: metadata.agentName ? String(metadata.agentName) : undefined,
76
+ tags: Array.isArray(metadata.hashtags) ? (metadata.hashtags as string[]) : undefined,
77
+ retrievalTool: "rag_search" as const,
78
+ retrievalArg: result.document.id || "",
79
+ };
80
+ }
81
+ }
@@ -16,6 +16,7 @@ const LESSONS_COLLECTION = "lessons";
16
16
 
17
17
  export class LessonSearchProvider implements SearchProvider {
18
18
  readonly name = "lessons";
19
+ readonly collectionName = "lessons";
19
20
  readonly description = "Agent lessons and insights";
20
21
 
21
22
  async search(
@@ -25,14 +26,6 @@ export class LessonSearchProvider implements SearchProvider {
25
26
  minScore: number
26
27
  ): Promise<SearchResult[]> {
27
28
  const ragService = RAGService.getInstance();
28
-
29
- // Check if the lessons collection exists
30
- const collections = await ragService.listCollections();
31
- if (!collections.includes(LESSONS_COLLECTION)) {
32
- logger.debug("[LessonSearchProvider] Lessons collection does not exist");
33
- return [];
34
- }
35
-
36
29
  const filter = buildProjectFilter(projectId);
37
30
 
38
31
  const results = await ragService.queryWithFilter(
@@ -10,6 +10,7 @@ import type { SearchProvider, SearchResult } from "../types";
10
10
 
11
11
  export class ReportSearchProvider implements SearchProvider {
12
12
  readonly name = "reports";
13
+ readonly collectionName = "project_reports";
13
14
  readonly description = "Project reports and documentation";
14
15
 
15
16
  async search(
@@ -51,7 +51,7 @@ export interface SearchResult {
51
51
  tags?: string[];
52
52
 
53
53
  /** Which tool to use to retrieve full content */
54
- retrievalTool: "report_read" | "lesson_get" | "conversation_get";
54
+ retrievalTool: "report_read" | "lesson_get" | "conversation_get" | "rag_search";
55
55
 
56
56
  /** The argument to pass to the retrieval tool */
57
57
  retrievalArg: string;
@@ -79,8 +79,21 @@ export interface SearchOptions {
79
79
  */
80
80
  prompt?: string;
81
81
 
82
- /** Which collections to search (defaults to all) */
82
+ /**
83
+ * Filter by **provider name** (defaults to all).
84
+ * Well-known provider names: "reports", "conversations", "lessons".
85
+ * Dynamically discovered RAG collections use their collection name as provider name
86
+ * (e.g., "custom_knowledge").
87
+ *
88
+ * When provided, searches exactly those collections (no scope filtering).
89
+ */
83
90
  collections?: string[];
91
+
92
+ /**
93
+ * Agent pubkey for scope-aware collection filtering.
94
+ * Used to include `personal` collections belonging to this agent.
95
+ */
96
+ agentPubkey?: string;
84
97
  }
85
98
 
86
99
  /**
@@ -88,12 +101,20 @@ export interface SearchOptions {
88
101
  * Each provider wraps a specific RAG collection.
89
102
  */
90
103
  export interface SearchProvider {
91
- /** Unique name for this provider (matches collection concept) */
104
+ /** Unique name for this provider (used for filtering via `collections` parameter) */
92
105
  readonly name: string;
93
106
 
94
107
  /** Human-readable description */
95
108
  readonly description: string;
96
109
 
110
+ /**
111
+ * The underlying RAG collection name this provider covers.
112
+ * Used to prevent duplicate generic providers for collections that already
113
+ * have a specialized provider. When undefined, the provider is not
114
+ * associated with a specific RAG collection (or uses its `name` directly).
115
+ */
116
+ readonly collectionName?: string;
117
+
97
118
  /**
98
119
  * Perform semantic search within this provider's collection.
99
120
  *
@@ -0,0 +1,148 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { agentStorage } from "@/agents/AgentStorage";
4
+ import { ensureDirectory, readFile } from "@/lib/fs";
5
+ import { config } from "@/services/ConfigService";
6
+ import { logger } from "@/utils/logger";
7
+
8
+ export interface SyncSystemPubkeyListOptions {
9
+ /**
10
+ * Extra pubkeys to force-include in the output file.
11
+ * Useful for "about to publish" contexts where the pubkey must be present
12
+ * even if other registries haven't synced yet.
13
+ */
14
+ additionalPubkeys?: Iterable<string>;
15
+ }
16
+
17
+ /**
18
+ * Maintains `$TENEX_BASE_DIR/daemon/whitelist.txt` as the canonical list of
19
+ * pubkeys known to belong to this TENEX system.
20
+ *
21
+ * The file contains one pubkey per line and is rebuilt from:
22
+ * - daemon/user whitelist from config
23
+ * - backend pubkey
24
+ * - all known agent pubkeys from storage
25
+ * - optional call-site additions (e.g., the pubkey being published right now)
26
+ */
27
+ export class SystemPubkeyListService {
28
+ private static instance: SystemPubkeyListService;
29
+
30
+ static getInstance(): SystemPubkeyListService {
31
+ if (!SystemPubkeyListService.instance) {
32
+ SystemPubkeyListService.instance = new SystemPubkeyListService();
33
+ }
34
+ return SystemPubkeyListService.instance;
35
+ }
36
+
37
+ /**
38
+ * Rebuild and persist daemon whitelist file.
39
+ * Idempotent: skips writes when content is unchanged.
40
+ */
41
+ async syncWhitelistFile(options: SyncSystemPubkeyListOptions = {}): Promise<void> {
42
+ const daemonDir = config.getConfigPath("daemon");
43
+ const whitelistPath = path.join(daemonDir, "whitelist.txt");
44
+ const pubkeys = await this.collectKnownSystemPubkeys(options.additionalPubkeys);
45
+ const content = this.serialize(pubkeys);
46
+
47
+ await ensureDirectory(daemonDir);
48
+
49
+ const existing = await this.safeReadFile(whitelistPath);
50
+ if (existing === content) {
51
+ return;
52
+ }
53
+
54
+ await this.atomicWrite(whitelistPath, content);
55
+ logger.debug("[SYSTEM_PUBKEY_LIST] Updated daemon whitelist.txt", {
56
+ path: whitelistPath,
57
+ pubkeyCount: pubkeys.length,
58
+ });
59
+ }
60
+
61
+ private async collectKnownSystemPubkeys(additionalPubkeys?: Iterable<string>): Promise<string[]> {
62
+ const pubkeys = new Set<string>();
63
+
64
+ // Whitelisted daemon pubkeys from loaded config
65
+ try {
66
+ const loadedConfig = config.getConfig();
67
+ const whitelisted = config.getWhitelistedPubkeys(undefined, loadedConfig);
68
+ for (const pubkey of whitelisted) {
69
+ this.addPubkey(pubkeys, pubkey);
70
+ }
71
+ } catch (error) {
72
+ logger.debug("[SYSTEM_PUBKEY_LIST] Failed to load whitelisted pubkeys", {
73
+ error: error instanceof Error ? error.message : String(error),
74
+ });
75
+ }
76
+
77
+ // TENEX backend pubkey
78
+ try {
79
+ const backendSigner = await config.getBackendSigner();
80
+ this.addPubkey(pubkeys, backendSigner.pubkey);
81
+ } catch (error) {
82
+ logger.debug("[SYSTEM_PUBKEY_LIST] Failed to load backend pubkey", {
83
+ error: error instanceof Error ? error.message : String(error),
84
+ });
85
+ }
86
+
87
+ // All known agents (across projects) from storage
88
+ try {
89
+ const knownAgentPubkeys = await agentStorage.getAllKnownPubkeys();
90
+ for (const pubkey of knownAgentPubkeys) {
91
+ this.addPubkey(pubkeys, pubkey);
92
+ }
93
+ } catch (error) {
94
+ logger.debug("[SYSTEM_PUBKEY_LIST] Failed to load known agent pubkeys", {
95
+ error: error instanceof Error ? error.message : String(error),
96
+ });
97
+ }
98
+
99
+ // Call-site additions (e.g., pubkey being published right now)
100
+ if (additionalPubkeys) {
101
+ for (const pubkey of additionalPubkeys) {
102
+ this.addPubkey(pubkeys, pubkey);
103
+ }
104
+ }
105
+
106
+ return Array.from(pubkeys).sort();
107
+ }
108
+
109
+ private addPubkey(pubkeys: Set<string>, candidate: string | undefined): void {
110
+ if (!candidate) {
111
+ return;
112
+ }
113
+
114
+ const trimmed = candidate.trim();
115
+ if (!trimmed) {
116
+ return;
117
+ }
118
+
119
+ pubkeys.add(trimmed);
120
+ }
121
+
122
+ private serialize(pubkeys: string[]): string {
123
+ if (pubkeys.length === 0) {
124
+ return "";
125
+ }
126
+ return `${pubkeys.join("\n")}\n`;
127
+ }
128
+
129
+ private async safeReadFile(filePath: string): Promise<string | null> {
130
+ try {
131
+ return await readFile(filePath, "utf-8");
132
+ } catch (error) {
133
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
134
+ return null;
135
+ }
136
+ throw error;
137
+ }
138
+ }
139
+
140
+ private async atomicWrite(filePath: string, content: string): Promise<void> {
141
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
142
+ await fs.writeFile(tempPath, content, "utf-8");
143
+ await fs.rename(tempPath, filePath);
144
+ }
145
+ }
146
+
147
+ export const getSystemPubkeyListService = (): SystemPubkeyListService =>
148
+ SystemPubkeyListService.getInstance();
@@ -24,9 +24,17 @@ export interface TrustResult {
24
24
  * A pubkey is trusted if it is:
25
25
  * - In the whitelisted pubkeys from config
26
26
  * - The backend's own pubkey
27
- * - An agent in the system (registered in ProjectContext)
27
+ * - An agent in the system (registered in ProjectContext or globally across all projects)
28
28
  *
29
29
  * Trust precedence (highest to lowest): whitelisted > backend > agent
30
+ *
31
+ * ## Agent Trust: Two-Tier Lookup + Daemon Seeding
32
+ * 1. **Project context** (sync): Check the current project's agent registry (fast, scoped)
33
+ * 2. **Global agent set** (sync): Check daemon-level set of all agent pubkeys across all projects
34
+ *
35
+ * The global agent set is seeded by the Daemon at startup from AgentStorage (covering
36
+ * not-yet-running projects) and kept in sync as projects start/stop. Each sync unions
37
+ * the active runtime pubkeys with the stored seed, so trust is never dropped.
30
38
  */
31
39
  export class TrustPubkeyService {
32
40
  private static instance: TrustPubkeyService;
@@ -37,6 +45,13 @@ export class TrustPubkeyService {
37
45
  /** Cached whitelist Set for O(1) lookups */
38
46
  private cachedWhitelistSet?: Set<Hexpubkey>;
39
47
 
48
+ /**
49
+ * Global set of agent pubkeys across ALL projects (running and discovered).
50
+ * Pushed by the Daemon whenever projects start/stop or agents are added.
51
+ * Frozen for safe concurrent reads.
52
+ */
53
+ private globalAgentPubkeys: ReadonlySet<Hexpubkey> = Object.freeze(new Set<Hexpubkey>());
54
+
40
55
  private constructor() {}
41
56
 
42
57
  /**
@@ -165,6 +180,20 @@ export class TrustPubkeyService {
165
180
  // Note: getBackendPubkey already logs debug message on failure
166
181
  }
167
182
 
183
+ /**
184
+ * Set the global agent pubkeys set (daemon-level, cross-project).
185
+ * Called by the Daemon when projects start/stop or agents are dynamically added.
186
+ * The set is frozen for safe concurrent reads.
187
+ *
188
+ * @param pubkeys Set of all known agent pubkeys across all projects
189
+ */
190
+ setGlobalAgentPubkeys(pubkeys: Set<Hexpubkey>): void {
191
+ this.globalAgentPubkeys = Object.freeze(new Set(pubkeys));
192
+ logger.debug("[TRUST_PUBKEY] Global agent pubkeys updated", {
193
+ count: pubkeys.size,
194
+ });
195
+ }
196
+
168
197
  /**
169
198
  * Get all currently trusted pubkeys.
170
199
  * Useful for debugging or displaying trust status.
@@ -196,6 +225,7 @@ export class TrustPubkeyService {
196
225
  }
197
226
 
198
227
  // 3. Add agent pubkeys (lowest priority)
228
+ // 3a. Current project context agents
199
229
  const projectCtx = projectContextStore.getContext();
200
230
  if (projectCtx) {
201
231
  for (const [_slug, agent] of projectCtx.agents) {
@@ -205,6 +235,13 @@ export class TrustPubkeyService {
205
235
  }
206
236
  }
207
237
 
238
+ // 3b. Global agent pubkeys (cross-project)
239
+ for (const pubkey of this.globalAgentPubkeys) {
240
+ if (!trustedMap.has(pubkey)) {
241
+ trustedMap.set(pubkey, "agent");
242
+ }
243
+ }
244
+
208
245
  // Convert map to array
209
246
  return Array.from(trustedMap.entries()).map(([pubkey, reason]) => ({
210
247
  pubkey,
@@ -296,25 +333,49 @@ export class TrustPubkeyService {
296
333
  }
297
334
 
298
335
  /**
299
- * Check if pubkey belongs to an agent in the system.
300
- * Returns false if no project context is available (e.g., during daemon startup
301
- * or when called outside of projectContextStore.run()).
336
+ * Check if pubkey belongs to an agent in the system (synchronous, two-tier).
337
+ *
338
+ * Tier 1: Current project context (fast, scoped to the active project)
339
+ * Tier 2: Global agent pubkeys set (daemon-level, covers all projects including non-running)
340
+ *
341
+ * The global set is maintained by the Daemon via setGlobalAgentPubkeys(),
342
+ * which unions active runtime pubkeys with the AgentStorage seed.
302
343
  */
303
344
  private isAgentPubkey(pubkey: Hexpubkey): boolean {
345
+ // Tier 1: Current project context (existing fast path)
304
346
  const projectCtx = projectContextStore.getContext();
305
- if (!projectCtx) {
306
- return false;
347
+ if (projectCtx?.getAgentByPubkey(pubkey) !== undefined) {
348
+ return true;
307
349
  }
308
- return projectCtx.getAgentByPubkey(pubkey) !== undefined;
350
+
351
+ // Tier 2: Global agent pubkeys (daemon-level, cross-project)
352
+ if (this.globalAgentPubkeys.has(pubkey)) {
353
+ return true;
354
+ }
355
+
356
+ return false;
309
357
  }
310
358
 
311
359
  /**
312
- * Clear all caches (useful for testing and config reloads)
360
+ * Clear config-derived caches (useful for testing and config reloads).
361
+ * Does NOT clear globalAgentPubkeys — that state is managed by the Daemon
362
+ * via setGlobalAgentPubkeys() and is not derived from config.
313
363
  */
314
364
  clearCache(): void {
315
365
  this.cachedBackendPubkey = undefined;
316
366
  this.cachedWhitelistSet = undefined;
317
- logger.debug("[TRUST_PUBKEY] Cache cleared");
367
+ logger.debug("[TRUST_PUBKEY] Config cache cleared");
368
+ }
369
+
370
+ /**
371
+ * Reset all state including daemon-managed global agent pubkeys.
372
+ * Use only in tests to fully reset the service.
373
+ */
374
+ resetAll(): void {
375
+ this.cachedBackendPubkey = undefined;
376
+ this.cachedWhitelistSet = undefined;
377
+ this.globalAgentPubkeys = Object.freeze(new Set<Hexpubkey>());
378
+ logger.debug("[TRUST_PUBKEY] Full state reset");
318
379
  }
319
380
  }
320
381
 
@@ -17,10 +17,7 @@ const DEFAULT_ENDPOINT = "http://localhost:4318/v1/traces";
17
17
  class ErrorHandlingExporterWrapper implements SpanExporter {
18
18
  private disabled = false;
19
19
 
20
- constructor(
21
- private traceExporter: OTLPTraceExporter,
22
- private exporterUrl: string
23
- ) {}
20
+ constructor(private traceExporter: OTLPTraceExporter) {}
24
21
 
25
22
  export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
26
23
  // Once disabled, drop all spans silently
@@ -31,14 +28,6 @@ class ErrorHandlingExporterWrapper implements SpanExporter {
31
28
 
32
29
  this.traceExporter.export(spans, (result) => {
33
30
  if (result.error && !this.disabled) {
34
- const errorMessage = result.error?.message || String(result.error);
35
- const isConnectionError = errorMessage.includes("ECONNREFUSED") || errorMessage.includes("connect");
36
- if (isConnectionError) {
37
- console.warn(`[Telemetry] ⚠️ Collector not available at ${this.exporterUrl}`);
38
- } else {
39
- console.error("[Telemetry] Export error:", errorMessage);
40
- }
41
- console.warn("[Telemetry] Disabling trace export");
42
31
  this.disabled = true;
43
32
  }
44
33
  resultCallback(result);
@@ -106,7 +95,7 @@ export function initializeTelemetry(
106
95
  });
107
96
 
108
97
  // Wrap the exporter with error handling
109
- const wrappedExporter = new ErrorHandlingExporterWrapper(traceExporter, exporterUrl);
98
+ const wrappedExporter = new ErrorHandlingExporterWrapper(traceExporter);
110
99
 
111
100
  sdk = createSDK(serviceName, wrappedExporter);
112
101
  sdk.start();
@@ -12,7 +12,6 @@ import { z } from "zod";
12
12
  import { RALRegistry } from "@/services/ral";
13
13
  import type { PendingDelegation } from "@/services/ral/types";
14
14
  import { APNsService } from "@/services/apns";
15
- import { streamPublisher } from "@/llm";
16
15
 
17
16
  /**
18
17
  * Schema for a single-select question.
@@ -303,10 +302,11 @@ async function executeAsk(input: AskInput, context: ToolExecutionContext): Promi
303
302
  });
304
303
  }
305
304
 
306
- // Send APNs push notification if user is not connected
305
+ // Send APNs push notification if APNs is enabled.
306
+ // Unix socket presence detection was removed with local socket streaming.
307
307
  try {
308
308
  const apnsService = APNsService.getInstance();
309
- if (apnsService.isEnabled() && !streamPublisher.isConnected()) {
309
+ if (apnsService.isEnabled()) {
310
310
  const bodyPreview = askContext.length > 100 ? askContext.substring(0, 100) + "…" : askContext;
311
311
  await apnsService.notifyIfNeeded(ownerPubkey, {
312
312
  title: "Agent needs your input",