sentinelayer-cli 0.11.3 โ†’ 0.11.4

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.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -69,6 +69,7 @@ import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
69
69
  import { mergeLiveSources } from "../session/live-source.js";
70
70
  import { listenSessionEvents } from "../session/listener.js";
71
71
  import { buildSessionRecap } from "../session/recap.js";
72
+ import { computeTranscriptStats } from "../session/transcript.js";
72
73
  import { deriveSessionTitle } from "../session/senti-naming.js";
73
74
  import { pushSessionTitleToApi } from "../session/title-sync.js";
74
75
  import {
@@ -96,6 +97,68 @@ function normalizeString(value) {
96
97
  return String(value || "").trim();
97
98
  }
98
99
 
100
+ function compareIsoDesc(left = "", right = "") {
101
+ return normalizeString(right).localeCompare(normalizeString(left));
102
+ }
103
+
104
+ function buildSessionParticipants({ statsAgents = [], registeredAgents = [] } = {}) {
105
+ const byAgentId = new Map();
106
+ for (const agent of Array.isArray(statsAgents) ? statsAgents : []) {
107
+ const agentId = normalizeString(agent?.agentId || agent?.id);
108
+ if (!agentId) continue;
109
+ byAgentId.set(agentId, {
110
+ ...agent,
111
+ agentId,
112
+ registered: false,
113
+ source: "events",
114
+ });
115
+ }
116
+
117
+ for (const agent of Array.isArray(registeredAgents) ? registeredAgents : []) {
118
+ const agentId = normalizeString(agent?.agentId || agent?.id);
119
+ if (!agentId) continue;
120
+ const existing = byAgentId.get(agentId);
121
+ if (existing) {
122
+ byAgentId.set(agentId, {
123
+ ...existing,
124
+ model: existing.model || normalizeString(agent.model),
125
+ role: normalizeString(agent.role) || existing.role,
126
+ status: normalizeString(agent.status) || existing.status,
127
+ registered: true,
128
+ source: "events+registry",
129
+ joinedAt: normalizeString(agent.joinedAt) || existing.joinedAt,
130
+ lastActivityAt: normalizeString(agent.lastActivityAt) || existing.lastActivityAt,
131
+ active: agent.active,
132
+ });
133
+ continue;
134
+ }
135
+ byAgentId.set(agentId, {
136
+ agentId,
137
+ displayName: normalizeString(agent.displayName) || agentId,
138
+ model: normalizeString(agent.model),
139
+ role: normalizeString(agent.role),
140
+ status: normalizeString(agent.status),
141
+ firstSeen: normalizeString(agent.joinedAt) || null,
142
+ lastSeen: normalizeString(agent.lastActivityAt) || normalizeString(agent.joinedAt) || null,
143
+ joinedAt: normalizeString(agent.joinedAt) || null,
144
+ lastActivityAt: normalizeString(agent.lastActivityAt) || null,
145
+ active: agent.active,
146
+ eventCount: 0,
147
+ activeSeconds: 0,
148
+ tokens: 0,
149
+ costUsd: 0,
150
+ registered: true,
151
+ source: "registry",
152
+ });
153
+ }
154
+
155
+ return [...byAgentId.values()].sort((left, right) => {
156
+ const eventDelta = Number(right.eventCount || 0) - Number(left.eventCount || 0);
157
+ if (eventDelta !== 0) return eventDelta;
158
+ return compareIsoDesc(left.lastSeen || left.lastActivityAt, right.lastSeen || right.lastActivityAt);
159
+ });
160
+ }
161
+
99
162
  function parsePositiveInteger(rawValue, field, fallbackValue) {
100
163
  if (rawValue === undefined || rawValue === null || String(rawValue).trim() === "") {
101
164
  return fallbackValue;
@@ -1291,6 +1354,8 @@ export function registerSessionCommand(program) {
1291
1354
  agentId: resolvedAgentId,
1292
1355
  model,
1293
1356
  role,
1357
+ trackProcessExit: false,
1358
+ awaitRemoteSync: Boolean(explicitAgent),
1294
1359
  });
1295
1360
  const agentJoinRelayed =
1296
1361
  Boolean(explicitAgent) &&
@@ -2269,12 +2334,23 @@ export function registerSessionCommand(program) {
2269
2334
  limit: 5_000,
2270
2335
  }),
2271
2336
  ]);
