@cohortapp/agent-sdk 2.18.19 → 2.18.20

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.
@@ -120,7 +120,7 @@ export function yamlScalar(s) {
120
120
  const QUOTED_SCALAR_FIELDS = [
121
121
  "id", "service", "channel", "channel_id", "sender", "sender_id",
122
122
  "sender_privilege", "timestamp", "subject", "thread_id", "raw_ref", "kind",
123
- "event_kind", "direct_reason", "scope_id", "channel_kind", "channel_type",
123
+ "event_kind", "author_kind", "direct_reason", "scope_id", "channel_kind", "channel_type",
124
124
  "event_type",
125
125
  ];
126
126
 
@@ -279,6 +279,22 @@ export function eventToInboxItem(ev) {
279
279
  item.event_kind = sanitizeYamlField(chainKind);
280
280
  }
281
281
 
282
+ // THE AUTHOR'S KIND ("HUMAN" | "AI_AGENT"), by the same rule and for a
283
+ // measured reason. `lib/org/work-ledger.mjs` sends `authorKind` to hq's board
284
+ // gate, whose clause 2 refuses a row for an agent author; this converter
285
+ // enumerates its fields, so before this line the kind `project.mjs` resolved
286
+ // died HERE and the ledger's `undefined` became the string "HUMAN". Every seat
287
+ // therefore asserted a human author for every ask — which is how an agent's
288
+ // own "Archived, logged." minted board rows on 2026-09-30.
289
+ //
290
+ // Read off the event, not the org projection: `project.mjs` stamps it at top
291
+ // level next to `sender`, and only when `facts.memberKinds` PROVED it. An
292
+ // unknown kind stays absent rather than defaulting, because a wrong assertion
293
+ // here would cost a legitimate row.
294
+ if (ev.author_kind === "HUMAN" || ev.author_kind === "AI_AGENT") {
295
+ item.author_kind = ev.author_kind;
296
+ }
297
+
282
298
  // THE DIRECTEDNESS REASON, for the same reason and by the same rule.
283
299
  //
284
300
  // `lib/org/inbound/directedness.mjs` does real work to answer WHY an event is
@@ -36,6 +36,7 @@
36
36
  "use strict";
37
37
 
38
38
  import { SURFACES } from "./surfaces.mjs";
39
+ import { authorKindOf } from "./directedness.mjs";
39
40
 
40
41
  /** Blank-safe string. */
41
42
  function s(v) {
@@ -88,6 +89,8 @@ export function toMessageEvent(o = {}) {
88
89
  const subject = subjectDetail ? `${def.subject}: ${subjectDetail}` : def.subject;
89
90
 
90
91
  const from = resolveFrom(hydrated, c, facts);
92
+ // "" when the directory read degraded — see the note at `author_kind` below.
93
+ const authorKind = authorKindOf(facts, from.id);
91
94
  const scope = scopeFor(verdict.surface, channelKind, def.scope);
92
95
 
93
96
  const ev = {
@@ -114,6 +117,26 @@ export function toMessageEvent(o = {}) {
114
117
  channel_id: channelId,
115
118
  channel_label: channelLabel,
116
119
  sender: s(from.name || from.id),
120
+ // THE AUTHOR'S KIND, when the directory PROVES it.
121
+ //
122
+ // hq's board gate (`src/server/work/threshold.ts` clause 2) refuses a row
123
+ // for an AI_AGENT author — "agent↔agent chatter is coordination, not a work
124
+ // request" — and that clause was UNREACHABLE from this plane for as long as
125
+ // it has existed. Nothing on a seat machine ever set an author kind, so
126
+ // `lib/org/work-ledger.mjs` turned the resulting `undefined` into the string
127
+ // "HUMAN" and every seat asserted a human author for every ask, always. On
128
+ // 2026-09-30 that let each seat's own closing status line ("Archived,
129
+ // logged.") mint a board row in an agent↔agent DM, a row every eighty
130
+ // seconds for eleven turns, titled with the raw sentence.
131
+ //
132
+ // `facts.memberKinds` is the one `member.list` the facts pass already makes,
133
+ // keyed by member id AND slug. OMITTED, never guessed, when that read
134
+ // degraded (`authorKindOf` returns "") — an absent field keeps the previous
135
+ // shape exactly, and hq now resolves the kind server-side from the message
136
+ // row anyway, so a blank here costs nothing and a WRONG value here would
137
+ // cost the refusal. This field is the poll lane's defence in depth, not the
138
+ // load-bearing guard.
139
+ ...(authorKind ? { author_kind: authorKind } : {}),
117
140
  is_reply: Boolean(hydrated.isReply),
118
141
  thread_context: hydrated.threadContext != null ? hydrated.threadContext : null,
119
142
  // VOICE_NOTE_OVERLAY (2026-09-26): attachment metadata from the hydrator rides on the event so
@@ -204,6 +204,34 @@ export async function recordWorkStep(a = {}) {
204
204
  // refused by name, and a seat that got its local classifier wrong loses a
205
205
  // well-shaped prompt rather than opening a row it should not have.
206
206
  collective: isRollCallItem(a.item),
207
+ // THE AUTHOR'S KIND, and WHAT THIS LINE CAN AND CANNOT SEE.
208
+ //
209
+ // hq's `work/threshold.ts` clause 2 refuses a row for an AI_AGENT author.
210
+ // Until 2026-09-30 this line could only ever produce "HUMAN": nothing on a
211
+ // seat machine set `author_kind`, so the field was structurally
212
+ // unreachable and the fallback below was the only branch that ran. Every
213
+ // seat asserted a human author for every ask, the clause never fired, and
214
+ // each seat's own closing status line ("Archived, logged.") minted a board
215
+ // row in an agent↔agent DM — one every eighty seconds for eleven turns.
216
+ //
217
+ // `lib/org/inbound/project.mjs` now stamps the kind from
218
+ // `facts.memberKinds`, and it survives the YAML round trip
219
+ // (`lib/channels/inbox-item.mjs` → `scripts/poller/utils.mjs` →
220
+ // `scripts/poller/inbox-scan-poller.mjs`);
221
+ // `lib/org/inbound/author-kind.test.mjs` pins that end to end, because an
222
+ // unreachable field is exactly the drift that made this line lie for as
223
+ // long as it existed. (That citation read `inbound-author-kind.test.mjs`,
224
+ // a file that has never existed — the same prose-vs-executable drift this
225
+ // comment is warning about, inside the comment whose job is to stop it.
226
+ // Written as a REPO-ROOT-RELATIVE PATH now, so `grep` finds it or fails.)
227
+ //
228
+ // STILL NOT THE LOAD-BEARING GUARD, deliberately. The fallback is "HUMAN"
229
+ // rather than "unknown" because a degraded directory read must not cost a
230
+ // real ask its row, and hq re-resolves the kind SERVER-SIDE from the
231
+ // message row regardless of what this says — an authenticated caller may
232
+ // not assert that a message another member wrote was written by a person.
233
+ // This is defence in depth for the poll lane: right when we know, silent
234
+ // when we do not, and never the thing the invariant rests on.
207
235
  authorKind: a.item && a.item.author_kind === "AI_AGENT" ? "AI_AGENT" : "HUMAN",
208
236
  deferred: a.deferred === true,
209
237
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cohortapp/agent-sdk",
3
- "version": "2.18.19",
3
+ "version": "2.18.20",
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": {
@@ -124,6 +124,15 @@ export function parseInboxItemYaml(body) {
124
124
  // obligation binds identically on the push and poll lanes.
125
125
  const eventKind = scalar("event_kind");
126
126
  if (eventKind) item.event_kind = eventKind;
127
+
128
+ // THE AUTHOR'S KIND, read back for `lib/org/work-ledger.mjs` → hq's board
129
+ // gate. Only the two values the gate understands are accepted: an item whose
130
+ // `author_kind` is junk is treated as UNKNOWN (absent) rather than passed
131
+ // through, so a malformed line cannot assert an author kind hq would trust.
132
+ const authorKind = scalar("author_kind");
133
+ if (authorKind === "HUMAN" || authorKind === "AI_AGENT") {
134
+ item.author_kind = authorKind;
135
+ }
127
136
  // The directedness verdict's own reason, and the surface's subject id. Same
128
137
  // rule as `event_kind`: attached only when the writer knew, so a non-org item
129
138
  // keeps exactly its old shape. `intake.fromInboxItem` PREFERS `direct_reason`
@@ -178,7 +178,7 @@ function renameLiveInbox(item, service, agentRoot, suffix) {
178
178
  const YAML_SAFE_SCALARS = [
179
179
  "id", "service", "channel", "channel_id", "sender", "sender_id",
180
180
  "sender_privilege", "timestamp", "subject", "thread_id", "raw_ref", "kind",
181
- "event_kind", "direct_reason", "scope_id", "channel_kind", "channel_type",
181
+ "event_kind", "author_kind", "direct_reason", "scope_id", "channel_kind", "channel_type",
182
182
  "event_type",
183
183
  ];
184
184
 
@@ -255,6 +255,19 @@ raw_ref: "${item.raw_ref || ""}"
255
255
  yaml += `event_kind: "${item.event_kind}"\n`;
256
256
  }
257
257
 
258
+ // THE AUTHOR'S KIND, written only when the projection PROVED it, by the same
259
+ // "absent means unknown" rule as `kind` and `event_kind` above.
260
+ //
261
+ // This writer enumerates its fields, as the note above says — so the kind that
262
+ // `lib/org/inbound/project.mjs` resolves from `facts.memberKinds` died at this
263
+ // boundary, and `lib/org/work-ledger.mjs` then told hq's board gate "HUMAN"
264
+ // for every ask any seat ever saw. hq's gate refuses a row for an AI_AGENT
265
+ // author; unreachable from the poll lane, it could not refuse the agent↔agent
266
+ // status lines that minted a board row every eighty seconds on 2026-09-30.
267
+ if (item.author_kind) {
268
+ yaml += `author_kind: "${item.author_kind}"\n`;
269
+ }
270
+
258
271
  // THE DIRECTEDNESS REASON and THE SURFACE'S SUBJECT ID, for exactly the reason
259
272
  // above: this writer enumerates its fields, so anything not named here dies at
260
273
  // the YAML boundary and the poll lane behaves differently from the push lane.