@inetafrica/open-claudia 3.5.63 → 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 +11 -0
- package/channels/kazee/adapter.js +29 -2
- package/channels/kazee/call/quick-voice.js +1 -0
- package/core/migration-backup.js +8 -2
- 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 +4 -1
- package/test-provider-migration-backup.js +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
|
|
3
14
|
## 3.5.63
|
|
4
15
|
|
|
5
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.
|
|
@@ -84,6 +84,17 @@ const INVALID_THREAD_RE = /INVALID_THREAD|Invalid thread|Thread not found in thi
|
|
|
84
84
|
// chat-central drops a typing threadRootId that is not a Mongo ObjectId.
|
|
85
85
|
const OBJECT_ID_RE = /^[0-9a-f]{24}$/i;
|
|
86
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
|
+
|
|
87
98
|
// The thread a message belongs to: its root's id, or "" for the main timeline.
|
|
88
99
|
// The field is an id today; accept a populated root doc too.
|
|
89
100
|
function threadRootOf(msg) {
|
|
@@ -1480,8 +1491,24 @@ class KazeeAdapter {
|
|
|
1480
1491
|
this._rememberSentSafely(id, body.threadRootId || null, channelId);
|
|
1481
1492
|
if (id && botTags.length) this._rememberTagsSafely(id, botTags);
|
|
1482
1493
|
if (id) return String(id);
|
|
1483
|
-
|
|
1484
|
-
|
|
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
|
|
1485
1512
|
// reply as a failed send and resend it — the Kazee double-send. Signal
|
|
1486
1513
|
// success with a truthy, non-editable sentinel so nothing sends twice.
|
|
1487
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/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) => {
|
|
@@ -639,7 +639,10 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
639
639
|
// 3.5.63 = 3.5.62 + passive groups judge captioned photos and files (APP-29), a proven bug
|
|
640
640
|
// (books bot logs, 14 Oct: Judith's captioned screenshots "group gate: silent") under
|
|
641
641
|
// Sumeet's standing clear-cut bug-fix rule (5 Oct 2026 12:08 EAT): canary first.
|
|
642
|
-
|
|
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");
|
|
643
646
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
644
647
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
645
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();
|