@cohortapp/agent-sdk 2.18.14 → 2.18.16
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/docs/runbooks/fleet-rollout.md +45 -1
- 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/comms/send-gate.mjs +59 -0
- package/lib/identity/persona.mjs +31 -2
- package/lib/org/inbound/hydrate.mjs +35 -1
- package/lib/org/inbound/project.mjs +3 -0
- package/lib/session/frontdoor.mjs +87 -8
- package/lib/session/handoffs.mjs +57 -0
- package/lib/session/inbox-claims.mjs +47 -2
- package/lib/session/revive.mjs +302 -5
- package/lib/telemetry/alerts.mjs +94 -0
- package/lib/telemetry/collect.mjs +250 -2
- package/package.json +1 -1
- package/scripts/daemon/agent-daemon.mjs +317 -14
- package/scripts/daemon/assurance.mjs +765 -45
- 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/fleet/rollout.mjs +292 -9
- package/scripts/hooks/pre-write-yaml-validate.mjs +63 -2
- package/scripts/local-triggers/autoupdate.sh +338 -20
|
@@ -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
|
|
@@ -166,6 +172,7 @@ import { snapshot as countersSnapshot } from "../diagnostics/counters.mjs";
|
|
|
166
172
|
import { replyDebtFromCounters } from "../daemon/reply-debt.mjs";
|
|
167
173
|
import { summarisePinnedDrift, countPins } from "../upgrade/pinned-drift.mjs";
|
|
168
174
|
import { IGNORED_DRIFT_REL } from "../upgrade/ignored-drift.mjs";
|
|
175
|
+
import { listHandoffs, DEFAULT_HANDOFF_DEADLINE_MS } from "../session/handoffs.mjs";
|
|
169
176
|
|
|
170
177
|
/** Default temperature probe ceiling — used only as a guard in alerts; here we just report. */
|
|
171
178
|
const VALID_STATES = new Set(["active", "idle", "busy", "error", "offline"]);
|
|
@@ -1129,6 +1136,44 @@ async function collectIdentity(o, osImpl) {
|
|
|
1129
1136
|
/** A main-session heartbeat older than this is not live. */
|
|
1130
1137
|
export const SESSION_STALE_MS = 90_000;
|
|
1131
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* A FAILED REVIVE, written by the on-host actors (the daemon's front-door
|
|
1141
|
+
* revive and `autoupdate.sh#revive_daemon`) when a kickstart returned but no
|
|
1142
|
+
* live pid could be confirmed — a failure to escalate, which has to reach the
|
|
1143
|
+
* fleet, not just the seat's log. Relative to the agent root; fresh window
|
|
1144
|
+
* bounds how long it rides the beat after the last failed attempt.
|
|
1145
|
+
*/
|
|
1146
|
+
export const REVIVE_NOTE_REL = "state/telemetry/revive-note.json";
|
|
1147
|
+
export const REVIVE_NOTE_FRESH_MS = 90 * 60 * 1000;
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Pure: a revive-note that NAMES budget exhaustion, or null.
|
|
1151
|
+
*
|
|
1152
|
+
* A separate legibility defect from a failed revive: when the revive ladder
|
|
1153
|
+
* spends its whole restart budget and STOPS (`shouldReviveFrontDoor` →
|
|
1154
|
+
* `budget-spent`), nothing is written — the seat goes quiet and a quiet system
|
|
1155
|
+
* looks healthy. A ladder that gave up is a failure that does not name itself.
|
|
1156
|
+
* This turns that silence into a beat note carried through the SAME channel as
|
|
1157
|
+
* `machine.reviveNote`, so the beat says "stopped because budget spent" rather
|
|
1158
|
+
* than nothing. Only the budget-spent verdict qualifies — any other verdict
|
|
1159
|
+
* (still confirming, backing off, answering) is not the ladder giving up.
|
|
1160
|
+
*
|
|
1161
|
+
* @param {{verdict?:string, target?:string, attempts?:number, at?:string}} a
|
|
1162
|
+
* @returns {{reason:string, target?:string, detail:string, at:string}|null}
|
|
1163
|
+
*/
|
|
1164
|
+
export function budgetSpentNote(a = {}) {
|
|
1165
|
+
const x = a && typeof a === "object" ? a : {};
|
|
1166
|
+
if (x.verdict !== "budget-spent") return null;
|
|
1167
|
+
const attempts = Number.isFinite(x.attempts) ? x.attempts : 0;
|
|
1168
|
+
const at = typeof x.at === "string" && x.at ? x.at : new Date().toISOString();
|
|
1169
|
+
return {
|
|
1170
|
+
reason: "revive-budget-spent",
|
|
1171
|
+
...(typeof x.target === "string" && x.target ? { target: x.target } : {}),
|
|
1172
|
+
detail: `revive budget spent after ${attempts} attempts`,
|
|
1173
|
+
at,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1132
1177
|
/**
|
|
1133
1178
|
* Pure: derive `{frontDoor, sessionLive}` from the heartbeat the main session's
|
|
1134
1179
|
* feed writes every 15 s (`state/session/heartbeat.json` `{pid, ppid, sessionId,
|
|
@@ -1389,13 +1434,145 @@ export function sessionNote(a = {}) {
|
|
|
1389
1434
|
};
|
|
1390
1435
|
}
|
|
1391
1436
|
|
|
1437
|
+
/**
|
|
1438
|
+
* Front-door-routed inbox lanes. `sweep`, `held` and `deferred` are the
|
|
1439
|
+
* consumer's OWN maintenance lanes — items it parked on purpose, not items
|
|
1440
|
+
* waiting on a person — and a `cohort-held-*` id is the sibling hold-queue
|
|
1441
|
+
* shape (see meeting-capture's `kind==HOLD`). Neither is evidence the front
|
|
1442
|
+
* door is failing to read; counting them would wedge a seat that is doing
|
|
1443
|
+
* exactly what it was told to do.
|
|
1444
|
+
*/
|
|
1445
|
+
const WEDGE_EXCLUDED_LANES = new Set(["sweep", "held", "deferred"]);
|
|
1446
|
+
const WEDGE_HELD_ID_RE = /^cohort-held-/;
|
|
1447
|
+
|
|
1448
|
+
/** An inbox item this file's `sessionWedge` treats as front-door-routed. */
|
|
1449
|
+
function isFrontDoorInboxItem(item) {
|
|
1450
|
+
if (!item || typeof item !== "object") return false;
|
|
1451
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
1452
|
+
if (WEDGE_HELD_ID_RE.test(id)) return false;
|
|
1453
|
+
const lane = typeof item.lane === "string" ? item.lane : "";
|
|
1454
|
+
if (WEDGE_EXCLUDED_LANES.has(lane)) return false;
|
|
1455
|
+
return true;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Pure: the wedge verdict — "beating but not reading". A seat can have a
|
|
1460
|
+
* perfectly live heartbeat while the process behind it has stopped actually
|
|
1461
|
+
* answering inbound (wedged on a dialog with no watchdog for THIS failure
|
|
1462
|
+
* mode, a deadlocked event loop, anything short of the process dying). Two
|
|
1463
|
+
* independent signals, checked in order, because they catch different
|
|
1464
|
+
* failures and the first is the harder evidence:
|
|
1465
|
+
*
|
|
1466
|
+
* 1. PRIMARY — an un-acked handoff (`lib/session/handoffs.mjs`) past its
|
|
1467
|
+
* deadline. The consumer handed a cadence tick to the front door and
|
|
1468
|
+
* nobody acked it in time; this is true regardless of which front door is
|
|
1469
|
+
* nominally live, because a handoff is only ever written TO whichever
|
|
1470
|
+
* door claimed the tick. Deadline math mirrors `expireHandoffs` exactly
|
|
1471
|
+
* (`deadlineAt`, else `enqueuedAt + deadlineMs`) so the two never
|
|
1472
|
+
* disagree about which handoff is overdue.
|
|
1473
|
+
* 2. SECONDARY — inbox-unconsumed. DEFERRED and HELD OFF in this build
|
|
1474
|
+
* (`SECONDARY_WEDGE_ENABLED === false`), because it cannot yet fire
|
|
1475
|
+
* correctly end-to-end (Hannah, PR #70 review #2). Three things must land
|
|
1476
|
+
* together first, NONE of them in this PR:
|
|
1477
|
+
* (i) a consume-stamp on EVERY front-door path — today only
|
|
1478
|
+
* `ackHandoff` stamps `lastConsumedAt`, so it advances only on
|
|
1479
|
+
* handoff acks (~every 30 min), never on the inbound
|
|
1480
|
+
* claim/reply/done path in session-runtime;
|
|
1481
|
+
* (ii) a real front-door inbox reader feeding `frontDoorInbox` into
|
|
1482
|
+
* `collectStatus` — nothing feeds it, so `inbox` is always `[]`;
|
|
1483
|
+
* (iii) lane/held keying matched to the real item shape — on this fleet
|
|
1484
|
+
* inbox items carry `channel`/`kind`/`claimed_by`, not `lane`, and
|
|
1485
|
+
* no id begins `cohort-held-`, so `isFrontDoorInboxItem`'s
|
|
1486
|
+
* exclusion is keyed on fields the items do not carry.
|
|
1487
|
+
* Shipped live at the 90s bound it would read most seats wedged, and
|
|
1488
|
+
* `rollout`'s `done` (which requires zero wedged) would never clear. The
|
|
1489
|
+
* branch, `isFrontDoorInboxItem` and `WEDGE_EXCLUDED_LANES` are kept
|
|
1490
|
+
* intact behind the flag so the follow-up wires them rather than rebuilds
|
|
1491
|
+
* them; the PRIMARY handoff-overdue tell is the real signal and is fully
|
|
1492
|
+
* live.
|
|
1493
|
+
*
|
|
1494
|
+
* Defensive by construction: every read is behind a type check, nothing here
|
|
1495
|
+
* touches a clock or the filesystem, and no input shape can make it throw.
|
|
1496
|
+
*
|
|
1497
|
+
* @param {object} [a]
|
|
1498
|
+
* @param {object[]} [a.handoffs] `listHandoffs()` records (or equivalent)
|
|
1499
|
+
* @param {object[]} [a.inbox] raw inbox items, each `{id?, lane?}`
|
|
1500
|
+
* @param {number|null} [a.lastConsumedAt] epoch ms, or null/undefined = never
|
|
1501
|
+
* @param {"session"|"daemon"} [a.frontDoor]
|
|
1502
|
+
* @param {boolean} [a.sessionLive]
|
|
1503
|
+
* @param {number} [a.now] epoch ms
|
|
1504
|
+
* @param {number} [a.deadlineMs] handoff deadline (default {@link DEFAULT_HANDOFF_DEADLINE_MS})
|
|
1505
|
+
* @param {number} [a.staleMs] inbox staleness bound (default {@link SESSION_STALE_MS})
|
|
1506
|
+
* @returns {{wedged:false}|{wedged:true, reason:"handoff-overdue"|"inbox-unconsumed", since?:string}}
|
|
1507
|
+
*/
|
|
1508
|
+
// SECONDARY (inbox-unconsumed) is deferred — see the header above. A named
|
|
1509
|
+
// const, not a deleted branch: the follow-up flips this once the consume-stamp,
|
|
1510
|
+
// the frontDoorInbox feed and the real lane/held keying are all wired.
|
|
1511
|
+
export const SECONDARY_WEDGE_ENABLED = false;
|
|
1512
|
+
|
|
1513
|
+
export function sessionWedge(a = {}) {
|
|
1514
|
+
const o = a && typeof a === "object" ? a : {};
|
|
1515
|
+
const now = Number(o.now);
|
|
1516
|
+
if (!Number.isFinite(now)) return { wedged: false };
|
|
1517
|
+
const deadlineMs = Number.isFinite(o.deadlineMs) && o.deadlineMs > 0 ? o.deadlineMs : DEFAULT_HANDOFF_DEADLINE_MS;
|
|
1518
|
+
const staleMs = Number.isFinite(o.staleMs) ? o.staleMs : SESSION_STALE_MS;
|
|
1519
|
+
|
|
1520
|
+
// 1. PRIMARY — mirrors expireHandoffs' deadline math exactly.
|
|
1521
|
+
const handoffs = Array.isArray(o.handoffs) ? o.handoffs : [];
|
|
1522
|
+
for (const rec of handoffs) {
|
|
1523
|
+
if (!rec || typeof rec !== "object" || rec.status !== "open") continue;
|
|
1524
|
+
let deadline = Date.parse(rec.deadlineAt || "");
|
|
1525
|
+
if (!Number.isFinite(deadline)) {
|
|
1526
|
+
const enq = Date.parse(rec.enqueuedAt || "");
|
|
1527
|
+
deadline = Number.isFinite(enq) ? enq + deadlineMs : now; // undated → already due
|
|
1528
|
+
}
|
|
1529
|
+
if (now >= deadline) {
|
|
1530
|
+
return { wedged: true, reason: "handoff-overdue", since: new Date(deadline).toISOString() };
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// 2. SECONDARY — DEFERRED (see header): held off until the consume-stamp,
|
|
1535
|
+
// the frontDoorInbox feed and the real lane/held keying are wired end-to-end.
|
|
1536
|
+
// Kept behind the flag so it is not shipped reading seats wedged spuriously.
|
|
1537
|
+
if (SECONDARY_WEDGE_ENABLED && o.frontDoor === "session" && o.sessionLive === true) {
|
|
1538
|
+
const inbox = Array.isArray(o.inbox) ? o.inbox : [];
|
|
1539
|
+
const routed = inbox.filter(isFrontDoorInboxItem);
|
|
1540
|
+
if (routed.length > 0) {
|
|
1541
|
+
const lastConsumedAt = Number.isFinite(o.lastConsumedAt) ? o.lastConsumedAt : null;
|
|
1542
|
+
const staleConsume = lastConsumedAt === null || now - lastConsumedAt > staleMs;
|
|
1543
|
+
if (staleConsume) {
|
|
1544
|
+
const since = lastConsumedAt !== null ? lastConsumedAt : oldestInboxAt(routed);
|
|
1545
|
+
return {
|
|
1546
|
+
wedged: true,
|
|
1547
|
+
reason: "inbox-unconsumed",
|
|
1548
|
+
...(Number.isFinite(since) ? { since: new Date(since).toISOString() } : {}),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
return { wedged: false };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/** Oldest `enqueuedAt`/`ts` among inbox items, epoch ms, else NaN. */
|
|
1558
|
+
function oldestInboxAt(items) {
|
|
1559
|
+
let oldest = NaN;
|
|
1560
|
+
for (const it of items) {
|
|
1561
|
+
const ms = Date.parse((it && (it.enqueuedAt || it.ts)) || "");
|
|
1562
|
+
if (Number.isFinite(ms) && (Number.isNaN(oldest) || ms < oldest)) oldest = ms;
|
|
1563
|
+
}
|
|
1564
|
+
return oldest;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1392
1567
|
/**
|
|
1393
1568
|
* Pure: the beat's `machine.upgrade` from state/autoupdate/last.json
|
|
1394
1569
|
* (`{from,to,at,ok,healthy,reason}` as autoupdate.sh writes it). Only the four
|
|
1395
1570
|
* fields the fleet view reads; anything malformed → null, and the field drops
|
|
1396
1571
|
* out rather than lying.
|
|
1397
1572
|
* @param {object|null} last
|
|
1398
|
-
* @returns {{at:string, from:string, to:string, ok:boolean
|
|
1573
|
+
* @returns {{at:string, from:string, to:string, ok:boolean, reason?:string,
|
|
1574
|
+
* healthy?:boolean, failStreak?:number, stuckSince?:string,
|
|
1575
|
+
* streakTarget?:string}|null}
|
|
1399
1576
|
*/
|
|
1400
1577
|
export function upgradeSummary(last) {
|
|
1401
1578
|
if (!last || typeof last !== "object") return null;
|
|
@@ -1413,6 +1590,19 @@ export function upgradeSummary(last) {
|
|
|
1413
1590
|
// Bounded on the way out: a reason is a short token plus at most a trimmed
|
|
1414
1591
|
// error line, and the beat is not a log shipper.
|
|
1415
1592
|
const reason = typeof last.reason === "string" ? last.reason.trim().slice(0, 300) : "";
|
|
1593
|
+
// `failStreak` — how many CONSECUTIVE upgrade attempts have failed, with the
|
|
1594
|
+
// instant the run of failures began and the version it is failing against.
|
|
1595
|
+
// Written by autoupdate.sh's write_last; see the long block above it for what
|
|
1596
|
+
// does and does not count as an attempt. This is the ONLY field here that is
|
|
1597
|
+
// about a pattern rather than an event, and it is the reason three seats sat
|
|
1598
|
+
// on 2.17.0 for days without anything saying so: every individual record was
|
|
1599
|
+
// a truthful `ok:false` that told a reader nothing about the fortieth one.
|
|
1600
|
+
//
|
|
1601
|
+
// Absent when zero, so a healthy seat's beat carries nothing extra and a
|
|
1602
|
+
// reader can treat presence as the finding.
|
|
1603
|
+
const failStreak = Number.isInteger(last.failStreak) && last.failStreak > 0 ? last.failStreak : 0;
|
|
1604
|
+
const stuckSince = typeof last.stuckSince === "string" && !Number.isNaN(Date.parse(last.stuckSince)) ? last.stuckSince : "";
|
|
1605
|
+
const streakTarget = typeof last.streakTarget === "string" ? last.streakTarget.trim().slice(0, 40) : "";
|
|
1416
1606
|
return {
|
|
1417
1607
|
at,
|
|
1418
1608
|
from: typeof last.from === "string" ? last.from : "",
|
|
@@ -1420,6 +1610,9 @@ export function upgradeSummary(last) {
|
|
|
1420
1610
|
ok: last.ok === true,
|
|
1421
1611
|
...(reason ? { reason } : {}),
|
|
1422
1612
|
...(typeof last.healthy === "boolean" ? { healthy: last.healthy } : {}),
|
|
1613
|
+
...(failStreak ? { failStreak } : {}),
|
|
1614
|
+
...(failStreak && stuckSince ? { stuckSince } : {}),
|
|
1615
|
+
...(failStreak && streakTarget ? { streakTarget } : {}),
|
|
1423
1616
|
};
|
|
1424
1617
|
}
|
|
1425
1618
|
|
|
@@ -2045,6 +2238,60 @@ export async function collectStatus(o = {}) {
|
|
|
2045
2238
|
} catch { /* no sessionNote — the beat still carries frontDoor/sessionLive */ }
|
|
2046
2239
|
}
|
|
2047
2240
|
|
|
2241
|
+
// 4b-iii. BEATING BUT NOT READING (DARK-SEAT) — `machine.wedge`, present
|
|
2242
|
+
// ONLY when wedged, exactly like `sessionNote` above. The decision is the
|
|
2243
|
+
// pure `sessionWedge`; this is the reads that feed it — the open handoff
|
|
2244
|
+
// ledger — and it is fail-open in the same way every other probe here is: a
|
|
2245
|
+
// throw anywhere (a corrupt handoff file) drops the field, never the beat.
|
|
2246
|
+
// `opt.handoffs` overrides disk, mirroring `opt.attention` above, so a
|
|
2247
|
+
// caller that already has the ledger in memory (the daemon, a test) is never
|
|
2248
|
+
// made to round-trip it through the filesystem.
|
|
2249
|
+
//
|
|
2250
|
+
// The `frontDoorInbox`/`lastConsumedAt` reads that fed the SECONDARY
|
|
2251
|
+
// (inbox-unconsumed) signal are gone from here: that signal is DEFERRED
|
|
2252
|
+
// (`SECONDARY_WEDGE_ENABLED === false` in sessionWedge), so feeding it would
|
|
2253
|
+
// be inert work. They return when the follow-up wires the secondary.
|
|
2254
|
+
try {
|
|
2255
|
+
const handoffs = opt.handoffs !== undefined ? opt.handoffs : listHandoffs(opt.agentRoot);
|
|
2256
|
+
const wedge = sessionWedge({
|
|
2257
|
+
handoffs,
|
|
2258
|
+
frontDoor: machine.frontDoor,
|
|
2259
|
+
sessionLive: machine.sessionLive,
|
|
2260
|
+
now: nowMs,
|
|
2261
|
+
deadlineMs: opt.handoffDeadlineMs,
|
|
2262
|
+
});
|
|
2263
|
+
if (wedge && wedge.wedged) machine.wedge = { reason: wedge.reason, ...(wedge.since ? { since: wedge.since } : {}) };
|
|
2264
|
+
} catch { /* no wedge field — the beat still carries frontDoor/sessionLive/sessionNote */ }
|
|
2265
|
+
|
|
2266
|
+
// 4b-iv. A FAILED REVIVE — `machine.reviveNote`, present only when a revive
|
|
2267
|
+
// kickstarted a job and then could NOT confirm a live pid (Jacob's
|
|
2268
|
+
// drain-then-restart-daemon.sh exited 0 with a dead daemon). The two on-host
|
|
2269
|
+
// actors — the daemon's front-door revive (agent-daemon.mjs) and the hourly
|
|
2270
|
+
// daemon backstop (autoupdate.sh#revive_daemon) — write it to
|
|
2271
|
+
// state/telemetry/revive-note.json; a no-pid-after-kickstart is a FAILURE TO
|
|
2272
|
+
// ESCALATE and has to reach the fleet, not just this seat's log — the seat
|
|
2273
|
+
// needs a person. Surfaced only while fresh (REVIVE_NOTE_FRESH_MS) so a
|
|
2274
|
+
// recovered seat ages out; the writers refresh it every attempt and clear it
|
|
2275
|
+
// on a confirmed revive. `machine` is an OPEN record (like wedge/upgrade), so
|
|
2276
|
+
// this lands without an hq schema change. Fail-open. `opt.reviveNote`
|
|
2277
|
+
// overrides disk for tests.
|
|
2278
|
+
try {
|
|
2279
|
+
const rn = opt.reviveNote !== undefined
|
|
2280
|
+
? opt.reviveNote
|
|
2281
|
+
: (opt.agentRoot ? safeReadJson(join(resolve(opt.agentRoot), REVIVE_NOTE_REL)) : null);
|
|
2282
|
+
if (rn && typeof rn === "object" && typeof rn.reason === "string") {
|
|
2283
|
+
const at = Date.parse(rn.at || "");
|
|
2284
|
+
const freshMs = Number.isFinite(opt.reviveNoteFreshMs) ? opt.reviveNoteFreshMs : REVIVE_NOTE_FRESH_MS;
|
|
2285
|
+
if (Number.isFinite(at) && nowMs - at <= freshMs) {
|
|
2286
|
+
machine.reviveNote = {
|
|
2287
|
+
reason: rn.reason,
|
|
2288
|
+
at: new Date(at).toISOString(),
|
|
2289
|
+
...(typeof rn.target === "string" && rn.target ? { target: rn.target } : {}),
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
} catch { /* no reviveNote — the beat still carries everything else */ }
|
|
2294
|
+
|
|
2048
2295
|
// 4c. last upgrade outcome (WP-M6) — `machine.upgrade`, absent until the
|
|
2049
2296
|
// first autoupdate attempt; a corrupt file drops the field, never the beat.
|
|
2050
2297
|
try {
|
|
@@ -2145,6 +2392,7 @@ export const _internals = {
|
|
|
2145
2392
|
readSdkVersion,
|
|
2146
2393
|
sessionLiveness,
|
|
2147
2394
|
sessionNote,
|
|
2395
|
+
sessionWedge,
|
|
2148
2396
|
sanitizeNoteDetail,
|
|
2149
2397
|
sessionJobLabel,
|
|
2150
2398
|
sessionJobInstalled,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.16",
|
|
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": {
|