funolio-agent 1.0.75 → 1.1.65

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 (236) hide show
  1. package/dist/auth/credential-reader.d.ts.map +1 -1
  2. package/dist/auth/credential-reader.js +4 -3
  3. package/dist/auth/credential-reader.js.map +1 -1
  4. package/dist/auth/token-refresh.d.ts +8 -0
  5. package/dist/auth/token-refresh.d.ts.map +1 -1
  6. package/dist/auth/token-refresh.js +82 -52
  7. package/dist/auth/token-refresh.js.map +1 -1
  8. package/dist/auto-organizer.d.ts.map +1 -1
  9. package/dist/auto-organizer.js +6 -7
  10. package/dist/auto-organizer.js.map +1 -1
  11. package/dist/bench-prefix.d.ts +16 -0
  12. package/dist/bench-prefix.d.ts.map +1 -0
  13. package/dist/bench-prefix.js +25 -0
  14. package/dist/bench-prefix.js.map +1 -0
  15. package/dist/bot-manager.d.ts.map +1 -1
  16. package/dist/bot-manager.js +23 -14
  17. package/dist/bot-manager.js.map +1 -1
  18. package/dist/chat-sync.d.ts +42 -0
  19. package/dist/chat-sync.d.ts.map +1 -0
  20. package/dist/chat-sync.js +95 -0
  21. package/dist/chat-sync.js.map +1 -0
  22. package/dist/clerk-model.d.ts +7 -0
  23. package/dist/clerk-model.d.ts.map +1 -1
  24. package/dist/clerk-model.js +42 -8
  25. package/dist/clerk-model.js.map +1 -1
  26. package/dist/cli-bootstrap-history.d.ts +10 -0
  27. package/dist/cli-bootstrap-history.d.ts.map +1 -0
  28. package/dist/cli-bootstrap-history.js +112 -0
  29. package/dist/cli-bootstrap-history.js.map +1 -0
  30. package/dist/cli-models.d.ts +8 -0
  31. package/dist/cli-models.d.ts.map +1 -0
  32. package/dist/cli-models.js +91 -0
  33. package/dist/cli-models.js.map +1 -0
  34. package/dist/cli-session-epoch.d.ts +13 -3
  35. package/dist/cli-session-epoch.d.ts.map +1 -1
  36. package/dist/cli-session-epoch.js +53 -4
  37. package/dist/cli-session-epoch.js.map +1 -1
  38. package/dist/codex-app-server-manager.d.ts +64 -4
  39. package/dist/codex-app-server-manager.d.ts.map +1 -1
  40. package/dist/codex-app-server-manager.js +755 -55
  41. package/dist/codex-app-server-manager.js.map +1 -1
  42. package/dist/commands/pool.d.ts +32 -0
  43. package/dist/commands/pool.d.ts.map +1 -1
  44. package/dist/commands/pool.js +145 -66
  45. package/dist/commands/pool.js.map +1 -1
  46. package/dist/commands/start.d.ts +21 -0
  47. package/dist/commands/start.d.ts.map +1 -1
  48. package/dist/commands/start.js +484 -63
  49. package/dist/commands/start.js.map +1 -1
  50. package/dist/commands/status.d.ts.map +1 -1
  51. package/dist/commands/status.js +5 -2
  52. package/dist/commands/status.js.map +1 -1
  53. package/dist/config.d.ts +1 -0
  54. package/dist/config.d.ts.map +1 -1
  55. package/dist/config.js +170 -58
  56. package/dist/config.js.map +1 -1
  57. package/dist/context-window.d.ts +37 -1
  58. package/dist/context-window.d.ts.map +1 -1
  59. package/dist/context-window.js +202 -16
  60. package/dist/context-window.js.map +1 -1
  61. package/dist/live-activity.d.ts +3 -1
  62. package/dist/live-activity.d.ts.map +1 -1
  63. package/dist/live-activity.js.map +1 -1
  64. package/dist/local-chat-execution.d.ts +114 -0
  65. package/dist/local-chat-execution.d.ts.map +1 -0
  66. package/dist/local-chat-execution.js +349 -0
  67. package/dist/local-chat-execution.js.map +1 -0
  68. package/dist/local-cli-pty-manager.d.ts +138 -3
  69. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  70. package/dist/local-cli-pty-manager.js +1415 -111
  71. package/dist/local-cli-pty-manager.js.map +1 -1
  72. package/dist/local-conversation-gateway.d.ts +110 -0
  73. package/dist/local-conversation-gateway.d.ts.map +1 -0
  74. package/dist/local-conversation-gateway.js +175 -0
  75. package/dist/local-conversation-gateway.js.map +1 -0
  76. package/dist/local-data.d.ts +235 -5
  77. package/dist/local-data.d.ts.map +1 -1
  78. package/dist/local-data.js +1066 -87
  79. package/dist/local-data.js.map +1 -1
  80. package/dist/local-db.d.ts +6 -0
  81. package/dist/local-db.d.ts.map +1 -1
  82. package/dist/local-db.js +376 -4
  83. package/dist/local-db.js.map +1 -1
  84. package/dist/local-funnel.d.ts.map +1 -1
  85. package/dist/local-funnel.js +6 -5
  86. package/dist/local-funnel.js.map +1 -1
  87. package/dist/local-server.d.ts +30 -0
  88. package/dist/local-server.d.ts.map +1 -1
  89. package/dist/local-server.js +2898 -319
  90. package/dist/local-server.js.map +1 -1
  91. package/dist/managed-process-registry.d.ts +59 -0
  92. package/dist/managed-process-registry.d.ts.map +1 -0
  93. package/dist/managed-process-registry.js +390 -0
  94. package/dist/managed-process-registry.js.map +1 -0
  95. package/dist/mcp/claude-config-writer.d.ts +5 -5
  96. package/dist/mcp/claude-config-writer.d.ts.map +1 -1
  97. package/dist/mcp/claude-config-writer.js +19 -11
  98. package/dist/mcp/claude-config-writer.js.map +1 -1
  99. package/dist/mcp/index.d.ts +4 -2
  100. package/dist/mcp/index.d.ts.map +1 -1
  101. package/dist/mcp/index.js.map +1 -1
  102. package/dist/mcp/sync-cli-config.d.ts +42 -4
  103. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  104. package/dist/mcp/sync-cli-config.js +497 -17
  105. package/dist/mcp/sync-cli-config.js.map +1 -1
  106. package/dist/message-loop.d.ts.map +1 -1
  107. package/dist/message-loop.js +43 -1
  108. package/dist/message-loop.js.map +1 -1
  109. package/dist/mqtt-client.d.ts +34 -0
  110. package/dist/mqtt-client.d.ts.map +1 -1
  111. package/dist/mqtt-client.js +270 -45
  112. package/dist/mqtt-client.js.map +1 -1
  113. package/dist/mqtt-data-relay.d.ts +44 -0
  114. package/dist/mqtt-data-relay.d.ts.map +1 -0
  115. package/dist/mqtt-data-relay.js +106 -0
  116. package/dist/mqtt-data-relay.js.map +1 -0
  117. package/dist/orchestration/capabilities.d.ts +13 -0
  118. package/dist/orchestration/capabilities.d.ts.map +1 -0
  119. package/dist/orchestration/capabilities.js +152 -0
  120. package/dist/orchestration/capabilities.js.map +1 -0
  121. package/dist/orchestration/dispatch-executor.d.ts +83 -0
  122. package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
  123. package/dist/orchestration/dispatch-executor.js +266 -0
  124. package/dist/orchestration/dispatch-executor.js.map +1 -0
  125. package/dist/orchestration/dispatch-hint.d.ts +134 -0
  126. package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
  127. package/dist/orchestration/dispatch-hint.js +247 -0
  128. package/dist/orchestration/dispatch-hint.js.map +1 -0
  129. package/dist/orchestration/dispatch-runner.d.ts +106 -0
  130. package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
  131. package/dist/orchestration/dispatch-runner.js +604 -0
  132. package/dist/orchestration/dispatch-runner.js.map +1 -0
  133. package/dist/orchestration/dispatch-tools.d.ts +167 -0
  134. package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
  135. package/dist/orchestration/dispatch-tools.js +328 -0
  136. package/dist/orchestration/dispatch-tools.js.map +1 -0
  137. package/dist/orchestration/front-door-policy.d.ts +35 -10
  138. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  139. package/dist/orchestration/front-door-policy.js +30 -267
  140. package/dist/orchestration/front-door-policy.js.map +1 -1
  141. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
  142. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
  143. package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
  144. package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
  145. package/dist/orchestration/orchestrator-operating-prompt.d.ts +14 -0
  146. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  147. package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
  148. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  149. package/dist/orchestration/plan-import.d.ts +39 -0
  150. package/dist/orchestration/plan-import.d.ts.map +1 -0
  151. package/dist/orchestration/plan-import.js +547 -0
  152. package/dist/orchestration/plan-import.js.map +1 -0
  153. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  154. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  155. package/dist/orchestration/worker-operating-prompt.js +36 -46
  156. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  157. package/dist/orchestrator.d.ts +195 -3
  158. package/dist/orchestrator.d.ts.map +1 -1
  159. package/dist/orchestrator.js +1970 -432
  160. package/dist/orchestrator.js.map +1 -1
  161. package/dist/providers/anthropic.d.ts.map +1 -1
  162. package/dist/providers/anthropic.js +8 -4
  163. package/dist/providers/anthropic.js.map +1 -1
  164. package/dist/providers/claude-cli.d.ts.map +1 -1
  165. package/dist/providers/claude-cli.js +28 -3
  166. package/dist/providers/claude-cli.js.map +1 -1
  167. package/dist/providers/codex-cli.d.ts +10 -6
  168. package/dist/providers/codex-cli.d.ts.map +1 -1
  169. package/dist/providers/codex-cli.js +190 -17
  170. package/dist/providers/codex-cli.js.map +1 -1
  171. package/dist/providers/google.d.ts.map +1 -1
  172. package/dist/providers/google.js +15 -5
  173. package/dist/providers/google.js.map +1 -1
  174. package/dist/providers/index.d.ts +15 -1
  175. package/dist/providers/index.d.ts.map +1 -1
  176. package/dist/providers/index.js.map +1 -1
  177. package/dist/providers/openai.d.ts +1 -1
  178. package/dist/providers/openai.d.ts.map +1 -1
  179. package/dist/providers/openai.js +13 -5
  180. package/dist/providers/openai.js.map +1 -1
  181. package/dist/server-adapter.d.ts +8 -0
  182. package/dist/server-adapter.d.ts.map +1 -1
  183. package/dist/server-adapter.js +7 -0
  184. package/dist/server-adapter.js.map +1 -1
  185. package/dist/service-mode.d.ts +1 -1
  186. package/dist/service-mode.d.ts.map +1 -1
  187. package/dist/service-mode.js +64 -1
  188. package/dist/service-mode.js.map +1 -1
  189. package/dist/service-setup-only.d.ts +8 -0
  190. package/dist/service-setup-only.d.ts.map +1 -0
  191. package/dist/service-setup-only.js +37 -0
  192. package/dist/service-setup-only.js.map +1 -0
  193. package/dist/slash-commands.d.ts +21 -0
  194. package/dist/slash-commands.d.ts.map +1 -0
  195. package/dist/slash-commands.js +99 -0
  196. package/dist/slash-commands.js.map +1 -0
  197. package/dist/subagent/index.d.ts +4 -2
  198. package/dist/subagent/index.d.ts.map +1 -1
  199. package/dist/subagent/index.js.map +1 -1
  200. package/dist/summarization-pipeline.d.ts.map +1 -1
  201. package/dist/summarization-pipeline.js +1 -9
  202. package/dist/summarization-pipeline.js.map +1 -1
  203. package/dist/token-counter.d.ts.map +1 -1
  204. package/dist/token-counter.js +11 -4
  205. package/dist/token-counter.js.map +1 -1
  206. package/dist/tool-filter.d.ts.map +1 -1
  207. package/dist/tool-filter.js +10 -6
  208. package/dist/tool-filter.js.map +1 -1
  209. package/dist/tools/admin-tools.d.ts.map +1 -1
  210. package/dist/tools/admin-tools.js +13 -4
  211. package/dist/tools/admin-tools.js.map +1 -1
  212. package/dist/tools/run-command.d.ts.map +1 -1
  213. package/dist/tools/run-command.js +5 -1
  214. package/dist/tools/run-command.js.map +1 -1
  215. package/dist/tools/search-conversation-history.d.ts.map +1 -1
  216. package/dist/tools/search-conversation-history.js +12 -2
  217. package/dist/tools/search-conversation-history.js.map +1 -1
  218. package/dist/tools/todo-tasks.d.ts.map +1 -1
  219. package/dist/tools/todo-tasks.js +77 -5
  220. package/dist/tools/todo-tasks.js.map +1 -1
  221. package/dist/usage-log.d.ts +62 -0
  222. package/dist/usage-log.d.ts.map +1 -0
  223. package/dist/usage-log.js +98 -0
  224. package/dist/usage-log.js.map +1 -0
  225. package/dist/wizard-state.d.ts +13 -0
  226. package/dist/wizard-state.d.ts.map +1 -1
  227. package/dist/wizard-state.js +61 -3
  228. package/dist/wizard-state.js.map +1 -1
  229. package/dist/wizard-support.d.ts.map +1 -1
  230. package/dist/wizard-support.js +27 -1
  231. package/dist/wizard-support.js.map +1 -1
  232. package/dist/workflow-engine.d.ts +40 -1
  233. package/dist/workflow-engine.d.ts.map +1 -1
  234. package/dist/workflow-engine.js +753 -93
  235. package/dist/workflow-engine.js.map +1 -1
  236. package/package.json +2 -2
