jinn-cli 0.5.3 → 0.7.0

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 (251) hide show
  1. package/dist/bin/jimmy.js +0 -0
  2. package/dist/src/cli/status.d.ts.map +1 -1
  3. package/dist/src/cli/status.js +10 -1
  4. package/dist/src/cli/status.js.map +1 -1
  5. package/dist/src/connectors/discord/format.d.ts +3 -0
  6. package/dist/src/connectors/discord/format.d.ts.map +1 -0
  7. package/dist/src/connectors/discord/format.js +33 -0
  8. package/dist/src/connectors/discord/format.js.map +1 -0
  9. package/dist/src/connectors/discord/index.d.ts +41 -0
  10. package/dist/src/connectors/discord/index.d.ts.map +1 -0
  11. package/dist/src/connectors/discord/index.js +302 -0
  12. package/dist/src/connectors/discord/index.js.map +1 -0
  13. package/dist/src/connectors/discord/remote.d.ts +33 -0
  14. package/dist/src/connectors/discord/remote.d.ts.map +1 -0
  15. package/dist/src/connectors/discord/remote.js +89 -0
  16. package/dist/src/connectors/discord/remote.js.map +1 -0
  17. package/dist/src/connectors/discord/threads.d.ts +5 -0
  18. package/dist/src/connectors/discord/threads.d.ts.map +1 -0
  19. package/dist/src/connectors/discord/threads.js +22 -0
  20. package/dist/src/connectors/discord/threads.js.map +1 -0
  21. package/dist/src/connectors/whatsapp/format.d.ts +2 -0
  22. package/dist/src/connectors/whatsapp/format.d.ts.map +1 -0
  23. package/dist/src/connectors/whatsapp/format.js +22 -0
  24. package/dist/src/connectors/whatsapp/format.js.map +1 -0
  25. package/dist/src/connectors/whatsapp/index.d.ts +42 -0
  26. package/dist/src/connectors/whatsapp/index.d.ts.map +1 -0
  27. package/dist/src/connectors/whatsapp/index.js +279 -0
  28. package/dist/src/connectors/whatsapp/index.js.map +1 -0
  29. package/dist/src/engines/claude.d.ts +5 -0
  30. package/dist/src/engines/claude.d.ts.map +1 -1
  31. package/dist/src/engines/claude.js +192 -22
  32. package/dist/src/engines/claude.js.map +1 -1
  33. package/dist/src/engines/mock.d.ts +16 -0
  34. package/dist/src/engines/mock.d.ts.map +1 -0
  35. package/dist/src/engines/mock.js +47 -0
  36. package/dist/src/engines/mock.js.map +1 -0
  37. package/dist/src/gateway/__tests__/costs.test.d.ts +2 -0
  38. package/dist/src/gateway/__tests__/costs.test.d.ts.map +1 -0
  39. package/dist/src/gateway/__tests__/costs.test.js +32 -0
  40. package/dist/src/gateway/__tests__/costs.test.js.map +1 -0
  41. package/dist/src/gateway/api.d.ts +1 -0
  42. package/dist/src/gateway/api.d.ts.map +1 -1
  43. package/dist/src/gateway/api.js +816 -18
  44. package/dist/src/gateway/api.js.map +1 -1
  45. package/dist/src/gateway/budgets.d.ts +16 -0
  46. package/dist/src/gateway/budgets.d.ts.map +1 -0
  47. package/dist/src/gateway/budgets.js +39 -0
  48. package/dist/src/gateway/budgets.js.map +1 -0
  49. package/dist/src/gateway/costs.d.ts +19 -0
  50. package/dist/src/gateway/costs.d.ts.map +1 -0
  51. package/dist/src/gateway/costs.js +39 -0
  52. package/dist/src/gateway/costs.js.map +1 -0
  53. package/dist/src/gateway/goals.d.ts +11 -0
  54. package/dist/src/gateway/goals.d.ts.map +1 -0
  55. package/dist/src/gateway/goals.js +72 -0
  56. package/dist/src/gateway/goals.js.map +1 -0
  57. package/dist/src/gateway/lifecycle.d.ts.map +1 -1
  58. package/dist/src/gateway/lifecycle.js +32 -15
  59. package/dist/src/gateway/lifecycle.js.map +1 -1
  60. package/dist/src/gateway/project-tagger.d.ts +20 -0
  61. package/dist/src/gateway/project-tagger.d.ts.map +1 -0
  62. package/dist/src/gateway/project-tagger.js +55 -0
  63. package/dist/src/gateway/project-tagger.js.map +1 -0
  64. package/dist/src/gateway/project-tagger.test.d.ts +2 -0
  65. package/dist/src/gateway/project-tagger.test.d.ts.map +1 -0
  66. package/dist/src/gateway/project-tagger.test.js +110 -0
  67. package/dist/src/gateway/project-tagger.test.js.map +1 -0
  68. package/dist/src/gateway/projects.d.ts +15 -0
  69. package/dist/src/gateway/projects.d.ts.map +1 -0
  70. package/dist/src/gateway/projects.js +48 -0
  71. package/dist/src/gateway/projects.js.map +1 -0
  72. package/dist/src/gateway/projects.test.d.ts +2 -0
  73. package/dist/src/gateway/projects.test.d.ts.map +1 -0
  74. package/dist/src/gateway/projects.test.js +85 -0
  75. package/dist/src/gateway/projects.test.js.map +1 -0
  76. package/dist/src/gateway/server.d.ts.map +1 -1
  77. package/dist/src/gateway/server.js +90 -2
  78. package/dist/src/gateway/server.js.map +1 -1
  79. package/dist/src/gateway/tasks.d.ts +14 -0
  80. package/dist/src/gateway/tasks.d.ts.map +1 -0
  81. package/dist/src/gateway/tasks.js +51 -0
  82. package/dist/src/gateway/tasks.js.map +1 -0
  83. package/dist/src/gateway/tasks.test.d.ts +2 -0
  84. package/dist/src/gateway/tasks.test.d.ts.map +1 -0
  85. package/dist/src/gateway/tasks.test.js +131 -0
  86. package/dist/src/gateway/tasks.test.js.map +1 -0
  87. package/dist/src/sessions/callbacks.d.ts +29 -0
  88. package/dist/src/sessions/callbacks.d.ts.map +1 -0
  89. package/dist/src/sessions/callbacks.js +110 -0
  90. package/dist/src/sessions/callbacks.js.map +1 -0
  91. package/dist/src/sessions/context.js +4 -3
  92. package/dist/src/sessions/context.js.map +1 -1
  93. package/dist/src/sessions/engine-override.d.ts +3 -0
  94. package/dist/src/sessions/engine-override.d.ts.map +1 -0
  95. package/dist/src/sessions/engine-override.js +42 -0
  96. package/dist/src/sessions/engine-override.js.map +1 -0
  97. package/dist/src/sessions/manager.d.ts.map +1 -1
  98. package/dist/src/sessions/manager.js +410 -40
  99. package/dist/src/sessions/manager.js.map +1 -1
  100. package/dist/src/sessions/queue.d.ts +19 -2
  101. package/dist/src/sessions/queue.d.ts.map +1 -1
  102. package/dist/src/sessions/queue.js +44 -13
  103. package/dist/src/sessions/queue.js.map +1 -1
  104. package/dist/src/sessions/registry.d.ts +21 -1
  105. package/dist/src/sessions/registry.d.ts.map +1 -1
  106. package/dist/src/sessions/registry.js +88 -0
  107. package/dist/src/sessions/registry.js.map +1 -1
  108. package/dist/src/shared/rateLimit.d.ts +13 -0
  109. package/dist/src/shared/rateLimit.d.ts.map +1 -0
  110. package/dist/src/shared/rateLimit.js +30 -0
  111. package/dist/src/shared/rateLimit.js.map +1 -0
  112. package/dist/src/shared/types.d.ts +59 -3
  113. package/dist/src/shared/types.d.ts.map +1 -1
  114. package/dist/src/shared/usageAwareness.d.ts +10 -0
  115. package/dist/src/shared/usageAwareness.d.ts.map +1 -0
  116. package/dist/src/shared/usageAwareness.js +62 -0
  117. package/dist/src/shared/usageAwareness.js.map +1 -0
  118. package/dist/web/404.html +1 -1
  119. package/dist/web/_next/static/1HnqYp-z0yEkKBQwXlWh1/_buildManifest.js +1 -0
  120. package/dist/web/_next/static/1HnqYp-z0yEkKBQwXlWh1/_ssgManifest.js +1 -0
  121. package/dist/web/_next/static/chunks/144-548cab85cf18301a.js +1 -0
  122. package/dist/web/_next/static/chunks/155-592ad81a5c00a38a.js +1 -0
  123. package/dist/web/_next/static/chunks/192-bd69b67fd03b9f3d.js +1 -0
  124. package/dist/web/_next/static/chunks/458-85ba1833ffcc2e6c.js +1 -0
  125. package/dist/web/_next/static/chunks/51-ea7256657692ba90.js +1 -0
  126. package/dist/web/_next/static/chunks/625-93dc90080662b8f4.js +1 -0
  127. package/dist/web/_next/static/chunks/6d25620b-d9f90746a7f2178c.js +1 -0
  128. package/dist/web/_next/static/chunks/7273c211.7ff69b7844551452.js +1 -0
  129. package/dist/web/_next/static/chunks/743.588b42b673795913.js +1 -0
  130. package/dist/web/_next/static/chunks/865-b4eb9a132b937321.js +1 -0
  131. package/dist/web/_next/static/chunks/943.1c6d37432bcad8e8.js +1 -0
  132. package/dist/web/_next/static/chunks/app/_not-found/page-22b69e9fb96ef3fc.js +1 -0
  133. package/dist/web/_next/static/chunks/app/chat/page-28df51d87ba7e82b.js +1 -0
  134. package/dist/web/_next/static/chunks/app/cron/page-4ede68bcdb3e76ca.js +1 -0
  135. package/dist/web/_next/static/chunks/app/kanban/page-0e1b2a66378b8a6b.js +1 -0
  136. package/dist/web/_next/static/chunks/app/layout-ab9ba2b24a88513d.js +1 -0
  137. package/dist/web/_next/static/chunks/app/logs/page-b2ea0e6b92c54706.js +1 -0
  138. package/dist/web/_next/static/chunks/app/org/page-05c8d33b3bf07d3d.js +1 -0
  139. package/dist/web/_next/static/chunks/app/page-57dcf41f8ef011a7.js +1 -0
  140. package/dist/web/_next/static/chunks/app/sessions/page-17f8dfdb90a924a7.js +1 -0
  141. package/dist/web/_next/static/chunks/app/settings/page-2903a5e9b8b0fc4a.js +1 -0
  142. package/dist/web/_next/static/chunks/app/skills/page-971feac69b3a1383.js +1 -0
  143. package/dist/web/_next/static/chunks/framework-33aa4529cc42a877.js +1 -0
  144. package/dist/web/_next/static/chunks/main-app-72a2d12a170701ef.js +1 -0
  145. package/dist/web/_next/static/chunks/main-c1296ca5f4362ccd.js +1 -0
  146. package/dist/web/_next/static/chunks/pages/_app-033e463982aa0ffc.js +1 -0
  147. package/dist/web/_next/static/chunks/pages/_error-de3b15b767c34e44.js +1 -0
  148. package/dist/web/_next/static/chunks/webpack-440561fa60ef8a8f.js +1 -0
  149. package/dist/web/_next/static/css/e79d77e53f783583.css +1 -0
  150. package/dist/web/chat.html +1 -1
  151. package/dist/web/chat.txt +12 -12
  152. package/dist/web/cron.html +1 -1
  153. package/dist/web/cron.txt +12 -12
  154. package/dist/web/index.html +1 -1
  155. package/dist/web/index.txt +12 -12
  156. package/dist/web/kanban.html +1 -1
  157. package/dist/web/kanban.txt +12 -12
  158. package/dist/web/logs.html +2 -2
  159. package/dist/web/logs.txt +12 -12
  160. package/dist/web/org.html +1 -1
  161. package/dist/web/org.txt +12 -12
  162. package/dist/web/sessions.html +1 -1
  163. package/dist/web/sessions.txt +16 -19
  164. package/dist/web/settings.html +1 -1
  165. package/dist/web/settings.txt +12 -12
  166. package/dist/web/skills.html +1 -1
  167. package/dist/web/skills.txt +12 -12
  168. package/dist/web.bak/404.html +1 -0
  169. package/dist/web.bak/_next/static/chunks/184-5a617386af9a1dd3.js +1 -0
  170. package/dist/web.bak/_next/static/chunks/700-ad04a2a5b3c8880f.js +1 -0
  171. package/dist/web.bak/_next/static/chunks/app/chat/page-36edb6fc1d1e880b.js +1 -0
  172. package/dist/{web/_next/static/chunks/app/costs/page-7940c2fe7e3dace1.js → web.bak/_next/static/chunks/app/costs/page-6c5cd46a6b3cde21.js} +1 -1
  173. package/dist/{web/_next/static/chunks/app/cron/page-f81a986689712af7.js → web.bak/_next/static/chunks/app/cron/page-210d9d333a7eed94.js} +1 -1
  174. package/dist/web.bak/_next/static/chunks/app/kanban/page-c32370bcd6a7c841.js +1 -0
  175. package/dist/web.bak/_next/static/chunks/app/layout-3ad6b73a0904c24b.js +1 -0
  176. package/dist/web.bak/_next/static/chunks/app/logs/page-7322a6789e16dca4.js +1 -0
  177. package/dist/{web/_next/static/chunks/app/org/page-3d44d51e94edb85e.js → web.bak/_next/static/chunks/app/org/page-02263c5702e0fd3e.js} +1 -1
  178. package/dist/{web/_next/static/chunks/app/page-7ac43789d477a51f.js → web.bak/_next/static/chunks/app/page-b68dcf7b8802c704.js} +1 -1
  179. package/dist/web.bak/_next/static/chunks/app/sessions/page-bf7ad2fac281c7d6.js +1 -0
  180. package/dist/web.bak/_next/static/chunks/app/settings/page-d3b89563c42be2e5.js +1 -0
  181. package/dist/{web/_next/static/chunks/app/skills/page-26b727333df9db45.js → web.bak/_next/static/chunks/app/skills/page-194f4e97f29ab581.js} +1 -1
  182. package/dist/web.bak/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  183. package/dist/web.bak/_next/static/css/b809b6af2d241fc8.css +1 -0
  184. package/dist/web.bak/chat.html +1 -0
  185. package/dist/web.bak/chat.txt +20 -0
  186. package/dist/web.bak/costs.html +16 -0
  187. package/dist/{web → web.bak}/costs.txt +4 -4
  188. package/dist/web.bak/cron.html +1 -0
  189. package/dist/web.bak/cron.txt +20 -0
  190. package/dist/web.bak/index.html +1 -0
  191. package/dist/web.bak/index.txt +20 -0
  192. package/dist/web.bak/kanban.html +1 -0
  193. package/dist/web.bak/kanban.txt +20 -0
  194. package/dist/web.bak/logs.html +7 -0
  195. package/dist/web.bak/logs.txt +20 -0
  196. package/dist/web.bak/org.html +1 -0
  197. package/dist/web.bak/org.txt +20 -0
  198. package/dist/web.bak/sessions.html +1 -0
  199. package/dist/web.bak/sessions.txt +17 -0
  200. package/dist/web.bak/settings.html +1 -0
  201. package/dist/web.bak/settings.txt +20 -0
  202. package/dist/web.bak/skills.html +1 -0
  203. package/dist/web.bak/skills.txt +20 -0
  204. package/package.json +10 -3
  205. package/dist/web/_next/static/chunks/282-4e9c26e9a600c58e.js +0 -1
  206. package/dist/web/_next/static/chunks/700-a7cbf54fe1fbf4bc.js +0 -1
  207. package/dist/web/_next/static/chunks/app/chat/page-757fcd211d059cb7.js +0 -1
  208. package/dist/web/_next/static/chunks/app/kanban/page-6ab8586b063ca3ac.js +0 -1
  209. package/dist/web/_next/static/chunks/app/layout-c24e2d25774ff71a.js +0 -1
  210. package/dist/web/_next/static/chunks/app/logs/page-388b787cb847ca97.js +0 -1
  211. package/dist/web/_next/static/chunks/app/sessions/page-18757fcd067b7e9d.js +0 -1
  212. package/dist/web/_next/static/chunks/app/settings/page-f62176848534f90a.js +0 -1
  213. package/dist/web/_next/static/css/bd612b1ca9b40306.css +0 -1
  214. package/dist/web/app-build-manifest.json +0 -3
  215. package/dist/web/build-manifest.json +0 -17
  216. package/dist/web/costs.html +0 -16
  217. package/dist/web/react-loadable-manifest.json +0 -1
  218. package/dist/web/server/app-paths-manifest.json +0 -1
  219. package/dist/web/server/interception-route-rewrite-manifest.js +0 -1
  220. package/dist/web/server/middleware-build-manifest.js +0 -19
  221. package/dist/web/server/middleware-manifest.json +0 -6
  222. package/dist/web/server/middleware-react-loadable-manifest.js +0 -1
  223. package/dist/web/server/next-font-manifest.js +0 -1
  224. package/dist/web/server/next-font-manifest.json +0 -1
  225. package/dist/web/server/pages-manifest.json +0 -1
  226. package/dist/web/server/server-reference-manifest.js +0 -1
  227. package/dist/web/server/server-reference-manifest.json +0 -5
  228. package/dist/web/static/development/_buildManifest.js +0 -1
  229. package/dist/web/static/development/_ssgManifest.js +0 -1
  230. package/dist/web/types/cache-life.d.ts +0 -141
  231. package/dist/web/types/package.json +0 -1
  232. package/dist/web/types/routes.d.ts +0 -66
  233. package/dist/web/types/validator.ts +0 -142
  234. /package/dist/{web/_next/static/J4YFiPdzNcFHieP2FIPPe → web.bak/_next/static/QrKxazgwMrykF2yLwDvUM}/_buildManifest.js +0 -0
  235. /package/dist/{web/_next/static/J4YFiPdzNcFHieP2FIPPe → web.bak/_next/static/QrKxazgwMrykF2yLwDvUM}/_ssgManifest.js +0 -0
  236. /package/dist/{web → web.bak}/_next/static/chunks/198-fd91406a158c5c25.js +0 -0
  237. /package/dist/{web → web.bak}/_next/static/chunks/517.62389e8d3c929c43.js +0 -0
  238. /package/dist/{web → web.bak}/_next/static/chunks/534-17c49c944e0d0fe1.js +0 -0
  239. /package/dist/{web → web.bak}/_next/static/chunks/573-070537ec2452d03e.js +0 -0
  240. /package/dist/{web → web.bak}/_next/static/chunks/590-2c34156c7417317e.js +0 -0
  241. /package/dist/{web → web.bak}/_next/static/chunks/7273c211.06e3b6021d90b73f.js +0 -0
  242. /package/dist/{web → web.bak}/_next/static/chunks/743-5bb03adbb0e4ddec.js +0 -0
  243. /package/dist/{web → web.bak}/_next/static/chunks/874.97d5a27895061057.js +0 -0
  244. /package/dist/{web → web.bak}/_next/static/chunks/8e6518bb-c26e82767f1faf66.js +0 -0
  245. /package/dist/{web → web.bak}/_next/static/chunks/app/_not-found/page-7d43a486b7014af3.js +0 -0
  246. /package/dist/{web → web.bak}/_next/static/chunks/framework-077b27ad7787463c.js +0 -0
  247. /package/dist/{web → web.bak}/_next/static/chunks/main-app-0c65af8a0fd99888.js +0 -0
  248. /package/dist/{web → web.bak}/_next/static/chunks/main-f1c74cefd4965abf.js +0 -0
  249. /package/dist/{web → web.bak}/_next/static/chunks/pages/_app-77a85fe7d6bca671.js +0 -0
  250. /package/dist/{web → web.bak}/_next/static/chunks/pages/_error-68febf4b34900064.js +0 -0
  251. /package/dist/{web → web.bak}/_next/static/chunks/webpack-0f39b7e91dce9791.js +0 -0
