charness 3.3.2 → 3.5.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 (277) hide show
  1. package/CHANGELOG.md +639 -0
  2. package/DEVELOPMENT.md +76 -0
  3. package/README.md +78 -7
  4. package/SETUP.md +65 -19
  5. package/dist/adapters/claudeCliAdapter.js +2 -2
  6. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  7. package/dist/adapters/claudeConfigDir.d.ts +18 -0
  8. package/dist/adapters/claudeConfigDir.d.ts.map +1 -1
  9. package/dist/adapters/claudeJsonStreamAdapter.d.ts +8 -1
  10. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  11. package/dist/adapters/claudeJsonStreamAdapter.js +24 -6
  12. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  13. package/dist/adapters/createAdapter.d.ts +2 -1
  14. package/dist/adapters/createAdapter.d.ts.map +1 -1
  15. package/dist/adapters/createAdapter.js +16 -0
  16. package/dist/adapters/createAdapter.js.map +1 -1
  17. package/dist/adapters/openCodeAdapter.d.ts +11 -0
  18. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  19. package/dist/adapters/openCodeAdapter.js +24 -9
  20. package/dist/adapters/openCodeAdapter.js.map +1 -1
  21. package/dist/agentLogin/agentLogin.d.ts +19 -1
  22. package/dist/agentLogin/agentLogin.d.ts.map +1 -1
  23. package/dist/agentLogin/agentLogin.js +64 -18
  24. package/dist/agentLogin/agentLogin.js.map +1 -1
  25. package/dist/apiErrorRetry.d.ts +3 -1
  26. package/dist/apiErrorRetry.d.ts.map +1 -1
  27. package/dist/apiErrorRetry.js +11 -1
  28. package/dist/apiErrorRetry.js.map +1 -1
  29. package/dist/bot.d.ts +0 -3
  30. package/dist/bot.d.ts.map +1 -1
  31. package/dist/bot.js +487 -182
  32. package/dist/bot.js.map +1 -1
  33. package/dist/changelogEntries.d.ts +25 -0
  34. package/dist/changelogEntries.d.ts.map +1 -0
  35. package/dist/changelogEntries.js +66 -0
  36. package/dist/changelogEntries.js.map +1 -0
  37. package/dist/cli/bot.d.ts +6 -2
  38. package/dist/cli/bot.d.ts.map +1 -1
  39. package/dist/cli/bot.js +34 -2
  40. package/dist/cli/bot.js.map +1 -1
  41. package/dist/cli/claudeProfiles.d.ts +1 -1
  42. package/dist/cli/claudeProfiles.d.ts.map +1 -1
  43. package/dist/cli/claudeProfiles.js +11 -6
  44. package/dist/cli/claudeProfiles.js.map +1 -1
  45. package/dist/cli/hot.d.ts +7 -0
  46. package/dist/cli/hot.d.ts.map +1 -1
  47. package/dist/cli/hot.js +53 -3
  48. package/dist/cli/hot.js.map +1 -1
  49. package/dist/cli.d.ts +1 -1
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +8 -0
  52. package/dist/cli.js.map +1 -1
  53. package/dist/connectors/telegram/bindPicker.d.ts +64 -0
  54. package/dist/connectors/telegram/bindPicker.d.ts.map +1 -0
  55. package/dist/connectors/telegram/bindPicker.js +97 -0
  56. package/dist/connectors/telegram/bindPicker.js.map +1 -0
  57. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +6 -2
  58. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
  59. package/dist/connectors/telegram/commands/claudeAccounts.js +127 -28
  60. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
  61. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
  62. package/dist/connectors/telegram/commands/fallbackChain.js +6 -2
  63. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
  64. package/dist/connectors/telegram/commands/selfUpdate.d.ts +16 -0
  65. package/dist/connectors/telegram/commands/selfUpdate.d.ts.map +1 -0
  66. package/dist/connectors/telegram/commands/selfUpdate.js +210 -0
  67. package/dist/connectors/telegram/commands/selfUpdate.js.map +1 -0
  68. package/dist/connectors/telegram/commands/stt.d.ts +1 -0
  69. package/dist/connectors/telegram/commands/stt.d.ts.map +1 -1
  70. package/dist/connectors/telegram/commands/stt.js +5 -0
  71. package/dist/connectors/telegram/commands/stt.js.map +1 -1
  72. package/dist/connectors/telegram/commands/topicCommand.d.ts +75 -0
  73. package/dist/connectors/telegram/commands/topicCommand.d.ts.map +1 -0
  74. package/dist/connectors/telegram/commands/topicCommand.js +391 -0
  75. package/dist/connectors/telegram/commands/topicCommand.js.map +1 -0
  76. package/dist/i18n/de.d.ts.map +1 -1
  77. package/dist/i18n/de.js +136 -3
  78. package/dist/i18n/de.js.map +1 -1
  79. package/dist/i18n/en.d.ts.map +1 -1
  80. package/dist/i18n/en.js +136 -3
  81. package/dist/i18n/en.js.map +1 -1
  82. package/dist/i18n/es.d.ts.map +1 -1
  83. package/dist/i18n/es.js +136 -3
  84. package/dist/i18n/es.js.map +1 -1
  85. package/dist/i18n/fr.d.ts.map +1 -1
  86. package/dist/i18n/fr.js +136 -3
  87. package/dist/i18n/fr.js.map +1 -1
  88. package/dist/i18n/hi.d.ts.map +1 -1
  89. package/dist/i18n/hi.js +136 -3
  90. package/dist/i18n/hi.js.map +1 -1
  91. package/dist/i18n/ja.d.ts.map +1 -1
  92. package/dist/i18n/ja.js +136 -3
  93. package/dist/i18n/ja.js.map +1 -1
  94. package/dist/i18n/ka.d.ts.map +1 -1
  95. package/dist/i18n/ka.js +136 -3
  96. package/dist/i18n/ka.js.map +1 -1
  97. package/dist/i18n/pt.d.ts.map +1 -1
  98. package/dist/i18n/pt.js +136 -3
  99. package/dist/i18n/pt.js.map +1 -1
  100. package/dist/i18n/ru.d.ts.map +1 -1
  101. package/dist/i18n/ru.js +136 -3
  102. package/dist/i18n/ru.js.map +1 -1
  103. package/dist/i18n/uk.d.ts.map +1 -1
  104. package/dist/i18n/uk.js +136 -3
  105. package/dist/i18n/uk.js.map +1 -1
  106. package/dist/i18n/uz.d.ts.map +1 -1
  107. package/dist/i18n/uz.js +136 -3
  108. package/dist/i18n/uz.js.map +1 -1
  109. package/dist/i18n/zh.d.ts.map +1 -1
  110. package/dist/i18n/zh.js +136 -3
  111. package/dist/i18n/zh.js.map +1 -1
  112. package/dist/instanceSettings.d.ts +12 -1
  113. package/dist/instanceSettings.d.ts.map +1 -1
  114. package/dist/instanceSettings.js +40 -4
  115. package/dist/instanceSettings.js.map +1 -1
  116. package/dist/scheduler/mcpSurface.d.ts.map +1 -1
  117. package/dist/scheduler/mcpSurface.js +4 -2
  118. package/dist/scheduler/mcpSurface.js.map +1 -1
  119. package/dist/selfUpdate/bootRollback.d.ts +22 -0
  120. package/dist/selfUpdate/bootRollback.d.ts.map +1 -0
  121. package/dist/selfUpdate/bootRollback.js +79 -0
  122. package/dist/selfUpdate/bootRollback.js.map +1 -0
  123. package/dist/selfUpdate/commandRunner.d.ts +21 -0
  124. package/dist/selfUpdate/commandRunner.d.ts.map +1 -0
  125. package/dist/selfUpdate/commandRunner.js +32 -0
  126. package/dist/selfUpdate/commandRunner.js.map +1 -0
  127. package/dist/selfUpdate/constants.d.ts +26 -0
  128. package/dist/selfUpdate/constants.d.ts.map +1 -0
  129. package/dist/selfUpdate/constants.js +31 -0
  130. package/dist/selfUpdate/constants.js.map +1 -0
  131. package/dist/selfUpdate/gitSource.d.ts +58 -0
  132. package/dist/selfUpdate/gitSource.d.ts.map +1 -0
  133. package/dist/selfUpdate/gitSource.js +183 -0
  134. package/dist/selfUpdate/gitSource.js.map +1 -0
  135. package/dist/selfUpdate/hostLock.d.ts +16 -0
  136. package/dist/selfUpdate/hostLock.d.ts.map +1 -0
  137. package/dist/selfUpdate/hostLock.js +101 -0
  138. package/dist/selfUpdate/hostLock.js.map +1 -0
  139. package/dist/selfUpdate/hotSupervisor.d.ts +47 -0
  140. package/dist/selfUpdate/hotSupervisor.d.ts.map +1 -0
  141. package/dist/selfUpdate/hotSupervisor.js +125 -0
  142. package/dist/selfUpdate/hotSupervisor.js.map +1 -0
  143. package/dist/selfUpdate/installKind.d.ts +45 -0
  144. package/dist/selfUpdate/installKind.d.ts.map +1 -0
  145. package/dist/selfUpdate/installKind.js +99 -0
  146. package/dist/selfUpdate/installKind.js.map +1 -0
  147. package/dist/selfUpdate/launchEnvironment.d.ts +3 -0
  148. package/dist/selfUpdate/launchEnvironment.d.ts.map +1 -0
  149. package/dist/selfUpdate/launchEnvironment.js +16 -0
  150. package/dist/selfUpdate/launchEnvironment.js.map +1 -0
  151. package/dist/selfUpdate/npmSource.d.ts +34 -0
  152. package/dist/selfUpdate/npmSource.d.ts.map +1 -0
  153. package/dist/selfUpdate/npmSource.js +107 -0
  154. package/dist/selfUpdate/npmSource.js.map +1 -0
  155. package/dist/selfUpdate/releaseNotes.d.ts +33 -0
  156. package/dist/selfUpdate/releaseNotes.d.ts.map +1 -0
  157. package/dist/selfUpdate/releaseNotes.js +115 -0
  158. package/dist/selfUpdate/releaseNotes.js.map +1 -0
  159. package/dist/selfUpdate/restart.d.ts +33 -0
  160. package/dist/selfUpdate/restart.d.ts.map +1 -0
  161. package/dist/selfUpdate/restart.js +94 -0
  162. package/dist/selfUpdate/restart.js.map +1 -0
  163. package/dist/selfUpdate/selfCheck.d.ts +20 -0
  164. package/dist/selfUpdate/selfCheck.d.ts.map +1 -0
  165. package/dist/selfUpdate/selfCheck.js +126 -0
  166. package/dist/selfUpdate/selfCheck.js.map +1 -0
  167. package/dist/selfUpdate/semver.d.ts +24 -0
  168. package/dist/selfUpdate/semver.d.ts.map +1 -0
  169. package/dist/selfUpdate/semver.js +165 -0
  170. package/dist/selfUpdate/semver.js.map +1 -0
  171. package/dist/selfUpdate/types.d.ts +51 -0
  172. package/dist/selfUpdate/types.d.ts.map +1 -0
  173. package/dist/selfUpdate/types.js +3 -0
  174. package/dist/selfUpdate/types.js.map +1 -0
  175. package/dist/selfUpdate/updateEngine.d.ts +110 -0
  176. package/dist/selfUpdate/updateEngine.d.ts.map +1 -0
  177. package/dist/selfUpdate/updateEngine.js +727 -0
  178. package/dist/selfUpdate/updateEngine.js.map +1 -0
  179. package/dist/selfUpdate/updateMessages.d.ts +84 -0
  180. package/dist/selfUpdate/updateMessages.d.ts.map +1 -0
  181. package/dist/selfUpdate/updateMessages.js +115 -0
  182. package/dist/selfUpdate/updateMessages.js.map +1 -0
  183. package/dist/selfUpdate/updateModes.d.ts +17 -0
  184. package/dist/selfUpdate/updateModes.d.ts.map +1 -0
  185. package/dist/selfUpdate/updateModes.js +14 -0
  186. package/dist/selfUpdate/updateModes.js.map +1 -0
  187. package/dist/selfUpdate/updatePending.d.ts +76 -0
  188. package/dist/selfUpdate/updatePending.d.ts.map +1 -0
  189. package/dist/selfUpdate/updatePending.js +131 -0
  190. package/dist/selfUpdate/updatePending.js.map +1 -0
  191. package/dist/selfUpdate/updatePolicy.d.ts +115 -0
  192. package/dist/selfUpdate/updatePolicy.d.ts.map +1 -0
  193. package/dist/selfUpdate/updatePolicy.js +83 -0
  194. package/dist/selfUpdate/updatePolicy.js.map +1 -0
  195. package/dist/selfUpdate/updateRuntime.d.ts +22 -0
  196. package/dist/selfUpdate/updateRuntime.d.ts.map +1 -0
  197. package/dist/selfUpdate/updateRuntime.js +148 -0
  198. package/dist/selfUpdate/updateRuntime.js.map +1 -0
  199. package/dist/startupPromptBuffer.d.ts +2 -0
  200. package/dist/startupPromptBuffer.d.ts.map +1 -1
  201. package/dist/startupPromptBuffer.js +4 -0
  202. package/dist/startupPromptBuffer.js.map +1 -1
  203. package/dist/state.d.ts +43 -0
  204. package/dist/state.d.ts.map +1 -1
  205. package/dist/state.js +54 -0
  206. package/dist/state.js.map +1 -1
  207. package/dist/types.d.ts +10 -0
  208. package/dist/types.d.ts.map +1 -1
  209. package/dist/utils/anthropicApiKeyCheck.d.ts +19 -0
  210. package/dist/utils/anthropicApiKeyCheck.d.ts.map +1 -0
  211. package/dist/utils/anthropicApiKeyCheck.js +41 -0
  212. package/dist/utils/anthropicApiKeyCheck.js.map +1 -0
  213. package/dist/utils/claudeAccountPickerPlan.d.ts +6 -1
  214. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
  215. package/dist/utils/claudeAccountPickerPlan.js +5 -1
  216. package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
  217. package/dist/utils/claudeApiKeyHelper.d.ts +19 -0
  218. package/dist/utils/claudeApiKeyHelper.d.ts.map +1 -0
  219. package/dist/utils/claudeApiKeyHelper.js +32 -0
  220. package/dist/utils/claudeApiKeyHelper.js.map +1 -0
  221. package/dist/utils/claudeApiSpend.d.ts +73 -0
  222. package/dist/utils/claudeApiSpend.d.ts.map +1 -0
  223. package/dist/utils/claudeApiSpend.js +82 -0
  224. package/dist/utils/claudeApiSpend.js.map +1 -0
  225. package/dist/utils/claudeCompactHook.d.ts +11 -6
  226. package/dist/utils/claudeCompactHook.d.ts.map +1 -1
  227. package/dist/utils/claudeCompactHook.js +28 -11
  228. package/dist/utils/claudeCompactHook.js.map +1 -1
  229. package/dist/utils/claudeLimitSwitch.d.ts +2 -1
  230. package/dist/utils/claudeLimitSwitch.d.ts.map +1 -1
  231. package/dist/utils/claudeLimitSwitch.js +3 -2
  232. package/dist/utils/claudeLimitSwitch.js.map +1 -1
  233. package/dist/utils/claudeProfileUsageLines.d.ts +18 -3
  234. package/dist/utils/claudeProfileUsageLines.d.ts.map +1 -1
  235. package/dist/utils/claudeProfileUsageLines.js +23 -2
  236. package/dist/utils/claudeProfileUsageLines.js.map +1 -1
  237. package/dist/utils/claudeProfiles.d.ts +57 -2
  238. package/dist/utils/claudeProfiles.d.ts.map +1 -1
  239. package/dist/utils/claudeProfiles.js +96 -3
  240. package/dist/utils/claudeProfiles.js.map +1 -1
  241. package/dist/utils/claudeStreamJson.d.ts +4 -1
  242. package/dist/utils/claudeStreamJson.d.ts.map +1 -1
  243. package/dist/utils/claudeStreamJson.js +6 -0
  244. package/dist/utils/claudeStreamJson.js.map +1 -1
  245. package/dist/utils/claudeTranscriptContent.d.ts +8 -0
  246. package/dist/utils/claudeTranscriptContent.d.ts.map +1 -1
  247. package/dist/utils/claudeTranscriptContent.js +44 -0
  248. package/dist/utils/claudeTranscriptContent.js.map +1 -1
  249. package/dist/utils/claudeUsage.d.ts +22 -6
  250. package/dist/utils/claudeUsage.d.ts.map +1 -1
  251. package/dist/utils/claudeUsage.js +12 -5
  252. package/dist/utils/claudeUsage.js.map +1 -1
  253. package/dist/utils/claudeUsageRefresher.d.ts +6 -0
  254. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
  255. package/dist/utils/claudeUsageRefresher.js +2 -2
  256. package/dist/utils/claudeUsageRefresher.js.map +1 -1
  257. package/dist/utils/fallbackChain.d.ts +15 -8
  258. package/dist/utils/fallbackChain.d.ts.map +1 -1
  259. package/dist/utils/fallbackChain.js +30 -13
  260. package/dist/utils/fallbackChain.js.map +1 -1
  261. package/dist/utils/limitsBoardPlan.d.ts +4 -2
  262. package/dist/utils/limitsBoardPlan.d.ts.map +1 -1
  263. package/dist/utils/limitsBoardPlan.js +12 -5
  264. package/dist/utils/limitsBoardPlan.js.map +1 -1
  265. package/dist/utils/recvPreviewRedaction.d.ts +7 -2
  266. package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
  267. package/dist/utils/recvPreviewRedaction.js +10 -4
  268. package/dist/utils/recvPreviewRedaction.js.map +1 -1
  269. package/dist/utils/threadStatusReport.d.ts +12 -1
  270. package/dist/utils/threadStatusReport.d.ts.map +1 -1
  271. package/dist/utils/threadStatusReport.js +13 -4
  272. package/dist/utils/threadStatusReport.js.map +1 -1
  273. package/dist/utils/updateScreenPlan.d.ts +38 -0
  274. package/dist/utils/updateScreenPlan.d.ts.map +1 -0
  275. package/dist/utils/updateScreenPlan.js +143 -0
  276. package/dist/utils/updateScreenPlan.js.map +1 -0
  277. package/package.json +3 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,639 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 3.5.0 — 2026-10-23