2337
+ const stats = computeTranscriptStats({
2338
+ sessionMeta: sessionPayload,
2339
+ events,
2340
+ });
2341
+ const participants = buildSessionParticipants({
2342
+ statsAgents: stats.agents,
2343
+ registeredAgents: agents,
2344
+ });
2272
2345
 
2273
2346
  let output;
2274
2347
  if (format === "ndjson") {
2275
2348
  const lines = [];
2276
2349
  lines.push(JSON.stringify({ kind: "session", value: sessionPayload }));
2277
2350
  for (const agent of agents) lines.push(JSON.stringify({ kind: "agent", value: agent }));
2351
+ for (const participant of participants) {
2352
+ lines.push(JSON.stringify({ kind: "participant", value: participant }));
2353
+ }
2278
2354
  for (const event of events) lines.push(JSON.stringify({ kind: "event", value: event }));
2279
2355
  for (const task of tasks.tasks || []) lines.push(JSON.stringify({ kind: "task", value: task }));
2280
2356
  output = `${lines.join("\n")}\n`;
@@ -2285,13 +2361,18 @@ export function registerSessionCommand(program) {
2285
2361
  exportedAt: new Date().toISOString(),
2286
2362
  session: sessionPayload,
2287
2363
  agents,
2364
+ participants,
2288
2365
  events,
2289
2366
  tasks: tasks.tasks || [],
2290
2367
  counts: {
2291
- agents: agents.length,
2368
+ agents: participants.length,
2369
+ participants: participants.length,
2370
+ derivedAgents: stats.agents.length,
2371
+ registeredAgents: agents.length,
2292
2372
  events: events.length,
2293
2373
  tasks: (tasks.tasks || []).length,
2294
2374
  },
2375
+ totals: stats.totals,
2295
2376
  },
2296
2377
  null,
2297
2378
  2,
@@ -2305,7 +2386,7 @@ export function registerSessionCommand(program) {
2305
2386
  await fsp.writeFile(outPath, output, "utf-8");
2306
2387
  console.log(
2307
2388
  pc.gray(
2308
- `Exported ${events.length} events / ${agents.length} agents / ${
2389
+ `Exported ${events.length} events / ${participants.length} participants (${agents.length} registered agents) / ${
2309
2390
  (tasks.tasks || []).length
2310
2391
  } tasks โ†’ ${outPath}`,
2311
2392
  ),
@@ -2399,6 +2480,10 @@ export function registerSessionCommand(program) {
2399
2480
  includeSystemEvents: options.systemEvents !== false,
2400
2481
  },
2401
2482
  });
2483
+ const participants = buildSessionParticipants({
2484
+ statsAgents: stats.agents,
2485
+ registeredAgents: agents,
2486
+ });
2402
2487
 
2403
2488
  const outArg = normalizeString(options.out);
2404
2489
  const outPath = outArg
@@ -2413,7 +2498,11 @@ export function registerSessionCommand(program) {
2413
2498
  outPath,
2414
2499
  bytes: Buffer.byteLength(markdown, "utf-8"),
2415
2500
  eventCount: events.length,
2416
- agentCount: agents.length,
2501
+ agentCount: participants.length,
2502
+ participantCount: participants.length,
2503
+ derivedAgentCount: stats.agents.length,
2504
+ registeredAgentCount: agents.length,
2505
+ participants,
2417
2506
  sessionLiveSeconds: stats.sessionLiveSeconds,
2418
2507
  sentiActions: stats.sentiActions,
2419
2508
  totals: stats.totals,
@@ -2426,7 +2515,7 @@ export function registerSessionCommand(program) {
2426
2515
  console.log(pc.bold(`Downloaded session ${normalizedSessionId} โ†’ ${outPath}`));
2427
2516
  console.log(
2428
2517
  pc.gray(
2429
- `${events.length} events ยท ${agents.length} agents ยท live ${stats.sessionLiveSeconds}s ยท senti=${stats.sentiActions} ยท tokens=${stats.totals.tokenTotal} ยท cost=$${stats.totals.costTotalUsd.toFixed(4)}`,
2518
+ `${events.length} events ยท ${participants.length} participants (${agents.length} registered agents) ยท live ${stats.sessionLiveSeconds}s ยท senti=${stats.sentiActions} ยท tokens=${stats.totals.tokenTotal} ยท cost=$${stats.totals.costTotalUsd.toFixed(4)}`,
2430
2519
  ),
2431
2520
  );
2432
2521
  });
@@ -156,14 +156,19 @@ async function writeAgentSnapshot(snapshotPath, snapshot) {
156
156
  await fsp.rename(tmpPath, snapshotPath);
157
157
  }
158
158
 
159
- async function emitAgentEvent(sessionId, event, payload, { targetPath = process.cwd() } = {}) {
159
+ async function emitAgentEvent(
160
+ sessionId,
161
+ event,
162
+ payload,
163
+ { targetPath = process.cwd(), awaitRemoteSync = false } = {}
164
+ ) {
160
165
  const envelope = createAgentEvent({
161
166
  event,
162
167
  agentId: payload.agentId,
163
168
  sessionId,
164
169
  payload,
165
170
  });
166
- await appendToStream(sessionId, envelope, { targetPath });
171
+ await appendToStream(sessionId, envelope, { targetPath, awaitRemoteSync });
167
172
  }
168
173
 
169
174
  function buildAgentSnapshotPath(paths, agentId) {
@@ -238,7 +243,14 @@ function _ensureExitHooksInstalled() {
238
243
 
239
244
  export async function registerAgent(
240
245
  sessionId,
241
- { agentId = "", model = "", role = "observer", targetPath = process.cwd() } = {}
246
+ {
247
+ agentId = "",
248
+ model = "",
249
+ role = "observer",
250
+ targetPath = process.cwd(),
251
+ trackProcessExit = true,
252
+ awaitRemoteSync = false,
253
+ } = {}
242
254
  ) {
243
255
  const paths = resolveSessionPaths(sessionId, { targetPath });
244
256
  const nowIso = new Date().toISOString();
@@ -289,8 +301,10 @@ export async function registerAgent(
289
301
  model: snapshot.model,
290
302
  role: snapshot.role,
291
303
  status: snapshot.status,
292
- }, { targetPath });
293
- _trackLocalAgent(paths.sessionId, snapshot.agentId, targetPath);
304
+ }, { targetPath, awaitRemoteSync });
305
+ if (trackProcessExit) {
306
+ _trackLocalAgent(paths.sessionId, snapshot.agentId, targetPath);
307
+ }
294
308
 
295
309
  if (renamedFrom) {
296
310
  const welcome = buildSentiWelcome({
@@ -310,6 +324,7 @@ export async function registerAgent(
310
324
  await emitContextBriefing(paths.sessionId, {
311
325
  forAgentId: snapshot.agentId,
312
326
  targetPath,
327
+ awaitRemoteSync,
313
328
  }).catch(() => {});
314
329
  }
315
330
 
@@ -559,6 +559,7 @@ export async function emitContextBriefing(
559
559
  targetPath = process.cwd(),
560
560
  nowIso = new Date().toISOString(),
561
561
  includeJoinRules = true,
562
+ awaitRemoteSync = false,
562
563
  } = {}
563
564
  ) {
564
565
  const recap = await buildSessionRecap(sessionId, {
@@ -588,6 +589,7 @@ export async function emitContextBriefing(
588
589
  });
589
590
  const persisted = await appendToStream(sessionId, event, {
590
591
  targetPath,
592
+ awaitRemoteSync,
591
593
  });
592
594
  return {
593
595
  recap,
@@ -166,14 +166,25 @@ async function pollSessionEventPages({
166
166
  };
167
167
  }
168
168
 
169
+ const pageEvents = Array.isArray(result.events) ? result.events : [];
169
170
  const nextCursor =
170
171
  typeof result.cursor === "string" && result.cursor.trim() ? result.cursor.trim() : cursor;
171
172
  const progressed = nextCursor && cursorAdvances(nextCursor, cursor);
173
+ if (nextCursor && cursor && !progressed && pageEvents.length === 0) {
174
+ return {
175
+ ok: true,
176
+ reason: "",
177
+ events,
178
+ cursor,
179
+ pageCount,
180
+ complete: true,
181
+ truncated: false,
182
+ };
183
+ }
172
184
  if (nextCursor && cursor && !progressed) {
173
185
  reason = "cursor_not_advanced";
174
186
  break;
175
187
  }
176
- const pageEvents = Array.isArray(result.events) ? result.events : [];
177
188
  events.push(...pageEvents);
178
189
  cursor = nextCursor || cursor;
179
190
 
@@ -337,7 +337,12 @@ function filterBySince(events = [], since) {
337
337
  export async function appendToStream(
338
338
  sessionId,
339
339
  event,
340
- { targetPath = process.cwd(), maxEvents = DEFAULT_MAX_STREAM_EVENTS, syncRemote = true } = {}
340
+ {
341
+ targetPath = process.cwd(),
342
+ maxEvents = DEFAULT_MAX_STREAM_EVENTS,
343
+ syncRemote = true,
344
+ awaitRemoteSync = false,
345
+ } = {}
341
346
  ) {
342
347
  const paths = resolveSessionPaths(sessionId, { targetPath });
343
348
  let metadata = await readSessionMetadata(paths);
@@ -381,9 +386,14 @@ export async function appendToStream(
381
386
 
382
387
  if (syncRemote) {
383
388
  // Best-effort dashboard sync. Never block local stream durability on API state.
384
- void syncSessionEventToApi(paths.sessionId, canonicalEvent, {
389
+ const syncPromise = syncSessionEventToApi(paths.sessionId, canonicalEvent, {
385
390
  targetPath,
386
391
  }).catch(() => {});
392
+ if (awaitRemoteSync) {
393
+ await syncPromise;
394
+ } else {
395
+ void syncPromise;
396
+ }
387
397
  }
388
398
 
389
399
  return canonicalEvent;
@@ -359,26 +359,32 @@ export function buildTranscriptMarkdown({
359
359
  `| ${avatarMd(identity)} | **${agent.displayName}** \`${agent.agentId}\` | ${agent.family} | ${formatDuration(agent.activeSeconds)} | ${agent.eventCount} | ${agent.tokens.toLocaleString("en-US")} | $${agent.costUsd.toFixed(4)} |`,
360
360
  );
361
361
  }
362
- if (stats.agents.length === 0) {
363
- lines.push("| ๐Ÿ‘ค | (no agents joined) | โ€” | 0s | 0 | 0 | $0.00 |");
364
- }
365
362
  // Surface registered-but-silent agents at the bottom of the table so
366
363
  // the participants list is comprehensive even if they never emitted
367
364
  // a stream event.
368
365
  const seenIds = new Set(stats.agents.map((a) => a.agentId));
366
+ const silentRegisteredAgents = [];
369
367
  for (const registered of agents || []) {
370
368
  const id = normalize(registered?.agentId);
371
369
  if (!id || seenIds.has(id)) continue;
370
+ seenIds.add(id);
372
371
  const profile = speakerProfiles.get(id) || null;
373
372
  const identity = resolveSpeakerIdentity({
374
373
  agentId: id,
375
374
  agentModel: registered.model || "",
376
375
  profile,
377
376
  });
377
+ silentRegisteredAgents.push({ id, identity });
378
+ }
379
+ if (stats.agents.length === 0 && silentRegisteredAgents.length === 0) {
380
+ lines.push("| ๐Ÿ‘ค | (no agents joined) | โ€” | 0s | 0 | 0 | $0.00 |");
381
+ }
382
+ for (const registered of silentRegisteredAgents) {
378
383
  lines.push(
379
- `| ${avatarMd(identity)} | **${identity.displayName}** \`${id}\` | ${identity.family} | 0s ยท idle | 0 | 0 | $0.0000 |`,
384
+ `| ${avatarMd(registered.identity)} | **${registered.identity.displayName}** \`${registered.id}\` | ${registered.identity.family} | 0s ยท idle | 0 | 0 | $0.0000 |`,
380
385
  );
381
386
  }
387
+ stats.participantCount = stats.agents.length + silentRegisteredAgents.length;
382
388
  lines.push("");
383
389
 
384
390
  // Conversation