@@ -1,13 +1,72 @@
1
1
  import fs from "node:fs";
2
- import { accumulateSessionCost, createSession, deleteSession, getSessionBySessionKey, insertMessage, updateSession, } from "./registry.js";
2
+ import { accumulateSessionCost, createSession, deleteSession, getSessionBySessionKey, getMessages, insertMessage, updateSession, } from "./registry.js";
3
+ import { notifyParentSession, notifyRateLimited, notifyRateLimitResumed, notifyDiscordChannel } from "./callbacks.js";
3
4
  import { buildContext } from "./context.js";
4
5
  import { SessionQueue } from "./queue.js";
5
6
  import { JINN_HOME } from "../shared/paths.js";
6
7
  import { logger } from "../shared/logger.js";
7
8
  import { resolveEffort } from "../shared/effort.js";
9
+ import { computeNextRetryDelayMs, computeRateLimitDeadlineMs, detectRateLimit } from "../shared/rateLimit.js";
10
+ import { getClaudeExpectedResetAt, isLikelyNearClaudeUsageLimit, recordClaudeRateLimit } from "../shared/usageAwareness.js";
8
11
  import { loadJobs } from "../cron/jobs.js";
9
12
  import { setCronJobEnabled, triggerCronJob } from "../cron/scheduler.js";
