@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
|
@@ -84,8 +84,14 @@
|
|
|
84
84
|
* // Also on `machine` (open record), so hq's fleet view can show it next
|
|
85
85
|
* // to sdkVersion without a schema change. `ok` is the attempt's verdict:
|
|
86
86
|
* // true = healthy on `to`; false = install failed or rolled back to `from`.
|
|
87
|
+
* // `failStreak` is the one field here that describes a PATTERN: N
|
|
88
|
+
* // consecutive failed attempts to reach @latest, since `stuckSince`,
|
|
89
|
+
* // against `streakTarget`. Absent below 1. A seat that has simply not
|
|
90
|
+
* // been asked (nothing newer published, or held after its last failure)
|
|
91
|
+
* // never accrues one — see autoupdate.sh's write_last.
|
|
87
92
|
* upgrade?: { at: ISO8601, from: string, to: string, ok: boolean,
|
|
88
|
-
* reason?: string, healthy?: boolean
|
|
93
|
+
* reason?: string, healthy?: boolean, // reason: WHY a failure failed
|
|
94
|
+
* failStreak?: number, stuckSince?: ISO8601, streakTarget?: string },
|
|
89
95
|
* // THE BEATING DAEMON — which process is emitting this beat, what code
|
|
90
96
|
* // it is actually running, and the last health-gate verdict on it.
|
|
91
97
|
* // Also on `machine` (open record). `sdkVersion` ABOVE is what is
|
|
@@ -99,6 +105,28 @@
|
|
|
99
105
|
* daemon?: { pid?: number, bootAt?: ISO8601, uptimeS?: number,
|
|
100
106
|
* sdkVersion?: string, dashboardAt?: ISO8601,
|
|
101
107
|
* healthy?: boolean, healthReason?: string },
|
|
108
|
+
* // WHICH UPSTREAM FIXES CANNOT REACH THIS SEAT. A `.maestroignore`
|
|
109
|
+
* // pin on a FRAMEWORK path is a fork, and upstream moves under it:
|
|
110
|
+
* // five of sixteen seats were degraded by exactly this on 2026-09-25,
|
|
111
|
+
* // two of them crash-looping at import time on a pinned deliver.mjs.
|
|
112
|
+
* // `stranded` counts framework pins where upstream holds lines this
|
|
113
|
+
* // seat refuses (`onlyUpstream > 0` in .maestro/ignored-drift.json) —
|
|
114
|
+
* // that, and not "a file differs", is the property that means no
|
|
115
|
+
* // release can ever reach this colleague. ABSENT ENTIRELY on a seat
|
|
116
|
+
* // that pins nothing; `{unknown:true}` when the seat has pins and
|
|
117
|
+
* // could not read its own report, which must never read as clean.
|
|
118
|
+
* // INVENTORY: carries the `at`/`sdkVersion` of the upgrade that took
|
|
119
|
+
* // it, not the snapshot's.
|
|
120
|
+
* // `patterns` counts lines in `.maestroignore`; `matched` counts the
|
|
121
|
+
* // entries the drift report covers. Two numbers, two names, on both
|
|
122
|
+
* // branches — one glob can match forty files, or none.
|
|
123
|
+
* pinnedDrift?: { patterns: number|null, matched: number, framework: number,
|
|
124
|
+
* seatOwned: number, drifting: number, stranded: number,
|
|
125
|
+
* strandedCode: number, localOnly: number,
|
|
126
|
+
* at: ISO8601|null, sdkVersion: string|null,
|
|
127
|
+
* worst: [{ path: string, behind: number, code: boolean }] }
|
|
128
|
+
* | { unknown: true, patterns: number|null, matched: null,
|
|
129
|
+
* reason: string },
|
|
102
130
|
* },
|
|
103
131
|
* claudeAuth: "ok"|"relogin_required"|"unknown",
|
|
104
132
|
* alerts: [{ id, severity, kind, detail }],
|
|
@@ -142,6 +170,8 @@ import { liveClaudeStats } from "../resource-governor.mjs";
|
|
|
142
170
|
import { agentFirstName } from "../session/identity.mjs";
|
|
143
171
|
import { snapshot as countersSnapshot } from "../diagnostics/counters.mjs";
|
|
144
172
|
import { replyDebtFromCounters } from "../daemon/reply-debt.mjs";
|
|
173
|
+
import { summarisePinnedDrift, countPins } from "../upgrade/pinned-drift.mjs";
|
|
174
|
+
import { IGNORED_DRIFT_REL } from "../upgrade/ignored-drift.mjs";
|
|
145
175
|
|
|
146
176
|
/** Default temperature probe ceiling — used only as a guard in alerts; here we just report. */
|
|
147
177
|
const VALID_STATES = new Set(["active", "idle", "busy", "error", "offline"]);
|
|
@@ -1229,6 +1259,13 @@ const ATTENTION_WHY = {
|
|
|
1229
1259
|
"no-heartbeat": "up, never beaten",
|
|
1230
1260
|
"stale-heartbeat": "no beat since this launch",
|
|
1231
1261
|
"beat-stopped": "beat, then stopped",
|
|
1262
|
+
// The session is not wedged — it never STARTS. Written by the supervisor
|
|
1263
|
+
// once N consecutive launches have failed identically
|
|
1264
|
+
// (scripts/session/supervisor.mjs, lib/session/launch-failure.mjs). It is a
|
|
1265
|
+
// different fault from the three above, with a different fix, and it is the
|
|
1266
|
+
// one that looked like health for days on a seat relaunching a dead resume
|
|
1267
|
+
// id every ten minutes.
|
|
1268
|
+
"launch-failing": "cannot launch",
|
|
1232
1269
|
};
|
|
1233
1270
|
|
|
1234
1271
|
/**
|
|
@@ -1364,7 +1401,9 @@ export function sessionNote(a = {}) {
|
|
|
1364
1401
|
* fields the fleet view reads; anything malformed → null, and the field drops
|
|
1365
1402
|
* out rather than lying.
|
|
1366
1403
|
* @param {object|null} last
|
|
1367
|
-
* @returns {{at:string, from:string, to:string, ok:boolean
|
|
1404
|
+
* @returns {{at:string, from:string, to:string, ok:boolean, reason?:string,
|
|
1405
|
+
* healthy?:boolean, failStreak?:number, stuckSince?:string,
|
|
1406
|
+
* streakTarget?:string}|null}
|
|
1368
1407
|
*/
|
|
1369
1408
|
export function upgradeSummary(last) {
|
|
1370
1409
|
if (!last || typeof last !== "object") return null;
|
|
@@ -1382,6 +1421,19 @@ export function upgradeSummary(last) {
|
|
|
1382
1421
|
// Bounded on the way out: a reason is a short token plus at most a trimmed
|
|
1383
1422
|
// error line, and the beat is not a log shipper.
|
|
1384
1423
|
const reason = typeof last.reason === "string" ? last.reason.trim().slice(0, 300) : "";
|
|
1424
|
+
// `failStreak` — how many CONSECUTIVE upgrade attempts have failed, with the
|
|
1425
|
+
// instant the run of failures began and the version it is failing against.
|
|
1426
|
+
// Written by autoupdate.sh's write_last; see the long block above it for what
|
|
1427
|
+
// does and does not count as an attempt. This is the ONLY field here that is
|
|
1428
|
+
// about a pattern rather than an event, and it is the reason three seats sat
|
|
1429
|
+
// on 2.17.0 for days without anything saying so: every individual record was
|
|
1430
|
+
// a truthful `ok:false` that told a reader nothing about the fortieth one.
|
|
1431
|
+
//
|
|
1432
|
+
// Absent when zero, so a healthy seat's beat carries nothing extra and a
|
|
1433
|
+
// reader can treat presence as the finding.
|
|
1434
|
+
const failStreak = Number.isInteger(last.failStreak) && last.failStreak > 0 ? last.failStreak : 0;
|
|
1435
|
+
const stuckSince = typeof last.stuckSince === "string" && !Number.isNaN(Date.parse(last.stuckSince)) ? last.stuckSince : "";
|
|
1436
|
+
const streakTarget = typeof last.streakTarget === "string" ? last.streakTarget.trim().slice(0, 40) : "";
|
|
1385
1437
|
return {
|
|
1386
1438
|
at,
|
|
1387
1439
|
from: typeof last.from === "string" ? last.from : "",
|
|
@@ -1389,6 +1441,9 @@ export function upgradeSummary(last) {
|
|
|
1389
1441
|
ok: last.ok === true,
|
|
1390
1442
|
...(reason ? { reason } : {}),
|
|
1391
1443
|
...(typeof last.healthy === "boolean" ? { healthy: last.healthy } : {}),
|
|
1444
|
+
...(failStreak ? { failStreak } : {}),
|
|
1445
|
+
...(failStreak && stuckSince ? { stuckSince } : {}),
|
|
1446
|
+
...(failStreak && streakTarget ? { streakTarget } : {}),
|
|
1392
1447
|
};
|
|
1393
1448
|
}
|
|
1394
1449
|
|
|
@@ -1509,6 +1564,86 @@ export function daemonSummary(dash, last) {
|
|
|
1509
1564
|
return Object.keys(out).length > 0 ? out : null;
|
|
1510
1565
|
}
|
|
1511
1566
|
|
|
1567
|
+
// ---------------------------------------------------------------------------
|
|
1568
|
+
// Pinned framework files — WHICH UPSTREAM FIXES CANNOT REACH THIS SEAT
|
|
1569
|
+
// (`machine.pinnedDrift`)
|
|
1570
|
+
// ---------------------------------------------------------------------------
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Pure: the beat's `machine.pinnedDrift`.
|
|
1574
|
+
*
|
|
1575
|
+
* WHY THIS FIELD EXISTS. A `.maestroignore` entry on a framework path is a
|
|
1576
|
+
* fork, and upstream keeps moving under it. MEASURED 2026-09-25, repairing
|
|
1577
|
+
* five of sixteen seats by hand: two were crash-looping at import time on a
|
|
1578
|
+
* pinned `lib/org/inbound/deliver.mjs`; two more pinned
|
|
1579
|
+
* `scripts/daemon/agent-daemon.mjs` and therefore could not receive the
|
|
1580
|
+
* front-door revive fix (2.18.11/12) OR the persona fix — one of them went on
|
|
1581
|
+
* emitting an AI self-introduction for days after it was fixed upstream,
|
|
1582
|
+
* because the fix could not physically arrive.
|
|
1583
|
+
*
|
|
1584
|
+
* Every one of those seats already knew. `maestro upgrade` has written the
|
|
1585
|
+
* per-file answer to `.maestro/ignored-drift.json` for weeks and NOTHING read
|
|
1586
|
+
* it — no warning, no beat field, no alert. This is the half that makes the
|
|
1587
|
+
* knowledge leave the machine it is about.
|
|
1588
|
+
*
|
|
1589
|
+
* THE PROPERTY WORTH CARRYING is not "a file differs". It is "upstream holds
|
|
1590
|
+
* lines this pin refuses", which is `onlyUpstream > 0` on a framework path —
|
|
1591
|
+
* see `lib/upgrade/pinned-drift.mjs` for why (on this seat the same day, two
|
|
1592
|
+
* of seven drifting pins refused nothing at all, which is what a pin is FOR).
|
|
1593
|
+
*
|
|
1594
|
+
* SMALL AND BOUNDED, like every other field on `machine`: a handful of counts
|
|
1595
|
+
* and at most three paths, each capped (the size floor is pinned by
|
|
1596
|
+
* `pinned-drift-beat.test.mjs`). It rides `machine` for the reason
|
|
1597
|
+
* `frontDoor` and `daemon` do — hq validates `machine` as an OPEN record
|
|
1598
|
+
* (`presence/beat.ts#statusSchema`) while `session` is `.strict()`, so a new
|
|
1599
|
+
* seat-level fact lands without a lock-step hq deploy.
|
|
1600
|
+
*
|
|
1601
|
+
* THREE ANSWERS, AND THE THIRD IS THE POINT:
|
|
1602
|
+
* · a seat that pins nothing returns `null` — the caller drops the key, and
|
|
1603
|
+
* hq grows no field and no alert for a seat with nothing wrong;
|
|
1604
|
+
* · a seat that pins something and cannot read its own report returns
|
|
1605
|
+
* `{unknown:true}` — never a clean bill. "Could not tell" and "nothing to
|
|
1606
|
+
* tell" are opposite instructions, and collapsing them is the exact
|
|
1607
|
+
* failure this whole field exists to end.
|
|
1608
|
+
*
|
|
1609
|
+
* INVENTORY, not a momentary reading: the report is written by the last
|
|
1610
|
+
* upgrade and stays true until the next one, so it carries its own `at` and
|
|
1611
|
+
* the `sdkVersion` it was taken against rather than borrowing the snapshot's.
|
|
1612
|
+
*
|
|
1613
|
+
* @param {object|null} report parsed `.maestro/ignored-drift.json`, or null
|
|
1614
|
+
* @param {number|null} pinCount pattern lines in `.maestroignore`, or null
|
|
1615
|
+
* when even that could not be read
|
|
1616
|
+
*/
|
|
1617
|
+
export function pinnedDriftSummary(report, pinCount) {
|
|
1618
|
+
return summarisePinnedDrift(report, { pinCount });
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/** `.maestro/ignored-drift.json`, or null — fail-open. */
|
|
1622
|
+
function readIgnoredDriftReport(agentRoot) {
|
|
1623
|
+
if (!agentRoot) return null;
|
|
1624
|
+
return safeReadJson(join(resolve(agentRoot), IGNORED_DRIFT_REL));
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* How many real patterns `.maestroignore` holds — `0` when the file is absent
|
|
1629
|
+
* (a seat with no pins), `null` when it exists and could not be read.
|
|
1630
|
+
*
|
|
1631
|
+
* The distinction is load-bearing: `0` is what lets the field be dropped
|
|
1632
|
+
* entirely, and `null` is what forces `unknown` instead of a clean answer.
|
|
1633
|
+
* A missing file is a definite zero; an unreadable one is not.
|
|
1634
|
+
*/
|
|
1635
|
+
function readPinCount(agentRoot) {
|
|
1636
|
+
if (!agentRoot) return null;
|
|
1637
|
+
const file = join(resolve(agentRoot), ".maestroignore");
|
|
1638
|
+
let text;
|
|
1639
|
+
try {
|
|
1640
|
+
text = readFileSync(file, "utf8");
|
|
1641
|
+
} catch (e) {
|
|
1642
|
+
return e && e.code === "ENOENT" ? 0 : null;
|
|
1643
|
+
}
|
|
1644
|
+
return countPins(text);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1512
1647
|
/** state/autoupdate/last.json, or null. */
|
|
1513
1648
|
function readAutoupdateLast(agentRoot) {
|
|
1514
1649
|
if (!agentRoot) return null;
|
|
@@ -1974,6 +2109,21 @@ export async function collectStatus(o = {}) {
|
|
|
1974
2109
|
if (d) machine.daemon = d;
|
|
1975
2110
|
} catch { /* no daemon field */ }
|
|
1976
2111
|
|
|
2112
|
+
// 4e. WHICH UPSTREAM FIXES CANNOT REACH THIS SEAT — `machine.pinnedDrift`,
|
|
2113
|
+
// absent on a seat that pins nothing. See `pinnedDriftSummary` for the five
|
|
2114
|
+
// seats this was measured on. Fail-open like every probe here: a throw drops
|
|
2115
|
+
// the field, never the beat — but note that a *readable* seat with pins and
|
|
2116
|
+
// an unreadable report deliberately lands `{unknown:true}` rather than
|
|
2117
|
+
// nothing, because "I could not tell" must never render as "clean".
|
|
2118
|
+
try {
|
|
2119
|
+
const pinReport = opt.ignoredDrift !== undefined
|
|
2120
|
+
? opt.ignoredDrift
|
|
2121
|
+
: readIgnoredDriftReport(opt.agentRoot);
|
|
2122
|
+
const pins = opt.pinCount !== undefined ? opt.pinCount : readPinCount(opt.agentRoot);
|
|
2123
|
+
const pd = pinnedDriftSummary(pinReport, pins);
|
|
2124
|
+
if (pd) machine.pinnedDrift = pd;
|
|
2125
|
+
} catch { /* no pinnedDrift field */ }
|
|
2126
|
+
|
|
1977
2127
|
const status = {
|
|
1978
2128
|
state,
|
|
1979
2129
|
activity: act.activity || "idle",
|
|
@@ -2026,6 +2176,9 @@ export const _internals = {
|
|
|
2026
2176
|
upgradeSummary,
|
|
2027
2177
|
parseDaemonHealthYaml,
|
|
2028
2178
|
daemonSummary,
|
|
2179
|
+
pinnedDriftSummary,
|
|
2180
|
+
readIgnoredDriftReport,
|
|
2181
|
+
readPinCount,
|
|
2029
2182
|
detectClaudeAuth,
|
|
2030
2183
|
resetAuthProbeCache,
|
|
2031
2184
|
scanForRelogin,
|