sentinelayer-cli 0.8.10 → 0.8.11

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.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -180,6 +180,62 @@ export function generateAgentId(modelName) {
180
180
  return `${prefix}-${suffix}`;
181
181
  }
182
182
 
183
+ // In-process registry of agents registered by *this* CLI process. The
184
+ // dashboard treats any participant without a terminal agent_leave /
185
+ // agent_killed / session_killed event as "active". When a CLI exits via
186
+ // SIGINT/SIGTERM/crash without explicitly leaving, the dashboard shows
187
+ // "Last activity: 15h ago — active" indefinitely. This registry lets a
188
+ // single process-wide exit hook flush leave events for every agent it
189
+ // owns so the participant roster stays honest.
190
+ const _localAgents = new Map(); // key: `${sessionId}::${agentId}` -> { sessionId, agentId, targetPath }
191
+ let _exitHooksInstalled = false;
192
+
193
+ function _agentKey(sessionId, agentId) {
194
+ return `${sessionId}::${agentId}`;
195
+ }
196
+
197
+ function _trackLocalAgent(sessionId, agentId, targetPath) {
198
+ _localAgents.set(_agentKey(sessionId, agentId), { sessionId, agentId, targetPath });
199
+ _ensureExitHooksInstalled();
200
+ }
201
+
202
+ function _untrackLocalAgent(sessionId, agentId) {
203
+ _localAgents.delete(_agentKey(sessionId, agentId));
204
+ }
205
+
206
+ async function _emitLeaveForAllLocalAgents(reason) {
207
+ const entries = [..._localAgents.values()];
208
+ _localAgents.clear();
209
+ for (const entry of entries) {
210
+ try {
211
+ await emitAgentEvent(
212
+ entry.sessionId,
213
+ "agent_leave",
214
+ { agentId: entry.agentId, reason, model: "unknown", role: "participant" },
215
+ { targetPath: entry.targetPath },
216
+ );
217
+ } catch {
218
+ // Best-effort: a stuck filesystem or network shouldn't block exit.
219
+ }
220
+ }
221
+ }
222
+
223
+ function _ensureExitHooksInstalled() {
224
+ if (_exitHooksInstalled) return;
225
+ _exitHooksInstalled = true;
226
+ const onSignal = (signal) => {
227
+ void _emitLeaveForAllLocalAgents("manual").finally(() => {
228
+ process.removeListener(signal, onSignal);
229
+ process.kill(process.pid, signal);
230
+ });
231
+ };
232
+ process.on("SIGINT", onSignal);
233
+ process.on("SIGTERM", onSignal);
234
+ process.on("beforeExit", () => {
235
+ void _emitLeaveForAllLocalAgents("manual");
236
+ });
237
+ }
238
+
183
239
  export async function registerAgent(
184
240
  sessionId,
185
241
  { agentId = "", model = "", role = "observer", targetPath = process.cwd() } = {}
@@ -234,6 +290,7 @@ export async function registerAgent(
234
290
  role: snapshot.role,
235
291
  status: snapshot.status,
236
292
  }, { targetPath });
293
+ _trackLocalAgent(paths.sessionId, snapshot.agentId, targetPath);
237
294
 
238
295
  if (renamedFrom) {
239
296
  const welcome = buildSentiWelcome({
@@ -347,6 +404,8 @@ export async function unregisterAgent(
347
404
  role: snapshot.role,
348
405
  model: snapshot.model,
349
406
  }, { targetPath });
407
+ // Already left explicitly — don't double-emit on process exit.
408
+ _untrackLocalAgent(paths.sessionId, snapshot.agentId);
350
409
 
351
410
  return {
352
411
  ...snapshot,