13
+ import { checkBudget } from "../gateway/budgets.js";
10
14
  import { resolveMcpServers, writeMcpConfigFile, cleanupMcpConfigFile } from "../mcp/resolver.js";
15
+ function maybeRevertEngineOverride(session) {
16
+ const meta = (session.transportMeta || {});
17
+ const override = meta["engineOverride"];
18
+ if (!override)
19
+ return session;
20
+ const originalEngine = typeof override.originalEngine === "string" ? override.originalEngine : null;
21
+ const originalEngineSessionId = typeof override.originalEngineSessionId === "string"
22
+ ? override.originalEngineSessionId
23
+ : null;
24
+ const syncSince = typeof override.syncSince === "string" ? override.syncSince : null;
25
+ const untilIso = typeof override.until === "string" ? override.until : null;
26
+ if (!originalEngine || !untilIso)
27
+ return session;
28
+ const until = new Date(untilIso);
29
+ if (Number.isNaN(until.getTime()))
30
+ return session;
31
+ if (until.getTime() > Date.now())
32
+ return session;
33
+ const engineSessionsRaw = meta["engineSessions"];
34
+ const engineSessions = (engineSessionsRaw && typeof engineSessionsRaw === "object" && !Array.isArray(engineSessionsRaw))
35
+ ? { ...engineSessionsRaw }
36
+ : {};
37
+ // Preserve the current engine session ID under its engine key
38
+ if (session.engine && session.engineSessionId) {
39
+ engineSessions[String(session.engine)] = session.engineSessionId;
40
+ }
41
+ const restoredSessionId = originalEngineSessionId
42
+ ?? (typeof engineSessions[originalEngine] === "string" ? engineSessions[originalEngine] : null);
43
+ const nextMeta = { ...meta, engineSessions };
44
+ if (originalEngine === "claude" && syncSince && session.engine !== "claude") {
45
+ nextMeta["claudeSyncSince"] = syncSince;
46
+ }
47
+ delete nextMeta["engineOverride"];
48
+ return updateSession(session.id, {
49
+ engine: originalEngine,
50
+ engineSessionId: restoredSessionId,
51
+ transportMeta: nextMeta,
52
+ lastError: null,
53
+ }) ?? session;
54
+ }
55
+ function mergeTransportMeta(existing, incoming) {
56
+ const baseExisting = (existing && typeof existing === "object" && !Array.isArray(existing))
57
+ ? existing
58
+ : {};
59
+ const baseIncoming = (incoming && typeof incoming === "object" && !Array.isArray(incoming))
60
+ ? incoming
61
+ : {};
62
+ const merged = { ...baseExisting, ...baseIncoming };
63
+ // Preserve Jinn internal keys from being overwritten by transport adapters.
64
+ for (const key of ["engineOverride", "engineSessions", "claudeSyncSince"]) {
65
+ if (baseExisting[key] !== undefined)
66
+ merged[key] = baseExisting[key];
67
+ }
68
+ return merged;
69
+ }
11
70
  export class SessionManager {
12
71
  config;
13
72
  engines;
@@ -52,18 +111,27 @@ export class SessionManager {
52
111
  (opts.employee ? ` (employee: ${opts.employee.name})` : ""));
53
112
  }
