@cohortapp/agent-sdk 2.14.0 → 2.15.0
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.
|
@@ -1060,7 +1060,27 @@ export function sessionLiveness(hb, nowMs, opts = {}) {
|
|
|
1060
1060
|
const staleMs = Number.isFinite(opts.staleMs) ? opts.staleMs : SESSION_STALE_MS;
|
|
1061
1061
|
const ts = typeof hb.ts === "number" ? hb.ts : Date.parse(hb.ts);
|
|
1062
1062
|
const age = nowMs - ts;
|
|
1063
|
-
|
|
1063
|
+
// SYMMETRIC around now, matching lib/session/liveness.mjs#heartbeatLiveness,
|
|
1064
|
+
// which this used to contradict: it accepted `-staleMs < age < staleMs` while
|
|
1065
|
+
// this demanded `age >= 0`, so the same heartbeat could be live to the session
|
|
1066
|
+
// runtime and dead to the beat.
|
|
1067
|
+
//
|
|
1068
|
+
// The asymmetry was not theoretical. A039 flapped between live and not-live
|
|
1069
|
+
// across consecutive beats on 2026-09-10/11 while its session kept running,
|
|
1070
|
+
// because a heartbeat written microseconds before the wall clock stepped back
|
|
1071
|
+
// reads as future-dated, and ANY future dating failed `age >= 0`. The seat is
|
|
1072
|
+
// within 30 s of the server, so this is ordinary same-machine clock motion
|
|
1073
|
+
// (NTP correcting a box 95 days up), not a broken clock — and a front-door
|
|
1074
|
+
// signal that flickers is worse than none, because it is the one field an
|
|
1075
|
+
// operator uses to decide whether to walk to a machine.
|
|
1076
|
+
//
|
|
1077
|
+
// Beyond the window a future heartbeat is still NOT live, and sessionNote
|
|
1078
|
+
// reports it as such ("last beat is in the future (clock skew)") rather than
|
|
1079
|
+
// silently believing a timestamp it cannot trust.
|
|
1080
|
+
// Strict on BOTH edges, because that is what heartbeatLiveness does
|
|
1081
|
+
// (`age >= staleMs` and `age <= -staleMs` are each "not live"). An edge that
|
|
1082
|
+
// differs by one millisecond is still two answers to one question.
|
|
1083
|
+
const live = Number.isFinite(age) && Math.abs(age) < staleMs;
|
|
1064
1084
|
return { frontDoor: "session", sessionLive: live };
|
|
1065
1085
|
}
|
|
1066
1086
|
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
ramLabel,
|
|
34
34
|
collectSpend24h,
|
|
35
35
|
collectInventory,
|
|
36
|
+
sessionLiveness,
|
|
37
|
+
SESSION_STALE_MS,
|
|
36
38
|
_internals,
|
|
37
39
|
} from "./collect.mjs";
|
|
38
40
|
|
|
@@ -1218,3 +1220,55 @@ test("collectStatus: machine.sessionNote appears ONLY when the front door is dow
|
|
|
1218
1220
|
}
|
|
1219
1221
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
1220
1222
|
});
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* The front-door signal must not flicker while the session is running.
|
|
1226
|
+
*
|
|
1227
|
+
* This function and `lib/session/liveness.mjs#heartbeatLiveness` answer one
|
|
1228
|
+
* question — "is the main session alive" — and they used to disagree: the
|
|
1229
|
+
* runtime accepted a heartbeat up to `staleMs` either side of now, this one
|
|
1230
|
+
* demanded `age >= 0`. So a heartbeat written microseconds before the wall
|
|
1231
|
+
* clock stepped back read as future-dated and flipped the seat to DOWN, while
|
|
1232
|
+
* the session carried on. A039 did exactly that across consecutive beats on
|
|
1233
|
+
* 2026-09-10/11 with its session healthy and its clock within 30 s of the
|
|
1234
|
+
* server. Symmetric now, and pinned here because the asymmetry was invisible:
|
|
1235
|
+
* every test used a past-dated heartbeat.
|
|
1236
|
+
*/
|
|
1237
|
+
test("sessionLiveness tolerates clock skew symmetrically, and reports a far-future beat as not live", () => {
|
|
1238
|
+
const now = Date.parse("2026-09-11T12:00:00Z");
|
|
1239
|
+
const at = (ms) => ({ ts: new Date(now - ms).toISOString() });
|
|
1240
|
+
|
|
1241
|
+
// Fresh, and a beat from the recent past: live, as before.
|
|
1242
|
+
assert.equal(sessionLiveness(at(0), now).sessionLive, true);
|
|
1243
|
+
assert.equal(sessionLiveness(at(30_000), now).sessionLive, true);
|
|
1244
|
+
|
|
1245
|
+
// THE REGRESSION: a beat dated slightly in the FUTURE is the clock moving,
|
|
1246
|
+
// not the session dying. Both of these used to return false.
|
|
1247
|
+
assert.equal(sessionLiveness(at(-1), now).sessionLive, true, "1 ms ahead is not a dead session");
|
|
1248
|
+
assert.equal(sessionLiveness(at(-30_000), now).sessionLive, true, "30 s ahead is ordinary clock motion");
|
|
1249
|
+
|
|
1250
|
+
// The window is symmetric and OPEN at both edges — the runtime's rule.
|
|
1251
|
+
assert.equal(sessionLiveness(at(SESSION_STALE_MS - 1), now).sessionLive, true);
|
|
1252
|
+
assert.equal(sessionLiveness(at(-(SESSION_STALE_MS - 1)), now).sessionLive, true);
|
|
1253
|
+
assert.equal(sessionLiveness(at(SESSION_STALE_MS), now).sessionLive, false, "too old");
|
|
1254
|
+
assert.equal(sessionLiveness(at(-SESSION_STALE_MS), now).sessionLive, false, "too far ahead to believe");
|
|
1255
|
+
|
|
1256
|
+
// The front door is still named in every case a heartbeat exists.
|
|
1257
|
+
assert.equal(sessionLiveness(at(10 * SESSION_STALE_MS), now).frontDoor, "session");
|
|
1258
|
+
// No heartbeat at all remains the daemon's lane.
|
|
1259
|
+
assert.deepEqual(sessionLiveness(null, now), { frontDoor: "daemon", sessionLive: false });
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
test("sessionLiveness agrees with the session runtime's own window", async () => {
|
|
1263
|
+
const { heartbeatLiveness, STALE_MS } = await import("../session/liveness.mjs");
|
|
1264
|
+
assert.equal(STALE_MS, SESSION_STALE_MS, "one window, two readers");
|
|
1265
|
+
const now = Date.parse("2026-09-11T12:00:00Z");
|
|
1266
|
+
for (const offset of [0, 1_000, -1_000, 45_000, -45_000, STALE_MS, -STALE_MS, STALE_MS + 1, -(STALE_MS + 1)]) {
|
|
1267
|
+
const hb = { ts: new Date(now - offset).toISOString(), pid: 0 };
|
|
1268
|
+
assert.equal(
|
|
1269
|
+
sessionLiveness(hb, now).sessionLive,
|
|
1270
|
+
heartbeatLiveness(hb, { now }).live,
|
|
1271
|
+
`the two readers disagree at offset ${offset} ms`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
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": {
|