@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 +19 -0
- package/channels/kazee/adapter.js +38 -5
- package/channels/kazee/call/quick-voice.js +1 -0
- package/core/migration-backup.js +8 -2
- package/core/passive-replies.js +3 -1
- package/core/system-prompt.js +6 -0
- package/package.json +1 -1
- package/test-kazee-message-id.js +38 -0
- package/test-provider-language.js +7 -1
- package/test-provider-migration-backup.js +30 -0
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
|
-
//
|
|
1095
|
-
//
|
|
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
|
|
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
|
-
|
|
1478
|
-
|
|
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
|
"",
|
package/core/migration-backup.js
CHANGED
|
@@ -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
|
-
|
|
263
|
-
|
|
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) {
|
package/core/passive-replies.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/core/system-prompt.js
CHANGED
|
@@ -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
package/test-kazee-message-id.js
CHANGED
|
@@ -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
|
-
|
|
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();
|