@inetafrica/open-claudia 2.14.4 → 2.14.5

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.14.5
4
+ - **`/channel remove <id>` is now a complete teardown — a re-add starts from a genuinely clean slate.** Removal used to clear only the `.env` channel config (`CHANNELS` + the `KAZEE_*` keys), but the wizard's v2.14.4 owner-capture also links the owner's Kazee handle onto their people record (`people.linkHandle`) and mirrors an identity mapping into `identities.json` (`setIdentityMapping`). Those were left behind, so a subsequent `/channel add kazee` stacked on stale handle + identity state. `/channel remove` now also drops every handle for that transport across all people records (new `people.unlinkAdapterHandles`) and removes its identity mappings — channel→canonical links and preferred-channel pointers (new `identity.removeTransportIdentities`) — and clears the previously-orphaned `KAZEE_BOT_USER_ID`.
5
+ - **The cleanup goes through the in-memory stores, so disk and the running process stay consistent.** `identities.json` is loaded once at boot into a shared in-memory object (`identity.js`), and `saveIdentities()` re-serializes that whole object on every write. Because `router.js` calls `autoLinkOwnerChannel` → `setIdentityMapping` → `saveIdentities` on each owner message, any entry lingering in memory is rewritten to disk continuously — so an out-of-band file edit can't stick, and a partial teardown leaves ghosts that reappear. Removing through the in-memory maps (then persisting) means the teardown holds. The `/channel remove` reply now reports what it cleared (e.g. `Removed channel: kazee — cleared 1 handle + identity links`).
6
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Scoped entirely to the owner-driven `/channel remove` path — inert for deployments that never remove a channel. Full `npm test` green.
7
+
3
8
  ## v2.14.4
4
9
  - **The owner no longer gets asked to double-approve their own Kazee reply — split-brain owner resolution is fixed.** On Kazee a person is identified by `userId` while the room is `channelId` (the chat-document); the two differ. The auth layer already recognised the owner correctly (via `access.matchesTransportOwner` → `currentUserId()`), but the independent relationship guardrail re-derived ownership from `channelId` — which never equals the configured `KAZEE_OWNER_USER_ID` — and so classed the owner as *external*, escalating the owner's own approval a second time. `relationship.speakerFor` now takes an optional third `userId` and keys identity/owner classification off `speakerId = userId || channelId` (the person), while still replying/escalating to `channelId` (the room). The raw per-user id is threaded to every subprocess egress guard via a new `OC_CHANNEL_USER_ID` env var + loopback query/body param (`bin/cli.js`, `bin/entity.js`, `bin/tool.js`, `bin/loopback-client.js`, `core/io.js`, `core/loopback.js`, `core/system-prompt.js`). **On Telegram `userId === channelId` in DMs, so this is a strict no-op** for existing Telegram deployments.
5
10
  - **`/channel add kazee` now links the owner's Kazee handle at capture time.** Owner recognition previously lived only in the env/adapter (`KAZEE_OWNER_USER_ID`); the owner's people record had no Kazee handle, so the guardrail (which resolves through canonical identity) re-derived the operator as external. `finalizeOwner` now calls `people.linkHandle(owner.id, { adapter: "kazee", channelId: ownerUserId, … })` so the wizard's captured owner is recognised the same way the rest of the bot resolves identity — belt-and-suspenders with the `speakerFor` fix above.
