sentinelayer-cli 0.30.8 → 0.30.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.30.8",
3
+ "version": "0.30.9",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -4324,8 +4324,9 @@ export function registerSessionCommand(program) {
4324
4324
  const durablePresenceEnabled = options.presence !== false;
4325
4325
  const publishPresence = durablePresenceEnabled && canPublishListenerPresence(agentId);
4326
4326
  const presenceIntervalMs = Math.max(1, presenceIntervalSeconds) * 1000;
4327
- const presenceKeepaliveMs =
4328
- Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds, 1) * 1000;
4327
+ const effectivePresenceKeepaliveSeconds =
4328
+ Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds, intervalSeconds, 1);
4329
+ const presenceKeepaliveMs = effectivePresenceKeepaliveSeconds * 1000;
4329
4330
  let lastPresenceHeartbeatMs = 0;
4330
4331
  let lastPresenceHeartbeatFingerprint = "";
4331
4332
 
@@ -4512,7 +4513,7 @@ export function registerSessionCommand(program) {
4512
4513
  const publishLifecycle = {
4513
4514
  ...lifecycle,
4514
4515
  presenceIntervalSeconds,
4515
- presenceKeepaliveSeconds: Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds),
4516
+ presenceKeepaliveSeconds: effectivePresenceKeepaliveSeconds,
4516
4517
  };
4517
4518
  if (lifecycleType === "heartbeat") {
4518
4519
  const nowMs = Date.now();
@@ -9,6 +9,7 @@ const LISTENER_EVENT_TYPES = new Set([
9
9
  // A heartbeat older than this (and not explicitly stopped) means the
10
10
  // listener likely died without a clean stop — show it as stale, not live.
11
11
  const DEFAULT_STALE_AFTER_MS = 180_000;
12
+ const MAX_STALE_GRACE_MS = 60_000;
12
13
 
13
14
  function normalizeString(value) {
14
15
  return String(value || "").trim();
@@ -29,6 +30,38 @@ function positiveInt(value) {
29
30
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
30
31
  }
31
32
 
33
+ function listenerStaleAfterMs({
34
+ staleAfterMs,
35
+ presenceKeepaliveSeconds,
36
+ presenceIntervalSeconds,
37
+ cadenceSeconds,
38
+ idleIntervalSeconds,
39
+ activeIntervalSeconds,
40
+ } = {}) {
41
+ const fallbackMs = Math.max(1, Number(staleAfterMs) || DEFAULT_STALE_AFTER_MS);
42
+ const keepaliveMs = presenceKeepaliveSeconds ? presenceKeepaliveSeconds * 1000 : null;
43
+ const expectedIntervalSeconds =
44
+ cadenceSeconds ||
45
+ presenceIntervalSeconds ||
46
+ idleIntervalSeconds ||
47
+ activeIntervalSeconds ||
48
+ null;
49
+ const expectedIntervalMs = expectedIntervalSeconds ? expectedIntervalSeconds * 1000 : 0;
50
+
51
+ if (keepaliveMs) {
52
+ // The listener can only publish on a poll tick. Allow one bounded poll
53
+ // interval after the advertised keepalive, not the old 2.5x keepalive
54
+ // window that made dead listeners look live for several extra minutes.
55
+ return keepaliveMs + Math.min(expectedIntervalMs || 0, MAX_STALE_GRACE_MS);
56
+ }
57
+
58
+ if (expectedIntervalMs) {
59
+ return Math.max(fallbackMs, Math.round(expectedIntervalMs * 2.5));
60
+ }
61
+
62
+ return fallbackMs;
63
+ }
64
+
32
65
  /**
33
66
  * Reduce a stream of session_listener_* events into one row per agent: who is
34
67
  * listening, at what cadence, and whether they're currently active or idle.
@@ -66,17 +99,14 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
66
99
  const cadenceSeconds = active
67
100
  ? activeIntervalSeconds || nextPollSeconds
68
101
  : idleIntervalSeconds || nextPollSeconds;
69
- const expectedPresenceSeconds =
70
- presenceKeepaliveSeconds ||
71
- presenceIntervalSeconds ||
72
- cadenceSeconds ||
73
- idleIntervalSeconds ||
74
- activeIntervalSeconds ||
75
- null;
76
- const staleAfterForRowMs = Math.max(
102
+ const staleAfterForRowMs = listenerStaleAfterMs({
77
103
  staleAfterMs,
78
- expectedPresenceSeconds ? expectedPresenceSeconds * 2_500 : 0,
79
- );
104
+ presenceKeepaliveSeconds,
105
+ presenceIntervalSeconds,
106
+ cadenceSeconds,
107
+ idleIntervalSeconds,
108
+ activeIntervalSeconds,
109
+ });
80
110
  let status;
81
111
  if (stopped) status = "stopped";
82
112
  else if (ageMs !== null && ageMs > staleAfterForRowMs) status = "stale";
@@ -94,6 +124,7 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
94
124
  presenceIntervalSeconds,
95
125
  presenceKeepaliveSeconds,
96
126
  nextPollSeconds,
127
+ staleAfterSeconds: Math.round(staleAfterForRowMs / 1000),
97
128
  lastSeenAt: epoch ? new Date(epoch).toISOString() : null,
98
129
  lastSeenAgoSeconds: ageMs !== null ? Math.round(ageMs / 1000) : null,
99
130
  lastHumanActivityAt: normalizeString(payload.lastHumanActivityAt) || null,