@inetafrica/open-claudia 3.0.24 → 3.0.26
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 +14 -0
- package/channels/spaces/adapter.js +71 -13
- package/core/runner.js +7 -1
- package/package.json +1 -1
- package/test-provider-language.js +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.0.26 — Spaces replies actually post (and stop saying "Queued.")
|
|
4
|
+
|
|
5
|
+
- **An engaged Spaces reply no longer fails with 400 "Parent comment not found."** Core threads every agent reply under the triggering message id — a Telegram-ism. On Spaces that id is a *notification* id, not a comment id, so the backend rejected the whole post and the bot silently failed to answer any mention. `send()` now only threads under an explicit `comment:`-prefixed id and otherwise posts a top-level task comment; if a stale/deleted parent still 400s, it retries once at top level so the reply always lands.
|
|
6
|
+
- **The bot threads its reply under the exact comment it's answering.** When a notification names a specific comment (a reply's `replyId`, or a mention/comment that targets a comment), the reply is threaded under it instead of floating at the task root. Task-level mentions and assignments stay top-level, as before.
|
|
7
|
+
- **No more "Queued." noise on a task thread.** The transient queue/compaction acknowledgement is a chat-UX affordance for Telegram/Kazee. On a Spaces task it posted a throwaway "Queued." comment when a second turn stacked (and, pre-fix, that post itself 400'd). Spaces now stays silent while queued and simply posts the real reply when its turn runs. Per-identity run serialization is unchanged and intended — one being, one hand.
|
|
8
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically (FROM the pre-baked `:base`). Existing pods pick this up on their next approved upgrade.
|
|
9
|
+
|
|
10
|
+
## v3.0.25 — The bot types back on a Spaces task
|
|
11
|
+
|
|
12
|
+
- **"Open Claudia is typing…" now shows on a Spaces task thread while the bot composes.** Core already runs a typing heartbeat around every agent turn (calling `adapter.typing()` every ~4s and `adapter.typingStop()` at turn-end); on Spaces those were no-op stubs. They now re-emit the exact `comment:typing` event a human client sends over the adapter's existing authenticated socket, so a teammate watching a task sees the bot compose like any other member — the same three-dot indicator the web and mobile apps already render.
|
|
13
|
+
- **Part 4 of the cross-repo typing indicator.** The backend (`comment:typing` re-broadcast to the `space:<id>` room, identity stamped from the authenticated socket, no persistence), web, and mobile surfaces shipped in prior deploys — human↔human typing was already live end-to-end. This closes the loop so the bot participates too.
|
|
14
|
+
- **Minimal by construction.** The adapter caches each task's `spaceId` (from the notification payload and the thread-context fetch) so the heartbeat — which only knows the `task:<id>` channel — can name the room. The emit sends just `targetId + spaceId + isTyping`; the server derives who is typing. Fully best-effort: no socket, no cached space, or a transport blip is a silent no-op, so a typing dot can never affect or delay the actual reply.
|
|
15
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically (FROM the pre-baked `:base`). Existing pods pick this up on their next approved upgrade.
|
|
16
|
+
|
|
3
17
|
## v3.0.24 — Spaces threads read like a group chat; memory notes go quiet unless you're watching
|
|
4
18
|
|
|
5
19
|
- **The "🧠 Jotted this down" memory notes are now gated behind `/recall`.** After each turn the pack reviewer still writes to long-term memory exactly as before — but the chat announcement of what it filed away only surfaces when `/recall` is on (`state.settings.showRecall`), the same toggle that already gates the recall/tool banners. Off by default, so ordinary turns stay clean; flip `/recall on` to watch memory (and recall) work. That announcement was the one learning-surface bypassing the gate; it no longer does.
|
|
@@ -59,6 +59,7 @@ class SpacesAdapter {
|
|
|
59
59
|
this._socket = null;
|
|
60
60
|
this._seen = new Set(); // notification-id dedup across socket + poll
|
|
61
61
|
this._taskChains = new Map(); // taskId -> tail promise, for per-task serialization
|
|
62
|
+
this._taskSpace = new Map(); // taskId -> spaceId, so typing() can name the room
|
|
62
63
|
this._pollTimer = null;
|
|
63
64
|
this._initTimer = null;
|
|
64
65
|
}
|
|
@@ -201,9 +202,22 @@ class SpacesAdapter {
|
|
|
201
202
|
actorName = actorName || `${actorId.firstName || ""} ${actorId.lastName || ""}`.trim() || actorId.email;
|
|
202
203
|
actorId = actorId.id || actorId._id;
|
|
203
204
|
}
|
|
205
|
+
// The specific comment to thread a reply under, when the event is about one:
|
|
206
|
+
// a reply → the reply comment (replyId); a comment/mention that targets a
|
|
207
|
+
// comment → that comment id. Absent for task-level mentions and assignments,
|
|
208
|
+
// where the reply is a top-level task comment.
|
|
209
|
+
const commentId = String(
|
|
210
|
+
data.replyId || inner.replyId
|
|
211
|
+
|| data.commentId || inner.commentId
|
|
212
|
+
|| (data.targetType === "comment" ? data.targetId : "")
|
|
213
|
+
|| (inner.targetType === "comment" ? inner.targetId : "")
|
|
214
|
+
|| (payload.targetType === "comment" ? payload.targetId : "")
|
|
215
|
+
|| "",
|
|
216
|
+
);
|
|
204
217
|
return {
|
|
205
218
|
type,
|
|
206
219
|
taskId,
|
|
220
|
+
commentId,
|
|
207
221
|
actorId: actorId ? String(actorId) : "",
|
|
208
222
|
actorName: actorName || "",
|
|
209
223
|
spaceId: payload.spaceId || inner.spaceId || null,
|
|
@@ -230,6 +244,10 @@ class SpacesAdapter {
|
|
|
230
244
|
const n = this._normalize(payload);
|
|
231
245
|
if (!n.taskId) return;
|
|
232
246
|
|
|
247
|
+
// Cache the task's space so typing() can address the space:<id> room later —
|
|
248
|
+
// the heartbeat only gets a channelId ("task:<id>"), not the spaceId.
|
|
249
|
+
if (n.spaceId) this._taskSpace.set(String(n.taskId), String(n.spaceId));
|
|
250
|
+
|
|
233
251
|
// Receiving any Spaces notification proves membership → mark connected. This
|
|
234
252
|
// is the socket half of the hybrid "connected ⇒ conversational" signal; no
|
|
235
253
|
// env flag needed to turn auto-replies on.
|
|
@@ -256,7 +274,10 @@ class SpacesAdapter {
|
|
|
256
274
|
userId: n.actorId || n.taskId,
|
|
257
275
|
type: "text",
|
|
258
276
|
text: n.content || `[${n.type} on task ${n.title || n.taskId}]`,
|
|
259
|
-
|
|
277
|
+
// Thread the bot's reply under the exact comment that triggered this engage
|
|
278
|
+
// (a reply, or a mention targeting a comment). Falls back to the
|
|
279
|
+
// notification id, which send() posts as a top-level task comment.
|
|
280
|
+
messageId: n.commentId ? `comment:${n.commentId}` : id,
|
|
260
281
|
from: { id: n.actorId, name: n.actorName },
|
|
261
282
|
spaces: { taskId: n.taskId, spaceId: n.spaceId, notificationType: n.type, deepLink: n.deepLink, title: n.title, mode: this._taskLoopMode() },
|
|
262
283
|
raw: payload,
|
|
@@ -317,6 +338,15 @@ class SpacesAdapter {
|
|
|
317
338
|
try { task = await this.client.getTask(taskId); }
|
|
318
339
|
catch (e) { task = null; }
|
|
319
340
|
|
|
341
|
+
// Cache the task's space for typing() — buildThreadContext runs at the very
|
|
342
|
+
// start of an engaged turn, before the typing heartbeat first fires.
|
|
343
|
+
if (task && typeof task === "object") {
|
|
344
|
+
const sid = task.spaceId && typeof task.spaceId === "object"
|
|
345
|
+
? (task.spaceId.id || task.spaceId._id || null)
|
|
346
|
+
: (task.spaceId || null);
|
|
347
|
+
if (sid) this._taskSpace.set(String(taskId), String(sid));
|
|
348
|
+
}
|
|
349
|
+
|
|
320
350
|
const lines = ["[Kazee Spaces thread context]"];
|
|
321
351
|
lines.push(`Task: ${(task && task.title) || envelope.spaces?.title || "(untitled task)"}`);
|
|
322
352
|
|
|
@@ -459,27 +489,55 @@ class SpacesAdapter {
|
|
|
459
489
|
async send(channelId, text, opts = {}) {
|
|
460
490
|
const taskId = String(channelId || "").replace(/^task:/, "");
|
|
461
491
|
if (!taskId) { console.error("Spaces send: no taskId in channelId", channelId); return null; }
|
|
492
|
+
// Thread only under an EXPLICIT comment id (a "comment:"-prefixed replyTo,
|
|
493
|
+
// set on an engaged reply/comment-mention). Core's default replyTo is the
|
|
494
|
+
// triggering NOTIFICATION id, which is not a comment — threading under it
|
|
495
|
+
// 400s ("Parent comment not found"), so treat anything else as a top-level
|
|
496
|
+
// task comment.
|
|
497
|
+
const replyTo = String(opts.replyTo || "");
|
|
498
|
+
const parentId = replyTo.startsWith("comment:") ? replyTo.slice("comment:".length) : null;
|
|
499
|
+
const post = (parent) => this.client.createComment({ taskId, text, mentions: opts.mentions || [], parentId: parent });
|
|
500
|
+
const idOf = (c) => (c && (c._id || c.id) ? String(c._id || c.id) : true);
|
|
462
501
|
try {
|
|
463
|
-
|
|
464
|
-
taskId,
|
|
465
|
-
text,
|
|
466
|
-
mentions: opts.mentions || [],
|
|
467
|
-
parentId: opts.replyTo ? String(opts.replyTo).replace(/^comment:/, "") : null,
|
|
468
|
-
});
|
|
469
|
-
return comment && (comment._id || comment.id) ? String(comment._id || comment.id) : true;
|
|
502
|
+
return idOf(await post(parentId));
|
|
470
503
|
} catch (e) {
|
|
504
|
+
// A stale/deleted parent 400s; retry once at top level so the reply always
|
|
505
|
+
// lands on the task rather than being dropped.
|
|
506
|
+
if (parentId && /parent comment not found/i.test(e.message || "")) {
|
|
507
|
+
try { return idOf(await post(null)); }
|
|
508
|
+
catch (e2) { console.error("Spaces send error (top-level retry):", e2.message); return null; }
|
|
509
|
+
}
|
|
471
510
|
console.error("Spaces send error:", e.message);
|
|
472
511
|
return null;
|
|
473
512
|
}
|
|
474
513
|
}
|
|
475
514
|
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// call them unconditionally.
|
|
515
|
+
// Comment edit/delete are out of scope for v1. These keep the adapter contract
|
|
516
|
+
// total so core code can call them unconditionally.
|
|
479
517
|
async edit() { /* not supported */ }
|
|
480
518
|
async delete() { /* not supported */ }
|
|
481
|
-
|
|
482
|
-
|
|
519
|
+
|
|
520
|
+
// Ephemeral "Open Claudia is typing…" on the task thread. Core's typing
|
|
521
|
+
// heartbeat (runner.js) calls typing() every ~4s during a run and typingStop()
|
|
522
|
+
// at turn-end; we re-emit the same comment:typing event a human client sends,
|
|
523
|
+
// so teammates watching the thread see the bot compose like anyone else. The
|
|
524
|
+
// backend stamps identity from the authenticated socket and re-broadcasts to
|
|
525
|
+
// space:<id> (excluding us), so we only send targetId + spaceId + isTyping.
|
|
526
|
+
// Best-effort: no socket, no cached space, or a transport blip is silently a
|
|
527
|
+
// no-op — a typing dot must never affect the actual reply.
|
|
528
|
+
async typing(channelId) { this._emitTyping(channelId, true); }
|
|
529
|
+
async typingStop(channelId) { this._emitTyping(channelId, false); }
|
|
530
|
+
|
|
531
|
+
_emitTyping(channelId, isTyping) {
|
|
532
|
+
try {
|
|
533
|
+
if (!this._socket?.connected) return;
|
|
534
|
+
const taskId = String(channelId || "").replace(/^task:/, "");
|
|
535
|
+
if (!taskId) return;
|
|
536
|
+
const spaceId = this._taskSpace.get(taskId);
|
|
537
|
+
if (!spaceId) return;
|
|
538
|
+
this._socket.emit("comment:typing", { targetType: "task", targetId: taskId, spaceId, isTyping: !!isTyping });
|
|
539
|
+
} catch (e) { /* typing is best-effort */ }
|
|
540
|
+
}
|
|
483
541
|
async sendVoice() { return false; }
|
|
484
542
|
async sendPhoto() { return false; }
|
|
485
543
|
async sendFile() { return false; }
|
package/core/runner.js
CHANGED
|
@@ -1982,7 +1982,13 @@ function runAgent(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1982
1982
|
}
|
|
1983
1983
|
const deferred = deferredRunResult();
|
|
1984
1984
|
state.messageQueue.push({ runContext, store, deferred, queuedAt: Date.now() });
|
|
1985
|
-
|
|
1985
|
+
// Transient queue/compaction acks are a chat-UX affordance (Telegram/Kazee).
|
|
1986
|
+
// On a Spaces task thread they'd post a throwaway "Queued." comment on the
|
|
1987
|
+
// task — noise. Spaces stays quiet and just posts the real reply when its
|
|
1988
|
+
// turn runs.
|
|
1989
|
+
if (currentAdapter()?.type !== "spaces") {
|
|
1990
|
+
send(state.isCompacting ? "Compacting context, will pick this up next…" : "Queued.", { replyTo: replyToMsgId }).catch(() => {});
|
|
1991
|
+
}
|
|
1986
1992
|
return deferred.promise;
|
|
1987
1993
|
}
|
|
1988
1994
|
return executeAdmittedRun(runContext, state, store, false, true);
|
package/package.json
CHANGED
|
@@ -32,16 +32,16 @@ assert.doesNotMatch(setup, /Your Claude Code bot is connected|Description=Claude
|
|
|
32
32
|
|
|
33
33
|
const pkg = JSON.parse(read("package.json"));
|
|
34
34
|
assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
35
|
-
// 3.0.
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// (
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
assert.strictEqual(pkg.version, "3.0.
|
|
35
|
+
// 3.0.26 approved by Sumeet 2026-07-15: fix Spaces reply posting. (1) An engaged
|
|
36
|
+
// reply no longer 400s — send() only threads under an explicit "comment:"-prefixed
|
|
37
|
+
// id (the triggering reply/comment-mention) and otherwise posts a top-level task
|
|
38
|
+
// comment, with a top-level retry if a stale parent 400s. (2) The bot threads its
|
|
39
|
+
// reply under the exact triggering comment when the notification names one
|
|
40
|
+
// (replyId / comment-target). (3) The transient "Queued." ack is suppressed on
|
|
41
|
+
// Spaces — a stacked turn stays silent and just posts the real reply when it runs,
|
|
42
|
+
// instead of dropping a throwaway "Queued." comment on the task. Builds on 3.0.25's
|
|
43
|
+
// comment:typing emit.
|
|
44
|
+
assert.strictEqual(pkg.version, "3.0.26", "release version must remain unchanged without explicit approval");
|
|
45
45
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
46
46
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
47
47
|
}
|