@cohortapp/agent-sdk 2.18.5 → 2.18.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.
@@ -939,4 +939,94 @@ export default {
939
939
  matchesMyName,
940
940
  taskIdFromFileKey,
941
941
  surfaceDef,
942
+ isMissedHumanMessage,
943
+ missedHumanRecord,
942
944
  };
945
+
946
+ // ─────────────────────────────────────────────────────────────────────────────
947
+ // A PERSON'S MESSAGE THAT THIS SEAT DROPPED
948
+ // ─────────────────────────────────────────────────────────────────────────────
949
+
950
+ /**
951
+ * Drop reasons that are NOT a person going unanswered, even when a human wrote
952
+ * the message. Each one says the room was never this seat's to read or the
953
+ * item was never a message at all, so counting it would bury the reasons that
954
+ * matter under noise every tick produces.
955
+ *
956
+ * `own_message` is here for the obvious reason; the membership/visibility ones
957
+ * because a room the seat cannot see is not a room it declined to answer in.
958
+ */
959
+ const NOT_A_MISSED_PERSON = Object.freeze([
960
+ "own_message",
961
+ "channel_not_visible",
962
+ "not_a_member",
963
+ "membership_unknown",
964
+ "channel_kind_unknown",
965
+ "surface_disabled",
966
+ ]);
967
+
968
+ /**
969
+ * PURE. Did this seat just drop a message a PERSON wrote in a room the seat
970
+ * belongs to?
971
+ *
972
+ * ── WHY THIS EXISTS ──────────────────────────────────────────────────────────
973
+ * `pullWideInbound` counts every drop into `stats.dropped[reason]` — a tally,
974
+ * by reason, with no identity and, unless something else happened that tick, no
975
+ * log line at all. That tally is why the 2026-09-21 #general roll-call took a
976
+ * reconstruction to explain: thirteen seats each incremented
977
+ * `dropped.no_mentions_not_threaded` by one and none of them said WHICH message
978
+ * or that a human had written it. A counter cannot be audited after the fact
979
+ * and cannot be correlated across seats.
980
+ *
981
+ * A person's message that this seat decided not to answer is a different class
982
+ * of event from ambient chatter it correctly ignored, and it is the only class
983
+ * anyone ever asks about afterwards. So it is named individually, once, at
984
+ * WARN — the same treatment `hydrate` already gives a body it could not read.
985
+ *
986
+ * Deliberately narrow, for the same reason the surface list is: this fires only
987
+ * for a MESSAGE-topic candidate, authored by a member the DIRECTORY says is
988
+ * HUMAN (never assumed from the absence of evidence), in a room whose
989
+ * membership this seat has PROVEN. Everything else is either not a person, not
990
+ * a room, or not knowable — and an over-broad warn line is a line operators
991
+ * learn to skip.
992
+ *
993
+ * @param {Candidate} cand
994
+ * @param {{directed:boolean, reason?:string}} verdict the resolved verdict
995
+ * @param {import("./facts.mjs").Facts} facts
996
+ * @param {string} me this seat's member id
997
+ * @returns {boolean}
998
+ */
999
+ export function isMissedHumanMessage(cand, verdict, facts, me) {
1000
+ if (!cand || !verdict || verdict.directed) return false;
1001
+ if (cand.topic !== "message") return false;
1002
+ const reason = String(verdict.reason || "");
1003
+ if (NOT_A_MISSED_PERSON.includes(reason)) return false;
1004
+ const channelId = cand.ids && cand.ids.channelId;
1005
+ if (!channelId) return false;
1006
+ // A room whose membership is PROVEN, not merely visible: a public channel the
1007
+ // seat can read but has not joined does not address it.
1008
+ const members = asSet(facts && facts.memberChannelIds);
1009
+ if (!has(members, channelId)) return false;
1010
+ const author = cand.actor;
1011
+ if (!author || (me && String(author) === String(me))) return false;
1012
+ // Proven human. `authorKindOf` returns "" when the directory read degraded,
1013
+ // and an unknown author is NOT reported as a missed person — a degraded tick
1014
+ // would otherwise warn about every message in every room.
1015
+ return authorKindOf(facts, author) === "HUMAN";
1016
+ }
1017
+
1018
+ /**
1019
+ * PURE. The redaction-safe record of one missed person's message. No body, no
1020
+ * prose: the ids an operator needs to go and look, plus the reason this seat
1021
+ * gave. Mirrors the payload shape hq's `responder.silence` rows carry, so the
1022
+ * two planes describe the same event in the same words.
1023
+ */
1024
+ export function missedHumanRecord(cand, verdict) {
1025
+ return {
1026
+ seq: cand && cand.seq != null ? cand.seq : null,
1027
+ messageId: (cand && cand.ids && cand.ids.messageId) || cand?.entityId || null,
1028
+ channelId: (cand && cand.ids && cand.ids.channelId) || null,
1029
+ author: (cand && cand.actor) || null,
1030
+ reason: String((verdict && verdict.reason) || "unknown"),
1031
+ };
1032
+ }
@@ -55,7 +55,12 @@
55
55
  "use strict";
