@inetafrica/open-claudia 2.4.0 → 2.4.2
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 +10 -0
- package/channels/telegram/adapter.js +5 -3
- package/channels/telegram/format.js +33 -4
- package/core/runner.js +7 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.4.2
|
|
4
|
+
- Fix replies losing everything written before tool calls. The backend's final `result` event carries only the LAST text segment of a turn, but the stream parser assigned it over the accumulated `assistantText` — so a turn shaped "long explanation → tool calls → short closing line" delivered only the closing line, which read as nonsense without its context. `evt.result` is now a fallback used only when nothing was accumulated (some Cursor turns). Fixed in both `runClaude` and the auxiliary runner.
|
|
5
|
+
|
|
6
|
+
## v2.4.1
|
|
7
|
+
- Fix Telegram messages occasionally arriving as raw HTML (`<b>`, `<code>` shown literally). Root cause: the Markdown→HTML pass could inject `<i>` tags *inside* model-authored `<code>` spans (two snake_case identifiers on one line pair their underscores as italics), producing unbalanced HTML that Telegram rejects — and the parse-failure fallback then resent the converted body verbatim, tags and all.
|
|
8
|
+
- `<code>`/`<pre>` spans are now stashed whole (content included) before Markdown conversion, so nothing can be injected inside them.
|
|
9
|
+
- The italic `_..._` rule only fires at word boundaries, so snake_case identifiers anywhere in the text can no longer pair up.
|
|
10
|
+
- Model-authored entities (`<`, `&`, `&#…;`) are preserved instead of double-escaped — `<code><name></code>` now renders as `<name>` instead of `<name>`.
|
|
11
|
+
- Last-resort fallback (send + edit) now strips tags and decodes entities (`htmlToPlain`) so a rejected message degrades to clean plain text, never raw markup.
|
|
12
|
+
|
|
3
13
|
## v2.4.0
|
|
4
14
|
- **FTS5 transcript index (cross-session recall).** Project transcripts are now indexed in SQLite FTS5 via Node's built-in `node:sqlite` (no new dependency), turning "did we discuss X last week?" into a ~50ms ranked lookup instead of a linear grep over JSONL.
|
|
5
15
|
- `core/transcript-index.js`: WAL-mode DB at `~/.open-claudia/transcripts/index.db` (0600), FTS5 table with porter tokenizer, per-file byte offsets for idempotent incremental indexing. Partial trailing lines wait for the next pass; a replaced/truncated transcript drops its stale rows and reindexes. Fail-soft: without `node:sqlite` every call no-ops and transcript-window remains the path.
|
|
@@ -10,7 +10,7 @@ const TelegramBot = require("node-telegram-bot-api");
|
|
|
10
10
|
const { TEMP_DIR, FILES_DIR, CONFIG_DIR } = require("../../core/config");
|
|
11
11
|
const { canonicalForChannel } = require("../../core/identity");
|
|
12
12
|
const { portableToInlineKeyboard } = require("../types");
|
|
13
|
-
const { telegramHtml } = require("./format");
|
|
13
|
+
const { telegramHtml, htmlToPlain } = require("./format");
|
|
14
14
|
|
|
15
15
|
class TelegramAdapter {
|
|
16
16
|
constructor({ id = "telegram", token, ownerChatId, chatIds }) {
|
|
@@ -205,7 +205,7 @@ class TelegramAdapter {
|
|
|
205
205
|
|
|
206
206
|
async send(channelId, text, opts = {}) {
|
|
207
207
|
const o = {};
|
|
208
|
-
|
|
208
|
+
let body = opts.parseMode === "HTML" ? telegramHtml(text) : text;
|
|
209
209
|
if (opts.parseMode) o.parse_mode = opts.parseMode;
|
|
210
210
|
const kb = this._normalizeKeyboard(opts.keyboard);
|
|
211
211
|
if (kb) o.reply_markup = kb;
|
|
@@ -229,7 +229,9 @@ class TelegramAdapter {
|
|
|
229
229
|
continue;
|
|
230
230
|
}
|
|
231
231
|
if (opts.parseMode && o.parse_mode) {
|
|
232
|
+
console.error("Send: HTML rejected, falling back to plain:", errMsg);
|
|
232
233
|
delete o.parse_mode;
|
|
234
|
+
body = htmlToPlain(body);
|
|
233
235
|
continue;
|
|
234
236
|
}
|
|
235
237
|
console.error("Send error:", errMsg);
|
|
@@ -255,7 +257,7 @@ class TelegramAdapter {
|
|
|
255
257
|
if (errMsg.includes("message is not modified")) return;
|
|
256
258
|
if (opts.parseMode && o.parse_mode) {
|
|
257
259
|
delete o.parse_mode;
|
|
258
|
-
try { await this.bot.editMessageText(
|
|
260
|
+
try { await this.bot.editMessageText(htmlToPlain(body), o); return; } catch (e2) {}
|
|
259
261
|
}
|
|
260
262
|
if (!errMsg.includes("message to edit not found")) console.error("Edit error:", errMsg);
|
|
261
263
|
}
|
|
@@ -14,6 +14,15 @@ function escapeHtml(text) {
|
|
|
14
14
|
.replace(/>/g, ">");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Like escapeHtml, but keeps entities the model already wrote (< & {)
|
|
18
|
+
// instead of double-escaping them into visible "<" text.
|
|
19
|
+
function escapeHtmlPreservingEntities(text) {
|
|
20
|
+
return String(text || "")
|
|
21
|
+
.replace(/&(?!(?:amp|lt|gt|quot|#\d+|#x[0-9a-fA-F]+);)/g, "&")
|
|
22
|
+
.replace(/</g, "<")
|
|
23
|
+
.replace(/>/g, ">");
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
function stripMarkdown(text) {
|
|
18
27
|
return String(text || "")
|
|
19
28
|
.replace(/[*_`~]/g, "")
|
|
@@ -48,6 +57,13 @@ function stashAllowedHtmlTags(text) {
|
|
|
48
57
|
};
|
|
49
58
|
let out = text;
|
|
50
59
|
|
|
60
|
+
// Stash whole model-authored <code>/<pre> spans, content included, so
|
|
61
|
+
// Markdown conversion can never inject tags inside them (e.g. two
|
|
62
|
+
// snake_case identifiers on one line becoming a stray <i> pair).
|
|
63
|
+
out = out.replace(/<(code|pre)>([\s\S]*?)<\/\1>/gi, (_, tag, inner) => {
|
|
64
|
+
return stash(`<${tag.toLowerCase()}>${inner}</${tag.toLowerCase()}>`);
|
|
65
|
+
});
|
|
66
|
+
|
|
51
67
|
// Hide model-authored Telegram HTML tags from Markdown conversion. This keeps
|
|
52
68
|
// underscores in hrefs such as wan2_2_video from becoming bogus <i> tags.
|
|
53
69
|
for (const tag of ["b", "strong", "i", "em", "u", "s", "strike", "del", "code", "pre", "blockquote"]) {
|
|
@@ -83,8 +99,9 @@ function convertMarkdownToHtml(text) {
|
|
|
83
99
|
out = out.replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "<b>$1</b>");
|
|
84
100
|
out = out.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, "<b>$1</b>");
|
|
85
101
|
|
|
86
|
-
// Italic.
|
|
87
|
-
|
|
102
|
+
// Italic. Only fire when the underscores sit at word boundaries, so
|
|
103
|
+
// snake_case identifiers (skill_manage ... skill_manage) never pair up.
|
|
104
|
+
out = out.replace(/(?<![A-Za-z0-9_])_([^_\n]+)_(?![A-Za-z0-9_])/g, "<i>$1</i>");
|
|
88
105
|
|
|
89
106
|
// Strikethrough and Telegram spoiler syntax.
|
|
90
107
|
out = out.replace(/~~([^~\n]+)~~/g, "<s>$1</s>");
|
|
@@ -95,7 +112,7 @@ function convertMarkdownToHtml(text) {
|
|
|
95
112
|
|
|
96
113
|
function telegramHtml(text) {
|
|
97
114
|
const { text: withoutCode, blocks } = stashCode(text);
|
|
98
|
-
let out =
|
|
115
|
+
let out = escapeHtmlPreservingEntities(withoutCode);
|
|
99
116
|
const { text: withoutHtmlTags, tags } = stashAllowedHtmlTags(out);
|
|
100
117
|
out = convertMarkdownToHtml(withoutHtmlTags);
|
|
101
118
|
|
|
@@ -110,4 +127,16 @@ function telegramHtml(text) {
|
|
|
110
127
|
return out;
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
|
|
130
|
+
// Last-resort plain text for when Telegram rejects the HTML: drop tags,
|
|
131
|
+
// decode entities, so the user never sees raw <b>/<code> markup.
|
|
132
|
+
function htmlToPlain(html) {
|
|
133
|
+
return String(html || "")
|
|
134
|
+
.replace(/<\/?[a-zA-Z][^>]*>/g, "")
|
|
135
|
+
.replace(/</g, "<")
|
|
136
|
+
.replace(/>/g, ">")
|
|
137
|
+
.replace(/"/g, '"')
|
|
138
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
|
|
139
|
+
.replace(/&/g, "&");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { escapeHtml, stripMarkdown, telegramHtml, htmlToPlain };
|
package/core/runner.js
CHANGED
|
@@ -500,7 +500,10 @@ async function runClaudeCapture(prompt, cwd, opts = {}) {
|
|
|
500
500
|
if (typeof evt.total_cost_usd === "number") state.sessionUsage.costUsd += evt.total_cost_usd;
|
|
501
501
|
saveState();
|
|
502
502
|
}
|
|
503
|
-
|
|
503
|
+
// evt.result only carries the FINAL text segment of the turn. Using it
|
|
504
|
+
// as anything but a fallback would clobber text streamed before tool
|
|
505
|
+
// calls (the "long reply, then tools, then short closing line" shape).
|
|
506
|
+
if (evt.type === "result" && evt.result && !assistantText.trim()) assistantText = evt.result;
|
|
504
507
|
}
|
|
505
508
|
});
|
|
506
509
|
proc.stderr.on("data", (d) => { stderrBuffer += d.toString(); });
|
|
@@ -803,7 +806,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
803
806
|
if (typeof evt.total_cost_usd === "number") state.sessionUsage.costUsd += evt.total_cost_usd;
|
|
804
807
|
saveState();
|
|
805
808
|
}
|
|
806
|
-
|
|
809
|
+
// Fallback only: evt.result is just the final text segment, and assigning
|
|
810
|
+
// it unconditionally wiped everything the model said before tool calls.
|
|
811
|
+
if (evt.type === "result" && evt.result && !assistantText.trim()) assistantText = evt.result;
|
|
807
812
|
}
|
|
808
813
|
});
|
|
809
814
|
|
package/package.json
CHANGED