charness 3.2.0 → 3.3.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 (313) hide show
  1. package/.env.example +3 -6
  2. package/DEVELOPMENT.md +15 -0
  3. package/README.md +111 -38
  4. package/SETUP.md +121 -16
  5. package/dist/adapters/adapterEventRouting.d.ts +2 -1
  6. package/dist/adapters/adapterEventRouting.d.ts.map +1 -1
  7. package/dist/adapters/adapterEventRouting.js +3 -0
  8. package/dist/adapters/adapterEventRouting.js.map +1 -1
  9. package/dist/adapters/adapterNames.d.ts +13 -0
  10. package/dist/adapters/adapterNames.d.ts.map +1 -1
  11. package/dist/adapters/adapterNames.js +16 -0
  12. package/dist/adapters/adapterNames.js.map +1 -1
  13. package/dist/adapters/backendUnavailableError.d.ts +11 -0
  14. package/dist/adapters/backendUnavailableError.d.ts.map +1 -0
  15. package/dist/adapters/backendUnavailableError.js +19 -0
  16. package/dist/adapters/backendUnavailableError.js.map +1 -0
  17. package/dist/adapters/claudeCliAdapter.d.ts +29 -6
  18. package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
  19. package/dist/adapters/claudeCliAdapter.js +70 -16
  20. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  21. package/dist/adapters/claudeJsonStreamAdapter.d.ts +36 -3
  22. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  23. package/dist/adapters/claudeJsonStreamAdapter.js +93 -30
  24. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  25. package/dist/adapters/createAdapter.d.ts +3 -14
  26. package/dist/adapters/createAdapter.d.ts.map +1 -1
  27. package/dist/adapters/createAdapter.js +9 -19
  28. package/dist/adapters/createAdapter.js.map +1 -1
  29. package/dist/adapters/openCodeAdapter.d.ts +140 -12
  30. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  31. package/dist/adapters/openCodeAdapter.js +385 -119
  32. package/dist/adapters/openCodeAdapter.js.map +1 -1
  33. package/dist/apiErrorRetry.d.ts +70 -6
  34. package/dist/apiErrorRetry.d.ts.map +1 -1
  35. package/dist/apiErrorRetry.js +312 -34
  36. package/dist/apiErrorRetry.js.map +1 -1
  37. package/dist/apiRetryKick.d.ts +26 -1
  38. package/dist/apiRetryKick.d.ts.map +1 -1
  39. package/dist/apiRetryKick.js +41 -2
  40. package/dist/apiRetryKick.js.map +1 -1
  41. package/dist/bot.d.ts +2 -1
  42. package/dist/bot.d.ts.map +1 -1
  43. package/dist/bot.js +736 -184
  44. package/dist/bot.js.map +1 -1
  45. package/dist/cli/claudeProfiles.d.ts +1 -1
  46. package/dist/cli/claudeProfiles.js +1 -1
  47. package/dist/cli/hot.d.ts +7 -1
  48. package/dist/cli/hot.d.ts.map +1 -1
  49. package/dist/cli/hot.js +29 -3
  50. package/dist/cli/hot.js.map +1 -1
  51. package/dist/configDirFile.d.ts +73 -0
  52. package/dist/configDirFile.d.ts.map +1 -0
  53. package/dist/configDirFile.js +216 -0
  54. package/dist/configDirFile.js.map +1 -0
  55. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +2 -2
  56. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -1
  57. package/dist/connectors/telegram/commands/autoContinueLimits.js +4 -4
  58. package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -1
  59. package/dist/connectors/telegram/commands/backendSwitch.d.ts +25 -0
  60. package/dist/connectors/telegram/commands/backendSwitch.d.ts.map +1 -0
  61. package/dist/connectors/telegram/commands/backendSwitch.js +104 -0
  62. package/dist/connectors/telegram/commands/backendSwitch.js.map +1 -0
  63. package/dist/connectors/telegram/commands/callbackMessage.d.ts +12 -0
  64. package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
  65. package/dist/connectors/telegram/commands/callbackMessage.js +24 -0
  66. package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
  67. package/dist/connectors/telegram/commands/callbackToast.d.ts +36 -0
  68. package/dist/connectors/telegram/commands/callbackToast.d.ts.map +1 -0
  69. package/dist/connectors/telegram/commands/callbackToast.js +83 -0
  70. package/dist/connectors/telegram/commands/callbackToast.js.map +1 -0
  71. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +18 -0
  72. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
  73. package/dist/connectors/telegram/commands/claudeAccounts.js +220 -66
  74. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
  75. package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
  76. package/dist/connectors/telegram/commands/displayModes.js +4 -2
  77. package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
  78. package/dist/connectors/telegram/commands/fallbackChain.d.ts +5 -8
  79. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
  80. package/dist/connectors/telegram/commands/fallbackChain.js +183 -193
  81. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
  82. package/dist/connectors/telegram/commands/limitsBoard.d.ts +40 -0
  83. package/dist/connectors/telegram/commands/limitsBoard.d.ts.map +1 -0
  84. package/dist/connectors/telegram/commands/limitsBoard.js +281 -0
  85. package/dist/connectors/telegram/commands/limitsBoard.js.map +1 -0
  86. package/dist/connectors/telegram/commands/modelPage.d.ts +130 -0
  87. package/dist/connectors/telegram/commands/modelPage.d.ts.map +1 -0
  88. package/dist/connectors/telegram/commands/modelPage.js +120 -0
  89. package/dist/connectors/telegram/commands/modelPage.js.map +1 -0
  90. package/dist/connectors/telegram/commands/modelProviders.d.ts +9 -1
  91. package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -1
  92. package/dist/connectors/telegram/commands/modelProviders.js +203 -111
  93. package/dist/connectors/telegram/commands/modelProviders.js.map +1 -1
  94. package/dist/connectors/telegram/commands/stt.d.ts +32 -0
  95. package/dist/connectors/telegram/commands/stt.d.ts.map +1 -0
  96. package/dist/connectors/telegram/commands/stt.js +460 -0
  97. package/dist/connectors/telegram/commands/stt.js.map +1 -0
  98. package/dist/effortLevels.d.ts +57 -7
  99. package/dist/effortLevels.d.ts.map +1 -1
  100. package/dist/effortLevels.js +65 -9
  101. package/dist/effortLevels.js.map +1 -1
  102. package/dist/i18n/de.d.ts.map +1 -1
  103. package/dist/i18n/de.js +160 -17
  104. package/dist/i18n/de.js.map +1 -1
  105. package/dist/i18n/en.d.ts.map +1 -1
  106. package/dist/i18n/en.js +160 -17
  107. package/dist/i18n/en.js.map +1 -1
  108. package/dist/i18n/es.d.ts.map +1 -1
  109. package/dist/i18n/es.js +160 -17
  110. package/dist/i18n/es.js.map +1 -1
  111. package/dist/i18n/fr.d.ts.map +1 -1
  112. package/dist/i18n/fr.js +160 -17
  113. package/dist/i18n/fr.js.map +1 -1
  114. package/dist/i18n/hi.d.ts.map +1 -1
  115. package/dist/i18n/hi.js +160 -17
  116. package/dist/i18n/hi.js.map +1 -1
  117. package/dist/i18n/ja.d.ts.map +1 -1
  118. package/dist/i18n/ja.js +160 -17
  119. package/dist/i18n/ja.js.map +1 -1
  120. package/dist/i18n/ka.d.ts.map +1 -1
  121. package/dist/i18n/ka.js +160 -17
  122. package/dist/i18n/ka.js.map +1 -1
  123. package/dist/i18n/pt.d.ts.map +1 -1
  124. package/dist/i18n/pt.js +160 -17
  125. package/dist/i18n/pt.js.map +1 -1
  126. package/dist/i18n/ru.d.ts.map +1 -1
  127. package/dist/i18n/ru.js +160 -17
  128. package/dist/i18n/ru.js.map +1 -1
  129. package/dist/i18n/uk.d.ts.map +1 -1
  130. package/dist/i18n/uk.js +160 -17
  131. package/dist/i18n/uk.js.map +1 -1
  132. package/dist/i18n/uz.d.ts.map +1 -1
  133. package/dist/i18n/uz.js +160 -17
  134. package/dist/i18n/uz.js.map +1 -1
  135. package/dist/i18n/zh.d.ts.map +1 -1
  136. package/dist/i18n/zh.js +160 -17
  137. package/dist/i18n/zh.js.map +1 -1
  138. package/dist/installManager.d.ts +20 -4
  139. package/dist/installManager.d.ts.map +1 -1
  140. package/dist/installManager.js +82 -14
  141. package/dist/installManager.js.map +1 -1
  142. package/dist/instanceSettings.d.ts +52 -0
  143. package/dist/instanceSettings.d.ts.map +1 -0
  144. package/dist/instanceSettings.js +136 -0
  145. package/dist/instanceSettings.js.map +1 -0
  146. package/dist/pinnedStatus.d.ts +7 -1
  147. package/dist/pinnedStatus.d.ts.map +1 -1
  148. package/dist/pinnedStatus.js +1 -1
  149. package/dist/pinnedStatus.js.map +1 -1
  150. package/dist/requests/requestLedger.d.ts +8 -0
  151. package/dist/requests/requestLedger.d.ts.map +1 -1
  152. package/dist/requests/requestLedger.js +19 -1
  153. package/dist/requests/requestLedger.js.map +1 -1
  154. package/dist/scheduler/mcpSurface.js +4 -4
  155. package/dist/scheduler/mcpSurface.js.map +1 -1
  156. package/dist/shutdown.d.ts +14 -0
  157. package/dist/shutdown.d.ts.map +1 -1
  158. package/dist/shutdown.js +16 -0
  159. package/dist/shutdown.js.map +1 -1
  160. package/dist/state.d.ts +54 -2
  161. package/dist/state.d.ts.map +1 -1
  162. package/dist/state.js +62 -1
  163. package/dist/state.js.map +1 -1
  164. package/dist/stt/sttAutoInstall.d.ts +39 -0
  165. package/dist/stt/sttAutoInstall.d.ts.map +1 -0
  166. package/dist/stt/sttAutoInstall.js +158 -0
  167. package/dist/stt/sttAutoInstall.js.map +1 -0
  168. package/dist/stt/sttChildEnv.d.ts +11 -0
  169. package/dist/stt/sttChildEnv.d.ts.map +1 -0
  170. package/dist/stt/sttChildEnv.js +30 -0
  171. package/dist/stt/sttChildEnv.js.map +1 -0
  172. package/dist/stt/sttConfigEdit.d.ts +71 -0
  173. package/dist/stt/sttConfigEdit.d.ts.map +1 -0
  174. package/dist/stt/sttConfigEdit.js +144 -0
  175. package/dist/stt/sttConfigEdit.js.map +1 -0
  176. package/dist/stt/sttInstall.d.ts +147 -0
  177. package/dist/stt/sttInstall.d.ts.map +1 -0
  178. package/dist/stt/sttInstall.js +491 -0
  179. package/dist/stt/sttInstall.js.map +1 -0
  180. package/dist/stt/sttInstallEntry.d.ts +2 -0
  181. package/dist/stt/sttInstallEntry.d.ts.map +1 -0
  182. package/dist/stt/sttInstallEntry.js +31 -0
  183. package/dist/stt/sttInstallEntry.js.map +1 -0
  184. package/dist/stt/sttInstallReporter.d.ts +45 -0
  185. package/dist/stt/sttInstallReporter.d.ts.map +1 -0
  186. package/dist/stt/sttInstallReporter.js +57 -0
  187. package/dist/stt/sttInstallReporter.js.map +1 -0
  188. package/dist/stt/sttRuntime.d.ts +35 -0
  189. package/dist/stt/sttRuntime.d.ts.map +1 -0
  190. package/dist/stt/sttRuntime.js +73 -0
  191. package/dist/stt/sttRuntime.js.map +1 -0
  192. package/dist/stt/sttScreenPlan.d.ts +193 -0
  193. package/dist/stt/sttScreenPlan.d.ts.map +1 -0
  194. package/dist/stt/sttScreenPlan.js +541 -0
  195. package/dist/stt/sttScreenPlan.js.map +1 -0
  196. package/dist/stt/sttSettings.d.ts +201 -0
  197. package/dist/stt/sttSettings.d.ts.map +1 -0
  198. package/dist/stt/sttSettings.js +387 -0
  199. package/dist/stt/sttSettings.js.map +1 -0
  200. package/dist/stt/sttSidecarOwner.d.ts +115 -0
  201. package/dist/stt/sttSidecarOwner.d.ts.map +1 -0
  202. package/dist/stt/sttSidecarOwner.js +475 -0
  203. package/dist/stt/sttSidecarOwner.js.map +1 -0
  204. package/dist/stt/transcribeChain.d.ts +36 -0
  205. package/dist/stt/transcribeChain.d.ts.map +1 -0
  206. package/dist/stt/transcribeChain.js +42 -0
  207. package/dist/stt/transcribeChain.js.map +1 -0
  208. package/dist/types.d.ts +77 -4
  209. package/dist/types.d.ts.map +1 -1
  210. package/dist/utils/adapterSwitchSessionIds.d.ts +32 -0
  211. package/dist/utils/adapterSwitchSessionIds.d.ts.map +1 -0
  212. package/dist/utils/adapterSwitchSessionIds.js +44 -0
  213. package/dist/utils/adapterSwitchSessionIds.js.map +1 -0
  214. package/dist/utils/backendSwitchPlan.d.ts +63 -0
  215. package/dist/utils/backendSwitchPlan.d.ts.map +1 -0
  216. package/dist/utils/backendSwitchPlan.js +73 -0
  217. package/dist/utils/backendSwitchPlan.js.map +1 -0
  218. package/dist/utils/claudeAccountPickerPlan.d.ts +12 -1
  219. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
  220. package/dist/utils/claudeAccountPickerPlan.js +20 -1
  221. package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
  222. package/dist/utils/claudeTranscriptContent.d.ts +24 -0
  223. package/dist/utils/claudeTranscriptContent.d.ts.map +1 -0
  224. package/dist/utils/claudeTranscriptContent.js +107 -0
  225. package/dist/utils/claudeTranscriptContent.js.map +1 -0
  226. package/dist/utils/claudeUsage.d.ts +47 -8
  227. package/dist/utils/claudeUsage.d.ts.map +1 -1
  228. package/dist/utils/claudeUsage.js +76 -14
  229. package/dist/utils/claudeUsage.js.map +1 -1
  230. package/dist/utils/claudeUsageRefresher.d.ts +8 -1
  231. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
  232. package/dist/utils/claudeUsageRefresher.js +9 -4
  233. package/dist/utils/claudeUsageRefresher.js.map +1 -1
  234. package/dist/utils/effortPickerPlan.d.ts +54 -0
  235. package/dist/utils/effortPickerPlan.d.ts.map +1 -0
  236. package/dist/utils/effortPickerPlan.js +56 -0
  237. package/dist/utils/effortPickerPlan.js.map +1 -0
  238. package/dist/utils/ensureSessionPlan.d.ts +20 -4
  239. package/dist/utils/ensureSessionPlan.d.ts.map +1 -1
  240. package/dist/utils/ensureSessionPlan.js +17 -4
  241. package/dist/utils/ensureSessionPlan.js.map +1 -1
  242. package/dist/utils/fallbackBridge.d.ts +6 -2
  243. package/dist/utils/fallbackBridge.d.ts.map +1 -1
  244. package/dist/utils/fallbackBridge.js +10 -3
  245. package/dist/utils/fallbackBridge.js.map +1 -1
  246. package/dist/utils/fallbackChain.d.ts +36 -0
  247. package/dist/utils/fallbackChain.d.ts.map +1 -1
  248. package/dist/utils/fallbackChain.js +65 -1
  249. package/dist/utils/fallbackChain.js.map +1 -1
  250. package/dist/utils/fallbackSwitchNotice.d.ts +41 -0
  251. package/dist/utils/fallbackSwitchNotice.d.ts.map +1 -0
  252. package/dist/utils/fallbackSwitchNotice.js +75 -0
  253. package/dist/utils/fallbackSwitchNotice.js.map +1 -0
  254. package/dist/utils/fileSendService.d.ts +33 -10
  255. package/dist/utils/fileSendService.d.ts.map +1 -1
  256. package/dist/utils/fileSendService.js +78 -27
  257. package/dist/utils/fileSendService.js.map +1 -1
  258. package/dist/utils/jsonStreamHost.d.ts +39 -0
  259. package/dist/utils/jsonStreamHost.d.ts.map +1 -1
  260. package/dist/utils/jsonStreamHost.js +60 -1
  261. package/dist/utils/jsonStreamHost.js.map +1 -1
  262. package/dist/utils/limitEpisodeRecovery.d.ts +16 -2
  263. package/dist/utils/limitEpisodeRecovery.d.ts.map +1 -1
  264. package/dist/utils/limitEpisodeRecovery.js +44 -3
  265. package/dist/utils/limitEpisodeRecovery.js.map +1 -1
  266. package/dist/utils/limitSettings.d.ts +25 -0
  267. package/dist/utils/limitSettings.d.ts.map +1 -0
  268. package/dist/utils/limitSettings.js +31 -0
  269. package/dist/utils/limitSettings.js.map +1 -0
  270. package/dist/utils/limitsBoardPlan.d.ts +52 -0
  271. package/dist/utils/limitsBoardPlan.d.ts.map +1 -0
  272. package/dist/utils/limitsBoardPlan.js +93 -0
  273. package/dist/utils/limitsBoardPlan.js.map +1 -0
  274. package/dist/utils/openCodeUsage.d.ts +93 -0
  275. package/dist/utils/openCodeUsage.d.ts.map +1 -0
  276. package/dist/utils/openCodeUsage.js +248 -0
  277. package/dist/utils/openCodeUsage.js.map +1 -0
  278. package/dist/utils/operatorSwitchPlan.d.ts +61 -0
  279. package/dist/utils/operatorSwitchPlan.d.ts.map +1 -0
  280. package/dist/utils/operatorSwitchPlan.js +32 -0
  281. package/dist/utils/operatorSwitchPlan.js.map +1 -0
  282. package/dist/utils/promptDelivery.d.ts +40 -0
  283. package/dist/utils/promptDelivery.d.ts.map +1 -1
  284. package/dist/utils/promptDelivery.js +31 -0
  285. package/dist/utils/promptDelivery.js.map +1 -1
  286. package/dist/utils/recvPreviewRedaction.d.ts +14 -4
  287. package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
  288. package/dist/utils/recvPreviewRedaction.js +19 -7
  289. package/dist/utils/recvPreviewRedaction.js.map +1 -1
  290. package/dist/utils/startupReadiness.d.ts +3 -3
  291. package/dist/utils/startupReadiness.d.ts.map +1 -1
  292. package/dist/utils/startupReadiness.js +2 -2
  293. package/dist/utils/startupReadiness.js.map +1 -1
  294. package/dist/utils/threadStatusReport.d.ts +3 -1
  295. package/dist/utils/threadStatusReport.d.ts.map +1 -1
  296. package/dist/utils/threadStatusReport.js +4 -3
  297. package/dist/utils/threadStatusReport.js.map +1 -1
  298. package/dist/utils/timezone.d.ts +23 -0
  299. package/dist/utils/timezone.d.ts.map +1 -1
  300. package/dist/utils/timezone.js +38 -0
  301. package/dist/utils/timezone.js.map +1 -1
  302. package/dist/utils/transcribeAudio.d.ts +25 -12
  303. package/dist/utils/transcribeAudio.d.ts.map +1 -1
  304. package/dist/utils/transcribeAudio.js +39 -21
  305. package/dist/utils/transcribeAudio.js.map +1 -1
  306. package/dist/utils/usageRefreshBudget.d.ts +9 -0
  307. package/dist/utils/usageRefreshBudget.d.ts.map +1 -0
  308. package/dist/utils/usageRefreshBudget.js +27 -0
  309. package/dist/utils/usageRefreshBudget.js.map +1 -0
  310. package/examples/.env.example +7 -3
  311. package/examples/docker-compose.yml +9 -7
  312. package/package.json +2 -1
  313. package/stt/gigaam_sidecar.py +800 -0
