verybot 0.1.8

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 (277) hide show
  1. package/README.md +167 -0
  2. package/dist/aliases/store.d.ts +21 -0
  3. package/dist/aliases/store.js +148 -0
  4. package/dist/aliases/types.d.ts +6 -0
  5. package/dist/aliases/types.js +1 -0
  6. package/dist/brain/agent-registry.d.ts +96 -0
  7. package/dist/brain/agent-registry.js +141 -0
  8. package/dist/brain/agent.d.ts +167 -0
  9. package/dist/brain/agent.js +932 -0
  10. package/dist/brain/channel-store.d.ts +27 -0
  11. package/dist/brain/channel-store.js +78 -0
  12. package/dist/brain/compaction.d.ts +37 -0
  13. package/dist/brain/compaction.js +214 -0
  14. package/dist/brain/context.d.ts +43 -0
  15. package/dist/brain/context.js +139 -0
  16. package/dist/brain/delegation-store.d.ts +33 -0
  17. package/dist/brain/delegation-store.js +106 -0
  18. package/dist/brain/loop.d.ts +24 -0
  19. package/dist/brain/loop.js +318 -0
  20. package/dist/brain/mcp-adapter.d.ts +43 -0
  21. package/dist/brain/mcp-adapter.js +244 -0
  22. package/dist/brain/memory-extractor.d.ts +26 -0
  23. package/dist/brain/memory-extractor.js +82 -0
  24. package/dist/brain/providers.d.ts +14 -0
  25. package/dist/brain/providers.js +85 -0
  26. package/dist/brain/queue.d.ts +18 -0
  27. package/dist/brain/queue.js +111 -0
  28. package/dist/brain/run-tools.d.ts +50 -0
  29. package/dist/brain/run-tools.js +136 -0
  30. package/dist/brain/session-key.d.ts +23 -0
  31. package/dist/brain/session-key.js +41 -0
  32. package/dist/brain/session-state.d.ts +36 -0
  33. package/dist/brain/session-state.js +51 -0
  34. package/dist/brain/session-store.d.ts +50 -0
  35. package/dist/brain/session-store.js +207 -0
  36. package/dist/brain/session.d.ts +32 -0
  37. package/dist/brain/session.js +75 -0
  38. package/dist/brain/task-subscriber.d.ts +56 -0
  39. package/dist/brain/task-subscriber.js +317 -0
  40. package/dist/brain/user-content.d.ts +16 -0
  41. package/dist/brain/user-content.js +32 -0
  42. package/dist/brain/utils.d.ts +4 -0
  43. package/dist/brain/utils.js +26 -0
  44. package/dist/brain/worker-coordinator.d.ts +25 -0
  45. package/dist/brain/worker-coordinator.js +83 -0
  46. package/dist/channels/commands.d.ts +50 -0
  47. package/dist/channels/commands.js +132 -0
  48. package/dist/channels/discord/channel.d.ts +29 -0
  49. package/dist/channels/discord/channel.js +159 -0
  50. package/dist/channels/discord/markdown.d.ts +19 -0
  51. package/dist/channels/discord/markdown.js +62 -0
  52. package/dist/channels/manager.d.ts +29 -0
  53. package/dist/channels/manager.js +100 -0
  54. package/dist/channels/slack/channel.d.ts +37 -0
  55. package/dist/channels/slack/channel.js +227 -0
  56. package/dist/channels/slack/markdown.d.ts +19 -0
  57. package/dist/channels/slack/markdown.js +62 -0
  58. package/dist/channels/specs.d.ts +32 -0
  59. package/dist/channels/specs.js +99 -0
  60. package/dist/channels/telegram/channel.d.ts +29 -0
  61. package/dist/channels/telegram/channel.js +182 -0
  62. package/dist/channels/telegram/markdown.d.ts +17 -0
  63. package/dist/channels/telegram/markdown.js +66 -0
  64. package/dist/channels/types.d.ts +26 -0
  65. package/dist/channels/types.js +1 -0
  66. package/dist/channels/whatsapp/channel.d.ts +34 -0
  67. package/dist/channels/whatsapp/channel.js +276 -0
  68. package/dist/channels/whatsapp/markdown.d.ts +20 -0
  69. package/dist/channels/whatsapp/markdown.js +51 -0
  70. package/dist/cli/claude-login.d.ts +5 -0
  71. package/dist/cli/claude-login.js +47 -0
  72. package/dist/cli/config.d.ts +5 -0
  73. package/dist/cli/config.js +78 -0
  74. package/dist/cli/index.d.ts +11 -0
  75. package/dist/cli/index.js +96 -0
  76. package/dist/computer/browser/actions.d.ts +31 -0
  77. package/dist/computer/browser/actions.js +148 -0
  78. package/dist/computer/browser/context-manager.d.ts +28 -0
  79. package/dist/computer/browser/context-manager.js +78 -0
  80. package/dist/computer/browser/manager.d.ts +91 -0
  81. package/dist/computer/browser/manager.js +344 -0
  82. package/dist/computer/browser/profile-badge.d.ts +13 -0
  83. package/dist/computer/browser/profile-badge.js +67 -0
  84. package/dist/computer/browser/screenshot.d.ts +5 -0
  85. package/dist/computer/browser/screenshot.js +21 -0
  86. package/dist/computer/browser/snapshot.d.ts +30 -0
  87. package/dist/computer/browser/snapshot.js +242 -0
  88. package/dist/computer/browser/tools.d.ts +5 -0
  89. package/dist/computer/browser/tools.js +167 -0
  90. package/dist/computer/browser/types.d.ts +26 -0
  91. package/dist/computer/browser/types.js +1 -0
  92. package/dist/computer/desktop/adapter.d.ts +25 -0
  93. package/dist/computer/desktop/adapter.js +11 -0
  94. package/dist/computer/desktop/macos.d.ts +24 -0
  95. package/dist/computer/desktop/macos.js +223 -0
  96. package/dist/computer/desktop/tools.d.ts +25 -0
  97. package/dist/computer/desktop/tools.js +114 -0
  98. package/dist/config/agent-config.d.ts +55 -0
  99. package/dist/config/agent-config.js +16 -0
  100. package/dist/config/model-catalog.d.ts +22 -0
  101. package/dist/config/model-catalog.js +112 -0
  102. package/dist/config/model-spec.d.ts +8 -0
  103. package/dist/config/model-spec.js +66 -0
  104. package/dist/config/store.d.ts +25 -0
  105. package/dist/config/store.js +143 -0
  106. package/dist/config.d.ts +110 -0
  107. package/dist/config.js +259 -0
  108. package/dist/control-ui/assets/index-Cbl7G5Sc.css +1 -0
  109. package/dist/control-ui/assets/index-Cu1P4C62.js +266 -0
  110. package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
  111. package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
  112. package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
  113. package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
  114. package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
  115. package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
  116. package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
  117. package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
  118. package/dist/control-ui/index.html +14 -0
  119. package/dist/control-ui/vite.svg +1 -0
  120. package/dist/events.d.ts +2 -0
  121. package/dist/events.js +11 -0
  122. package/dist/gateway/broadcast.d.ts +5 -0
  123. package/dist/gateway/broadcast.js +33 -0
  124. package/dist/gateway/methods/aliases.d.ts +17 -0
  125. package/dist/gateway/methods/aliases.js +22 -0
  126. package/dist/gateway/methods/chat.d.ts +33 -0
  127. package/dist/gateway/methods/chat.js +37 -0
  128. package/dist/gateway/methods/config.d.ts +14 -0
  129. package/dist/gateway/methods/config.js +24 -0
  130. package/dist/gateway/methods/models.d.ts +10 -0
  131. package/dist/gateway/methods/models.js +14 -0
  132. package/dist/gateway/methods/playbooks.d.ts +45 -0
  133. package/dist/gateway/methods/playbooks.js +488 -0
  134. package/dist/gateway/methods/prompt-templates.d.ts +27 -0
  135. package/dist/gateway/methods/prompt-templates.js +106 -0
  136. package/dist/gateway/methods/scheduler.d.ts +62 -0
  137. package/dist/gateway/methods/scheduler.js +129 -0
  138. package/dist/gateway/methods/sessions.d.ts +44 -0
  139. package/dist/gateway/methods/sessions.js +111 -0
  140. package/dist/gateway/methods/system.d.ts +12 -0
  141. package/dist/gateway/methods/system.js +39 -0
  142. package/dist/gateway/methods/tasks.d.ts +40 -0
  143. package/dist/gateway/methods/tasks.js +151 -0
  144. package/dist/gateway/methods/teams.d.ts +69 -0
  145. package/dist/gateway/methods/teams.js +376 -0
  146. package/dist/gateway/methods/tools.d.ts +6 -0
  147. package/dist/gateway/methods/tools.js +7 -0
  148. package/dist/gateway/methods/whatsapp.d.ts +19 -0
  149. package/dist/gateway/methods/whatsapp.js +35 -0
  150. package/dist/gateway/rpc.d.ts +38 -0
  151. package/dist/gateway/rpc.js +79 -0
  152. package/dist/gateway/server.d.ts +9 -0
  153. package/dist/gateway/server.js +137 -0
  154. package/dist/index.d.ts +1 -0
  155. package/dist/index.js +254 -0
  156. package/dist/integrations/github.d.ts +7 -0
  157. package/dist/integrations/github.js +133 -0
  158. package/dist/integrations/mcp.d.ts +7 -0
  159. package/dist/integrations/mcp.js +106 -0
  160. package/dist/integrations/registry.d.ts +47 -0
  161. package/dist/integrations/registry.js +332 -0
  162. package/dist/integrations/scanner.d.ts +10 -0
  163. package/dist/integrations/scanner.js +122 -0
  164. package/dist/integrations/twitter.d.ts +10 -0
  165. package/dist/integrations/twitter.js +120 -0
  166. package/dist/integrations/types.d.ts +72 -0
  167. package/dist/integrations/types.js +1 -0
  168. package/dist/logger.d.ts +16 -0
  169. package/dist/logger.js +104 -0
  170. package/dist/markdown/chunk.d.ts +9 -0
  171. package/dist/markdown/chunk.js +52 -0
  172. package/dist/markdown/ir.d.ts +37 -0
  173. package/dist/markdown/ir.js +529 -0
  174. package/dist/markdown/render.d.ts +22 -0
  175. package/dist/markdown/render.js +148 -0
  176. package/dist/markdown/table-render.d.ts +43 -0
  177. package/dist/markdown/table-render.js +219 -0
  178. package/dist/markdown/tables.d.ts +17 -0
  179. package/dist/markdown/tables.js +27 -0
  180. package/dist/memory/embedding.d.ts +16 -0
  181. package/dist/memory/embedding.js +66 -0
  182. package/dist/memory/explicit.d.ts +16 -0
  183. package/dist/memory/explicit.js +29 -0
  184. package/dist/memory/extractor.d.ts +13 -0
  185. package/dist/memory/extractor.js +82 -0
  186. package/dist/memory/search.d.ts +15 -0
  187. package/dist/memory/search.js +57 -0
  188. package/dist/memory/session-learning.d.ts +23 -0
  189. package/dist/memory/session-learning.js +55 -0
  190. package/dist/memory/store.d.ts +36 -0
  191. package/dist/memory/store.js +334 -0
  192. package/dist/memory/types.d.ts +9 -0
  193. package/dist/memory/types.js +2 -0
  194. package/dist/paths.d.ts +28 -0
  195. package/dist/paths.js +48 -0
  196. package/dist/prompt-templates/builtins/index.d.ts +4 -0
  197. package/dist/prompt-templates/builtins/index.js +5 -0
  198. package/dist/prompt-templates/builtins/planner.d.ts +4 -0
  199. package/dist/prompt-templates/builtins/planner.js +77 -0
  200. package/dist/prompt-templates/store.d.ts +45 -0
  201. package/dist/prompt-templates/store.js +224 -0
  202. package/dist/prompt-templates/types.d.ts +10 -0
  203. package/dist/prompt-templates/types.js +1 -0
  204. package/dist/scheduler/connected-channels.d.ts +24 -0
  205. package/dist/scheduler/connected-channels.js +57 -0
  206. package/dist/scheduler/scheduler.d.ts +22 -0
  207. package/dist/scheduler/scheduler.js +132 -0
  208. package/dist/scheduler/store.d.ts +27 -0
  209. package/dist/scheduler/store.js +205 -0
  210. package/dist/scheduler/types.d.ts +29 -0
  211. package/dist/scheduler/types.js +1 -0
  212. package/dist/security/command-validator.d.ts +22 -0
  213. package/dist/security/command-validator.js +160 -0
  214. package/dist/security/docker-sandbox.d.ts +48 -0
  215. package/dist/security/docker-sandbox.js +218 -0
  216. package/dist/security/env-filter.d.ts +8 -0
  217. package/dist/security/env-filter.js +41 -0
  218. package/dist/skills/loader.d.ts +33 -0
  219. package/dist/skills/loader.js +132 -0
  220. package/dist/skills/prompt.d.ts +6 -0
  221. package/dist/skills/prompt.js +17 -0
  222. package/dist/skills/read-tool.d.ts +7 -0
  223. package/dist/skills/read-tool.js +24 -0
  224. package/dist/skills/scanner.d.ts +6 -0
  225. package/dist/skills/scanner.js +73 -0
  226. package/dist/skills/types.d.ts +15 -0
  227. package/dist/skills/types.js +1 -0
  228. package/dist/tasks/inline-attachment-content.d.ts +9 -0
  229. package/dist/tasks/inline-attachment-content.js +64 -0
  230. package/dist/tasks/store.d.ts +112 -0
  231. package/dist/tasks/store.js +519 -0
  232. package/dist/tasks/types.d.ts +129 -0
  233. package/dist/tasks/types.js +80 -0
  234. package/dist/teams/status-config.d.ts +8 -0
  235. package/dist/teams/status-config.js +40 -0
  236. package/dist/teams/store.d.ts +111 -0
  237. package/dist/teams/store.js +671 -0
  238. package/dist/teams/types.d.ts +30 -0
  239. package/dist/teams/types.js +1 -0
  240. package/dist/tools/bash.d.ts +18 -0
  241. package/dist/tools/bash.js +64 -0
  242. package/dist/tools/channel-history.d.ts +10 -0
  243. package/dist/tools/channel-history.js +43 -0
  244. package/dist/tools/delegate.d.ts +20 -0
  245. package/dist/tools/delegate.js +299 -0
  246. package/dist/tools/fs.d.ts +4 -0
  247. package/dist/tools/fs.js +335 -0
  248. package/dist/tools/integration-toggle.d.ts +14 -0
  249. package/dist/tools/integration-toggle.js +47 -0
  250. package/dist/tools/memory.d.ts +13 -0
  251. package/dist/tools/memory.js +59 -0
  252. package/dist/tools/prompt-templates.d.ts +7 -0
  253. package/dist/tools/prompt-templates.js +133 -0
  254. package/dist/tools/registry.d.ts +6 -0
  255. package/dist/tools/registry.js +9 -0
  256. package/dist/tools/schedule.d.ts +8 -0
  257. package/dist/tools/schedule.js +219 -0
  258. package/dist/tools/speak.d.ts +10 -0
  259. package/dist/tools/speak.js +56 -0
  260. package/dist/tools/tasks.d.ts +67 -0
  261. package/dist/tools/tasks.js +288 -0
  262. package/dist/tools/teams.d.ts +22 -0
  263. package/dist/tools/teams.js +470 -0
  264. package/dist/tools/web-fetch.d.ts +3 -0
  265. package/dist/tools/web-fetch.js +22 -0
  266. package/dist/tts/edge.d.ts +10 -0
  267. package/dist/tts/edge.js +60 -0
  268. package/dist/tts/speak.d.ts +12 -0
  269. package/dist/tts/speak.js +81 -0
  270. package/dist/tts/transcribe.d.ts +5 -0
  271. package/dist/tts/transcribe.js +40 -0
  272. package/dist/utils.d.ts +5 -0
  273. package/dist/utils.js +22 -0
  274. package/dist/version.d.ts +1 -0
  275. package/dist/version.js +13 -0
  276. package/package.json +102 -0
  277. package/verybot.js +2 -0
