charness 3.3.1 → 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.
- package/CHANGELOG.md +612 -0
- package/DEVELOPMENT.md +76 -0
- package/README.md +5 -2
- package/SETUP.md +66 -20
- package/dist/adapters/claudeJsonStreamAdapter.d.ts +2 -0
- package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.js +15 -2
- package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
- package/dist/adapters/openCodeAdapter.d.ts +13 -1
- package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/openCodeAdapter.js +35 -13
- package/dist/adapters/openCodeAdapter.js.map +1 -1
- package/dist/agentLogin/agentLogin.d.ts +1 -0
- package/dist/agentLogin/agentLogin.d.ts.map +1 -1
- package/dist/agentLogin/agentLogin.js +5 -0
- package/dist/agentLogin/agentLogin.js.map +1 -1
- package/dist/apiErrorRetry.d.ts +10 -4
- package/dist/apiErrorRetry.d.ts.map +1 -1
- package/dist/apiErrorRetry.js +14 -7
- package/dist/apiErrorRetry.js.map +1 -1
- package/dist/apiRetryKick.d.ts +6 -0
- package/dist/apiRetryKick.d.ts.map +1 -1
- package/dist/apiRetryKick.js +2 -1
- package/dist/apiRetryKick.js.map +1 -1
- package/dist/bot.d.ts +0 -3
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +337 -173
- package/dist/bot.js.map +1 -1
- package/dist/changelogEntries.d.ts +25 -0
- package/dist/changelogEntries.d.ts.map +1 -0
- package/dist/changelogEntries.js +66 -0
- package/dist/changelogEntries.js.map +1 -0
- package/dist/cli/bot.d.ts +6 -2
- package/dist/cli/bot.d.ts.map +1 -1
- package/dist/cli/bot.js +34 -2
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/hot.d.ts +7 -0
- package/dist/cli/hot.d.ts.map +1 -1
- package/dist/cli/hot.js +53 -3
- package/dist/cli/hot.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/connectors/telegram/bindPicker.d.ts +64 -0
- package/dist/connectors/telegram/bindPicker.d.ts.map +1 -0
- package/dist/connectors/telegram/bindPicker.js +97 -0
- package/dist/connectors/telegram/bindPicker.js.map +1 -0
- package/dist/connectors/telegram/commands/selfUpdate.d.ts +16 -0
- package/dist/connectors/telegram/commands/selfUpdate.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/selfUpdate.js +210 -0
- package/dist/connectors/telegram/commands/selfUpdate.js.map +1 -0
- package/dist/connectors/telegram/commands/stt.d.ts +1 -0
- package/dist/connectors/telegram/commands/stt.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/stt.js +5 -0
- package/dist/connectors/telegram/commands/stt.js.map +1 -1
- package/dist/connectors/telegram/commands/topicCommand.d.ts +75 -0
- package/dist/connectors/telegram/commands/topicCommand.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/topicCommand.js +391 -0
- package/dist/connectors/telegram/commands/topicCommand.js.map +1 -0
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +109 -3
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +109 -3
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +109 -3
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +109 -3
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +109 -3
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +109 -3
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +109 -3
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +109 -3
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +109 -3
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +109 -3
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +109 -3
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +109 -3
- package/dist/i18n/zh.js.map +1 -1
- package/dist/installManager.d.ts +28 -0
- package/dist/installManager.d.ts.map +1 -1
- package/dist/installManager.js +85 -11
- package/dist/installManager.js.map +1 -1
- package/dist/instanceSettings.d.ts +12 -1
- package/dist/instanceSettings.d.ts.map +1 -1
- package/dist/instanceSettings.js +40 -4
- package/dist/instanceSettings.js.map +1 -1
- package/dist/selfUpdate/bootRollback.d.ts +22 -0
- package/dist/selfUpdate/bootRollback.d.ts.map +1 -0
- package/dist/selfUpdate/bootRollback.js +79 -0
- package/dist/selfUpdate/bootRollback.js.map +1 -0
- package/dist/selfUpdate/commandRunner.d.ts +21 -0
- package/dist/selfUpdate/commandRunner.d.ts.map +1 -0
- package/dist/selfUpdate/commandRunner.js +32 -0
- package/dist/selfUpdate/commandRunner.js.map +1 -0
- package/dist/selfUpdate/constants.d.ts +26 -0
- package/dist/selfUpdate/constants.d.ts.map +1 -0
- package/dist/selfUpdate/constants.js +31 -0
- package/dist/selfUpdate/constants.js.map +1 -0
- package/dist/selfUpdate/gitSource.d.ts +58 -0
- package/dist/selfUpdate/gitSource.d.ts.map +1 -0
- package/dist/selfUpdate/gitSource.js +183 -0
- package/dist/selfUpdate/gitSource.js.map +1 -0
- package/dist/selfUpdate/hostLock.d.ts +16 -0
- package/dist/selfUpdate/hostLock.d.ts.map +1 -0
- package/dist/selfUpdate/hostLock.js +101 -0
- package/dist/selfUpdate/hostLock.js.map +1 -0
- package/dist/selfUpdate/hotSupervisor.d.ts +47 -0
- package/dist/selfUpdate/hotSupervisor.d.ts.map +1 -0
- package/dist/selfUpdate/hotSupervisor.js +125 -0
- package/dist/selfUpdate/hotSupervisor.js.map +1 -0
- package/dist/selfUpdate/installKind.d.ts +45 -0
- package/dist/selfUpdate/installKind.d.ts.map +1 -0
- package/dist/selfUpdate/installKind.js +99 -0
- package/dist/selfUpdate/installKind.js.map +1 -0
- package/dist/selfUpdate/launchEnvironment.d.ts +3 -0
- package/dist/selfUpdate/launchEnvironment.d.ts.map +1 -0
- package/dist/selfUpdate/launchEnvironment.js +16 -0
- package/dist/selfUpdate/launchEnvironment.js.map +1 -0
- package/dist/selfUpdate/npmSource.d.ts +34 -0
- package/dist/selfUpdate/npmSource.d.ts.map +1 -0
- package/dist/selfUpdate/npmSource.js +107 -0
- package/dist/selfUpdate/npmSource.js.map +1 -0
- package/dist/selfUpdate/releaseNotes.d.ts +33 -0
- package/dist/selfUpdate/releaseNotes.d.ts.map +1 -0
- package/dist/selfUpdate/releaseNotes.js +115 -0
- package/dist/selfUpdate/releaseNotes.js.map +1 -0
- package/dist/selfUpdate/restart.d.ts +33 -0
- package/dist/selfUpdate/restart.d.ts.map +1 -0
- package/dist/selfUpdate/restart.js +94 -0
- package/dist/selfUpdate/restart.js.map +1 -0
- package/dist/selfUpdate/selfCheck.d.ts +20 -0
- package/dist/selfUpdate/selfCheck.d.ts.map +1 -0
- package/dist/selfUpdate/selfCheck.js +126 -0
- package/dist/selfUpdate/selfCheck.js.map +1 -0
- package/dist/selfUpdate/semver.d.ts +24 -0
- package/dist/selfUpdate/semver.d.ts.map +1 -0
- package/dist/selfUpdate/semver.js +165 -0
- package/dist/selfUpdate/semver.js.map +1 -0
- package/dist/selfUpdate/types.d.ts +51 -0
- package/dist/selfUpdate/types.d.ts.map +1 -0
- package/dist/selfUpdate/types.js +3 -0
- package/dist/selfUpdate/types.js.map +1 -0
- package/dist/selfUpdate/updateEngine.d.ts +110 -0
- package/dist/selfUpdate/updateEngine.d.ts.map +1 -0
- package/dist/selfUpdate/updateEngine.js +727 -0
- package/dist/selfUpdate/updateEngine.js.map +1 -0
- package/dist/selfUpdate/updateMessages.d.ts +84 -0
- package/dist/selfUpdate/updateMessages.d.ts.map +1 -0
- package/dist/selfUpdate/updateMessages.js +115 -0
- package/dist/selfUpdate/updateMessages.js.map +1 -0
- package/dist/selfUpdate/updateModes.d.ts +17 -0
- package/dist/selfUpdate/updateModes.d.ts.map +1 -0
- package/dist/selfUpdate/updateModes.js +14 -0
- package/dist/selfUpdate/updateModes.js.map +1 -0
- package/dist/selfUpdate/updatePending.d.ts +76 -0
- package/dist/selfUpdate/updatePending.d.ts.map +1 -0
- package/dist/selfUpdate/updatePending.js +131 -0
- package/dist/selfUpdate/updatePending.js.map +1 -0
- package/dist/selfUpdate/updatePolicy.d.ts +115 -0
- package/dist/selfUpdate/updatePolicy.d.ts.map +1 -0
- package/dist/selfUpdate/updatePolicy.js +83 -0
- package/dist/selfUpdate/updatePolicy.js.map +1 -0
- package/dist/selfUpdate/updateRuntime.d.ts +22 -0
- package/dist/selfUpdate/updateRuntime.d.ts.map +1 -0
- package/dist/selfUpdate/updateRuntime.js +148 -0
- package/dist/selfUpdate/updateRuntime.js.map +1 -0
- package/dist/startupPromptBuffer.d.ts +2 -0
- package/dist/startupPromptBuffer.d.ts.map +1 -1
- package/dist/startupPromptBuffer.js +4 -0
- package/dist/startupPromptBuffer.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +18 -0
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/promptDelivery.d.ts +10 -4
- package/dist/utils/promptDelivery.d.ts.map +1 -1
- package/dist/utils/promptDelivery.js +6 -3
- package/dist/utils/promptDelivery.js.map +1 -1
- package/dist/utils/updateScreenPlan.d.ts +38 -0
- package/dist/utils/updateScreenPlan.d.ts.map +1 -0
- package/dist/utils/updateScreenPlan.js +143 -0
- package/dist/utils/updateScreenPlan.js.map +1 -0
- 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.
|