@@ -36,6 +36,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.mapServerBotsToAgents = mapServerBotsToAgents;
40
+ exports.isServerSeedAgentId = isServerSeedAgentId;
41
+ exports.selectPreferredActiveAgentId = selectPreferredActiveAgentId;
42
+ exports.normalizeLocalModeAgents = normalizeLocalModeAgents;
39
43
  exports.startCommand = startCommand;
40
44
  const path = __importStar(require("path"));
41
45
  const fs = __importStar(require("fs"));
@@ -45,14 +49,18 @@ const chalk_1 = __importDefault(require("chalk"));
45
49
  const config_1 = require("../config");
46
50
  const mqtt_client_1 = require("../mqtt-client");
47
51
  const bot_manager_1 = require("../bot-manager");
52
+ const mqtt_data_relay_1 = require("../mqtt-data-relay");
48
53
  const service_mode_1 = require("../service-mode");
49
54
  const index_1 = require("../providers/index");
50
55
  const sync_cli_config_1 = require("../mcp/sync-cli-config");
51
56
  const agent_config_1 = require("../agent-config");
57
+ const service_setup_only_1 = require("../service-setup-only");
52
58
  // refreshOAuthToken removed — token refresh now handled per-request in message-loop.ts
53
59
  const local_db_1 = require("../local-db");
