@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 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
- messageId: id,
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
- const comment = await this.client.createComment({
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
- // Spaces has no per-thread bot typing indicator, and comment edit/delete are
477
- // out of scope for v1. These keep the adapter contract total so core code can
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
- async typing() { /* no-op */ }
482
- async typingStop() { /* no-op */ }
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
- send(state.isCompacting ? "Compacting context, will pick this up next…" : "Queued.", { replyTo: replyToMsgId }).catch(() => {});
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.0.24",
3
+ "version": "3.0.26",
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": {
@@ -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.24 approved by Sumeet 2026-07-14: turn-context add-on (an engaged Spaces
36
- // turn gets task metadata + every comment since the bot's last reply, via the
37
- // channel-agnostic buildThreadContext() adapter seam the router prepends) PLUS
38
- // /recall now gates the "🧠 Jotted this down" memory-write announcements memory
39
- // still writes every turn, the chat note only surfaces when /recall is on
40
- // (state.settings.showRecall), matching how the recall/tool banners already gate.
41
- // 3.0.23 shipped the turn-context work but its CI build failed on THIS gate
42
- // (package.json bumped without updating the assertion) nothing published under
43
- // 3.0.23; 3.0.24 is the corrected re-ship (same pattern as 3.0.21 → 3.0.22).
44
- assert.strictEqual(pkg.version, "3.0.24", "release version must remain unchanged without explicit approval");
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
  }