package/core/handlers.js CHANGED
@@ -1674,21 +1674,32 @@ register({
1674
1674
  if (id === "telegram") return send("Refusing to remove the Telegram adapter from a /channel command — that's your own connection.");
1675
1675
  const result = await registry.removeAdapter(id);
1676
1676
  if (!result.ok) return send(result.error);
1677
+ const transport = id.split(":")[0];
1677
1678
  // Strip from .env so the change survives restart.
1678
1679
  const channels = (config.CHANNELS || "telegram").split(",").map((s) => s.trim()).filter(Boolean);
1679
- const kept = channels.filter((c) => c !== id && !c.startsWith(id + ":") && c !== id.split(":")[0]);
1680
+ const kept = channels.filter((c) => c !== id && !c.startsWith(id + ":") && c !== transport);
1680
1681
  const next = kept.join(",") || "telegram";
1681
1682
  saveEnvKey("CHANNELS", next);
1682
1683
  config.CHANNELS = next;
1683
- if (id === "kazee") {
1684
- saveEnvKey("KAZEE_URL", "");
1685
- saveEnvKey("KAZEE_BOT_TOKEN", "");
1686
- saveEnvKey("KAZEE_OWNER_USER_ID", "");
1687
- config.KAZEE_URL = "";
1688
- config.KAZEE_BOT_TOKEN = "";
1689
- config.KAZEE_OWNER_USER_ID = "";
1684
+ if (transport === "kazee") {
1685
+ for (const k of ["KAZEE_URL", "KAZEE_BOT_TOKEN", "KAZEE_OWNER_USER_ID", "KAZEE_BOT_USER_ID"]) {
1686
+ saveEnvKey(k, "");
1687
+ config[k] = "";
1688
+ }
1690
1689
  }
1691
- return send(`Removed channel: ${id}`);
1690
+ // Complete the teardown: drop this transport's handles from every person
1691
+ // record and its identity mappings. Both mutate through the in-memory
1692
+ // stores, so disk and the running process stay consistent — otherwise a
1693
+ // stale handle/link survives in memory and gets rewritten on the next
1694
+ // add, which is exactly what made a re-add feel un-clean.
1695
+ let unlinked = 0;
1696
+ try { unlinked = require("./people").unlinkAdapterHandles(transport).length; } catch (e) {}
1697
+ let idsCleared = false;
1698
+ try { idsCleared = require("./identity").removeTransportIdentities(transport); } catch (e) {}
1699
+ const extra = (unlinked || idsCleared)
1700
+ ? ` — cleared ${unlinked} handle${unlinked === 1 ? "" : "s"} + identity links`
1701
+ : "";
1702
+ return send(`Removed channel: ${id}${extra}`);
1692
1703
  }
1693
1704
 
1694
1705
  send("Usage: /channel | /channel add kazee | /channel remove <id>");
package/core/identity.js CHANGED
@@ -137,6 +137,25 @@ function setIdentityMapping(transport, channelId, canonicalUserId) {
137
137
  };
138
138
  }
139
139
 
140
+ // Remove every identity mapping for a transport — used when a channel is torn
141
+ // down (/channel remove) so no stale channel→canonical link or preferred-channel
142
+ // pointer lingers to be rewritten to disk on the next identity write. Mutates
143
+ // the in-memory map the whole process shares, THEN persists, so memory and disk
144
+ // stay in sync (a disk-only edit would be resurrected by the cached object).
145
+ function removeTransportIdentities(transport) {
146
+ const t = String(transport || "").toLowerCase();
147
+ if (!t) return false;
148
+ let changed = false;
149
+ for (const key of Object.keys(identities.channels)) {
150
+ if (key.slice(0, t.length + 1) === t + ":") { delete identities.channels[key]; changed = true; }
151
+ }
152
+ for (const [userId, pref] of Object.entries(identities.preferred)) {
153
+ if (pref && String(pref.transport || "").toLowerCase() === t) { delete identities.preferred[userId]; changed = true; }
154
+ }
155
+ if (changed) saveIdentities();
156
+ return changed;
157
+ }
158
+
140
159
  module.exports = {
141
160
  identities,
142
161
  normalizeCanonicalUserId,
@@ -149,5 +168,6 @@ module.exports = {
149
168
  canonicalForTelegram,
150
169
  canonicalForStoredUserKey,
151
170
  setIdentityMapping,
171
+ removeTransportIdentities,
152
172
  saveIdentities,
153
173
  };
package/core/people.js CHANGED
@@ -159,6 +159,30 @@ function unlinkHandle(personId, { adapter, channelId }) {
159
159
  return p;
160
160
  }
161
161
 
162
+ // Remove every handle for a given adapter across ALL people records — used when
163
+ // a channel is torn down (/channel remove) so no stranded handle survives to be
164
+ // re-linked or re-mirrored on a later add. primaryChannel falls back to the next
165
+ // remaining handle. Returns the ids of the people that were changed.
166
+ function unlinkAdapterHandles(adapter) {
167
+ const a = String(adapter || "").toLowerCase();
168
+ if (!a) return [];
169
+ const all = load();
170
+ const affected = [];
171
+ for (const p of all) {
172
+ const before = (p.handles || []).length;
173
+ if (!before) continue;
174
+ p.handles = p.handles.filter((h) => h.adapter !== a);
175
+ if (p.handles.length === before) continue;
176
+ if (p.primaryChannel && p.primaryChannel.adapter === a) {
177
+ p.primaryChannel = p.handles[0] ? { adapter: p.handles[0].adapter, channelId: p.handles[0].channelId } : null;
178
+ }
179
+ p.updatedAt = nowIso();
180
+ affected.push(p.id);
181
+ }
182
+ if (affected.length) save(all);
183
+ return affected;
184
+ }
185
+
162
186
  function setPrimary(personId, { adapter, channelId }) {
163
187
  const all = load();
164
188
  const p = all.find((x) => x.id === personId);
@@ -264,7 +288,7 @@ module.exports = {
264
288
  load, save, list, roster,
265
289
  add, update, remove,
266
290
  findById, findByName, findByCanonicalUserId, findByHandle,
267
- linkHandle, unlinkHandle, setPrimary,
291
+ linkHandle, unlinkHandle, unlinkAdapterHandles, setPrimary,
268
292
  note, removeNote,
269
293
  setEntitySlug, resolveEntitySlug,
270
294
  owners, hasOwnerRecord, seedOwnerFromLegacy,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.14.4",
3
+ "version": "2.14.5",
4
4
  "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
5
5
  "main": "bot.js",
6
6
  "bin": {