@cohortapp/agent-sdk 2.18.18 → 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.
- package/lib/assurance/first-reply.mjs +62 -0
- package/lib/channels/inbox-item.mjs +17 -1
- package/lib/org/inbound/project.mjs +23 -0
- package/lib/org/work-ledger.mjs +28 -0
- package/lib/session/ask-ledger.mjs +22 -0
- package/package.json +1 -1
- package/scripts/poller/inbox-scan-poller.mjs +9 -0
- package/scripts/poller/utils.mjs +14 -1
|
@@ -421,3 +421,65 @@ export default {
|
|
|
421
421
|
firstReplyPlan,
|
|
422
422
|
stampFirstReplyPlan,
|
|
423
423
|
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The AGENT STATUS NOTICE family — a seat narrating its own progress, failure
|
|
427
|
+
* or recovery to a room.
|
|
428
|
+
*
|
|
429
|
+
* ── WHY THIS EXISTS, AND WHY IT IS SEPARATE FROM THE RETIRED REGISTER ───────
|
|
430
|
+
*
|
|
431
|
+
* The register above answers "may this seat SEND this line?". This answers a
|
|
432
|
+
* different question: "is this line, arriving from ANOTHER seat, a piece of
|
|
433
|
+
* work?" — and the answer is always no. A colleague saying how their own task
|
|
434
|
+
* went is a status report. It is not an ask, and it must never mint a board row.
|
|
435
|
+
*
|
|
436
|
+
* On 2026-09-30 it did. `assurance.isOwnNarratorText` already recognised these
|
|
437
|
+
* notices, but only for SELF-echo — a seat ignoring its own words. Nothing
|
|
438
|
+
* stopped Jacob's recovery notice reaching Candace as an ordinary message, and
|
|
439
|
+
* with no `--title` on the claim the row took the ask's own first sentence as
|
|
440
|
+
* its title. Production, in three hours:
|
|
441
|
+
*
|
|
442
|
+
* "Here's where I got to: Archived, logged."
|
|
443
|
+
* "Here's where I got to: Declined, logged, no reply."
|
|
444
|
+
* "Here's where I got to: Here's where I got to: Queued, not archived …"
|
|
445
|
+
*
|
|
446
|
+
* Sixty-four rows, several seats each opening and closing the same non-task,
|
|
447
|
+
* and the doubled title is the output of the path re-entering its own input —
|
|
448
|
+
* a row minted from a report ABOUT a row. One of them read "eleventh turn".
|
|
449
|
+
*
|
|
450
|
+
* FROZEN LITERALS for the same reason the register above is frozen: a notice
|
|
451
|
+
* arriving from a seat that has not upgraded yet must still be recognised long
|
|
452
|
+
* after the local composer that produced it has changed.
|
|
453
|
+
*/
|
|
454
|
+
const STATUS_NOTICE_PATTERNS = Object.freeze([
|
|
455
|
+
// composeSilentSuccess — the recovery notice, with or without an excerpt.
|
|
456
|
+
// The excerpt is unbounded prose, so this anchors on the head alone.
|
|
457
|
+
/^finished this but didn'?t get a reply out to you\s*[—–-]\s*my fault\b/i,
|
|
458
|
+
// composeInterrupted.
|
|
459
|
+
/^heads up\s*[—–-]\s*my session on this was interrupted\b/i,
|
|
460
|
+
// notice-voice's generated failure/progress lines, which name the topic:
|
|
461
|
+
// The work on "X" broke before it finished: …
|
|
462
|
+
// The work on "X" didn't get through: …
|
|
463
|
+
/^the work (on ".+?"|you asked for)\s+(broke|didn'?t|never|has)\b/i,
|
|
464
|
+
// The board-status shape that closed the loop on itself.
|
|
465
|
+
/^here'?s where i got to:/i,
|
|
466
|
+
]);
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Is this body a seat narrating its own work, rather than asking for any?
|
|
470
|
+
*
|
|
471
|
+
* Judged per PARAGRAPH like the register above, because these notices are
|
|
472
|
+
* routinely concatenated with an excerpt or with each other.
|
|
473
|
+
*
|
|
474
|
+
* @param {unknown} body
|
|
475
|
+
* @returns {boolean}
|
|
476
|
+
*/
|
|
477
|
+
export function isAgentStatusNotice(body) {
|
|
478
|
+
const paras = String(body == null ? "" : body).split(/\n\s*\n/);
|
|
479
|
+
for (const p of paras) {
|
|
480
|
+
const line = normaliseLine(p);
|
|
481
|
+
if (!line) continue;
|
|
482
|
+
if (STATUS_NOTICE_PATTERNS.some((re) => re.test(line))) return true;
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
@@ -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
|
package/lib/org/work-ledger.mjs
CHANGED
|
@@ -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
|
};
|
|
@@ -215,6 +215,28 @@ export function ledgerStepsFor(a = {}) {
|
|
|
215
215
|
* @returns {Promise<{steps:Array<object>, results:Array<object>}>}
|
|
216
216
|
*/
|
|
217
217
|
export async function runAskLedger(a = {}) {
|
|
218
|
+
// ── A STATUS REPORT IS NOT WORK ──────────────────────────────────────────
|
|
219
|
+
//
|
|
220
|
+
// A colleague narrating how their own task went is not an ask, and a row
|
|
221
|
+
// opened from one is a row nobody requested. On 2026-09-30 this minted 64
|
|
222
|
+
// rows in three hours — titled "Here's where I got to: Archived, logged."
|
|
223
|
+
// and once "Here's where I got to: Here's where I got to: …", which is a row
|
|
224
|
+
// minted from a report ABOUT a row. Several seats each opened and closed the
|
|
225
|
+
// same non-task; one row's note read "eleventh turn".
|
|
226
|
+
//
|
|
227
|
+
// The notice family was already recognised (`assurance.isOwnNarratorText`)
|
|
228
|
+
// but only for SELF-echo, so a seat ignored its own words and happily
|
|
229
|
+
// actioned everyone else's. This applies the same recognition to the question
|
|
230
|
+
// that matters here: not "did I say this?" but "is this work?".
|
|
231
|
+
//
|
|
232
|
+
// Deliberately at the LEDGER, not at ingestion: the seat should still READ a
|
|
233
|
+
// colleague's status notice and may still reply to it. What it must not do is
|
|
234
|
+
// open a board row for it.
|
|
235
|
+
const { isAgentStatusNotice } = await import("../assurance/first-reply.mjs");
|
|
236
|
+
const body = (a.item && (a.item.content || a.item.subject)) || a.text || "";
|
|
237
|
+
if (isAgentStatusNotice(body)) {
|
|
238
|
+
return { steps: [], results: [{ tracked: false, reason: "status-notice-not-an-ask" }] };
|
|
239
|
+
}
|
|
218
240
|
const steps = ledgerStepsFor(a);
|
|
219
241
|
const results = [];
|
|
220
242
|
if (steps.length === 0) return { steps, results };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.18.
|
|
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`
|
package/scripts/poller/utils.mjs
CHANGED
|
@@ -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.
|