54
60
  const local_server_1 = require("../local-server");
55
61
  const data = __importStar(require("../local-data"));
62
+ const chat_sync_1 = require("../chat-sync");
63
+ const pool_1 = require("./pool");
56
64
  const AUTH_SESSION_KEY = 'auth.session';
57
65
  const DESKTOP_PREFS_KEY = 'desktop.preferences';
58
66
  const WIZARD_PROFILE_KEY = 'wizard.profile';
@@ -140,7 +148,7 @@ function readLastMaintenanceRestartAtMs() {
140
148
  function markMaintenanceRestartAt(now) {
141
149
  data.setSetting(LAST_MAINTENANCE_RESTART_KEY, now.toISOString());
142
150
  }
143
- function shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestartAtMs, botManager) {
151
+ function shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestartAtMs, botManager, processStartAtMs) {
144
152
  const hour = now.getHours();
145
153
  if (hour < 1 || hour >= 4)
146
154
  return false;
@@ -150,7 +158,280 @@ function shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestar
150
158
  return false;
151
159
  if (lastMaintenanceRestartAtMs && (now.getTime() - lastMaintenanceRestartAtMs) < MIN_MAINTENANCE_RESTART_INTERVAL_MS)
152
160
  return false;
153
- return (now.getTime() - lastInteractionAtMs) >= AGENT_IDLE_RESTART_MS;
161
+ // Count idle from the most recent of (process start, last interaction). Without this
162
+ // floor, a stale `agent.last_interaction_at` value in local.db would make the timer
163
+ // fire ~5 minutes after a cold start instead of after a real idle window.
164
+ const idleReference = Math.max(lastInteractionAtMs, processStartAtMs);
165
+ return (now.getTime() - idleReference) >= AGENT_IDLE_RESTART_MS;
166
+ }
167
+ function isAgentPermissionMode(value) {
168
+ return value === 'autopilot' || value === 'approve-destructive' || value === 'approve-all';
169
+ }
170
+ function toManagedAgentConfigName(agent) {
171
+ const safeName = (agent.name || 'agent')
172
+ .toLowerCase()
173
+ .replace(/[^a-z0-9-_]+/g, '-')
174
+ .replace(/^-+|-+$/g, '')
175
+ .slice(0, 24) || 'agent';
176
+ const safeId = (agent.id || 'bot')
177
+ .toLowerCase()
178
+ .replace(/[^a-z0-9]/g, '')
179
+ .slice(0, 12) || 'bot';
180
+ return `managed-${safeName}-${safeId}`;
181
+ }
182
+ function mapServerBotsToAgents(serverBots, existingAgents = []) {
183
+ const existingById = new Map(existingAgents.map((agent) => [agent.id, agent]));
184
+ const mappedById = new Map(serverBots
185
+ .filter((bot) => !!bot?.id && !!bot?.name)
186
+ .map((bot) => {
187
+ const existing = existingById.get(bot.id);
188
+ return [
189
+ bot.id,
190
+ {
191
+ id: bot.id,
192
+ name: bot.name,
193
+ projectDir: existing?.projectDir || '.',
194
+ provider: bot.llmProvider || existing?.provider || bot.runtimeProvider || 'openai',
195
+ model: bot.llmModel || existing?.model || bot.runtimeModel || 'claude-opus-4-6',
196
+ runtimeProvider: bot.runtimeProvider || existing?.runtimeProvider || undefined,
197
+ runtimeModel: bot.runtimeModel || existing?.runtimeModel || undefined,
198
+ accessMode: bot.accessMode || existing?.accessMode || undefined,
199
+ enabledTools: Array.isArray(bot.tools) ? bot.tools : existing?.enabledTools,
200
+ enabledMcpTools: Array.isArray(bot.enabledMcpTools) ? bot.enabledMcpTools : existing?.enabledMcpTools,
201
+ permissionMode: isAgentPermissionMode(bot.permissionMode)
202
+ ? bot.permissionMode
203
+ : (existing?.permissionMode || 'autopilot'),
204
+ systemPrompt: existing?.systemPrompt,
205
+ agentDescription: existing?.agentDescription,
206
+ createdAt: existing?.createdAt || new Date().toISOString(),
207
+ },
208
+ ];
209
+ }));
210
+ const merged = [];
211
+ for (const existing of existingAgents) {
212
+ const mapped = mappedById.get(existing.id);
213
+ merged.push(mapped || existing);
214
+ if (mapped)
215
+ mappedById.delete(existing.id);
216
+ }
217
+ for (const mapped of mappedById.values()) {
218
+ merged.push(mapped);
219
+ }
220
+ return merged;
221
+ }
222
+ function isServerSeedAgentId(agentId) {
223
+ return typeof agentId === 'string' && /^seed-bot-/i.test(agentId);
224
+ }
225
+ function selectPreferredActiveAgentId(agents, providers, preferredId) {
226
+ if (!Array.isArray(agents) || agents.length === 0)
227
+ return null;
228
+ const providerIds = new Set((providers || []).map((provider) => provider.id));
229
+ const isRunnable = (agent) => providerIds.has(agent.runtimeProvider || agent.provider);
230
+ const preferred = preferredId ? agents.find((agent) => agent.id === preferredId) : undefined;
231
+ if (preferred && isRunnable(preferred))
232
+ return preferred.id;
233
+ const firstRunnable = agents.find(isRunnable);
234
+ if (firstRunnable)
235
+ return firstRunnable.id;
236
+ return preferred?.id || agents[0]?.id || null;
237
+ }
238
+ function normalizeLocalModeAgents(config) {
239
+ const existingAgents = Array.isArray(config.agents) ? config.agents : [];
240
+ const filteredAgents = existingAgents.filter((agent) => !isServerSeedAgentId(agent.id));
241
+ const removedCount = existingAgents.length - filteredAgents.length;
242
+ const preferredId = isServerSeedAgentId(config.activeAgentId) ? null : config.activeAgentId;
243
+ const selectedActiveAgentId = selectPreferredActiveAgentId(filteredAgents, config.providers, preferredId);
244
+ let changed = false;
245
+ if (removedCount > 0) {
246
+ config.agents = filteredAgents;
247
+ changed = true;
248
+ }
249
+ if ((selectedActiveAgentId || null) !== (config.activeAgentId || null)) {
250
+ config.activeAgentId = selectedActiveAgentId || undefined;
251
+ changed = true;
252
+ }
253
+ return { changed, removedCount };
254
+ }
255
+ function syncConfiguredAgentsToLocalConfigs(agents) {
256
+ if (!Array.isArray(agents) || agents.length === 0)
257
+ return;
258
+ let synced = 0;
259
+ for (const agent of agents) {
260
+ if (!agent?.id)
261
+ continue;
262
+ const configName = toManagedAgentConfigName(agent);
263
+ (0, agent_config_1.saveAgentConfig)(configName, {
264
+ name: agent.name || agent.id,
265
+ botId: agent.id,
266
+ provider: agent.provider,
267
+ runtimeProvider: agent.runtimeProvider,
268
+ model: agent.model,
269
+ runtimeModel: agent.runtimeModel,
270
+ accessMode: agent.accessMode,
271
+ workspace: agent.projectDir || '.',
272
+ permissionMode: agent.permissionMode,
273
+ enabledTools: agent.enabledTools,
274
+ enabledMcpTools: agent.enabledMcpTools,
275
+ systemPrompt: agent.systemPrompt || agent.agentDescription,
276
+ createdAt: agent.createdAt || new Date().toISOString(),
277
+ });
278
+ synced++;
279
+ }
280
+ if (synced > 0) {
281
+ console.log(chalk_1.default.gray(` Synced ${synced} managed agent config(s) to ~/.funolio/agents/`));
282
+ }
283
+ }
284
+ async function fetchMcpInstallations(apiUrl, authToken) {
285
+ const res = await fetch(`${apiUrl}/api/v1/bot/mcp-installations`, {
286
+ headers: { Authorization: `Bearer ${authToken}` },
287
+ });
288
+ if (!res.ok) {
289
+ const text = await res.text().catch(() => '');
290
+ throw new Error(`mcp-installations ${res.status}: ${text}`);
291
+ }
292
+ const data = (await res.json());
293
+ return Array.isArray(data.installations) ? data.installations : [];
294
+ }
295
+ /**
296
+ * Sync approved MCP installations from the cloud into the local MCP manager.
297
+ *
298
+ * Flow:
299
+ * 1. Fetch installations from /api/v1/bot/mcp-installations
300
+ * 2. For each "ready" installation, ensure the MCP is launched with the
301
+ * current env vars (Google refresh token in particular rotates).
302
+ * 3. Skip installations with status "needs_reauth" or "unknown_server";
303
+ * log why.
304
+ *
305
+ * Idempotent: MCPManager.installAndLaunch short-circuits when a server is
306
+ * already running. Env-var changes trigger reloadIntegrationServers for the
307
+ * Google family so the subprocess restarts with fresh tokens.
308
+ */
309
+ async function syncCloudMcpInstallations(opts) {
310
+ const { apiUrl, authToken, mcpManager } = opts;
311
+ let installations;
312
+ try {
313
+ installations = await fetchMcpInstallations(apiUrl, authToken);
314
+ }
315
+ catch (err) {
316
+ throw new Error(`fetch failed: ${err?.message || err}`);
317
+ }
318
+ if (installations.length === 0) {
319
+ if (process.env.FUNOLIO_AGENT_DEBUG === 'true') {
320
+ console.log(chalk_1.default.gray(' [mcp-sync] no approved MCP installations for this user'));
321
+ }
322
+ return;
323
+ }
324
+ const { MCP_REGISTRY } = await Promise.resolve().then(() => __importStar(require('../mcp/registry-shared')));
325
+ let launched = 0;
326
+ let skipped = 0;
327
+ let googleRefreshed = false;
328
+ for (const inst of installations) {
329
+ if (inst.status !== 'ready') {
330
+ skipped++;
331
+ if (process.env.FUNOLIO_AGENT_DEBUG === 'true') {
332
+ console.log(chalk_1.default.gray(` [mcp-sync] skipping ${inst.serverId}: ${inst.status}${inst.error ? ` (${inst.error})` : ''}`));
333
+ }
334
+ continue;
335
+ }
336
+ const entry = MCP_REGISTRY.find((e) => e.id === inst.serverId);
337
+ if (!entry) {
338
+ skipped++;
339
+ continue;
340
+ }
341
+ const runningInfo = mcpManager.getServerInfo(inst.serverId);
342
+ if (runningInfo) {
343
+ // Already running. If this is a Google-family MCP we may still want
344
+ // to restart it so the subprocess picks up a rotated refresh token.
345
+ // The batch-restart below handles that.
346
+ continue;
347
+ }
348
+ try {
349
+ await mcpManager.installAndLaunch(entry, inst.envVars, inst.alwaysOn);
350
+ launched++;
351
+ }
352
+ catch (err) {
353
+ console.error(chalk_1.default.yellow(` [mcp-sync] failed to launch ${inst.serverId}: ${err?.message || err}`));
354
+ }
355
+ }
356
+ // If any already-running Google MCP has a changed refresh token, restart
357
+ // the Google family so they pick up the fresh credential. The manager's
358
+ // existing reloadIntegrationServers does the heavy lifting.
359
+ try {
360
+ const hasGoogleReady = installations.some((i) => i.status === 'ready' && /^google-|^gmail$/.test(i.serverId));
361
+ if (hasGoogleReady) {
362
+ const running = mcpManager.getRunningServers();
363
+ const googleRunning = running.some((id) => /^google-|^gmail$/.test(id));
364
+ if (googleRunning) {
365
+ await mcpManager.reloadIntegrationServers('google');
366
+ googleRefreshed = true;
367
+ }
368
+ }
369
+ }
370
+ catch (err) {
371
+ console.error(chalk_1.default.yellow(` [mcp-sync] google reload failed: ${err?.message || err}`));
372
+ }
373
+ if (launched > 0 || googleRefreshed) {
374
+ console.log(chalk_1.default.green(` ✓ MCP sync: ${launched} launched, ${skipped} skipped${googleRefreshed ? ', google refreshed' : ''}`));
375
+ }
376
+ }
377
+ async function fetchPoolSubscriptions(apiUrl, authToken) {
378
+ const res = await fetch(`${apiUrl}/api/v1/bot/pool-subscriptions`, {
379
+ headers: { Authorization: `Bearer ${authToken}` },
380
+ });
381
+ if (!res.ok) {
382
+ const text = await res.text().catch(() => '');
383
+ throw new Error(`pool-subscriptions ${res.status}: ${text}`);
384
+ }
385
+ const data = (await res.json());
386
+ return Array.isArray(data.subscriptions) ? data.subscriptions : [];
387
+ }
388
+ async function setupPoolSubscriptions(opts) {
389
+ const { apiUrl, authToken, mqttClient, config } = opts;
390
+ let subs = [];
391
+ try {
392
+ subs = await fetchPoolSubscriptions(apiUrl, authToken);
393
+ }
394
+ catch (err) {
395
+ console.error(chalk_1.default.yellow(`⚠ Failed to fetch pool subscriptions: ${err?.message || err}`));
396
+ return;
397
+ }
398
+ if (subs.length === 0) {
399
+ console.log(chalk_1.default.gray(' No team pool provider instances for this user.'));
400
+ return;
401
+ }
402
+ console.log(chalk_1.default.blue(` Subscribing to ${subs.length} team-pool request topic(s)...`));
403
+ const publisher = {
404
+ publish: (topic, payload, o) => mqttClient.publish(topic, payload, { qos: (o?.qos ?? 1) }),
405
+ };
406
+ for (const sub of subs) {
407
+ const { teamId, instanceId, provider: providerName } = sub;
408
+ const providerConfig = (config.providers || []).find((p) => p.id === providerName);
409
+ if (!providerConfig) {
410
+ console.log(chalk_1.default.yellow(` ⚠ No local provider config for "${providerName}" (teamId=${teamId.slice(0, 8)}); skipping`));
411
+ continue;
412
+ }
413
+ const topic = `funolio/team/${teamId}/provider/${instanceId}/request/+`;
414
+ mqttClient.subscribeTopic(topic, async (_t, payload) => {
415
+ let request;
416
+ try {
417
+ request = JSON.parse(payload.toString());
418
+ }
419
+ catch {
420
+ console.error(chalk_1.default.red('Pool request: failed to parse payload'));
421
+ return;
422
+ }
423
+ console.log(chalk_1.default.blue(`📨 Pool request ${String(request.requestId).slice(0, 8)}... team=${teamId.slice(0, 8)} model=${request.model}`));
424
+ await (0, pool_1.handlePoolRequest)({
425
+ teamId,
426
+ instanceId,
427
+ providerName,
428
+ providerConfig,
429
+ request,
430
+ publisher,
431
+ });
432
+ }, { qos: 1 });
433
+ console.log(chalk_1.default.gray(` • ${topic}`));
434
+ }
154
435
  }
