@essentialai/cogent-plugin 3.25.8 → 3.25.9

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.8",
3
+ "version": "3.25.9",
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",
@@ -655,18 +655,80 @@ async function loadCredentials(credentialPath) {
655
655
  async function saveCredentials(creds, credentialPath) {
656
656
  const filePath = resolveCredentialPath(credentialPath);
657
657
  await fs4.mkdir(path4.dirname(filePath), { recursive: true, mode: 448 });
658
- await fs4.writeFile(
659
- filePath,
660
- JSON.stringify(creds, null, 2) + "\n",
661
- { encoding: "utf-8", mode: 384 }
662
- );
663
- await fs4.chmod(filePath, 384);
658
+ let out = creds;
659
+ if (creds.peers === void 0) {
660
+ const prior = await loadCredentials(filePath);
661
+ if (prior?.peers) out = { ...creds, peers: prior.peers };
662
+ }
663
+ const tmp = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
664
+ await fs4.writeFile(tmp, JSON.stringify(out, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
665
+ await fs4.chmod(tmp, 384);
666
+ await fs4.rename(tmp, filePath);
664
667
  }
665
668
  async function rememberIssuedPeerSecret(sessionId, peerId, peerSecret) {
666
669
  const creds = await loadCredentials();
667
670
  if (!creds || creds.sessionId !== sessionId) return;
668
- if (creds.peerId === peerId && creds.peerSecret === peerSecret) return;
669
- await saveCredentials({ ...creds, peerId, peerSecret, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
671
+ const entry = creds.peers?.[peerId];
672
+ const entryCurrent = !entry || entry.sessionId !== sessionId || entry.peerSecret === peerSecret;
673
+ if (creds.peerId === peerId && creds.peerSecret === peerSecret && entryCurrent) return;
674
+ const peers = entry && entry.sessionId === sessionId ? { ...creds.peers, [peerId]: { ...entry, peerSecret, savedAt: (/* @__PURE__ */ new Date()).toISOString() } } : creds.peers;
675
+ const base = entry && entry.sessionId === sessionId ? { ...creds, ...entry } : creds;
676
+ await saveCredentials({ ...base, peerId, peerSecret, ...peers ? { peers } : {}, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
677
+ }
678
+ function setCurrentCloudConnection(c2) {
679
+ currentCloudConnection = c2;
680
+ }
681
+ function getCurrentCloudConnection() {
682
+ return currentCloudConnection;
683
+ }
684
+ function peerCredentials(folder, peerId) {
685
+ if (!folder) return null;
686
+ if (folder.peerId === peerId) {
687
+ const { peers: _p, ...top } = folder;
688
+ return top;
689
+ }
690
+ const e = folder.peers?.[peerId];
691
+ return e ? { ...e, peerId } : null;
692
+ }
693
+ function peerRecordsOf(folder) {
694
+ if (!folder?.peers) return [];
695
+ return Object.entries(folder.peers).map(([peerId, e]) => ({ ...e, peerId }));
696
+ }
697
+ async function loadPeerRecord(peerId, credentialPath) {
698
+ return peerCredentials(await loadCredentials(credentialPath), peerId);
699
+ }
700
+ async function loadPeerRecords(credentialPath) {
701
+ return peerRecordsOf(await loadCredentials(credentialPath));
702
+ }
703
+ async function savePeerRecord(rec, credentialPath) {
704
+ const folder = await loadCredentials(credentialPath);
705
+ if (!folder) return;
706
+ const { peerId, peers: _p, ...entry } = rec;
707
+ await saveCredentials({ ...folder, peers: { ...folder.peers ?? {}, [peerId]: entry } }, credentialPath);
708
+ }
709
+ async function deletePeerRecord(peerId, credentialPath) {
710
+ const folder = await loadCredentials(credentialPath);
711
+ if (!folder?.peers?.[peerId]) return;
712
+ const { [peerId]: _gone, ...rest } = folder.peers;
713
+ await saveCredentials({ ...folder, peers: rest }, credentialPath);
714
+ }
715
+ function chooseRestoreCredentials(folder, records, ownSession, platform) {
716
+ if (records.length === 0) return { creds: folder };
717
+ const top = folder?.peerId ? peerCredentials(folder, folder.peerId) : null;
718
+ const pick2 = (r) => peerCredentials(folder, r.peerId) ?? r;
719
+ if (ownSession) {
720
+ const mine = records.filter((r) => r.agentSession === ownSession);
721
+ if (mine.length > 0) return { creds: pick2(mine[0]) };
722
+ }
723
+ const sameKind = records.filter((r) => r.platform === platform);
724
+ if (sameKind.length === 1) return { creds: pick2(sameKind[0]) };
725
+ if (sameKind.length > 1 && top && sameKind.some((r) => r.peerId === top.peerId)) return { creds: top };
726
+ if (sameKind.length === 0 && top && !records.some((r) => r.peerId === top.peerId)) return { creds: top };
727
+ const names = records.map((r) => `'${r.peerId}' (${r.platform ?? "unknown agent"})`).join(", ");
728
+ return {
729
+ creds: null,
730
+ skip: sameKind.length === 0 ? `this folder's saved names belong to other agents \u2014 ${names}; this ${platform} agent has none, so it registers its own name explicitly` : `this folder has several saved ${platform} names and this bridge cannot tell which is its own \u2014 ${names}; the agent registers its name explicitly`
731
+ };
670
732
  }
671
733
  async function legacyGlobalCredentialsExist() {
672
734
  try {
@@ -676,7 +738,7 @@ async function legacyGlobalCredentialsExist() {
676
738
  return false;
677
739
  }
678
740
  }
679
- var LEGACY_GLOBAL_CREDENTIAL_PATH, joinedInProcessFlag, cloudAttemptInProcessFlag, cloudAttemptGeneration, cloudAttemptListeners;
741
+ var LEGACY_GLOBAL_CREDENTIAL_PATH, joinedInProcessFlag, cloudAttemptInProcessFlag, cloudAttemptGeneration, cloudAttemptListeners, currentCloudConnection;
680
742
  var init_credential_store = __esm({
681
743
  "src/cloud/credential-store.ts"() {
682
744
  "use strict";
@@ -688,6 +750,7 @@ var init_credential_store = __esm({
688
750
  cloudAttemptInProcessFlag = false;
689
751
  cloudAttemptGeneration = 0;
690
752
  cloudAttemptListeners = /* @__PURE__ */ new Set();
753
+ currentCloudConnection = null;
691
754
  }
692
755
  });
693
756
 
@@ -26412,8 +26475,8 @@ var init_stdio2 = __esm({
26412
26475
  // src/constants.ts
26413
26476
  import { createRequire } from "node:module";
26414
26477
  function resolveVersion() {
26415
- if ("3.25.8") {
26416
- return "3.25.8";
26478
+ if ("3.25.9") {
26479
+ return "3.25.9";
26417
26480
  }
26418
26481
  try {
26419
26482
  const require2 = createRequire(import.meta.url);
@@ -26692,6 +26755,11 @@ var init_config = __esm({
26692
26755
  // launches via the daemon (see bin/cogent-codex) and sets this to `app-server`.
26693
26756
  // Only affects COGENT_PLATFORM=codex; claude/gemini/mail rails never read it.
26694
26757
  COGENT_CODEX_WAKE: import_zod2.z.enum(["auto", "exec-resume", "app-server"]).default("auto"),
26758
+ // COG-86: what a Claude wake may do. `read` (default) = Read, Glob and Grep only: the wake answers but cannot edit,
26759
+ // run commands or commit. A wake resumes the SAME session a human may have open in a terminal, and nothing can
26760
+ // tell reliably that the terminal is there; on 2026-10-15 a full-tool wake edited a live session's files, then
26761
+ // committed and pushed. `full` = every tool (the behaviour before 3.25.9), for an agent no human drives.
26762
+ COGENT_WAKE_TOOLS: import_zod2.z.enum(["read", "full"]).default("read"),
26695
26763
  // Auto-recovery for a blocked Codex sandbox. On a host where the OS sandbox
26696
26764
  // can't initialize (unprivileged user namespaces blocked — Ubuntu 24.04's
26697
26765
  // apparmor_restrict_unprivileged_userns, containers, hardened kernels),
@@ -27352,6 +27420,11 @@ async function execClaude(sessionId, message, cwd, timeoutMs) {
27352
27420
  // as a raw JSON blob. Flag drift is guarded by claude-preflight.ts.
27353
27421
  "--output-format",
27354
27422
  "json",
27423
+ // COG-86: an ALLOW list, never a deny list. --disallowedTools Edit,Write,NotebookEdit,Bash was measured to leak
27424
+ // (the Monitor tool, loaded through ToolSearch, ran a shell and wrote a file) and would miss every tool a later
27425
+ // claude adds. --tools is variadic, so it must be followed by another option, never by the message. A claude
27426
+ // too old to know --tools exits "unknown option" — the wake fails, it never runs with every tool.
27427
+ ...config2.COGENT_WAKE_TOOLS === "read" ? ["--tools", READ_ONLY_WAKE_TOOLS, "--append-system-prompt", READ_ONLY_WAKE_NOTICE] : [],
27355
27428
  "-p",
27356
27429
  truncated
27357
27430
  ];
@@ -27480,12 +27553,15 @@ function parseClaudeJsonReply(raw) {
27480
27553
  const result = typeof env.result === "string" ? env.result : null;
27481
27554
  return { result, isError: env.is_error === true || env.subtype !== "success" };
27482
27555
  }
27556
+ var READ_ONLY_WAKE_TOOLS, READ_ONLY_WAKE_NOTICE;
27483
27557
  var init_cc_cli = __esm({
27484
27558
  "src/services/cc-cli.ts"() {
27485
27559
  "use strict";
27486
27560
  init_config();
27487
27561
  init_agent_config();
27488
27562
  init_agent_sessions();
27563
+ READ_ONLY_WAKE_TOOLS = "Read,Glob,Grep";
27564
+ READ_ONLY_WAKE_NOTICE = "This is an automatic Cogent wake. You can read files but cannot change them or run commands. If the message asks for changes, say what must change and that it has to be done in the agent's own session.";
27489
27565
  }
27490
27566
  });
27491
27567
 
@@ -33900,6 +33976,7 @@ function announceBridge(cwd, peerId, version2, endpoint, sessionId = agentSessio
33900
33976
  cwd: path19.resolve(cwd),
33901
33977
  parentPid: process.ppid,
33902
33978
  ...sessionId ? { sessionId } : {},
33979
+ platform: process.env.COGENT_PLATFORM || "cc",
33903
33980
  // 🔴 A headless bridge never had a client to lose (review r7 FIX2-2, SEC2-2): when its launching
33904
33981
  // shell exited, the changed parent read as "client gone" and any bridge that started took its name.
33905
33982
  ...startedWithoutClient() ? { noClient: true } : {}
@@ -35407,7 +35484,13 @@ function mayBeOwnPreSessionBridge(holder, ownSession) {
35407
35484
  const [maj, min, pat] = [Number(v[1]), Number(v[2]), Number(v[3])];
35408
35485
  return maj < 3 || maj === 3 && (min < 25 || min === 25 && pat < 7);
35409
35486
  }
35410
- function heldNameAdvice(peerId) {
35487
+ function heldNameAdvice(peerId, holder, own) {
35488
+ const otherSession = !!(holder?.sessionId && own?.sessionId && holder.sessionId !== own.sessionId);
35489
+ const otherPlatform = !!(holder?.platform && own?.platform && holder.platform !== own.platform);
35490
+ if (otherSession || otherPlatform) {
35491
+ const who = [holder?.platform && `a ${holder.platform} agent`, holder?.sessionId && `session ${holder.sessionId}`].filter(Boolean).join(", ");
35492
+ return `'${peerId}' is held by ANOTHER agent's live bridge in this folder (${who}). It keeps the name while that agent runs, so waiting will not free it. Do not take the name and do not rename: tell the user which agent holds '${peerId}' and ask them to have THAT agent run /cogent:register with ITS OWN peer name (which releases '${peerId}'); after that, run /cogent:register again with the SAME peer name '${peerId}'`;
35493
+ }
35411
35494
  return `If this agent was just restarted, its previous connection closes within ${HOLDER_LINGER_SECONDS} seconds: wait ${HOLDER_LINGER_SECONDS}+ seconds, then run /cogent:register again with the SAME peer name '${peerId}'. If '${peerId}' is still held after that, another live connection is using it \u2014 another agent, or an old window of this one still open. Do not take the name and do not rename: ask the user`;
35412
35495
  }
35413
35496
  function anotherAgentSessionAmong(others, ownSession) {
@@ -35425,7 +35508,7 @@ function otherBridgeNotice(others, peerId, verdict, ownSession) {
35425
35508
  }
35426
35509
  return `${who} is registered as '${peerId}' too. NOBODY answers for '${peerId}' in this channel now: the bridge that started last (pid ${verdict.owner?.pid ?? "unknown"}) is the one that wakes its agent, but in this channel the relay registration now belongs to this bridge, so the relay refuses that bridge's replies here and this bridge does not wake. It stays that way until that bridge is closed or its agent registers again. If that bridge is registered in another channel, it still answers there.`;
35427
35510
  });
35428
- return lines.join(" ") + (anotherAgentSessionAmong(others, ownSession) ? ` Two agents in one folder are not supported yet: each agent needs its own peer name and, for now, its own folder.` : "");
35511
+ return lines.join(" ") + (anotherAgentSessionAmong(others, ownSession) ? ` Each agent in this folder keeps its own saved name (COG-80): after a restart it comes back under that name, not the other agent's.` : "");
35429
35512
  }
35430
35513
  function autoPresenceEnabled(env = process.env) {
35431
35514
  const v = env.COGENT_AUTO_PRESENCE;
@@ -35441,6 +35524,10 @@ async function restorePresence(deps) {
35441
35524
  return { state: "online", restored: false, detail: "registered by this process" };
35442
35525
  }
35443
35526
  const creds = await deps.loadCredentials();
35527
+ const skip = creds ? void 0 : deps.restoreSkip?.();
35528
+ if (skip) {
35529
+ return { state: "deferred", restored: false, detail: `nothing restored: ${skip}` };
35530
+ }
35444
35531
  if (!creds || !creds.endpoint || !creds.sessionId || !creds.token) {
35445
35532
  return {
35446
35533
  state: "deferred",
@@ -35479,7 +35566,7 @@ async function restorePresence(deps) {
35479
35566
  const why = {
35480
35567
  "same-session": `That bridge serves this same agent session (${session})`,
35481
35568
  "no-session": `Neither bridge records an agent session (codex, gemini), so it cannot be told whether that bridge is this agent's previous one or another agent's. ${take}`,
35482
- "other-session": `That bridge serves another agent session (${servedBy.sessionId}; ours: ${session}). Two agents in one folder are not supported yet: a second agent needs its own folder and peer name. ${take}`,
35569
+ "other-session": `That bridge serves another agent session (${servedBy.sessionId}; ours: ${session}). ` + heldNameAdvice(peerId, { sessionId: servedBy.sessionId }, { sessionId: session }),
35483
35570
  unknown: `Its agent session cannot be compared with this one (that bridge: ${servedBy.sessionId ?? "not recorded"}; this bridge: ${session ?? "not known"}): it may be this agent's own other bridge of an older version, or another agent's. ${take}`
35484
35571
  };
35485
35572
  return {
@@ -35789,7 +35876,9 @@ var init_claude_preflight = __esm({
35789
35876
  { flag: "--dangerously-skip-permissions", usedFor: "unattended auto-relay execution" },
35790
35877
  { flag: "--no-session-persistence", usedFor: "don't pollute the resumed session transcript" },
35791
35878
  { flag: "--strict-mcp-config", usedFor: "isolate MCP config of the resumed process" },
35792
- { flag: "--output-format", usedFor: "structured JSON reply envelope (Slice 3 capture)" }
35879
+ { flag: "--output-format", usedFor: "structured JSON reply envelope (Slice 3 capture)" },
35880
+ { flag: "--tools", usedFor: "read-only wake: Read, Glob and Grep only (COG-86)" },
35881
+ { flag: "--append-system-prompt", usedFor: "tell a read-only wake it cannot change anything (COG-86)" }
35793
35882
  ];
35794
35883
  }
35795
35884
  });
@@ -36019,7 +36108,9 @@ async function restorePeerPresence(attempt = 0) {
36019
36108
  },
36020
36109
  agentSessionId: () => agentSessionId(),
36021
36110
  startedByWakeTurn: () => insideCodexWakeTurn(process.pid, ps()),
36022
- loadCredentials: () => loadCredentials(),
36111
+ // COG-80: the identity THIS agent saved, not whichever agent registered in this folder last.
36112
+ loadCredentials: () => restoreCandidate(),
36113
+ restoreSkip: () => lastRestoreSkip,
36023
36114
  cwd: () => process.cwd(),
36024
36115
  recheckSeconds: Math.round(standbyRecheckMs() / 1e3),
36025
36116
  goOnline: async ({ peerId, endpoint, sessionId, token, cwd, label, peerSecret }) => {
@@ -36027,7 +36118,7 @@ async function restorePeerPresence(attempt = 0) {
36027
36118
  await reinitCloudBackend(endpoint, sessionId, token);
36028
36119
  const backend = getBackend();
36029
36120
  await backend.registerPeer(...presenceRegisterArgs({ peerId, sessionId, cwd, label, peerSecret }));
36030
- const slot = await loadCredentials();
36121
+ const slot = await restoreCandidate();
36031
36122
  if (!stillCurrent() || slot?.sessionId !== sessionId || slot?.endpoint !== endpoint) {
36032
36123
  throw new Error(PRESENCE_SUPERSEDED);
36033
36124
  }
@@ -36159,9 +36250,20 @@ async function resolvePendingLabel() {
36159
36250
  logger.warn(`Post-connect label upgrade failed (non-fatal): ${err}`);
36160
36251
  }
36161
36252
  }
36253
+ async function restoreCandidate() {
36254
+ const choice = chooseRestoreCredentials(
36255
+ await loadCredentials(),
36256
+ await loadPeerRecords(),
36257
+ agentSessionId(),
36258
+ getConfig().COGENT_PLATFORM
36259
+ );
36260
+ lastRestoreSkip = choice.skip;
36261
+ return choice.creds;
36262
+ }
36162
36263
  async function reinitCloudBackend(endpoint, sessionId, token) {
36163
36264
  const config2 = getConfig();
36164
36265
  cloudHttpClient = new HttpClient(endpoint, token);
36266
+ setCurrentCloudConnection({ endpoint, sessionId, token });
36165
36267
  const newBackend = new HttpBackend(cloudHttpClient, sessionId);
36166
36268
  resetBackend();
36167
36269
  initBackend(newBackend);
@@ -36266,6 +36368,7 @@ async function runStartup() {
36266
36368
  cloudInbox = new MessageInbox();
36267
36369
  bindInboxNotifier();
36268
36370
  cloudHttpClient = new HttpClient(effectiveEndpoint, effectiveToken);
36371
+ setCurrentCloudConnection({ endpoint: effectiveEndpoint, sessionId: effectiveSessionId, token: effectiveToken });
36269
36372
  cloudWsClient = createCloudWsClient(
36270
36373
  effectiveEndpoint,
36271
36374
  effectiveSessionId,
@@ -36300,7 +36403,7 @@ async function runPreflights() {
36300
36403
  await preflightClaude(config2.COGENT_CLAUDE_PATH);
36301
36404
  }
36302
36405
  }
36303
- var UUID_V4_RE, execFileAsync3, PERSIST_PATH, legacyWarnEmitted, cloudWsClient, cloudInbox, cloudHttpClient, cloudModeActive, inboxNotifier, notifierBoundTo, reRegistrationInProgress, presenceRetry, ignoredHolders, presenceSupersedeBound, PRESENCE_SUPERSEDED, pendingLabelResolve;
36406
+ var UUID_V4_RE, execFileAsync3, PERSIST_PATH, legacyWarnEmitted, cloudWsClient, cloudInbox, cloudHttpClient, cloudModeActive, inboxNotifier, notifierBoundTo, reRegistrationInProgress, presenceRetry, ignoredHolders, presenceSupersedeBound, PRESENCE_SUPERSEDED, pendingLabelResolve, lastRestoreSkip;
36304
36407
  var init_startup = __esm({
36305
36408
  "src/startup.ts"() {
36306
36409
  "use strict";
@@ -36495,7 +36598,7 @@ function registerRegisterPeerTool(server) {
36495
36598
  new BridgeError(
36496
36599
  "PEER_HELD_IN_FOLDER" /* PEER_HELD_IN_FOLDER */,
36497
36600
  `'${peerId}' is held by another live connection in this folder (bridge pid ${holder.pid}, version ${registryText(holder.version)}); this bridge has NOT registered`,
36498
- heldNameAdvice(peerId)
36601
+ heldNameAdvice(peerId, holder, { sessionId: own, platform: config2.COGENT_PLATFORM })
36499
36602
  )
36500
36603
  );
36501
36604
  }
@@ -36612,7 +36715,9 @@ function registerRegisterPeerTool(server) {
36612
36715
  let storedPeerSecret = argPeerSecret;
36613
36716
  if (storedPeerSecret === void 0 && isCloudMode()) {
36614
36717
  try {
36615
- storedPeerSecret = (await loadCredentials())?.peerSecret;
36718
+ const own = await loadPeerRecord(peerId);
36719
+ const folder = own ? null : await loadCredentials();
36720
+ storedPeerSecret = own?.peerSecret ?? (!folder?.peerId || folder.peerId === peerId ? folder?.peerSecret : void 0);
36616
36721
  } catch {
36617
36722
  storedPeerSecret = void 0;
36618
36723
  }
@@ -36637,7 +36742,8 @@ function registerRegisterPeerTool(server) {
36637
36742
  let recovered;
36638
36743
  if (/PEER_NOT_YOURS/.test(msg) && isCloudMode()) {
36639
36744
  try {
36640
- const creds = await loadCredentials();
36745
+ const folderCreds = await loadCredentials();
36746
+ const creds = await loadPeerRecord(peerId) ?? (!folderCreds?.peerId || folderCreds.peerId === peerId ? folderCreds : null);
36641
36747
  if (creds?.ownerToken && creds.ownerToken !== creds.token && creds.endpoint && creds.sessionId) {
36642
36748
  logger.warn(
36643
36749
  `register_peer: '${peerId}' is owned by an earlier token (its peerSecret was lost by a pre-3.23.5 re-join). Re-proving ownership with the stored owner token.`
@@ -36659,7 +36765,7 @@ function registerRegisterPeerTool(server) {
36659
36765
  recovered = await res.json();
36660
36766
  const secret = recovered?.peerSecret;
36661
36767
  if (secret) {
36662
- await saveCredentials({ ...creds, peerId, peerSecret: secret, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
36768
+ await savePeerRecord({ ...creds, peerId, peerSecret: secret, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
36663
36769
  logger.info(`register_peer: ownership reclaimed for '${peerId}'; a fresh peerSecret is stored.`);
36664
36770
  }
36665
36771
  }
@@ -36693,13 +36799,24 @@ function registerRegisterPeerTool(server) {
36693
36799
  try {
36694
36800
  const creds = await loadCredentials();
36695
36801
  const issuedSecret = peer?.peerSecret;
36696
- if (creds && (creds.peerId !== peerId || issuedSecret && creds.peerSecret !== issuedSecret)) {
36697
- await saveCredentials({
36698
- ...creds,
36699
- peerId,
36700
- ...issuedSecret ? { peerSecret: issuedSecret } : {},
36802
+ const platform = getConfig().COGENT_PLATFORM;
36803
+ const agent = { agentSession: (platform === "cc" ? agentSessionId() : void 0) ?? sessionId, platform };
36804
+ if (creds) {
36805
+ const channel = getCurrentCloudConnection() ?? { endpoint: creds.endpoint, sessionId: creds.sessionId, token: creds.token };
36806
+ const mine = peerCredentials(creds, peerId);
36807
+ const sameChannel = !!mine && mine.sessionId === channel.sessionId && mine.endpoint === channel.endpoint;
36808
+ const secret = issuedSecret ?? (sameChannel ? mine.peerSecret : void 0);
36809
+ const ownerToken = sameChannel ? mine.ownerToken ?? (mine.token !== channel.token ? mine.token : void 0) : void 0;
36810
+ const label2 = creds.sessionId === channel.sessionId ? creds.label : sameChannel ? mine.label : void 0;
36811
+ const entry = {
36812
+ ...channel,
36813
+ ...label2 ? { label: label2 } : {},
36814
+ ...agent,
36815
+ ...secret ? { peerSecret: secret } : {},
36816
+ ...ownerToken ? { ownerToken } : {},
36701
36817
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
36702
- });
36818
+ };
36819
+ await saveCredentials({ ...entry, peerId, peers: { ...creds.peers ?? {}, [peerId]: entry } });
36703
36820
  }
36704
36821
  } catch (err) {
36705
36822
  logger.debug(
@@ -37819,7 +37936,8 @@ function registerDeregisterPeerTool(server) {
37819
37936
  const backend = getBackend();
37820
37937
  let ownershipProof;
37821
37938
  try {
37822
- ownershipProof = (await loadCredentials())?.peerSecret;
37939
+ const folder = await loadCredentials();
37940
+ ownershipProof = (await loadPeerRecord(peerId))?.peerSecret ?? (!folder?.peerId || folder.peerId === peerId ? folder?.peerSecret : void 0);
37823
37941
  } catch {
37824
37942
  }
37825
37943
  const { removed, mailboxDeprovisioned } = await backend.deregisterPeer(peerId, ownershipProof);
@@ -37840,6 +37958,7 @@ function registerDeregisterPeerTool(server) {
37840
37958
  await saveCredentials({ ...rest, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
37841
37959
  logger.info("cleared stored peerId after deregister", { peerId });
37842
37960
  }
37961
+ await deletePeerRecord(peerId);
37843
37962
  } catch (err) {
37844
37963
  logger.warn("failed to clear stored peerId after deregister", { error: err });
37845
37964
  }
@@ -84,6 +84,27 @@ export function buildAgentSessionRecord(payload, nowIso) {
84
84
  };
85
85
  }
86
86
 
87
+ /**
88
+ * Which saved identity this hook's agent is (COG-80). MUST follow src/cloud/credential-store.ts
89
+ * chooseRestoreCredentials + peerCredentials. `folder` is the folder file; its `peers` map holds one
90
+ * entry per registered peer. The top-level fields name the last-registered peer and are that peer's
91
+ * newest write. Returns null when the folder's names provably belong to other agents (stay silent
92
+ * rather than act as the other agent in the folder).
93
+ */
94
+ export function chooseHookCredentials(folder, sessionId, platform) {
95
+ if (!folder) return null;
96
+ const records = Object.entries(folder.peers || {}).map(([peerId, e]) => ({ ...e, peerId }));
97
+ if (records.length === 0) return folder;
98
+ const resolve = (peerId) => (folder.peerId === peerId ? folder : { ...folder.peers[peerId], peerId });
99
+ const mine = sessionId ? records.filter((r) => r.agentSession === sessionId) : [];
100
+ if (mine.length > 0) return resolve(mine[0].peerId);
101
+ const sameKind = records.filter((r) => r.platform === platform);
102
+ if (sameKind.length === 1) return resolve(sameKind[0].peerId);
103
+ if (sameKind.length > 1 && folder.peerId && sameKind.some((r) => r.peerId === folder.peerId)) return folder;
104
+ if (sameKind.length === 0 && folder.peerId && !records.some((r) => r.peerId === folder.peerId)) return folder;
105
+ return null;
106
+ }
107
+
87
108
  function isForMe(msg, me, scope) {
88
109
  if (msg.toPeerId === me) return scope.directed;
89
110
  if (msg.toPeerId === "*") {
@@ -20,6 +20,8 @@ import {
20
20
  activeInFlightIds,
21
21
  agentSessionRecordPath,
22
22
  buildAgentSessionRecord,
23
+ chooseHookCredentials,
24
+ rootFromTranscriptPath,
23
25
  } from "./check-on-stop.lib.mjs";
24
26
 
25
27
  const HTTP_TIMEOUT_MS = 5000; // < the 8s hook timeout; only bites on a degraded relay
@@ -143,7 +145,9 @@ async function main() {
143
145
  if (envOff(env.COGENT_CHECK_ON_STOP)) return process.exit(0); // opt-out
144
146
  const cwd = input.cwd || process.cwd();
145
147
 
146
- const creds = await readJson(credPath(cwd, env));
148
+ // COG-80: this agent's own record — the folder file names whichever agent registered last.
149
+ const creds = chooseHookCredentials(await readJson(credPath(cwd, env)), input.session_id,
150
+ rootFromTranscriptPath(input.transcript_path) ? "cc" : "codex");
147
151
  if (!creds || !creds.endpoint || !creds.sessionId || !creds.token) {
148
152
  return process.exit(0); // not registered in cloud mode → nothing to do
149
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essentialai/cogent-plugin",
3
- "version": "3.25.8",
3
+ "version": "3.25.9",
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",