im-hub-pro 0.2.29

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 (384) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/README.zh-CN.md +496 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +921 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/core/acp-server.d.ts +8 -0
  9. package/dist/core/acp-server.d.ts.map +1 -0
  10. package/dist/core/acp-server.js +266 -0
  11. package/dist/core/acp-server.js.map +1 -0
  12. package/dist/core/agent-base.d.ts +94 -0
  13. package/dist/core/agent-base.d.ts.map +1 -0
  14. package/dist/core/agent-base.js +374 -0
  15. package/dist/core/agent-base.js.map +1 -0
  16. package/dist/core/agent-cwd.d.ts +45 -0
  17. package/dist/core/agent-cwd.d.ts.map +1 -0
  18. package/dist/core/agent-cwd.js +178 -0
  19. package/dist/core/agent-cwd.js.map +1 -0
  20. package/dist/core/agent-cwd.test.d.ts +2 -0
  21. package/dist/core/agent-cwd.test.d.ts.map +1 -0
  22. package/dist/core/agent-cwd.test.js +149 -0
  23. package/dist/core/agent-cwd.test.js.map +1 -0
  24. package/dist/core/approval-bus.d.ts +232 -0
  25. package/dist/core/approval-bus.d.ts.map +1 -0
  26. package/dist/core/approval-bus.js +703 -0
  27. package/dist/core/approval-bus.js.map +1 -0
  28. package/dist/core/approval-bus.synthetic.test.d.ts +2 -0
  29. package/dist/core/approval-bus.synthetic.test.d.ts.map +1 -0
  30. package/dist/core/approval-bus.synthetic.test.js +182 -0
  31. package/dist/core/approval-bus.synthetic.test.js.map +1 -0
  32. package/dist/core/approval-bus.test.d.ts +2 -0
  33. package/dist/core/approval-bus.test.d.ts.map +1 -0
  34. package/dist/core/approval-bus.test.js +537 -0
  35. package/dist/core/approval-bus.test.js.map +1 -0
  36. package/dist/core/approval-router.d.ts +95 -0
  37. package/dist/core/approval-router.d.ts.map +1 -0
  38. package/dist/core/approval-router.js +450 -0
  39. package/dist/core/approval-router.js.map +1 -0
  40. package/dist/core/approval-router.test.d.ts +2 -0
  41. package/dist/core/approval-router.test.d.ts.map +1 -0
  42. package/dist/core/approval-router.test.js +413 -0
  43. package/dist/core/approval-router.test.js.map +1 -0
  44. package/dist/core/audit-log.d.ts +55 -0
  45. package/dist/core/audit-log.d.ts.map +1 -0
  46. package/dist/core/audit-log.js +203 -0
  47. package/dist/core/audit-log.js.map +1 -0
  48. package/dist/core/bgjob-reader.d.ts +65 -0
  49. package/dist/core/bgjob-reader.d.ts.map +1 -0
  50. package/dist/core/bgjob-reader.js +212 -0
  51. package/dist/core/bgjob-reader.js.map +1 -0
  52. package/dist/core/bgjob-reader.test.d.ts +2 -0
  53. package/dist/core/bgjob-reader.test.d.ts.map +1 -0
  54. package/dist/core/bgjob-reader.test.js +178 -0
  55. package/dist/core/bgjob-reader.test.js.map +1 -0
  56. package/dist/core/circuit-breaker.d.ts +37 -0
  57. package/dist/core/circuit-breaker.d.ts.map +1 -0
  58. package/dist/core/circuit-breaker.js +115 -0
  59. package/dist/core/circuit-breaker.js.map +1 -0
  60. package/dist/core/commands/agent.d.ts +4 -0
  61. package/dist/core/commands/agent.d.ts.map +1 -0
  62. package/dist/core/commands/agent.js +21 -0
  63. package/dist/core/commands/agent.js.map +1 -0
  64. package/dist/core/commands/approval.d.ts +3 -0
  65. package/dist/core/commands/approval.d.ts.map +1 -0
  66. package/dist/core/commands/approval.js +44 -0
  67. package/dist/core/commands/approval.js.map +1 -0
  68. package/dist/core/commands/approval.test.d.ts +2 -0
  69. package/dist/core/commands/approval.test.d.ts.map +1 -0
  70. package/dist/core/commands/approval.test.js +85 -0
  71. package/dist/core/commands/approval.test.js.map +1 -0
  72. package/dist/core/commands/audit.d.ts +3 -0
  73. package/dist/core/commands/audit.d.ts.map +1 -0
  74. package/dist/core/commands/audit.js +84 -0
  75. package/dist/core/commands/audit.js.map +1 -0
  76. package/dist/core/commands/builtin.d.ts +3 -0
  77. package/dist/core/commands/builtin.d.ts.map +1 -0
  78. package/dist/core/commands/builtin.js +26 -0
  79. package/dist/core/commands/builtin.js.map +1 -0
  80. package/dist/core/commands/job.d.ts +3 -0
  81. package/dist/core/commands/job.d.ts.map +1 -0
  82. package/dist/core/commands/job.js +195 -0
  83. package/dist/core/commands/job.js.map +1 -0
  84. package/dist/core/commands/model.d.ts +9 -0
  85. package/dist/core/commands/model.d.ts.map +1 -0
  86. package/dist/core/commands/model.js +183 -0
  87. package/dist/core/commands/model.js.map +1 -0
  88. package/dist/core/commands/plan.d.ts +3 -0
  89. package/dist/core/commands/plan.d.ts.map +1 -0
  90. package/dist/core/commands/plan.js +75 -0
  91. package/dist/core/commands/plan.js.map +1 -0
  92. package/dist/core/commands/plan.test.d.ts +2 -0
  93. package/dist/core/commands/plan.test.d.ts.map +1 -0
  94. package/dist/core/commands/plan.test.js +122 -0
  95. package/dist/core/commands/plan.test.js.map +1 -0
  96. package/dist/core/commands/router.d.ts +3 -0
  97. package/dist/core/commands/router.d.ts.map +1 -0
  98. package/dist/core/commands/router.js +71 -0
  99. package/dist/core/commands/router.js.map +1 -0
  100. package/dist/core/commands/schedule.d.ts +3 -0
  101. package/dist/core/commands/schedule.d.ts.map +1 -0
  102. package/dist/core/commands/schedule.js +123 -0
  103. package/dist/core/commands/schedule.js.map +1 -0
  104. package/dist/core/commands/sessions.d.ts +3 -0
  105. package/dist/core/commands/sessions.d.ts.map +1 -0
  106. package/dist/core/commands/sessions.js +88 -0
  107. package/dist/core/commands/sessions.js.map +1 -0
  108. package/dist/core/commands/stats.d.ts +3 -0
  109. package/dist/core/commands/stats.d.ts.map +1 -0
  110. package/dist/core/commands/stats.js +73 -0
  111. package/dist/core/commands/stats.js.map +1 -0
  112. package/dist/core/commands/think.d.ts +3 -0
  113. package/dist/core/commands/think.d.ts.map +1 -0
  114. package/dist/core/commands/think.js +28 -0
  115. package/dist/core/commands/think.js.map +1 -0
  116. package/dist/core/commands/workspaces.d.ts +3 -0
  117. package/dist/core/commands/workspaces.d.ts.map +1 -0
  118. package/dist/core/commands/workspaces.js +47 -0
  119. package/dist/core/commands/workspaces.js.map +1 -0
  120. package/dist/core/config-schema.d.ts +58 -0
  121. package/dist/core/config-schema.d.ts.map +1 -0
  122. package/dist/core/config-schema.js +63 -0
  123. package/dist/core/config-schema.js.map +1 -0
  124. package/dist/core/cron.d.ts +29 -0
  125. package/dist/core/cron.d.ts.map +1 -0
  126. package/dist/core/cron.js +184 -0
  127. package/dist/core/cron.js.map +1 -0
  128. package/dist/core/event-bus.d.ts +80 -0
  129. package/dist/core/event-bus.d.ts.map +1 -0
  130. package/dist/core/event-bus.js +62 -0
  131. package/dist/core/event-bus.js.map +1 -0
  132. package/dist/core/intent-llm.d.ts +27 -0
  133. package/dist/core/intent-llm.d.ts.map +1 -0
  134. package/dist/core/intent-llm.js +170 -0
  135. package/dist/core/intent-llm.js.map +1 -0
  136. package/dist/core/intent.d.ts +12 -0
  137. package/dist/core/intent.d.ts.map +1 -0
  138. package/dist/core/intent.js +187 -0
  139. package/dist/core/intent.js.map +1 -0
  140. package/dist/core/job-board.d.ts +84 -0
  141. package/dist/core/job-board.d.ts.map +1 -0
  142. package/dist/core/job-board.js +379 -0
  143. package/dist/core/job-board.js.map +1 -0
  144. package/dist/core/logger.d.ts +6 -0
  145. package/dist/core/logger.d.ts.map +1 -0
  146. package/dist/core/logger.js +54 -0
  147. package/dist/core/logger.js.map +1 -0
  148. package/dist/core/metrics.d.ts +55 -0
  149. package/dist/core/metrics.d.ts.map +1 -0
  150. package/dist/core/metrics.js +291 -0
  151. package/dist/core/metrics.js.map +1 -0
  152. package/dist/core/onboarding.d.ts +94 -0
  153. package/dist/core/onboarding.d.ts.map +1 -0
  154. package/dist/core/onboarding.js +426 -0
  155. package/dist/core/onboarding.js.map +1 -0
  156. package/dist/core/onboarding.test.d.ts +2 -0
  157. package/dist/core/onboarding.test.d.ts.map +1 -0
  158. package/dist/core/onboarding.test.js +112 -0
  159. package/dist/core/onboarding.test.js.map +1 -0
  160. package/dist/core/rate-limiter.d.ts +44 -0
  161. package/dist/core/rate-limiter.d.ts.map +1 -0
  162. package/dist/core/rate-limiter.js +115 -0
  163. package/dist/core/rate-limiter.js.map +1 -0
  164. package/dist/core/registry.d.ts +32 -0
  165. package/dist/core/registry.d.ts.map +1 -0
  166. package/dist/core/registry.js +122 -0
  167. package/dist/core/registry.js.map +1 -0
  168. package/dist/core/router.d.ts +41 -0
  169. package/dist/core/router.d.ts.map +1 -0
  170. package/dist/core/router.js +431 -0
  171. package/dist/core/router.js.map +1 -0
  172. package/dist/core/schedule.d.ts +65 -0
  173. package/dist/core/schedule.d.ts.map +1 -0
  174. package/dist/core/schedule.js +316 -0
  175. package/dist/core/schedule.js.map +1 -0
  176. package/dist/core/session-subtasks.test.d.ts +2 -0
  177. package/dist/core/session-subtasks.test.d.ts.map +1 -0
  178. package/dist/core/session-subtasks.test.js +88 -0
  179. package/dist/core/session-subtasks.test.js.map +1 -0
  180. package/dist/core/session.d.ts +182 -0
  181. package/dist/core/session.d.ts.map +1 -0
  182. package/dist/core/session.js +774 -0
  183. package/dist/core/session.js.map +1 -0
  184. package/dist/core/sqlite-helper.d.ts +37 -0
  185. package/dist/core/sqlite-helper.d.ts.map +1 -0
  186. package/dist/core/sqlite-helper.js +79 -0
  187. package/dist/core/sqlite-helper.js.map +1 -0
  188. package/dist/core/transcribe.d.ts +25 -0
  189. package/dist/core/transcribe.d.ts.map +1 -0
  190. package/dist/core/transcribe.js +217 -0
  191. package/dist/core/transcribe.js.map +1 -0
  192. package/dist/core/transcribe.test.d.ts +2 -0
  193. package/dist/core/transcribe.test.d.ts.map +1 -0
  194. package/dist/core/transcribe.test.js +163 -0
  195. package/dist/core/transcribe.test.js.map +1 -0
  196. package/dist/core/types.d.ts +352 -0
  197. package/dist/core/types.d.ts.map +1 -0
  198. package/dist/core/types.js +3 -0
  199. package/dist/core/types.js.map +1 -0
  200. package/dist/core/workspace.d.ts +67 -0
  201. package/dist/core/workspace.d.ts.map +1 -0
  202. package/dist/core/workspace.js +113 -0
  203. package/dist/core/workspace.js.map +1 -0
  204. package/dist/index.d.ts +5 -0
  205. package/dist/index.d.ts.map +1 -0
  206. package/dist/index.js +6 -0
  207. package/dist/index.js.map +1 -0
  208. package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
  209. package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
  210. package/dist/plugins/agents/acp/acp-adapter.js +49 -0
  211. package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
  212. package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
  213. package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
  214. package/dist/plugins/agents/acp/acp-client.js +175 -0
  215. package/dist/plugins/agents/acp/acp-client.js.map +1 -0
  216. package/dist/plugins/agents/acp/discovery.d.ts +19 -0
  217. package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
  218. package/dist/plugins/agents/acp/discovery.js +109 -0
  219. package/dist/plugins/agents/acp/discovery.js.map +1 -0
  220. package/dist/plugins/agents/acp/index.d.ts +4 -0
  221. package/dist/plugins/agents/acp/index.d.ts.map +1 -0
  222. package/dist/plugins/agents/acp/index.js +4 -0
  223. package/dist/plugins/agents/acp/index.js.map +1 -0
  224. package/dist/plugins/agents/acp/types.d.ts +62 -0
  225. package/dist/plugins/agents/acp/types.d.ts.map +1 -0
  226. package/dist/plugins/agents/acp/types.js +5 -0
  227. package/dist/plugins/agents/acp/types.js.map +1 -0
  228. package/dist/plugins/agents/claude-code/adapter.test.d.ts +2 -0
  229. package/dist/plugins/agents/claude-code/adapter.test.d.ts.map +1 -0
  230. package/dist/plugins/agents/claude-code/adapter.test.js +195 -0
  231. package/dist/plugins/agents/claude-code/adapter.test.js.map +1 -0
  232. package/dist/plugins/agents/claude-code/index.d.ts +25 -0
  233. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
  234. package/dist/plugins/agents/claude-code/index.js +184 -0
  235. package/dist/plugins/agents/claude-code/index.js.map +1 -0
  236. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +42 -0
  237. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
  238. package/dist/plugins/agents/claude-code/mcp-approval-server.js +235 -0
  239. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
  240. package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts +2 -0
  241. package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts.map +1 -0
  242. package/dist/plugins/agents/claude-code/mcp-approval-server.test.js +188 -0
  243. package/dist/plugins/agents/claude-code/mcp-approval-server.test.js.map +1 -0
  244. package/dist/plugins/agents/codex/adapter.test.d.ts +2 -0
  245. package/dist/plugins/agents/codex/adapter.test.d.ts.map +1 -0
  246. package/dist/plugins/agents/codex/adapter.test.js +192 -0
  247. package/dist/plugins/agents/codex/adapter.test.js.map +1 -0
  248. package/dist/plugins/agents/codex/index.d.ts +37 -0
  249. package/dist/plugins/agents/codex/index.d.ts.map +1 -0
  250. package/dist/plugins/agents/codex/index.js +254 -0
  251. package/dist/plugins/agents/codex/index.js.map +1 -0
  252. package/dist/plugins/agents/copilot/index.d.ts +35 -0
  253. package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
  254. package/dist/plugins/agents/copilot/index.js +182 -0
  255. package/dist/plugins/agents/copilot/index.js.map +1 -0
  256. package/dist/plugins/agents/opencode/adapter.test.d.ts +2 -0
  257. package/dist/plugins/agents/opencode/adapter.test.d.ts.map +1 -0
  258. package/dist/plugins/agents/opencode/adapter.test.js +139 -0
  259. package/dist/plugins/agents/opencode/adapter.test.js.map +1 -0
  260. package/dist/plugins/agents/opencode/http-adapter.test.d.ts +2 -0
  261. package/dist/plugins/agents/opencode/http-adapter.test.d.ts.map +1 -0
  262. package/dist/plugins/agents/opencode/http-adapter.test.js +492 -0
  263. package/dist/plugins/agents/opencode/http-adapter.test.js.map +1 -0
  264. package/dist/plugins/agents/opencode/index.d.ts +5 -0
  265. package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
  266. package/dist/plugins/agents/opencode/index.js +30 -0
  267. package/dist/plugins/agents/opencode/index.js.map +1 -0
  268. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +138 -0
  269. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
  270. package/dist/plugins/agents/opencode/opencode-http-adapter.js +549 -0
  271. package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
  272. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +24 -0
  273. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
  274. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +103 -0
  275. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
  276. package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
  277. package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
  278. package/dist/plugins/agents/opencode/serve-manager.js +190 -0
  279. package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
  280. package/dist/plugins/messengers/discord/discord-adapter.d.ts +22 -0
  281. package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
  282. package/dist/plugins/messengers/discord/discord-adapter.js +241 -0
  283. package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
  284. package/dist/plugins/messengers/discord/discord-adapter.test.d.ts +2 -0
  285. package/dist/plugins/messengers/discord/discord-adapter.test.d.ts.map +1 -0
  286. package/dist/plugins/messengers/discord/discord-adapter.test.js +332 -0
  287. package/dist/plugins/messengers/discord/discord-adapter.test.js.map +1 -0
  288. package/dist/plugins/messengers/discord/index.d.ts +4 -0
  289. package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
  290. package/dist/plugins/messengers/discord/index.js +4 -0
  291. package/dist/plugins/messengers/discord/index.js.map +1 -0
  292. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
  293. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
  294. package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
  295. package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
  296. package/dist/plugins/messengers/discord/types.d.ts +9 -0
  297. package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
  298. package/dist/plugins/messengers/discord/types.js +3 -0
  299. package/dist/plugins/messengers/discord/types.js.map +1 -0
  300. package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
  301. package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
  302. package/dist/plugins/messengers/feishu/card-builder.js +89 -0
  303. package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
  304. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +33 -0
  305. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
  306. package/dist/plugins/messengers/feishu/feishu-adapter.js +195 -0
  307. package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
  308. package/dist/plugins/messengers/feishu/feishu-client.d.ts +44 -0
  309. package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
  310. package/dist/plugins/messengers/feishu/feishu-client.js +120 -0
  311. package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
  312. package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts +2 -0
  313. package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts.map +1 -0
  314. package/dist/plugins/messengers/feishu/feishu-dedup.test.js +70 -0
  315. package/dist/plugins/messengers/feishu/feishu-dedup.test.js.map +1 -0
  316. package/dist/plugins/messengers/feishu/index.d.ts +4 -0
  317. package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
  318. package/dist/plugins/messengers/feishu/index.js +4 -0
  319. package/dist/plugins/messengers/feishu/index.js.map +1 -0
  320. package/dist/plugins/messengers/feishu/types.d.ts +113 -0
  321. package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
  322. package/dist/plugins/messengers/feishu/types.js +4 -0
  323. package/dist/plugins/messengers/feishu/types.js.map +1 -0
  324. package/dist/plugins/messengers/telegram/index.d.ts +4 -0
  325. package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
  326. package/dist/plugins/messengers/telegram/index.js +4 -0
  327. package/dist/plugins/messengers/telegram/index.js.map +1 -0
  328. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
  329. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
  330. package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
  331. package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
  332. package/dist/plugins/messengers/telegram/media-download.d.ts +51 -0
  333. package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
  334. package/dist/plugins/messengers/telegram/media-download.js +224 -0
  335. package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
  336. package/dist/plugins/messengers/telegram/media-download.test.d.ts +2 -0
  337. package/dist/plugins/messengers/telegram/media-download.test.d.ts.map +1 -0
  338. package/dist/plugins/messengers/telegram/media-download.test.js +125 -0
  339. package/dist/plugins/messengers/telegram/media-download.test.js.map +1 -0
  340. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +62 -0
  341. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
  342. package/dist/plugins/messengers/telegram/telegram-adapter.js +653 -0
  343. package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
  344. package/dist/plugins/messengers/telegram/types.d.ts +47 -0
  345. package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
  346. package/dist/plugins/messengers/telegram/types.js +3 -0
  347. package/dist/plugins/messengers/telegram/types.js.map +1 -0
  348. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +68 -0
  349. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
  350. package/dist/plugins/messengers/wechat/ilink-adapter.js +483 -0
  351. package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
  352. package/dist/plugins/messengers/wechat/ilink-client.d.ts +66 -0
  353. package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
  354. package/dist/plugins/messengers/wechat/ilink-client.js +288 -0
  355. package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
  356. package/dist/plugins/messengers/wechat/ilink-types.d.ts +173 -0
  357. package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
  358. package/dist/plugins/messengers/wechat/ilink-types.js +12 -0
  359. package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
  360. package/dist/utils/backoff.d.ts +35 -0
  361. package/dist/utils/backoff.d.ts.map +1 -0
  362. package/dist/utils/backoff.js +59 -0
  363. package/dist/utils/backoff.js.map +1 -0
  364. package/dist/utils/cross-platform.d.ts +26 -0
  365. package/dist/utils/cross-platform.d.ts.map +1 -0
  366. package/dist/utils/cross-platform.js +58 -0
  367. package/dist/utils/cross-platform.js.map +1 -0
  368. package/dist/utils/message-split.d.ts +14 -0
  369. package/dist/utils/message-split.d.ts.map +1 -0
  370. package/dist/utils/message-split.js +65 -0
  371. package/dist/utils/message-split.js.map +1 -0
  372. package/dist/utils/safe-equal.d.ts +2 -0
  373. package/dist/utils/safe-equal.d.ts.map +1 -0
  374. package/dist/utils/safe-equal.js +11 -0
  375. package/dist/utils/safe-equal.js.map +1 -0
  376. package/dist/web/public/_app.js +196 -0
  377. package/dist/web/public/index.html +935 -0
  378. package/dist/web/public/settings.html +1181 -0
  379. package/dist/web/public/tasks.html +1827 -0
  380. package/dist/web/server.d.ts +11 -0
  381. package/dist/web/server.d.ts.map +1 -0
  382. package/dist/web/server.js +1820 -0
  383. package/dist/web/server.js.map +1 -0
  384. package/package.json +73 -0