package/.env.example CHANGED
@@ -52,9 +52,9 @@ ALLOWED_GROUP_ID=
52
52
  # state.json). See plan §10.7.
53
53
  # DATA_DIR=/home/user/.charness-mybot
54
54
 
55
- # Voice transcription. Recommended: without GROQ_API_KEY, voice messages are
56
- # not transcribed unless you intentionally configure the OpenAI fallback below.
57
- # GROQ_API_KEY=
55
+ # Voice transcription is NOT configured here: engines and their API keys live in
56
+ # ~/.config/charness/stt.json (in the directory of this file's real location — beside the target when this
57
+ # file is a symlink) — see SETUP.md "Voice input".
58
58
 
59
59
  # ── Advanced / only when your setup needs it ────────────────────────────────
60
60
 
@@ -62,9 +62,6 @@ ALLOWED_GROUP_ID=
62
62
  # cwd (for example, a custom systemd service). Normal launch uses $PWD instead.
63
63
  # WORK_ROOT=/home/user/src
64
64
 
65
- # OpenAI Whisper fallback for voice. Usually unnecessary when GROQ_API_KEY is set.
66
- # OPENAI_API_KEY=
67
-
68
65
  # Not needed for normal agent auth. Claude/OpenCode credentials are configured
69
66
  # through those CLIs themselves; set provider env vars only if a custom MCP,