56
56
 
57
57
  import { read as clientRead } from "../client.mjs";
58
- import { classifyEvent, resolveDirected } from "./directedness.mjs";
58
+ import {
59
+ classifyEvent,
60
+ resolveDirected,
61
+ isMissedHumanMessage,
62
+ missedHumanRecord,
63
+ } from "./directedness.mjs";
59
64
  import { resolveFacts, DEFAULT_LIMITS } from "./facts.mjs";
60
65
  import { hydrate } from "./hydrate.mjs";
61
66
  import { toMessageEvent } from "./project.mjs";
@@ -106,6 +111,11 @@ export async function pullWideInbound(o = {}) {
106
111
  delivered: 0,
107
112
  bySurface: {},
108
113
  dropped: {},
114
+ // EVERY person's message this tick decided not to answer, named. See
115
+ // `directedness.mjs#isMissedHumanMessage`: `dropped` is a tally by reason
116
+ // and cannot say WHICH message or that a human wrote it, which is the only
117
+ // question anyone asks after a room goes quiet.
118
+ unanswered: [],
109
119
  degraded: [],
110
120
  calls: 0,
111
121
  reads: 0,
@@ -195,6 +205,18 @@ export async function pullWideInbound(o = {}) {
195
205
  const verdict = resolveDirected(cand, me, facts, { enabled, meAliases });
196
206
  if (!verdict.directed) {
197
207
  stats.dropped[verdict.reason] = (stats.dropped[verdict.reason] || 0) + 1;
208
+ // A PERSON's message this seat is dropping is not ambient chatter, and a
209
+ // counter is not a record of it. Name it — once, at WARN, with the ids —
210
+ // so "I posted and nobody replied" is answerable from this machine's own
211
+ // log instead of by reasoning backwards from thirteen tallies.
212
+ if (isMissedHumanMessage(cand, verdict, facts, me)) {
213
+ const rec = missedHumanRecord(cand, verdict);
214
+ stats.unanswered.push(rec);
215
+ log(
216
+ "warn",
217
+ `[inbound] NOT answering a person: message ${rec.messageId || cand.seq} in channel ${rec.channelId} from ${rec.author} — ${rec.reason}`
218
+ );
219
+ }
198
220
  continue;
199
221
  }
200
222
  directed.push({ cand, verdict });
@@ -3317,7 +3317,19 @@ export function messagingSearch(params, o = {}) {
3317
3317
  * message ({ messageId }) or a channel over a window ({ channelId,
3318
3318
  * windowDays? }); one of the two is required. Each row carries the operator
3319
3319
  * reason (human_mentioned | addressed_to_human | nobody_elected |
3320
- * no_responders) and its text. The agent-plane twin of the "Unanswered
3320
+ * no_responders | handed_to_daemons) and its text; a `handed_to_daemons` row
3321
+ * also carries `daemonDriven`, the seats hq stood down for — every eligible
3322
+ * colleague in the room was answering from its OWN machine, so hq deliberately
3323
+ * said nothing and a room that then heard nothing is a fault on the DAEMON
3324
+ * plane, with the seat list already in hand.
3325
+ *
3326
+ * The same sentence is NOT repeated in `protocol.mjs`: that file is pinned by
3327
+ * `protocol.checksum` to hq's vendored copy, so even a comment there is a
3328
+ * cross-repo re-vendor (`scripts/sync-protocol.mjs`). The vocabulary's source
3329
+ * of truth is hq `src/server/llm-responder/silence-verdict.ts`; this JSDoc is
3330
+ * the agent-facing restatement of it.
3331
+ *
3332
+ * The agent-plane twin of the "Unanswered
3321
3333
  * messages" panel on /settings/ai — same reader, same decoder.
3322
3334
  * @param {object} params - { channelId?, messageId?, windowDays?, limit? }
3323
3335
  * @param {object} o - { base, token, fetchImpl? }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cohortapp/agent-sdk",
3
- "version": "2.18.5",
3
+ "version": "2.18.6",
4
4
  "description": "Cohort Agent SDK — autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
5
5
  "type": "module",
6
6
  "bin": {