@cohortapp/agent-sdk 2.18.13 → 2.18.15
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/bin/maestro.mjs +38 -1
- package/docs/runbooks/fleet-rollout.md +58 -7
- package/docs/runbooks/recovery-and-failover.md +18 -0
- package/lib/assurance/batch.mjs +353 -0
- package/lib/assurance/first-reply.mjs +423 -0
- package/lib/assurance/notice-voice.mjs +357 -0
- package/lib/assurance/plan-note.mjs +43 -0
- package/lib/assurance/room-budget.mjs +55 -6
- package/lib/cadence-failure-class.mjs +245 -0
- package/lib/claude-bin.mjs +26 -7
- package/lib/cli/doctor-checks.mjs +149 -1
- package/lib/comms/send-gate.mjs +59 -0
- package/lib/diagnostics/alerts.mjs +33 -0
- package/lib/engine/agents/usage.mjs +45 -0
- package/lib/engine/budget.mjs +293 -29
- package/lib/engine/cli.mjs +54 -5
- package/lib/engine/loop.mjs +30 -0
- package/lib/engine/output/json.mjs +26 -0
- package/lib/engine/wire/errors.mjs +179 -0
- package/lib/engine/wire/search.mjs +44 -8
- package/lib/identity/persona.mjs +31 -2
- package/lib/org/quota.mjs +27 -0
- package/lib/session/config.mjs +4 -0
- package/lib/session/identity.mjs +71 -7
- package/lib/session/launch-failure.mjs +251 -0
- package/lib/session/resume-target.mjs +86 -0
- package/lib/telemetry/alerts.mjs +94 -0
- package/lib/telemetry/collect.mjs +155 -2
- package/lib/upgrade/pinned-drift.mjs +467 -0
- package/package.json +1 -1
- package/scaffold/config/alerts.yaml +7 -0
- package/scripts/ci/check-cadence-prompts-exist.mjs +96 -0
- package/scripts/ci/check.mjs +3 -0
- package/scripts/daemon/agent-daemon.mjs +75 -5
- package/scripts/daemon/assurance.mjs +709 -44
- package/scripts/daemon/cadence-consumer.mjs +281 -34
- package/scripts/daemon/deliver.mjs +109 -0
- package/scripts/daemon/dispatcher.mjs +21 -3
- package/scripts/daemon/inbox-deferral.mjs +102 -9
- package/scripts/daemon/session-lock.mjs +41 -1
- package/scripts/emergency-stop.sh +114 -13
- package/scripts/fleet/rollout.mjs +256 -10
- package/scripts/healthcheck.sh +131 -33
- package/scripts/local-triggers/autoupdate.sh +144 -11
- package/scripts/resume-operations.sh +101 -6
- package/scripts/session/supervisor.mjs +198 -5
|
@@ -67,6 +67,7 @@ import { classifyItem, isDirectedAtAgent } from "./classifier.mjs";
|
|
|
67
67
|
// gets a deterministic classification (no LLM triage), no holding ack, and a
|
|
68
68
|
// per-seat jitter so thirteen answers do not land in the same second.
|
|
69
69
|
import { isRollCallItem, rollCallJitterMs } from "../../lib/org/inbound/broadcast.mjs";
|
|
70
|
+
import { stampFirstReplyPlan } from "../../lib/assurance/first-reply.mjs";
|
|
70
71
|
import { isRoomSurface } from "../../lib/org/inbound/surfaces.mjs";
|
|
71
72
|
import { isMembershipReason } from "../../lib/org/inbound/directedness.mjs";
|
|
72
73
|
import { cohortSurfaceOf, cohortSurfaceLabel, cohortSurfaceIsDeclared } from "./deliver.mjs";
|
|
@@ -96,7 +97,7 @@ import {
|
|
|
96
97
|
isPidAlive,
|
|
97
98
|
} from "./assurance.mjs";
|
|
98
99
|
import { recordPoll, recordClassification, recordSession, writeHealthDashboard } from "./health.mjs";
|
|
99
|
-
import { acquireLock, releaseLock, updateLock, scanStaleLocks, acquireThreadLock, claimRequest, hasActiveClaim, sweepStaleItemClaims, sanitiseItemId } from "./session-lock.mjs";
|
|
100
|
+
import { acquireLock, releaseLock, updateLock, scanStaleLocks, acquireThreadLock, threadLockKey, claimRequest, hasActiveClaim, sweepStaleItemClaims, sanitiseItemId } from "./session-lock.mjs";
|
|
100
101
|
import { markDeferred } from "./inbox-deferral.mjs";
|
|
101
102
|
import { parseQueueItems, rankBacklog, resolveBacklogWeights } from "../../lib/backlog.mjs";
|
|
102
103
|
import { readLatestGaps } from "../../lib/goals/gaps.mjs";
|
|
@@ -322,10 +323,23 @@ async function reviveFrontDoorIfShut(state, deps = {}) {
|
|
|
322
323
|
return verdict;
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Drain one service's inbox: poll, dedup, stamp the flurry plan, process.
|
|
328
|
+
*
|
|
329
|
+
* @param {{name:string, fn:Function}} svc
|
|
330
|
+
* @param {object} [deps] TEST-ONLY seams; PRODUCTION PASSES NOTHING, so the
|
|
331
|
+
* defaults are the real collaborators and behaviour is byte-identical. The
|
|
332
|
+
* same idiom as `processItem`/`sweepBacklog` above. Exported (and seamed) so
|
|
333
|
+
* the ONE-FIRST-REPLY-PER-FLURRY stamping has a test that fails when the call
|
|
334
|
+
* is removed — see `notice-emission.test.mjs`.
|
|
335
|
+
* @param {object} [deps.frontDoorGate] override the front-door gate
|
|
336
|
+
* @param {Function} [deps.processItem] override processItem
|
|
337
|
+
*/
|
|
338
|
+
export async function pollService(svc, deps = {}) {
|
|
339
|
+
const processItemImpl = deps.processItem || processItem;
|
|
326
340
|
try {
|
|
327
341
|
// Front door: a live main session owns this service's inbox — leave it.
|
|
328
|
-
const fd = _frontDoorGate.check(svc.name);
|
|
342
|
+
const fd = (deps.frontDoorGate || _frontDoorGate).check(svc.name);
|
|
329
343
|
if (fd.changed) console.log(`[daemon] front door for ${svc.name}: ${fd.dispatch ? "daemon dispatches" : "main session owns intake"} (${fd.reason})`);
|
|
330
344
|
if (!fd.dispatch) return 0;
|
|
331
345
|
const result = await svc.fn();
|
|
@@ -358,8 +372,33 @@ async function pollService(svc) {
|
|
|
358
372
|
return lock.acquired;
|
|
359
373
|
});
|
|
360
374
|
|
|
375
|
+
// ── ONE FIRST REPLY PER FLURRY ───────────────────────────────────────
|
|
376
|
+
// "If there's a flurry of 4 messages in one go by others, instead of having
|
|
377
|
+
// a generic reply per message, it would be responding to the batch of
|
|
378
|
+
// messages." (owner, 2026-09-25.) The batch is computed HERE, over the whole
|
|
379
|
+
// poll, because this is the only place that sees more than one item at a
|
|
380
|
+
// time — `processItem` is called per item and structurally cannot know it is
|
|
381
|
+
// the third of four. Every non-leader carries `covered_by_batch`, which
|
|
382
|
+
// `assurance.shouldAcknowledge` turns into `ack:false` and the daemon makes
|
|
383
|
+
// durable as `interimForbidden`, so no later sweep in any process speaks for
|
|
384
|
+
// it either. The leader is told how many it stands for, so the one line it
|
|
385
|
+
// writes can answer all of them.
|
|
386
|
+
//
|
|
387
|
+
// WORK IS UNAFFECTED. Every item still gets its own `processItem`, its own
|
|
388
|
+
// obligation and its own answer; what is batched is only the FIRST thing
|
|
389
|
+
// said. Silence about timing is not silence about outcome.
|
|
390
|
+
//
|
|
391
|
+
// The stamping itself is `assurance/first-reply.stampFirstReplyPlan`, not
|
|
392
|
+
// four lines here, because four lines here had NO TEST THAT COULD SEE THEM:
|
|
393
|
+
// the only call site was inside this module-internal function, so deleting
|
|
394
|
+
// them left the whole suite green — including a test named "wired, not just
|
|
395
|
+
// available". `first-reply.test.mjs` now exercises the stamp and
|
|
396
|
+
// `notice-emission.test.mjs` drives THIS function through `pollService`'s
|
|
397
|
+
// injected seams, so removing the call is red in both.
|
|
398
|
+
stampFirstReplyPlan(newItems);
|
|
399
|
+
|
|
361
400
|
for (const item of newItems) {
|
|
362
|
-
await
|
|
401
|
+
await processItemImpl(item, svc.name);
|
|
363
402
|
}
|
|
364
403
|
|
|
365
404
|
if (result.errors.length > 0) {
|
|
@@ -1107,7 +1146,38 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
|
|
|
1107
1146
|
// This check runs BEFORE quick reply to prevent ALL duplicate responses.
|
|
1108
1147
|
{
|
|
1109
1148
|
const channel = item.channel_id || (item.raw_ref ? item.raw_ref.match(/slack:([^:]+):/)?.[1] : null) || item.channel;
|
|
1110
|
-
|
|
1149
|
+
// THE UNTREADED CHANNEL POST USED TO TAKE NO LOCK AT ALL.
|
|
1150
|
+
//
|
|
1151
|
+
// `item.thread_id || (isDm ? "dm-channel" : null)` is null for exactly
|
|
1152
|
+
// one shape: a message posted to the main surface of a channel or group,
|
|
1153
|
+
// not in a thread, not a DM. That is the shape the owner screenshotted —
|
|
1154
|
+
// a flurry of messages into a room — and it was the one shape with no
|
|
1155
|
+
// dedup: each message cleared this block, spawned its own session, and
|
|
1156
|
+
// opened its own obligation, so the room got N sessions and N chances at
|
|
1157
|
+
// a holding line for one conversation.
|
|
1158
|
+
//
|
|
1159
|
+
// `channel-main` is the DM lock's counterpart for that surface: one
|
|
1160
|
+
// session at a time on a room's main feed, the rest deferred and
|
|
1161
|
+
// promoted at session close exactly like a DM burst.
|
|
1162
|
+
//
|
|
1163
|
+
// THIS IS ONLY SAFE BECAUSE OF THE BUNDLING FIX BESIDE IT. Deferral ends
|
|
1164
|
+
// in `promoteDeferred`, which collapses a group to its latest member and
|
|
1165
|
+
// marks the rest `.processed-bundled`. On a shared channel the deferred
|
|
1166
|
+
// set is several PEOPLE, not one person's flurry, so an unconditional
|
|
1167
|
+
// collapse here would silently bin a colleague's question — which is why
|
|
1168
|
+
// inbox-deferral.mjs now splits a group by sender unless the winner
|
|
1169
|
+
// actually carries the room's history. Widening this lock without that
|
|
1170
|
+
// change would trade a message storm for lost messages.
|
|
1171
|
+
//
|
|
1172
|
+
// The off-switch is real and deliberate: MAESTRO_CHANNEL_MAIN_LOCK=0
|
|
1173
|
+
// restores the previous behaviour on a seat without a code change, in
|
|
1174
|
+
// the same style as every other threshold in the assurance stack.
|
|
1175
|
+
const threadTs = threadLockKey({
|
|
1176
|
+
channel,
|
|
1177
|
+
threadId: item.thread_id,
|
|
1178
|
+
isDm,
|
|
1179
|
+
channelMainLock: String(process.env.MAESTRO_CHANNEL_MAIN_LOCK ?? "1") !== "0",
|
|
1180
|
+
});
|
|
1111
1181
|
if (threadTs && channel) {
|
|
1112
1182
|
const threadCheck = acquireThreadLock(channel, threadTs);
|
|
1113
1183
|
if (!threadCheck.allowed) {
|