70
67
  # OpenCode plugin, or auth resolver explicitly requires them.
package/DEVELOPMENT.md CHANGED
@@ -74,6 +74,21 @@ Each adapter implements `AgentAdapter` from `src/types.ts`:
74
74
  `toolResult`, `subagentStatus`, `apiError`, `started`, `stopped`, `closed`,
75
75
  `error` (all emit `SessionKey` first)
76
76
 
77
+ ### Voice recognition (the engine chain and the local sidecar)
78
+
79
+ A voice note walks an ordered chain of speech engines read from `stt.json` (`src/stt/`). The local GigaAM engine is
80
+ the one engine that is a PROCESS of ours, owned by the bot like OpenCode's server but started on demand and stopped
81
+ with its owner:
82
+
83
+ ```
84
+ voice note ──> transcribeWithEngines ──> engine 1 (local) ──ensure──> sttSidecarOwner ──> gigaam_sidecar.py
85
+ │ fall back on any engine 2 (Groq) │ adopt the record, or spawn; (ONE process, 127.0.0.1:0,
86
+ │ failure, no retry engine 3 (your URL) │ hosted outside the bot's tree token, idle exit)
87
+ └── last engine retries └── DATA_DIR/stt/sidecar.json
88
+ owner = the `charness hot` supervisor (a worker reload keeps the sidecar) or the plain bot process;
89
+ the sidecar polls its owner and exits within ~2 s of its death, even after a SIGKILL.
90
+ ```
91
+
77
92
  ## Local development
78
93
 
