@essentialai/cogent-plugin 3.25.4 → 3.25.6

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogent",
3
- "version": "3.25.4",
3
+ "version": "3.25.6",
4
4
  "description": "Inter-session communication bridge for Claude Code with Slack integration. Enables CC agents and Slack team members to communicate in real time.",
5
5
  "author": {
6
6
  "name": "Essential AI Solutions",
@@ -625,6 +625,20 @@ function hasJoinedInProcess() {
625
625
  function markCloudAttemptInProcess() {
626
626
  cloudAttemptInProcessFlag = true;
627
627
  joinedInProcessFlag = false;
628
+ cloudAttemptGeneration++;
629
+ for (const fn of cloudAttemptListeners) {
630
+ try {
631
+ fn();
632
+ } catch {
633
+ }
634
+ }
635
+ }
636
+ function getCloudAttemptGeneration() {
637
+ return cloudAttemptGeneration;
638
+ }
639
+ function onCloudAttemptInProcess(fn) {
640
+ cloudAttemptListeners.add(fn);
641
+ return () => cloudAttemptListeners.delete(fn);
628
642
  }
629
643
  function hasCloudAttemptInProcess() {
630
644
  return cloudAttemptInProcessFlag;
@@ -662,7 +676,7 @@ async function legacyGlobalCredentialsExist() {
662
676
  return false;
663
677
  }
664
678
  }
665
- var LEGACY_GLOBAL_CREDENTIAL_PATH, joinedInProcessFlag, cloudAttemptInProcessFlag;
679
+ var LEGACY_GLOBAL_CREDENTIAL_PATH, joinedInProcessFlag, cloudAttemptInProcessFlag, cloudAttemptGeneration, cloudAttemptListeners;
666
680
  var init_credential_store = __esm({
667
681
  "src/cloud/credential-store.ts"() {
668
682
  "use strict";
@@ -672,6 +686,8 @@ var init_credential_store = __esm({
672
686
  );
673
687
  joinedInProcessFlag = false;
674
688
  cloudAttemptInProcessFlag = false;
689
+ cloudAttemptGeneration = 0;
690
+ cloudAttemptListeners = /* @__PURE__ */ new Set();
675
691
  }
676
692
  });
677
693
 
@@ -26396,8 +26412,8 @@ var init_stdio2 = __esm({
26396
26412
  // src/constants.ts
26397
26413
  import { createRequire } from "node:module";
26398
26414
  function resolveVersion() {
26399
- if ("3.25.4") {
26400
- return "3.25.4";
26415
+ if ("3.25.6") {
26416
+ return "3.25.6";
26401
26417
  }
26402
26418
  try {
26403
26419
  const require2 = createRequire(import.meta.url);
@@ -26787,11 +26803,14 @@ var init_errors4 = __esm({
26787
26803
  BridgeErrorCode2["DIR_NOT_WRITABLE"] = "DIR_NOT_WRITABLE";
26788
26804
  BridgeErrorCode2["NETWORK_TIMEOUT"] = "NETWORK_TIMEOUT";
26789
26805
  BridgeErrorCode2["JOIN_NOT_CONFIRMED"] = "JOIN_NOT_CONFIRMED";
26806
+ BridgeErrorCode2["PEER_NOT_YOURS"] = "PEER_NOT_YOURS";
26790
26807
  return BridgeErrorCode2;
26791
26808
  })(BridgeErrorCode || {});
26792
26809
  BridgeError = class extends Error {
26793
26810
  code;
26794
26811
  suggestion;
26812
+ /** Relay hint: ms until a refused peerId can be reclaimed (PEER_NOT_YOURS only). */
26813
+ retryAfterMs;
26795
26814
  constructor(code, message, suggestion) {
26796
26815
  const fullMessage = suggestion ? `${code}: ${message}. ${suggestion}` : `${code}: ${message}`;
26797
26816
  super(fullMessage);
@@ -28278,12 +28297,16 @@ var init_http_client = __esm({
28278
28297
  } catch {
28279
28298
  errorBody = { message: resp.statusText };
28280
28299
  }
28281
- const code = errorBody.code && errorBody.code in BridgeErrorCode ? errorBody.code : "STARTUP_FAILED" /* STARTUP_FAILED */;
28282
- throw new BridgeError(
28300
+ const code = /PEER_NOT_YOURS/.test(errorBody.message ?? "") ? "PEER_NOT_YOURS" /* PEER_NOT_YOURS */ : errorBody.code && errorBody.code in BridgeErrorCode ? errorBody.code : "STARTUP_FAILED" /* STARTUP_FAILED */;
28301
+ const err = new BridgeError(
28283
28302
  code,
28284
28303
  errorBody.message ?? `HTTP ${resp.status}`,
28285
28304
  errorBody.suggestion
28286
28305
  );
28306
+ if (typeof errorBody.retryAfterMs === "number" && Number.isFinite(errorBody.retryAfterMs)) {
28307
+ err.retryAfterMs = errorBody.retryAfterMs;
28308
+ }
28309
+ throw err;
28287
28310
  }
28288
28311
  };
28289
28312
  }
@@ -35510,12 +35533,14 @@ var init_claude_preflight = __esm({
35510
35533
  var startup_exports = {};
35511
35534
  __export(startup_exports, {
35512
35535
  autoRelay: () => autoRelay,
35536
+ cancelPresenceRetry: () => cancelPresenceRetry,
35513
35537
  cloudHttpClient: () => cloudHttpClient,
35514
35538
  cloudInbox: () => cloudInbox,
35515
35539
  cloudWsClient: () => cloudWsClient,
35516
35540
  hasPendingLabelResolve: () => hasPendingLabelResolve,
35517
35541
  isCloudMode: () => isCloudMode2,
35518
35542
  presenceRegisterArgs: () => presenceRegisterArgs,
35543
+ presenceRetryPending: () => presenceRetryPending,
35519
35544
  reinitCloudBackend: () => reinitCloudBackend,
35520
35545
  resolvePendingLabel: () => resolvePendingLabel,
35521
35546
  restorePeerPresence: () => restorePeerPresence,
@@ -35668,7 +35693,29 @@ function presenceRegisterArgs(p) {
35668
35693
  // AUD-003 ownership proof — reclaims a peer owned by an earlier token
35669
35694
  ];
35670
35695
  }
35671
- async function restorePeerPresence() {
35696
+ function presenceRetryPending() {
35697
+ return presenceRetry !== null;
35698
+ }
35699
+ function cancelPresenceRetry() {
35700
+ if (presenceRetry) clearTimeout(presenceRetry);
35701
+ presenceRetry = null;
35702
+ }
35703
+ function presenceRetryDelayMs(attempt) {
35704
+ const base = Number(process.env.COGENT_PRESENCE_RETRY_MS);
35705
+ return Math.min((Number.isFinite(base) && base > 0 ? base : 3e4) * 2 ** attempt, 3e5);
35706
+ }
35707
+ async function restorePeerPresence(attempt = 0) {
35708
+ if (!presenceSupersedeBound) {
35709
+ presenceSupersedeBound = true;
35710
+ onCloudAttemptInProcess(() => cancelPresenceRetry());
35711
+ }
35712
+ const superseded = { state: "deferred", restored: false, detail: PRESENCE_SUPERSEDED };
35713
+ if (hasCloudAttemptInProcess()) {
35714
+ logPresence(superseded);
35715
+ return superseded;
35716
+ }
35717
+ const generation = getCloudAttemptGeneration();
35718
+ const stillCurrent = () => getCloudAttemptGeneration() === generation;
35672
35719
  const outcome = await restorePresence({
35673
35720
  // The endpoint being a relay URL is what makes presence meaningful. We deliberately do
35674
35721
  // NOT gate on the active backend being the cloud one: at startup it is still the
@@ -35689,16 +35736,37 @@ async function restorePeerPresence() {
35689
35736
  loadCredentials: () => loadCredentials(),
35690
35737
  cwd: () => process.cwd(),
35691
35738
  goOnline: async ({ peerId, endpoint, sessionId, token, cwd, label, peerSecret }) => {
35739
+ if (!stillCurrent()) throw new Error(PRESENCE_SUPERSEDED);
35692
35740
  await reinitCloudBackend(endpoint, sessionId, token);
35693
35741
  const backend = getBackend();
35694
35742
  await backend.registerPeer(...presenceRegisterArgs({ peerId, sessionId, cwd, label, peerSecret }));
35743
+ const slot = await loadCredentials();
35744
+ if (!stillCurrent() || slot?.sessionId !== sessionId || slot?.endpoint !== endpoint) {
35745
+ throw new Error(PRESENCE_SUPERSEDED);
35746
+ }
35695
35747
  autoRelay.setLocalPeer(peerId, sessionId, cwd, "agent", label, sessionId);
35696
35748
  if (cloudWsClient) cloudWsClient.setPeerId(peerId);
35697
35749
  heartbeat.restart();
35750
+ markJoinedInProcess();
35698
35751
  }
35699
35752
  });
35753
+ if (!stillCurrent()) {
35754
+ logPresence(superseded);
35755
+ return superseded;
35756
+ }
35700
35757
  setPresence(outcome);
35701
35758
  logPresence(outcome);
35759
+ if (outcome.state === "offline" && outcome.detail.startsWith("presence restore failed")) {
35760
+ const delay = presenceRetryDelayMs(attempt);
35761
+ cancelPresenceRetry();
35762
+ presenceRetry = setTimeout(() => {
35763
+ presenceRetry = null;
35764
+ void restorePeerPresence(attempt + 1).catch(() => {
35765
+ });
35766
+ }, delay);
35767
+ presenceRetry.unref?.();
35768
+ logger.warn(`presence: restore failed \u2014 retrying in ${Math.round(delay / 1e3)}s`);
35769
+ }
35702
35770
  return outcome;
35703
35771
  }
35704
35772
  function createCloudWsClient(endpoint, sessionId, token, http, inbox, pollIntervalMs) {
@@ -35938,7 +36006,7 @@ async function runPreflights() {
35938
36006
  await preflightClaude(config2.COGENT_CLAUDE_PATH);
35939
36007
  }
35940
36008
  }
35941
- var UUID_V4_RE, execFileAsync3, PERSIST_PATH, legacyWarnEmitted, cloudWsClient, cloudInbox, cloudHttpClient, cloudModeActive, inboxNotifier, notifierBoundTo, reRegistrationInProgress, pendingLabelResolve;
36009
+ var UUID_V4_RE, execFileAsync3, PERSIST_PATH, legacyWarnEmitted, cloudWsClient, cloudInbox, cloudHttpClient, cloudModeActive, inboxNotifier, notifierBoundTo, reRegistrationInProgress, presenceRetry, presenceSupersedeBound, PRESENCE_SUPERSEDED, pendingLabelResolve;
35942
36010
  var init_startup = __esm({
35943
36011
  "src/startup.ts"() {
35944
36012
  "use strict";
@@ -35971,6 +36039,9 @@ var init_startup = __esm({
35971
36039
  inboxNotifier = null;
35972
36040
  notifierBoundTo = null;
35973
36041
  reRegistrationInProgress = false;
36042
+ presenceRetry = null;
36043
+ presenceSupersedeBound = false;
36044
+ PRESENCE_SUPERSEDED = "presence restore superseded: an explicit cogent_join_session/cogent_create_session ran in this process and owns the channel choice";
35974
36045
  pendingLabelResolve = null;
35975
36046
  }
35976
36047
  });
@@ -36043,6 +36114,30 @@ function unresolvedSessionReason(r) {
36043
36114
  if (r.sessionId) return null;
36044
36115
  return r.candidateCount === 0 ? "no-candidates" : "ambiguous";
36045
36116
  }
36117
+ function peerReclaimWaitMs() {
36118
+ const v = Number(process.env.COGENT_PEER_RECLAIM_WAIT_MS);
36119
+ return Number.isFinite(v) && v >= 0 ? v : 5e4;
36120
+ }
36121
+ async function retrySamePeerId(first, attempt, budgetMs = peerReclaimWaitMs(), sleep4 = (ms) => new Promise((r) => setTimeout(r, ms))) {
36122
+ let err = first;
36123
+ const start = Date.now();
36124
+ let backoff = 1e3;
36125
+ for (; ; ) {
36126
+ const remaining = budgetMs - (Date.now() - start);
36127
+ const retryAfter = err.retryAfterMs;
36128
+ if (retryAfter === void 0 || remaining <= 0 || retryAfter > remaining) throw err;
36129
+ const wait = Math.min(Math.max(retryAfter, backoff), remaining);
36130
+ logger.warn(`register_peer: peerId held by an earlier registration \u2014 retrying the SAME id in ${Math.round(wait / 1e3)}s`);
36131
+ await sleep4(wait);
36132
+ backoff = Math.min(backoff * 2, 15e3);
36133
+ try {
36134
+ return await attempt();
36135
+ } catch (next) {
36136
+ if (!(next instanceof BridgeError && next.code === "PEER_NOT_YOURS" /* PEER_NOT_YOURS */)) throw next;
36137
+ err = next;
36138
+ }
36139
+ }
36140
+ }
36046
36141
  function registerRegisterPeerTool(server) {
36047
36142
  server.registerTool(
36048
36143
  "cogent_register_peer",
@@ -36104,6 +36199,17 @@ function registerRegisterPeerTool(server) {
36104
36199
  }
36105
36200
  }
36106
36201
  if (!cloudSessionId) {
36202
+ const stored = await loadCredentials();
36203
+ if (stored?.sessionId) {
36204
+ const channel = stored.label ? `'${stored.label}' (${stored.sessionId})` : `'${stored.sessionId}'`;
36205
+ return errorResult(
36206
+ new BridgeError(
36207
+ "JOIN_NOT_CONFIRMED" /* JOIN_NOT_CONFIRMED */,
36208
+ `This working directory is already on channel ${channel} at ${stored.endpoint}, but this bridge is not online on it, so cogent_register_peer will NOT auto-create a new channel (that would silently move you off your channel while reporting success)`,
36209
+ `Call cogent_join_session for channel ${channel} (name + secret, + Org_ID for Team channels) and confirm it returns success, then retry cogent_register_peer with the SAME peerId. If that join reports the channel no longer exists, or you do not know its secret, ask the user which channel this agent belongs on and join it (cogent_join_session) or create it (cogent_create_session) explicitly, then register with the SAME peerId \u2014 a channel is never auto-created over a stored one`
36210
+ )
36211
+ );
36212
+ }
36107
36213
  autoSecret = crypto10.randomBytes(16).toString("hex");
36108
36214
  const resp = await fetch(
36109
36215
  `${endpoint}/api/sessions`,
@@ -36239,6 +36345,24 @@ function registerRegisterPeerTool(server) {
36239
36345
  logger.debug(`register_peer: ownership recovery failed: ${recoverErr.message}`);
36240
36346
  }
36241
36347
  }
36348
+ if (!recovered && regErr instanceof BridgeError && regErr.code === "PEER_NOT_YOURS" /* PEER_NOT_YOURS */) {
36349
+ recovered = await retrySamePeerId(
36350
+ regErr,
36351
+ () => backend.registerPeer(
36352
+ peerId,
36353
+ sessionId,
36354
+ cwd,
36355
+ label,
36356
+ CLIENT_VERSION,
36357
+ mode,
36358
+ channelSessionId,
36359
+ capabilities,
36360
+ workspaceId,
36361
+ threadId,
36362
+ storedPeerSecret
36363
+ )
36364
+ );
36365
+ }
36242
36366
  if (!recovered) throw regErr;
36243
36367
  peer = recovered;
36244
36368
  }
@@ -79,6 +79,8 @@ Call `cogent_register_peer` with:
79
79
  - `label`: `CHANNEL_PEER_LABEL`, or derive from peerId (capitalize, replace hyphens with spaces)
80
80
  - `mode`: `"agent"` (default — auto-replies to directed messages). Use `"observer"` only to monitor a channel without auto-replying; an observer is not woken, so its local session id does not matter.
81
81
 
82
+ > 🔴 **HARD RULE — never change your peerId.** Register under the EXACT peer name you were given — never a suffixed, "-recovery", "-2" or otherwise invented variant: your peers address you by that name, and a renamed peer silently stops receiving their messages. If `cogent_register_peer` fails with **`PEER_NOT_YOURS`**, the name is almost always held by your OWN earlier registration: retry the **SAME** `peerId` after the `retryAfterMs` / time the error states (or stop the other session using it). If it returns **`JOIN_NOT_CONFIRMED`**, re-join the channel it names — do not create a new one. Report the failure to the user rather than picking another name.
83
+
82
84
  ### Step 4: Confirm
83
85
 
84
86
  Call `cogent_list_peers` to show who is online.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essentialai/cogent-plugin",
3
- "version": "3.25.4",
3
+ "version": "3.25.6",
4
4
  "description": "Cogent — Claude Code plugin (skills + slash-commands + MCP server) for the cross-agent comms fabric.",
5
5
  "author": { "name": "Essential AI Solutions Ltd.", "url": "https://essentialai.uk" },
6
6
  "homepage": "https://cogent.tools",
@@ -65,6 +65,8 @@ Call `cogent_register_peer` with:
65
65
  - `cwd`: the absolute working directory path (output of `pwd`)
66
66
  - `label`: the chosen label
67
67
 
68
+ > 🔴 **HARD RULE — never change your peerId.** Register under the EXACT peer name you were given — never a suffixed, "-recovery", "-2" or otherwise invented variant: your peers address you by that name, and a renamed peer silently stops receiving their messages. If `cogent_register_peer` fails with **`PEER_NOT_YOURS`**, the name is almost always held by your OWN earlier registration: retry the **SAME** `peerId` after the `retryAfterMs` / time the error states (or stop the other session using it). If it returns **`JOIN_NOT_CONFIRMED`**, re-join the channel it names — do not create a new one. Report the failure to the user rather than picking another name.
69
+
68
70
  ## Step 4: Confirm Registration
69
71
 
70
72
  After successful registration, inform the user: