@inetafrica/open-claudia 2.9.1 → 2.9.3
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 +4 -0
- package/channels/kazee/adapter.js +56 -13
- package/channels/telegram/adapter.js +3 -1
- package/channels/types.js +2 -1
- package/core/io.js +2 -2
- package/core/runner.js +34 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.9.3
|
|
4
|
+
- **Bot advertises a distinct "thinking" state before its first token (Kazee parity Phase 2).** The typing pipeline now carries an additive `state` discriminator (`'thinking' | 'typing'`) — no new socket event. The runner advertises `thinking` from the moment a turn starts until the first stream event (text delta, assistant content, tool call, or result), then flips to plain `typing`; Kazee clients render "<name> is thinking…" in the header/chat-list for that pre-token window and fall back to normal typing text otherwise. The Telegram adapter ignores `state` (its chat action has no such distinction), so the change is inert there. Pairs with the chat-central `message:interactiveUpdated` broadcast (server-authoritative button selection) landing in the same release across chat-central/web/mobile.
|
|
5
|
+
- **Ships the v2.9.2 work that never published.** v2.9.2 (Kazee REST media fallback when a socket send lacks a media path, plus an explicit `typing:stop` on turn end) was committed to main but no tag was ever pushed, so CI never published it. This tag carries it to npm alongside the thinking-state change.
|
|
6
|
+
|
|
3
7
|
## v2.9.1
|
|
4
8
|
- **Post-turn reviewer no longer rewrites pack sections blind — context starvation fixed.** The reviewer prompt showed only the pack *index* (name + description), so a State rewrite was regenerated from the turn text alone and silently destroyed durable facts the turn didn't restate; worse, with no project name in the turn text it could attribute the work to the wrong pack entirely (observed live: an AQUELLE product-shot turn filed under Alpha Protein, wiping a promoted root-cause fact). The runner now passes the turn's **active packs** (recall-surfaced ∪ explicitly opened) into `reviewTurn`; the prompt includes their **full current Stance/Procedure/State + recent journal**, with two new rules: attribute updates to an active pack unless the turn clearly names another, and treat section rewrites as **merges** — carry forward every still-true fact, drop only what the turn contradicted or completed.
|
|
5
9
|
- **Structural enforcement, not just prompt guidance.** `applyAction` drops section rewrites (State/Stance/Procedure) targeting any pack whose current content the reviewer did not see — journal lines still land; the announce line notes the drop. Applies to the update path, the exists-on-create path, and the versioned-duplicate fold.
|
|
@@ -256,6 +256,9 @@ class KazeeAdapter {
|
|
|
256
256
|
body.type = "interactive";
|
|
257
257
|
body.interactive = { buttons };
|
|
258
258
|
}
|
|
259
|
+
// Progressive delivery of the bot's own answer — tell the server this is a
|
|
260
|
+
// streaming edit so it skips editHistory + the "edited" marker.
|
|
261
|
+
if (opts.streaming) body.streaming = true;
|
|
259
262
|
try {
|
|
260
263
|
await this._request("PUT", `/message/${encodeURIComponent(messageId)}`, body);
|
|
261
264
|
} catch (e) {
|
|
@@ -299,30 +302,56 @@ class KazeeAdapter {
|
|
|
299
302
|
console.error("Kazee upload returned no media id:", JSON.stringify(upload).slice(0, 300));
|
|
300
303
|
return false;
|
|
301
304
|
}
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
305
|
+
const type = this._kazeeFileType(fileName);
|
|
306
|
+
// Prefer the v2 socket event `message:send` (real-time fan-out), which
|
|
307
|
+
// accepts `mediaIds` referencing already-saved Media docs. If the socket
|
|
308
|
+
// is down or the ack errors, fall back to the REST sendMessage action —
|
|
309
|
+
// it now accepts already-uploaded media via `media_ids` (previously it
|
|
310
|
+
// only took media_url and re-Media.create'd, which always 500'd).
|
|
307
311
|
const payload = {
|
|
308
312
|
chatId: channelId,
|
|
309
313
|
content: caption || "",
|
|
310
|
-
type
|
|
314
|
+
type,
|
|
311
315
|
findMeCode: `oc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
312
316
|
mediaIds: [mediaId],
|
|
313
317
|
};
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
+
try {
|
|
319
|
+
const res = await this._socketEmit("message:send", payload, 30000);
|
|
320
|
+
if (res && res.code) {
|
|
321
|
+
console.error(`Kazee message:send error (${res.code}), falling back to REST:`, res.message);
|
|
322
|
+
return await this._sendMediaViaRest(channelId, mediaId, type, caption);
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
} catch (sockErr) {
|
|
326
|
+
console.error("Kazee message:send unavailable, falling back to REST:", sockErr.message);
|
|
327
|
+
return await this._sendMediaViaRest(channelId, mediaId, type, caption);
|
|
318
328
|
}
|
|
319
|
-
return true;
|
|
320
329
|
} catch (e) {
|
|
321
330
|
console.error("Kazee sendFile error:", e.message);
|
|
322
331
|
return false;
|
|
323
332
|
}
|
|
324
333
|
}
|
|
325
334
|
|
|
335
|
+
// REST fallback for posting an already-uploaded Media doc when the socket
|
|
336
|
+
// path is unavailable. Mirrors send()'s route (POST /chat/:id/message →
|
|
337
|
+
// sendMessage action) but references the saved media by id.
|
|
338
|
+
async _sendMediaViaRest(channelId, mediaId, type, caption) {
|
|
339
|
+
try {
|
|
340
|
+
await this._request("POST", `/chat/${encodeURIComponent(channelId)}/message`, {
|
|
341
|
+
chat_id: channelId,
|
|
342
|
+
sender: this.botUserId,
|
|
343
|
+
content: caption || "",
|
|
344
|
+
type,
|
|
345
|
+
media_ids: [mediaId],
|
|
346
|
+
findMeCode: `oc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
347
|
+
});
|
|
348
|
+
return true;
|
|
349
|
+
} catch (e) {
|
|
350
|
+
console.error("Kazee REST media send failed:", e.message);
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
326
355
|
// Promisified ack-style socket emit. chat-central v2 handlers respond
|
|
327
356
|
// either with a success body or an { code, message, action } error.
|
|
328
357
|
_socketEmit(event, payload, timeoutMs = 15000) {
|
|
@@ -355,8 +384,22 @@ class KazeeAdapter {
|
|
|
355
384
|
return "file";
|
|
356
385
|
}
|
|
357
386
|
|
|
358
|
-
|
|
359
|
-
|
|
387
|
+
// `state` is an optional richer indicator: "thinking" (the bot is reasoning
|
|
388
|
+
// pre-first-token) vs the default "typing". Kazee clients render "thinking"
|
|
389
|
+
// distinctly; older clients ignore the field and fall back to "typing".
|
|
390
|
+
async typing(channelId, state) {
|
|
391
|
+
try {
|
|
392
|
+
const payload = { chatId: channelId };
|
|
393
|
+
if (state) payload.state = state;
|
|
394
|
+
this._socket?.emit?.("typing:start", payload);
|
|
395
|
+
} catch (e) {}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Socket channels keep rendering "typing…" until an explicit stop. Telegram
|
|
399
|
+
// has no equivalent (its chat action self-expires), so this is Kazee-only and
|
|
400
|
+
// called via optional chaining from the core typing heartbeat cleanup.
|
|
401
|
+
async typingStop(channelId) {
|
|
402
|
+
try { this._socket?.emit?.("typing:stop", { chatId: channelId }); } catch (e) {}
|
|
360
403
|
}
|
|
361
404
|
|
|
362
405
|
async downloadMedia(media) {
|
|
@@ -351,7 +351,9 @@ class TelegramAdapter {
|
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
|
|
354
|
+
// Telegram exposes only a single "typing" chat action, so the optional
|
|
355
|
+
// `state` (thinking vs typing) is accepted for signature parity but ignored.
|
|
356
|
+
async typing(channelId, _state) {
|
|
355
357
|
try { await this.bot.sendChatAction(channelId, "typing"); } catch (e) {}
|
|
356
358
|
}
|
|
357
359
|
|
package/channels/types.js
CHANGED
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
* delete(channelId, messageId): Promise<void>
|
|
56
56
|
* sendVoice(channelId, oggPath): Promise<boolean>
|
|
57
57
|
* sendFile(channelId, filePath, caption?): Promise<boolean>
|
|
58
|
-
* typing(channelId): Promise<void>
|
|
58
|
+
* typing(channelId, state?): Promise<void> // state: "thinking" | "typing" (default). Non-socket channels ignore state.
|
|
59
|
+
* typingStop?(channelId): Promise<void> // optional — socket channels only
|
|
59
60
|
* downloadMedia(mediaRef): Promise<localPath|null>
|
|
60
61
|
* registerCommands(commands: CommandSpec[]): Promise<void>
|
|
61
62
|
*/
|
package/core/io.js
CHANGED
|
@@ -52,11 +52,11 @@ async function sendFile(filePath, caption) {
|
|
|
52
52
|
return adapter.sendFile(channelId, filePath, caption);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
async function typing() {
|
|
55
|
+
async function typing(state) {
|
|
56
56
|
const adapter = currentAdapter();
|
|
57
57
|
const channelId = currentChannelId();
|
|
58
58
|
if (!adapter || !channelId) return;
|
|
59
|
-
return adapter.typing(channelId);
|
|
59
|
+
return adapter.typing(channelId, state);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function splitMessage(text, maxLen = 4000) {
|
package/core/runner.js
CHANGED
|
@@ -812,12 +812,30 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
812
812
|
// receipt through the recall/discoverer phase and into streaming, not just
|
|
813
813
|
// for the ~5s a single typing action lasts. Stored on state so /stop can
|
|
814
814
|
// clear it instantly. Cleared on close/error/cancel.
|
|
815
|
+
// While true, the bot is reasoning before its first output token, so the
|
|
816
|
+
// typing heartbeat advertises the richer "thinking" state. Flipped to false
|
|
817
|
+
// the moment the first assistant text / tool_use / text_delta arrives.
|
|
818
|
+
state.thinkingPhase = true;
|
|
815
819
|
const startTyping = () => {
|
|
816
820
|
if (!adapter || !channelId) return;
|
|
817
|
-
adapter.typing(channelId).catch(() => {});
|
|
818
|
-
if (!state.typingHeartbeat) state.typingHeartbeat = setInterval(() => adapter.typing(channelId).catch(() => {}), 4000);
|
|
821
|
+
adapter.typing(channelId, state.thinkingPhase ? "thinking" : undefined).catch(() => {});
|
|
822
|
+
if (!state.typingHeartbeat) state.typingHeartbeat = setInterval(() => adapter.typing(channelId, state.thinkingPhase ? "thinking" : undefined).catch(() => {}), 4000);
|
|
823
|
+
};
|
|
824
|
+
// Called on the first real output token to leave the thinking phase and, if
|
|
825
|
+
// the turn is still live, immediately advertise plain "typing" so the client
|
|
826
|
+
// flips without waiting for the next 4s heartbeat.
|
|
827
|
+
const endThinking = () => {
|
|
828
|
+
if (!state.thinkingPhase) return;
|
|
829
|
+
state.thinkingPhase = false;
|
|
830
|
+
if (adapter && channelId && state.typingHeartbeat) adapter.typing(channelId).catch(() => {});
|
|
831
|
+
};
|
|
832
|
+
const stopTyping = () => {
|
|
833
|
+
state.thinkingPhase = false;
|
|
834
|
+
if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; }
|
|
835
|
+
// Telegram's chat action auto-expires, but socket-based channels (Kazee)
|
|
836
|
+
// keep showing "typing…" until told to stop. Optional on the adapter.
|
|
837
|
+
if (adapter && channelId) { try { adapter.typingStop?.(channelId); } catch (e) {} }
|
|
819
838
|
};
|
|
820
|
-
const stopTyping = () => { if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; } };
|
|
821
839
|
// Pre-spawn window (recall/compaction/auth): /stop has no process to kill, so
|
|
822
840
|
// it sets state.cancelRequested and we bail at the checkpoint before spawning.
|
|
823
841
|
state.preparingRun = true;
|
|
@@ -1173,7 +1191,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1173
1191
|
if (!state.statusMessageId && assistantText) {
|
|
1174
1192
|
state.statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, telegramHtmlOpts({ replyTo: replyToMsgId }));
|
|
1175
1193
|
} else if (state.statusMessageId) {
|
|
1176
|
-
await editMessage(state.statusMessageId, display.length > 4000 ? display.slice(-4000) : display, telegramHtmlOpts());
|
|
1194
|
+
await editMessage(state.statusMessageId, display.length > 4000 ? display.slice(-4000) : display, telegramHtmlOpts({ streaming: true }));
|
|
1177
1195
|
}
|
|
1178
1196
|
lastUpdate = display;
|
|
1179
1197
|
}
|
|
@@ -1195,6 +1213,17 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1195
1213
|
const lastNewline = state.streamBuffer.lastIndexOf("\n");
|
|
1196
1214
|
state.streamBuffer = lastNewline >= 0 ? state.streamBuffer.slice(lastNewline + 1) : state.streamBuffer;
|
|
1197
1215
|
for (const evt of events) {
|
|
1216
|
+
// Leave the "thinking" phase the moment the model produces any real
|
|
1217
|
+
// output — streamed text, a finished assistant block, a tool call, or a
|
|
1218
|
+
// terminal result. Idempotent (guards on state.thinkingPhase internally).
|
|
1219
|
+
if (state.thinkingPhase && (
|
|
1220
|
+
(evt.type === "stream_event" && evt.event?.type === "content_block_delta" && evt.event.delta?.type === "text_delta") ||
|
|
1221
|
+
(evt.type === "assistant" && evt.message?.content) ||
|
|
1222
|
+
(evt.type === "tool_call" && evt.subtype === "started") ||
|
|
1223
|
+
evt.type === "result"
|
|
1224
|
+
)) {
|
|
1225
|
+
endThinking();
|
|
1226
|
+
}
|
|
1198
1227
|
// Voice latency probe: first "system" event = CLI ready (cold-start done);
|
|
1199
1228
|
// "result" = generation finished (before the TTS tail drains).
|
|
1200
1229
|
if (voiceStreaming && vlFirstSysAt == null && evt.type === "system") vlFirstSysAt = Date.now();
|
|
@@ -1399,7 +1428,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1399
1428
|
const firstChunk = chunks[0];
|
|
1400
1429
|
|
|
1401
1430
|
if (state.statusMessageId && chunks.length === 1) {
|
|
1402
|
-
await editMessage(state.statusMessageId, firstChunk, telegramHtmlOpts());
|
|
1431
|
+
await editMessage(state.statusMessageId, firstChunk, telegramHtmlOpts({ streaming: true }));
|
|
1403
1432
|
} else {
|
|
1404
1433
|
const sent = await send(firstChunk, telegramHtmlOpts({ replyTo: replyToMsgId }));
|
|
1405
1434
|
if (!sent) await send(firstChunk);
|
package/package.json
CHANGED