@inetafrica/open-claudia 3.5.62 → 3.5.64

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.64
4
+
5
+ 3.5.63 plus three fixes found in a review of every unreleased branch (Sumeet, 15 Oct 08:29 EAT: "see if there's anything in back branches ... get ready for a release of everything that's currently unreleased"; 09:17 EAT "yes to all").
6
+
7
+ - **A leftover start-up lock no longer keeps a bot down** (`core/migration-backup.js`). In a container the bot is process 1 after every restart, so a lock file left by a run that died holding it (it says "1") looked alive to the liveness check forever, and the bot exited at start with MIGRATION_LOCKED on every restart. network-bot crash-looped 100+ times on 10 Oct and kazee-tickets-bot 192 times from 14 Oct 15:30 EAT; both were cleared by hand. A lock naming our own process is now treated as stale; a live other holder still blocks. Written on 10 Oct but never pushed until now.
8
+ - **A post chat-central answered without saving is not treated as posted** (`channels/kazee/adapter.js`, from the 5 Oct calls branch).
9
+ - **Calls: the job rules from the 5 Oct test calls** (`core/system-prompt.js`, `channels/kazee/call/quick-voice.js`), lost when calls were rebuilt on the quick voice: Cazy, Kazy, Cozy or Kazi mean Kazee; look in our tools, memory and on the web before saying there is no record or a system cannot be reached; work out dates against today and check states before saying them; a write only after the caller's yes to that exact action.
10
+ - **Changelog notes that never reached this line:** 3.5.33 (quiet chats by default: no "Heard:" echo, progress message or notes lines unless /heard, /progress or /notes turn them on) and 3.5.47 (the guard's floor is scoped, so a group's own rules can allow read-only diagnostics; credentials stay blocked). Both changes are already in the code.
11
+
12
+ Tests: `test-provider-migration-backup.js` (a lock naming our own pid is reclaimed; fails without the fix), `test-kazee-message-id.js`, `test-call-quick-voice.js` (the call rules on every call path and in the quick voice). Known: `test-cokept-warming.js` fails on the Mac's Node 26 on this release and on 3.5.59 alike; the cloud build passes it. Not looked into yet.
13
+
14
+ ## 3.5.63
15
+
16
+ 3.5.62 plus a passive-group fix (APP-29; Sumeet, 14 Oct: "my chat with Judith Brenda and books bot ... doesn't seem to be auto waking yet it's definitely relevant"). Canary first.
17
+
18
+ - **A captioned screenshot or file is judged in a "reply when they can help" group.** The group gate only handed plain text to the passive check, so every message with an attachment was skipped, captioned or not. In Kazee Books update, Judith's three screenshots ("Add 'Transaction' under banking", the exchange rate, "Editing of Journal Entry missing") each logged `Kazee group gate: silent` and the books bot only answered when tagged. A photo or file with a caption now goes to the check, shown as `[photo] <caption>`, and if the bot replies the attachment rides along to the turn. Voice notes, words-less media, commands, cards and other bots' messages are still skipped.
19
+
20
+ Tests: `test-passive-replies.js` (captioned photo and file judged; a words-less voice note still skipped; the check's prompt reads `[photo] <caption>`).
21
+
3
22
  ## 3.5.62
4
23
 
5
24
  3.5.61 plus spend split by part (APP-28; Sumeet, 14 Oct: "how much is used for memory? How much is used for work? How much is used for the dream?"). Canary first.
@@ -47,6 +47,9 @@ const CATCHUP_FETCH_TIMEOUT_MS = 15000;
47
47
  const CONTEXT_MAX_MESSAGES = 40;
48
48
  const CONTEXT_BODY_LIMIT = 280;
49
49
  const COLD_START_WINDOW = 40;
50
+ // What a passive group's check reads (router types): text, and photos or
51
+ // files when they carry a caption. Voice notes have no words until heard.
52
+ const PASSIVE_JUDGED_TYPES = new Set(["text", "photo", "document"]);
50
53
 
51
54
  // Chat threads ("reply in thread"). Button presses carry no thread, so the
52
55
  // adapter remembers which thread each message it posted went into (and that
@@ -81,6 +84,17 @@ const INVALID_THREAD_RE = /INVALID_THREAD|Invalid thread|Thread not found in thi
81
84
  // chat-central drops a typing threadRootId that is not a Mongo ObjectId.
82
85
  const OBJECT_ID_RE = /^[0-9a-f]{24}$/i;
83
86
 
87
+ // chat-central's answer to a post it did NOT save: handleChatMessage
88
+ // (src/handlers/messageHandler.ts) catches the failed save, logs "Returning
89
+ // payload without save on receive" and returns our own payload, stamped
90
+ // `receivedAt`, which send_message answers 201 { success: true, message }.
91
+ // That payload has no `_id` and carries the text as `message`, not `content`.
92
+ function unsavedPayload(res) {
93
+ const m = res && res.message && typeof res.message === "object" ? res.message : null;
94
+ if (!m || m._id || m.id) return false;
95
+ return m.receivedAt != null || (typeof m.message === "string" && !("content" in m));
96
+ }
97
+
84
98
  // The thread a message belongs to: its root's id, or "" for the main timeline.
85
99
  // The field is an id today; accept a populated root doc too.
86
100
  function threadRootOf(msg) {
@@ -1091,10 +1105,13 @@ class KazeeAdapter {
1091
1105
  }
1092
1106
 
1093
1107
  // Whether an unaddressed message in a passive group is the bot's to judge:
1094
- // plain text a person wrote. Never another bot (no bot-to-bot chatter),
1095
- // never a command, system notice, card or media without words (v1).
1108
+ // words a person wrote, as plain text or as the caption on a photo or file
1109
+ // (14 Oct: Judith's Books screenshots captioned "Editing of Journal Entry
1110
+ // missing" never reached the check). Never another bot (no bot-to-bot
1111
+ // chatter), never a command, system notice, card, voice note or media
1112
+ // without words.
1096
1113
  _passiveCandidate(msg, envelope, meta, senderId) {
1097
- if (envelope.type !== "text" || !String(envelope.text || "").trim()) return false;
1114
+ if (!PASSIVE_JUDGED_TYPES.has(envelope.type) || !String(envelope.text || "").trim()) return false;
1098
1115
  if (msg.type === "system" || msg.type === "interactive" || msg.card) return false;
1099
1116
  if ((meta.botIds || []).includes(String(senderId))) return false;
1100
1117
  return true;
@@ -1474,8 +1491,24 @@ class KazeeAdapter {
1474
1491
  this._rememberSentSafely(id, body.threadRootId || null, channelId);
1475
1492
  if (id && botTags.length) this._rememberTagsSafely(id, botTags);
1476
1493
  if (id) return String(id);
1477
- // POST returned 2xx but carried no message id (server fast-ack / payload
1478
- // fallback path). Returning null makes the caller treat a delivered
1494
+ if (unsavedPayload(res)) {
1495
+ // Nothing was posted (see unsavedPayload). A replyTo that is not a
1496
+ // Mongo ObjectId fails the Message schema's cast inside createMessage,
1497
+ // before the insert, so nothing was saved and one retry without the
1498
+ // quote cannot post twice. (handleChatMessage's catch also covers
1499
+ // steps after the insert, so with any other cause this answer does
1500
+ // not prove nothing was saved: no retry then.)
1501
+ if (body.replyTo && !OBJECT_ID_RE.test(String(body.replyTo))) {
1502
+ console.error(`Kazee send: not saved with replyTo ${String(body.replyTo).slice(0, 80)}; retrying once without it`);
1503
+ delete body.replyTo;
1504
+ attempt--;
1505
+ continue;
1506
+ }
1507
+ console.error("Kazee send: NOT posted: the server returned the message unsaved (no id)");
1508
+ return null;
1509
+ }
1510
+ // POST returned 2xx but carried no message id and is not the unsaved
1511
+ // payload above (a server fast-ack). Returning null makes the caller treat a delivered
1479
1512
  // reply as a failed send and resend it — the Kazee double-send. Signal
1480
1513
  // success with a truthy, non-editable sentinel so nothing sends twice.
1481
1514
  console.error("Kazee send: POST ok but no message id in response keys:", Object.keys(res || {}).join(","));
@@ -86,6 +86,7 @@ function buildQuickPrompt({ ownerName = "", relay = false } = {}) {
86
86
  : "You keep the conversation going. Claudia herself (the full assistant, with tools) does the real work in the background, and her answers are spoken when they are ready.",
87
87
  "",
88
88
  "A message about what the caller just said starts with [Caller], then [Work]: what Claudia is doing for them right now, then [Now]: their local day and time. Start your reply with ONE tag, then the spoken line on the same line.",
89
+ "The caller's words come from speech recognition, which mishears names: Cazy, Kazy, Cozy or Kazi mean Kazee (the platform and its apps, like Kazee Chat). Always say Kazee.",
89
90
  "",
90
91
  "You answer yourself whatever you can answer from what you are given: the time or day from [Now], what Claudia is doing or already answered on this call from [Work], what you two were working on from the chat you were given at call start, and everyday general knowledge that does not depend on their business, systems, people, data or anything recent (a spelling, simple sums, a well-known fact). Never make them wait for Claudia for those. Anything about their business, systems, people, data, money, or anything recent or that you are not sure of, is Claudia's.",
91
92
  "",
@@ -259,8 +259,14 @@ function acquireLock(root) {
259
259
  const stat = assertRegularFile(filePath, "MIGRATION_LOCKED", "migration lock");
260
260
  const pid = Number(fs.readFileSync(filePath, "utf8").trim());
261
261
  if (Number.isSafeInteger(pid) && pid > 0 && Date.now() - stat.mtimeMs > 1000) {
262
- try { process.kill(pid, 0); }
263
- catch (probeError) { stale = probeError.code === "ESRCH"; }
262
+ // We haven't taken the lock yet, so one naming our own pid was left by an
263
+ // earlier run that died holding it. In a container the bot is pid 1 after
264
+ // every restart, so the liveness probe below would find "us" alive forever.
265
+ if (pid === process.pid) stale = true;
266
+ else {
267
+ try { process.kill(pid, 0); }
268
+ catch (probeError) { stale = probeError.code === "ESRCH"; }
269
+ }
264
270
  }
265
271
  } catch (_) {}
266
272
  if (stale) {
@@ -196,7 +196,9 @@ function createPassiveGate(dependencies = {}) {
196
196
  for (const item of batch) {
197
197
  const id = String(item.envelope.messageId || "");
198
198
  if (id && !seen.has(id)) {
199
- list.push({ id, who: item.envelope.from?.name || "Someone", text: item.envelope.text || "", ago: "just now" });
199
+ // A captioned photo or file reads the way the room's history shows it.
200
+ const kind = item.envelope.media?.length ? `[${item.envelope.type}] ` : "";
201
+ list.push({ id, who: item.envelope.from?.name || "Someone", text: kind + (item.envelope.text || ""), ago: "just now" });
200
202
  seen.add(id);
201
203
  }
202
204
  }
@@ -1260,6 +1260,12 @@ const LIVE_CALL_GUIDANCE = [
1260
1260
  "- Keep the work moving (the caller, 12 Oct): answer what they asked first, directly. Then, when there is an obvious next step, end with it in a few words: an offer (\"Want me to fix it now?\"), a suggestion, or \"Does that answer it?\". Leave it out for a quick fact, small talk, or when they are wrapping up, and never change the subject just to have one.",
1261
1261
  "- If your reply answers more than one of their questions, open each part with a few words naming its topic, like \"On the mail backup, ...\" then \"On your voice question, ...\", so they can tell which answer is which.",
1262
1262
  "- Do NOT read out code, file paths, command syntax, IDs, or URLs. Describe them in plain words, or offer to send the details to the chat as text.",
1263
+ // Job rules from the 5 Oct test calls (calls v2: d5e65cf, 2213352, f7db2e7,
1264
+ // d82a8c7, 0b20579), brought over when the call was rebuilt on the quick voice.
1265
+ "- The caller's words come from speech recognition, which mishears names: Cazy, Kazy, Cozy or Kazi mean Kazee (the platform and its apps, like Kazee Chat). Always say and write Kazee.",
1266
+ "- Look before you say you do not know something, have no record of it, or cannot reach a system: our own systems (tasks, CRM, Books, People, tickets, calendars, the PBX) through our tools (open-claudia tool search, then tool show and tool run); memory (entity show, pack show, transcript-search --all); the outside world (events, news, weather, places, businesses, prices, opening hours, public facts) on the web with WebSearch or WebFetch. If you still cannot, say what you tried and offer the closest thing you can do instead.",
1267
+ "- Work out \"tonight\", \"tomorrow\" or \"on Saturday\" from the record's own date against today's date in the runtime state; if unsure, say the date. Before you say something is built, live, done, fixed or broken, check it with a tool; if you only have notes, say \"my notes say\".",
1268
+ "- Writes (sending, changing, booking, creating, deleting): do one only after the caller has said yes to that exact action on this call; until then do the read-only part and ask. Destructive actions always go to the chat's approval buttons, never a spoken yes.",
1263
1269
  "- Speak first, then work. Unless your whole reply is one short sentence you can say right now, your FIRST action is a call-say of a few words (about 3 to 12) that shows you heard them and says what you will do, for example \"Sure, let me check the call logs.\", \"Good question, give me a second.\" or \"Yep, on it.\" Do it before any lookup and before working out the answer: think after it, not before.",
1264
1270
  "- If the work runs on, say a brief progress line with call-say about every ten seconds (\"Still looking, nearly there.\"). Tool time is dead air the caller sits through, so keep lookups short and offer longer digging after the call.",
1265
1271
  "- When a useful, complete answer sentence is ready before your full response, speak it with call-say too. This command delivers checked speech into this call; ordinary provider previews are not spoken. Acknowledgements and what-you-will-do-next lines are fine; never state a result or claim a completed action before tool evidence.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.5.62",
3
+ "version": "3.5.64",
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": {
@@ -89,6 +89,44 @@ async function sendFailing() {
89
89
  // success-without-id path was double-sending.
90
90
  assert.strictEqual(await sendFailing(), null, "real failure → null");
91
91
 
92
+ // chat-central's unsaved answer (P3e, 5 Oct call): handleChatMessage could
93
+ // not save the message (a call turn's synthetic replyTo failed the ObjectId
94
+ // cast) and send_message answered 201 with our own payload, no _id. That is
95
+ // NOT a post.
96
+ const unsaved = (extra = {}) => ({ success: true, message: { message: "hello", type: "text", findMeCode: "oc-1", receivedAt: "2026-10-05T04:28:29.184Z", ...extra } });
97
+ const scripted = (...answers) => {
98
+ const a = makeAdapter();
99
+ a.posts = [];
100
+ a._request = async (_method, _route, body) => { a.posts.push({ ...body }); return answers.shift(); };
101
+ return a;
102
+ };
103
+ // (1) With a replyTo that is not a Kazee message id: one retry without it.
104
+ let a = scripted(unsaved({ replyTo: "call-101221138374860-1791174486793-t5" }), { success: true, message: { _id: OID } });
105
+ assert.strictEqual(await a.send("room1", "hello", { replyTo: "call-101221138374860-1791174486793-t5" }), OID, "the retry's id");
106
+ assert.strictEqual(a.posts.length, 2, "one retry");
107
+ assert.strictEqual(a.posts[0].replyTo, "call-101221138374860-1791174486793-t5");
108
+ assert.ok(!("replyTo" in a.posts[1]), "the retry carries no replyTo");
109
+ // A retry that is unsaved too: not posted, and no third POST.
110
+ a = scripted(unsaved(), unsaved());
111
+ assert.strictEqual(await a.send("room1", "hello", { replyTo: "call-1-2-t3" }), null, "still unsaved → null");
112
+ assert.strictEqual(a.posts.length, 2, "at most one retry");
113
+ // (2) Unsaved without a replyTo: null, no second POST. The payload shape
114
+ // without receivedAt (`message` text, no `content`) counts too.
115
+ a = scripted(unsaved());
116
+ assert.strictEqual(await a.send("room1", "hello", {}), null, "unsaved → not posted");
117
+ assert.strictEqual(a.posts.length, 1, "no retry without a replyTo");
118
+ a = scripted({ success: true, message: { message: "hello", type: "text" } });
119
+ assert.strictEqual(await a.send("room1", "hello", {}), null, "the payload shape without receivedAt");
120
+ // A real ObjectId replyTo: the unsaved answer does not prove nothing was
121
+ // saved (the server's catch also covers steps after the insert): no retry.
122
+ a = scripted(unsaved(), { success: true, message: { _id: OID } });
123
+ assert.strictEqual(await a.send("room1", "hello", { replyTo: OID }), null, "no retry with a real replyTo");
124
+ assert.strictEqual(a.posts.length, 1);
125
+ // (3) Any other 2xx without an id is still the fast-ack sentinel: one POST.
126
+ a = scripted({ success: true, message: { content: "hello", type: "text" } });
127
+ assert.strictEqual(await a.send("room1", "hello", { replyTo: "call-1-2-t3" }), true, "other idless shape → sentinel true");
128
+ assert.strictEqual(a.posts.length, 1, "never posted twice");
129
+
92
130
  fs.rmSync(HOME, { recursive: true, force: true });
93
131
  console.log("kazee-message-id OK");
94
132
  })().catch((e) => {
@@ -636,7 +636,13 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
636
636
  // 3.5.62 = 3.5.61 + every background call logged by purpose and priced from its tokens, the
637
637
  // dream's cost line priced from tokens and split by part, under Sumeet's 2026-10-14 11:10 EAT
638
638
  // "yes go ahead as you see fit" (to: shall I build the cost-by-part tracking?): canary first.
639
- assert.strictEqual(pkg.version, "3.5.62", "release version must remain unchanged without explicit approval");
639
+ // 3.5.63 = 3.5.62 + passive groups judge captioned photos and files (APP-29), a proven bug
640
+ // (books bot logs, 14 Oct: Judith's captioned screenshots "group gate: silent") under
641
+ // Sumeet's standing clear-cut bug-fix rule (5 Oct 2026 12:08 EAT): canary first.
642
+ // 3.5.64 = 3.5.63 + the own-pid migration lock fix (kazee-tickets-bot crash loop, 14 Oct),
643
+ // a chat post chat-central answered unsaved is not posted, and the 5 Oct call job rules;
644
+ // Sumeet, 15 Oct 09:17 EAT: "yes to all" (canary on network-bot, then every bot).
645
+ assert.strictEqual(pkg.version, "3.5.64", "release version must remain unchanged without explicit approval");
640
646
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
641
647
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
642
648
  }
@@ -238,6 +238,35 @@ function corruptionAndLockProbe() {
238
238
  }
239
239
  }
240
240
 
241
+ function leftoverLockProbe() {
242
+ const root = tempRoot("leftover-lock");
243
+ try {
244
+ const configDir = installFixture("claude-only", root);
245
+ const opts = options(configDir);
246
+ ensureMigrationSnapshot(opts);
247
+ const lockPath = path.join(configDir, "migration-backups", MIGRATION_ID, ".lock");
248
+ const old = new Date(Date.now() - 60_000);
249
+ const leaveLock = (pid) => {
250
+ fs.writeFileSync(lockPath, `${pid}\n`, { mode: 0o600 });
251
+ fs.utimesSync(lockPath, old, old);
252
+ };
253
+
254
+ // A live other process still holds the lock.
255
+ leaveLock(process.ppid);
256
+ assert.throws(() => ensureMigrationSnapshot(opts), (error) => error.code === "MIGRATION_LOCKED");
257
+
258
+ // In a container the bot is pid 1 after every restart, so a lock left by a run
259
+ // that died holding it names our own pid: it is stale, not held (network-bot,
260
+ // 10 Oct 2026: 100 restarts on MIGRATION_LOCKED).
261
+ leaveLock(process.pid);
262
+ const snapshot = ensureMigrationSnapshot(opts);
263
+ assert.ok(snapshot.reused, "leftover own-pid lock reclaimed");
264
+ assert.ok(!fs.existsSync(lockPath), "lock released after the reclaimed run");
265
+ } finally {
266
+ fs.rmSync(root, { recursive: true, force: true });
267
+ }
268
+ }
269
+
241
270
  function activationProbe() {
242
271
  const root = tempRoot("activate");
243
272
  try {
@@ -464,6 +493,7 @@ function integrationAndDocsProbe() {
464
493
  fixtureMatrix();
465
494
  interruptionProbe();
466
495
  corruptionAndLockProbe();
496
+ leftoverLockProbe();
467
497
  activationProbe();
468
498
  rollbackProbe();
469
499
  sourceChangeProbe();