sentinelayer-cli 0.30.4 → 0.30.5
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
package/src/commands/session.js
CHANGED
|
@@ -65,6 +65,10 @@ import {
|
|
|
65
65
|
updateSessionTitle,
|
|
66
66
|
} from "../session/store.js";
|
|
67
67
|
import { fetchSessionListeners, formatListenerLine } from "../session/listeners.js";
|
|
68
|
+
import {
|
|
69
|
+
filterSessionMaterialEvents,
|
|
70
|
+
isSessionControlEvent,
|
|
71
|
+
} from "../session/control-events.js";
|
|
68
72
|
import {
|
|
69
73
|
acceptSessionInvitation,
|
|
70
74
|
normalizeSessionOnboarding,
|
|
@@ -703,6 +707,80 @@ function mergeSessionActionEvents(events = [], actionEvents = []) {
|
|
|
703
707
|
.map((entry) => entry.event);
|
|
704
708
|
}
|
|
705
709
|
|
|
710
|
+
async function pollSessionEventsBeforeTranscriptTail(sessionId, {
|
|
711
|
+
targetPath,
|
|
712
|
+
beforeSequence = null,
|
|
713
|
+
tail = 20,
|
|
714
|
+
includeControlEvents = false,
|
|
715
|
+
timeoutMs = 15_000,
|
|
716
|
+
maxPages = 10,
|
|
717
|
+
} = {}) {
|
|
718
|
+
const targetMaterialCount = Math.max(1, Math.floor(Number(tail) || 20));
|
|
719
|
+
const pageLimit = includeControlEvents
|
|
720
|
+
? Math.min(200, targetMaterialCount)
|
|
721
|
+
: 200;
|
|
722
|
+
const maxPageCount = Math.max(1, Math.min(25, Math.floor(Number(maxPages) || 10)));
|
|
723
|
+
let nextBeforeSequence = beforeSequence;
|
|
724
|
+
let latestResult = null;
|
|
725
|
+
let combinedEvents = [];
|
|
726
|
+
let pageCount = 0;
|
|
727
|
+
|
|
728
|
+
for (let page = 0; page < maxPageCount; page += 1) {
|
|
729
|
+
const result = await pollSessionEventsBefore(sessionId, {
|
|
730
|
+
targetPath,
|
|
731
|
+
beforeSequence: nextBeforeSequence,
|
|
732
|
+
limit: pageLimit,
|
|
733
|
+
timeoutMs,
|
|
734
|
+
});
|
|
735
|
+
pageCount += 1;
|
|
736
|
+
if (!result?.ok) {
|
|
737
|
+
if (!latestResult) return { ...result, pageCount };
|
|
738
|
+
latestResult = {
|
|
739
|
+
...latestResult,
|
|
740
|
+
reason: result?.reason || latestResult.reason || "partial_tail_fetch",
|
|
741
|
+
partial: true,
|
|
742
|
+
};
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
latestResult = result;
|
|
747
|
+
const pageEvents = Array.isArray(result.events) ? result.events : [];
|
|
748
|
+
combinedEvents = [...pageEvents, ...combinedEvents];
|
|
749
|
+
const materialCount = includeControlEvents
|
|
750
|
+
? combinedEvents.length
|
|
751
|
+
: filterSessionMaterialEvents(combinedEvents).length;
|
|
752
|
+
|
|
753
|
+
if (includeControlEvents || materialCount >= targetMaterialCount) break;
|
|
754
|
+
if (pageEvents.length < pageLimit) break;
|
|
755
|
+
|
|
756
|
+
const candidateBefore = Number(result.beforeSequence || 0);
|
|
757
|
+
if (!Number.isFinite(candidateBefore) || candidateBefore <= 0) break;
|
|
758
|
+
const currentBefore = Number(nextBeforeSequence || 0);
|
|
759
|
+
if (currentBefore > 0 && candidateBefore >= currentBefore) break;
|
|
760
|
+
nextBeforeSequence = candidateBefore;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (!latestResult) {
|
|
764
|
+
return {
|
|
765
|
+
ok: true,
|
|
766
|
+
reason: "",
|
|
767
|
+
events: [],
|
|
768
|
+
cursor: null,
|
|
769
|
+
beforeSequence: beforeSequence || null,
|
|
770
|
+
pageCount,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
...latestResult,
|
|
776
|
+
events: combinedEvents,
|
|
777
|
+
cursor: normalizeString(combinedEvents[combinedEvents.length - 1]?.cursor) || latestResult.cursor || null,
|
|
778
|
+
pageCount,
|
|
779
|
+
materialBackfillComplete:
|
|
780
|
+
includeControlEvents || filterSessionMaterialEvents(combinedEvents).length >= targetMaterialCount,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
706
784
|
async function appendActionEventIfMissing(sessionId, actionEvent, { targetPath } = {}) {
|
|
707
785
|
if (!actionEvent) return { appended: false, reason: "no_event", event: null };
|
|
708
786
|
const existingEvents = await readStream(sessionId, { targetPath, tail: 0 }).catch(() => []);
|
|
@@ -1604,6 +1682,54 @@ function compactPayload(record = {}) {
|
|
|
1604
1682
|
);
|
|
1605
1683
|
}
|
|
1606
1684
|
|
|
1685
|
+
function listenerPresenceHeartbeatFingerprint(lifecycle = {}) {
|
|
1686
|
+
return JSON.stringify(
|
|
1687
|
+
compactPayload({
|
|
1688
|
+
active: Boolean(lifecycle.active),
|
|
1689
|
+
state: normalizeString(lifecycle.state) || "heartbeat",
|
|
1690
|
+
transport: normalizeString(lifecycle.transport) || null,
|
|
1691
|
+
reason: normalizeString(lifecycle.reason) || null,
|
|
1692
|
+
idleIntervalSeconds: lifecycle.idleIntervalSeconds ?? null,
|
|
1693
|
+
activeIntervalSeconds: lifecycle.activeIntervalSeconds ?? null,
|
|
1694
|
+
activeWindowSeconds: lifecycle.activeWindowSeconds ?? null,
|
|
1695
|
+
lastHumanActivityAt: lifecycle.lastHumanActivityAt || null,
|
|
1696
|
+
}),
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
export function shouldPublishListenerPresenceHeartbeat({
|
|
1701
|
+
lifecycle = {},
|
|
1702
|
+
nowMs = Date.now(),
|
|
1703
|
+
lastHeartbeatMs = 0,
|
|
1704
|
+
lastFingerprint = "",
|
|
1705
|
+
presenceIntervalMs = 60_000,
|
|
1706
|
+
presenceKeepaliveMs = 180_000,
|
|
1707
|
+
} = {}) {
|
|
1708
|
+
const lifecycleType = normalizeString(lifecycle.type) || "heartbeat";
|
|
1709
|
+
if (lifecycleType !== "heartbeat") {
|
|
1710
|
+
return { publish: true, fingerprint: lastFingerprint, reason: "lifecycle" };
|
|
1711
|
+
}
|
|
1712
|
+
const fingerprint = listenerPresenceHeartbeatFingerprint(lifecycle);
|
|
1713
|
+
if (lifecycle.stopping) {
|
|
1714
|
+
return { publish: true, fingerprint, reason: "stopping" };
|
|
1715
|
+
}
|
|
1716
|
+
const currentMs = Number(nowMs) || Date.now();
|
|
1717
|
+
const previousMs = Number(lastHeartbeatMs) || 0;
|
|
1718
|
+
if (previousMs <= 0) {
|
|
1719
|
+
return { publish: true, fingerprint, reason: "first" };
|
|
1720
|
+
}
|
|
1721
|
+
if (fingerprint !== lastFingerprint) {
|
|
1722
|
+
return { publish: true, fingerprint, reason: "changed" };
|
|
1723
|
+
}
|
|
1724
|
+
const minIntervalMs = Math.max(1, Number(presenceIntervalMs) || 60_000);
|
|
1725
|
+
const keepaliveMs = Math.max(minIntervalMs, Number(presenceKeepaliveMs) || 180_000);
|
|
1726
|
+
const elapsedMs = currentMs - previousMs;
|
|
1727
|
+
if (elapsedMs >= keepaliveMs) {
|
|
1728
|
+
return { publish: true, fingerprint, reason: "keepalive" };
|
|
1729
|
+
}
|
|
1730
|
+
return { publish: false, fingerprint, reason: elapsedMs < minIntervalMs ? "interval" : "unchanged" };
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1607
1733
|
async function publishListenerPresenceEvent({
|
|
1608
1734
|
sessionId,
|
|
1609
1735
|
targetPath,
|
|
@@ -1653,6 +1779,8 @@ async function publishListenerPresenceEvent({
|
|
|
1653
1779
|
idleIntervalSeconds: lifecycle.idleIntervalSeconds,
|
|
1654
1780
|
activeIntervalSeconds: lifecycle.activeIntervalSeconds,
|
|
1655
1781
|
activeWindowSeconds: lifecycle.activeWindowSeconds,
|
|
1782
|
+
presenceIntervalSeconds: lifecycle.presenceIntervalSeconds,
|
|
1783
|
+
presenceKeepaliveSeconds: lifecycle.presenceKeepaliveSeconds,
|
|
1656
1784
|
lastHumanActivityAt: lifecycle.lastHumanActivityAt,
|
|
1657
1785
|
lastSleepMs: lifecycle.lastSleepMs,
|
|
1658
1786
|
nextPollMs: lifecycle.nextPollMs,
|
|
@@ -2117,13 +2245,6 @@ function formatEventLine(event = {}) {
|
|
|
2117
2245
|
return `${ts} ${agentId} ${type}`;
|
|
2118
2246
|
}
|
|
2119
2247
|
|
|
2120
|
-
function isSessionControlEvent(event = {}) {
|
|
2121
|
-
const payload = event?.payload && typeof event.payload === "object" ? event.payload : {};
|
|
2122
|
-
const type = normalizeString(event.event || event.type).toLowerCase();
|
|
2123
|
-
if (type.startsWith("session_listener_")) return true;
|
|
2124
|
-
return normalizeString(payload.source).toLowerCase() === "session_listen";
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
2248
|
function summarizeSessionTailEvent(event = null) {
|
|
2128
2249
|
if (!event || typeof event !== "object") return null;
|
|
2129
2250
|
return {
|
|
@@ -4059,6 +4180,11 @@ export function registerSessionCommand(program) {
|
|
|
4059
4180
|
"Minimum seconds between remote listener heartbeat events (default 60)",
|
|
4060
4181
|
"60",
|
|
4061
4182
|
)
|
|
4183
|
+
.option(
|
|
4184
|
+
"--presence-keepalive <seconds>",
|
|
4185
|
+
"Maximum seconds between unchanged remote listener heartbeat events (default 180)",
|
|
4186
|
+
"180",
|
|
4187
|
+
)
|
|
4062
4188
|
.option(
|
|
4063
4189
|
"--no-presence",
|
|
4064
4190
|
"Do not publish durable listener lifecycle/heartbeat events",
|
|
@@ -4111,6 +4237,11 @@ export function registerSessionCommand(program) {
|
|
|
4111
4237
|
"presence-interval",
|
|
4112
4238
|
60,
|
|
4113
4239
|
);
|
|
4240
|
+
const presenceKeepaliveSeconds = parsePositiveInteger(
|
|
4241
|
+
options.presenceKeepalive,
|
|
4242
|
+
"presence-keepalive",
|
|
4243
|
+
180,
|
|
4244
|
+
);
|
|
4114
4245
|
const listenerIdentity = inferSessionAgentIdentity({
|
|
4115
4246
|
agentId,
|
|
4116
4247
|
model: options.model,
|
|
@@ -4193,7 +4324,10 @@ export function registerSessionCommand(program) {
|
|
|
4193
4324
|
const durablePresenceEnabled = options.presence !== false;
|
|
4194
4325
|
const publishPresence = durablePresenceEnabled && canPublishListenerPresence(agentId);
|
|
4195
4326
|
const presenceIntervalMs = Math.max(1, presenceIntervalSeconds) * 1000;
|
|
4327
|
+
const presenceKeepaliveMs =
|
|
4328
|
+
Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds, 1) * 1000;
|
|
4196
4329
|
let lastPresenceHeartbeatMs = 0;
|
|
4330
|
+
let lastPresenceHeartbeatFingerprint = "";
|
|
4197
4331
|
|
|
4198
4332
|
// Periodic in-session success reminders (ack, claim work, reply
|
|
4199
4333
|
// in-thread). Long-running interactive listeners only — skipped under
|
|
@@ -4375,11 +4509,25 @@ export function registerSessionCommand(program) {
|
|
|
4375
4509
|
}
|
|
4376
4510
|
if (!publishPresence) return;
|
|
4377
4511
|
const lifecycleType = normalizeString(lifecycle?.type);
|
|
4512
|
+
const publishLifecycle = {
|
|
4513
|
+
...lifecycle,
|
|
4514
|
+
presenceIntervalSeconds,
|
|
4515
|
+
presenceKeepaliveSeconds: Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds),
|
|
4516
|
+
};
|
|
4378
4517
|
if (lifecycleType === "heartbeat") {
|
|
4379
4518
|
const nowMs = Date.now();
|
|
4380
|
-
|
|
4519
|
+
const heartbeatDecision = shouldPublishListenerPresenceHeartbeat({
|
|
4520
|
+
lifecycle: publishLifecycle,
|
|
4521
|
+
nowMs,
|
|
4522
|
+
lastHeartbeatMs: lastPresenceHeartbeatMs,
|
|
4523
|
+
lastFingerprint: lastPresenceHeartbeatFingerprint,
|
|
4524
|
+
presenceIntervalMs,
|
|
4525
|
+
presenceKeepaliveMs,
|
|
4526
|
+
});
|
|
4527
|
+
if (!heartbeatDecision.publish) {
|
|
4381
4528
|
return;
|
|
4382
4529
|
}
|
|
4530
|
+
lastPresenceHeartbeatFingerprint = heartbeatDecision.fingerprint;
|
|
4383
4531
|
lastPresenceHeartbeatMs = nowMs;
|
|
4384
4532
|
}
|
|
4385
4533
|
await publishListenerPresenceEvent({
|
|
@@ -4391,7 +4539,7 @@ export function registerSessionCommand(program) {
|
|
|
4391
4539
|
provider,
|
|
4392
4540
|
clientKind,
|
|
4393
4541
|
listenerId,
|
|
4394
|
-
lifecycle,
|
|
4542
|
+
lifecycle: publishLifecycle,
|
|
4395
4543
|
});
|
|
4396
4544
|
},
|
|
4397
4545
|
});
|
|
@@ -4914,9 +5062,6 @@ export function registerSessionCommand(program) {
|
|
|
4914
5062
|
const beforeSequence = parseOptionalPositiveInteger(options.beforeSequence, "before-sequence");
|
|
4915
5063
|
const emitJson = shouldEmitJson(options, command);
|
|
4916
5064
|
const includeControlEvents = Boolean(options.includeControlEvents);
|
|
4917
|
-
const remoteTailLimit = includeControlEvents
|
|
4918
|
-
? tail
|
|
4919
|
-
: Math.min(200, Math.max(tail, tail + 20));
|
|
4920
5065
|
const readAgentId = await defaultAgentId(options.agent, targetPath);
|
|
4921
5066
|
|
|
4922
5067
|
let hydration = null;
|
|
@@ -4935,11 +5080,13 @@ export function registerSessionCommand(program) {
|
|
|
4935
5080
|
hydration = await hydrateSessionFromRemote({
|
|
4936
5081
|
sessionId: normalizedSessionId,
|
|
4937
5082
|
targetPath,
|
|
5083
|
+
includeControlEvents,
|
|
4938
5084
|
});
|
|
4939
|
-
remoteTail = await
|
|
5085
|
+
remoteTail = await pollSessionEventsBeforeTranscriptTail(normalizedSessionId, {
|
|
4940
5086
|
targetPath,
|
|
4941
5087
|
beforeSequence,
|
|
4942
|
-
|
|
5088
|
+
tail,
|
|
5089
|
+
includeControlEvents,
|
|
4943
5090
|
timeoutMs: 15_000,
|
|
4944
5091
|
});
|
|
4945
5092
|
if (options.actions !== false) {
|
|
@@ -5016,14 +5163,17 @@ export function registerSessionCommand(program) {
|
|
|
5016
5163
|
let remoteTailAppended = 0;
|
|
5017
5164
|
let remoteTailDisplayedOnly = 0;
|
|
5018
5165
|
let remoteTailUpgraded = 0;
|
|
5019
|
-
|
|
5166
|
+
const remoteTailAppendEvents = includeControlEvents
|
|
5167
|
+
? remoteTail?.events
|
|
5168
|
+
: filterSessionMaterialEvents(remoteTail?.events || []);
|
|
5169
|
+
if (remoteTail?.ok && Array.isArray(remoteTailAppendEvents) && remoteTailAppendEvents.length > 0) {
|
|
5020
5170
|
const knownKeys = new Set();
|
|
5021
5171
|
const displayIndexByKey = new Map();
|
|
5022
5172
|
for (const [index, event] of displayEvents.entries()) {
|
|
5023
5173
|
addSessionEventIdentityKeys(knownKeys, event);
|
|
5024
5174
|
indexSessionEventIdentityKeys(displayIndexByKey, displayEvents, event, index);
|
|
5025
5175
|
}
|
|
5026
|
-
for (const event of
|
|
5176
|
+
for (const event of remoteTailAppendEvents) {
|
|
5027
5177
|
const existingIndex = findSessionEventIdentityIndex(displayIndexByKey, event);
|
|
5028
5178
|
if (existingIndex >= 0) {
|
|
5029
5179
|
const existing = displayEvents[existingIndex];
|
|
@@ -5115,6 +5265,8 @@ export function registerSessionCommand(program) {
|
|
|
5115
5265
|
count: Array.isArray(remoteTail.events) ? remoteTail.events.length : 0,
|
|
5116
5266
|
cursor: remoteTail.cursor || null,
|
|
5117
5267
|
beforeSequence: remoteTail.beforeSequence || null,
|
|
5268
|
+
pageCount: Number(remoteTail.pageCount || 0),
|
|
5269
|
+
materialBackfillComplete: Boolean(remoteTail.materialBackfillComplete),
|
|
5118
5270
|
verified: Boolean(remoteTail.ok),
|
|
5119
5271
|
appended: remoteTailAppended,
|
|
5120
5272
|
displayedOnly: remoteTailDisplayedOnly,
|
|
@@ -5190,6 +5342,7 @@ export function registerSessionCommand(program) {
|
|
|
5190
5342
|
signal: ac.signal,
|
|
5191
5343
|
})) {
|
|
5192
5344
|
if (item.event) {
|
|
5345
|
+
if (!includeControlEvents && isSessionControlEvent(item.event)) continue;
|
|
5193
5346
|
if (emitJson) {
|
|
5194
5347
|
console.log(JSON.stringify({ source: item.source, event: item.event }));
|
|
5195
5348
|
} else {
|
|
@@ -5218,6 +5371,9 @@ export function registerSessionCommand(program) {
|
|
|
5218
5371
|
continue;
|
|
5219
5372
|
}
|
|
5220
5373
|
addSessionEventIdentityKeys(seenFollowEvents, event);
|
|
5374
|
+
if (!includeControlEvents && isSessionControlEvent(event)) {
|
|
5375
|
+
continue;
|
|
5376
|
+
}
|
|
5221
5377
|
if (emitJson) {
|
|
5222
5378
|
console.log(JSON.stringify(event));
|
|
5223
5379
|
} else {
|
|
@@ -5659,6 +5815,7 @@ export function registerSessionCommand(program) {
|
|
|
5659
5815
|
"Hydrate from the SentinelLayer API before exporting and include remote message actions",
|
|
5660
5816
|
)
|
|
5661
5817
|
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
5818
|
+
.option("--include-control-events", "Include listener/control-plane events in the exported transcript")
|
|
5662
5819
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
5663
5820
|
.action(async (sessionId, options) => {
|
|
5664
5821
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -5676,6 +5833,7 @@ export function registerSessionCommand(program) {
|
|
|
5676
5833
|
hydration = await hydrateSessionFromRemote({
|
|
5677
5834
|
sessionId: normalizedSessionId,
|
|
5678
5835
|
targetPath,
|
|
5836
|
+
includeControlEvents: Boolean(options.includeControlEvents),
|
|
5679
5837
|
}).catch((error) => ({ ok: false, reason: error?.message || "hydrate_failed" }));
|
|
5680
5838
|
if (options.actions !== false) {
|
|
5681
5839
|
remoteActions = await listSessionMessageActions(normalizedSessionId, {
|
|
@@ -5708,7 +5866,12 @@ export function registerSessionCommand(program) {
|
|
|
5708
5866
|
const actionEvents = remoteActions?.ok
|
|
5709
5867
|
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
5710
5868
|
: [];
|
|
5711
|
-
const
|
|
5869
|
+
const includeControlEvents = Boolean(options.includeControlEvents);
|
|
5870
|
+
const transcriptBaseEvents = includeControlEvents
|
|
5871
|
+
? events
|
|
5872
|
+
: filterSessionMaterialEvents(events);
|
|
5873
|
+
const hiddenControlEventCount = events.length - transcriptBaseEvents.length;
|
|
5874
|
+
const exportEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
|
|
5712
5875
|
const stats = computeTranscriptStats({
|
|
5713
5876
|
sessionMeta: sessionPayload,
|
|
5714
5877
|
events: exportEvents,
|
|
@@ -5722,6 +5885,15 @@ export function registerSessionCommand(program) {
|
|
|
5722
5885
|
if (format === "ndjson") {
|
|
5723
5886
|
const lines = [];
|
|
5724
5887
|
lines.push(JSON.stringify({ kind: "session", value: sessionPayload }));
|
|
5888
|
+
lines.push(JSON.stringify({
|
|
5889
|
+
kind: "export_metadata",
|
|
5890
|
+
value: {
|
|
5891
|
+
includeControlEvents,
|
|
5892
|
+
hiddenControlEventCount,
|
|
5893
|
+
rawEventCount: events.length,
|
|
5894
|
+
eventCount: exportEvents.length,
|
|
5895
|
+
},
|
|
5896
|
+
}));
|
|
5725
5897
|
for (const agent of agents) lines.push(JSON.stringify({ kind: "agent", value: agent }));
|
|
5726
5898
|
for (const participant of participants) {
|
|
5727
5899
|
lines.push(JSON.stringify({ kind: "participant", value: participant }));
|
|
@@ -5763,10 +5935,12 @@ export function registerSessionCommand(program) {
|
|
|
5763
5935
|
registeredAgents: agents.length,
|
|
5764
5936
|
events: exportEvents.length,
|
|
5765
5937
|
rawEvents: events.length,
|
|
5938
|
+
hiddenControlEvents: hiddenControlEventCount,
|
|
5766
5939
|
actions: Array.isArray(remoteActions?.actions) ? remoteActions.actions.length : 0,
|
|
5767
5940
|
actionEvents: actionEvents.length,
|
|
5768
5941
|
tasks: (tasks.tasks || []).length,
|
|
5769
5942
|
},
|
|
5943
|
+
includeControlEvents,
|
|
5770
5944
|
totals: stats.totals,
|
|
5771
5945
|
},
|
|
5772
5946
|
null,
|
|
@@ -5783,7 +5957,7 @@ export function registerSessionCommand(program) {
|
|
|
5783
5957
|
pc.gray(
|
|
5784
5958
|
`Exported ${exportEvents.length} events / ${participants.length} participants (${agents.length} registered agents) / ${
|
|
5785
5959
|
(tasks.tasks || []).length
|
|
5786
|
-
} tasks → ${outPath}`,
|
|
5960
|
+
} tasks${hiddenControlEventCount ? ` (${hiddenControlEventCount} control events omitted)` : ""} → ${outPath}`,
|
|
5787
5961
|
),
|
|
5788
5962
|
);
|
|
5789
5963
|
} else {
|
|
@@ -5806,6 +5980,7 @@ export function registerSessionCommand(program) {
|
|
|
5806
5980
|
"Hydrate from the SentinelLayer API before rendering (pulls web-posted messages into the local NDJSON)",
|
|
5807
5981
|
)
|
|
5808
5982
|
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
5983
|
+
.option("--include-control-events", "Include listener/control-plane events in the Markdown transcript")
|
|
5809
5984
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
5810
5985
|
.option("--json", "Emit machine-readable output")
|
|
5811
5986
|
.action(async (sessionId, options, command) => {
|
|
@@ -5822,6 +5997,7 @@ export function registerSessionCommand(program) {
|
|
|
5822
5997
|
hydration = await hydrateSessionFromRemote({
|
|
5823
5998
|
sessionId: normalizedSessionId,
|
|
5824
5999
|
targetPath,
|
|
6000
|
+
includeControlEvents: Boolean(options.includeControlEvents),
|
|
5825
6001
|
}).catch((error) => ({ ok: false, reason: error?.message || "hydrate_failed" }));
|
|
5826
6002
|
if (options.actions !== false) {
|
|
5827
6003
|
remoteActions = await listSessionMessageActions(normalizedSessionId, {
|
|
@@ -5844,7 +6020,12 @@ export function registerSessionCommand(program) {
|
|
|
5844
6020
|
const actionEvents = remoteActions?.ok
|
|
5845
6021
|
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
5846
6022
|
: [];
|
|
5847
|
-
const
|
|
6023
|
+
const includeControlEvents = Boolean(options.includeControlEvents);
|
|
6024
|
+
const transcriptBaseEvents = includeControlEvents
|
|
6025
|
+
? events
|
|
6026
|
+
: filterSessionMaterialEvents(events);
|
|
6027
|
+
const hiddenControlEventCount = events.length - transcriptBaseEvents.length;
|
|
6028
|
+
const transcriptEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
|
|
5848
6029
|
|
|
5849
6030
|
// Pull GitHub/Google avatar + display name from the active auth
|
|
5850
6031
|
// session so any human-id seen in the stream renders with the
|
|
@@ -5909,6 +6090,8 @@ export function registerSessionCommand(program) {
|
|
|
5909
6090
|
rawEventCount: events.length,
|
|
5910
6091
|
actionCount: Array.isArray(remoteActions?.actions) ? remoteActions.actions.length : 0,
|
|
5911
6092
|
actionEventCount: actionEvents.length,
|
|
6093
|
+
includeControlEvents,
|
|
6094
|
+
hiddenControlEventCount,
|
|
5912
6095
|
agentCount: participants.length,
|
|
5913
6096
|
participantCount: participants.length,
|
|
5914
6097
|
derivedAgentCount: stats.agents.length,
|
|
@@ -5940,6 +6123,9 @@ export function registerSessionCommand(program) {
|
|
|
5940
6123
|
`${transcriptEvents.length} events · ${participants.length} participants (${agents.length} registered agents) · actions=${payload.actionCount} · live ${stats.sessionLiveSeconds}s · senti=${stats.sentiActions} · tokens=${stats.totals.tokenTotal} · cost=$${stats.totals.costTotalUsd.toFixed(4)}`,
|
|
5941
6124
|
),
|
|
5942
6125
|
);
|
|
6126
|
+
if (hiddenControlEventCount > 0) {
|
|
6127
|
+
console.log(pc.gray(`${hiddenControlEventCount} control events omitted; rerun with --include-control-events to inspect them.`));
|
|
6128
|
+
}
|
|
5943
6129
|
});
|
|
5944
6130
|
|
|
5945
6131
|
session
|
|
@@ -5,6 +5,7 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
5
5
|
import { createAgentEvent } from "../events/schema.js";
|
|
6
6
|
import { appendToStream } from "../session/stream.js";
|
|
7
7
|
import { eventMatchesAgent } from "../session/listener.js";
|
|
8
|
+
import { isSessionControlEvent } from "../session/control-events.js";
|
|
8
9
|
import {
|
|
9
10
|
listSessionMessageActions,
|
|
10
11
|
pollSessionEvents,
|
|
@@ -102,20 +103,6 @@ function eventAgentId(event = {}) {
|
|
|
102
103
|
return normalizeAgentId(event?.agent?.id || event?.agentId || event?.agent_id, "");
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
function eventName(event = {}) {
|
|
106
|
-
return normalizeString(event?.event || event?.type).toLowerCase();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function isControlEvent(event = {}) {
|
|
110
|
-
const name = eventName(event);
|
|
111
|
-
const payload = isPlainObject(event?.payload) ? event.payload : {};
|
|
112
|
-
return (
|
|
113
|
-
name.startsWith("session_listen_") ||
|
|
114
|
-
name === "agent_heartbeat" ||
|
|
115
|
-
normalizeString(payload.source).toLowerCase() === "session_listen"
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
106
|
function safePayload(payload = {}) {
|
|
120
107
|
if (!isPlainObject(payload)) return {};
|
|
121
108
|
const next = { ...payload };
|
|
@@ -627,7 +614,7 @@ export function createSessionMcpToolHandlers({
|
|
|
627
614
|
}
|
|
628
615
|
|
|
629
616
|
const events = (Array.isArray(result.events) ? result.events : []).filter((event) => {
|
|
630
|
-
if (!includeControlEvents &&
|
|
617
|
+
if (!includeControlEvents && isSessionControlEvent(event)) return false;
|
|
631
618
|
if (!includeSelf && eventAgentId(event) === agentId) return false;
|
|
632
619
|
return eventMatchesAgent(event, agentId);
|
|
633
620
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const SESSION_CONTROL_EVENT_TYPES = new Set([
|
|
2
|
+
"listener_stop",
|
|
3
|
+
"session_coaching",
|
|
4
|
+
"session_listen_catchup",
|
|
5
|
+
"session_listen_error",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function normalizeString(value) {
|
|
9
|
+
return String(value || "").trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function eventType(event = {}) {
|
|
13
|
+
return normalizeString(event?.event || event?.type).toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function payloadSource(event = {}) {
|
|
17
|
+
const payload = event?.payload && typeof event.payload === "object" && !Array.isArray(event.payload)
|
|
18
|
+
? event.payload
|
|
19
|
+
: {};
|
|
20
|
+
return normalizeString(payload.source).toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isSessionListenerLifecycleEvent(event = {}) {
|
|
24
|
+
const type = eventType(event);
|
|
25
|
+
if (type.startsWith("session_listener_")) return true;
|
|
26
|
+
return payloadSource(event) === "session_listen";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isSessionControlEvent(event = {}) {
|
|
30
|
+
const type = eventType(event);
|
|
31
|
+
return (
|
|
32
|
+
isSessionListenerLifecycleEvent(event) ||
|
|
33
|
+
type.startsWith("session_listen_") ||
|
|
34
|
+
SESSION_CONTROL_EVENT_TYPES.has(type)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function filterSessionMaterialEvents(events = []) {
|
|
39
|
+
return (Array.isArray(events) ? events : []).filter((event) => !isSessionControlEvent(event));
|
|
40
|
+
}
|
package/src/session/listener.js
CHANGED
|
@@ -2,6 +2,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
2
2
|
|
|
3
3
|
import { pollSessionEvents, pollSessionEventsBefore, streamSessionEvents } from "./sync.js";
|
|
4
4
|
import { cursorAdvances, readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
5
|
+
import { isSessionListenerLifecycleEvent } from "./control-events.js";
|
|
5
6
|
|
|
6
7
|
const BROADCAST_RECIPIENTS = new Set([
|
|
7
8
|
"*",
|
|
@@ -223,14 +224,6 @@ function isRecentActivity(activityMs, nowMs, windowMs) {
|
|
|
223
224
|
);
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
function isListenerLifecycleEvent(event = {}) {
|
|
227
|
-
if (!isPlainObject(event)) return false;
|
|
228
|
-
const payload = isPlainObject(event.payload) ? event.payload : {};
|
|
229
|
-
const eventType = normalizeString(event.event || event.type).toLowerCase();
|
|
230
|
-
if (eventType.startsWith("session_listener_")) return true;
|
|
231
|
-
return normalizeString(payload.source).toLowerCase() === "session_listen";
|
|
232
|
-
}
|
|
233
|
-
|
|
234
227
|
/**
|
|
235
228
|
* Poll session events in the background and emit only events addressed to
|
|
236
229
|
* the current agent or broadcast to everyone. The loop advances its cursor
|
|
@@ -409,7 +402,7 @@ export async function listenSessionEvents({
|
|
|
409
402
|
if (isRecentActivity(activityMs, observedAtMs, activeWindowMs)) {
|
|
410
403
|
lastHumanActivityMs = Math.max(lastHumanActivityMs, activityMs);
|
|
411
404
|
}
|
|
412
|
-
if (
|
|
405
|
+
if (isSessionListenerLifecycleEvent(event)) continue;
|
|
413
406
|
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
414
407
|
visibleEvents.push(event);
|
|
415
408
|
}
|
package/src/session/listeners.js
CHANGED
|
@@ -56,6 +56,8 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
|
|
|
56
56
|
const active = Boolean(payload.active);
|
|
57
57
|
const idleIntervalSeconds = positiveInt(payload.idleIntervalSeconds);
|
|
58
58
|
const activeIntervalSeconds = positiveInt(payload.activeIntervalSeconds);
|
|
59
|
+
const presenceKeepaliveSeconds = positiveInt(payload.presenceKeepaliveSeconds);
|
|
60
|
+
const presenceIntervalSeconds = positiveInt(payload.presenceIntervalSeconds);
|
|
59
61
|
const nextPollSeconds = positiveInt(payload.nextPollMs)
|
|
60
62
|
? Math.round(positiveInt(payload.nextPollMs) / 1000)
|
|
61
63
|
: null;
|
|
@@ -64,9 +66,20 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
|
|
|
64
66
|
const cadenceSeconds = active
|
|
65
67
|
? activeIntervalSeconds || nextPollSeconds
|
|
66
68
|
: idleIntervalSeconds || nextPollSeconds;
|
|
69
|
+
const expectedPresenceSeconds =
|
|
70
|
+
presenceKeepaliveSeconds ||
|
|
71
|
+
presenceIntervalSeconds ||
|
|
72
|
+
cadenceSeconds ||
|
|
73
|
+
idleIntervalSeconds ||
|
|
74
|
+
activeIntervalSeconds ||
|
|
75
|
+
null;
|
|
76
|
+
const staleAfterForRowMs = Math.max(
|
|
77
|
+
staleAfterMs,
|
|
78
|
+
expectedPresenceSeconds ? expectedPresenceSeconds * 2_500 : 0,
|
|
79
|
+
);
|
|
67
80
|
let status;
|
|
68
81
|
if (stopped) status = "stopped";
|
|
69
|
-
else if (ageMs !== null && ageMs >
|
|
82
|
+
else if (ageMs !== null && ageMs > staleAfterForRowMs) status = "stale";
|
|
70
83
|
else status = active ? "active" : "idle";
|
|
71
84
|
|
|
72
85
|
rows.push({
|
|
@@ -78,6 +91,8 @@ export function summarizeListeners(events = [], { nowMs = Date.now(), staleAfter
|
|
|
78
91
|
cadenceSeconds: cadenceSeconds ?? null,
|
|
79
92
|
idleIntervalSeconds,
|
|
80
93
|
activeIntervalSeconds,
|
|
94
|
+
presenceIntervalSeconds,
|
|
95
|
+
presenceKeepaliveSeconds,
|
|
81
96
|
nextPollSeconds,
|
|
82
97
|
lastSeenAt: epoch ? new Date(epoch).toISOString() : null,
|
|
83
98
|
lastSeenAgoSeconds: ageMs !== null ? Math.round(ageMs / 1000) : null,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
sessionEventHasKnownIdentity,
|
|
34
34
|
sessionEventUpgradesExisting,
|
|
35
35
|
} from "./event-identity.js";
|
|
36
|
+
import { isSessionControlEvent } from "./control-events.js";
|
|
36
37
|
|
|
37
38
|
const EVENTS_CURSOR_SUFFIX = "events";
|
|
38
39
|
const DEFAULT_EVENT_PAGE_LIMIT = 200;
|
|
@@ -273,6 +274,7 @@ export async function hydrateSessionFromRemote({
|
|
|
273
274
|
probeOpenCircuit = true,
|
|
274
275
|
eventPageLimit = DEFAULT_EVENT_PAGE_LIMIT,
|
|
275
276
|
maxEventPages = DEFAULT_MAX_EVENT_PAGES,
|
|
277
|
+
includeControlEvents = false,
|
|
276
278
|
} = {}) {
|
|
277
279
|
if (!sessionId || typeof sessionId !== "string") {
|
|
278
280
|
return {
|
|
@@ -392,7 +394,17 @@ export async function hydrateSessionFromRemote({
|
|
|
392
394
|
newEvents.push(event);
|
|
393
395
|
}
|
|
394
396
|
}
|
|
395
|
-
|
|
397
|
+
let skippedControlEventCount = 0;
|
|
398
|
+
const materialNewEvents = [];
|
|
399
|
+
for (const event of newEvents) {
|
|
400
|
+
if (!includeControlEvents && isSessionControlEvent(event)) {
|
|
401
|
+
skippedControlEventCount += 1;
|
|
402
|
+
addSessionEventIdentityKeys(successfulRelayKeys, event);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
materialNewEvents.push(event);
|
|
406
|
+
}
|
|
407
|
+
if (materialNewEvents.length > 0) {
|
|
396
408
|
try {
|
|
397
409
|
const localSession = await _ensureLocalSession(sessionId, { targetPath });
|
|
398
410
|
materializedLocalSession = Boolean(localSession?.materialized);
|
|
@@ -403,10 +415,11 @@ export async function hydrateSessionFromRemote({
|
|
|
403
415
|
// returns a structured result.
|
|
404
416
|
}
|
|
405
417
|
}
|
|
418
|
+
|
|
406
419
|
const appendEvents =
|
|
407
420
|
remoteStatus && !["active", "pending"].includes(remoteStatus)
|
|
408
|
-
?
|
|
409
|
-
:
|
|
421
|
+
? materialNewEvents.map((event) => markPostKillEvent(event))
|
|
422
|
+
: materialNewEvents;
|
|
410
423
|
const failedRelayKeys = new Set();
|
|
411
424
|
for (const event of appendEvents) {
|
|
412
425
|
try {
|
|
@@ -451,6 +464,7 @@ export async function hydrateSessionFromRemote({
|
|
|
451
464
|
persistedCursor,
|
|
452
465
|
humanRelayed: (humanResult?.events || []).length,
|
|
453
466
|
eventsRelayed: (eventsResult?.events || []).length,
|
|
467
|
+
controlEventsSkipped: skippedControlEventCount,
|
|
454
468
|
eventsCursor:
|
|
455
469
|
typeof eventsResult?.cursor === "string" ? eventsResult.cursor : eventsCursor || null,
|
|
456
470
|
eventsPageCount: Number(eventsResult?.pageCount || 0),
|