funolio-agent 1.0.53 → 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 (276) hide show
  1. package/dist/approval.d.ts +1 -6
  2. package/dist/approval.d.ts.map +1 -1
  3. package/dist/approval.js +2 -7
  4. package/dist/approval.js.map +1 -1
  5. package/dist/auth/credential-reader.d.ts.map +1 -1
  6. package/dist/auth/credential-reader.js +4 -3
  7. package/dist/auth/credential-reader.js.map +1 -1
  8. package/dist/auth/token-refresh.d.ts +8 -0
  9. package/dist/auth/token-refresh.d.ts.map +1 -1
  10. package/dist/auth/token-refresh.js +82 -52
  11. package/dist/auth/token-refresh.js.map +1 -1
  12. package/dist/auto-organizer.d.ts.map +1 -1
  13. package/dist/auto-organizer.js +6 -7
  14. package/dist/auto-organizer.js.map +1 -1
  15. package/dist/bench-prefix.d.ts +16 -0
  16. package/dist/bench-prefix.d.ts.map +1 -0
  17. package/dist/bench-prefix.js +25 -0
  18. package/dist/bench-prefix.js.map +1 -0
  19. package/dist/bot-manager.d.ts +5 -1
  20. package/dist/bot-manager.d.ts.map +1 -1
  21. package/dist/bot-manager.js +46 -27
  22. package/dist/bot-manager.js.map +1 -1
  23. package/dist/chat-sync.d.ts +42 -0
  24. package/dist/chat-sync.d.ts.map +1 -0
  25. package/dist/chat-sync.js +95 -0
  26. package/dist/chat-sync.js.map +1 -0
  27. package/dist/clerk-model.d.ts +7 -0
  28. package/dist/clerk-model.d.ts.map +1 -1
  29. package/dist/clerk-model.js +42 -8
  30. package/dist/clerk-model.js.map +1 -1
  31. package/dist/cli-bootstrap-history.d.ts +10 -0
  32. package/dist/cli-bootstrap-history.d.ts.map +1 -0
  33. package/dist/cli-bootstrap-history.js +112 -0
  34. package/dist/cli-bootstrap-history.js.map +1 -0
  35. package/dist/cli-models.d.ts +8 -0
  36. package/dist/cli-models.d.ts.map +1 -0
  37. package/dist/cli-models.js +91 -0
  38. package/dist/cli-models.js.map +1 -0
  39. package/dist/cli-session-epoch.d.ts +13 -3
  40. package/dist/cli-session-epoch.d.ts.map +1 -1
  41. package/dist/cli-session-epoch.js +53 -4
  42. package/dist/cli-session-epoch.js.map +1 -1
  43. package/dist/cli-session-registry.d.ts +35 -0
  44. package/dist/cli-session-registry.d.ts.map +1 -0
  45. package/dist/cli-session-registry.js +177 -0
  46. package/dist/cli-session-registry.js.map +1 -0
  47. package/dist/cli.js +62 -0
  48. package/dist/cli.js.map +1 -1
  49. package/dist/codex-app-server-manager.d.ts +189 -0
  50. package/dist/codex-app-server-manager.d.ts.map +1 -0
  51. package/dist/codex-app-server-manager.js +1468 -0
  52. package/dist/codex-app-server-manager.js.map +1 -0
  53. package/dist/commands/init.d.ts.map +1 -1
  54. package/dist/commands/init.js +8 -30
  55. package/dist/commands/init.js.map +1 -1
  56. package/dist/commands/pool.d.ts +32 -0
  57. package/dist/commands/pool.d.ts.map +1 -1
  58. package/dist/commands/pool.js +145 -66
  59. package/dist/commands/pool.js.map +1 -1
  60. package/dist/commands/setup.d.ts +4 -1
  61. package/dist/commands/setup.d.ts.map +1 -1
  62. package/dist/commands/setup.js +9 -25
  63. package/dist/commands/setup.js.map +1 -1
  64. package/dist/commands/start.d.ts +21 -0
  65. package/dist/commands/start.d.ts.map +1 -1
  66. package/dist/commands/start.js +559 -63
  67. package/dist/commands/start.js.map +1 -1
  68. package/dist/commands/status.d.ts.map +1 -1
  69. package/dist/commands/status.js +5 -2
  70. package/dist/commands/status.js.map +1 -1
  71. package/dist/completion-marker.d.ts +7 -0
  72. package/dist/completion-marker.d.ts.map +1 -0
  73. package/dist/completion-marker.js +28 -0
  74. package/dist/completion-marker.js.map +1 -0
  75. package/dist/config.d.ts +7 -2
  76. package/dist/config.d.ts.map +1 -1
  77. package/dist/config.js +184 -60
  78. package/dist/config.js.map +1 -1
  79. package/dist/context-window.d.ts +37 -1
  80. package/dist/context-window.d.ts.map +1 -1
  81. package/dist/context-window.js +210 -17
  82. package/dist/context-window.js.map +1 -1
  83. package/dist/live-activity.d.ts +31 -0
  84. package/dist/live-activity.d.ts.map +1 -0
  85. package/dist/live-activity.js +36 -0
  86. package/dist/live-activity.js.map +1 -0
  87. package/dist/local-chat-execution.d.ts +114 -0
  88. package/dist/local-chat-execution.d.ts.map +1 -0
  89. package/dist/local-chat-execution.js +349 -0
  90. package/dist/local-chat-execution.js.map +1 -0
  91. package/dist/local-cli-pty-manager.d.ts +186 -0
  92. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  93. package/dist/local-cli-pty-manager.js +2581 -164
  94. package/dist/local-cli-pty-manager.js.map +1 -1
  95. package/dist/local-conversation-gateway.d.ts +110 -0
  96. package/dist/local-conversation-gateway.d.ts.map +1 -0
  97. package/dist/local-conversation-gateway.js +175 -0
  98. package/dist/local-conversation-gateway.js.map +1 -0
  99. package/dist/local-data.d.ts +276 -5
  100. package/dist/local-data.d.ts.map +1 -1
  101. package/dist/local-data.js +1201 -86
  102. package/dist/local-data.js.map +1 -1
  103. package/dist/local-db.d.ts +6 -0
  104. package/dist/local-db.d.ts.map +1 -1
  105. package/dist/local-db.js +428 -2
  106. package/dist/local-db.js.map +1 -1
  107. package/dist/local-funnel.d.ts.map +1 -1
  108. package/dist/local-funnel.js +6 -5
  109. package/dist/local-funnel.js.map +1 -1
  110. package/dist/local-server.d.ts +55 -0
  111. package/dist/local-server.d.ts.map +1 -1
  112. package/dist/local-server.js +3281 -441
  113. package/dist/local-server.js.map +1 -1
  114. package/dist/managed-process-registry.d.ts +59 -0
  115. package/dist/managed-process-registry.d.ts.map +1 -0
  116. package/dist/managed-process-registry.js +390 -0
  117. package/dist/managed-process-registry.js.map +1 -0
  118. package/dist/mcp/claude-config-writer.d.ts +5 -5
  119. package/dist/mcp/claude-config-writer.d.ts.map +1 -1
  120. package/dist/mcp/claude-config-writer.js +19 -11
  121. package/dist/mcp/claude-config-writer.js.map +1 -1
  122. package/dist/mcp/index.d.ts +4 -2
  123. package/dist/mcp/index.d.ts.map +1 -1
  124. package/dist/mcp/index.js.map +1 -1
  125. package/dist/mcp/sync-cli-config.d.ts +42 -4
  126. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  127. package/dist/mcp/sync-cli-config.js +497 -17
  128. package/dist/mcp/sync-cli-config.js.map +1 -1
  129. package/dist/message-loop.d.ts +6 -0
  130. package/dist/message-loop.d.ts.map +1 -1
  131. package/dist/message-loop.js +281 -89
  132. package/dist/message-loop.js.map +1 -1
  133. package/dist/mqtt-client.d.ts +44 -1
  134. package/dist/mqtt-client.d.ts.map +1 -1
  135. package/dist/mqtt-client.js +284 -46
  136. package/dist/mqtt-client.js.map +1 -1
  137. package/dist/mqtt-data-relay.d.ts +44 -0
  138. package/dist/mqtt-data-relay.d.ts.map +1 -0
  139. package/dist/mqtt-data-relay.js +106 -0
  140. package/dist/mqtt-data-relay.js.map +1 -0
  141. package/dist/oauth.d.ts.map +1 -1
  142. package/dist/oauth.js +69 -29
  143. package/dist/oauth.js.map +1 -1
  144. package/dist/orchestration/capabilities.d.ts +13 -0
  145. package/dist/orchestration/capabilities.d.ts.map +1 -0
  146. package/dist/orchestration/capabilities.js +152 -0
  147. package/dist/orchestration/capabilities.js.map +1 -0
  148. package/dist/orchestration/dispatch-executor.d.ts +83 -0
  149. package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
  150. package/dist/orchestration/dispatch-executor.js +266 -0
  151. package/dist/orchestration/dispatch-executor.js.map +1 -0
  152. package/dist/orchestration/dispatch-hint.d.ts +134 -0
  153. package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
  154. package/dist/orchestration/dispatch-hint.js +247 -0
  155. package/dist/orchestration/dispatch-hint.js.map +1 -0
  156. package/dist/orchestration/dispatch-runner.d.ts +106 -0
  157. package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
  158. package/dist/orchestration/dispatch-runner.js +604 -0
  159. package/dist/orchestration/dispatch-runner.js.map +1 -0
  160. package/dist/orchestration/dispatch-tools.d.ts +167 -0
  161. package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
  162. package/dist/orchestration/dispatch-tools.js +328 -0
  163. package/dist/orchestration/dispatch-tools.js.map +1 -0
  164. package/dist/orchestration/front-door-policy.d.ts +35 -10
  165. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  166. package/dist/orchestration/front-door-policy.js +30 -267
  167. package/dist/orchestration/front-door-policy.js.map +1 -1
  168. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
  169. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
  170. package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
  171. package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
  172. package/dist/orchestration/orchestrator-operating-prompt.d.ts +15 -0
  173. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  174. package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
  175. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  176. package/dist/orchestration/plan-import.d.ts +39 -0
  177. package/dist/orchestration/plan-import.d.ts.map +1 -0
  178. package/dist/orchestration/plan-import.js +547 -0
  179. package/dist/orchestration/plan-import.js.map +1 -0
  180. package/dist/orchestration/validation.d.ts +40 -0
  181. package/dist/orchestration/validation.d.ts.map +1 -0
  182. package/dist/orchestration/validation.js +203 -0
  183. package/dist/orchestration/validation.js.map +1 -0
  184. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  185. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  186. package/dist/orchestration/worker-operating-prompt.js +36 -46
  187. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  188. package/dist/orchestrator.d.ts +214 -33
  189. package/dist/orchestrator.d.ts.map +1 -1
  190. package/dist/orchestrator.js +2200 -1100
  191. package/dist/orchestrator.js.map +1 -1
  192. package/dist/providers/anthropic.d.ts.map +1 -1
  193. package/dist/providers/anthropic.js +8 -4
  194. package/dist/providers/anthropic.js.map +1 -1
  195. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  196. package/dist/providers/claude-cli-prompt.js +49 -5
  197. package/dist/providers/claude-cli-prompt.js.map +1 -1
  198. package/dist/providers/claude-cli.d.ts.map +1 -1
  199. package/dist/providers/claude-cli.js +81 -5
  200. package/dist/providers/claude-cli.js.map +1 -1
  201. package/dist/providers/codex-cli.d.ts +10 -6
  202. package/dist/providers/codex-cli.d.ts.map +1 -1
  203. package/dist/providers/codex-cli.js +204 -26
  204. package/dist/providers/codex-cli.js.map +1 -1
  205. package/dist/providers/google.d.ts.map +1 -1
  206. package/dist/providers/google.js +15 -5
  207. package/dist/providers/google.js.map +1 -1
  208. package/dist/providers/index.d.ts +15 -1
  209. package/dist/providers/index.d.ts.map +1 -1
  210. package/dist/providers/index.js.map +1 -1
  211. package/dist/providers/openai.d.ts +1 -1
  212. package/dist/providers/openai.d.ts.map +1 -1
  213. package/dist/providers/openai.js +13 -5
  214. package/dist/providers/openai.js.map +1 -1
  215. package/dist/response-guard.js +1 -1
  216. package/dist/response-guard.js.map +1 -1
  217. package/dist/server-adapter.d.ts +8 -0
  218. package/dist/server-adapter.d.ts.map +1 -1
  219. package/dist/server-adapter.js +7 -0
  220. package/dist/server-adapter.js.map +1 -1
  221. package/dist/service-mode.d.ts +1 -1
  222. package/dist/service-mode.d.ts.map +1 -1
  223. package/dist/service-mode.js +64 -1
  224. package/dist/service-mode.js.map +1 -1
  225. package/dist/service-setup-only.d.ts +8 -0
  226. package/dist/service-setup-only.d.ts.map +1 -0
  227. package/dist/service-setup-only.js +37 -0
  228. package/dist/service-setup-only.js.map +1 -0
  229. package/dist/slash-commands.d.ts +21 -0
  230. package/dist/slash-commands.d.ts.map +1 -0
  231. package/dist/slash-commands.js +99 -0
  232. package/dist/slash-commands.js.map +1 -0
  233. package/dist/subagent/index.d.ts +4 -2
  234. package/dist/subagent/index.d.ts.map +1 -1
  235. package/dist/subagent/index.js.map +1 -1
  236. package/dist/summarization-pipeline.d.ts.map +1 -1
  237. package/dist/summarization-pipeline.js +1 -9
  238. package/dist/summarization-pipeline.js.map +1 -1
  239. package/dist/token-counter.d.ts.map +1 -1
  240. package/dist/token-counter.js +11 -4
  241. package/dist/token-counter.js.map +1 -1
  242. package/dist/tool-filter.d.ts.map +1 -1
  243. package/dist/tool-filter.js +10 -6
  244. package/dist/tool-filter.js.map +1 -1
  245. package/dist/tools/admin-tools.d.ts.map +1 -1
  246. package/dist/tools/admin-tools.js +20 -5
  247. package/dist/tools/admin-tools.js.map +1 -1
  248. package/dist/tools/index.d.ts.map +1 -1
  249. package/dist/tools/index.js +2 -1
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/run-command.d.ts.map +1 -1
  252. package/dist/tools/run-command.js +5 -1
  253. package/dist/tools/run-command.js.map +1 -1
  254. package/dist/tools/search-conversation-history.d.ts +16 -0
  255. package/dist/tools/search-conversation-history.d.ts.map +1 -0
  256. package/dist/tools/search-conversation-history.js +334 -0
  257. package/dist/tools/search-conversation-history.js.map +1 -0
  258. package/dist/tools/todo-tasks.d.ts.map +1 -1
  259. package/dist/tools/todo-tasks.js +77 -5
  260. package/dist/tools/todo-tasks.js.map +1 -1
  261. package/dist/usage-log.d.ts +62 -0
  262. package/dist/usage-log.d.ts.map +1 -0
  263. package/dist/usage-log.js +98 -0
  264. package/dist/usage-log.js.map +1 -0
  265. package/dist/wizard-state.d.ts +20 -0
  266. package/dist/wizard-state.d.ts.map +1 -1
  267. package/dist/wizard-state.js +90 -3
  268. package/dist/wizard-state.js.map +1 -1
  269. package/dist/wizard-support.d.ts.map +1 -1
  270. package/dist/wizard-support.js +27 -1
  271. package/dist/wizard-support.js.map +1 -1
  272. package/dist/workflow-engine.d.ts +44 -2
  273. package/dist/workflow-engine.d.ts.map +1 -1
  274. package/dist/workflow-engine.js +932 -111
  275. package/dist/workflow-engine.js.map +1 -1
  276. 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';
