@inetafrica/open-claudia 3.1.14 → 3.1.16

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.16 — CI-deflake: no live network in unit tests, CI-safe enforcer deadlines
4
+
5
+ - Identical bot behaviour to v3.1.15 (which never shipped: its pipeline died twice on loaded-runner test flakes — the tag is retired unretagged per release hygiene).
6
+ - `test-telegram-conflict-heal-guard.js` stubbed `_probeTelegramReachable`/`_anyTurnActive`: the wedge-exit assertion used to probe the real api.telegram.org, so a slow/blocked CI egress sent `_heal` down the outage-pause path instead of the exit under test. It also now isolates `OPEN_CLAUDIA_CONFIG_DIR` — the exercised exit path was writing a bogus "polling wedged" `last-exit.json` into the real `~/.open-claudia` on every local test run.
7
+ - `test-provider-enforcer.js` deadlines made CI-safe: the judged-probe service backstop 2000→8000ms (children answer in ms; 2000 was missed by child boot under runner load), the deliberate-timeout probe 2500→5000ms boot headroom.
8
+
9
+ ## v3.1.15 — Voice-note replies speak a conversational summary, not the chat text
10
+
11
+ - Replying to a voice note used to TTS the full formatted chat answer verbatim — bullets, emoji, pipeline IDs and all ("this isn't human hearable"). On voice-reply turns the provider is now asked to end its reply with a dedicated spoken script in `<voice-script>` markers: the chat message keeps the full text answer (script stripped), and only the script — two to four conversational sentences written for the ear — is synthesized into the voice note.
12
+ - If the provider omits the script, a sanitizing fallback (`speechFallback`) strips code blocks, HTML, URLs, emoji, and list markers, caps the speech at the opening sentences, and closes with "The rest is in the text message." — a verbatim readout can no longer reach TTS on text channels.
13
+ - The Kazee call bridge keeps its existing in-call guidance and streams `voiceScript || finalText`, so stray markers are never spoken there either.
14
+ - New extraction/fallback/wiring assertions in `test-media-speech.js`.
15
+
3
16
  ## v3.1.14 — Call bridge production hardening: serialized speech, barge-in, group attribution, limits
4
17
 
5
18
  - **TTS is serialized through a queue with cancellation.** Concurrent `speak()` calls used to interleave two 20ms-frame RTP loops over the same sequence/timestamp counters — garbled audio whenever the provider emitted more than one message. Utterances now play strictly one at a time; `cancelSpeech()` (generation-based) stops the current utterance mid-frame and resolves everything queued, and `close()` cancels so no pending `speak()` can hang the adapter.
package/core/runner.js CHANGED
@@ -1647,6 +1647,18 @@ async function deliverForegroundResult(result, runContext, state = currentState(
1647
1647
  }
1648
1648
  }
1649
1649
 
1650
+ // The <voice-script> block (if the provider wrote one) is a spoken summary
1651
+ // for the voice note — strip it from the chat text; the guard above already
1652
+ // vetted the full output including the script.
1653
+ const { VOICE_REPLY_MODE } = require("./config");
1654
+ const { shouldReplyWithVoice, extractVoiceScript, speechFallback } = require("./voice-policy");
1655
+ let voiceScript = "";
1656
+ if (result.ok) {
1657
+ const parts = extractVoiceScript(finalText);
1658
+ voiceScript = parts.script;
1659
+ finalText = parts.text || parts.script || finalText;
1660
+ }
1661
+
1650
1662
  const chunks = splitMessage(finalText);