79
94
  ```bash
package/README.md CHANGED
@@ -25,8 +25,8 @@ These hold whichever surface you drive it from — the same core serves all of t
25
25
 
26
26
  - **Agent backends** — OpenCode (native server API over HTTP+SSE) and Claude Code (three backends — json-stream, per-turn and tmux)
27
27
  - **Sleeping agents** — a Claude json-stream topic's process is stopped when idle (or after every turn in per-turn mode) and the next message resumes the same conversation by itself, prompt cache still warm
28
- - **Several Claude accounts** — keep more than one Claude subscription signed in side by side and choose per topic which one a session runs on (`/accounts`); every account's usage windows are shown next to it. See [Several accounts](#several-accounts-accounts)
29
- - **Waits out usage limits** — when the agent hits a usage / session limit the bot reads the reset time out of the error (honouring the timezone the provider quotes), waits, then tells the agent to continue by itself and pins the "work resumed" message so a muted topic still notifies you; `/limits` turns waiting off per topic or skips a single resume. With a **fallback chain** (`/limits`: other Claude accounts, OpenCode providers and models, in your order of preference) the limit notice offers «🔁 Switch to …» buttons to move the topic to the next entry with room and continue at once — or the bot does it by itself when you turn switching on; across backends the new agent gets a context bridge with the previous one's last turns
28
+ - **Several Claude accounts** — keep more than one Claude subscription signed in side by side and choose per topic which one a session runs on (`/accounts`); every account's usage windows are shown next to it, and `/limits` is a one-tap switch board over all of them and over the OpenCode providers. See [Several accounts](#several-accounts-accounts)
29
+ - **Waits out usage limits** — when the agent hits a usage / session limit the bot reads the reset time out of the error (honouring the timezone the provider quotes; a mere warning such as `Approaching session limit · resets …` or the switch onto extra usage is not a limit), waits, then tells the agent to continue by itself and pins the "work resumed" message so a muted topic still notifies you; `/limits_autoswitch` turns waiting off per topic and `/limits` skips a single resume. With a **fallback chain** (`/limits_autoswitch`: other Claude accounts, OpenCode providers and models, in your order of preference) the limit notice offers «🔁 Switch to …» buttons to move the topic to the next entry with room and continue at once — or the bot does it by itself when you turn switching on; across backends the new agent gets a context bridge with the previous one's last turns
30
30
  - **Restart-safe** — agents run as external processes (tmux, `opencode serve`) that the bot re-adopts after a restart instead of killing them; a prompt sent while a session boots is buffered and replayed. See [Restart behaviour](#restart-behaviour)
31
31
 
32
32
  ### Telegram
@@ -48,7 +48,7 @@ setup, no extra dashboards — direct access to your own **OpenCode** /
48
48
  - **Reminders** — `/reminders` sets a plain reminder with inline buttons (how often, which day, what time, then the text by typing or voice); at the due time the bot posts and pins it itself, so even a muted topic notifies you. No agent, no bound folder, no date parsing — works in any topic, General included
49
49
  - **Inbound files** — photos / documents / video / audio sent to a topic are saved and announced to the agent; albums arrive as one prompt
50
50
  - **Outbound files** — the agent sends files back to you — a generated chart, a screenshot, a log, a rendered PDF — single files or albums, straight into the topic
51
- - **Voice input** — Whisper transcription via Groq (preferred) or OpenAI
51
+ - **Voice input** — voice notes are transcribed by an ordered chain of engines — Groq, OpenAI or your own OpenAI-compatible server — set up in `/stt` (or `stt.json`); a local GigaAM engine is optional; if one fails, the next takes over
52
52
  - **Display verbosity and view** — `/verbosity` (plus `/thinking`, `/tool_results`, `/subagent`) per topic: detail `minimal|short|full`, and what the topic shows — `stream` (default), `stream_answers` or `answers` (only the agent's pinned answers, no stream)
53
53
  - **Time-aware prompts** — `/timestamps on` prepends each forwarded prompt's send time (local-offset ISO), so a days-long session knows what "yesterday" or "2 days ago" means; agent-facing only, per topic
54
54
  - **Reply context** — reply to a message in a topic and the quoted text is folded into the prompt the agent receives, so it sees what you point at without re-pasting; works for text, voice and files (a photo, a document, an album…), both agents; a reply TO a photo, file or voice note names it, with the path of the bot's saved copy when it has one
@@ -94,8 +94,8 @@ actually start an agent or terminal in the folder.
94
94
  | `/terminal` | Open a raw `$SHELL` in the bound folder — see [Raw terminal](#raw-terminal-terminal-telegram) |
95
95
  | `/claude_mode` | Switch this topic's Claude backend (json-stream ⇄ per-turn ⇄ tmux-scrape); bare shows a picker — see [Claude Code backends](#claude-code-backends-claude_mode) |
96
96
  | `/model` | Switch model through a two-level picker: providers, then that provider's models (10 per page). On OpenCode the first row is 🕘 Recently used — a flat list of the models picked before, across providers (bot-wide, most recent first, up to 10), one tap to switch; it appears once something has been picked. Each provider row has a 🙈 to hide it from the picker (bot-wide, persisted; 👁 brings it back) — `/model <provider/model>` still reaches a hidden provider. If OpenCode is waiting on an old provider's retry, the next prompt interrupts that wait and starts with the selected model instead of sitting queued behind it |
97
- | `/accounts` | Every account the bot works with, on one screen. **Claude accounts** (when you keep several subscriptions signed in): each profile with its email, plan (personal Pro/Max or corporate Team/Enterprise), how many topics use it and its usage windows, ✓ on the active one; tap a profile to switch this topic (a running session is restarted under it, same conversation — at the end of its turn if it is working); in **General** the pick becomes the default for all topics. Per profile: 🔑 signs it in again, 🗑 removes it (after a confirmation; for `default`, which IS `~/.claude`, «remove» signs it out — see below). The usage lines give each window's percentage USED and when it resets as a time from now (`5h 5% 🔄 in 4h · week 20% 🔄 in 5d 8h · Fable 4% 🔄 in 5d 8h`; «🔄 in 2h», «🔄 in 2d 1h», «🔄 in 30m»; Fable only on a plan that has its own weekly window); when the 5h window's reset is unknown the weekly one is shown in its place, and with no reset at all only the percentage. The hint paragraph under the account list says the percentages are used. «➕ Claude account» asks for a name and runs the sign-in. **OpenCode providers**: the connected ones are listed with a «🔌 Disconnect <provider>» button and a 🔑 (connect again) each (the disconnect asks, then removes the stored credentials; a provider OpenCode enables from an environment variable, e.g. `OPENROUTER_API_KEY`, stays active — the reply says so and points at hiding it in `/model`); «➕ OpenCode provider» asks for the provider id and continues with the connect flow (OAuth methods when available, otherwise an API key whose message is deleted from Telegram). What happens when an account hits a usage limit is set in `/limits`. Text forms: `/accounts add <name>`, `/accounts use <name>`, `/accounts login <name>`, `/accounts remove <name>`, `/accounts autoswitch on\|off`. `/account`, `/connect [provider [key]]` and `/disconnect [provider]` still work as hidden aliases (the menu lists only `/accounts`). See [Several accounts](#several-accounts-accounts) |
98
- | `/effort` | Set reasoning effort (per-thread) via inline buttons. Claude: native `/effort` levels (`low…ultracode`). OpenCode: the current model's variants, applied per-prompt. No env configuration |
97
+ | `/accounts` | Every account the bot works with, on one screen. **Claude accounts** (when you keep several subscriptions signed in): each profile with its email, plan (personal Pro/Max or corporate Team/Enterprise), how many topics use it and its usage windows, ✓ on the active one; tap a profile to switch this topic (a running session is restarted under it, same conversation — at the end of its turn if it is working; on a topic that runs OpenCode the tap switches the topic to Claude on that account, after a confirmation when there is a conversation to hand over); in **General** the pick becomes the default for all topics. The title says where the topic really runs (`Claude «work»`, `OpenCode openai/gpt-5`) and the ✓ sits on that row — on no account while OpenCode runs. Per profile: 🔑 signs it in again, 🗑 removes it (after a confirmation; for `default`, which IS `~/.claude`, «remove» signs it out — see below). The usage lines give each window's percentage USED and when it resets as a time from now (`5h 5% 🔄 in 4h · week 20% 🔄 in 5d 8h · Fable 4% 🔄 in 5d 8h`; «🔄 in 2h», «🔄 in 2d 1h», «🔄 in 30m»; Fable only on a plan that has its own weekly window); when the 5h window's reset is unknown the weekly one is shown in its place, and with no reset at all only the percentage. The hint paragraph under the account list says the percentages are used. «➕ Claude account» asks for a name and runs the sign-in. **OpenCode providers**: the connected ones are listed with how each is signed in (`ChatGPT subscription` with its ~5 h / ~7 d usage windows like an account's, `API key`). In a topic a provider row is `[provider] [🔑] [🔌]`: a tap switches the topic to the provider through its model page (✓ on the model it runs, ⛔ on a limited one; a provider with one model is picked at once), 🔑 connects it again, 🔌 disconnects it (it asks, then removes the stored credentials; a provider OpenCode enables from an environment variable, e.g. `OPENROUTER_API_KEY`, stays active — the reply says so and points at hiding it in `/model`); in **General** a provider row is «🔌 Disconnect <provider>» and 🔑 only; «➕ OpenCode provider» asks for the provider id and continues with the connect flow (OAuth methods when available, otherwise an API key whose message is deleted from Telegram). What happens when an account hits a usage limit is set in `/limits_autoswitch`. Text forms: `/accounts add <name>`, `/accounts use <name>`, `/accounts use <provider>/<model>` (an OpenCode model, in a topic), `/accounts login <name>`, `/accounts remove <name>`, `/accounts autoswitch on\|off`. `/account`, `/connect [provider [key]]` and `/disconnect [provider]` still work as hidden aliases (the menu lists only `/accounts`). See [Several accounts](#several-accounts-accounts) |
98
+ | `/effort` | Reasoning effort. **In a topic:** shows the level its session runs and where the next start's level comes from («medium — as in General» / «high — set for this topic»; a running session keeps the level it started with when General moves on: «xhigh (medium from the next start) — as in General») with a button per level — Claude: the native `/effort` levels (`low…ultracode`); OpenCode: the current model's variants, applied per-prompt; General's `ultracode` is the model's highest variant and `auto` sends no variant — and «As in General (medium)», named with the level the topic will really get (OpenCode: rounded to the model's variants), which drops the topic's own pick (a live session is brought to the default at once). **In General:** the default effort for every topic that has no effort of its own (✓ on the current one; `medium` until you pick), saved to `settings.json` and applied from each topic's next session start or wake-up — running sessions are not restarted. No env configuration — see [Instance settings](SETUP.md#instance-settings-settingsjson) |
99
99
  | `/verbosity` | Output-verbosity macro (`minimal\|short\|full`): sets the thinking, tool-results and sub-agent display prefs at once; `/thinking`, `/tool_results`, `/subagent` point-override afterwards. Mixed prefs show as "custom" in the picker. The picker's second row (or `/verbosity stream\|stream_answers\|answers`) sets what the topic SHOWS — "Stream only" (default), "Stream + answers" (the stream plus the agent's pinned answers) or "Answers only" (the pinned answers, no stream; an agent that keeps writing after its final answer is reminded once to send what matters); the answers are written like for a manager or QA — brief and to the point; a switch applies from the next message, and `/status` names the view |
100
100
  | `/thinking` | Chain-of-thought display: `full` keeps the reasoning, `short` collapses to "💭 thought for Ns", `minimal` keeps only the live working cue |
101
101
  | `/tool_results` | Completed tool-call output: `full` whole body, `short` capped (15 lines / 1200 chars), `minimal` transient 🔧 status only |
@@ -104,19 +104,21 @@ actually start an agent or terminal in the folder.
104
104
  | `/rename_session` | Rename the current live session (OpenCode; Claude transcripts have no title) |
105
105
  | `/quit`, `/q` | End the session — Claude: graceful double Ctrl+C; OpenCode/terminal: `stopSession`. Releases the persisted session id, so a bot restart won't auto-reattach it (resume later via `/sessions`). A topic whose agent process is gone but whose session is kept (it "sleeps": the next message, file, voice note, `/compact`, `/model` or `/effort` resumes it by itself) is released the same way |
106
106
  | `/new`, `/clear_session` | End the current session and start a fresh one (same adapter) |
107
- | `/status` | This thread's status: agent, bound folder, the Claude account it runs on and the resolved workdir; a live session also shows the model it is actually running, effort, start time, runtime version and context use (anything the backend cannot report renders as unknown). On a Claude topic the account is its profile (name · email as in `/accounts`, marked ⛔ while it is at its limit, with a second row for a switch still pending); an OpenCode topic's model already names its provider |
107
+ | `/status` | This thread's status: agent, bound folder, the Claude account it runs on and the resolved workdir; a live session also shows the model it is actually running, the effort it runs (when the default moved on under it: «xhigh (medium from the next start)»), start time, runtime version and context use (anything the backend cannot report renders as unknown). On a Claude topic the account is its profile (name · email as in `/accounts`, marked ⛔ while it is at its limit, with a second row for a switch still pending); an OpenCode topic's model already names its provider |
108
108
  | `/output` | Last 500 lines of agent output (sent as at most 5 chunks; the overflow is reported as omitted) |
109
109
  | `/c`, `/y`, `/n` | Ctrl+C / "y" / "n" |
110
110
  | `/enter`, `/up`, `/down`, `/tab` | tmux key passthrough |
111
111
  | `/esc`, `/escape` | Send a raw Escape — interrupt the current turn / dismiss a selector |
112
112
  | `/schedule` | Schedule a prompt in free text — the agent parses the time and owns the job; see [Scheduler](#scheduler-schedule-telegram) |
113
+ | `/stt` | Voice recognition, from any topic (General included); instance-wide, applied to the next voice note — no restart. The main screen holds only what applies to recognition as a whole: the ordered list of **engines** (the first that works answers; when one fails the next takes over) — one full-width button each (`1. GigaAM (local) · running · 10s+25% · ✓` — name, the local engine's short state, timeout `base s + % of the voice`, ✓ usable / ✗ disabled or skipped) — then «➕ Add engine», «🌐 Language» (auto or `ru en uk de fr es`) and «✖️ Close». A tap on an engine opens its **card**: ⬆️ Up / ⬇️ Down, ✅ Use / 🚫 Don't use, ⏱ Timeout (presets), then what the kind adds — 🔑 key for **Groq**, **OpenAI** or a **custom** OpenAI-compatible server (its address, a base like `http://host:8000` is completed; then «does it need a key?»), and 🗑 Remove. A pasted key is deleted from the topic the moment it is accepted, never shown again and kept out of the logs; an address with a login in it is refused. **GigaAM (local)** (Russian only, runs on this machine) is added through «➕ Add engine» like the others: installed, it joins the list at once; not installed, a confirmation (≈ 400 MB on disk; ≈ 0.5 GB RAM while it runs, up to ≈ 0.9 GB on a long voice, nothing while stopped) leads to the install with a live progress message, and the message becomes its card. Its card holds everything local: the status line (ready · running / stopped / paused after failed starts / installing / not installed), «⏱ Exit after idle» (5 / 15 / 30 / 60 min or never), «⚙️ Workers» (1 / 2 / 4 / 8), «🔄 Try again now» (while paused), «Remove from list» (the files stay) and «🗑 Remove model files». Everything lives in one file, `stt.json` — see [Voice input](SETUP.md#voice-input). `/voice` and `/speech` are hidden aliases |
113
114
  | `/reminders` | Reminders set with buttons only — the bot posts and pins them itself, with no agent involved; works in any topic, bound or not. See [Reminders](#reminders-reminders-telegram) |
114
115
  | `/clear_messages` | Delete bot messages in this topic (up to 48h, Telegram limit) |
115
116
  | `/clear` | Forwarded to the agent (Claude wipes context; OpenCode plain text) — not a bot command anymore. Also purges the topic's file-intake dir |
116
117
  | `/compact` | Compact the agent's context — a real, confirmed compaction on OpenCode and on the default (stream) Claude backend; the tmux Claude backend has the literal command forwarded (its TUI compacts); terminal: not supported |
117
118
  | `/compact_on_idle` | Toggle auto-compaction after ~55 min idle (Enable/Disable picker; `AGENT_IDLE_MINUTES` sets the window). Per topic; run it in **General** to set the default for all topics. On by default. Each idle compaction posts one short notice; the picker's «Show summary» / «Hide summary» row (the `/compact_summary` switch) adds the agent's full summary after it. Fires at most once per active period (until you write again); if a question was pending it is re-asked with tappable buttons after the compaction. On the json-stream backends the idle mark also STOPS the agent process, compaction or not — the topic sleeps and your next message resumes it (`/status` says so) |
118
119
  | `/compact_summary` | Toggle posting the agent's full summary after a compaction — `/compact`, the idle compaction and the agent's own `compact_conversation` (Enable/Disable picker). Per topic; run it in **General** to set the default for all topics. Off by default: a compaction ends in a short line. On OpenCode it also hides the summary OpenCode streams itself |
119
- | `/limits` | Everything about a usage limit on one screen, with a short text: at the top each Claude account's **usage windows** exactly as `/accounts` shows them, under a «Usage (% used)» heading (`default — ⏱ 5h 5% 🔄 in 4h · week 20% 🔄 in 5d 8h · Fable 4% 🔄 in 5d 8h`; a signed-out account is named, one never polled is left out), then what the topic runs on and where its two settings come from. **Fallback models** — an ordered list of Claude accounts (with an optional model) and OpenCode providers with a model, top = preferred, where a topic continues when its agent hits a limit. One full-width button per entry (`1. Claude · work · opus ⛔ 14:00` — limited until, 🔒 not signed in, ❓ no longer offered, 🚫 left out of automatic switching, ✓ where the topic runs); a tap opens that entry in the same message with ⬆️ Up / ⬇️ Down, 🚫 Don't use / ✅ Use (a left-out entry stays usable by hand through `/model` and `/accounts`), 🗑 Remove and ⬅️ Back. Below the list: «➕ Add» (account or provider, then the model), «🔄 Fill from connected» (a draft from what is connected; entries you left out stay left out; asks first when a list exists), «🗑 Clear». The list is ONE instance-wide list and is editable from every topic. **Two settings** decide what a limit does: «🔁 Auto switch» — auto switch to a fallback model (OFF by default: the limit notice carries «🔁 Switch to …» buttons instead) and «⏳ Wait for reset» — wait for the reset and continue (ON by default; while a wait is armed the topic screen also offers «⏭ Skip once»). In General they are the defaults for all topics and each is one button that flips the value. In a regular topic each is one short full-width button showing the effective value (`🔁 Auto switch: ON`; one line of text above the list says where each value comes from — `🔁 as in General · ⏳ set for this topic`); a tap opens the setting in the same message with ON / OFF / «As in General (<the General value>)» — the topic's own choice marked ✓ — and ⬅️ Back, and a choice applies and returns to the screen. Switch ON: the topic moves to the first available entry at once. OFF: buttons on the notice, and if none is pressed the bot waits for the reset (waiting ON); both OFF: it only reports the limit. When every enabled entry, the current one included, is limited, the wait ends at the EARLIEST reset and the topic continues on that entry (the notice names it and the time). Text forms: `/limits rebuild`, `/limits clear`, `/limits switch on\|off\|same`, `/limits wait on\|off\|same`. `/fallback` and `/auto_continue_limits [on\|off]` still work as hidden aliases. See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits) |
120
+ | `/limits` | The **switch board**: every signed-in Claude account and every connected OpenCode provider on one screen. The **message** carries the figures — `Usage (% used)` and a line per account in the same wording `/accounts` uses (`default — ⏱ 5h 14% 🔄 in 3h · week 3% 🔄 in 6d`; `⛔ at its limit until …` while the account is limited; `(stale)` when the last poll could not refresh), a provider signed in through a ChatGPT subscription after them (`openai (ChatGPT) — ⏱ 5h 12% 🔄 in 3h · week 40% 🔄 in 4d`), the two automatic-switching settings' state with a link to `/limits_autoswitch`, and the caption «Choose the active model:». The **buttons** are one full-width name each — `✓ Claude · work`, `Claude · default`, `OpenCode · openai` — with no figure on them (the figures do not fit a phone's button); ✓ marks where the topic runs; a signed-out account is not listed. **A tap switches this topic**: another Claude account (the running session restarts under it, same conversation — at the end of its turn when it is working), an OpenCode provider (opens its model page, 10 models per page with ⛔ on a limited one; a provider with one model switches at once), the ✓ row (the model page of where the topic runs: change the model). Across backends with a conversation to hand over the bot asks first (✅ Continue / ✖️ Cancel); while a usage-limit wait is armed the tap is the limit move — the wait ends and the work continues, no question — and «⏭ Skip once» is offered. An unbound topic is told to bind a folder first. In **General** a Claude row sets the default account for all topics (the caption reads «Choose the default Claude account:»), and an OpenCode row only says that a model is picked inside a topic. The limit notice names this command. The old text forms `/limits rebuild`, `/limits clear`, `/limits switch …`, `/limits wait …` still work (they are `/limits_autoswitch`'s verbs). See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch) |
121
+ | `/limits_autoswitch` | The setup of automatic switching on a usage limit, with a short text: a title, what the topic runs on and where its two settings come from. **Fallback models** — an ordered list of Claude accounts (with an optional model) and OpenCode providers with a model, top = preferred, where a topic continues when its agent hits a limit. One full-width button per entry (`✓ 1. Claude · work · opus`, `2. Claude · default ⛔ 14:00`, `3. OpenCode · openai · gpt-5 🚫` — ✓ where the topic runs, ⛔ limited until, 🔒 not signed in, ❓ no longer offered, 🚫 left out of automatic switching); a tap opens that entry in the same message with ⬆️ Up / ⬇️ Down, 🚫 Don't use / ✅ Use (a left-out entry stays usable by hand through `/model` and `/accounts`), 🗑 Remove and ⬅️ Back. Below the list: «➕ Add» (account or provider, then the model; an entry of a backend the list does not use yet — an OpenCode model next to Claude accounts, or the reverse — asks first, because a limit then hands a conversation over in compacted form; including a 🚫 entry that does that asks the same way), «🔄 Fill from connected» (a draft from what is connected; entries you left out stay left out; asks first when a list exists), «🗑 Clear». The list is ONE instance-wide list and is editable from every topic. **Two settings** decide what a limit does: «🔁 Auto switch» — auto switch to a fallback model (OFF by default: the limit notice carries «🔁 Switch to …» buttons instead) and «⏳ Wait for reset» — wait for the reset and continue (ON by default; while a wait is armed the topic screen also offers «⏭ Skip once»). In General they are the defaults for all topics and each is one button that flips the value. In a regular topic each is one short full-width button showing the effective value (`🔁 Auto switch: ON`; one line of text above the list says where each value comes from — `🔁 as in General · ⏳ set for this topic`); a tap opens the setting in the same message with ON / OFF / «As in General (<the General value>)» — the topic's own choice marked ✓ — and ⬅️ Back, and a choice applies and returns to the screen. Switch ON: the topic moves to the first available entry at once. OFF: buttons on the notice, and if none is pressed the bot waits for the reset (waiting ON); both OFF: it only reports the limit. When every enabled entry, the current one included, is limited, the wait ends at the EARLIEST reset and the topic continues on that entry (the notice names it and the time). Text forms: `/limits_autoswitch rebuild`, `/limits_autoswitch clear`, `/limits_autoswitch switch on\|off\|same`, `/limits_autoswitch wait on\|off\|same` (the same verbs work after `/limits`). `/fallback` and `/auto_continue_limits [on\|off]` still work as hidden aliases. See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch) |
120
122
  | `/bind` | Bare: current binding + folder picker, with «leave current dir» (the old `/unbind`) and «create new folder» buttons |