@@ -60,6 +68,58 @@ const LOCAL_SERVER_PORT = Number(process.env.FUNOLIO_LOCAL_PORT || 18420);
60
68
  const MESSAGE_ACTIVITY_RETENTION_HOURS = 24;
61
69
  const MAINTENANCE_INTERVAL_MS = 5 * 60 * 1000;
62
70
  const AGENT_IDLE_RESTART_MS = 30 * 60 * 1000;
71
+ const LAST_MAINTENANCE_RESTART_KEY = 'agent.last_maintenance_restart_at';
72
+ const MIN_MAINTENANCE_RESTART_INTERVAL_MS = 20 * 60 * 60 * 1000;
73
+ const SERVICE_LOCK_FILENAME = 'service.lock';
74
+ function resolveServiceLockPath() {
75
+ return path.join((0, config_1.getConfigDir)(), SERVICE_LOCK_FILENAME);
76
+ }
77
+ function isPidAlive(pid) {
78
+ if (!Number.isFinite(pid) || pid <= 0)
79
+ return false;
80
+ try {
81
+ process.kill(pid, 0);
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ function releaseServiceLock(lockPath) {
89
+ try {
90
+ if (fs.existsSync(lockPath))
91
+ fs.unlinkSync(lockPath);
92
+ }
93
+ catch {
94
+ // best effort
95
+ }
96
+ }
97
+ function acquireServiceLock(mode, userId) {
98
+ const lockPath = resolveServiceLockPath();
99
+ if (fs.existsSync(lockPath)) {
100
+ try {
101
+ const existing = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
102
+ if (existing?.pid && isPidAlive(existing.pid)) {
103
+ return { lockPath, acquired: false, existingPid: existing.pid };
104
+ }
105
+ fs.unlinkSync(lockPath);
106
+ }
107
+ catch {
108
+ try {
109
+ fs.unlinkSync(lockPath);
110
+ }
111
+ catch { }
112
+ }
113
+ }
114
+ const payload = {
115
+ pid: process.pid,
116
+ createdAt: new Date().toISOString(),
117
+ mode,
118
+ userId,
119
+ };
120
+ fs.writeFileSync(lockPath, JSON.stringify(payload));
121
+ return { lockPath, acquired: true };
122
+ }
63
123
  function cleanupExpiredMessageActivityRows() {
64
124
  try {
65
125
  const deleted = data.deleteExpiredMessageActivities();
@@ -78,7 +138,17 @@ function readLastInteractionAtMs() {
78
138
  const parsed = Date.parse(raw);
79
139
  return Number.isNaN(parsed) ? 0 : parsed;
80
140
  }
81
- function shouldRunNightlyRestart(now, lastInteractionAtMs, botManager) {
141
+ function readLastMaintenanceRestartAtMs() {
142
+ const raw = data.getSetting(LAST_MAINTENANCE_RESTART_KEY);
143
+ if (!raw)
144
+ return 0;
145
+ const parsed = Date.parse(raw);
146
+ return Number.isNaN(parsed) ? 0 : parsed;
147
+ }
148
+ function markMaintenanceRestartAt(now) {
149
+ data.setSetting(LAST_MAINTENANCE_RESTART_KEY, now.toISOString());
150
+ }
151
+ function shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestartAtMs, botManager, processStartAtMs) {
82
152
  const hour = now.getHours();
83
153
  if (hour < 1 || hour >= 4)
84
154
  return false;
@@ -86,7 +156,282 @@ function shouldRunNightlyRestart(now, lastInteractionAtMs, botManager) {
86
156
  return false;
87
157
  if (!lastInteractionAtMs)
88
158
  return false;
89
- return (now.getTime() - lastInteractionAtMs) >= AGENT_IDLE_RESTART_MS;
159
+ if (lastMaintenanceRestartAtMs && (now.getTime() - lastMaintenanceRestartAtMs) < MIN_MAINTENANCE_RESTART_INTERVAL_MS)
160
+ return false;
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
+ }
90
435
  }
91
436
  function parseJson(value) {
92
437
  if (!value)
@@ -107,10 +452,25 @@ function loadDbRuntimeConfig() {
107
452
  const botRows = data.listAgentProfiles();
108
453
  const projects = data.listProjects({ includeArchived: true });
109
454
  const firstProviderId = providerRows[0]?.provider_id;
110
- 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];
111
471
  const providers = providerRows.map((row) => ({
112
472
  id: row.provider_id,
113
- authType: row.auth_type === 'oauth' ? 'oauth' : 'apiKey',
473
+ authType: row.auth_type === 'oauth' ? 'oauth' : row.auth_type === 'cli' ? 'cli' : 'apiKey',
114
474
  apiKey: row.api_key_enc || undefined,
115
475
  oauthToken: row.oauth_token || undefined,
116
476
  oauthRefreshToken: row.oauth_refresh_token || undefined,
@@ -119,13 +479,16 @@ function loadDbRuntimeConfig() {
119
479
  label: row.label || undefined,
120
480
  }));
121
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] || '';
122
485
  const projectDir = projects.find((project) => project.bot_ids.includes(row.id) && project.folder)?.folder || '.';
123
486
  return {
124
487
  id: row.id,
125
488
  name: row.name,
126
489
  projectDir,
127
- provider: row.provider,
128
- model: row.model,
490
+ provider: resolvedProviderId,
491
+ model: resolvedModel,
129
492
  enabledTools: parseJson(row.enabled_builtin_tools_json),
130
493
  enabledMcpTools: parseJson(row.enabled_mcp_tools_json),
131
494
  permissionMode: row.permission_mode || 'autopilot',
@@ -140,7 +503,7 @@ function loadDbRuntimeConfig() {
140
503
  return {
141
504
  auth,
142
505
  providers,
143
- defaultProvider: defaultBot?.provider || firstProviderId,
506
+ defaultProvider: (defaultBot ? (resolveBotProviderConnection(defaultBot)?.provider_id || defaultBot.provider) : undefined) || firstProviderId,
144
507
  agents,
145
508
  activeAgentId: defaultBot?.id,
146
509
  autostart: !!desktopPrefs?.launchOnStartup,
@@ -179,13 +542,22 @@ async function startCommand(projectDir, options) {
179
542
  process.env.FUNOLIO_RUN_CONTEXT = 'windows-service';
180
543
  }
181
544
  const isServiceMode = options.mode === 'service' || options.mode === 'windows-service';
545
+ let serviceLockPath = null;
182
546
  if (isServiceMode) {
183
547
  (0, service_mode_1.enableServiceMode)();
548
+ const lock = acquireServiceLock(options.mode || 'service');
549
+ if (!lock.acquired) {
550
+ console.error(chalk_1.default.red(`✗ Another service agent is already running (pid ${lock.existingPid}).`));
551
+ process.exit(1);
552
+ }
553
+ serviceLockPath = lock.lockPath;
554
+ }
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';
184
560
  }
185
- const legacyConfig = (0, config_1.migrateConfig)((0, config_1.loadConfig)());
186
- const dbConfig = loadDbRuntimeConfig();
187
- const useDbRuntime = !!dbConfig;
188
- const config = dbConfig || legacyConfig;
189
561
  const authConfig = config.auth || null;
190
562
  const hasAuth = !!authConfig;
191
563
  const authExpired = hasAuth
@@ -193,9 +565,7 @@ async function startCommand(projectDir, options) {
193
565
  : true;
194
566
  const runSetupOnly = async (reason) => {
195
567
  if (isServiceMode) {
196
- console.log(chalk_1.default.yellow(`Setup-only mode: ${reason}`));
197
- (0, service_mode_1.emitEvent)('status', { status: 'setup_only', reason });
198
- await new Promise(() => { });
568
+ await (0, service_setup_only_1.waitForServiceSetup)(reason, service_mode_1.emitEvent, options.setupOnlySignal);
199
569
  return true;
200
570
  }
201
571
  return false;
@@ -204,6 +574,15 @@ async function startCommand(projectDir, options) {
204
574
  let localServerStarted = false;
205
575
  const runtimeConnectionMode = config.connectionMode === 'server' ? 'server' : 'local';
206
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
+ }
207
586
  process.env.FUNOLIO_CONNECTION_MODE = runtimeConnectionMode;
208
587
  if (runtimeServerBaseUrl) {
209
588
  process.env.FUNOLIO_SERVER_BASE_URL = runtimeServerBaseUrl;
@@ -222,6 +601,9 @@ async function startCommand(projectDir, options) {
222
601
  const dbInfo = (0, local_db_1.initLocalDb)();
223
602
  console.log(chalk_1.default.green(`✓ Local DB ready: ${dbInfo.path}`));
224
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
+ }
225
607
  cleanupExpiredMessageActivityRows();
226
608
  if (isServiceMode) {
227
609
  (0, service_mode_1.emitEvent)('local_db', { status: 'ready', path: dbInfo.path, walMode: dbInfo.walMode });
@@ -308,55 +690,69 @@ async function startCommand(projectDir, options) {
308
690
  process.exit(1);
309
691
  }
310
692
  }
311
- // Re-sync provider credentials from server (picks up web re-auth tokens)
312
- if (config.providers?.some(p => p.authType === 'oauth')) {
313
- try {
314
- const res = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/config`, {
315
- headers: { Authorization: `Bearer ${auth.token}` },
316
- });
317
- if (res.ok) {
318
- const serverConfig = await res.json();
319
- if (serverConfig.providers) {
320
- let updated = false;
321
- for (const sp of serverConfig.providers) {
322
- if (sp.connectionType !== 'oauth' || !sp.access_token)
323
- continue;
324
- const local = config.providers.find(p => p.id === sp.id);
325
- if (!local || local.authType !== 'oauth')
326
- continue;
327
- // Only update if server has a different (newer) token
328
- if (local.oauthToken !== sp.access_token) {
329
- local.oauthToken = sp.access_token;
330
- local.oauthRefreshToken = sp.refresh_token;
331
- local.oauthExpiresAt = sp.expires_at;
332
- updated = true;
333
- console.log(chalk_1.default.green(`✓ Updated ${sp.id} OAuth token from server`));
334
- }
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`));
335
717
  }
336
- if (updated) {
337
- (0, config_1.saveConfig)(config);
338
- // Also update agent config files on disk
339
- for (const p of config.providers) {
340
- if (p.authType !== 'oauth' || !p.oauthToken)
341
- continue;
342
- const { listAgents, loadAgentConfig: loadAC, saveAgentConfig: saveAC } = await Promise.resolve().then(() => __importStar(require('../agent-config')));
343
- for (const agentName of listAgents()) {
344
- const ac = loadAC(agentName);
345
- if (ac && ac.provider === p.id && ac.oauthToken) {
346
- ac.oauthToken = p.oauthToken;
347
- ac.oauthRefreshToken = p.oauthRefreshToken;
348
- ac.oauthExpiresAt = p.oauthExpiresAt;
349
- saveAC(agentName, ac);
350
- }
351
- }
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);
352
748
  }
353
749
  }
354
750
  }
355
751
  }
356
752
  }
357
- catch (err) {
358
- console.log(chalk_1.default.gray(` Server config re-sync skipped: ${err.message}`));
359
- }
753
+ }
754
+ catch (err) {
755
+ console.log(chalk_1.default.gray(` Server config re-sync skipped: ${err.message}`));
360
756
  }
361
757
  // Load agent-specific config if --agent was specified
362
758
  const agentLocalConfig = options.agent ? (0, agent_config_1.loadAgentConfig)(options.agent) : null;
@@ -481,8 +877,10 @@ async function startCommand(projectDir, options) {
481
877
  // Enforce strict subscription routing semantics.
482
878
  if (effectiveAccessMode === 'openai_subscription') {
483
879
  apiKey = undefined;
484
- if (provider === 'codex-cli')
485
- 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.
486
884
  }
487
885
  if (effectiveAccessMode === 'anthropic_subscription') {
488
886
  provider = 'claude-cli';
@@ -492,10 +890,17 @@ async function startCommand(projectDir, options) {
492
890
  model = 'claude-code';
493
891
  }
494
892
  }
495
- // 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);
496
901
  // OAuth tokens from claude.ai can't be used with the Anthropic API directly,
497
902
  // so we prefer claude-cli when Claude Code is installed.
498
- if (!apiKey && !oauthToken && !options.provider && !agentLocalConfig?.provider && provider !== 'claude-cli') {
903
+ if (!apiKey && !oauthToken && !providerWasExplicit && provider !== 'claude-cli') {
499
904
  const claudeVersion = detectClaudeCli();
500
905
  if (claudeVersion) {
501
906
  console.log(chalk_1.default.green(`✓ Claude Code detected (${claudeVersion}), using your subscription`));
@@ -571,6 +976,9 @@ async function startCommand(projectDir, options) {
571
976
  const dbInfo = (0, local_db_1.initLocalDb)();
572
977
  console.log(chalk_1.default.green(`✓ Local DB ready: ${dbInfo.path}`));
573
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
+ }
574
982
  cleanupExpiredMessageActivityRows();
575
983
  if (isServiceMode) {
576
984
  (0, service_mode_1.emitEvent)('local_db', { status: 'ready', path: dbInfo.path, walMode: dbInfo.walMode });
@@ -621,6 +1029,10 @@ async function startCommand(projectDir, options) {
621
1029
  authToken: auth.token,
622
1030
  refreshUrl: `${config_1.FUNOLIO_API_URL}/api/v1/agent/auth/refresh`,
623
1031
  });
1032
+ (0, chat_sync_1.configureChatSyncPublisher)({
1033
+ userId: auth.userId,
1034
+ publish: (topic, payload, opts) => mqttClient.publish(topic, payload, opts),
1035
+ });
624
1036
  // Create and auto-launch MCP manager for persistent tool discovery
625
1037
  const { MCPManager } = await Promise.resolve().then(() => __importStar(require('../mcp/manager')));
626
1038
  const mcpManager = new MCPManager();
@@ -649,6 +1061,7 @@ async function startCommand(projectDir, options) {
649
1061
  });
650
1062
  let maintenanceTimer = null;
651
1063
  let maintenanceRestartPending = false;
1064
+ const processStartAtMs = Date.now();
652
1065
  // Handle shutdown
653
1066
  const shutdown = async () => {
654
1067
  console.log(chalk_1.default.yellow('\nShutting down...'));
@@ -666,6 +1079,8 @@ async function startCommand(projectDir, options) {
666
1079
  }).catch(() => { });
667
1080
  await mcpManager.shutdown();
668
1081
  await mqttClient.disconnect();
1082
+ if (serviceLockPath)
1083
+ releaseServiceLock(serviceLockPath);
669
1084
  process.exit(0);
670
1085
  };
671
1086
  process.on('SIGINT', shutdown);
@@ -759,6 +1174,9 @@ async function startCommand(projectDir, options) {
759
1174
  console.log(chalk_1.default.gray(' This terminal can stay open or run in background.\n'));
760
1175
  // Start the active agent loop (driven by agents[] + activeAgentId)
761
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);
762
1180
  // Start all additional bot loops from ~/.funolio/agents/ configs
763
1181
  await botManager.startAllBots();
764
1182
  try {
@@ -775,9 +1193,11 @@ async function startCommand(projectDir, options) {
775
1193
  return;
776
1194
  const now = new Date();
777
1195
  const lastInteractionAtMs = readLastInteractionAtMs();
778
- if (!shouldRunNightlyRestart(now, lastInteractionAtMs, botManager))
1196
+ const lastMaintenanceRestartAtMs = readLastMaintenanceRestartAtMs();
1197
+ if (!shouldRunNightlyRestart(now, lastInteractionAtMs, lastMaintenanceRestartAtMs, botManager, processStartAtMs))
779
1198
  return;
780
1199
  maintenanceRestartPending = true;
1200
+ markMaintenanceRestartAt(now);
781
1201
  const reason = `nightly maintenance restart after ${MESSAGE_ACTIVITY_RETENTION_HOURS}h activity retention window`;
782
1202
  console.log(chalk_1.default.yellow(` Restarting agent for ${reason}`));
783
1203
  try {
@@ -793,7 +1213,10 @@ async function startCommand(projectDir, options) {
793
1213
  if (isServiceMode) {
794
1214
  (0, service_mode_1.emitEvent)('agent_restart', { reason });
795
1215
  }
796
- 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);
797
1220
  }, MAINTENANCE_INTERVAL_MS);
798
1221
  if (config.agents && config.agents.length > 1) {
799
1222
  console.log(chalk_1.default.blue(` ${config.agents.length} agent(s) configured (active: ${activeAgentConfig?.name || 'default'})`));
@@ -824,6 +1247,71 @@ async function startCommand(projectDir, options) {
824
1247
  }
825
1248
  }
826
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();
827
1315
  // OAuth token refresh is handled per-request in message-loop.ts via ensureFreshToken()
828
1316
  // No background timer needed — this matches the pi-ai/Claude Code CLI pattern of
829
1317
  // refreshing lazily before each API call, avoiding race conditions on single-use refresh tokens
@@ -846,6 +1334,14 @@ async function startCommand(projectDir, options) {
846
1334
  // Start listening for commands — BotManager routes to the right bot
847
1335
  // Also handle MCP marketplace install/uninstall commands
848
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
+ }
849
1345
  if (message.type === 'command' && message.action === 'mcp_install') {
850
1346
  const { serverId, envVars } = message;
851
1347
  console.log(chalk_1.default.cyan(`📦 Marketplace: installing MCP server "${serverId}"...`));