@essentialai/cogent-plugin 3.20.3 → 3.20.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogent",
3
- "version": "3.20.3",
3
+ "version": "3.20.5",
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",
@@ -26389,8 +26389,8 @@ var init_stdio2 = __esm({
26389
26389
  // src/constants.ts
26390
26390
  import { createRequire } from "node:module";
26391
26391
  function resolveVersion() {
26392
- if ("3.20.3") {
26393
- return "3.20.3";
26392
+ if ("3.20.5") {
26393
+ return "3.20.5";
26394
26394
  }
26395
26395
  try {
26396
26396
  const require2 = createRequire(import.meta.url);
@@ -98,6 +98,21 @@ export function selectUnanswered({ messages, me, scope }) {
98
98
  // My message (a reply or my own auto-relay echo) answers a pending item.
99
99
  // A directed reply clears that sender's oldest; a broadcast reply clears
100
100
  // only the oldest pending BROADCAST (never a directed message).
101
+ //
102
+ // COG-20: EXCEPT a meta-non-answer. When the real-time wake's capture fails, the agent
103
+ // sees its own (undelivered) answer plus this hook saying "unanswered", and resolves the
104
+ // contradiction by sending "Already answered… no duplicate sent." That is not an answer —
105
+ // but because this pairing used to clear on ANY outbound, it marked the message answered
106
+ // and silenced the alarm FOREVER, so the human only ever saw the meta-message. Skipping it
107
+ // here keeps the message pending until a real answer is sent.
108
+ //
109
+ // Same defect, second instance: rail B records its OWN failure notice ("Cogent could not
110
+ // capture a reply…" / "📨 Queued for …") as a message FROM me with success:false. Counting
111
+ // that as an answer would make C blind to exactly the miss C exists to catch. A record that
112
+ // is explicitly a failure is never an answer — check that first, it is stronger than any
113
+ // string match.
114
+ if (m.success === false || m.error) continue;
115
+ if (isMetaNonAnswer(m.message)) continue;
101
116
  if (m.toPeerId === "*") dequeueOldestBroadcast();
102
117
  else dequeueFrom(m.toPeerId);
103
118
  continue;
@@ -113,6 +128,27 @@ export function selectUnanswered({ messages, me, scope }) {
113
128
  return { items: items.map(({ _i, ...m }) => m) };
114
129
  }
115
130
 
131
+ /**
132
+ * Is this outbound of mine a "meta non-answer" — a note ABOUT answering rather than an answer?
133
+ * (COG-20)
134
+ *
135
+ * Real observed values that reached a human instead of the answer:
136
+ * "Already answered through the automatic Slack relay. No duplicate sent."
137
+ * "Already answered through the automatic Slack relay with live Jira and repository status. …"
138
+ *
139
+ * Deliberately CONSERVATIVE — it must not swallow a genuine answer that happens to contain the
140
+ * words "already answered". Two gates: the text must be SHORT (real answers carry substance; every
141
+ * observed meta-message was < 120 chars) AND match the pattern. A false negative just restores the
142
+ * old behaviour; a false positive would re-nag the agent into sending a duplicate, so short-only.
143
+ * An empty reply is always a non-answer.
144
+ */
145
+ export function isMetaNonAnswer(text) {
146
+ const t = String(text ?? "").replace(/\s+/g, " ").trim().toLowerCase();
147
+ if (!t) return true;
148
+ if (t.length > 200) return false; // long enough to carry a real answer — never suppress it
149
+ return /already (answered|replied|responded|sent|handled)|no duplicate|duplicate (not )?sent|answered (already|via|through)|responded (already|via|through)/.test(t);
150
+ }
151
+
116
152
  /** Build the { decision: "block" } reason string handed back to the agent. */
117
153
  export function buildBlockReason(items, { firstUse }) {
118
154
  const n = items.length;
@@ -120,11 +156,21 @@ export function buildBlockReason(items, { firstUse }) {
120
156
  const snippet = String(m.message || "").replace(/\s+/g, " ").slice(0, 120);
121
157
  return ` - from ${m.fromPeerId}${m.toPeerId === "*" ? " (channel)" : ""}: ${snippet}`;
122
158
  });
159
+ // COG-20: the wording matters as much as the detection. The previous text ended with "Do not
160
+ // repeat work you already did this turn", which — combined with the real-time wake prompt telling
161
+ // the agent NOT to call cogent_send_message — pushed the agent to answer with a meta-message
162
+ // ("Already answered… no duplicate sent") instead of the substance. It had in fact answered; the
163
+ // capture just never delivered it. So: state plainly that nothing was delivered, and demand the
164
+ // answer itself.
123
165
  let reason =
124
- `You have ${n} unanswered Cogent message${n === 1 ? "" : "s"} that arrived while you were busy:\n` +
166
+ `You have ${n} unanswered Cogent message${n === 1 ? "" : "s"} the channel has NO recorded reply ` +
167
+ `from you${n === 1 ? "" : " for these"}:\n` +
125
168
  lines.join("\n") +
126
- `\n\nRead the channel with cogent_get_history and reply to each with cogent_send_message ` +
127
- `(or spawn a Task sub-agent to handle them), then continue. Do not repeat work you already did this turn.`;
169
+ `\n\nIf you already composed an answer this turn, it was NOT delivered — sending it again is ` +
170
+ `correct, not a duplicate. Read the channel with cogent_get_history if you need context, then ` +
171
+ `reply with the ANSWER ITSELF via cogent_send_message (or spawn a Task sub-agent) and continue. ` +
172
+ `NEVER reply with a note about having already answered (e.g. "already answered", "no duplicate ` +
173
+ `sent") — that is not an answer, and it is all the sender will ever see.`;
128
174
  if (firstUse) {
129
175
  reason +=
130
176
  `\n\n(Cogent auto-check is on: it catches messages that arrive while you're busy. ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essentialai/cogent-plugin",
3
- "version": "3.20.3",
3
+ "version": "3.20.5",
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",