54
113
  else {
114
+ const mergedMeta = mergeTransportMeta(session.transportMeta, msg.transportMeta);
55
115
  session = updateSession(session.id, {
56
116
  replyContext: msg.replyContext,
57
117
  messageId: msg.messageId ?? null,
58
- transportMeta: msg.transportMeta ?? null,
118
+ transportMeta: mergedMeta,
59
119
  ...(opts.model ? { model: opts.model } : {}),
60
120
  }) ?? session;
61
121
  }
122
+ session = maybeRevertEngineOverride(session);
62
123
  const target = connector.reconstructTarget(msg.replyContext);
63
124
  target.messageTs ??= msg.messageId;
64
125
  const attachmentPaths = msg.attachments
65
126
  .map((attachment) => attachment.localPath)
66
127
  .filter((filePath) => !!filePath);
128
+ if (session.status === "waiting") {
129
+ const expectedResetAt = getClaudeExpectedResetAt();
130
+ const resumeText = expectedResetAt
131
+ ? expectedResetAt.toLocaleString("en-GB", { weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })
132
+ : null;
133
+ await connector.replyMessage(target, `⏳ Still paused due to Claude usage limit${resumeText ? ` (resets ${resumeText})` : ""}. I queued this message and will respond automatically.`).catch(() => { });
134
+ }
67
135
  if (session.status === "running" && this.queue.isRunning(msg.sessionKey) && connector.getCapabilities().reactions) {
68
136
  await connector.addReaction(target, "clock1").catch(() => { });
69
137
  }