6
+
7
+ **You can see what an account billed to API credits has spent.** On the default Claude backend each turn's cost is
8
+ counted for such an account — only the part that turn added to the conversation, never a subscription turn's — and shown
9
+ under the account in `/accounts` and `/limits` (`💳 $12.34 spent since 23.10 · last turn $0.21`), on `/status`, when a
10
+ limit switches a topic onto it («billed per request … spent through the bot so far») and in its «credits used up»
11
+ notice. It is what the bot saw, not the Console's balance. A request on the account that goes through again also ends
12
+ its «credits used up» mark.
13
+
14
+ **An account billed to API credits never spends money unasked, and «Credit balance is too low» no longer waits for a
15
+ reset that never comes.** The `claude_profiles` recommendation and `charness claude-profiles --best` never name such an
16
+ account, the switch to another Claude account never offers it, and «🔄 Fill from connected» puts it last in the
17
+ fallback list and left out of automatic switching — tap it on the `/limits` board (`Claude · console 💳`), or include it.
18
+ When its credits run out, the topic gets one `💸` notice instead of hourly retries, the account is marked as out of
19
+ credits (`💸` in `/limits_autoswitch`, `/accounts`, `/limits`, `/status`) until you pick it or sign it in again, and the
20
+ fallback list takes over as for a limit: an available entry, a «Switch to …» button, or a wait for an entry that comes
21
+ back at a known time. An API rate limit (429) is still a short retry.
22
+
23
+ **A Claude account can bill the API credits of a Claude Console organization.** `/accounts` → «➕ Claude API account
24
+ (Console credits)» (or `/accounts add <name> api`) signs it in with `claude auth login --console`, which creates an API
25
+ key in that organization and keeps it in the account's own directory; or paste an existing API key while the sign-in
26
+ waits (on its own or as an `ANTHROPIC_API_KEY=…` line) — the message is deleted, the key is checked with the API and kept
27
+ for that account only, never in `state.json` or in an environment variable. A key pasted where the name is asked, or typed
28
+ on the `/accounts` command line, is deleted too. `/accounts` shows such an account as `💳 API · pay per use`, `/status` marks it
29
+ `💳 API`, and picking it says every request is billed. Your subscription accounts are unchanged: they still never
30
+ see an `ANTHROPIC_API_KEY`. Any `sk-ant-…` text is redacted from the output trace.
31
+
32
+ ## 3.4.0 — 2026-10-23
33
+
34
+ **Charness now updates itself.** Every hour the bot checks for a new version; when nobody is using it — no agent working,
35
+ no input waiting, nobody has written for 10 minutes — it installs it and restarts in place, the same process, so every agent
36
+ keeps running, and General gets «Updated a → b» with what the new version brought. An npm install takes a version 4 hours
37
+ after it appeared; a git checkout takes a pull (in hot mode the supervisor re-executes itself when one of its own files
38
+ changed — in Docker the entrypoint sees no exit); a major version, a version that needs a newer Node or a checkout range
39
+ that adds a migration guide is offered with a button instead of installed, and a version that does not start is rolled
40
+ back. `/update` in General switches it between Automatic (the default), Notify only and Off, switches the «what's new»
41
+ list and shows what the last check saw; `/version` says when an update is available.
42
+
43
+ **`/topic` opens another topic on a folder.** `/topic` asks for the new topic's name (`/topic <name>` takes it from the
44
+ command), then for the folder: the one this topic is bound to («as here») or another one from the same picker as `/bind`,
45
+ «create new folder» included — in General and in an unbound topic the picker comes first. The bot creates the topic, binds
46
+ it, greets it as `/bind` does and answers with an «Open topic →» button; no agent starts by itself. The bot needs the
47
+ `can_manage_topics` right (without it the usual hint is shown), «✖️ Cancel» (on the folder question and in the picker) and
48
+ `/esc` (at any step) end it, and in the bot's private chat, which has no topics, the command says so.
49
+
50
+ **`/interrupt` stops a running OpenCode turn.** `/esc` answered «Not supported for OpenCode», so an OpenCode topic had
51
+ no way to stop the agent and redirect it. `/interrupt` (the new name of `/esc` — both work, and `/interrupt` is in the
52
+ command menu) aborts the generation, cancels a question the agent was waiting on, and leaves the conversation as it
53
+ is: the next message steers the agent. On the Claude
54
+ stream-json backends `/esc` now also declines a question the agent is waiting on (it used to leave it open under the
55
+ aborted turn). Stopping a turn before the agent had written anything (also with `/c`) is no longer taken for a stuck
56
+ session, whose "recovery" ran the request again.
57
+
58
+ ## 3.3.2 — 2026-10-23
59
+
60
+ **On Windows the bot can start an `opencode` that npm installed.** 3.3.0 found npm's `opencode.cmd` on PATH but still
61
+ started the bare `opencode`, which Windows cannot run that way, so the OpenCode server did not come up. The bot now runs
62
+ the script npm's shim wraps (another batch file through `cmd.exe`), and installing a missing CLI works on Windows too.
63
+
64
+ **A Claude CLI that dies as it starts is reported as a failed start.** The topic used to be told the session was
65
+ «ready», then that it «ended» — once per attempt — after waiting out a 15-second handshake each time. It now gets one
66
+ line with the CLI's own error, at once.
67
+
68
+ **When the agent cannot be started again for a message, the topic gets one line saying why.** A message or a button tap
69
+ that had to wake a sleeping conversation added «No agent running» under the start's own reason. And a re-asked
70
+ question's buttons stopped working when its answer reached no session — they keep working now.
71
+
72
+ **Gemini's «Requests ending with a model turn are not supported» recovers even right after another retry.** It was
73
+ counted as a repeat of any retry in the two minutes before — Google's short «reset after 2s» retry, for one — and the
74
+ bot gave up without continuing the work. Each kind of retry still stops at its own limit when they alternate.
75
+
76
+ **A `/compact` that fails the way the turn before it did is shown.** The failure was taken for the previous turn's,
77
+ already posted, and dropped: neither shown nor retried.
78
+
79
+ **A daily quota's «retry your request in 30s» keeps the usual usage-limit wait.** Only a retry hint placed right after
80
+ «retry» was recognised; with a few words in between, the bot waited 30 seconds and marked the model limited for that long.
81
+
82
+ **The give-up line reads right for one attempt.** «Couldn't resume after 1 attempts» is now «Couldn't resume (attempts:
83
+ 1)», in every language.
84
+
85
+ ## 3.3.1 — 2026-10-23
86
+
87
+ **The npm listing names the `charness` command again.** The 3.3.0 listing on npm showed no commands (installs
88
+ and `npx` still worked, the package itself carried them).
89
+
90
+ ## 3.3.0 — 2026-10-23
91
+
92
+ **A usage limit waits until the exact reset when the provider sends one, instead of a guessed hour.** Claude reports the
93
+ reset of the window that refused the request, but the bot read it only from the message text. A wording that names no
94
+ time, such as «This request would exceed your account's rate limit», fell back to a retry every hour once the limit was
95
+ confirmed. On the default Claude backend the wait now ends at the reset Claude reported. A ChatGPT subscription limit on
96
+ OpenCode now waits until the reset in the provider's response (`resets_at`, or `resets_in_seconds`), not the hourly retry.
97
+ An older Claude Code's `Claude AI usage limit reached|<epoch>` is read the same way.
98
+
99
+ **The agent can send files to the topic on Windows and macOS.** `send_file_to_user`, and `send_messages_to_user` with a
100
+ file, refused with «secure file sending is unsupported on win32», and an agent could stop its work over it. Files are now
101
+ opened by their checked path on those systems, held to the file that was checked inside the bound folder, so a path
102
+ swapped meanwhile still cannot reach outside it. If a send does fail, the agent is told to give the file's path in its
103
+ reply and carry on.
104
+
105
+ **On Windows the bot finds the agent CLI installed on PATH.** It looked the tool up with `which`, which Windows lacks,
106
+ so an installed `opencode` read as missing.
107
+
108
+ **Gemini's «Requests ending with a model turn are not supported» on OpenCode recovers by itself.** OpenCode v1 sometimes
109
+ sends Gemini a conversation that ends on the model's own turn, and the turn stopped with the provider's raw text. The
110
+ topic now gets one short line and the bot continues the work at once; if the same refusal comes right back, it says it
111
+ could not resume, as for other errors.
112
+
113
+ **An OpenCode failure is posted once, without an auth plugin's debug block.** OpenCode reports most failures twice, and
114
+ both were posted, each as the provider's whole message cut at 500 characters — with opencode-antigravity-auth that
115
+ included a «[Debug Info]» block naming your Google project. One line is posted now, and the block stays in the bot log.
116
+ A sub-agent's failed step is posted once too: only its session error shows.
117
+
118
+ **Google's «You have exhausted your capacity on this model. Your quota will reset after …» is read by its duration.** The
119
+ Gemini subscription and Antigravity quotas say this both for a cap that clears in seconds («after 2s») and for a spent plan
120
+ («after 146h52m11s»). The seconds-long one was announced as «🚧 Usage limit reached» with an hour's wait, and an hours-long
121
+ one inside Google's RESOURCE_EXHAUSTED was retried a few times and given up. A reset within a few minutes is now a short
122
+ retry; a longer one is waited for until the time it names. A daily quota keeps the usual hour-long wait: the short
123
+ «Please retry in 40s» Google sends beside it is a retry hint, not the time the quota resets.
124
+
125
+ **A voice message no longer fails when two starts of the local speech engine meet.** When the bot restarted while the
126
+ local engine was loading, the second start finds the first one running and steps aside — on a busy machine that was taken
127
+ for a failed start, and the voice was not transcribed by the local engine.
128
+
129
+ **A reset time written without a time zone is no longer guessed.** An ISO stamp with no zone («resets at 2026-10-22
130
+ 23:29») was read in the bot's own zone — hours off when the provider meant another. It now counts only with a zone after it;
131
+ otherwise the reset is treated as unknown and the topic retries hourly (or as the provider's Retry-After says).
132
+
133
+ **A reset written in several parts («reset in 2 hours 30 minutes») is read whole.** Only the first part was read, so
134
+ the topic resumed half an hour early (OpenCode Go writes its limits that way, «in 1 day 3 hours» too), hit the limit
135
+ again and used up a retry.
136
+
137
+ **Anthropic's «This request would exceed your account's rate limit» is checked before the topic waits for the reset.** The
138
+ same refusal also comes while the account shows plenty of room, and the bot took it for an exhausted window: «🚧 Usage limit
139
+ reached — will auto-resume after reset» and hours of waiting. It now retries once after two minutes, and waits for the reset
140
+ only when the refusal comes again right after that retry — or when the account's usage shows a window at 100 %.
141
+
142
+ **A sub-agent's limit, or another session's in the same folder, no longer stops the topic.** On OpenCode a sub-agent runs
143
+ in a child session — often on another model or provider — and the folder's events can also carry a session the topic does
144
+ not own (your own `opencode` in the same folder, an old session still retrying). Their quota errors were taken for the
145
+ topic's: «🚧 Usage limit reached», the topic's messages held and its model marked limited while its own agent kept working.
146
+ Only the topic's own session can arm a limit wait now.
147
+
148
+ **An OpenCode error's response headers no longer turn it into a usage limit, or set its reset.** The bot read the whole
149
+ error OpenCode relays, response headers included, and headers name limits and resets of their own: a plain 400 could be
150
+ announced as «🚧 Usage limit reached», and a real limit could be given the reset of a per-minute counter (or a stamp a day
151
+ away). Now only what the provider said — its message, its response body, the status code — is read, and a reset that the
152
+ message does not name comes from the provider's Retry-After.
153
+
154
+ **A per-minute provider throttle is no longer announced as an exhausted usage limit.** Google answers a used-up
155
+ PER-MINUTE Gemini quota with the same «You exceeded your current quota … Please retry in 40s» / «Resource has been
156
+ exhausted» as a per-day one, and a concurrency or tokens-per-minute cap reads «exceeded your rate limit» / «limit
157
+ exceeded». The bot took all of them for a usage limit: «🚧 Usage limit reached» and a wait of an hour or more for a
158
+ limit that cleared in seconds. They are now retried within minutes; a quota that names a day, a week or a month — and
159
+ the explicit usage windows — still waits for its reset.
160
+
161
+ **Two requests opened in the same instant keep their order.** When two people wrote to a topic within the same
162
+ millisecond, the bot could take the older request for the newer one — and answer or remind the wrong person first.
163
+
164
+ **«⏭ Skip once» and a given-up limit wait stay settled across a bot restart.** When a Claude topic's limit wait ended
165
+ without its resume — skipped, given up, or taken over — the next restart (hot mode restarts on every update) found the
166
+ same limit error at the end of the agent's log and armed the wait again: the «usage limit reached» notice came back and the
167
+ topic was resumed after all. Such a wait is now remembered as handled; a new limit error later is still picked up.
168
+
169
+ **A weekly usage limit waits for its reset date instead of retrying every hour.** A limit more than a day away is written
170
+ with its date — «You've hit your weekly limit · resets Oct 25, 3pm» — which the bot could not read: it resumed the topic into
171
+ the limit every hour, and after six tries gave up with «Couldn't resume». It now waits for that date and time (in the zone
172
+ the message names) and resumes once.
173
+
174
+ **A usage-limit reset written with its time zone is read in that zone.** Claude Code names the zone it wrote the reset in
175
+ — «You've hit your session limit · resets 2:10pm (Europe/Moscow)» — and the bot read the clock in its own zone instead: when
176
+ the two differ, the topic resumed too early (and hit the limit again) or waited hours too long. Daylight saving is the named
177
+ zone's own.
178
+
179
+ **A usage warning is no longer announced as an exhausted limit.** Claude Code warns before a limit is reached —
180
+ «Approaching session limit · resets 11:29pm», «You've used 90% of your weekly limit · resets …», and «… continuing with usage
181
+ credits» / «Now using extra usage» once paid extra usage takes over. On a topic running Claude in tmux mode the bot read the
182
+ warning as the limit itself: it posted «🚧 Usage limit reached — will auto-resume after reset (~<time>)» while the account still
183
+ had room, and the work waited for the reset. Only the real block («You've hit your session limit · resets …») stops a topic now.
184
+
185
+ **After a restart, a limit that had already reset is no longer announced as exhausted until the next day.** When the bot
186
+ came back to a topic still parked on a limit error (`resets 10:50pm`), it read the reset time against the restart instead
187
+ of the error, so a window that had reset at 22:50 became «resumes after reset (~tomorrow 22:50)». The topic now resumes at
188
+ once with the «limit window has reset» line, and no limit notice.
189
+
190
+ **An OpenCode server that goes down no longer floods the topics with errors and drops their sessions.** When `opencode
191
+ serve` stopped, the bot made one attempt to start it again; if that attempt failed — the binary not found («spawn opencode
192
+ ENOENT»), a slow start — every OpenCode topic got the raw error and lost its session, and a message sent meanwhile failed
193
+ with «fetch failed». A session start or resume that found the server down failed the same way. Now every way of bringing the
194
+ server up installs a missing binary first and retries for about a minute. A restart is silent: the sessions are restored,
195
+ only a topic whose reply was in progress is told it was lost, and a message sent during the restart is delivered once the
196
+ server is back. Only if every attempt fails does a topic get one short line, in its language, saying the next message will
197
+ try again — and that next message no longer claims the conversation was lost and starts a new one. A binary that only lives
198
+ in `~/.npm-global/bin` is no longer reinstalled on every start.
199
+
200
+ **A conversation whose transcript Claude Code's cleanup removed no longer kills the topic's agent.** Claude Code deletes
201
+ the transcripts of old conversations; a running agent that is stopped after that writes only a metadata stub back, which the
202
+ CLI refuses to resume («No conversation found»). The bot took the stub for a conversation: a change of account, model or
203
+ effort ended with an error in the topic and a dead agent, and the next message failed the same way. A transcript is now
204
+ resumed only when it holds messages. A topic whose conversation is gone gets one line saying so and a fresh session — on its
205
+ next message, and also when an account, model or effort change re-spawned its running agent onto the lost conversation (it
206
+ starts again under the same id; the agent has forgotten the earlier context, which the line says). The same goes for a sleeping
207
+ conversation whose transcript the cleanup removed entirely (no agent was running to leave a stub): the bot remembers that
208
+ it took turns. A session that never took a turn has no transcript and nothing to lose: it wakes silently. A re-spawn waits
209
+ up to 5 s for the old process to exit and kills it if it will not, so two agents never run on one conversation.
210
+
211
+ **The `/limits` board showed the old ✓ after a slow default change.** Changing the default Claude account from General
212
+ re-spawns every running topic that follows it, which can take about half a minute; Telegram had stopped waiting for the
213
+ tap by then, the pop-up was refused, and the board was never redrawn until the next `/limits`. The board (and `/accounts`,
214
+ the model pages, `/limits_autoswitch` — the limit notice's «Switch to …» button included — and the `/model` and `/effort` pickers)
215
+ now always finish what the tap does, and a confirmation whose pop-up could not be shown is posted as a message.
216
+
217
+ **`/limits` is a switch board: every account and provider with its usage, one tap to move the topic.** The message opens
218
+ with `Usage (% used)` and a line per signed-in Claude account in the `/accounts` wording — `default — ⏱ 5h 14% 🔄 in 3h · week 3%
219
+ 🔄 in 6d`, `⛔ at its limit until Sun 01:00` while limited — and the ChatGPT subscription's windows after them (`openai (ChatGPT) — ⏱ 5h …`),
220
+ then the automatic-switching state and the caption «Choose the active model:». Below it every signed-in Claude account and each connected
221
+ OpenCode provider is one button with its name only — `✓ Claude · work`, `Claude · default`, `OpenCode · openai` (the figures do not fit a
222
+ phone's button) — and a tap switches this topic: another
223
+ account (model and conversation kept), an OpenCode provider (its model page opens; the same OpenCode session just changes the
224
+ model), the ✓ row (change the model of where the topic runs). Across backends with a conversation to hand over it asks first;
225
+ while a usage-limit wait is armed the tap is the limit move (on the limited Claude account a model the limit does not cover — `opus` under a Fable-only weekly limit — continues the work too). The usage-limit notice now ends «The limit resets <time>.
226
+ Switch: /limits». In General a Claude row sets the default account.
227
+
228
+ **Fable is judged on all three of its windows.** The session window and the shared weekly «all models» window bound every
229
+ model, Fable included; Fable additionally drains its own weekly window. The `claude_profiles` recommendation (MCP tool and
230
+ `charness claude-profiles`), the fallback chain's availability and the account ranking on a limit used to look at Fable's own
231
+ window and the session window only, so an account with its shared weekly window exhausted could still be recommended for Fable.
232
+
233
+ **The fallback setup is its own screen, `/limits_autoswitch`.** The ordered fallback list, «🔁 Auto switch» and «⏳ Wait for
234
+ reset» moved there from the old `/limits` (minus the usage block); its rows now read `✓ 1. Claude · work · opus` /
235
+ `3. OpenCode · openai · gpt-5`. `/limits rebuild|clear|switch|wait …`, `/fallback` and `/auto_continue_limits` keep working.
236
+
237
+ **`/accounts` tells the truth about OpenCode.** Its title and ✓ say where the topic really runs (`OpenCode openai/gpt-5`),
238
+ an OpenCode provider row switches the topic to that provider through the model page (`/accounts use <provider>/<model>` is
239
+ the typed form), and a provider signed in with a ChatGPT subscription shows its ~5 h / ~7 d usage windows like a Claude account.
240
+
241
+ **A default reasoning effort for the whole instance: `/effort` in General.** New sessions no longer start on `xhigh`
242
+ in every topic that never picked a level: the default is `medium`, and the General topic's `/effort` sets it for all
243
+ topics at once (saved in `settings.json`, see [SETUP.md § Instance settings](SETUP.md#instance-settings-settingsjson)).
244
+ A topic's own `/effort` pick still wins, and «As in General» in a topic gives it up again.
245
+
246
+ **Voice recognition: one settings file, `/stt`, and a recogniser that runs on your machine.** The speech engines
247
+ (Groq, OpenAI, any OpenAI-compatible server, and the local GigaAM v3) are an ordered list in `stt.json`, edited from
248
+ `/stt` or by hand. Details: [SETUP.md § Voice input](SETUP.md#voice-input).
249
+
250
+ **A shorter notice when a usage limit moves a topic to another account or model.** One line now says which account
251
+ (or OpenCode model) hit its limit, where the topic continues — its model only when the switch changes it — when the
252
+ limit resets, and `/limits` to switch, instead of repeating every label and listing four commands.
253
+
254
+ **Switching a topic between Claude and OpenCode by hand now asks first, and a Claude account tapped in `/accounts` on an
255
+ OpenCode topic really switches.** `/claude`, `/opencode` (and the agent buttons / «start …» phrases) on a topic that
256
+ runs the other backend, and a Claude account picked in `/accounts` (`/accounts use <name>`) on an OpenCode topic —
257
+ which only recorded the pick before — ask «Switch this topic from OpenCode to Claude «work»? The conversation context
258
+ will be handed over in compacted form.» with ✅ Continue / ✖️ Cancel when there is a conversation to hand over; Continue
259
+ moves the topic the way a usage limit does (the other backend's parked conversation is resumed, the context bridge is the
260
+ first prompt), Cancel changes nothing, and a topic with nothing to hand over switches at once. `/limits_autoswitch` asks
261
+ the same way before a rung of a backend the list does not use yet is added or included again.
262
+
263
+ **An answer tapped on a re-asked question no longer gets lost.** The idle compaction posts the re-asked question just
264
+ before it stops the agent's process; a tap while that process was being stopped went to it and vanished. Such a tap is
265
+ now held and reaches the agent once the conversation is resumed, like a message typed at that moment. A tap or a message
266
+ that came a moment before the stop began, and was still on its way to the agent when it started, is held or wakes the
267
+ conversation the same way.
268
+
269
+ ### Upgrading
270
+ - **`/limits` is a different screen now.** The fallback list and the two settings are in `/limits_autoswitch`; `/limits`
271
+ is the switch board. Old buttons on an old `/limits` message keep working and re-render that message as
272
+ `/limits_autoswitch`; the text forms (`/limits rebuild`, `/limits switch on`, …) keep working too. A bot menu entry for
273
+ `/limits_autoswitch` is added at the next start.
274
+ - **A new outgoing request.** When OpenCode holds a ChatGPT subscription login for the `openai` provider, the bot reads
275
+ its usage windows with the stored token from `https://chatgpt.com/backend-api/wham/usage` (every 10 minutes and when
276
+ `/limits` or `/accounts` opens). The token is never refreshed or logged; an API-key provider triggers no request.
277
+ - **The default effort is `medium` now (it was `xhigh`).** Topics with no `/effort` pick of their own start their next
278
+ session on `medium`; running sessions keep their level. To get the old behaviour back, pick `xhigh` in General's
279
+ `/effort` (or write `{ "defaultEffort": "xhigh" }` into `settings.json` beside your env file).
280
+ - **`GROQ_API_KEY` / `OPENAI_API_KEY` move out of the env file.** At the first start without a `stt.json` the bot
281
+ copies them into it (Groq first) and logs a `[deprecated]` line; from then on the env keys are ignored. Use `/stt`
282
+ or edit `stt.json`, and delete the keys from the env file when convenient.
283
+ - **`stt.json` lives beside your real env file** (a symlinked `.env` is resolved: it goes next to the target). That
284
+ directory must be writable and survive restarts.
285
+ - **Docker: mount the config DIRECTORY, not only the env file.** `examples/docker-compose.yml` now takes
286
+ `INSTANCE_CONFIG_DIR` (the directory that holds the env file) in place of `INSTANCE_ENV_FILE`: move the env file into
287
+ a directory of its own, e.g. `/srv/charness/config/instance.env`, mount that directory writable and point `ENV_FILE`
288
+ at the file inside it (`docker run` form: SETUP.md § Run with Docker). With the old single-file mount `stt.json` lands
289
+ on the container's own disk: it is lost whenever the container is re-created, and the old keys are copied in again each
290
+ time. The bot warns at boot when it runs in a container and `stt.json` is not on a mount (`[STT] … is on this
291
+ container's own disk`), and when the directory cannot be written at all. The local engine itself is not available in
292
+ the Docker image yet (no `python3-venv`).
293
+
294
+ ### Added
295
+ - **`/effort` in General** sets the instance default; **`/effort` in a topic** shows where its level comes from and
296
+ offers «As in General». `/status`, the ready notice and the pinned banner show the level a topic's session really runs —
297
+ after a General change a running session keeps its level, shown as «xhigh (medium from the next start)». OpenCode
298
+ rounds General's value to the model's variants (`ultracode` = its highest, `auto` = no variant).
299
+ The default lives in `settings.json` (beside `stt.json`), hand-editable without a restart.
300
+ - **`/stt`**: the main screen is the engine list, «➕ Add engine» and the language; each engine has a card (order, use /
301
+ don't use, timeout, and a key for the hosted ones); Groq, OpenAI and a custom server are added through «➕ Add engine».
302
+ A pasted key or an address with a login is deleted from the topic at once and kept out of the logs and the output trace.
303
+ - **Local recognition (GigaAM v3, Russian):** a recogniser process that starts with the first voice that needs it,
304
+ survives hot reloads, stops after idle time and with its owner, and falls back to the next engine on any failure.
305
+ It is added through «➕ Add engine» like any other (a confirmation and a live install progress when the model is not
306
+ installed), and everything local-specific — status, idle exit, workers, try again after failed starts, remove from list,
307
+ remove the model files — is on its own card, not on the main screen; the cloud engines' cards show only the common
308
+ controls and their key or address. Memory: about 0.5 GB while it runs, up to about 0.9 GB on a long voice.
309
+
310
+ ### Fixed
311
+ - A voice note whose Telegram `duration` is 0 (a sender that gave no length) is treated as of unknown length, not as a
312
+ 0-second voice: it gets the same time budget as a voice without a length, so a long one no longer falls back to the
313
+ next engine at the bare base timeout.
314
+ - Going from Claude to OpenCode and back (`/opencode` then `/claude`, a usage-limit switch to an OpenCode model and back) resumes
315
+ the Claude conversation the topic left; it used to come back to a fresh one, because leaving OpenCode overwrote the
316
+ parked Claude conversation id with the OpenCode session's. The same holds the other way round.
317
+ - A Claude json-stream session re-attached after a bot restart keeps its model (a `/model` pick, or Jira's configured
318
+ one) when it is re-spawned for an effort or account change; it used to restart on Claude's default model.
319
+ - An OpenCode model that has no effort levels shows none: `/status`, the pinned banner and the ready notice no longer
320
+ name General's default for a session that runs without any level (`/effort` already said «not available»).
321
+
322
+ ## 3.2.0 — 2026-10-16
323
+
324
+ **Renamed: what was left under the TelegramCode name is now Charness.** The old names still work and each one
325
+ in use prints a `[deprecated]` line at start — rename them when you see it. Steps for an npm install, a source
326
+ checkout with self-update and Docker: [MIGRATION_RENAME_TO_CHARNESS.md](MIGRATION_RENAME_TO_CHARNESS.md).
327
+
328
+ ### Breaking / renamed
329
+ - **`charness` is the command in every message, help text and document.** `telegramcode` and `telegramCode`
330
+ stay as aliases.
331
+ - **Global config: `~/.config/charness/.env`** (was `~/.config/telegramcode/.env`; the older
332
+ `~/.config/telegram-code` too). Moved by the automatic migration below; by hand:
333
+ `mv ~/.config/telegramcode ~/.config/charness`.
334
+ - **Default data dir: `~/.charness`** (was `~/.telegramCode`). An existing `~/.telegramCode` is kept while
335
+ `~/.charness` does not exist; an explicit `DATA_DIR` is untouched. Moved by the automatic migration below; by
336
+ hand, with the bot stopped and no agent running — or set `DATA_DIR` to the old path to keep it.
337
+ - **Hot-mode variables: `CHARNESS_HOT_RELOAD`, `CHARNESS_OPENCODE_PROCESS_FILE`** (were `TELEGRAMCODE_HOT_RELOAD`,
338
+ `TELEGRAMCODE_OPENCODE_PROCESS_FILE`). The supervisor sets both; restart `charness hot` once after the update.
339
+ - **MCP client-id header: `x-charness-mcp-client-id`** (was `x-telegramcode-mcp-client-id`); the server accepts
340
+ both.
341
+ - **OpenCode compaction plugin: `charness-compaction.js`** (was `telegramcode-compaction.js`); the old file is
342
+ removed when the new one is installed.
343
+ - **Docker: image, service and container `charness`, `charness-main`**; mounts `/opt/charness`,
344
+ `/home/charness`; restart command `charness-restart-bot`; compose variables `CHARNESS_DATA_DIR`,
345
+ `CHARNESS_WORK_ROOT`. A running container of the old image is not affected; the new entrypoint accepts the old
346
+ mount points and `telegramcode-restart-bot` still works.
347
+
348
+ ### Added
349
+ - **Automatic migration at a cold start.** The bot moves `~/.config/telegramcode` (or the older
350
+ `~/.config/telegram-code`) to `~/.config/charness` and the default `~/.telegramCode` to `~/.charness`, leaves a
351
+ relative symlink at each old path (running agents and scripts keep working), renames `TELEGRAMCODE_*` keys to
352
+ `CHARNESS_*` in the config file it moved (the original kept as `.env.bak`) and logs one `[migrate]` line per
353
+ action. It runs under the instance lock before the state is loaded, only on the plain start and the hot
354
+ supervisor's start (never on a worker reload), never for an explicit `DATA_DIR` or an `ENV_FILE` instance,
355
+ and is idempotent. Details: [MIGRATION_RENAME_TO_CHARNESS.md](MIGRATION_RENAME_TO_CHARNESS.md#automatic-migration).
356
+ - `scripts/self-update.sh` names a newly added `MIGRATION_*.md` in its log.
357
+ - **Jira: an @-mention of the AI account in a comment wakes it to answer**, without the issue being handed over:
358
+ it reads and checks, changes nothing, and its answer is a comment. On with a `trustedGroup` (only a trusted
359
+ author and last editor count); `wakeOnMention: false` turns it off. See SETUP.md § Jira.
360
+ - **Jira: an answer notifies the person it is for** — it @-mentions the requester of a mention and the authors
361
+ of the comments it replies to.
362
+ - **Answers are written briefly, in plain words, for a manager or QA** — on Jira issues and in Telegram topics
363
+ alike.
364
+
365
+ ### Changed
366
+ - Jira: a hand-over is noticed within 30 s by default (`pollIntervalSeconds` default 30, was 90).
367
+
368
+ ### Fixed
369
+ - Jira: a mention no longer displaces a hand-over without a requester, and a superseded question no longer asks
370
+ to assign the issue back.
371
+ - A Claude session adopted under the old data-dir path is no longer re-spawned as another account.
372
+
373
+ ## 3.0.0 — 2026-10-07
374
+
375
+ **Published as `charness`.** The npm package is renamed from `telegramcode` to
376
+ `charness`, after the project. To move over:
377
+ `npm uninstall -g telegramcode && npm install -g charness`. The `telegramcode`
378
+ command, the `~/.config/telegramcode` config, the `~/.telegramCode` data dir and
379
+ the `TELEGRAMCODE_*` variables stay as they were, so an existing setup carries
380
+ over unchanged; `charness` is a new alias of the command.
381
+
382
+ ### Added
383
+ - **Requests and answers.** A message, a voice note or a scheduled run opens a
384
+ request; the agent answers it with the `answer_request` tool (progress,
385
+ question or final), and every answer is pinned in the topic (only the latest
386
+ stays pinned). An agent that ends its turn without answering is woken, and
387
+ the operator is alerted when waking gives up. A usage-limit stop answers the
388
+ open request.
389
+ - **`/verbosity` views**: Stream only, Stream + answers, Answers only. In
390
+ Answers only, an agent that keeps writing after its final answer is reminded
391
+ to send whatever matters.
392
+ - **Sleeping Claude conversations.** An idle json-stream conversation is
393
+ compacted and its process stopped; the next message resumes it. A third
394
+ Claude backend, per-turn, stops the process after every turn.
395
+ - **Compaction**: `/compact` on every agent backend, compaction on idle, the
396
+ agent's own `compact_conversation` tool, and the summary posted to the topic
397
+ on request (`/compact_summary`, off by default). Every compaction summary
398
+ names the skills to load again.
399
+ - **Usage limits**: wait for the reset and resume by itself
400
+ (`/auto_continue_limits`).
401
+ - **`/reminders`**: reminders the bot posts and pins itself, without an agent.
402
+ - **Watchdog checks**: `schedule_create` with a `checkCommand` runs a check on
403
+ schedule and wakes the agent on the first failure.
404
+ - **`/timezone`**: one instance-wide time zone for schedules and timestamps.
405
+ - **`/model`**: a two-level picker with pages and per-provider disabling;
406
+ the model and effort are shown when an agent starts or switches.
407
+ - **OpenCode `/connect`** works with ordinary providers.
408
+ - **Replies**: the replied-to message goes into the agent's prompt, and a
409
+ replied-to photo, file or voice note is named with the bot's saved copy.
410
+ - **Voice**: transcription is retried after a timeout or a transient error.
411
+ - **`send_messages_to_user`**: each message may carry a file.
412
+ - **Jira Cloud connector** (`CONNECTORS=jira`): an issue assigned to the AI
413
+ account becomes a request in its own session, and the answers come back as
414
+ comments.
415
+ - **Docker**: the whole bot runs in one container (`docker/`).
416
+
417
+ ### Fixed
418
+ - Many fixes in output streaming, session restore after a restart, OpenCode
419
+ event routing and the Claude screen scrape; see the git history.
420
+
421
+ ## Earlier changes, shipped in the 2.0.x packages
422
+
423
+ ### Changed
424
+ - **`WORK_DIR` is no longer fatal.** Launch `telegramcode` from the parent
425
+ folder containing your projects — that `$PWD` becomes the work root.
426
+ `WORK_ROOT` is now an advanced override (services / containers where the
427
+ process cwd can't be controlled); a stray legacy `WORK_DIR` is ignored, not a
428
+ startup error.
429
+ - **`ALLOWED_GROUP_ID` is now optional.** Leave it empty to auto-pair with the
430
+ first forum supergroup an admin contacts the bot from; set it only to pin a
431
+ specific group before first contact.
432
+ - **OpenCode no longer interrupts a running turn for a new prompt.** A prompt
433
+ sent while OpenCode is busy is queued by `prompt_async` and picked up promptly
434
+ instead of aborting the live turn (which only lost work). Claude keeps its
435
+ Escape-and-wait — its TUI ignores typed input mid-turn.
436
+ - **Bot UI language is resolved per Telegram chat.** A `/language` override wins,
437
+ otherwise the bot uses Telegram `language_code`, then the last seen chat
438
+ locale, then English; the old global env language knob is removed.
439
+
440
+ ## 2.1.0 — 2026-05-13
441
+
442
+ **Audit fixes release.** Full repo audit (50 findings across security,
443
+ concurrency, resource handling, error contracts, infra) closed in 20
444
+ focused commits. See `agent/tasks/actual/2026-05-13-audit-fixes.md` for
445
+ the full plan; highlights below.
446
+
447
+ ### Security
448
+ - **Two RCEs closed** in `claudeCliAdapter`: `sendInput` and
449
+ `startSession`/`resumeSession` built tmux command lines via
450
+ `execSync` with `JSON.stringify`-interpolated user input, letting any
451
+ allowed user run arbitrary host commands via `$(…)` / backticks in a
452
+ thread message. Reroute all tmux calls onto the argv-based helper;
453
+ single-quote every element of the final `tmux new-session` shell
454
+ command (S1).
455
+ - **Forum-topic auth bypass** closed: `forum_topic_created` /
456
+ `_closed` / `_reopened` are now gated on `ALLOWED_USERS`, not just
457
+ the chat allowlist. A non-allowed admin previously could rename a
458
+ topic to fuzzy-match a sensitive WORK_ROOT subdir and have the next
459
+ allowed-user message launch an agent there (S2).
460
+ - **OpenCode SSE cross-thread events**: `permission.asked` and
461
+ `question.asked` events without `sessionID` used to fan out to every
462
+ active thread, auto-approving permissions and stacking duplicate
463
+ questions. Now any event without a `sessionID` is dropped (S5).
464
+ - **SSRF guard** on `OPENCODE_URL` (loopback by default, opt in with
465
+ `OPENCODE_ALLOW_REMOTE=1`); 30 s timeout on every fetch; abortable
466
+ SSE reader; 10-minute wall-clock cap on SSE reconnect (S7).
467
+ - **Docker hardening**: drop `user: root`, `cap_drop: [ALL]`,
468
+ `security_opt: no-new-privileges`, bounded `mem_limit`/`pids_limit`,
469
+ healthcheck. `.docker-entrypoint.sh` uses `runuser -- "$@"` instead
470
+ of `su -c "$*"` so argv stays argv. claude-code install moved to
471
+ build time with a pinned version (S16).
472
+
473
+ ### Bugs
474
+ - `resume_<idx>` callback: full session id is recovered from a
475
+ per-thread map instead of being truncated into callback_data, which
476
+ used to make OpenCode resume target a non-existent id (S4).
477
+ - Pinned-banner pipeline serialised per key so concurrent adapter
478
+ events can't stack duplicate pinned messages (S6).
479
+ - OpenCode lifecycle: per-key serialisation of start/stop, cleanup of
480
+ `statusDebounceTimer` and `reconnectTimer` on stop, explicit
481
+ teardown of every session after `restartServer` (S8).
482
+ - Claude adapter: auto-Enter / auto-Accept timers tracked + cleared on
483
+ stop; `pollOutput` uses self-rescheduling `setTimeout` with
484
+ re-entrancy guard; `adoptExistingTmuxSession` rejects zombie panes
485
+ with no live child; capture-pane scrollback raised to 2000 lines (S9).
486
+ - `/clear` snapshots message ids under `state.withLock` so agent
487
+ messages pushed mid-deletion can't slip out of tracking (S11).
488
+ - `/output` flips `markNeedsNewMessage` and surfaces "+N more chunks
489
+ omitted" instead of silently dropping (S11).
490
+ - `/new` routes through `applyBinding` so collision warnings reach the
491
+ new thread (S11).
492
+ - `switchThreadAdapter` clears the previous adapter's stored session id
493
+ so post-restart reattach doesn't pick the wrong adapter (S12).
494
+ - In-memory per-thread maps GC'd against `state.json` every 60 s so
495
+ orphan entries from UI-deleted topics don't linger (S13).
496
+ - Numeric model-selection reply unconditionally returns; previously
497
+ fell through to NL-start + agent-forward when `setModel` was absent
498
+ (S12).
499
+ - `downloadFile` capped at 5 redirects with 20 s `setTimeout`;
500
+ `transcribeAudio` got the same timeout and a proper error handler
501
+ installed before `form.pipe(req)`. Voice file URL now built via
502
+ `ctx.telegram.getFileLink` so the bot token isn't materialised in a
503
+ JS string (S14).
504
+
505
+ ### Contracts
506
+ - `AgentAdapter` events documented: `startSession` throws on failure,
507
+ `error` is for async failures after a successful start, `closed` is
508
+ unsolicited death, `stopped` is explicit teardown. Claude's two
509
+ start paths now throw + clean up. `setModel` unified to
510
+ `Promise<string | null>`. OpenCode SSE giveup emits `closed` so the
511
+ bot wipes persisted ids (S10).
512
+
513
+ ### Hygiene
514
+ - `tsconfig.json`: `noUnusedLocals`, `noUnusedParameters`,
515
+ `noFallthroughCasesInSwitch`, `noImplicitOverride`,
516
+ `exclude: src/__tests__/**`.
517
+ - `package.json`: drop `ts-node`, move `typescript` to devDeps,
518
+ `private: true`, `engines.node >=22`.
519
+ - `BOT_LANG` lowercased + trimmed with a warn on unknown values.
520
+ - 19 hardcoded English `ctx.answerCbQuery` strings migrated to a new
521
+ `cb.*` namespace in i18n.
522
+ - `.env.example` no longer leaks the author's home path; the two-
523
+ instance example uses `REPLACE_ME` so a copy-pasted file fails
524
+ validation visibly.
525
+ - DATA_DIR resolved via `resolveDataDir()` everywhere; previously the
526
+ two adapters dropped state files in `$HOME` instead of `DATA_DIR`,
527
+ breaking two-instance isolation (S3).
528
+
529
+ ### Tests
530
+ - 13 new tests: i18n placeholders + fallback (S19/#50), agent NL
531
+ trigger parser extracted into `src/agentTrigger.ts` with full ru/en
532
+ coverage (S19/#25), state.json forward-compat round-trip with an
533
+ unknown future field. 98 tests passing (R9/R10 still skipped).
534
+
535
+ ### Deferred
536
+ - `noUncheckedIndexedAccess` in `tsconfig.json` (106 errors fallout —
537
+ needs a careful per-site task).
538
+ - Complete MarkdownV2 escaper (regression risk).
539
+ - bot.ts test coverage (3300 LOC — separate task).
540
+
541
+ ## 2.0.0 — 2026-05-12
542
+
543
+ **Breaking release.** The bot now routes through a Telegram forum
544
+ supergroup with one topic per task; the 1.x "one bot = one private chat
545
+ = one folder" mode is removed. See [Migration](README.md#migration-from-1x).
546
+
547
+ ### Added
548
+ - **Forum-supergroup routing.** `ThreadKey = (chatId, threadId)` is the
549
+ routing key everywhere; threads are bound to subfolders of `WORK_ROOT`
550
+ and run their own `claude` or `opencode` session in parallel.
551
+ - **Persistent state** (`src/state.ts`) — `${DATA_DIR}/state.json` with
552
+ atomic writes (write tmp → fsync fd → rename → fsync parent dir),
553
+ per-key async-lock for concurrent `/bind`, corrupted-file archival to
554
+ `state.json.corrupted-<ts>` with a fresh restart, debounced saves with
555
+ immediate flush on critical paths.
556
+ - **Restart-safe sessions** — claude tmux sessions named
557
+ `claude-${chatId}-${threadId}` are re-adopted on boot via
558
+ `tmux ls`; opencode SSE streams are reopened from persisted
559
+ `opencodeSessionId`. Orphan tmux sessions are killed.
560
+ - **Per-thread Claude session id** — `crypto.randomUUID()` →
561
+ `claude --session-id <uuid>` on a fresh start, `--resume <uuid>` on
562
+ resume. Fixes the 1.x picker hang.
563
+ - **MCP hierarchy** — user (`~/.claude/settings.json`) /
564
+ group (`${DATA_DIR}/mcp.json`) /
565
+ project (`${workDir}/.mcp.json`) /
566
+ thread (`${DATA_DIR}/threads/<key>.json`). `${VAR}` placeholders are
567
+ expanded by the bot before claude sees the config; tmp files are
568
+ written `0600` and cleaned up on `stopSession`.
569
+ - **Commands.** `/bind`, `/unbind`, `/where` (per topic);
570
+ `/ls`, `/list`, `/new`, `/status` (global), `/doctor`, `/mcp`,
571
+ `/whoami`, `/version`, context-aware `/help`, `/stop-all`.
572
+ - **Path traversal protection** — NFC normalisation, control-char
573
+ rejection, `realpathSync` + strict containment, symlink-out rejection,
574
+ `/bind .` rejection.
575
+ - **Auto-welcome** when the bot is added to a forum supergroup, and
576
+ rich post-bind welcome with CLAUDE.md / .mcp.json / git stats.
577
+ - **Token-bucket rate limiter** — 1 token/sec per chat, burst 5,
578
+ layered with reactive 429-retry. Prevents the multi-thread N-msg/sec
579
+ storm that would otherwise breach Telegram's per-chat budget.
580
+ - **i18n** — `BOT_LANG=ru|en` (default ru) via `src/i18n.ts`.
581
+ - **Two-instance docker-compose** (`examples/docker-compose.yml`) — pet
582
+ + work, isolated `DATA_DIR`, group, token, `OPENCODE_URL` port.
583
+ - **Test suite** — `yarn test` (node:test + tsx). 54 tests cover R1
584
+ (ThreadKey round-trip), R2 (gating matrix), R3 (path-traversal
585
+ corpus), R4 (legacy file migration), R5 (concurrent state writes),
586
+ R6 (corrupted state archival), R7 (rate-limit token bucket + 429
587
+ retry), R8 (send-error classification).
588
+
589
+ ### Changed
590
+ - **`WORK_DIR` → `WORK_ROOT`.** `WORK_DIR` is now fatal — bot fails to
591
+ start with a migration hint. `WORK_ROOT` points at the *parent* folder
592
+ containing your projects.
593
+ - **`ALLOWED_GROUP_ID` is required.** Without it the bot has no idea
594
+ which forum supergroup to listen to.
595
+ - **`DATA_DIR`** — single-instance default `~/.telegramCode`, mandatory
596
+ for two-instance setups.
597
+ - **`--dangerously-skip-permissions`** hardcoded for symmetry with
598
+ opencode's auto-approve.
599
+ - **`/clear`** scoped to the current thread (uses persisted message ids),
600
+ chunked at 100 per call, surfaces the Telegram 48 h limit.
601
+ - **README** rewritten end-to-end around forum supergroup setup,
602
+ troubleshooting, MCP hierarchy, two-instance compose, migration.
603
+
604
+ ### Removed
605
+ - One-bot-per-private-chat mode and `WORK_DIR` env var.
606
+ - Legacy `~/.telegram-bot-messages.json` (renamed to `.bak` on first
607
+ start; no message-id migration into the new schema).
608
+
609
+ ### Deferred (Phase 9+)
610
+ - `/mcp-add`, `/mcp-remove`, `/mcp-restart` UI commands.
611
+ - OpenCode per-thread MCP override.
612
+ - Routines / opencode-scheduler integration (`/schedule`,
613
+ `/schedule-cloud`) — Phase 8.
614
+ - Claude Code hooks integration + `--input-format stream-json`
615
+ rewrite — Phase 9.
616
+
617
+ ### Stage 7 polish (shipped 2026-05-12)
618
+ - Per-thread pinned status banner (`📁 subdir · agent · model ·
619
+ state`) — edits in place on every adapter lifecycle event,
620
+ unpinned on `/unbind`.
621
+ - Fuzzy auto-bind on `forum_topic_created` — separator drift
622
+ (`my-api` ↔ `my_api` ↔ `my api`) is folded alongside case + NFC.
623
+ - `/bind` keyboard pagination at 20 folders/page with
624
+ `[⬅️ Prev] [N/M] [Next ➡️]` nav row; `WORK_ROOT` listing cap
625
+ raised 50 → 200.
626
+ - R9 (tmux re-attach) and R10 (claude `--resume` on bogus UUID)
627
+ filed as documented skip-tests pending an integration harness.
628
+
629
+ ### Migration
630
+ 1. Stop the 1.x bot.
631
+ 2. Create a forum supergroup, add the bot, follow
632
+ [README Quick Start](README.md#2-create-the-forum-supergroup).
633
+ 3. Rename `WORK_DIR` → `WORK_ROOT` and point at the parent folder.
634
+ 4. Add `ALLOWED_GROUP_ID` (find it via `/whoami` in the group).
635
+ 5. Set `DATA_DIR` if you run multiple instances on one host.
636
+ 6. `docker compose up -d`.
637
+
638
+ The legacy `~/.telegram-bot-messages.json` is moved to `.bak` on first
639
+ start; message-id history does not migrate.