121
123
  | `/mcp` | List MCP servers active for this thread |
122
124
 
@@ -150,10 +152,11 @@ In a bound topic you can also type:
150
152
  - `claude fix the bug` / `opencode add tests`
151
153
  - A plain message after `/claude` is already running → routed as input.
152
154
 
153
- Voice messages are transcribed via Groq Whisper (free) or OpenAI Whisper
154
- (fallback) and follow the same routing. If the provider times out or fails
155
- temporarily, the bot says so in the topic and retries automatically (after 5 s,
156
- then 15 s) — no need to re-record the voice note.
155
+ Voice messages are transcribed by the speech engines you set up with `/stt` (Groq,
156
+ OpenAI, any OpenAI-compatible server, or the local GigaAM engine — see [Voice input](SETUP.md#voice-input)) and follow
157
+ the same routing. The engines form an ordered chain: when one fails, the next one answers within the
158
+ same voice note. The last one retries a temporary failure automatically (after 5 s, then 15 s) and
159
+ says so in the topic — no need to re-record the voice note.
157
160
 
158
161
  `/terminal` is never started from a natural-language phrase — only the
159
162
  explicit command opens a shell.
@@ -219,14 +222,18 @@ so one account's usage limit does not stop every topic.
219
222
  profile keeps your current login out of the way), finish the sign-in and paste
220
223
  the code back into the topic. The code message is deleted; the bot confirms
221
224
  with the account's email and plan.
222
- - **Switching:** tap a profile in `/accounts` (or `/accounts use <name>`). A
225
+ - **Switching:** tap a profile in `/accounts` or an account on the `/limits`
226
+ board (or `/accounts use <name>`). A
223
227
  running session on the default (stream) backend is restarted under the new
224
228
  account with the same conversation — right away when idle, at the end of its
225
229
  turn when it is working; a sleeping topic simply resumes under it. The
226
230
  tmux-scrape backend is reopened the same way when idle and waits for the next
227
231
  start when busy. In the **General** topic the pick is the default for all
228
232
  topics; a topic with its own pick keeps it («↩ Follow the default» drops it).
229
- A profile that is not signed in cannot be picked.
233
+ A profile that is not signed in cannot be picked. On a topic that runs OpenCode the
234
+ pick is a switch to Claude on that account (see *A manual switch between Claude and
235
+ OpenCode asks first* under the fallback models). While a usage-limit wait is armed a
236
+ pick is the limit move: the wait ends and the work continues on the account you tapped.
230
237
  - **Removing:** «🗑 Remove» deletes the profile's login directory. The profile
231
238
  that is currently the instance default cannot be removed (pick another one in
232
239
  General first); topics that had picked the removed profile return to the
@@ -250,33 +257,42 @@ so one account's usage limit does not stop every topic.
250
257
  percentage is shown. `charness claude-profiles` prints
251
258
  the same relative form; its `--json` and the MCP tool keep ISO instants. The bot reads the figures with
252
259
  each profile's own login every 10 minutes, when `/accounts` or `/limits` opens and when an
253
- agent asks; a json-stream session's own rate-limit events keep them current
260
+ agent asks (the ChatGPT subscription of an OpenCode provider is read the same way, see below); a json-stream session's own rate-limit events keep them current
254
261
  between polls. A token the CLI has not renewed yet shows the last figures as
255
262
  `(stale)`; a profile whose session was refused on a window is marked
256
263
  `⛔ at its limit until …` and is skipped by the recommendation below.
257
264
  - **Another account takes over on a limit.** When a topic's account hits a
258
- usage limit the bot looks at the [fallback chain](#usage-limits-and-the-fallback-models-limits);
265
+ usage limit the bot looks at the [fallback chain](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch);
259
266
  without one it ranks the other signed-in profiles by headroom on the windows
260
267
  the topic's model drains (fresh figures, not marked limited). The limit
261
268
  notice carries «🔁 Switch to <entry>» buttons, best first: a tap moves the
262
269
  topic there (the running session is restarted under the new account with the
263
270
  same conversation), drops the wait for the reset and tells the agent to
264
271
  continue right away. With **switching** on («🔁 Auto switch to a
265
- fallback model» in `/limits`, or `/limits switch on`; in General the default for all
266
- topics) the bot does the same by itself and posts one line — «Usage
267
- limit on a — this topic switched to b and continues», naming when the
268
- exhausted account resets. It is OFF by default: moving work between
269
- subscriptions is your call. The exhausted profile is marked limited until its
270
- window resets, so it is not picked again before then; the topic is never
271
- moved back by itself — switch it back by hand when the window resets. When
272
- nothing can take over the bot waits for the reset as before. An agent that is
273
- still working switches at the end of its turn (the wait stays armed).
272
+ fallback model» in `/limits_autoswitch`, or `/limits_autoswitch switch on`; in General the default for all
273
+ topics) the bot does the same by itself and posts one short line — «Account
274
+ «a» hit its usage limit — this topic continues on «b». The limit resets
275
+ <time>. Switch: /limits». The target is named by its account alone
276
+ and gets its model only when the switch changes it («b» (sonnet)); an
277
+ OpenCode rung is written without quotes on either side — the target as
278
+ «OpenCode provider/model», the side that hit its limit as «Model
279
+ provider/model». It is OFF by default: moving
280
+ work between subscriptions is your call. The exhausted profile is marked
281
+ limited until its window resets, so it is not picked again before then. The
282
+ reset itself does not move the topic back: `/limits`, which the notice names,
283
+ lists every account and provider with its usage and one tap moves the topic
284
+ (see below). Only a wait that ends on another entry
285
+ moves a topic by itself (see below). When nothing can take over the bot waits
286
+ for the reset as before. A Claude session that works when a limit hits is
287
+ moved at once (the turn is dead; background tasks it started are stopped and
288
+ the notice says so); only a tmux Claude session with nothing to resume yet is
289
+ left to the end of its turn.
274
290
  - **Which account for a child process.** An agent that launches a detached
275
291
  `claude` (a worker, a reviewer) asks the bot's `telegramBot` MCP tool
276
292
  `claude_profiles` — optionally with the model it will run — and gets every
277
293
  profile with its windows plus `recommended`: the signed-in, unlimited profile
278
- with the most headroom (Fable → Fable's weekly window and the session window;
279
- any other model → the shared weekly window and the session window). It then
294
+ with the most headroom (the session window and the shared weekly window bound
295
+ every model; Fable additionally drains its own weekly window). It then
280
296
  starts the child with `CLAUDE_CONFIG_DIR=<configDir>` (`null` = the default
281
297
  profile: set nothing). The same answer from a shell, for a worker without
282
298
  MCP: `charness claude-profiles [--best] [--model <name>] [--json]` —
@@ -284,12 +300,39 @@ so one account's usage limit does not stop every topic.
284
300
  `CLAUDE_CONFIG_DIR="$(charness claude-profiles --best --model fable)"`
285
301
  works as is; it reads the bot's cache off `DATA_DIR/state.json`.
286
302
 
287
- ### Usage limits and the fallback models (`/limits`)
303
+ ### Usage limits and the fallback models (`/limits` and `/limits_autoswitch`)
288
304
 
289
305
  When a topic's agent stops on a usage limit — a Claude account window, an
290
- OpenAI quota through OpenCode, a Google `RESOURCE_EXHAUSTED` — the bot can keep
291
- the work going somewhere else instead of waiting. `/limits` holds ONE
292
- instance-wide, ordered list of **fallback models** (where to go), top = preferred:
306
+ OpenAI quota through OpenCode, a Google quota spent for the day or the plan — the
307
+ bot can keep the work going somewhere else instead of waiting (a per-minute
308
+ throttle, or a reset a few minutes away, is simply retried). Two screens split
309
+ the job:
310
+ **`/limits` is the switch board** — every signed-in Claude account and every connected
311
+ OpenCode provider with its usage, one tap to move the topic there by hand — and
312
+ **`/limits_autoswitch` is the setup of the automatic switching**: ONE
313
+ instance-wide, ordered list of **fallback models** (where to go), top = preferred,
314
+ and the two settings (auto switch, wait for the reset):
315
+
316
+ - **The board.** `/limits` (see the command table) is where the notice sends you
317
+ («The limit resets <time>. Switch: /limits»): a Claude account row changes the
318
+ account (model and conversation kept), an OpenCode provider row opens its model page
319
+ and a pick changes the model (the same session), the ✓ row changes the model of where
320
+ the topic runs, and across backends with a conversation the bot asks first. Every
321
+ entry point — a board row, an `/accounts` row, a model page, `/accounts use` — takes the
322
+ one switch path, so a pick behaves the same wherever it is made. While a limit wait is
323
+ armed a tap is the limit move: another account, another provider or model, and — on the
324
+ limited Claude account itself — a model the limit does not cover (a Fable-only weekly limit
325
+ does not stop `opus` or `sonnet`; the usage windows say what is exhausted) continue the work
326
+ at once; a pick still inside the limit's scope (Fable again, or any model while the session or
327
+ the shared weekly window is exhausted — Fable drains both too — or when the windows are unknown)
328
+ only changes the model and the wait stays. A tap that switched answers with a short «✅ This topic runs on …».
329
+ - **OpenCode usage.** A provider signed in through a **ChatGPT subscription** (the `openai`
330
+ provider with an `oauth` record in OpenCode's `auth.json`) shows its ~5 h and ~7 d
331
+ windows in the `/limits` message and in `/accounts`, read the way a Claude account's are: the stored
332
+ token as it is (never refreshed by the bot; an expired one marks the figures out of date),
333
+ every 10 minutes and when a screen opens, cached in `state.json`. API-key providers and
334
+ other subscriptions show only how they are signed in (`/accounts`); a model that hit a limit carries `⛔ <reset>` on its model page.
335
+ A failed read just leaves the provider without a usage line.
293
336
 
294
337
  - **Left out of switching.** ✅ / 🚫 on a row includes or excludes that entry from
295
338
  automatic switching and from the notice's switch buttons. A connected
@@ -303,7 +346,7 @@ instance-wide, ordered list of **fallback models** (where to go), top = preferre
303
346
  an OpenCode provider with a model (`openai/gpt-5`, `google/gemini-2.5-pro`,
304
347
  …). Only what is actually connected is offered: the signed-in profiles, and
305
348
  the providers the OpenCode server is authenticated for (its live catalog).
306
- - **Drafting.** «🔄 Fill from connected» (or `/limits rebuild`) writes a draft from what
349
+ - **Drafting.** «🔄 Fill from connected» (or `/limits_autoswitch rebuild`) writes a draft from what
307
350
  is signed in: the Claude accounts in their `/accounts` order (keeping the
308
351
  topic's model), then the OpenCode models topics picked recently, then every
309
352
  other provider's strongest model; an account or provider you had left out of
@@ -339,9 +382,21 @@ instance-wide, ordered list of **fallback models** (where to go), top = preferre
339
382
  report in one short message what the task is, what is done and what remains.
340
383
  A later `/claude` or `/opencode` returns to the parked conversation; `/new`
341
384
  and `/quit` release it as usual.
342
- - The topic is **never moved back by itself** when the exhausted entry resets
343
- — the switch notice names the reset time; switch back by hand (`/accounts`,
344
- `/model`, `/claude`, `/opencode`).
385
+ - **A manual switch between Claude and OpenCode asks first.** `/claude` or `/opencode` (also the agent
386
+ buttons and the natural-language «start …» phrases) on a topic that runs the other backend, and a Claude
387
+ account tapped in `/accounts` or on the `/limits` board (or `/accounts use <name>`) — and an OpenCode provider's model picked on a topic that runs Claude — move the topic the same
388
+ way as a limit does — parked conversation of the target resumed, the context bridge as its first prompt (text typed
389
+ with the command, `/opencode do X`, rides in it). When the topic holds a conversation to hand over (an agent with at
390
+ least one turn, running or asleep) the bot first asks «🔁 Switch this topic from OpenCode to Claude «work»? The
391
+ conversation context will be handed over in compacted form.» with ✅ Continue / ✖️ Cancel: Continue switches and
392
+ turns the question into one line saying so, Cancel changes nothing; either consumes the keyboard, and an old
393
+ question only says it is out of date. A topic with nothing to hand over (no agent, or one just started and not yet
394
+ spoken to) switches at once, and a Claude account tapped on a Claude topic applies at once, as before. `/model`
395
+ lists only the current backend's models, so it never switches backend.
396
+ - The reset of the exhausted entry does **not** move the topic back by itself —
397
+ the switch notice names the reset time. The one thing that can is the wait: when
398
+ every enabled entry is limited, the wait is re-aimed at the entry that resets
399
+ first, and that may be the one the topic left (see above).
345
400
 
346
401
  ### Scheduler (`/schedule`) (Telegram)
347
402
 
@@ -485,9 +540,12 @@ agent even when two clients use the same JSON-RPC request id. It exposes:
485
540
  - `send_file_to_user` — lets the agent push files from the bound folder into the
486
541
  topic. A single MP4 uses Bot API `sendVideo`; eligible all-video and mixed
487
542
  photo/video albums use `sendMediaGroup`, with each MP4 represented by an
488
- `InputMediaVideo` entry. `as_file:true` explicitly forces documents. Secure
489
- outbound file traversal currently requires Linux; on macOS this tool
490
- fails closed until a native descriptor-relative bridge is available. Existing
543
+ `InputMediaVideo` entry. `as_file:true` explicitly forces documents. A file is
544
+ opened so that a path swapped after validation cannot reach outside the bound
545
+ folder: on Linux component by component beneath a pinned folder descriptor
546
+ (`/proc/self/fd`); on Windows and macOS by its canonical path, held to the
547
+ validated file's device/inode identity and resolved again inside the folder
548
+ after the open. Existing
491
549
  path, count, caption, and size caps apply; canonical upload basenames have
492
550
  control characters and quoted-string metacharacters replaced before they enter multipart headers. Cancelling
493
551
  the MCP call removes queued work. While any upload stream is still being
@@ -544,6 +602,21 @@ or adopted ownership is revalidated against the exact hostname and port before
544
602
  a signal is sent, without trusting a reused PID or assuming an adopted listener
545
603
  owns its process group.
546
604
  A source rebuild therefore does not terminate an in-flight OpenCode turn.
605
+ When `opencode serve` itself dies, the bot brings it back the same way a session
606
+ start does — installing the binary first if it is missing, retrying a failed
607
+ start for about a minute — and restores the sessions silently: only a topic whose
608
+ reply was in progress is told it was lost, and a message sent meanwhile is held
609
+ and delivered once the server is back. Only if every attempt fails does a topic
610
+ get one short line saying the next message will try again (the error itself goes
611
+ to the bot log).
612
+ A turn's failure is posted once, although OpenCode reports most failures twice
613
+ (the session error and the failed message); a provider plugin's `[Debug Info]`
614
+ block (opencode-antigravity-auth appends one with your Google project id) is kept
615
+ out of the topic and stays in the bot log. Gemini's "Requests ending with a model
616
+ turn are not supported" — OpenCode v1's agent loop sends a history that ends on
617
+ the model's own turn, fixed upstream only in v2 — gets one short line and the
618
+ bot's "continue" at once, which is a user turn; the same refusal right after it
619
+ ends with the usual give-up line.
547
620
  The supervisor reads the same checkout `.env` as the worker before the initial
548
621
  start. Hot mode supports Linux and macOS; it refuses to start on Windows because
549
622
  nodemon cannot gracefully drain its worker tree there.
@@ -668,7 +741,7 @@ Yes, both a bot restart and an agent-server restart. State is persisted to disk;
668
741
  Yes. Each topic picks its backend independently: OpenCode over its native HTTP+SSE server, or Claude Code (tmux or json-stream). One topic is one project with its own isolated session, like terminal tabs, and two topics can point at the same folder for parallel work.
669
742
 
670
743
  **Can I talk to the agent by voice?**
671
- Yes. Send a voice message to a topic and it is transcribed with Whisper (via Groq or OpenAI) and forwarded to the agent as a normal prompt, so you can vibe code hands-free from your phone.
744
+ Yes. Send a voice message to a topic and it is transcribed (by Groq, OpenAI or your own Whisper-compatible server) and forwarded to the agent as a normal prompt, so you can vibe code hands-free from your phone.
672
745
 
673
746
  ### Documentation
674
747
 
package/SETUP.md CHANGED
@@ -172,17 +172,16 @@ $EDITOR ~/.config/charness/.env
172
172
  ```
173
173
 
174
174
  ```dotenv
175
- # ~/.config/charness/.env — the one place your keys live
175
+ # ~/.config/charness/.env — the bot token and the instance settings
176
176
  TELEGRAM_BOT_TOKEN=123456789:ABCdef...
177
- # GROQ_API_KEY=... # optional: free voice transcription
178
177
  # OWNER_USER_ID=... # set to drive the bot from its DM (step 2)
179
178
  ```
180
179
 
181
- All keys the bot needs live in this one file — the bot token and the optional
182
- `GROQ_API_KEY` for voice sit side by side, no separate stores. (Agent
183
- provider auth is the exception, and lives inside the agents themselves:
184
- `claude login` for Claude, `/connect` or OpenCode's own config for OpenCode —
185
- see [Environment Variables](#environment-variables).) The wrapper loads env
180
+ The bot token lives in this file; the voice-recognition keys live next to it, in
181
+ [`stt.json`](#voice-input). (Agent provider auth is the exception, and lives
182
+ inside the agents themselves: `claude login` for Claude, `/connect` or
183
+ OpenCode's own config for OpenCode — see
184
+ [Environment Variables](#environment-variables).) The wrapper loads env
186
185
  from two locations, in order — a global base and an optional per-project
187
186
  override:
188
187
 
@@ -277,6 +276,107 @@ Then: `/quit` ends the session; bare `/bind` inspects/reconfigures the binding;
277
276
  > uses, so a thread started in Telegram can be picked up with `claude --resume`
278
277
  > and vice versa.
279
278
 
279
+ ### Voice input
280
+
281
+ Open **`/stt`** in any topic (General included) to set voice recognition up with buttons: add Groq, OpenAI or your own
282
+ OpenAI-compatible server (the key is pasted into the chat and deleted at once), reorder or switch off engines, set each
283
+ engine's timeout and the language, and enable the [local engine](#local-speech-recognition-gigaam). Every tap is saved to
284
+ the file below and applies to the next voice note. The rest of this section is for hand editing.
285
+
286
+ Voice notes are transcribed by an **ordered chain of speech engines**: the first available one is tried,
287
+ and when it fails (refused connection, timeout, an error answer) the next one answers the same voice note
288
+ at once. Only the last engine retries a temporary failure (after 5 s, then 15 s). An empty transcript is
289
+ final — silence is never "fixed" by asking another engine.
290
+
291
+ Everything lives in one file, **`stt.json`**, in the same directory as your real `.env`
292
+ (`~/.config/charness/stt.json`; for an isolated instance, beside its `ENV_FILE`). If that `.env` is a symlink — the
293
+ keys kept on an encrypted volume, say — `stt.json` goes beside the symlink's **target**, so the API keys stay where
294
+ the rest of your secrets are and never land in the directory that only holds the link. **That directory must be
295
+ writable and survive a restart** — under Docker, mount the config directory, not only the env file
296
+ ([Run with Docker](#run-with-docker)); when the file cannot be written, `/stt` says so and names the path, and the boot
297
+ log says it once (`[STT] … cannot be written`). It holds the settings and the API keys, is written with mode
298
+ `0600`, and is re-read when it changes — an edit applies to the next voice note, no restart. If the file is
299
+ invalid, the bot keeps the last good settings, logs a `[STT]` line naming the field (never a value), and
300
+ refuses voice with a notice when nothing valid was ever loaded. A voice note that no engine can take gets a notice
301
+ that names the cause: nothing set up (or all switched off), the local engine still installing, or engines listed but not
302
+ usable right now.
303
+
304
+ ```json
305
+ {
306
+ "engines": [
307
+ { "id": "q1", "kind": "groq", "apiKey": "gsk_..." },
308
+ { "id": "o1", "kind": "openai", "apiKey": "sk-...", "timeout": { "baseSeconds": 10, "percentOfDuration": 25 } },
309
+ { "id": "u1", "kind": "url", "url": "http://127.0.0.1:8000/v1/audio/transcriptions", "model": "whisper-1" }
310
+ ],
311
+ "language": "ru"
312
+ }
313
+ ```
314
+
315
+ | Field | Meaning |
316
+ |---|---|
317
+ | `engines` | Ordered, at most 6. `kind`: `groq`, `openai`, `url` (any OpenAI-compatible `/audio/transcriptions` endpoint — give the full URL; `apiKey` is optional there) or `gigaam` (the local engine). `id` is 1–8 lowercase letters or digits, unique. `isDisabled: true` keeps an engine listed but unused |
318
+ | `timeout` | Per engine: `baseSeconds` + `percentOfDuration` % of the voice's length (default 10 s + 25 %), so a 30 s voice gets 17.5 s and a 5 min voice 85 s |
319
+ | `language` | Two-letter code (`ru`, `en`, …) sent to the engines as a hint; leave it out to let the engine detect the language |
320
+ | `gigaam` | `idleMinutes` (default 15, `0` = never) and `workers` (1, 2, 4 or 8; default 4) of the local engine |
321
+
322
+ An engine without what it needs (no key, no URL) is skipped and logged once. With no usable engine, a voice
323
+ note is answered with a pointer to this setup.
324
+
325
+ **Coming from `GROQ_API_KEY` / `OPENAI_API_KEY` in `.env`?** On the first start without a `stt.json`, the bot
326
+ writes one from those keys (Groq first, then OpenAI) and logs
327
+ `[deprecated] GROQ_API_KEY/OPENAI_API_KEY in .env is deprecated, voice settings now live in stt.json`. From then
328
+ on the env keys are ignored — edit `stt.json` instead.
329
+
330
+ #### Local speech recognition (GigaAM)
331
+
332
+ Besides the hosted engines, the bot can recognise Russian voice notes **on your own machine** with GigaAM v3 (an
333
+ `onnx-asr` model, int8). It is a `gigaam` engine in `stt.json`, added from `/stt` («➕ Add engine → GigaAM (local)»; when the model is not installed yet, with a confirmation and a live progress message) or
334
+ by adding `{ "id": "g", "kind": "gigaam" }` to `stt.json` by hand — in that case the bot installs it by itself at the
335
+ next start and shows the progress in General (or in its log).
336
+
337
+ - **Needs** `uv` (on `PATH` or in `~/.local/bin`), or `python3` ≥ 3.10 with the venv module (`apt install python3-venv`
338
+ on Debian/Ubuntu). The install builds a private venv — your Python is left alone.
339
+ - **Disk:** about 400 MB under `<DATA_DIR>/stt` (the model is 216 MB; the rest is the Python packages and their cache).
340
+ - **RAM:** nothing while it is not running. The recogniser process starts with the first voice note that needs it (about
341
+ +2–3 s once), uses about 0.5 GB for short voices and up to about 0.9 GB while a 5-minute voice is recognised with the
342
+ default 4 workers, and exits by itself after 15 idle minutes (`gigaam.idleMinutes`, `0` = never). Idle time, workers,
343
+ «Try again now» and removing the model files are on the engine's card in `/stt`, not on the main screen.
344
+ - **Speed** (4 cores, loaded): a short voice in 1–2 s, a 5-minute voice in about 20 s — long voices are cut at pauses and
345
+ recognised in parallel. A hot reload of the bot keeps a running recogniser; stopping or killing the bot takes it down.
346
+ - **Russian only.** English words come out in Cyrillic or garbled; with a `language` other than `ru` set, the engine is
347
+ skipped and the next one in the chain answers.
348
+ - It listens on `127.0.0.1` only, on a port the OS picks, and answers only a per-start secret token.
349
+ - **Docker is not supported yet** (the image has no `python3-venv`).
350
+
351
+ ### Instance settings (`settings.json`)
352
+
353
+ Settings that belong to the whole instance, not to a topic, live in one file, **`settings.json`**, in the same
354
+ directory as your real `.env` and `stt.json` (`~/.config/charness/settings.json`; for an isolated instance, beside its
355
+ `ENV_FILE`; a symlinked `.env` is resolved exactly as for `stt.json`). The directory must be writable and survive
356
+ restarts — under Docker the mounted config directory covers it ([Run with Docker](#run-with-docker)); when it cannot
357
+ be written, the tap that needed it says so and names the path. The file is optional and is re-read when it changes:
358
+ a hand edit applies to the next session start, no restart.
359
+
360
+ ```json
361
+ { "defaultEffort": "medium" }
362
+ ```
363
+
364
+ | Field | Meaning |
365
+ |---|---|
366
+ | `defaultEffort` | The reasoning effort of every topic that has no `/effort` pick of its own: `low`, `medium`, `high`, `xhigh`, `max`, `auto` or `ultracode`. Set it from **`/effort` in the General topic** (a tap writes the file), or by hand. Leave it out and the built-in default, `medium`, applies |
367
+
368
+ What a new Claude session starts with, first that exists: the topic's own `/effort` pick, the platform's default
369
+ (a Jira conversation: `effort` in `jira.json`, `high` when absent — Jira never reads `defaultEffort`), `defaultEffort`,
370
+ then `medium`. A change applies from each topic's next session start
371
+ or wake-up — running sessions are not restarted, and `/status`, the pinned banner and `/effort` show the level a running
372
+ session really uses, with the next start's beside it («xhigh (medium from the next start)»). OpenCode takes the same
373
+ level and runs the nearest variant its model offers (`ultracode` is the model's highest variant, `auto` sends no variant). In a topic, `/effort` shows the level and whether it is the topic's own or «as in General», and «As in
374
+ General» drops the topic's pick (a live session is brought to the default at once).
375
+
376
+ The file is checked strictly: an unknown level or a mistyped key is logged once as `[Settings] settings.json
377
+ defaultEffort: …` (naming the field, never echoing a value) and the built-in defaults apply until it is fixed — a
378
+ stale copy of an earlier valid file is not used. A `/effort` tap in General over a broken file replaces it.
379
+
280
380
  ### Required files structure
281
381
 
282
382
  Launch `charness` from the parent folder that holds your project repos —
@@ -341,7 +441,6 @@ talk to the bot there.
341
441
  | `ALLOWED_GROUP_ID` | (auto-pair) | Numeric forum supergroup id (`-100…`). Leave empty to auto-pair with the first forum group a group **admin/creator** contacts the bot from (id is saved to `state.json`; re-point with `/pair`). A **name** is not accepted. A numeric value disables auto-pairing |
342
442
  | `CHAT_MODE` | `both` | Which surface(s) this instance serves: `group`, `dm`, or `both` — see [Two surfaces](README.md#two-surfaces-group-topics-bot-dm-or-both) |
343
443
  | `OWNER_USER_ID` | — | **Optional.** Numeric Telegram user id of the owner — leave it unset to run group-only (the DM surface stays inert; a group works fully without it). Set it only to enable the owner-DM surface; **required** solely for `CHAT_MODE=dm` |
344
- | `GROQ_API_KEY` | — | Recommended for voice transcription. Without it, voice messages are not transcribed unless you intentionally configure the OpenAI fallback |
345
444
 
346
445
  **Localized in 12 languages** — the bot UI (commands, buttons, notices) speaks
347
446
  中文, English, Français, ქართული, Deutsch, हिन्दी, 日本語, Português, Русский,
@@ -360,7 +459,6 @@ disappears and a short confirmation stays in the chosen language;
360
459
 
361
460
  | Variable | Default | Set only when |
362
461
  |---|---|---|
363
- | `OPENAI_API_KEY` | — | You intentionally use OpenAI Whisper as the voice fallback instead of Groq |
364
462
  | `SHELL` | `/bin/bash` | You want `/terminal` to open a different shell than your login shell (host env var, not set by the bot) |
365
463
 
366
464
  ## Jira setup
@@ -474,7 +572,8 @@ lends the container the folders it mounts, so the bot and its agents see nothing
474
572
 
475
573
  ```
476
574
  host container charness-<instance>
477
- your instance env file (tokens) ── ro ──▶ the bot (hot mode) + a restart loop
575
+ your config directory ──────▶ the bot (hot mode) + a restart loop;
576
+ (instance.env and stt.json) the env file is read, stt.json is written
478
577
  DATA_DIR, WORK_ROOT ──────▶ tmux, the agents, OpenCode, the bot MCP
479
578
  a clone of this repository ──────▶ /opt/charness
480
579
  the agents' home folder ──────▶ /home/charness: their Claude / OpenCode
@@ -493,10 +592,12 @@ the agents' home folder ──────▶ /home/charness: their
493
592
  names (build from the checkout's root, which is how the build reads it; rebuild after a yarn bump so no
494
593
  container has to download it). On first start the container installs the checkout's dependencies for the
495
594
  image and builds it; the bot installs Claude Code and OpenCode into the agents' home.
496
- 2. **Instance env file**, as for a host install (`TELEGRAM_BOT_TOKEN`, `DATA_DIR`, `WORK_ROOT`, …). It stays on
497
- the host and is mounted read-only. Every host path it names (`DATA_DIR`, `WORK_ROOT`, `TMUX_TMPDIR`, …) is
498
- mounted at the SAME path, so `state.json`, sessions and file paths stay valid inside and an instance can move
499
- between host and container unchanged.
595
+ 2. **Instance config directory**, with the env file as for a host install inside it (`TELEGRAM_BOT_TOKEN`,
596
+ `DATA_DIR`, `WORK_ROOT`, …): `/srv/charness/config/instance.env`. It stays on the host and the **directory** is
597
+ mounted writable, not just the file: the voice settings (`stt.json`, with the speech-engine API keys) and the instance settings (`settings.json`) are saved
598
+ beside the env file, and a container's own disk is gone when the container is re-created. Every host path the
599
+ env file names (`DATA_DIR`, `WORK_ROOT`, `TMUX_TMPDIR`, …) is mounted at the SAME path, so `state.json`,
600
+ sessions and file paths stay valid inside and an instance can move between host and container unchanged.
500
601
  3. **Run** (or use `examples/docker-compose.yml`):
501
602
 
502
603
  ```bash
@@ -506,8 +607,8 @@ the agents' home folder ──────▶ /home/charness: their
506
607
  -v /srv/charness/home:/home/charness \
507
608
  -v /srv/charness/data:/srv/charness/data \
508
609
  -v /srv/projects:/srv/projects \
509
- -v /srv/charness/instance.env:/srv/charness/instance.env:ro \
510
- -e ENV_FILE=/srv/charness/instance.env \
610
+ -v /srv/charness/config:/srv/charness/config \
611
+ -e ENV_FILE=/srv/charness/config/instance.env \
511
612
  charness
512
613
  ```
513
614
 
@@ -531,6 +632,10 @@ Running it:
531
632
  - **A container restart is a reboot**: every agent process ends; each conversation resumes with its next message.
532
633
  - **Rootless Docker** (recommended): the container's root is your user, so everything written on the mounts is
533
634
  yours. **Rootful Docker**: add `--user "$(id -u):$(id -g)"` and give that user the agents' home.
635
+ - **Upgrading a container made with the single-file mount** (`INSTANCE_ENV_FILE`, the env file mounted `:ro`): move the
636
+ env file into a directory of its own, mount the directory as in step 3 and set `ENV_FILE` to the file inside it. Until
637
+ then `stt.json` sits on the container's own disk and is lost when the container is re-created; the bot says so at boot
638
+ (`[STT] … is on this container's own disk`).
534
639
  - **Moving a host instance in**: stop it first (the lock refuses a lock written on another host), mount its
535
640
  folders at their paths, and copy its conversations' folders from `~/.claude/projects/` into the agents' home
536
641
  `.claude/projects/` — without them each conversation starts a fresh session on its next message.
@@ -9,7 +9,7 @@ import type { SessionKey } from '../sessionKey';
9
9
  * usage-limit handling, and the teardown of request-side state.
10
10
  */
11
11
  /** Every event an agent adapter emits (`createAdapter.ts` wires each). */
12
- export declare const adapterEventNames: readonly ["output", "status", "question", "thinking", "toolResult", "subagentStatus", "apiError", "noResponse", "questionGone", "closed", "started", "stopped", "suspended", "turnEnded", "contextCompacted", "error"];
12
+ export declare const adapterEventNames: readonly ["output", "status", "question", "thinking", "toolResult", "subagentStatus", "apiError", "noResponse", "questionGone", "closed", "started", "stopped", "suspended", "turnEnded", "contextCompacted", "conversationLost", "error"];
13
13
  export type AdapterEventName = (typeof adapterEventNames)[number];
14
14
  /**
15
15
  * @name ForeignKeyEventRoute
@@ -35,6 +35,7 @@ export declare const foreignKeyEventRoutes: {
35
35
  readonly suspended: "none";
36
36
  readonly turnEnded: "all";
37
37
  readonly contextCompacted: "all";
38
+ readonly conversationLost: "requestSide";
38
39
  readonly started: "none";
39
40
  readonly error: "requestSide";
40
41
  };
@@ -1 +1 @@
1
- {"version":3,"file":"adapterEventRouting.d.ts","sourceRoot":"","sources":["../../src/adapters/adapterEventRouting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD;;;;;;;;GAQG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,wNAiBpB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,aAAa,GAAG,KAAK,CAAC;AAElE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;CAyB2C,CAAC;AAE9E,8EAA8E;AAC9E,KAAK,oBAAoB,GAAG;KACzB,CAAC,IAAI,gBAAgB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,GAAG,CAAC,GAAG,KAAK;CAC7F,CAAC,gBAAgB,CAAC,CAAC;AAEpB;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,EAC7D,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,CAAC,EACZ,KAAK,EAAE,MAAM,IAAI,EAGjB,GAAG,aAAa,EAAE,CAAC,SAAS,oBAAoB,GAAG,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,GAAG,EAAE,GAClF,IAAI,CAIN"}
1
+ {"version":3,"file":"adapterEventRouting.d.ts","sourceRoot":"","sources":["../../src/adapters/adapterEventRouting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD;;;;;;;;GAQG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,4OAkBpB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,aAAa,GAAG,KAAK,CAAC;AAElE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;CA2B2C,CAAC;AAE9E,8EAA8E;AAC9E,KAAK,oBAAoB,GAAG;KACzB,CAAC,IAAI,gBAAgB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,GAAG,CAAC,GAAG,KAAK;CAC7F,CAAC,gBAAgB,CAAC,CAAC;AAEpB;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,EAC7D,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,CAAC,EACZ,KAAK,EAAE,MAAM,IAAI,EAGjB,GAAG,aAAa,EAAE,CAAC,SAAS,oBAAoB,GAAG,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,GAAG,EAAE,GAClF,IAAI,CAIN"}