@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 {
|
|
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 });
|
package/lib/org/ui-parity.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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": {
|