155
436
  function parseJson(value) {
156
437
  if (!value)
@@ -171,10 +452,25 @@ function loadDbRuntimeConfig() {
171
452
  const botRows = data.listAgentProfiles();
172
453
  const projects = data.listProjects({ includeArchived: true });
173
454
  const firstProviderId = providerRows[0]?.provider_id;
174
- const defaultBot = botRows.find((bot) => bot.is_default === 1) || botRows[0];
455
+ const providerByConnectionId = new Map(providerRows.map((row) => [row.id, row]));
456
+ const providerByProviderId = new Map(providerRows.map((row) => [row.provider_id, row]));
457
+ const resolveBotProviderConnection = (bot) => {
458
+ if (bot.provider_connection_id) {
459
+ const direct = providerByConnectionId.get(bot.provider_connection_id);
460
+ if (direct)
461
+ return direct;
462
+ }
463
+ return providerByProviderId.get(bot.provider);
464
+ };
465
+ const isRunnableBot = (bot) => !!resolveBotProviderConnection(bot);
466
+ const defaultBot = botRows.find((bot) => bot.is_default === 1 && isRunnableBot(bot))
467
+ || botRows.find((bot) => bot.is_active === 1 && isRunnableBot(bot))
468
+ || botRows.find(isRunnableBot)
469
+ || botRows.find((bot) => bot.is_default === 1)
470
+ || botRows[0];
175
471
  const providers = providerRows.map((row) => ({
176
472
  id: row.provider_id,
177
- authType: row.auth_type === 'oauth' ? 'oauth' : 'apiKey',
473
+ authType: row.auth_type === 'oauth' ? 'oauth' : row.auth_type === 'cli' ? 'cli' : 'apiKey',
178
474
  apiKey: row.api_key_enc || undefined,
179
475
  oauthToken: row.oauth_token || undefined,
180
476
  oauthRefreshToken: row.oauth_refresh_token || undefined,
@@ -183,13 +479,16 @@ function loadDbRuntimeConfig() {
183
479
  label: row.label || undefined,
184
480
  }));
185
481
  const agents = botRows.map((row) => {
482
+ const resolvedConnection = resolveBotProviderConnection(row);
483
+ const resolvedProviderId = resolvedConnection?.provider_id || row.provider;
484
+ const resolvedModel = row.model || resolvedConnection?.default_model || config_1.DEFAULT_MODELS[resolvedProviderId] || '';
186
485
  const projectDir = projects.find((project) => project.bot_ids.includes(row.id) && project.folder)?.folder || '.';
187
486
  return {
188
487
  id: row.id,
189
488
  name: row.name,
190
489
  projectDir,
191
- provider: row.provider,
192
- model: row.model,
490
+ provider: resolvedProviderId,
491
+ model: resolvedModel,
193
492
  enabledTools: parseJson(row.enabled_builtin_tools_json),
194
493
  enabledMcpTools: parseJson(row.enabled_mcp_tools_json),
195
494
  permissionMode: row.permission_mode || 'autopilot',
@@ -204,7 +503,7 @@ function loadDbRuntimeConfig() {
204
503
  return {
205
504
  auth,
206
505
  providers,
207
- defaultProvider: defaultBot?.provider || firstProviderId,
506
+ defaultProvider: (defaultBot ? (resolveBotProviderConnection(defaultBot)?.provider_id || defaultBot.provider) : undefined) || firstProviderId,
208
507
  agents,
209
508
  activeAgentId: defaultBot?.id,
210
509
  autostart: !!desktopPrefs?.launchOnStartup,
@@ -253,10 +552,12 @@ async function startCommand(projectDir, options) {
253
552
  }
254
553
  serviceLockPath = lock.lockPath;
255
554
  }
256
- const legacyConfig = (0, config_1.migrateConfig)((0, config_1.loadConfig)());
257
- const dbConfig = loadDbRuntimeConfig();
258
- const useDbRuntime = !!dbConfig;
259
- const config = dbConfig || legacyConfig;
555
+ const config = (0, config_1.migrateConfig)((0, config_1.loadConfig)());
556
+ const useDbRuntime = (0, config_1.hasDbConfig)();
557
+ if (options.mode === 'service' && config.connectionMode === 'local') {
558
+ process.env.LOCAL_FIRST_ENABLED = 'true';
559
+ process.env.FUNOLIO_RUN_CONTEXT = process.env.FUNOLIO_RUN_CONTEXT || 'service';
560
+ }
260
561
  const authConfig = config.auth || null;
261
562
  const hasAuth = !!authConfig;
262
563
  const authExpired = hasAuth
@@ -264,9 +565,7 @@ async function startCommand(projectDir, options) {
264
565
  : true;
265
566
  const runSetupOnly = async (reason) => {
266
567
  if (isServiceMode) {
267
- console.log(chalk_1.default.yellow(`Setup-only mode: ${reason}`));
268
- (0, service_mode_1.emitEvent)('status', { status: 'setup_only', reason });
269
- await new Promise(() => { });
568
+ await (0, service_setup_only_1.waitForServiceSetup)(reason, service_mode_1.emitEvent, options.setupOnlySignal);
270
569
  return true;
271
570
  }
272
571
  return false;
@@ -275,6 +574,15 @@ async function startCommand(projectDir, options) {
275
574
  let localServerStarted = false;
276
575
  const runtimeConnectionMode = config.connectionMode === 'server' ? 'server' : 'local';
277
576
  const runtimeServerBaseUrl = typeof config.serverBaseUrl === 'string' ? config.serverBaseUrl.trim() : '';
577
+ if (runtimeConnectionMode === 'local') {
578
+ const localNormalization = normalizeLocalModeAgents(config);
579
+ if (localNormalization.changed) {
580
+ (0, config_1.saveConfig)(config);
581
+ if (localNormalization.removedCount > 0) {
582
+ console.log(chalk_1.default.gray(` Removed ${localNormalization.removedCount} server-only bot profile(s) from local mode`));
583
+ }
584
+ }
585
+ }
278
586
  process.env.FUNOLIO_CONNECTION_MODE = runtimeConnectionMode;
279
587
  if (runtimeServerBaseUrl) {
280
588
  process.env.FUNOLIO_SERVER_BASE_URL = runtimeServerBaseUrl;
@@ -293,6 +601,9 @@ async function startCommand(projectDir, options) {
293
601
  const dbInfo = (0, local_db_1.initLocalDb)();
294
602
  console.log(chalk_1.default.green(`✓ Local DB ready: ${dbInfo.path}`));
295
603
  console.log(chalk_1.default.gray(` WAL mode: ${dbInfo.walMode}, tables: ${dbInfo.tableCount}, write: ${dbInfo.testWrite}, read: ${dbInfo.testRead}`));
604
+ if (dbInfo.claudeSessionCounterSync?.updated) {
605
+ console.log(chalk_1.default.gray(` Claude session counter advanced from ${dbInfo.claudeSessionCounterSync.dbValueBefore} to ${dbInfo.claudeSessionCounterSync.dbValueAfter} to match existing ~/.claude Funolio sessions`));
606
+ }
296
607
  cleanupExpiredMessageActivityRows();
297
608
  if (isServiceMode) {
298
609
  (0, service_mode_1.emitEvent)('local_db', { status: 'ready', path: dbInfo.path, walMode: dbInfo.walMode });
@@ -379,55 +690,69 @@ async function startCommand(projectDir, options) {
379
690
  process.exit(1);
380
691
  }
381
692
  }
382
- // Re-sync provider credentials from server (picks up web re-auth tokens)
383
- if (config.providers?.some(p => p.authType === 'oauth')) {
384
- try {
385
- const res = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/config`, {
386
- headers: { Authorization: `Bearer ${auth.token}` },
387
- });
388
- if (res.ok) {
389
- const serverConfig = await res.json();
390
- if (serverConfig.providers) {
391
- let updated = false;
392
- for (const sp of serverConfig.providers) {
393
- if (sp.connectionType !== 'oauth' || !sp.access_token)
394
- continue;
395
- const local = config.providers.find(p => p.id === sp.id);
396
- if (!local || local.authType !== 'oauth')
397
- continue;
398
- // Only update if server has a different (newer) token
399
- if (local.oauthToken !== sp.access_token) {
400
- local.oauthToken = sp.access_token;
401
- local.oauthRefreshToken = sp.refresh_token;
402
- local.oauthExpiresAt = sp.expires_at;
403
- updated = true;
404
- console.log(chalk_1.default.green(`✓ Updated ${sp.id} OAuth token from server`));
405
- }
693
+ // Re-sync providers from server (picks up web re-auth tokens).
694
+ // In local mode, keep the local bot roster authoritative and do not import
695
+ // the server seed-bot catalog into the desktop agent list.
696
+ try {
697
+ const res = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/config`, {
698
+ headers: { Authorization: `Bearer ${auth.token}` },
699
+ });
700
+ if (res.ok) {
701
+ const serverConfig = await res.json();
702
+ let updated = false;
703
+ if (serverConfig.providers && config.providers?.some(p => p.authType === 'oauth')) {
704
+ for (const sp of serverConfig.providers) {
705
+ if (sp.connectionType !== 'oauth' || !sp.access_token)
706
+ continue;
707
+ const local = config.providers.find(p => p.id === sp.id);
708
+ if (!local || local.authType !== 'oauth')
709
+ continue;
710
+ // Only update if server has a different (newer) token
711
+ if (local.oauthToken !== sp.access_token) {
712
+ local.oauthToken = sp.access_token;
713
+ local.oauthRefreshToken = sp.refresh_token;
714
+ local.oauthExpiresAt = sp.expires_at;
715
+ updated = true;
716
+ console.log(chalk_1.default.green(`✓ Updated ${sp.id} OAuth token from server`));
406
717
  }
407
- if (updated) {
408
- (0, config_1.saveConfig)(config);
409
- // Also update agent config files on disk
410
- for (const p of config.providers) {
411
- if (p.authType !== 'oauth' || !p.oauthToken)
412
- continue;
413
- const { listAgents, loadAgentConfig: loadAC, saveAgentConfig: saveAC } = await Promise.resolve().then(() => __importStar(require('../agent-config')));
414
- for (const agentName of listAgents()) {
415
- const ac = loadAC(agentName);
416
- if (ac && ac.provider === p.id && ac.oauthToken) {
417
- ac.oauthToken = p.oauthToken;
418
- ac.oauthRefreshToken = p.oauthRefreshToken;
419
- ac.oauthExpiresAt = p.oauthExpiresAt;
420
- saveAC(agentName, ac);
421
- }
422
- }
718
+ }
719
+ }
720
+ if (runtimeConnectionMode === 'server' && Array.isArray(serverConfig.bots) && serverConfig.bots.length > 0) {
721
+ const nextAgents = mapServerBotsToAgents(serverConfig.bots, config.agents || []);
722
+ const sameAgents = JSON.stringify(config.agents || []) === JSON.stringify(nextAgents);
723
+ if (!sameAgents) {
724
+ config.agents = nextAgents;
725
+ updated = true;
726
+ console.log(chalk_1.default.green(`✓ Synced ${nextAgents.length} bot profile(s) from server`));
727
+ }
728
+ const selectedActiveAgentId = selectPreferredActiveAgentId(nextAgents, config.providers, config.activeAgentId);
729
+ if (selectedActiveAgentId && selectedActiveAgentId !== config.activeAgentId) {
730
+ config.activeAgentId = selectedActiveAgentId;
731
+ updated = true;
732
+ }
733
+ }
734
+ if (updated) {
735
+ (0, config_1.saveConfig)(config);
736
+ // Also update agent config files on disk for OAuth providers
737
+ for (const p of config.providers || []) {
738
+ if (p.authType !== 'oauth' || !p.oauthToken)
739
+ continue;
740
+ const { listAgents, loadAgentConfig: loadAC, saveAgentConfig: saveAC } = await Promise.resolve().then(() => __importStar(require('../agent-config')));
741
+ for (const agentName of listAgents()) {
742
+ const ac = loadAC(agentName);
743
+ if (ac && ac.provider === p.id && ac.oauthToken) {
744
+ ac.oauthToken = p.oauthToken;
745
+ ac.oauthRefreshToken = p.oauthRefreshToken;
746
+ ac.oauthExpiresAt = p.oauthExpiresAt;
747
+ saveAC(agentName, ac);
423
748
  }
424
749
  }
425
750
  }
426
751
  }
427
752
  }
428
- catch (err) {
429
- console.log(chalk_1.default.gray(` Server config re-sync skipped: ${err.message}`));
430
- }
753
+ }
754
+ catch (err) {
755
+ console.log(chalk_1.default.gray(` Server config re-sync skipped: ${err.message}`));
431
756
  }
432
757
  // Load agent-specific config if --agent was specified
433
758
  const agentLocalConfig = options.agent ? (0, agent_config_1.loadAgentConfig)(options.agent) : null;
@@ -552,8 +877,10 @@ async function startCommand(projectDir, options) {
552
877
  // Enforce strict subscription routing semantics.
553
878
  if (effectiveAccessMode === 'openai_subscription') {
554
879
  apiKey = undefined;
555
- if (provider === 'codex-cli')
556
- provider = 'openai';
880
+ oauthToken = undefined;
881
+ // Preserve codex-cli when it was explicitly selected. The CLI manages its
882
+ // own auth/session state and should not be rewritten into the raw OpenAI
883
+ // API path during local-agent startup.
557
884
  }
558
885
  if (effectiveAccessMode === 'anthropic_subscription') {
559
886
  provider = 'claude-cli';
@@ -563,10 +890,17 @@ async function startCommand(projectDir, options) {
563
890
  model = 'claude-code';
564
891
  }
565
892
  }
566
- // Auto-detect Claude Code CLI if no API key/token and no provider explicitly set.
893
+ // Auto-detect Claude Code CLI only when the provider was not explicitly chosen.
894
+ // Otherwise we can accidentally rewrite an explicit Codex/OpenAI bot selection
895
+ // into Claude just because no API key was passed on the outer config object.
896
+ const providerWasExplicit = !!(options.provider ||
897
+ agentLocalConfig?.runtimeProvider ||
898
+ activeAgentConfig?.runtimeProvider ||
899
+ agentLocalConfig?.provider ||
900
+ activeAgentConfig?.provider);
567
901
  // OAuth tokens from claude.ai can't be used with the Anthropic API directly,
568
902
  // so we prefer claude-cli when Claude Code is installed.
569
- if (!apiKey && !oauthToken && !options.provider && !agentLocalConfig?.provider && provider !== 'claude-cli') {
903
+ if (!apiKey && !oauthToken && !providerWasExplicit && provider !== 'claude-cli') {
570
904
  const claudeVersion = detectClaudeCli();
571
905
  if (claudeVersion) {
572
906
  console.log(chalk_1.default.green(`✓ Claude Code detected (${claudeVersion}), using your subscription`));
@@ -642,6 +976,9 @@ async function startCommand(projectDir, options) {
642
976
  const dbInfo = (0, local_db_1.initLocalDb)();
643
977
  console.log(chalk_1.default.green(`✓ Local DB ready: ${dbInfo.path}`));
644
978
  console.log(chalk_1.default.gray(` WAL mode: ${dbInfo.walMode}, tables: ${dbInfo.tableCount}, write: ${dbInfo.testWrite}, read: ${dbInfo.testRead}`));
979
+ if (dbInfo.claudeSessionCounterSync?.updated) {
980
+ console.log(chalk_1.default.gray(` Claude session counter advanced from ${dbInfo.claudeSessionCounterSync.dbValueBefore} to ${dbInfo.claudeSessionCounterSync.dbValueAfter} to match existing ~/.claude Funolio sessions`));
981
+ }
645
982
  cleanupExpiredMessageActivityRows();
646
983
  if (isServiceMode) {
647
984
  (0, service_mode_1.emitEvent)('local_db', { status: 'ready', path: dbInfo.path, walMode: dbInfo.walMode });
@@ -692,6 +1029,10 @@ async function startCommand(projectDir, options) {
692
1029
  authToken: auth.token,
693
1030
  refreshUrl: `${config_1.FUNOLIO_API_URL}/api/v1/agent/auth/refresh`,
694
1031
  });
1032
+ (0, chat_sync_1.configureChatSyncPublisher)({
1033
+ userId: auth.userId,
1034
+ publish: (topic, payload, opts) => mqttClient.publish(topic, payload, opts),
1035
+ });
695
1036
  // Create and auto-launch MCP manager for persistent tool discovery
696
1037
  const { MCPManager } = await Promise.resolve().then(() => __importStar(require('../mcp/manager')));
697
1038
  const mcpManager = new MCPManager();
@@ -720,6 +1061,7 @@ async function startCommand(projectDir, options) {
720
1061
  });
721
1062
  let maintenanceTimer = null;
722
1063
  let maintenanceRestartPending = false;
1064
+ const processStartAtMs = Date.now();
723
1065
  // Handle shutdown
724
1066
  const shutdown = async () => {
725
1067
  console.log(chalk_1.default.yellow('\nShutting down...'));
@@ -832,6 +1174,9 @@ async function startCommand(projectDir, options) {
832
1174
  console.log(chalk_1.default.gray(' This terminal can stay open or run in background.\n'));
833
1175
  // Start the active agent loop (driven by agents[] + activeAgentId)
834
1176
  await botManager.startActive();
1177
+ // Materialize configured server bots into local agent files so explicit
1178
+ // bot routing can resolve botId -> loop mappings after restarts.
1179
+ syncConfiguredAgentsToLocalConfigs(config.agents);
835
1180
  // Start all additional bot loops from ~/.funolio/agents/ configs
836
1181
  await botManager.startAllBots();
837
1182
  try {
@@ -849,7 +1194,7 @@ async function startCommand(projectDir, options) {
849
1194
  const now = new Date();
850
1195
  const lastInteractionAtMs = readLastInteractionAtMs();
851
1196
  const lastMaintenanceRestartAtMs = readLastMaintenanceRestartAtMs();
852
- if (!shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestartAtMs, botManager))
1197
+ if (!shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestartAtMs, botManager, processStartAtMs))
853
1198
  return;
854
1199
  maintenanceRestartPending = true;
855
1200
  markMaintenanceRestartAt(now);
@@ -868,7 +1213,10 @@ async function startCommand(projectDir, options) {
868
1213
  if (isServiceMode) {
869
1214
  (0, service_mode_1.emitEvent)('agent_restart', { reason });
870
1215
  }
871
- setTimeout(() => process.exit(0), 1500);
1216
+ // Exit with EX_TEMPFAIL (75) so systemd's `Restart=on-failure` triggers a
1217
+ // restart. `process.exit(0)` is treated as clean and would leave the agent
1218
+ // dead until a human starts it (observed incident: ~2 days of downtime).
1219
+ setTimeout(() => process.exit(75), 1500);
872
1220
  }, MAINTENANCE_INTERVAL_MS);
873
1221
  if (config.agents && config.agents.length > 1) {
874
1222
  console.log(chalk_1.default.blue(` ${config.agents.length} agent(s) configured (active: ${activeAgentConfig?.name || 'default'})`));
@@ -899,6 +1247,71 @@ async function startCommand(projectDir, options) {
899
1247
  }
900
1248
  }
901
1249
  console.log(chalk_1.default.gray(' Waiting for commands...\n'));
1250
+ // Subscribe to team-pool request topics for every provider instance this user hosts.
1251
+ // Without this, V2 TeamBot chats routed through the team pool never reach this agent
1252
+ // and time out at 120s. Previously this subscription was only done by the dedicated
1253
+ // `funolio-agent pool provide` command, not by `start --mode service`.
1254
+ setupPoolSubscriptions({
1255
+ apiUrl: config_1.FUNOLIO_API_URL,
1256
+ authToken: auth.token,
1257
+ mqttClient,
1258
+ config,
1259
+ }).catch((err) => {
1260
+ console.error(chalk_1.default.yellow(`⚠ Pool subscription setup failed: ${err?.message || err}`));
1261
+ });
1262
+ // Keep liveness fresh: re-poll /api/v1/bot/pool-subscriptions every 2 minutes.
1263
+ // The GET side-effects a `lastSeenAt = NOW()` update on all of this user's
1264
+ // TeamLlmProviderInstance rows. Without this, the server's V2 completions
1265
+ // route fast-fails with 503 NO_RUNTIME because the staleness check
1266
+ // (>5 min = offline) trips — even while the agent is actively subscribed
1267
+ // to the pool topic.
1268
+ //
1269
+ // The re-poll also picks up any new teams the user joined after the agent
1270
+ // started: fetchPoolSubscriptions returns the up-to-date set, so any newly
1271
+ // added instance gets subscribed on the next tick.
1272
+ const POOL_LIVENESS_INTERVAL_MS = 2 * 60 * 1000;
1273
+ setInterval(() => {
1274
+ fetchPoolSubscriptions(config_1.FUNOLIO_API_URL, auth.token)
1275
+ .then(() => {
1276
+ // Discovery of brand-new team memberships is handled in a later
1277
+ // iteration; for now the GET alone is enough to keep liveness fresh.
1278
+ })
1279
+ .catch((err) => {
1280
+ // Non-fatal — next tick will retry. Don't spam logs.
1281
+ if (process.env.FUNOLIO_AGENT_DEBUG === 'true') {
1282
+ console.error(chalk_1.default.gray(` [pool-liveness] refresh failed: ${err?.message || err}`));
1283
+ }
1284
+ });
1285
+ }, POOL_LIVENESS_INTERVAL_MS).unref();
1286
+ // Sync approved MCP installations from the cloud. The user approves MCPs
1287
+ // via the web UI (stored in UserMcpServer rows server-side), but the
1288
+ // agent's MCP manager only auto-launches from ~/.funolio/mcp-servers.json
1289
+ // which is never populated on VM/remote agent installs. Without this
1290
+ // sync, users who approved Google Workspace/Drive/Gmail/Chat via the web
1291
+ // see `mcp=0` in the tool filter at startup and tool calls fail.
1292
+ syncCloudMcpInstallations({
1293
+ apiUrl: config_1.FUNOLIO_API_URL,
1294
+ authToken: auth.token,
1295
+ mcpManager,
1296
+ }).catch((err) => {
1297
+ console.error(chalk_1.default.yellow(`⚠ MCP installation sync failed: ${err?.message || err}`));
1298
+ });
1299
+ // Re-sync every 10 minutes so newly approved MCPs show up and Google
1300
+ // tokens get refreshed before they expire (GOOGLE_REFRESH_BUFFER_MS on
1301
+ // the server is 5 min; polling at 10 min is well inside the safety
1302
+ // margin of a typical 1-hour access token TTL).
1303
+ const MCP_SYNC_INTERVAL_MS = 10 * 60 * 1000;
1304
+ setInterval(() => {
1305
+ syncCloudMcpInstallations({
1306
+ apiUrl: config_1.FUNOLIO_API_URL,
1307
+ authToken: auth.token,
1308
+ mcpManager,
1309
+ }).catch((err) => {
1310
+ if (process.env.FUNOLIO_AGENT_DEBUG === 'true') {
1311
+ console.error(chalk_1.default.gray(` [mcp-sync] refresh failed: ${err?.message || err}`));
1312
+ }
1313
+ });
1314
+ }, MCP_SYNC_INTERVAL_MS).unref();
902
1315
  // OAuth token refresh is handled per-request in message-loop.ts via ensureFreshToken()
903
1316
  // No background timer needed — this matches the pi-ai/Claude Code CLI pattern of
904
1317
  // refreshing lazily before each API call, avoiding race conditions on single-use refresh tokens
@@ -921,6 +1334,14 @@ async function startCommand(projectDir, options) {
921
1334
  // Start listening for commands — BotManager routes to the right bot
922
1335
  // Also handle MCP marketplace install/uninstall commands
923
1336
  mqttClient.onCommand(async (message) => {
1337
+ // Local-DB data relay (mobile1.txt Phase 1). When the cloud needs to
1338
+ // serve a personal-desktop user's data via mobile/web, it publishes a
1339
+ // data_request command here; we forward to the local HTTP server and
1340
+ // reply on the results topic. Always handle this BEFORE existing
1341
+ // command dispatch since it's its own message type.
1342
+ if (await (0, mqtt_data_relay_1.handleDataRequestMessage)(message, mqttClient)) {
1343
+ return;
1344
+ }
924
1345
  if (message.type === 'command' && message.action === 'mcp_install') {
925
1346
  const { serverId, envVars } = message;
926
1347
  console.log(chalk_1.default.cyan(`📦 Marketplace: installing MCP server "${serverId}"...`));