@@ -93,7 +161,7 @@ export class SessionManager {
93
161
  status: "running",
94
162
  replyContext: msg.replyContext,
95
163
  messageId: msg.messageId ?? null,
96
- transportMeta: msg.transportMeta ?? null,
164
+ transportMeta: mergeTransportMeta(session.transportMeta, msg.transportMeta),
97
165
  lastActivity: new Date().toISOString(),
98
166
  });
99
167
  // Resolve MCP config before try block so it's accessible in catch for cleanup
@@ -120,75 +188,363 @@ export class SessionManager {
120
188
  }
121
189
  }
122
190
  const effortLevel = resolveEffort(engineConfig, session, employee);
123
- let result;
124
- try {
125
- result = await engine.run({
126
- prompt: msg.text,
127
- resumeSessionId: session.engineSessionId ?? undefined,
128
- systemPrompt,
129
- cwd: JINN_HOME,
130
- bin: engineConfig.bin,
131
- model: session.model ?? engineConfig.model,
132
- effortLevel,
133
- cliFlags: employee?.cliFlags,
134
- mcpConfigPath,
135
- attachments: attachments.length > 0 ? attachments : undefined,
136
- sessionId: session.id,
137
- });
191
+ // If we previously switched to GPT while Claude was rate-limited, inject a sync transcript
192
+ // so Claude can resume with full context when it comes back online.
193
+ const syncSinceIso = session.transportMeta?.claudeSyncSince;
194
+ let promptToRun = msg.text;
195
+ const syncSinceMs = typeof syncSinceIso === "string" ? new Date(syncSinceIso).getTime() : NaN;
196
+ const syncRequested = session.engine === "claude" && typeof syncSinceIso === "string" && Number.isFinite(syncSinceMs);
197
+ if (syncRequested) {
198
+ const sinceMessages = getMessages(session.id)
199
+ .filter((m) => (m.role === "user" || m.role === "assistant") && m.timestamp >= syncSinceMs)
200
+ .map((m) => `${m.role.toUpperCase()}: ${m.content}`);
201
+ const transcript = sinceMessages.slice(-20).join("\n\n");
202
+ promptToRun =
203
+ `We temporarily switched to GPT due to a Claude usage limit. Sync your context with this transcript (most recent last), then respond to the last USER message.\n\n${transcript}`;
204
+ }
205
+ // Budget enforcement — check BEFORE engine.run()
206
+ if (session.employee) {
207
+ const budgetConfig = this.config.budgets?.employees;
208
+ if (budgetConfig && session.employee in budgetConfig) {
209
+ const budgetStatus = checkBudget(session.employee, budgetConfig);
210
+ if (budgetStatus === 'paused') {
211
+ logger.warn(`Session ${session.id} blocked: employee "${session.employee}" has exceeded their budget`);
212
+ const pausedMsg = `Budget limit exceeded for employee "${session.employee}". Session blocked.`;
213
+ updateSession(session.id, {
214
+ status: 'error',
215
+ lastActivity: new Date().toISOString(),
216
+ lastError: pausedMsg,
217
+ });
218
+ if (decorateMessages && connector.setTypingStatus) {
219
+ await connector.setTypingStatus(target.channel, threadTs, '').catch(() => { });
220
+ }
221
+ await connector.replyMessage(target, `⛔ ${pausedMsg}`).catch(() => { });
222
+ if (decorateMessages && capabilities.reactions) {
223
+ await connector.removeReaction(target, 'eyes').catch(() => { });
224
+ }
225
+ return;
226
+ }
227
+ }
138
228
  }
139
- finally {
140
- // Clean up temp attachment files downloaded from Slack
141
- for (const filePath of attachments) {
142
- try {
143
- fs.rmSync(filePath, { force: true });
229
+ // Heuristic preflight warning: Claude usage limits don't expose a precise "remaining" budget.
230
+ // If we've hit the limit recently and this looks like a heavy turn, warn before we spend time.
231
+ if (decorateMessages && session.engine === "claude" && isLikelyNearClaudeUsageLimit()) {
232
+ const modelName = (session.model ?? engineConfig.model ?? "").toLowerCase();
233
+ const heavyEffort = ["high", "xhigh", "max"].includes((effortLevel || "").toLowerCase());
234
+ const heavyModel = modelName.includes("opus");
235
+ const looksBig = attachments.length > 0 || msg.text.length > 6000;
236
+ if ((heavyEffort || heavyModel) && looksBig) {
237
+ const expectedResetAt = getClaudeExpectedResetAt();
238
+ const resumeText = expectedResetAt
239
+ ? expectedResetAt.toLocaleString("en-GB", { weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })
240
+ : null;
241
+ await connector.replyMessage(target, `⚠️ Heads up: Claude usage limits were hit recently, and this looks like a bigger task. If you're near the limit, it may pause${resumeText ? ` until ~${resumeText}` : ""}.`).catch(() => { });
242
+ }
243
+ }
244
+ const result = await engine.run({
245
+ prompt: promptToRun,
246
+ resumeSessionId: session.engineSessionId ?? undefined,
247
+ systemPrompt,
248
+ cwd: JINN_HOME,
249
+ bin: engineConfig.bin,
250
+ model: session.model ?? engineConfig.model,
251
+ effortLevel,
252
+ cliFlags: employee?.cliFlags,
253
+ mcpConfigPath,
254
+ attachments: attachments.length > 0 ? attachments : undefined,
255
+ sessionId: session.id,
256
+ });
257
+ const wasInterrupted = result.error?.startsWith("Interrupted");
258
+ // Detect rate limit / usage limit errors and auto-retry
259
+ const rateLimit = !wasInterrupted ? detectRateLimit(result) : { limited: false };
260
+ if (rateLimit.limited) {
261
+ recordClaudeRateLimit(rateLimit.resetsAt);
262
+ const strategy = this.config.sessions?.rateLimitStrategy ?? "fallback";
263
+ // Optional fallback: switch to GPT (Codex) while Claude resets
264
+ if (session.engine === "claude" && strategy === "fallback") {
265
+ const fallbackName = this.config.sessions?.fallbackEngine ?? "codex";
266
+ const fallbackEngine = this.engines.get(fallbackName);
267
+ if (fallbackEngine) {
268
+ const { resumeAt } = computeNextRetryDelayMs(rateLimit.resetsAt);
269
+ const until = resumeAt ?? new Date(Date.now() + 6 * 60 * 60_000);
270
+ const syncSince = new Date().toISOString();
271
+ const resumeText = resumeAt
272
+ ? resumeAt.toLocaleString("en-GB", { weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })
273
+ : null;
274
+ notifyDiscordChannel(`⚠️ Claude usage limit reached. Session ${session.id}${session.employee ? ` (${session.employee})` : ""} switching to GPT.`);
275
+ await connector.replyMessage(target, `⚠️ Claude usage limit reached${resumeText ? `. Resets ${resumeText}` : ""}. Switching to GPT for now.`).catch(() => { });
276
+ const nextMeta = { ...(session.transportMeta || {}) };
277
+ const engineSessionsRaw = nextMeta.engineSessions;
278
+ const engineSessions = (engineSessionsRaw && typeof engineSessionsRaw === "object" && !Array.isArray(engineSessionsRaw))
279
+ ? { ...engineSessionsRaw }
280
+ : {};
281
+ if (session.engineSessionId) {
282
+ engineSessions.claude = session.engineSessionId;
283
+ }
284
+ nextMeta.engineSessions = engineSessions;
285
+ nextMeta.engineOverride = { originalEngine: "claude", originalEngineSessionId: session.engineSessionId, until: until.toISOString(), syncSince };
286
+ updateSession(session.id, {
287
+ engine: fallbackName,
288
+ // Keep Claude engine_session_id intact for later restore; Codex will return its own thread id.
289
+ transportMeta: nextMeta,
290
+ status: "running",
291
+ lastActivity: new Date().toISOString(),
292
+ lastError: resumeAt
293
+ ? `Claude usage limit — using GPT until ${resumeAt.toISOString()}`
294
+ : "Claude usage limit — using GPT temporarily",
295
+ });
296
+ const fallbackConfig = this.config.engines.codex;
297
+ const fallbackEffort = resolveEffort(fallbackConfig, session, employee);
298
+ const codexResume = typeof engineSessions.codex === "string" ? engineSessions.codex : undefined;
299
+ const history = getMessages(session.id)
300
+ .filter((m) => m.role === "user" || m.role === "assistant")
301
+ .map((m) => `${m.role.toUpperCase()}: ${m.content}`);
302
+ const historyText = history.slice(-12).join("\n\n");
303
+ const fallbackPrompt = codexResume
304
+ ? msg.text
305
+ : `Continue this conversation and respond to the last USER message.\n\nConversation so far:\n\n${historyText}`;
306
+ const fallbackResult = await fallbackEngine.run({
307
+ prompt: fallbackPrompt,
308
+ resumeSessionId: codexResume,
309
+ systemPrompt,
310
+ cwd: JINN_HOME,
311
+ bin: fallbackConfig.bin,
312
+ model: session.model ?? fallbackConfig.model,
313
+ effortLevel: fallbackEffort,
314
+ cliFlags: employee?.cliFlags,
315
+ attachments: attachments.length > 0 ? attachments : undefined,
316
+ sessionId: session.id,
317
+ });
318
+ const fallbackText = fallbackResult.result?.trim()
319
+ ? fallbackResult.result
320
+ : fallbackResult.error || "(No response from engine)";
321
+ insertMessage(session.id, "assistant", fallbackText);
322
+ if (fallbackResult.cost || fallbackResult.numTurns) {
323
+ accumulateSessionCost(session.id, fallbackResult.cost ?? 0, fallbackResult.numTurns ?? 1);
324
+ }
325
+ // Persist Codex thread id so future fallbacks can resume it
326
+ const nextEngineSessions = { ...engineSessions };
327
+ if (fallbackResult.sessionId) {
328
+ nextEngineSessions.codex = fallbackResult.sessionId;
329
+ }
330
+ const metaAfter = { ...(getSessionBySessionKey(msg.sessionKey)?.transportMeta || nextMeta) };
331
+ metaAfter.engineSessions = nextEngineSessions;
332
+ updateSession(session.id, { transportMeta: metaAfter });
333
+ if (decorateMessages && connector.setTypingStatus) {
334
+ await connector.setTypingStatus(target.channel, threadTs, "").catch(() => { });
335
+ }
336
+ await connector.replyMessage(target, fallbackText).catch(() => { });
337
+ if (decorateMessages && capabilities.reactions) {
338
+ await connector.removeReaction(target, "eyes").catch(() => { });
339
+ }
340
+ const updated = updateSession(session.id, {
341
+ engineSessionId: fallbackResult.sessionId,
342
+ status: fallbackResult.error ? "error" : "idle",
343
+ replyContext: msg.replyContext,
344
+ messageId: msg.messageId ?? null,
345
+ transportMeta: mergeTransportMeta(getSessionBySessionKey(msg.sessionKey)?.transportMeta ?? session.transportMeta, msg.transportMeta),
346
+ lastActivity: new Date().toISOString(),
347
+ lastError: fallbackResult.error ?? null,
348
+ });
349
+ if (updated) {
350
+ notifyParentSession(updated, { result: fallbackResult.result, error: fallbackResult.error ?? null, cost: fallbackResult.cost, durationMs: fallbackResult.durationMs });
351
+ }
352
+ return;
353
+ }
354
+ }
355
+ const waitEmoji = "hourglass_flowing_sand";
356
+ const { delayMs, resumeAt } = computeNextRetryDelayMs(rateLimit.resetsAt);
357
+ const deadlineMs = computeRateLimitDeadlineMs(rateLimit.resetsAt, rateLimit.resetsAt ? 30 * 60_000 : 6 * 60 * 60_000);
358
+ const resumeText = resumeAt
359
+ ? resumeAt.toLocaleString("en-GB", { weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })
360
+ : null;
361
+ logger.info(`Session ${session.id} hit Claude usage limit — will auto-retry ${resumeAt ? `at ${resumeAt.toISOString()}` : `in ${Math.round(delayMs / 1000)}s`}`);
362
+ // Send hardcoded Discord notification — does not depend on LLM
363
+ notifyDiscordChannel(`⚠️ Claude usage limit reached. Session ${session.id}${session.employee ? ` (${session.employee})` : ""} paused${resumeText ? ` until ${resumeText}` : ""}.`);
364
+ // Clear "thinking" UI and show waiting state
365
+ if (decorateMessages && connector.setTypingStatus) {
366
+ await connector.setTypingStatus(target.channel, threadTs, "").catch(() => { });
367
+ }
368
+ if (decorateMessages && capabilities.reactions) {
369
+ await connector.removeReaction(target, "eyes").catch(() => { });
370
+ await connector.addReaction(target, waitEmoji).catch(() => { });
371
+ }
372
+ const waitingSession = updateSession(session.id, {
373
+ ...(result.sessionId?.trim() ? { engineSessionId: result.sessionId } : {}),
374
+ status: "waiting",
375
+ lastActivity: new Date().toISOString(),
376
+ lastError: resumeAt
377
+ ? `Claude usage limit — resumes ${resumeAt.toISOString()}`
378
+ : "Claude usage limit — waiting for reset",
379
+ }) ?? session;
380
+ notifyRateLimited(waitingSession, resumeAt
381
+ ? resumeAt.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })
382
+ : undefined);
383
+ await connector.replyMessage(target, `⏳ Claude usage limit reached${resumeText ? `. Resets ${resumeText}` : ""} — I'll continue automatically.`).catch(() => { });
384
+ // Keep lastActivity fresh while waiting (UI / status endpoints)
385
+ const heartbeat = setInterval(() => {
386
+ updateSession(session.id, { status: "waiting", lastActivity: new Date().toISOString() });
387
+ }, 60_000);
388
+ try {
389
+ let attempt = 0;
390
+ let nextDelayMs = delayMs;
391
+ while (Date.now() < deadlineMs) {
392
+ await new Promise(r => setTimeout(r, nextDelayMs));
393
+ attempt++;
394
+ // Check if session was stopped while waiting
395
+ const currentSession = getSessionBySessionKey(msg.sessionKey);
396
+ if (!currentSession || currentSession.status === "error") {
397
+ logger.info(`Session ${session.id} stopped while waiting for usage reset`);
398
+ return;
399
+ }
400
+ // Show active processing again
401
+ if (decorateMessages && connector.setTypingStatus) {
402
+ await connector.setTypingStatus(target.channel, threadTs, "is thinking...").catch(() => { });
403
+ }
404
+ if (decorateMessages && capabilities.reactions) {
405
+ await connector.removeReaction(target, waitEmoji).catch(() => { });
406
+ await connector.addReaction(target, "eyes").catch(() => { });
407
+ }
408
+ logger.info(`Session ${session.id} retrying after usage limit (attempt ${attempt})`);
409
+ const retryResult = await engine.run({
410
+ prompt: msg.text,
411
+ resumeSessionId: currentSession.engineSessionId ?? undefined,
412
+ systemPrompt,
413
+ cwd: JINN_HOME,
414
+ bin: engineConfig.bin,
415
+ model: currentSession.model ?? engineConfig.model,
416
+ effortLevel,
417
+ cliFlags: employee?.cliFlags,
418
+ mcpConfigPath,
419
+ attachments: attachments.length > 0 ? attachments : undefined,
420
+ sessionId: session.id,
421
+ });
422
+ const retryInterrupted = retryResult.error?.startsWith("Interrupted");
423
+ const retryRateLimit = !retryInterrupted ? detectRateLimit(retryResult) : { limited: false };
424
+ if (retryRateLimit.limited) {
425
+ recordClaudeRateLimit(retryRateLimit.resetsAt);
426
+ logger.info(`Session ${session.id} still rate limited (attempt ${attempt})`);
427
+ const next = computeNextRetryDelayMs(retryRateLimit.resetsAt);
428
+ nextDelayMs = next.delayMs;
429
+ // Return to waiting UI state
430
+ if (decorateMessages && connector.setTypingStatus) {
431
+ await connector.setTypingStatus(target.channel, threadTs, "").catch(() => { });
432
+ }
433
+ if (decorateMessages && capabilities.reactions) {
434
+ await connector.removeReaction(target, "eyes").catch(() => { });
435
+ await connector.addReaction(target, waitEmoji).catch(() => { });
436
+ }
437
+ updateSession(session.id, {
438
+ ...(retryResult.sessionId?.trim() ? { engineSessionId: retryResult.sessionId } : {}),
439
+ status: "waiting",
440
+ lastActivity: new Date().toISOString(),
441
+ lastError: next.resumeAt
442
+ ? `Claude usage limit — resumes ${next.resumeAt.toISOString()}`
443
+ : "Claude usage limit — waiting for reset",
444
+ });
445
+ continue;
446
+ }
447
+ // Success or different error — handle normally
448
+ const retryText = retryResult.result?.trim()
449
+ ? retryResult.result
450
+ : retryResult.error || "(No response from engine)";
451
+ insertMessage(session.id, "assistant", retryText);
452
+ if (retryResult.cost || retryResult.numTurns) {
453
+ accumulateSessionCost(session.id, retryResult.cost ?? 0, retryResult.numTurns ?? 1);
454
+ }
455
+ // Clear typing indicator & reactions
456
+ if (decorateMessages && connector.setTypingStatus) {
457
+ await connector.setTypingStatus(target.channel, threadTs, "").catch(() => { });
458
+ }
459
+ if (decorateMessages && capabilities.reactions) {
460
+ await connector.removeReaction(target, "eyes").catch(() => { });
461
+ await connector.removeReaction(target, waitEmoji).catch(() => { });
462
+ }
463
+ await connector.replyMessage(target, retryText).catch(() => { });
464
+ const retryUpdated = updateSession(session.id, {
465
+ ...(retryResult.sessionId?.trim() ? { engineSessionId: retryResult.sessionId } : {}),
466
+ status: retryResult.error ? "error" : "idle",
467
+ replyContext: msg.replyContext,
468
+ messageId: msg.messageId ?? null,
469
+ transportMeta: msg.transportMeta ?? null,
470
+ lastActivity: new Date().toISOString(),
471
+ lastError: retryResult.error ?? null,
472
+ });
473
+ if (retryUpdated) {
474
+ notifyRateLimitResumed(retryUpdated);
475
+ notifyDiscordChannel(`✅ Claude usage limit cleared. Session ${session.id}${session.employee ? ` (${session.employee})` : ""} resumed.`);
476
+ notifyParentSession(retryUpdated, { result: retryResult.result, error: retryResult.error ?? null, cost: retryResult.cost, durationMs: retryResult.durationMs });
477
+ }
478
+ logger.info(`Session ${session.id} resumed after usage reset`);
479
+ return;
144
480
  }
145
- catch {
146
- // Ignore cleanup errors best effort
481
+ // Exhausted waiting window
482
+ notifyDiscordChannel(`❌ Claude usage limit did not clear in time. Session ${session.id}${session.employee ? ` (${session.employee})` : ""} has been stopped.`);
483
+ await connector.replyMessage(target, "Usage limit didn't reset in time. Please try again later.").catch(() => { });
484
+ updateSession(session.id, {
485
+ status: "error",
486
+ lastActivity: new Date().toISOString(),
487
+ lastError: "Claude usage limit did not clear in time",
488
+ });
489
+ // Clear reactions on failure
490
+ if (decorateMessages && capabilities.reactions) {
491
+ await connector.removeReaction(target, "eyes").catch(() => { });
492
+ await connector.removeReaction(target, waitEmoji).catch(() => { });
147
493
  }
494
+ return;
495
+ }
496
+ finally {
497
+ clearInterval(heartbeat);
148
498
  }
149
499
  }
150
500
  const responseText = result.result?.trim()
151
501
  ? result.result
152
502
  : result.error || "(No response from engine)";
153
503
  insertMessage(session.id, "assistant", responseText);
154
- // Clean up temp MCP config
155
- if (mcpConfigPath)
156
- cleanupMcpConfigFile(session.id);
157
- // Track cost
158
504
  if (result.cost || result.numTurns) {
159
505
  accumulateSessionCost(session.id, result.cost ?? 0, result.numTurns ?? 1);
160
506
  }
161
- // Clear typing indicator before sending response
162
507
  if (decorateMessages && connector.setTypingStatus) {
163
508
  await connector.setTypingStatus(target.channel, threadTs, "").catch(() => { });
164
509
  }
165
- await connector.replyMessage(target, responseText);
510
+ if (!wasInterrupted) {
511
+ await connector.replyMessage(target, responseText);
512
+ }
166
513
  if (decorateMessages && capabilities.reactions) {
167
514
  await connector.removeReaction(target, "eyes").catch(() => { });
168
515
  }
169
- updateSession(session.id, {
170
- engineSessionId: result.sessionId,
171
- status: result.error ? "error" : "idle",
516
+ const updatedSession = updateSession(session.id, {
517
+ ...(result.sessionId?.trim() ? { engineSessionId: result.sessionId } : {}),
518
+ status: wasInterrupted ? "idle" : (result.error ? "error" : "idle"),
172
519
  replyContext: msg.replyContext,
173
520
  messageId: msg.messageId ?? null,
174
- transportMeta: msg.transportMeta ?? null,
521
+ transportMeta: (() => {
522
+ const merged = mergeTransportMeta(getSessionBySessionKey(msg.sessionKey)?.transportMeta ?? session.transportMeta, msg.transportMeta);
523
+ if (syncRequested && !rateLimit.limited && !wasInterrupted) {
524
+ delete merged["claudeSyncSince"];
525
+ }
526
+ return merged;
527
+ })(),
175
528
  lastActivity: new Date().toISOString(),
176
- lastError: result.error ?? null,
529
+ lastError: wasInterrupted ? null : (result.error ?? null),
177
530
  });
531
+ if (updatedSession) {
532
+ notifyParentSession(updatedSession, { result: result.result, error: wasInterrupted ? null : (result.error ?? null), cost: result.cost, durationMs: result.durationMs });
533
+ }
178
534
  logger.info(`Session ${session.id} completed in ${result.durationMs ?? 0}ms` +
179
535
  (result.cost ? ` ($${result.cost.toFixed(4)})` : ""));
180
536
  }
181
537
  catch (err) {
182
538
  const errMsg = err instanceof Error ? err.message : String(err);
183
539
  logger.error(`Session ${session.id} error: ${errMsg}`);
184
- // Clean up temp MCP config on error
185
- if (mcpConfigPath)
186
- cleanupMcpConfigFile(session.id);
187
- updateSession(session.id, {
540
+ const erroredSession = updateSession(session.id, {
188
541
  status: "error",
189
542
  lastActivity: new Date().toISOString(),
190
543
  lastError: errMsg,
191
544
  });
545
+ if (erroredSession) {
546
+ notifyParentSession(erroredSession, { error: errMsg });
547
+ }
192
548
  // Clear typing indicator on error
193
549
  if (decorateMessages && connector.setTypingStatus) {
194
550
  await connector.setTypingStatus(target.channel, threadTs, "").catch(() => { });
@@ -196,7 +552,21 @@ export class SessionManager {
196
552
  await connector.replyMessage(target, `Error: ${errMsg}`).catch(() => { });
197
553
  if (decorateMessages && capabilities.reactions) {
198
554
  await connector.removeReaction(target, "eyes").catch(() => { });
555
+ await connector.removeReaction(target, "hourglass_flowing_sand").catch(() => { });
556
+ }
557
+ }
558
+ finally {
559
+ // Clean up temp attachment files downloaded from Slack
560
+ for (const filePath of attachments) {
561
+ try {
562
+ fs.rmSync(filePath, { force: true });
563
+ }
564
+ catch {
565
+ // Ignore cleanup errors — best effort
566
+ }
199
567
  }
568
+ if (mcpConfigPath)
569
+ cleanupMcpConfigFile(session.id);
200
570
  }
201
571
  }
202
572
  async handleCommand(msg, connector) {