@@ -0,0 +1,932 @@
1
+ import { loadConfig, injectSecretsIntoEnv, hasConfiguredModel } from "../config.js";
2
+ import { INTEGRATIONS_DIR, MEMORY_DB_PATH } from "../paths.js";
3
+ import { Session } from "./session.js";
4
+ import { SessionStore } from "./session-store.js";
5
+ import { MessageQueue } from "./queue.js";
6
+ import { buildSystemPrompt } from "./context.js";
7
+ import { runLoop } from "./loop.js";
8
+ import { getModel } from "./providers.js";
9
+ import { adaptTools } from "./mcp-adapter.js";
10
+ import { resolveModelDef } from "../config/model-catalog.js";
11
+ import { isContextOverflowError, estimateTokens, estimateStringTokens, compactSchedulerRuns, SCHEDULED_TASK_PREFIX } from "./compaction.js";
12
+ import { DEFAULT_SAFE_BINS } from "../security/command-validator.js";
13
+ import { IntegrationRegistry } from "../integrations/registry.js";
14
+ import { resolveInlineAttachmentContent } from "../tasks/inline-attachment-content.js";
15
+ import { BrowserManager } from "../computer/browser/manager.js";
16
+ import { ChannelStore } from "./channel-store.js";
17
+ import { DelegationStore } from "./delegation-store.js";
18
+ import { TeamRegistry, parseModel } from "./agent-registry.js";
19
+ import { DEFAULT_TEAM_ID } from "../config/agent-config.js";
20
+ import { buildChannelSpecs } from "../channels/specs.js";
21
+ import { buildUserMessageContent, mergeImageDataUrls } from "./user-content.js";
22
+ import { logger } from "../logger.js";
23
+ import { saveExplicitMemory } from "../memory/explicit.js";
24
+ import { learnSessionMemories } from "../memory/session-learning.js";
25
+ import { emit, on } from "../events.js";
26
+ import { buildSessionKey, parseSessionKey, deriveMemoryTeamId } from "./session-key.js";
27
+ import { setsEqual, friendlyError } from "./utils.js";
28
+ import { SessionStateMap } from "./session-state.js";
29
+ import { MemoryExtractor } from "./memory-extractor.js";
30
+ import { WorkerCoordinator } from "./worker-coordinator.js";
31
+ import { TaskSubscriberManager } from "./task-subscriber.js";
32
+ import { buildRunTools } from "./run-tools.js";
33
+ /** Max compaction retries on context overflow before re-throwing. */
34
+ const MAX_COMPACTION_RETRIES = 3;
35
+ /** Skip compaction when estimated total tokens are below this fraction of the context window. */
36
+ const COMPACTION_SKIP_THRESHOLD = 0.6;
37
+ /** Session key suffix that identifies the team's shared scheduler session. */
38
+ const SCHEDULER_SESSION_SUFFIX = ":scheduler:main";
39
+ const MODEL_NOT_CONFIGURED_MESSAGE = "Model is not configured. Open Settings -> Agent and choose a model.";
40
+ const MODEL_NOT_CONFIGURED_REPLY = `${MODEL_NOT_CONFIGURED_MESSAGE} ` +
41
+ "If Codex CLI or Claude CLI already works on this machine, select that provider in Settings -> Agent.";
42
+ function resolveGlobalModel(model) {
43
+ if (!hasConfiguredModel(model)) {
44
+ return {
45
+ // Keep runtime bootable without a configured model so UI setup can proceed.
46
+ model: {},
47
+ modelId: "",
48
+ modelDef: resolveModelDef("", model.contextWindow),
49
+ configured: false,
50
+ };
51
+ }
52
+ return {
53
+ model: getModel(model.provider, model.id, {
54
+ codexReasoningEffort: model.codexReasoningEffort,
55
+ }),
56
+ modelId: model.id,
57
+ modelDef: resolveModelDef(model.id, model.contextWindow),
58
+ configured: true,
59
+ };
60
+ }
61
+ export class Agent {
62
+ sessions = new SessionStateMap();
63
+ sessionStore;
64
+ queue;
65
+ model;
66
+ modelId;
67
+ identity;
68
+ language;
69
+ tools;
70
+ modelDef;
71
+ memoryStore;
72
+ embeddingProvider;
73
+ memoryMaxResults;
74
+ config;
75
+ configStore;
76
+ sandbox;
77
+ skillManager;
78
+ integrationRegistry;
79
+ scheduleStore;
80
+ taskStore;
81
+ desktopAdapter;
82
+ browserManager;
83
+ sessionBrowserManagers = new Map();
84
+ teamRegistry = null;
85
+ delegationStore;
86
+ channelStore;
87
+ teamStore;
88
+ promptTemplateStore;
89
+ channelManager;
90
+ connectedChannels;
91
+ lastConfigMtime = null;
92
+ teamRegistryDirty = false;
93
+ newSessionPending = false;
94
+ unsubTeamChange = null;
95
+ memoryExtractor = null;
96
+ workerCoordinator;
97
+ taskSubscriber = null;
98
+ /** Cleanup functions for HTTP MCP servers (keyed by session key). */
99
+ mcpCleanups = new Map();
100
+ constructor(deps) {
101
+ this.tools = deps.tools;
102
+ this.sessionStore = new SessionStore(deps.dataDir);
103
+ this.memoryStore = deps.memoryStore ?? null;
104
+ this.embeddingProvider = deps.embeddingProvider ?? null;
105
+ this.config = deps.config;
106
+ this.configStore = deps.configStore;
107
+ this.sandbox = deps.sandbox ?? null;
108
+ this.skillManager = deps.skillManager ?? { systemPrompt: "", readTool: null };
109
+ this.integrationRegistry = deps.integrationRegistry ?? new IntegrationRegistry();
110
+ this.scheduleStore = deps.scheduleStore ?? null;
111
+ this.taskStore = deps.taskStore ?? null;
112
+ this.desktopAdapter = deps.desktopAdapter ?? null;
113
+ this.browserManager = deps.browserManager ?? null;
114
+ this.delegationStore = deps.delegationStore ?? null;
115
+ this.channelStore = deps.channelStore ?? null;
116
+ this.teamStore = deps.teamStore ?? null;
117
+ this.promptTemplateStore = deps.promptTemplateStore ?? null;
118
+ this.channelManager = deps.channelManager ?? null;
119
+ this.connectedChannels = deps.connectedChannels ?? null;
120
+ // Apply config synchronously so fields are initialized immediately (no `!` assertions)
121
+ const initialModel = resolveGlobalModel(deps.config.model);
122
+ this.model = initialModel.model;
123
+ this.modelId = initialModel.modelId;
124
+ this.modelDef = initialModel.modelDef;
125
+ if (!initialModel.configured)
126
+ logger.warn(MODEL_NOT_CONFIGURED_MESSAGE);
127
+ this.identity = deps.config.identity;
128
+ this.language = deps.config.language;
129
+ this.memoryMaxResults = deps.config.memory.maxResults;
130
+ if (this.memoryStore) {
131
+ this.memoryExtractor = new MemoryExtractor(this.model, this.memoryStore, this.embeddingProvider);
132
+ }
133
+ // Build team registry eagerly so getTeams() works before first message
134
+ this.rebuildTeamRegistry();
135
+ // Mark dirty when teams change so next main() call rebuilds the registry
136
+ this.unsubTeamChange = on("teamChange", () => { this.teamRegistryDirty = true; });
137
+ this.queue = new MessageQueue({
138
+ mode: "collect",
139
+ processMessage: (sessionKey, text, signal, images) => this.main(sessionKey, text, images, signal),
140
+ });
141
+ this.workerCoordinator = new WorkerCoordinator(this.sessions, this.queue);
142
+ // Start pull-based task subscriber if both stores are available
143
+ if (this.taskStore && this.teamStore) {
144
+ this.taskSubscriber = new TaskSubscriberManager({
145
+ taskStore: this.taskStore,
146
+ teamStore: this.teamStore,
147
+ sessionStore: this.sessionStore,
148
+ memoryStore: this.memoryStore,
149
+ embeddingProvider: this.embeddingProvider,
150
+ memoryMaxResults: this.memoryMaxResults,
151
+ config: deps.config,
152
+ baseTools: this.tools,
153
+ skillManager: this.skillManager,
154
+ browserConfig: this.browserManager ? {
155
+ headless: deps.config.browserHeadless,
156
+ userAgent: deps.config.browserUserAgent || undefined,
157
+ mode: deps.config.browserMode,
158
+ modeOptions: deps.config.browserModeOptions,
159
+ } : null,
160
+ sandboxEnabled: !!this.sandbox,
161
+ });
162
+ this.taskSubscriber.start();
163
+ }
164
+ }
165
+ /** Force an immediate config reload + channel reconciliation. */
166
+ async forceConfigReload() {
167
+ this.lastConfigMtime = null;
168
+ await this.reloadConfig();
169
+ }
170
+ getSession(key) {
171
+ return this.sessions.get(key)?.session;
172
+ }
173
+ getStore() {
174
+ return this.sessionStore;
175
+ }
176
+ /** Called from channels (Telegram, Discord, etc.) */
177
+ async handleMessage(msg, channel, agentId) {
178
+ let sessionKey = `${msg.channelType}:${msg.channelId}`;
179
+ try {
180
+ const teamId = agentId
181
+ ? this.teamRegistry?.resolveTeam(agentId)?.teamId
182
+ : msg.teamId;
183
+ if (!teamId) {
184
+ throw new Error("Missing teamId for incoming channel message");
185
+ }
186
+ sessionKey = buildSessionKey(teamId, msg.channelType, msg.channelId);
187
+ const text = msg.text ?? "";
188
+ // Eagerly create session so agentId + replyCallback are set before the queue runs
189
+ const state = await this.getOrCreateSession(sessionKey);
190
+ state.teamId = teamId;
191
+ state.channelType = msg.channelType;
192
+ state.channelId = msg.channelId;
193
+ this.applyAgentBinding(state, sessionKey, agentId);
194
+ if (!state.replyCallback) {
195
+ state.replyCallback = (reply) => this.deliverReply(msg, channel, reply);
196
+ }
197
+ const reply = await this.queue.enqueue(sessionKey, text);
198
+ if (reply)
199
+ await this.deliverReply(msg, channel, reply);
200
+ }
201
+ catch (err) {
202
+ logger.error(`Agent error [${this.sessionLabel(sessionKey)}]: ${err}`);
203
+ const userMsg = friendlyError(err);
204
+ emit("chat", {
205
+ sessionKey,
206
+ state: "final",
207
+ message: { role: "assistant", content: userMsg },
208
+ });
209
+ try {
210
+ await channel.send(msg.channelId, userMsg);
211
+ }
212
+ catch (sendErr) {
213
+ logger.error(`Failed to send error to user [${this.sessionLabel(sessionKey)}]: ${sendErr}`);
214
+ }
215
+ }
216
+ }
217
+ /** Deliver reply as text or voice based on TTS reply mode. */
218
+ async deliverReply(msg, channel, reply) {
219
+ const { replyMode } = this.config.tts;
220
+ const shouldVoice = this.config.tts.enabled &&
221
+ channel.sendVoice &&
222
+ (replyMode === "voice" || (replyMode === "inbound" && msg.isVoice));
223
+ if (shouldVoice) {
224
+ try {
225
+ const { synthesize } = await import("../tts/edge.js");
226
+ const audioPath = await synthesize(reply);
227
+ await channel.sendVoice(msg.channelId, audioPath);
228
+ }
229
+ catch (err) {
230
+ logger.error(`Voice reply failed, falling back to text: ${err instanceof Error ? err.message : err}`);
231
+ }
232
+ }
233
+ await channel.send(msg.channelId, reply);
234
+ }
235
+ /** Called from gateway RPC (WebSocket UI) */
236
+ async handleGatewayMessage(sessionKey, text, agentId, images) {
237
+ try {
238
+ const state = await this.getOrCreateSession(sessionKey);
239
+ const parsed = parseSessionKey(sessionKey);
240
+ const parts = sessionKey.split(":");
241
+ if (!parsed.teamId) {
242
+ throw new Error("sessionKey must include a teamId");
243
+ }
244
+ state.teamId = parsed.teamId;
245
+ state.channelType = parsed.channelType ?? "gateway";
246
+ state.channelId =
247
+ !parsed.isWorker && parts.length >= 3
248
+ ? parts.slice(2).join(":")
249
+ : sessionKey;
250
+ this.applyAgentBinding(state, sessionKey, agentId);
251
+ return await this.queue.enqueue(sessionKey, text, images);
252
+ }
253
+ catch (err) {
254
+ logger.error(`Gateway error [${this.sessionLabel(sessionKey)}]: ${err}`);
255
+ const errorReply = friendlyError(err);
256
+ // Emit a "chat" final event so the WebSocket UI renders the error
257
+ // instead of hanging on a loading state.
258
+ emit("chat", {
259
+ sessionKey,
260
+ agentId,
261
+ state: "final",
262
+ message: { role: "assistant", content: errorReply },
263
+ });
264
+ return errorReply;
265
+ }
266
+ }
267
+ /** Validate and bind an agentId to an existing session state. */
268
+ applyAgentBinding(state, sessionKey, agentId) {
269
+ if (!agentId)
270
+ return;
271
+ // Validate using the team ID already set on the state (derived from session key),
272
+ // not via resolveTeam(agentId) which can collide across teams.
273
+ if (this.teamRegistry && state.teamId) {
274
+ const registry = this.teamRegistry.getTeamRegistry(state.teamId);
275
+ if (!registry) {
276
+ logger.warn(`[${this.sessionLabel(sessionKey)}] Unknown team "${this.teamLabel(state.teamId)}" — ignoring agentId`);
277
+ return;
278
+ }
279
+ // Allow binding to this team's orchestrator OR one of its workers.
280
+ const teamAgentIds = registry.buildIdToNameMap();
281
+ if (!teamAgentIds.has(agentId)) {
282
+ logger.warn(`[${this.sessionLabel(sessionKey)}] agentId "${agentId}" does not belong to team "${this.teamLabel(state.teamId)}" — ignoring`);
283
+ return;
284
+ }
285
+ }
286
+ if (state.agentId && state.agentId !== agentId) {
287
+ logger.warn(`[${this.sessionLabel(sessionKey)}] Ignoring agentId change from "${state.agentId}" to "${agentId}" — clear session first`);
288
+ }
289
+ else if (!state.agentId) {
290
+ state.agentId = agentId;
291
+ }
292
+ }
293
+ /** Return teams for the UI picker. Reads from TeamStore for immediate visibility. */
294
+ getTeams() {
295
+ if (!this.teamStore)
296
+ return [];
297
+ const teamConfigs = this.teamStore.toTeamConfigs();
298
+ return teamConfigs.map((t) => ({
299
+ id: t.id,
300
+ name: t.name ?? t.id,
301
+ color: t.color ?? "",
302
+ orchestratorId: t.orchestrator.id,
303
+ orchestratorIdentity: t.orchestrator.identity,
304
+ orchestratorModel: t.orchestrator.model ? parseModel(t.orchestrator.model).modelId : "",
305
+ workerCount: t.workers.length,
306
+ workers: t.workers.map((w) => ({
307
+ id: w.id,
308
+ name: w.name,
309
+ subscriptions: w.subscriptions ?? [],
310
+ concurrency: w.concurrency ?? 1,
311
+ })),
312
+ statuses: t.statuses,
313
+ }));
314
+ }
315
+ /** Abort current run for a session. */
316
+ abortSession(sessionKey) {
317
+ return this.queue.abort(sessionKey);
318
+ }
319
+ /** Clear conversation history (memories are preserved across clears). */
320
+ async clearSession(sessionKey) {
321
+ logger.info(`Clearing session ${sessionKey}`);
322
+ // Extract any remaining facts in background — don't block the clear
323
+ const state = this.sessions.get(sessionKey);
324
+ if (this.memoryExtractor && state && state.messagesSinceExtraction > 0) {
325
+ const messages = state.session.getMessages();
326
+ this.memoryExtractor.extractAndSaveFacts(sessionKey, messages, deriveMemoryTeamId(sessionKey)).catch((err) => {
327
+ logger.warn(`Pre-clear extraction failed: ${err instanceof Error ? err.message : err}`);
328
+ });
329
+ }
330
+ this.sessions.delete(sessionKey);
331
+ this.queue.deleteLane(sessionKey);
332
+ await this.sessionStore.clear(sessionKey);
333
+ const sessionBrowser = this.sessionBrowserManagers.get(sessionKey);
334
+ if (sessionBrowser) {
335
+ this.sessionBrowserManagers.delete(sessionKey);
336
+ try {
337
+ await sessionBrowser.close();
338
+ logger.debug(`Closed per-session browser for ${sessionKey}`);
339
+ }
340
+ catch (err) {
341
+ logger.warn(`Failed to close per-session browser: ${err}`);
342
+ }
343
+ }
344
+ // NEW: Clean up browser pages for this session
345
+ if (this.browserManager && this.config.browserMode === "per-tab-per-session") {
346
+ try {
347
+ await this.browserManager.clearSessionPages(sessionKey);
348
+ logger.debug(`Cleaned up browser pages for session ${sessionKey}`);
349
+ }
350
+ catch (err) {
351
+ logger.warn(`Failed to clean browser pages: ${err}`);
352
+ }
353
+ }
354
+ // Shut down any HTTP MCP server for this session
355
+ const cleanup = this.mcpCleanups.get(sessionKey);
356
+ if (cleanup) {
357
+ this.mcpCleanups.delete(sessionKey);
358
+ await cleanup();
359
+ }
360
+ }
361
+ /** Clear old sessions while keeping the newest N sessions (optionally within a team). */
362
+ async clearOldSessions(keepLatest, teamId) {
363
+ if (!Number.isInteger(keepLatest) || keepLatest < 0) {
364
+ throw new Error("keepLatest must be a non-negative integer");
365
+ }
366
+ const scopedSessions = this.sessionStore
367
+ .list()
368
+ .filter((entry) => {
369
+ if (!teamId)
370
+ return true;
371
+ const sessionTeamId = entry.teamId ?? parseSessionKey(entry.key).teamId;
372
+ return sessionTeamId === teamId;
373
+ });
374
+ const staleSessionKeys = scopedSessions
375
+ .slice(keepLatest)
376
+ .map((entry) => entry.key);
377
+ for (const sessionKey of staleSessionKeys) {
378
+ await this.clearSession(sessionKey);
379
+ }
380
+ return staleSessionKeys.length;
381
+ }
382
+ /** Save one explicit fact into long-term memory for a session. */
383
+ async rememberMemory(sessionKey, fact) {
384
+ if (!this.memoryStore) {
385
+ throw new Error("Memory is not enabled");
386
+ }
387
+ return saveExplicitMemory(this.memoryStore, this.embeddingProvider, {
388
+ fact,
389
+ source: sessionKey,
390
+ teamId: deriveMemoryTeamId(sessionKey),
391
+ });
392
+ }
393
+ /** Save one explicit fact from a channel command (team-aware). */
394
+ async rememberChannelMemory(channelType, channelId, fact, teamId) {
395
+ if (!teamId)
396
+ throw new Error("teamId is required");
397
+ const sessionKey = buildSessionKey(teamId, channelType, channelId);
398
+ return this.rememberMemory(sessionKey, fact);
399
+ }
400
+ /** Auto-learn facts from the current session, optionally filtered by topic. */
401
+ async learnMemory(sessionKey, topic) {
402
+ if (!this.memoryStore) {
403
+ throw new Error("Memory is not enabled");
404
+ }
405
+ const state = await this.getOrCreateSession(sessionKey);
406
+ const model = this.resolveLearningModel(sessionKey);
407
+ return learnSessionMemories({
408
+ model,
409
+ memoryStore: this.memoryStore,
410
+ embeddingProvider: this.embeddingProvider,
411
+ sessionKey,
412
+ messages: state.session.getMessages(),
413
+ teamId: deriveMemoryTeamId(sessionKey),
414
+ topic,
415
+ });
416
+ }
417
+ /** Auto-learn facts from channel session context. */
418
+ async learnChannelMemory(channelType, channelId, topic, teamId) {
419
+ if (!teamId)
420
+ throw new Error("teamId is required");
421
+ const sessionKey = buildSessionKey(teamId, channelType, channelId);
422
+ return this.learnMemory(sessionKey, topic);
423
+ }
424
+ /**
425
+ * Run a scheduled task in a persistent scheduler session. Returns the raw LLM reply.
426
+ * Serialized through the message queue to avoid races with human messages.
427
+ * The caller (Scheduler) handles [SKIP] detection and channel delivery.
428
+ */
429
+ async runScheduledTask(opts) {
430
+ const schedulerKey = `${opts.teamId}${SCHEDULER_SESSION_SUFFIX}`;
431
+ const state = await this.getOrCreateSession(schedulerKey);
432
+ state.teamId = opts.teamId;
433
+ state.channelType = "scheduler";
434
+ state.channelId = "main";
435
+ // Store integrations for this scheduled run so main() can pick them up
436
+ state.scheduledIntegrations = opts.integrations;
437
+ return this.queue.enqueue(schedulerKey, `${SCHEDULED_TASK_PREFIX} ${opts.prompt}`);
438
+ }
439
+ /**
440
+ * Handle a human message sent to the scheduler session.
441
+ * Serialized via the message queue to avoid overlap with scheduled task executions.
442
+ */
443
+ async handleSchedulerMessage(teamId, text, senderInfo) {
444
+ const schedulerKey = `${teamId}${SCHEDULER_SESSION_SUFFIX}`;
445
+ const state = await this.getOrCreateSession(schedulerKey);
446
+ state.teamId = teamId;
447
+ state.channelType = "scheduler";
448
+ state.channelId = "main";
449
+ const prefixed = senderInfo ? `[${senderInfo}] ${text}` : text;
450
+ return this.queue.enqueue(schedulerKey, prefixed);
451
+ }
452
+ /** Get the scheduler session for a team (for history display). */
453
+ getSchedulerSession(teamId) {
454
+ const schedulerKey = `${teamId}${SCHEDULER_SESSION_SUFFIX}`;
455
+ return this.sessions.get(schedulerKey)?.session;
456
+ }
457
+ /** Start initial channels based on current config. Called once at boot. */
458
+ async initChannels() {
459
+ await this.reconcileChannels(this.config);
460
+ }
461
+ /** Clean up sandbox containers. */
462
+ cleanupSandbox() {
463
+ this.sandbox?.cleanupAll();
464
+ }
465
+ /** Extract remaining facts from all active sessions (call before shutdown). */
466
+ async flushMemories() {
467
+ if (this.memoryExtractor) {
468
+ await this.memoryExtractor.flushAll(this.sessions);
469
+ }
470
+ this.workerCoordinator.clearAllTimers();
471
+ this.taskSubscriber?.stop();
472
+ this.unsubTeamChange?.();
473
+ // Shut down all HTTP MCP servers
474
+ const cleanups = [...this.mcpCleanups.values()];
475
+ this.mcpCleanups.clear();
476
+ await Promise.allSettled(cleanups.map((fn) => fn()));
477
+ await this.closeAllSessionBrowsers();
478
+ }
479
+ async getOrCreateSession(key) {
480
+ const existing = this.sessions.get(key);
481
+ if (existing)
482
+ return existing;
483
+ this.newSessionPending = true;
484
+ const session = await this.sessionStore.load(key) ?? new Session(key);
485
+ const state = this.sessions.getOrCreate(key, session);
486
+ // Seed token estimate from restored messages so compaction skip check is accurate
487
+ if (session.messageCount > 0) {
488
+ state.estimatedMsgTokens = estimateTokens(session.getMessages());
489
+ }
490
+ return state;
491
+ }
492
+ /** Build tools, adapt for MCP providers, and run the inference loop. */
493
+ async buildAdaptAndRun(opts) {
494
+ const label = this.sessionLabel(opts.sessionKey);
495
+ const deps = this.buildRunToolsDeps(opts.teamScopedRegistry, `${opts.runProvider}:${opts.runModelId}`, this.getRunBrowserManager(opts.sessionKey), opts.runBaseTools);
496
+ let tools = buildRunTools(deps, opts.sessionKey, opts.activeIntegrations, opts.channelInfo, opts.effectiveAgentId, opts.effectiveAgentRole, opts.taskTeamId, opts.scheduleTeamId, label, opts.workspaceCwd, opts.runToolAllowlist);
497
+ // Clean up any previous MCP server, then adapt for MCP-based providers
498
+ const prevCleanup = this.mcpCleanups.get(opts.sessionKey);
499
+ if (prevCleanup) {
500
+ this.mcpCleanups.delete(opts.sessionKey);
501
+ await prevCleanup();
502
+ }
503
+ const adapted = await adaptTools(opts.runProvider, opts.runModelId, opts.runModel, tools, {
504
+ sandboxEnabled: !!this.sandbox,
505
+ codexReasoningEffort: opts.runCodexReasoningEffort,
506
+ cwd: opts.workspaceCwd,
507
+ });
508
+ if (adapted.cleanup)
509
+ this.mcpCleanups.set(opts.sessionKey, adapted.cleanup);
510
+ tools = adapted.tools;
511
+ return this.runLoopWithRetry(opts.session, opts.system, tools, opts.sessionKey, adapted.model, opts.contextWindow, opts.maxSteps, opts.abortSignal);
512
+ }
513
+ /** Build the RunToolsDeps from current agent state (agentRegistry filled per-call). */
514
+ buildRunToolsDeps(agentRegistry, effectiveModel, browserManager, baseTools) {
515
+ const configuredGlobalModel = hasConfiguredModel(this.config.model)
516
+ ? `${this.config.model.provider}:${this.config.model.id}`
517
+ : "";
518
+ return {
519
+ baseTools: baseTools ?? this.tools,
520
+ config: this.config,
521
+ memoryStore: this.memoryStore,
522
+ embeddingProvider: this.embeddingProvider,
523
+ memoryMaxResults: this.memoryMaxResults,
524
+ sandbox: this.sandbox,
525
+ skillManager: this.skillManager,
526
+ integrationRegistry: this.integrationRegistry,
527
+ scheduleStore: this.scheduleStore,
528
+ taskStore: this.taskStore,
529
+ teamStore: this.teamStore,
530
+ promptTemplateStore: this.promptTemplateStore,
531
+ desktopAdapter: this.desktopAdapter,
532
+ browserManager: browserManager ?? this.browserManager,
533
+ effectiveModel: effectiveModel ?? configuredGlobalModel,
534
+ agentRegistry,
535
+ delegationStore: this.delegationStore,
536
+ channelStore: this.channelStore,
537
+ channelManager: this.channelManager,
538
+ sessionStore: this.sessionStore,
539
+ modelId: this.modelId,
540
+ onWorkerComplete: this.workerCoordinator.onWorkerComplete.bind(this.workerCoordinator),
541
+ };
542
+ }
543
+ /** Return all tool names available in the current config (for UI discovery). */
544
+ getToolNames() {
545
+ const deps = this.buildRunToolsDeps(null);
546
+ const tools = buildRunTools(deps, "__discovery__", new Set());
547
+ return Object.keys(tools);
548
+ }
549
+ /** Run the loop with compaction retries on context overflow. */
550
+ async runLoopWithRetry(session, system, tools, sessionKey, model, contextWindow, maxSteps, abortSignal) {
551
+ const effectiveModel = model ?? this.model;
552
+ const effectiveContextWindow = contextWindow ?? this.modelDef.contextWindow;
553
+ const effectiveMaxSteps = maxSteps && maxSteps > 0 ? maxSteps : this.config.model.maxSteps;
554
+ for (let attempt = 1; attempt <= MAX_COMPACTION_RETRIES + 1; attempt++) {
555
+ try {
556
+ const { text, assistantContent } = await runLoop({
557
+ model: effectiveModel,
558
+ system,
559
+ messages: session.getMessages(),
560
+ tools,
561
+ sessionKey,
562
+ abortSignal,
563
+ sessionLabel: this.sessionLabel(sessionKey),
564
+ maxSteps: effectiveMaxSteps,
565
+ });
566
+ return { text, assistantContent };
567
+ }
568
+ catch (err) {
569
+ if (!isContextOverflowError(err) || attempt > MAX_COMPACTION_RETRIES) {
570
+ throw err;
571
+ }
572
+ logger.warn(`Context overflow on attempt ${attempt}/${MAX_COMPACTION_RETRIES}, forcing compaction`);
573
+ const compacted = await session.compact(effectiveModel, effectiveContextWindow, system);
574
+ if (!compacted) {
575
+ logger.warn("Compaction had no effect, emergency truncating to last 10 messages");
576
+ session.truncate(10);
577
+ }
578
+ }
579
+ }
580
+ throw new Error("Unreachable: all compaction retries exhausted");
581
+ }
582
+ /** (Re)build team registry from TeamStore (or skip if no store). */
583
+ rebuildTeamRegistry() {
584
+ if (!this.teamStore) {
585
+ this.teamRegistry = null;
586
+ return;
587
+ }
588
+ const teamConfigs = this.teamStore.toTeamConfigs();
589
+ if (teamConfigs.length === 0) {
590
+ this.teamRegistry = null;
591
+ return;
592
+ }
593
+ this.teamRegistry = new TeamRegistry(teamConfigs, {
594
+ memoryStore: this.memoryStore,
595
+ embeddingProvider: this.embeddingProvider,
596
+ baseTools: this.tools,
597
+ config: this.config,
598
+ });
599
+ }
600
+ /** Reload config from disk so API key / model changes take effect immediately. */
601
+ async reloadConfig() {
602
+ // Skip reload if config file hasn't changed — unless it's a new session
603
+ const mtime = this.configStore.mtime();
604
+ if (!this.newSessionPending && mtime !== null && mtime === this.lastConfigMtime)
605
+ return;
606
+ this.lastConfigMtime = mtime;
607
+ logger.info(`Reloading config`);
608
+ injectSecretsIntoEnv(this.configStore);
609
+ const previousMode = this.config.browserMode;
610
+ const fresh = loadConfig(this.configStore);
611
+ const resolvedModel = resolveGlobalModel(fresh.model);
612
+ this.model = resolvedModel.model;
613
+ this.modelId = resolvedModel.modelId;
614
+ this.modelDef = resolvedModel.modelDef;
615
+ if (!resolvedModel.configured)
616
+ logger.warn(MODEL_NOT_CONFIGURED_MESSAGE);
617
+ this.identity = fresh.identity;
618
+ this.language = fresh.language;
619
+ this.memoryMaxResults = fresh.memory.maxResults;
620
+ this.config = fresh;
621
+ if (previousMode === "per-browser-per-session" && fresh.browserMode !== "per-browser-per-session") {
622
+ await this.closeAllSessionBrowsers();
623
+ }
624
+ if (previousMode !== "per-browser-per-session" && fresh.browserMode === "per-browser-per-session") {
625
+ await this.browserManager?.close();
626
+ }
627
+ // Update memory extractor model
628
+ this.memoryExtractor?.setModel(this.model);
629
+ // Push browser config changes so next launch uses fresh values
630
+ await this.browserManager?.updateConfig({
631
+ headless: fresh.browserHeadless,
632
+ userAgent: fresh.browserUserAgent || undefined,
633
+ mode: fresh.browserMode,
634
+ modeOptions: fresh.browserModeOptions,
635
+ });
636
+ if (fresh.browserMode === "per-browser-per-session") {
637
+ const perSessionConfig = this.buildPerSessionBrowserConfig(fresh);
638
+ for (const manager of this.sessionBrowserManagers.values()) {
639
+ await manager.updateConfig(perSessionConfig);
640
+ }
641
+ }
642
+ // Hot-reload channels in background (non-blocking)
643
+ this.reconcileChannels(fresh).catch((err) => {
644
+ logger.error(`Channel reconcile failed: ${err instanceof Error ? err.message : err}`);
645
+ });
646
+ }
647
+ /**
648
+ * Build channel specs from config and reconcile with ChannelManager.
649
+ * Handlers use dynamic orchestrator lookup (reads this.config at call time)
650
+ * so orchestrator-only changes don't require a channel restart.
651
+ */
652
+ async reconcileChannels(config) {
653
+ if (!this.channelManager)
654
+ return;
655
+ const specs = buildChannelSpecs(config, {
656
+ onMessage: (msg, ch) => this.handleMessage(msg, ch),
657
+ onClear: async (channelType, channelId, teamId) => {
658
+ if (!teamId)
659
+ throw new Error("teamId is required");
660
+ await this.clearSession(buildSessionKey(teamId, channelType, channelId));
661
+ },
662
+ onLearn: (channelType, channelId, topic, teamId) => this.learnChannelMemory(channelType, channelId, topic, teamId),
663
+ onRemember: (channelType, channelId, fact, teamId) => this.rememberChannelMemory(channelType, channelId, fact, teamId),
664
+ listTeams: () => this.getTeams().map((t) => ({ id: t.id, name: t.name })),
665
+ connectedChannels: this.connectedChannels,
666
+ getChannelTableConfig: (channel) => this.config.channels[channel]
667
+ ?.markdown?.tables,
668
+ });
669
+ await this.channelManager.reconcile(specs);
670
+ }
671
+ /** Resolve a human-readable team label for logs/console (name preferred, falls back to ID). */
672
+ teamLabel(teamId) {
673
+ const team = this.teamStore?.getTeamById(teamId);
674
+ return team?.name ?? teamId;
675
+ }
676
+ /** Build a human-readable session label for logs: replaces team UUID with team name. */
677
+ sessionLabel(sessionKey) {
678
+ const colonIdx = sessionKey.indexOf(":");
679
+ if (colonIdx <= 0)
680
+ return sessionKey;
681
+ const teamId = sessionKey.slice(0, colonIdx);
682
+ return this.teamLabel(teamId) + sessionKey.slice(colonIdx);
683
+ }
684
+ resolveLearningModel(sessionKey) {
685
+ const { teamId } = parseSessionKey(sessionKey);
686
+ if (teamId && this.teamRegistry) {
687
+ const registry = this.teamRegistry.getTeamRegistry(teamId);
688
+ if (registry) {
689
+ return registry.resolveOrchestrator().model;
690
+ }
691
+ }
692
+ return this.model;
693
+ }
694
+ buildPerSessionBrowserConfig(config) {
695
+ return {
696
+ headless: config.browserHeadless,
697
+ userAgent: config.browserUserAgent || undefined,
698
+ // Per-session managers are already isolated by process/context ownership.
699
+ mode: "shared",
700
+ modeOptions: config.browserModeOptions,
701
+ profileDir: "temp",
702
+ };
703
+ }
704
+ getRunBrowserManager(sessionKey) {
705
+ if (!this.browserManager)
706
+ return null;
707
+ if (this.config.browserMode !== "per-browser-per-session")
708
+ return this.browserManager;
709
+ const existing = this.sessionBrowserManagers.get(sessionKey);
710
+ if (existing)
711
+ return existing;
712
+ const manager = new BrowserManager(this.buildPerSessionBrowserConfig(this.config));
713
+ this.sessionBrowserManagers.set(sessionKey, manager);
714
+ logger.info(`[${this.sessionLabel(sessionKey)}] Created per-session browser manager`);
715
+ return manager;
716
+ }
717
+ async closeAllSessionBrowsers() {
718
+ if (this.sessionBrowserManagers.size === 0)
719
+ return;
720
+ const managers = [...this.sessionBrowserManagers.values()];
721
+ this.sessionBrowserManagers.clear();
722
+ await Promise.allSettled(managers.map((manager) => manager.close()));
723
+ }
724
+ /** Enrich session index entry with team/agent name metadata for UI display. */
725
+ enrichSessionMetadata(sessionKey, teamId) {
726
+ const meta = { teamId };
727
+ if (this.teamStore) {
728
+ const team = this.teamStore.getTeamById(teamId);
729
+ if (team)
730
+ meta.teamName = team.name;
731
+ const parsed = parseSessionKey(sessionKey);
732
+ meta.channelType = parsed.channelType;
733
+ if (parsed.isWorker && parsed.agentName) {
734
+ meta.agentName = parsed.agentName;
735
+ const agent = this.teamStore.getAgentByName(teamId, parsed.agentName);
736
+ if (agent)
737
+ meta.agentId = agent.id;
738
+ }
739
+ }
740
+ this.sessionStore.updateMetadata(sessionKey, meta);
741
+ }
742
+ async main(sessionKey, text, images, abortSignal) {
743
+ await this.reloadConfig();
744
+ // Rebuild team registry when teams were modified or a new session started
745
+ if (this.teamRegistryDirty || this.newSessionPending) {
746
+ this.teamRegistryDirty = false;
747
+ this.newSessionPending = false;
748
+ this.rebuildTeamRegistry();
749
+ if (this.teamRegistry) {
750
+ if (!this.delegationStore) {
751
+ this.delegationStore = await DelegationStore.create(MEMORY_DB_PATH);
752
+ }
753
+ if (!this.channelStore) {
754
+ this.channelStore = await ChannelStore.create(MEMORY_DB_PATH);
755
+ }
756
+ }
757
+ }
758
+ // Session is guaranteed to exist (created eagerly in handleMessage/handleGatewayMessage)
759
+ const state = this.sessions.get(sessionKey);
760
+ if (!state) {
761
+ logger.warn(`[${this.sessionLabel(sessionKey)}] Session was cleared mid-queue, skipping`);
762
+ return "";
763
+ }
764
+ const { session } = state;
765
+ // Resolve per-session agent (orchestrator/worker) via team registry
766
+ const effectiveAgentId = state.agentId;
767
+ const effectiveTeamId = state.teamId;
768
+ if (!effectiveTeamId) {
769
+ throw new Error("Session missing teamId");
770
+ }
771
+ let effectiveAgentRole;
772
+ let runModel = this.model;
773
+ let runModelDef = this.modelDef;
774
+ let runIdentity = this.identity;
775
+ let runBaseTools = this.tools;
776
+ let runToolAllowlist;
777
+ let runModelId = this.modelId;
778
+ let runProvider = this.config.model.provider;
779
+ let runCodexReasoningEffort = this.config.model.codexReasoningEffort;
780
+ let runMaxSteps = 0; // 0 = inherit global default
781
+ let teamScopedRegistry = null;
782
+ if (this.teamRegistry) {
783
+ // Resolve by team ID (source of truth from session key), not orchestrator ID
784
+ const registry = this.teamRegistry.getTeamRegistry(effectiveTeamId);
785
+ if (registry) {
786
+ teamScopedRegistry = registry;
787
+ const resolvedById = effectiveAgentId
788
+ ? registry.resolveAgentById(effectiveAgentId)
789
+ : { role: "orchestrator", resolved: registry.resolveOrchestrator() };
790
+ const selected = resolvedById ?? { role: "orchestrator", resolved: registry.resolveOrchestrator() };
791
+ if (effectiveAgentId && !resolvedById) {
792
+ logger.warn(`[${this.sessionLabel(sessionKey)}] Bound agent "${effectiveAgentId}" not found in team "${this.teamLabel(effectiveTeamId)}" — falling back to orchestrator`);
793
+ }
794
+ effectiveAgentRole = selected.role;
795
+ const resolved = selected.resolved;
796
+ runModel = resolved.model;
797
+ runModelDef = resolved.modelDef;
798
+ runIdentity = resolved.agentConfig.identity;
799
+ runBaseTools = resolved.tools;
800
+ runToolAllowlist = resolved.agentConfig.tools;
801
+ const parsed = parseModel(resolved.agentConfig.model);
802
+ runModelId = parsed.modelId;
803
+ runProvider = parsed.provider;
804
+ runCodexReasoningEffort = parsed.codexReasoningEffort;
805
+ runMaxSteps = resolved.agentConfig.maxSteps;
806
+ logger.info(`[${this.sessionLabel(sessionKey)}] Using team "${this.teamLabel(effectiveTeamId)}" ${selected.role} "${resolved.agentConfig.id}"`);
807
+ }
808
+ }
809
+ if (!runProvider.trim() || !runModelId.trim()) {
810
+ logger.warn(`[${this.sessionLabel(sessionKey)}] ${MODEL_NOT_CONFIGURED_REPLY}`);
811
+ return MODEL_NOT_CONFIGURED_REPLY;
812
+ }
813
+ const { normalizedText, imageDataUrls: inlineAttachmentImages } = await resolveInlineAttachmentContent(text);
814
+ const allImageDataUrls = mergeImageDataUrls(images, inlineAttachmentImages);
815
+ // Track delegation depth via worker coordinator
816
+ this.workerCoordinator.trackDelegationDepth(sessionKey, normalizedText);
817
+ // Detect scheduler session from key pattern
818
+ const isSchedulerSession = sessionKey.endsWith(SCHEDULER_SESSION_SUFFIX);
819
+ const isScheduledTask = normalizedText.startsWith(SCHEDULED_TASK_PREFIX);
820
+ // For scheduled tasks: compact previous runs before appending
821
+ if (isSchedulerSession && isScheduledTask) {
822
+ compactSchedulerRuns(session);
823
+ }
824
+ const content = buildUserMessageContent(normalizedText, allImageDataUrls);
825
+ session.append({ role: "user", content });
826
+ state.estimatedMsgTokens += estimateStringTokens(normalizedText);
827
+ // Persist the user turn immediately so history survives page switches
828
+ // even while the assistant run is still in progress.
829
+ await this.sessionStore.save(session);
830
+ // Re-discover integrations at the start of each new session.
831
+ if (!state.integrations) {
832
+ await this.integrationRegistry.refresh(this.configStore, INTEGRATIONS_DIR, this.config);
833
+ state.integrations = new Set();
834
+ }
835
+ // For scheduled tasks, merge in the scheduled integrations then consume them
836
+ let activeIntegrations = state.integrations;
837
+ if (isScheduledTask && state.scheduledIntegrations) {
838
+ const merged = new Set(activeIntegrations);
839
+ for (const name of state.scheduledIntegrations) {
840
+ if (this.integrationRegistry.has(name))
841
+ merged.add(name);
842
+ }
843
+ activeIntegrations = merged;
844
+ state.scheduledIntegrations = undefined;
845
+ }
846
+ // Team context for prompts (workspace + user variables).
847
+ const teamContext = effectiveTeamId !== DEFAULT_TEAM_ID
848
+ ? (this.teamRegistry?.getTeamConfig(effectiveTeamId)
849
+ ?? this.teamStore?.toTeamConfigs().find((t) => t.id === effectiveTeamId))
850
+ : undefined;
851
+ // Build system prompt first so compaction can account for its token cost
852
+ const system = buildSystemPrompt({
853
+ identity: runIdentity,
854
+ modelId: runModelId,
855
+ teamWorkspace: teamContext?.workspace,
856
+ teamVariables: teamContext?.variables,
857
+ language: this.language,
858
+ hasMemory: this.memoryStore !== null,
859
+ bashMode: this.config.bash.security,
860
+ bashSafeBins: [...DEFAULT_SAFE_BINS, ...this.config.bash.safeBins],
861
+ hasDesktop: this.config.desktop.enabled,
862
+ skillListing: this.skillManager.systemPrompt,
863
+ integrationListing: this.integrationRegistry.buildListing(activeIntegrations),
864
+ activeIntegrationPrompts: this.integrationRegistry.getPromptsFor(activeIntegrations),
865
+ hasScheduler: this.scheduleStore !== null,
866
+ scheduledTask: isScheduledTask,
867
+ schedulerSession: isSchedulerSession,
868
+ hasTTS: this.config.tts.enabled,
869
+ channelType: state.channelType,
870
+ hasDelegation: effectiveAgentRole === "orchestrator" &&
871
+ !!teamScopedRegistry &&
872
+ teamScopedRegistry.delegatableWorkers().length > 0,
873
+ });
874
+ // Skip compaction when clearly under budget (avoids O(n) token scan on every message)
875
+ const sysTokens = estimateStringTokens(system);
876
+ if (state.estimatedMsgTokens + sysTokens > runModelDef.contextWindow * COMPACTION_SKIP_THRESHOLD) {
877
+ const compacted = await session.compact(runModel, runModelDef.contextWindow, system);
878
+ if (compacted)
879
+ state.estimatedMsgTokens = estimateTokens(session.getMessages());
880
+ }
881
+ // Snapshot active integrations before the run to detect mid-run changes
882
+ const activeBefore = new Set(activeIntegrations);
883
+ // Build per-run tools, adapt for MCP providers, and execute the loop
884
+ // Task tools: default team sees all tasks; non-default teams are scoped to their own tasks
885
+ // Schedule tools: always pass effectiveTeamId so schedules work for all teams
886
+ const runOpts = {
887
+ session,
888
+ sessionKey,
889
+ system,
890
+ abortSignal,
891
+ teamScopedRegistry,
892
+ activeIntegrations,
893
+ channelInfo: { channelType: state.channelType ?? "unknown", channelId: state.channelId ?? sessionKey },
894
+ effectiveAgentId,
895
+ effectiveAgentRole,
896
+ taskTeamId: effectiveTeamId !== DEFAULT_TEAM_ID ? effectiveTeamId : undefined,
897
+ scheduleTeamId: effectiveTeamId,
898
+ workspaceCwd: teamContext?.workspace,
899
+ runBaseTools,
900
+ runToolAllowlist,
901
+ runProvider,
902
+ runModelId,
903
+ runCodexReasoningEffort,
904
+ runModel,
905
+ contextWindow: runModelDef.contextWindow,
906
+ maxSteps: runMaxSteps,
907
+ };
908
+ let runResult = await this.buildAdaptAndRun(runOpts);
909
+ // If integrations were enabled/disabled during the run, re-run with updated tools
910
+ if (!abortSignal?.aborted && !setsEqual(activeIntegrations, activeBefore)) {
911
+ logger.info(`[${this.sessionLabel(sessionKey)}] Integrations changed mid-run, re-running with updated tools`);
912
+ runResult = await this.buildAdaptAndRun(runOpts);
913
+ }
914
+ const reply = runResult.text;
915
+ if (runResult.assistantContent !== null || reply.length > 0) {
916
+ session.append({ role: "assistant", content: runResult.assistantContent ?? reply });
917
+ }
918
+ // Refresh running token estimate (used by compaction skip check on next message)
919
+ const msgTokens = estimateTokens(session.getMessages());
920
+ state.estimatedMsgTokens = msgTokens;
921
+ logger.info(`[${this.sessionLabel(sessionKey)}] Context: ~${msgTokens + sysTokens} tokens (system: ${sysTokens}, messages: ${msgTokens}, budget: ${runModelDef.contextWindow})`);
922
+ await this.sessionStore.save(session);
923
+ // Enrich session metadata with team/agent names for UI display
924
+ this.enrichSessionMetadata(sessionKey, effectiveTeamId);
925
+ // Track and maybe extract facts via memory extractor
926
+ if (this.memoryExtractor) {
927
+ const memoryTeamId = effectiveTeamId !== DEFAULT_TEAM_ID ? effectiveTeamId : undefined;
928
+ this.memoryExtractor.trackAndMaybeExtract(sessionKey, this.sessions, session.getMessages(), memoryTeamId);
929
+ }
930
+ return reply;
931
+ }
932
+ }