Skip to content

Commit 100b591

Browse files
fix telegram html send failures and harden docker rebuild fingerprinting
1 parent de35c28 commit 100b591

6 files changed

Lines changed: 32 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ All notable changes to this project are documented in this file.
1111
### Added
1212

1313
- Deterministic Docker redeploy script: `scripts/docker-redeploy.sh` (`npm run docker:redeploy`).
14+
- now auto-stamps `OPENCODE_REMOTE_BUILD_ID` from git short SHA + timestamp when not explicitly set.
1415
- Runtime fingerprint logging at startup (version/build-id/token fingerprint/mode summary).
1516
- Polling recovery diagnostics in status surfaces:
1617
- reset cooldown timing
@@ -134,6 +135,7 @@ All notable changes to this project are documented in this file.
134135
- Telegram polling conflict owner alerts are now cooldown-limited to reduce repeated notification spam.
135136
- Telegram polling startup now prepares session state (`deleteWebhook` + `close` with retry-after-aware cooldown handling).
136137
- Polling loop now enforces single in-flight cycle to avoid overlap and improve conflict stability.
138+
- Telegram outbound message send now escapes HTML entities before `parse_mode=HTML` delivery to prevent `can't parse entities` send failures.
137139
- Runtime `/status` output now includes transport health and polling conflict recovery timing.
138140
- SQLite schema now includes transport lease support (`transport_leases`) to protect polling ownership on shared DB deployments.
139141
- Webhook mode now fails fast unless `telegram.webhookSecret` is configured.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ docker compose logs -f remote
8686
```
8787

8888
This redeploy command forces a no-cache image build and container recreate so Docker always runs the latest source changes.
89+
It also stamps each build with `OPENCODE_REMOTE_BUILD_ID` (git short SHA + timestamp by default) for runtime fingerprint verification.
8990

9091
Webhook-first production profile:
9192

docs/OPERATIONS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ npm run docker:redeploy
9696
```
9797

9898
This performs a no-cache image rebuild and force-recreates the `remote` service to prevent stale code/image mismatches.
99+
By default it sets `OPENCODE_REMOTE_BUILD_ID` to `git-short-sha + timestamp` so startup fingerprint logs prove which build is running.
99100

100101
## Initial Provisioning
101102

scripts/docker-redeploy.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ set -euo pipefail
44
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
55
cd "$ROOT_DIR"
66

7+
if [[ -z "${OPENCODE_REMOTE_BUILD_ID:-}" ]]; then
8+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
9+
OPENCODE_REMOTE_BUILD_ID="$(git rev-parse --short HEAD)-$(date +%Y%m%d%H%M%S)"
10+
else
11+
OPENCODE_REMOTE_BUILD_ID="local-$(date +%Y%m%d%H%M%S)"
12+
fi
13+
fi
14+
export OPENCODE_REMOTE_BUILD_ID
15+
16+
echo "[redeploy] Using build id: ${OPENCODE_REMOTE_BUILD_ID}"
17+
718
echo "[redeploy] Building compose service image (no cache)..."
819
docker compose build --no-cache remote
920

src/transport/telegram.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,8 @@ export class TelegramTransport {
553553
return;
554554
}
555555

556-
const chunks = this.chunkMessage(String(text || ''), 4096);
556+
const escaped = this.escapeHtml(String(text || ''));
557+
const chunks = this.chunkMessage(escaped, 4096);
557558
for (const chunk of chunks) {
558559
await this.api('sendMessage', {
559560
chat_id: chatId,
@@ -587,6 +588,13 @@ export class TelegramTransport {
587588
return chunks;
588589
}
589590

591+
escapeHtml(text: string): string {
592+
return String(text || '')
593+
.replace(/&/g, '&')
594+
.replace(/</g, '&lt;')
595+
.replace(/>/g, '&gt;');
596+
}
597+
590598
async moveToDeadLetter(update: TelegramUpdate, error: unknown, attempts: number) {
591599
const sender =
592600
String(update?.message?.from?.id || update?.callback_query?.from?.id || '') || null;

tests/telegram.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ test('normalizes plain telegram shorthand to slash commands', () => {
139139
assert.equal(transport.normalizeBody('help'), '/help');
140140
});
141141

142+
test('escapes html entities for telegram html parse mode', () => {
143+
const transport = new TelegramTransportStub(async () => null);
144+
assert.equal(
145+
transport.escapeHtml('Run: <runId> & <status>'),
146+
'Run: &lt;runId&gt; &amp; &lt;status&gt;',
147+
);
148+
});
149+
142150
test('polling loop does not overlap concurrent getUpdates requests', async () => {
143151
class PollingProbe extends TelegramTransport {
144152
current = 0;

0 commit comments

Comments
 (0)