@@ -0,0 +1,774 @@
1
+ // Session manager — per-conversation state
2
+ //
3
+ // On-disk layout (one directory tree per home):
4
+ // ~/.im-hub/sessions/<safe-key>.json — metadata (no messages)
5
+ // ~/.im-hub/sessions/<safe-key>.log — append-only JSONL of messages
6
+ //
7
+ // Splitting the message log out of the JSON metadata avoids rewriting the
8
+ // entire history on every chat turn (the old behavior was an O(N) write
9
+ // per message). All metadata writes are atomic via writeFile→rename.
10
+ import { createHash, randomBytes } from 'crypto';
11
+ import { homedir } from 'os';
12
+ import { join } from 'path';
13
+ import { mkdir, readFile, writeFile, rename, unlink, appendFile, readdir } from 'fs/promises';
14
+ import { approvalBus } from './approval-bus.js';
15
+ import { logger as rootLogger } from './logger.js';
16
+ const log = rootLogger.child({ component: 'session' });
17
+ const SESSIONS_DIR = join(homedir(), '.im-hub', 'sessions');
18
+ function sanitizeKey(raw) {
19
+ return raw.replace(/[^A-Za-z0-9_-]/g, (c) => {
20
+ return createHash('sha256').update(c).digest('hex').slice(0, 8);
21
+ });
22
+ }
23
+ function sessionFilePath(key) {
24
+ const safe = sanitizeKey(key);
25
+ return join(SESSIONS_DIR, `${safe}.json`);
26
+ }
27
+ function sessionLogPath(key) {
28
+ const safe = sanitizeKey(key);
29
+ return join(SESSIONS_DIR, `${safe}.log`);
30
+ }
31
+ // Two-tier TTL (split out to fix the "agent drift after long pause" issue):
32
+ //
33
+ // MESSAGES_TTL — how long the in-memory chat history sticks around before
34
+ // we drop it from RAM and delete the .log file. Short by
35
+ // default (30 min) because a long pause usually means the
36
+ // user has switched topics; replaying stale messages back to
37
+ // the agent just bloats the prompt.
38
+ //
39
+ // META_TTL — how long the *session metadata* (agent, model, variant,
40
+ // claudeSessionId, claudeSessionPrimed, usage stats) lives
41
+ // on disk. Long by default (7 days) so the thread's "sticky
42
+ // agent" and resumable claude-code session id survive
43
+ // overnight / weekend gaps. Without this, a 30-minute
44
+ // silence followed by a coding-keyword message would
45
+ // re-classify and switch agents (e.g. claude-code → opencode).
46
+ //
47
+ // Both are env-overridable for ops tuning.
48
+ function envInt(name, fallback) {
49
+ const raw = process.env[name];
50
+ if (!raw)
51
+ return fallback;
52
+ const n = parseInt(raw, 10);
53
+ return Number.isFinite(n) && n > 0 ? n : fallback;
54
+ }
55
+ const MESSAGES_TTL = envInt('IMHUB_SESSION_MESSAGES_TTL_MS', 30 * 60 * 1000);
56
+ const META_TTL = envInt('IMHUB_SESSION_META_TTL_MS', 7 * 24 * 60 * 60 * 1000);
57
+ // Back-compat: external callers (tests, schedule.ts) used to import DEFAULT_TTL
58
+ // to mean "the one ttl". Keep the symbol pointing at META_TTL so anywhere it
59
+ // still appears in logs/metrics gets the long-lived value.
60
+ const DEFAULT_TTL = META_TTL;
61
+ const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
62
+ function metaStale(s, now = Date.now()) {
63
+ return now - s.lastActivity.getTime() > META_TTL;
64
+ }
65
+ function messagesStale(s, now = Date.now()) {
66
+ return now - s.lastActivity.getTime() > MESSAGES_TTL;
67
+ }
68
+ class SessionManager {
69
+ sessions = new Map();
70
+ cleanupTimer;
71
+ /**
72
+ * Per-key promise chain used to serialize writes that perform a
73
+ * read-modify-write on the in-memory session + persistent JSONL log.
74
+ *
75
+ * Without this, two concurrent {@link addMessage} calls on the same key
76
+ * could interleave: A reads cached session, B reads cached session, both
77
+ * push a different message into the same array, then both write — the
78
+ * later write wins and the earlier message is lost from disk (the
79
+ * in-memory copy is fine because both pushes happened on the same object
80
+ * reference, but the JSONL log only reflects the most recent appendFile +
81
+ * meta save).
82
+ *
83
+ * The lock is keyed by `${platform}:${channelId}:${threadId}`; different
84
+ * threads still proceed in parallel.
85
+ */
86
+ writeQueues = new Map();
87
+ async start() {
88
+ // Ensure sessions directory exists
89
+ await mkdir(SESSIONS_DIR, { recursive: true });
90
+ // Start cleanup timer
91
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
92
+ log.info({ dir: SESSIONS_DIR }, 'Session manager started');
93
+ }
94
+ stop() {
95
+ if (this.cleanupTimer) {
96
+ clearInterval(this.cleanupTimer);
97
+ }
98
+ }
99
+ /**
100
+ * Run `fn` while holding a per-key serial lock. Subsequent calls with the
101
+ * same key wait until prior ones settle (success OR failure). Returns the
102
+ * value of `fn`. Internal use only — keeps {@link addMessage} race-free
103
+ * without introducing an external dep like p-queue.
104
+ */
105
+ async withLock(key, fn) {
106
+ const prev = this.writeQueues.get(key) ?? Promise.resolve();
107
+ // Chain even on failure so a thrown error in one segment doesn't poison
108
+ // subsequent waiters with that same rejection.
109
+ const next = prev.then(fn, fn);
110
+ this.writeQueues.set(key, next);
111
+ try {
112
+ return await next;
113
+ }
114
+ finally {
115
+ // Only clear if we're still the tail; another caller may have queued
116
+ // behind us in the meantime and we shouldn't drop their reference.
117
+ if (this.writeQueues.get(key) === next) {
118
+ this.writeQueues.delete(key);
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Get or create a session for a conversation
124
+ * Session key: `${platform}:${channelId}:${threadId}`
125
+ */
126
+ async getOrCreateSession(platform, channelId, threadId, agent) {
127
+ const key = `${platform}:${channelId}:${threadId}`;
128
+ const now = new Date();
129
+ // Check memory cache
130
+ let session = this.sessions.get(key);
131
+ if (session) {
132
+ if (metaStale(session, now.getTime())) {
133
+ session = undefined; // fully expired → create new below
134
+ }
135
+ else {
136
+ if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
137
+ session.messages = [];
138
+ try {
139
+ await unlink(sessionLogPath(key));
140
+ }
141
+ catch { /* no log to drop */ }
142
+ }
143
+ session.lastActivity = now;
144
+ await this.saveSessionMeta(key, session);
145
+ return session;
146
+ }
147
+ }
148
+ // Try loading from disk
149
+ session = await this.loadSession(key);
150
+ if (session && !metaStale(session, now.getTime())) {
151
+ if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
152
+ session.messages = [];
153
+ try {
154
+ await unlink(sessionLogPath(key));
155
+ }
156
+ catch { /* no log to drop */ }
157
+ }
158
+ session.lastActivity = now;
159
+ this.sessions.set(key, session);
160
+ await this.saveSessionMeta(key, session);
161
+ return session;
162
+ }
163
+ // Create new session
164
+ session = {
165
+ id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
166
+ channelId,
167
+ threadId,
168
+ platform,
169
+ agent,
170
+ createdAt: now,
171
+ lastActivity: now,
172
+ ttl: DEFAULT_TTL,
173
+ messages: [],
174
+ };
175
+ this.sessions.set(key, session);
176
+ await this.saveSession(key, session);
177
+ return session;
178
+ }
179
+ /**
180
+ * Get existing session without creating a new one
181
+ * Returns undefined if no session exists or it's expired
182
+ */
183
+ async getExistingSession(platform, channelId, threadId) {
184
+ const key = `${platform}:${channelId}:${threadId}`;
185
+ const now = new Date();
186
+ // Check memory cache
187
+ let session = this.sessions.get(key);
188
+ if (session) {
189
+ if (metaStale(session, now.getTime())) {
190
+ return undefined;
191
+ }
192
+ if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
193
+ // Drop stale chat history but preserve metadata (sticky agent,
194
+ // claudeSessionId etc.) — that's the whole point of META_TTL.
195
+ session.messages = [];
196
+ try {
197
+ await unlink(sessionLogPath(key));
198
+ }
199
+ catch { /* ignore */ }
200
+ }
201
+ return session;
202
+ }
203
+ // Try loading from disk
204
+ session = await this.loadSession(key);
205
+ if (session && !metaStale(session, now.getTime())) {
206
+ if (messagesStale(session, now.getTime()) && session.messages.length > 0) {
207
+ session.messages = [];
208
+ try {
209
+ await unlink(sessionLogPath(key));
210
+ }
211
+ catch { /* ignore */ }
212
+ }
213
+ this.sessions.set(key, session);
214
+ return session;
215
+ }
216
+ return undefined;
217
+ }
218
+ /**
219
+ * Switch the agent for a session.
220
+ *
221
+ * Generates a new session id but preserves thread identity AND every
222
+ * thread-level field that isn't agent-specific:
223
+ * - usage (per-thread /stats roll-up)
224
+ * - subtasks/active (subtask state lives at thread level)
225
+ * - claudeSessionId (Claude UUID survives /oc → /cc round-trips so the
226
+ * underlying ~/.claude/projects jsonl keeps continuing
227
+ * when the user comes back to claude)
228
+ *
229
+ * `model` and `variant` are reset because they live in different namespaces
230
+ * across CLIs (`opencode` model ≠ `claude` model); carrying them across
231
+ * would just feed the new agent an unrecognized argument.
232
+ */
233
+ async switchAgent(platform, channelId, threadId, newAgent) {
234
+ const key = `${platform}:${channelId}:${threadId}`;
235
+ // Get existing session or create new
236
+ const existing = this.sessions.get(key) || await this.loadSession(key);
237
+ const now = new Date();
238
+ const session = {
239
+ id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
240
+ channelId,
241
+ threadId,
242
+ platform,
243
+ agent: newAgent,
244
+ createdAt: existing?.createdAt || now,
245
+ lastActivity: now,
246
+ ttl: DEFAULT_TTL,
247
+ messages: existing?.messages || [],
248
+ usage: existing?.usage,
249
+ activeSubtaskId: existing?.activeSubtaskId,
250
+ subtasks: existing?.subtasks,
251
+ subtaskCounter: existing?.subtaskCounter,
252
+ claudeSessionId: existing?.claudeSessionId,
253
+ claudeSessionPrimed: existing?.claudeSessionPrimed,
254
+ opencodeSessionId: existing?.opencodeSessionId,
255
+ codexSessionId: existing?.codexSessionId,
256
+ planMode: existing?.planMode,
257
+ };
258
+ this.sessions.set(key, session);
259
+ await this.saveSession(key, session);
260
+ return session;
261
+ }
262
+ /**
263
+ * Append a message to the session history.
264
+ *
265
+ * Performance: instead of re-serializing the entire session JSON every
266
+ * turn, the message body is appended to a JSONL log file alongside the
267
+ * metadata (which gets a tiny atomic update for `lastActivity`).
268
+ *
269
+ * Concurrency: the entire read-modify-write is serialized per session key
270
+ * via {@link withLock} so two concurrent calls on the same thread (e.g.
271
+ * the IM message landing event AND a tool-result event arriving within
272
+ * the same ms) cannot lose either message to a lost-update race.
273
+ */
274
+ async addMessage(platform, channelId, threadId, message) {
275
+ const key = `${platform}:${channelId}:${threadId}`;
276
+ return this.withLock(key, async () => {
277
+ const session = this.sessions.get(key) || await this.loadSession(key);
278
+ if (!session)
279
+ return;
280
+ session.messages.push(message);
281
+ session.lastActivity = new Date();
282
+ this.sessions.set(key, session);
283
+ // Append-only log avoids rewriting the entire history per turn.
284
+ try {
285
+ await appendFile(sessionLogPath(key), JSON.stringify(message) + '\n');
286
+ }
287
+ catch {
288
+ // Disk error → fall back to full save which will catch it again
289
+ }
290
+ // Persist metadata only (now small & cheap).
291
+ await this.saveSessionMeta(key, session);
292
+ });
293
+ }
294
+ /**
295
+ * Persist `model` / `variant` / arbitrary patchable fields. Used by
296
+ * `/model`, `/think` etc so the change survives a restart between turns.
297
+ * Mutates the in-memory session in place AND writes metadata atomically.
298
+ */
299
+ async patchSession(platform, channelId, threadId, patch) {
300
+ const key = `${platform}:${channelId}:${threadId}`;
301
+ const session = this.sessions.get(key) || await this.loadSession(key);
302
+ if (!session)
303
+ return undefined;
304
+ if (patch.model !== undefined)
305
+ session.model = patch.model || undefined;
306
+ if (patch.variant !== undefined)
307
+ session.variant = patch.variant || undefined;
308
+ if (patch.agent !== undefined)
309
+ session.agent = patch.agent;
310
+ if (patch.planMode !== undefined) {
311
+ // Normalize to canonical shape: true keeps the flag, false drops it.
312
+ // Storing only the truthy state keeps the on-disk JSON small and lets
313
+ // a missing field unambiguously mean "off".
314
+ if (patch.planMode)
315
+ session.planMode = true;
316
+ else
317
+ delete session.planMode;
318
+ }
319
+ session.lastActivity = new Date();
320
+ this.sessions.set(key, session);
321
+ await this.saveSessionMeta(key, session);
322
+ return session;
323
+ }
324
+ /**
325
+ * Persist claude-code resumable session bookkeeping (UUID + primed flag).
326
+ * Returns the updated session, or undefined if no session exists yet for
327
+ * this thread. Caller is expected to ensure the session exists first.
328
+ */
329
+ async setClaudeSessionId(platform, channelId, threadId, claudeSessionId) {
330
+ const key = `${platform}:${channelId}:${threadId}`;
331
+ const session = this.sessions.get(key) || await this.loadSession(key);
332
+ if (!session)
333
+ return undefined;
334
+ session.claudeSessionId = claudeSessionId;
335
+ session.lastActivity = new Date();
336
+ this.sessions.set(key, session);
337
+ await this.saveSessionMeta(key, session);
338
+ return session;
339
+ }
340
+ async markClaudeSessionPrimed(platform, channelId, threadId) {
341
+ const key = `${platform}:${channelId}:${threadId}`;
342
+ const session = this.sessions.get(key) || await this.loadSession(key);
343
+ if (!session || session.claudeSessionPrimed)
344
+ return;
345
+ session.claudeSessionPrimed = true;
346
+ session.lastActivity = new Date();
347
+ this.sessions.set(key, session);
348
+ await this.saveSessionMeta(key, session);
349
+ }
350
+ /**
351
+ * Persist opencode's native session id (`ses_…`) once we've seen it in the
352
+ * adapter's stream. Idempotent — calling with the same id is a no-op so
353
+ * the per-event callback can fire as many times as opencode sends events.
354
+ */
355
+ async setOpencodeSessionId(platform, channelId, threadId, opencodeSessionId) {
356
+ const key = `${platform}:${channelId}:${threadId}`;
357
+ const session = this.sessions.get(key) || await this.loadSession(key);
358
+ if (!session)
359
+ return undefined;
360
+ if (session.opencodeSessionId === opencodeSessionId)
361
+ return session;
362
+ session.opencodeSessionId = opencodeSessionId;
363
+ session.lastActivity = new Date();
364
+ this.sessions.set(key, session);
365
+ await this.saveSessionMeta(key, session);
366
+ return session;
367
+ }
368
+ /**
369
+ * Persist codex's native thread id (UUID) once we've seen it in the
370
+ * adapter's `thread.started` event. Idempotent — same id may fire multiple
371
+ * times per spawn. Mirrors setOpencodeSessionId.
372
+ */
373
+ async setCodexSessionId(platform, channelId, threadId, codexSessionId) {
374
+ const key = `${platform}:${channelId}:${threadId}`;
375
+ const session = this.sessions.get(key) || await this.loadSession(key);
376
+ if (!session)
377
+ return undefined;
378
+ if (session.codexSessionId === codexSessionId)
379
+ return session;
380
+ session.codexSessionId = codexSessionId;
381
+ session.lastActivity = new Date();
382
+ this.sessions.set(key, session);
383
+ await this.saveSessionMeta(key, session);
384
+ return session;
385
+ }
386
+ /**
387
+ * Increment the per-session usage roll-up after a successful agent
388
+ * invocation. Used by router.callAgentWithHistory to power /stats.
389
+ */
390
+ async recordUsage(platform, channelId, threadId, delta) {
391
+ const key = `${platform}:${channelId}:${threadId}`;
392
+ const session = this.sessions.get(key) || await this.loadSession(key);
393
+ if (!session)
394
+ return;
395
+ if (!session.usage) {
396
+ session.usage = {
397
+ turns: 0,
398
+ costUsd: 0,
399
+ promptChars: 0,
400
+ responseChars: 0,
401
+ durationMsTotal: 0,
402
+ startedAt: new Date().toISOString(),
403
+ };
404
+ }
405
+ session.usage.turns += 1;
406
+ session.usage.costUsd += Number.isFinite(delta.costUsd) ? delta.costUsd : 0;
407
+ session.usage.promptChars += Number.isFinite(delta.promptChars) ? delta.promptChars : 0;
408
+ session.usage.responseChars += Number.isFinite(delta.responseChars) ? delta.responseChars : 0;
409
+ session.usage.durationMsTotal += Number.isFinite(delta.durationMs) ? delta.durationMs : 0;
410
+ session.lastActivity = new Date();
411
+ this.sessions.set(key, session);
412
+ await this.saveSessionMeta(key, session);
413
+ }
414
+ /**
415
+ * Reset conversation history (keep session but clear messages)
416
+ */
417
+ async resetConversation(platform, channelId, threadId) {
418
+ const key = `${platform}:${channelId}:${threadId}`;
419
+ const session = this.sessions.get(key) || await this.loadSession(key);
420
+ if (session) {
421
+ session.messages = [];
422
+ session.lastActivity = new Date();
423
+ session.id = `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`; // New session ID
424
+ // Forget the old per-agent CLI sessions — /new should give a clean slate
425
+ // for both Claude (`--resume`) and opencode (`--session`).
426
+ delete session.claudeSessionId;
427
+ delete session.claudeSessionPrimed;
428
+ delete session.opencodeSessionId;
429
+ delete session.codexSessionId;
430
+ // Plan mode is per-conversation intent ("先规划再动手") — a fresh
431
+ // conversation always starts at "off" so users don't get a surprising
432
+ // read-only run after /new.
433
+ delete session.planMode;
434
+ // Drop any per-thread auto-allow approval rules so the new conversation
435
+ // starts back at "ask every time".
436
+ try {
437
+ approvalBus.clearAutoAllowForThread(threadId);
438
+ }
439
+ catch { /* ignore */ }
440
+ this.sessions.set(key, session);
441
+ await this.saveSession(key, session);
442
+ return session;
443
+ }
444
+ return undefined;
445
+ }
446
+ /**
447
+ * Get session with messages (convenience method)
448
+ */
449
+ async getSessionWithHistory(platform, channelId, threadId) {
450
+ const session = await this.getExistingSession(platform, channelId, threadId);
451
+ if (session) {
452
+ return { session, messages: session.messages };
453
+ }
454
+ return undefined;
455
+ }
456
+ /**
457
+ * Create or get a subtask session (independent from parent).
458
+ */
459
+ async getOrCreateSubSession(platform, channelId, threadId, subtaskId, agent) {
460
+ const key = `${platform}:${channelId}:${threadId}:sub:${subtaskId}`;
461
+ const now = new Date();
462
+ let session = this.sessions.get(key) || await this.loadSession(key);
463
+ if (session) {
464
+ session.lastActivity = now;
465
+ return session;
466
+ }
467
+ session = {
468
+ id: `sub-${platform}-${channelId}-${threadId}-${subtaskId}`,
469
+ channelId, threadId, platform, agent,
470
+ createdAt: now, lastActivity: now, ttl: DEFAULT_TTL, messages: [],
471
+ };
472
+ this.sessions.set(key, session);
473
+ await this.saveSession(key, session);
474
+ return session;
475
+ }
476
+ /**
477
+ * Set active subtask id on parent session — subsequent messages route to the subtask.
478
+ */
479
+ async setActiveSubtask(platform, channelId, threadId, taskId) {
480
+ const key = `${platform}:${channelId}:${threadId}`;
481
+ const session = this.sessions.get(key) || await this.loadSession(key);
482
+ if (!session)
483
+ return;
484
+ session.activeSubtaskId = taskId;
485
+ this.sessions.set(key, session);
486
+ await this.saveSession(key, session);
487
+ }
488
+ /**
489
+ * Get subtask metadata list from parent session.
490
+ */
491
+ async getSubtasks(platform, channelId, threadId) {
492
+ const key = `${platform}:${channelId}:${threadId}`;
493
+ const session = this.sessions.get(key) || await this.loadSession(key);
494
+ return session?.subtasks || [];
495
+ }
496
+ /**
497
+ * Scan all session files on disk and return every subtask, flattened, with
498
+ * its parent platform/channelId/threadId/agent attached so the dashboard
499
+ * can render subtasks across all conversations.
500
+ *
501
+ * Session files live as `<sanitized-key>.json` under SESSIONS_DIR. The
502
+ * sanitized key is one-way (sha256-prefix per non-alnum char), so we
503
+ * cannot reverse it — but each session file preserves the original
504
+ * platform/channelId/threadId fields, which is what we need.
505
+ */
506
+ async listAllSubtasks(opts = {}) {
507
+ let names;
508
+ try {
509
+ names = await readdir(SESSIONS_DIR);
510
+ }
511
+ catch {
512
+ return [];
513
+ }
514
+ const out = [];
515
+ for (const name of names) {
516
+ if (!name.endsWith('.json'))
517
+ continue;
518
+ try {
519
+ const raw = await readFile(join(SESSIONS_DIR, name), 'utf-8');
520
+ const parsed = JSON.parse(raw);
521
+ if (!parsed.subtasks?.length)
522
+ continue;
523
+ // Filter by parent-agent up-front so we don't allocate items we'll
524
+ // discard. Subtasks inherit the parent session's agent — there is
525
+ // no per-subtask agent override today.
526
+ if (opts.agent && (parsed.agent || '') !== opts.agent)
527
+ continue;
528
+ for (const st of parsed.subtasks) {
529
+ out.push({
530
+ ...st,
531
+ createdAt: st.createdAt ? new Date(st.createdAt) : new Date(0),
532
+ completedAt: st.completedAt ? new Date(st.completedAt) : undefined,
533
+ platform: parsed.platform || '',
534
+ channelId: parsed.channelId || '',
535
+ threadId: parsed.threadId || '',
536
+ parentAgent: parsed.agent || '',
537
+ parentSessionId: parsed.id || '',
538
+ });
539
+ }
540
+ }
541
+ catch {
542
+ // skip corrupt session file
543
+ }
544
+ }
545
+ // newest first
546
+ out.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
547
+ return out;
548
+ }
549
+ /**
550
+ * Update subtask metadata in parent session.
551
+ */
552
+ async updateSubtask(platform, channelId, threadId, taskId, patch) {
553
+ const key = `${platform}:${channelId}:${threadId}`;
554
+ const session = this.sessions.get(key) || await this.loadSession(key);
555
+ if (!session)
556
+ return;
557
+ if (!session.subtasks) {
558
+ session.subtasks = [];
559
+ }
560
+ const idx = session.subtasks.findIndex(s => s.id === taskId);
561
+ if (idx >= 0) {
562
+ session.subtasks[idx] = { ...session.subtasks[idx], ...patch };
563
+ }
564
+ else {
565
+ session.subtasks.push({ id: taskId, ...patch });
566
+ }
567
+ this.sessions.set(key, session);
568
+ await this.saveSession(key, session);
569
+ }
570
+ /**
571
+ * Get next subtask id and persist the increment.
572
+ *
573
+ * Previously returned 1 when the parent session didn't exist yet, but
574
+ * never created one — second call returned 1 again, leading to subtask
575
+ * id collisions. Now we lazy-create the parent session so the counter
576
+ * increments durably from the first call.
577
+ */
578
+ async nextSubtaskId(platform, channelId, threadId, agent = '') {
579
+ const key = `${platform}:${channelId}:${threadId}`;
580
+ let session = this.sessions.get(key) || await this.loadSession(key);
581
+ if (!session) {
582
+ const now = new Date();
583
+ session = {
584
+ id: `${platform}-${channelId}-${threadId}-${Date.now()}-${randomBytes(4).toString('hex')}`,
585
+ channelId, threadId, platform, agent,
586
+ createdAt: now, lastActivity: now, ttl: DEFAULT_TTL, messages: [],
587
+ subtaskCounter: 0,
588
+ };
589
+ }
590
+ session.subtaskCounter = (session.subtaskCounter || 0) + 1;
591
+ this.sessions.set(key, session);
592
+ await this.saveSession(key, session);
593
+ return session.subtaskCounter;
594
+ }
595
+ /**
596
+ * Persist the full session (metadata + messages). Used for the legacy
597
+ * one-file format on resetConversation() and switchAgent() — anywhere
598
+ * the messages array itself was rewritten. Atomic via tmp+rename.
599
+ */
600
+ async saveSession(key, session) {
601
+ await this.saveSessionMeta(key, session);
602
+ // Rewrite the JSONL log to match the in-memory messages array. This is
603
+ // only called from paths that actually mutate `messages` wholesale
604
+ // (resetConversation, switchAgent). addMessage uses appendFile which
605
+ // is far cheaper for the hot path.
606
+ const logPath = sessionLogPath(key);
607
+ try {
608
+ const lines = session.messages.map((m) => JSON.stringify(m)).join('\n');
609
+ await this.atomicWrite(logPath, lines + (lines ? '\n' : ''));
610
+ }
611
+ catch {
612
+ // disk failure — in-memory state is still authoritative
613
+ }
614
+ }
615
+ /** Persist metadata only (no messages payload), atomically. */
616
+ async saveSessionMeta(key, session) {
617
+ const filePath = sessionFilePath(key);
618
+ try {
619
+ const meta = {
620
+ id: session.id,
621
+ channelId: session.channelId,
622
+ threadId: session.threadId,
623
+ platform: session.platform,
624
+ agent: session.agent,
625
+ model: session.model,
626
+ variant: session.variant,
627
+ usage: session.usage,
628
+ createdAt: session.createdAt,
629
+ lastActivity: session.lastActivity,
630
+ ttl: session.ttl,
631
+ activeSubtaskId: session.activeSubtaskId,
632
+ subtasks: session.subtasks,
633
+ subtaskCounter: session.subtaskCounter,
634
+ claudeSessionId: session.claudeSessionId,
635
+ claudeSessionPrimed: session.claudeSessionPrimed,
636
+ opencodeSessionId: session.opencodeSessionId,
637
+ codexSessionId: session.codexSessionId,
638
+ planMode: session.planMode,
639
+ messageCount: session.messages.length,
640
+ };
641
+ await this.atomicWrite(filePath, JSON.stringify(meta, null, 2));
642
+ }
643
+ catch {
644
+ // ignore
645
+ }
646
+ }
647
+ /** Crash-safe write: tmp file + atomic rename.
648
+ *
649
+ * Recovers from ENOENT (parent dir missing) by mkdir-recursive + retry
650
+ * exactly once. This keeps the manager robust to environments where
651
+ * start() wasn't called yet — tests in particular tend to import
652
+ * sessionManager without going through the start() lifecycle, and
653
+ * saveSessionMeta's outer try/catch would otherwise swallow the ENOENT
654
+ * and silently produce a no-op write (the bug that caused
655
+ * session-subtasks.test.ts to fail on a fresh CI runner). */
656
+ async atomicWrite(filePath, contents) {
657
+ const tmp = `${filePath}.${randomBytes(4).toString('hex')}.tmp`;
658
+ try {
659
+ await writeFile(tmp, contents);
660
+ }
661
+ catch (err) {
662
+ if (err.code === 'ENOENT') {
663
+ await mkdir(SESSIONS_DIR, { recursive: true });
664
+ await writeFile(tmp, contents);
665
+ }
666
+ else {
667
+ throw err;
668
+ }
669
+ }
670
+ try {
671
+ await rename(tmp, filePath);
672
+ }
673
+ catch (err) {
674
+ try {
675
+ await unlink(tmp);
676
+ }
677
+ catch { /* ignore */ }
678
+ throw err;
679
+ }
680
+ }
681
+ async loadSession(key) {
682
+ const filePath = sessionFilePath(key);
683
+ try {
684
+ const data = await readFile(filePath, 'utf-8');
685
+ const parsed = JSON.parse(data);
686
+ const session = {
687
+ id: parsed.id,
688
+ channelId: parsed.channelId,
689
+ threadId: parsed.threadId,
690
+ platform: parsed.platform,
691
+ agent: parsed.agent,
692
+ model: parsed.model,
693
+ variant: parsed.variant,
694
+ usage: parsed.usage,
695
+ createdAt: new Date(parsed.createdAt),
696
+ lastActivity: new Date(parsed.lastActivity),
697
+ ttl: parsed.ttl,
698
+ messages: parsed.messages || [], // legacy one-file format
699
+ activeSubtaskId: parsed.activeSubtaskId,
700
+ subtasks: parsed.subtasks,
701
+ subtaskCounter: parsed.subtaskCounter,
702
+ claudeSessionId: parsed.claudeSessionId,
703
+ claudeSessionPrimed: parsed.claudeSessionPrimed,
704
+ opencodeSessionId: parsed.opencodeSessionId,
705
+ codexSessionId: parsed.codexSessionId,
706
+ planMode: parsed.planMode,
707
+ };
708
+ // Convert message timestamps from legacy format if present
709
+ session.messages = session.messages.map((msg) => ({
710
+ ...msg,
711
+ timestamp: new Date(msg.timestamp),
712
+ }));
713
+ // Then merge in JSONL log entries (new format).
714
+ try {
715
+ const log = await readFile(sessionLogPath(key), 'utf-8');
716
+ const logged = [];
717
+ for (const line of log.split('\n')) {
718
+ if (!line.trim())
719
+ continue;
720
+ try {
721
+ const m = JSON.parse(line);
722
+ logged.push({ ...m, timestamp: new Date(m.timestamp) });
723
+ }
724
+ catch { /* skip corrupt line */ }
725
+ }
726
+ // The log is authoritative for new-format sessions. If both exist
727
+ // (rare, after a save followed by addMessage), the log wins.
728
+ if (logged.length > 0) {
729
+ session.messages = logged;
730
+ }
731
+ }
732
+ catch {
733
+ // No log file — legacy format only
734
+ }
735
+ return session;
736
+ }
737
+ catch {
738
+ return undefined;
739
+ }
740
+ }
741
+ async cleanup() {
742
+ const now = Date.now();
743
+ for (const [key, session] of this.sessions.entries()) {
744
+ const idle = now - session.lastActivity.getTime();
745
+ if (idle > META_TTL) {
746
+ // Full eviction: thread truly cold. Drop both files + cache entry.
747
+ this.sessions.delete(key);
748
+ const filePath = sessionFilePath(key);
749
+ const logPath = sessionLogPath(key);
750
+ try {
751
+ await unlink(filePath);
752
+ }
753
+ catch { /* ignore */ }
754
+ try {
755
+ await unlink(logPath);
756
+ }
757
+ catch { /* ignore */ }
758
+ }
759
+ else if (idle > MESSAGES_TTL && session.messages.length > 0) {
760
+ // Messages-only eviction: keep sticky agent / claudeSessionId on disk
761
+ // (meta file untouched), drop chat log + in-memory messages so the
762
+ // next turn starts with a fresh history but the same routing.
763
+ session.messages = [];
764
+ const logPath = sessionLogPath(key);
765
+ try {
766
+ await unlink(logPath);
767
+ }
768
+ catch { /* ignore */ }
769
+ }
770
+ }
771
+ }
772
+ }
773
+ export const sessionManager = new SessionManager();
774
+ //# sourceMappingURL=session.js.map