1651
1663
  for (let index = 0; index < chunks.length; index += 1) {
1652
1664
  let sent = await send(chunks[index], telegramHtmlOpts(index === 0 ? { replyTo: runContext.replyToMsgId } : {}));
@@ -1654,25 +1666,26 @@ async function deliverForegroundResult(result, runContext, state = currentState(
1654
1666
  if (!sent.ok) throw new Error(`failed to deliver response chunk ${index + 1}`);
1655
1667
  }
1656
1668
 
1657
- const { VOICE_REPLY_MODE } = require("./config");
1658
- const { shouldReplyWithVoice } = require("./voice-policy");
1659
1669
  const shouldSpeak = shouldReplyWithVoice(VOICE_REPLY_MODE, runContext.lastInputWasVoice);
1660
1670
  if (result.ok && shouldSpeak) {
1661
1671
  const { currentTransport } = require("./context");
1662
1672
  if (currentTransport() === "voice") {
1663
- const sentences = splitSentences(finalText);
1673
+ const sentences = splitSentences(voiceScript || finalText);
1664
1674
  let spoke = false;
1665
1675
  for (const sentence of sentences) {
1666
1676
  const clip = await synthSentenceMp3(sentence);
1667
1677
  if (clip) { spoke = true; await sendVoice(clip); }
1668
1678
  }
1669
1679
  if (!spoke) {
1670
- const voicePath = await textToVoice(finalText);
1680
+ const voicePath = await textToVoice(voiceScript || finalText);
1671
1681
  if (voicePath) await sendVoice(voicePath);
1672
1682
  }
1673
1683
  await sendVoiceEnd();
1674
1684
  } else {
1675
- const voicePath = await textToVoice(finalText);
1685
+ // Never TTS the formatted chat text verbatim: use the provider's spoken
1686
+ // script, else a sanitized short fallback.
1687
+ const spoken = voiceScript || speechFallback(finalText);
1688
+ const voicePath = spoken ? await textToVoice(spoken) : null;
1676
1689
  if (voicePath) await sendVoice(voicePath);
1677
1690
  }
1678
1691
  }
@@ -995,10 +995,29 @@ const VOICE_REPLY_GUIDANCE = [
995
995
  "- If the answer genuinely needs code or exact detail, say so briefly and offer to send it as text rather than reading it aloud.",
996
996
  ].join("\n");
997
997
 
998
+ // Text-channel voice turns (a Telegram/Kazee voice note): the chat reply stays
999
+ // a normal full text answer, but a voice note is ALSO sent back. Asking for a
1000
+ // dedicated spoken script — stripped from the text by the runner — is what
1001
+ // keeps that voice note conversational instead of a verbatim TTS of the
1002
+ // formatted chat text.
1003
+ const VOICE_NOTE_GUIDANCE = [
1004
+ "",
1005
+ "",
1006
+ "## Voice note reply",
1007
+ "The user sent this message as a voice note, and a spoken voice note will be sent back alongside your text reply. Do both:",
1008
+ "- Write your normal text reply as usual — full detail and formatting are fine; it is delivered as text.",
1009
+ "- End the reply with a spoken script between <voice-script> and </voice-script> markers. That block is stripped from the text message and only synthesized to audio.",
1010
+ "Script rules: two to four short sentences a person would naturally SAY — summarise the essence conversationally, do not re-read the text reply. No formatting, no emoji, no lists. Never speak code, IDs, file paths, URLs, or command syntax — describe them in plain words or say the details are in the text message.",
1011
+ ].join("\n");
1012
+
998
1013
  function voiceReplyGuidance(opts = {}) {
999
1014
  try {
1000
1015
  const { currentTransport } = require("./context");
1001
- if (currentTransport() === "voice" && promptState(opts).lastInputWasVoice) return VOICE_REPLY_GUIDANCE;
1016
+ const state = promptState(opts);
1017
+ if (currentTransport() === "voice") return state.lastInputWasVoice ? VOICE_REPLY_GUIDANCE : "";
1018
+ const { VOICE_REPLY_MODE } = require("./config");
1019
+ const { shouldReplyWithVoice } = require("./voice-policy");
1020
+ if (shouldReplyWithVoice(VOICE_REPLY_MODE, state.lastInputWasVoice)) return VOICE_NOTE_GUIDANCE;
1002
1021
  } catch (e) {}
1003
1022
  return "";
1004
1023
  }
@@ -4,4 +4,49 @@ function shouldReplyWithVoice(mode, lastInputWasVoice) {
4
4
  return mode === "always" || (mode === "on-voice-input" && !!lastInputWasVoice);
5
5
  }
6
6
 
7
- module.exports = { shouldReplyWithVoice };
7
+ // On voice-reply turns the provider is asked (system-prompt voice-note
8
+ // guidance) to end its reply with a spoken script between these markers. The
9
+ // block is stripped from the chat text and ONLY synthesized — chat keeps the
10
+ // full formatted answer while the voice note carries an ear-friendly summary
11
+ // instead of a verbatim TTS readout of bullets, emoji, and IDs.
12
+ const VOICE_SCRIPT_RE = /<voice-script>([\s\S]*?)(?:<\/voice-script>|$)/gi;
13
+
14
+ function extractVoiceScript(text) {
15
+ const raw = String(text || "");
16
+ const scripts = [];
17
+ const stripped = raw.replace(VOICE_SCRIPT_RE, (_match, body) => {
18
+ const clean = String(body || "").trim();
19
+ if (clean) scripts.push(clean);
20
+ return "";
21
+ });
22
+ return { text: stripped.trim(), script: scripts.join(" ").trim() };
23
+ }
24
+
25
+ // Last-resort shaping when the provider supplied no <voice-script>: strip
26
+ // everything that reads badly aloud and cap to the opening sentences, so a
27
+ // missed script degrades to a short spoken intro rather than a verbatim dump
28
+ // of formatted text into TTS.
29
+ const FALLBACK_MAX_CHARS = 400;
30
+
31
+ function speechFallback(text) {
32
+ const plain = String(text || "")
33
+ .replace(/```[\s\S]*?```/g, " ")
34
+ .replace(/<[^>\n]{0,120}>/g, " ")
35
+ .replace(/https?:\/\/\S+/gi, " ")
36
+ .replace(/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE0F}\u{200D}]/gu, "")
37
+ .replace(/^\s*[-*•▸►]+\s+/gm, "")
38
+ .replace(/[*_`#|]+/g, " ")
39
+ .replace(/\s+/g, " ")
40
+ .trim();
41
+ if (plain.length <= FALLBACK_MAX_CHARS) return plain;
42
+ const sentences = plain.match(/[^.!?]+[.!?]+(?:\s+|$)/g) || [];
43
+ let out = "";
44
+ for (const sentence of sentences) {
45
+ if (out && out.length + sentence.length > FALLBACK_MAX_CHARS) break;
46
+ out += sentence;
47
+ }
48
+ out = out.trim() || plain.slice(0, FALLBACK_MAX_CHARS).trim();
49
+ return `${out} The rest is in the text message.`;
50
+ }
51
+
52
+ module.exports = { shouldReplyWithVoice, extractVoiceScript, speechFallback };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.1.14",
3
+ "version": "3.1.16",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -95,7 +95,10 @@ async function providerProbe(kind) {
95
95
  const service = enforcer.createEnforcerService({
96
96
  spawnUtilityAgent: utility.spawnUtilityAgent,
97
97
  audit,
98
- timeoutMs: 2000,
98
+ // Generous: children answer in ms, so the green path never waits this out.
99
+ // The deadline exists only as the fail-closed backstop, and 2000ms was
100
+ // missed by child boot on a loaded CI runner (pipeline 10445 flake).
101
+ timeoutMs: 8000,
99
102
  });
100
103
  const speaker = { name: "External Fixture", relationship: "external", mandate: "May discuss invoice status only." };
101
104
  const secret = ["sk", "proj", "guard", "fixture", "private"].join("-");
@@ -106,9 +109,10 @@ async function providerProbe(kind) {
106
109
  const timeoutService = enforcer.createEnforcerService({
107
110
  spawnUtilityAgent: utility.spawnUtilityAgent,
108
111
  audit,
109
- // 2500ms so the hung child boots on a loaded CI runner (writing capture #5
110
- // and arming its SIGTERM recorder) before the deadline fires.
111
- timeoutMs: 2500,
112
+ // Bounded (this deadline MUST fire — the child hangs by design), but with
113
+ // enough headroom for the hung child to boot on a loaded CI runner (write
114
+ // capture #5, arm its SIGTERM recorder) before the deadline kills it.
115
+ timeoutMs: 5000,
112
116
  });
113
117
  const timedOut = await timeoutService.vetReply(speaker, "timeout verdict fixture");
114
118
 
@@ -212,7 +212,17 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
212
212
  // production hardening — serialized TTS with barge-in cancellation, echo gate,
213
213
  // per-feed STT with group speaker attribution, duration/idle limits with a
214
214
  // spoken goodbye, and the call tests wired into the main npm test chain.
215
- assert.strictEqual(pkg.version, "3.1.14", "release version must remain unchanged without explicit approval");
215
+ // 3.1.15 approved by Sumeet 2026-07-22 ("if replying in voice it needs to be
216
+ // summaries etc but not just conversions directly"): voice-note replies now
217
+ // speak a provider-written conversational <voice-script> summary (stripped from
218
+ // the chat text) instead of a verbatim TTS of the formatted answer, with a
219
+ // sanitizing speechFallback when the script is missing.
220
+ // 3.1.16 approved by Sumeet 2026-07-22 (same calls-directive authority; re-cut
221
+ // of the never-published 3.1.15 after two loaded-runner CI flakes): test-only
222
+ // deflake — heal-guard test stubs its live api.telegram.org probe and isolates
223
+ // CONFIG_DIR; enforcer probe deadlines raised to CI-safe values (2000→8000ms
224
+ // backstop, 2500→5000ms deliberate-timeout boot headroom). No runtime changes.
225
+ assert.strictEqual(pkg.version, "3.1.16", "release version must remain unchanged without explicit approval");
216
226
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
217
227
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
218
228
  }