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