sentinelayer-cli 0.11.3 โ 0.12.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.
- package/package.json +1 -1
- package/src/commands/session.js +584 -12
- package/src/session/agent-registry.js +20 -5
- package/src/session/recap.js +2 -0
- package/src/session/remote-hydrate.js +12 -1
- package/src/session/stream.js +12 -2
- package/src/session/sync.js +343 -0
- package/src/session/transcript.js +13 -4
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fsp from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
|
|
@@ -59,9 +59,12 @@ import {
|
|
|
59
59
|
} from "../session/event-identity.js";
|
|
60
60
|
import { readSessionPreview } from "../session/preview.js";
|
|
61
61
|
import {
|
|
62
|
+
createSessionMessageAction,
|
|
63
|
+
listSessionMessageActions,
|
|
62
64
|
listSessionsFromApi,
|
|
63
65
|
probeSessionAccess,
|
|
64
66
|
pollSessionEventsBefore,
|
|
67
|
+
searchSessionEvents,
|
|
65
68
|
syncSessionEventToApi,
|
|
66
69
|
syncSessionMetadataToApi,
|
|
67
70
|
} from "../session/sync.js";
|
|
@@ -69,6 +72,7 @@ import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
|
69
72
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
70
73
|
import { listenSessionEvents } from "../session/listener.js";
|
|
71
74
|
import { buildSessionRecap } from "../session/recap.js";
|
|
75
|
+
import { computeTranscriptStats } from "../session/transcript.js";
|
|
72
76
|
import { deriveSessionTitle } from "../session/senti-naming.js";
|
|
73
77
|
import { pushSessionTitleToApi } from "../session/title-sync.js";
|
|
74
78
|
import {
|
|
@@ -96,6 +100,236 @@ function normalizeString(value) {
|
|
|
96
100
|
return String(value || "").trim();
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
104
|
+
"ack",
|
|
105
|
+
"working_on",
|
|
106
|
+
"reply",
|
|
107
|
+
"like",
|
|
108
|
+
"dislike",
|
|
109
|
+
"disregard",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
function normalizeSessionMessageActionType(value) {
|
|
113
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
114
|
+
if (!SESSION_MESSAGE_ACTION_TYPES.has(normalized)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`action type must be one of: ${[...SESSION_MESSAGE_ACTION_TYPES].join(", ")}.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseOptionalPositiveInteger(rawValue, field) {
|
|
123
|
+
if (rawValue === undefined || rawValue === null || String(rawValue).trim() === "") {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return parsePositiveInteger(rawValue, field, null);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function shortSha256(value) {
|
|
130
|
+
return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 32);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function actionEventType(actionType) {
|
|
134
|
+
if (actionType === "reply") return "session_reply";
|
|
135
|
+
if (actionType === "like" || actionType === "dislike") return "session_reaction";
|
|
136
|
+
return "session_action";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function actionTargetSequence(action = {}) {
|
|
140
|
+
const value = Number(action.targetSequenceId ?? action.target_sequence_id ?? 0);
|
|
141
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function actionTargetCursor(action = {}) {
|
|
145
|
+
return normalizeString(action.targetCursor ?? action.target_cursor);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function actionActorId(action = {}) {
|
|
149
|
+
return normalizeString(action.actorId ?? action.actor_id) || "unknown";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function actionCreatedAt(action = {}) {
|
|
153
|
+
return normalizeString(action.createdAt ?? action.created_at) || new Date().toISOString();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function actionDisplayMessage(action = {}) {
|
|
157
|
+
const actionType = normalizeString(action.actionType ?? action.action_type).toLowerCase();
|
|
158
|
+
const targetSequence = actionTargetSequence(action);
|
|
159
|
+
const targetLabel = targetSequence ? `#${targetSequence}` : actionTargetCursor(action) || "target";
|
|
160
|
+
const note = normalizeString(action.note);
|
|
161
|
+
if (note) return `${actionType} ${targetLabel}: ${note}`;
|
|
162
|
+
return `${actionType} ${targetLabel}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildSessionActionEvent(sessionId, action = {}) {
|
|
166
|
+
const actionType = normalizeString(action.actionType ?? action.action_type).toLowerCase();
|
|
167
|
+
if (!SESSION_MESSAGE_ACTION_TYPES.has(actionType)) return null;
|
|
168
|
+
const id =
|
|
169
|
+
normalizeString(action.id) ||
|
|
170
|
+
shortSha256(
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
actionType,
|
|
173
|
+
targetSequenceId: actionTargetSequence(action),
|
|
174
|
+
targetCursor: actionTargetCursor(action),
|
|
175
|
+
actorId: actionActorId(action),
|
|
176
|
+
note: normalizeString(action.note),
|
|
177
|
+
createdAt: actionCreatedAt(action),
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
const actorId = actionActorId(action);
|
|
181
|
+
const event = createAgentEvent({
|
|
182
|
+
event: actionEventType(actionType),
|
|
183
|
+
agent: {
|
|
184
|
+
id: actorId,
|
|
185
|
+
role: normalizeString(action.actorRole ?? action.actor_role) || undefined,
|
|
186
|
+
model: normalizeString(action.actorKind ?? action.actor_kind) || "session-action",
|
|
187
|
+
},
|
|
188
|
+
sessionId,
|
|
189
|
+
ts: actionCreatedAt(action),
|
|
190
|
+
payload: {
|
|
191
|
+
actionId: id,
|
|
192
|
+
actionType,
|
|
193
|
+
targetSequenceId: actionTargetSequence(action),
|
|
194
|
+
targetCursor: actionTargetCursor(action) || null,
|
|
195
|
+
note: normalizeString(action.note) || null,
|
|
196
|
+
message: actionDisplayMessage(action),
|
|
197
|
+
source: "session_action",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
event.eventId = `session-action-${id}`;
|
|
201
|
+
event.idempotencyToken = normalizeString(action.idempotencyKey ?? action.idempotency_key) || event.eventId;
|
|
202
|
+
event.cursor = `action:${id}`;
|
|
203
|
+
return event;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildSessionActionEvents(sessionId, actions = []) {
|
|
207
|
+
return (Array.isArray(actions) ? actions : [])
|
|
208
|
+
.map((action) => buildSessionActionEvent(sessionId, action))
|
|
209
|
+
.filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function eventTimestampMs(event = {}) {
|
|
213
|
+
for (const key of ["ts", "timestamp", "createdAt", "at"]) {
|
|
214
|
+
const epoch = Date.parse(normalizeString(event?.[key]));
|
|
215
|
+
if (Number.isFinite(epoch)) return epoch;
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function eventSequenceNumber(event = {}) {
|
|
221
|
+
for (const key of ["sequenceId", "sequence_id", "sequence"]) {
|
|
222
|
+
const value = Number(event?.[key]);
|
|
223
|
+
if (Number.isFinite(value)) return value;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function mergeSessionActionEvents(events = [], actionEvents = []) {
|
|
229
|
+
return dedupeSessionEvents([...(Array.isArray(events) ? events : []), ...actionEvents])
|
|
230
|
+
.map((event, index) => ({ event, index }))
|
|
231
|
+
.sort((left, right) => {
|
|
232
|
+
const timeDiff = eventTimestampMs(left.event) - eventTimestampMs(right.event);
|
|
233
|
+
if (timeDiff !== 0) return timeDiff;
|
|
234
|
+
const sequenceDiff = eventSequenceNumber(left.event) - eventSequenceNumber(right.event);
|
|
235
|
+
if (sequenceDiff !== 0) return sequenceDiff;
|
|
236
|
+
return left.index - right.index;
|
|
237
|
+
})
|
|
238
|
+
.map((entry) => entry.event);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function appendActionEventIfMissing(sessionId, actionEvent, { targetPath } = {}) {
|
|
242
|
+
if (!actionEvent) return { appended: false, reason: "no_event", event: null };
|
|
243
|
+
const existingEvents = await readStream(sessionId, { targetPath, tail: 0 }).catch(() => []);
|
|
244
|
+
const knownKeys = new Set();
|
|
245
|
+
for (const event of existingEvents) {
|
|
246
|
+
addSessionEventIdentityKeys(knownKeys, event);
|
|
247
|
+
}
|
|
248
|
+
if (sessionEventHasKnownIdentity(actionEvent, knownKeys)) {
|
|
249
|
+
return { appended: false, reason: "already_present", event: actionEvent };
|
|
250
|
+
}
|
|
251
|
+
const appended = await appendToStream(sessionId, actionEvent, {
|
|
252
|
+
targetPath,
|
|
253
|
+
syncRemote: false,
|
|
254
|
+
});
|
|
255
|
+
return { appended: true, reason: "", event: appended };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function defaultActionIdempotencyKey({
|
|
259
|
+
actionType,
|
|
260
|
+
targetSequenceId,
|
|
261
|
+
targetCursor,
|
|
262
|
+
note,
|
|
263
|
+
agentId,
|
|
264
|
+
} = {}) {
|
|
265
|
+
const target = targetSequenceId ? `seq:${targetSequenceId}` : `cursor:${normalizeString(targetCursor)}`;
|
|
266
|
+
const noteHash = note ? shortSha256(note) : "none";
|
|
267
|
+
const actor = normalizeString(agentId) || "user";
|
|
268
|
+
return `cli:${normalizeString(actionType).toLowerCase()}:${target}:${actor}:${noteHash}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function compareIsoDesc(left = "", right = "") {
|
|
272
|
+
return normalizeString(right).localeCompare(normalizeString(left));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function buildSessionParticipants({ statsAgents = [], registeredAgents = [] } = {}) {
|
|
276
|
+
const byAgentId = new Map();
|
|
277
|
+
for (const agent of Array.isArray(statsAgents) ? statsAgents : []) {
|
|
278
|
+
const agentId = normalizeString(agent?.agentId || agent?.id);
|
|
279
|
+
if (!agentId) continue;
|
|
280
|
+
byAgentId.set(agentId, {
|
|
281
|
+
...agent,
|
|
282
|
+
agentId,
|
|
283
|
+
registered: false,
|
|
284
|
+
source: "events",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const agent of Array.isArray(registeredAgents) ? registeredAgents : []) {
|
|
289
|
+
const agentId = normalizeString(agent?.agentId || agent?.id);
|
|
290
|
+
if (!agentId) continue;
|
|
291
|
+
const existing = byAgentId.get(agentId);
|
|
292
|
+
if (existing) {
|
|
293
|
+
byAgentId.set(agentId, {
|
|
294
|
+
...existing,
|
|
295
|
+
model: existing.model || normalizeString(agent.model),
|
|
296
|
+
role: normalizeString(agent.role) || existing.role,
|
|
297
|
+
status: normalizeString(agent.status) || existing.status,
|
|
298
|
+
registered: true,
|
|
299
|
+
source: "events+registry",
|
|
300
|
+
joinedAt: normalizeString(agent.joinedAt) || existing.joinedAt,
|
|
301
|
+
lastActivityAt: normalizeString(agent.lastActivityAt) || existing.lastActivityAt,
|
|
302
|
+
active: agent.active,
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
byAgentId.set(agentId, {
|
|
307
|
+
agentId,
|
|
308
|
+
displayName: normalizeString(agent.displayName) || agentId,
|
|
309
|
+
model: normalizeString(agent.model),
|
|
310
|
+
role: normalizeString(agent.role),
|
|
311
|
+
status: normalizeString(agent.status),
|
|
312
|
+
firstSeen: normalizeString(agent.joinedAt) || null,
|
|
313
|
+
lastSeen: normalizeString(agent.lastActivityAt) || normalizeString(agent.joinedAt) || null,
|
|
314
|
+
joinedAt: normalizeString(agent.joinedAt) || null,
|
|
315
|
+
lastActivityAt: normalizeString(agent.lastActivityAt) || null,
|
|
316
|
+
active: agent.active,
|
|
317
|
+
eventCount: 0,
|
|
318
|
+
activeSeconds: 0,
|
|
319
|
+
tokens: 0,
|
|
320
|
+
costUsd: 0,
|
|
321
|
+
registered: true,
|
|
322
|
+
source: "registry",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [...byAgentId.values()].sort((left, right) => {
|
|
327
|
+
const eventDelta = Number(right.eventCount || 0) - Number(left.eventCount || 0);
|
|
328
|
+
if (eventDelta !== 0) return eventDelta;
|
|
329
|
+
return compareIsoDesc(left.lastSeen || left.lastActivityAt, right.lastSeen || right.lastActivityAt);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
99
333
|
function parsePositiveInteger(rawValue, field, fallbackValue) {
|
|
100
334
|
if (rawValue === undefined || rawValue === null || String(rawValue).trim() === "") {
|
|
101
335
|
return fallbackValue;
|
|
@@ -590,6 +824,16 @@ function formatEventLine(event = {}) {
|
|
|
590
824
|
const type = normalizeString(event.event || event.type) || "event";
|
|
591
825
|
const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
|
|
592
826
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
827
|
+
if (type === "session_action" || type === "session_reply" || type === "session_reaction") {
|
|
828
|
+
const actionType = normalizeString(payload.actionType) || type.replace(/^session_/, "");
|
|
829
|
+
const targetSequence = Number(payload.targetSequenceId || 0);
|
|
830
|
+
const target = Number.isFinite(targetSequence) && targetSequence > 0
|
|
831
|
+
? `#${Math.floor(targetSequence)}`
|
|
832
|
+
: normalizeString(payload.targetCursor) || "target";
|
|
833
|
+
const note = normalizeString(payload.note || payload.message || "");
|
|
834
|
+
const suffix = note && note !== `${actionType} ${target}` ? `: ${note}` : "";
|
|
835
|
+
return `${ts} ${agentId} ${type} ${actionType} ${target}${suffix}`;
|
|
836
|
+
}
|
|
593
837
|
const message = normalizeString(payload.message || payload.response || payload.alert || payload.reason || "");
|
|
594
838
|
if (message) {
|
|
595
839
|
return `${ts} ${agentId} ${type}: ${message}`;
|
|
@@ -1291,6 +1535,8 @@ export function registerSessionCommand(program) {
|
|
|
1291
1535
|
agentId: resolvedAgentId,
|
|
1292
1536
|
model,
|
|
1293
1537
|
role,
|
|
1538
|
+
trackProcessExit: false,
|
|
1539
|
+
awaitRemoteSync: Boolean(explicitAgent),
|
|
1294
1540
|
});
|
|
1295
1541
|
const agentJoinRelayed =
|
|
1296
1542
|
Boolean(explicitAgent) &&
|
|
@@ -1348,6 +1594,8 @@ export function registerSessionCommand(program) {
|
|
|
1348
1594
|
.description("Send a message to the session")
|
|
1349
1595
|
.option("--agent <id>", "Agent id to emit from", "cli-user")
|
|
1350
1596
|
.option("--to <agent>", "Direct the message to a specific agent id")
|
|
1597
|
+
.option("--reply-to <sequence>", "Mark this message as a reply to a target sequence id")
|
|
1598
|
+
.option("--reply-cursor <cursor>", "Mark this message as a reply to a target event cursor")
|
|
1351
1599
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
1352
1600
|
.option("--json", "Emit machine-readable output")
|
|
1353
1601
|
.action(async (sessionId, message, options, command) => {
|
|
@@ -1365,6 +1613,8 @@ export function registerSessionCommand(program) {
|
|
|
1365
1613
|
targetPath,
|
|
1366
1614
|
});
|
|
1367
1615
|
const to = normalizeString(options.to);
|
|
1616
|
+
const replyToSequenceId = parseOptionalPositiveInteger(options.replyTo, "reply-to");
|
|
1617
|
+
const replyToCursor = normalizeString(options.replyCursor);
|
|
1368
1618
|
const eventPayload = {
|
|
1369
1619
|
message: normalizedMessage,
|
|
1370
1620
|
channel: "session",
|
|
@@ -1372,6 +1622,12 @@ export function registerSessionCommand(program) {
|
|
|
1372
1622
|
if (to) {
|
|
1373
1623
|
eventPayload.to = to;
|
|
1374
1624
|
}
|
|
1625
|
+
if (replyToSequenceId) {
|
|
1626
|
+
eventPayload.replyToSequenceId = replyToSequenceId;
|
|
1627
|
+
}
|
|
1628
|
+
if (replyToCursor) {
|
|
1629
|
+
eventPayload.replyToCursor = replyToCursor;
|
|
1630
|
+
}
|
|
1375
1631
|
const clientMessageId = `cli-${randomUUID()}`;
|
|
1376
1632
|
const event = createAgentEvent({
|
|
1377
1633
|
event: "session_message",
|
|
@@ -1504,6 +1760,140 @@ export function registerSessionCommand(program) {
|
|
|
1504
1760
|
console.log(formatEventLine(persisted));
|
|
1505
1761
|
});
|
|
1506
1762
|
|
|
1763
|
+
async function runMessageActionCommand({
|
|
1764
|
+
sessionId,
|
|
1765
|
+
actionType,
|
|
1766
|
+
options,
|
|
1767
|
+
command,
|
|
1768
|
+
commandName = "session action",
|
|
1769
|
+
targetSequenceId: targetSequenceIdOverride = null,
|
|
1770
|
+
targetCursor: targetCursorOverride = "",
|
|
1771
|
+
note: noteOverride = "",
|
|
1772
|
+
} = {}) {
|
|
1773
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1774
|
+
if (!normalizedSessionId) {
|
|
1775
|
+
throw new Error("session id is required.");
|
|
1776
|
+
}
|
|
1777
|
+
const normalizedActionType = normalizeSessionMessageActionType(actionType);
|
|
1778
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
1779
|
+
const targetSequenceId =
|
|
1780
|
+
targetSequenceIdOverride ||
|
|
1781
|
+
parseOptionalPositiveInteger(options.targetSequence, "target-sequence");
|
|
1782
|
+
const targetCursor = normalizeString(targetCursorOverride) || normalizeString(options.targetCursor);
|
|
1783
|
+
if (!targetSequenceId && !targetCursor) {
|
|
1784
|
+
throw new Error("Provide --target-sequence or --target-cursor.");
|
|
1785
|
+
}
|
|
1786
|
+
await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
|
|
1787
|
+
const note = normalizeString(noteOverride) || normalizeString(options.note);
|
|
1788
|
+
const agentId = await defaultAgentId(options.agent, targetPath);
|
|
1789
|
+
const idempotencyKey =
|
|
1790
|
+
normalizeString(options.idempotencyKey) ||
|
|
1791
|
+
defaultActionIdempotencyKey({
|
|
1792
|
+
actionType: normalizedActionType,
|
|
1793
|
+
targetSequenceId,
|
|
1794
|
+
targetCursor,
|
|
1795
|
+
note,
|
|
1796
|
+
agentId,
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
const result = await createSessionMessageAction(normalizedSessionId, {
|
|
1800
|
+
actionType: normalizedActionType,
|
|
1801
|
+
targetPath,
|
|
1802
|
+
targetSequenceId,
|
|
1803
|
+
targetCursor,
|
|
1804
|
+
note,
|
|
1805
|
+
metadata: {
|
|
1806
|
+
source: "cli",
|
|
1807
|
+
agentId,
|
|
1808
|
+
},
|
|
1809
|
+
idempotencyKey,
|
|
1810
|
+
timeoutMs: 15_000,
|
|
1811
|
+
});
|
|
1812
|
+
if (!result.ok || !result.action) {
|
|
1813
|
+
throw new Error(`Session action failed (${result.reason || "unknown"}).`);
|
|
1814
|
+
}
|
|
1815
|
+
const actionEvent = buildSessionActionEvent(normalizedSessionId, result.action);
|
|
1816
|
+
const localAppend = await appendActionEventIfMissing(normalizedSessionId, actionEvent, {
|
|
1817
|
+
targetPath,
|
|
1818
|
+
});
|
|
1819
|
+
const payload = {
|
|
1820
|
+
command: commandName,
|
|
1821
|
+
targetPath,
|
|
1822
|
+
sessionId: normalizedSessionId,
|
|
1823
|
+
actionType: normalizedActionType,
|
|
1824
|
+
duplicate: Boolean(result.duplicate),
|
|
1825
|
+
action: result.action,
|
|
1826
|
+
event: localAppend.event,
|
|
1827
|
+
localAppend: {
|
|
1828
|
+
appended: Boolean(localAppend.appended),
|
|
1829
|
+
reason: localAppend.reason || "",
|
|
1830
|
+
},
|
|
1831
|
+
};
|
|
1832
|
+
if (shouldEmitJson(options, command)) {
|
|
1833
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1834
|
+
return payload;
|
|
1835
|
+
}
|
|
1836
|
+
console.log(formatEventLine(localAppend.event || actionEvent));
|
|
1837
|
+
return payload;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
session
|
|
1841
|
+
.command("action <sessionId> <actionType>")
|
|
1842
|
+
.description("Create a message action for a target session event")
|
|
1843
|
+
.option("--target-sequence <n>", "Target event sequence id")
|
|
1844
|
+
.option("--target-cursor <cursor>", "Target event cursor")
|
|
1845
|
+
.option("--note <text>", "Optional action note or reply body")
|
|
1846
|
+
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
1847
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
1848
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1849
|
+
.option("--json", "Emit machine-readable output")
|
|
1850
|
+
.action(async (sessionId, actionType, options, command) => {
|
|
1851
|
+
await runMessageActionCommand({ sessionId, actionType, options, command });
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
session
|
|
1855
|
+
.command("react <sessionId> <reaction>")
|
|
1856
|
+
.description("React to a target session event with like or dislike")
|
|
1857
|
+
.option("--target-sequence <n>", "Target event sequence id")
|
|
1858
|
+
.option("--target-cursor <cursor>", "Target event cursor")
|
|
1859
|
+
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
1860
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
1861
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1862
|
+
.option("--json", "Emit machine-readable output")
|
|
1863
|
+
.action(async (sessionId, reaction, options, command) => {
|
|
1864
|
+
const normalizedReaction = normalizeSessionMessageActionType(reaction);
|
|
1865
|
+
if (normalizedReaction !== "like" && normalizedReaction !== "dislike") {
|
|
1866
|
+
throw new Error("reaction must be one of: like, dislike.");
|
|
1867
|
+
}
|
|
1868
|
+
await runMessageActionCommand({
|
|
1869
|
+
sessionId,
|
|
1870
|
+
actionType: normalizedReaction,
|
|
1871
|
+
options,
|
|
1872
|
+
command,
|
|
1873
|
+
commandName: "session react",
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
session
|
|
1878
|
+
.command("reply <sessionId> <targetSequenceId> <message...>")
|
|
1879
|
+
.description("Reply to a target session event using the message-action channel")
|
|
1880
|
+
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
1881
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
1882
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1883
|
+
.option("--json", "Emit machine-readable output")
|
|
1884
|
+
.action(async (sessionId, targetSequenceId, messageParts, options, command) => {
|
|
1885
|
+
const message = Array.isArray(messageParts) ? messageParts.join(" ") : messageParts;
|
|
1886
|
+
await runMessageActionCommand({
|
|
1887
|
+
sessionId,
|
|
1888
|
+
actionType: "reply",
|
|
1889
|
+
options,
|
|
1890
|
+
command,
|
|
1891
|
+
commandName: "session reply",
|
|
1892
|
+
targetSequenceId: parsePositiveInteger(targetSequenceId, "targetSequenceId", 0),
|
|
1893
|
+
note: message,
|
|
1894
|
+
});
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1507
1897
|
session
|
|
1508
1898
|
.command("listen")
|
|
1509
1899
|
.description("Background-poll a session for events addressed to this agent or broadcast")
|
|
@@ -1708,6 +2098,8 @@ export function registerSessionCommand(program) {
|
|
|
1708
2098
|
"--remote",
|
|
1709
2099
|
"Hydrate from the SentinelLayer API before reading (pulls web-posted messages into the local NDJSON)",
|
|
1710
2100
|
)
|
|
2101
|
+
.option("--before-sequence <n>", "Remote page ending before this sequence id")
|
|
2102
|
+
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
1711
2103
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
1712
2104
|
.option("--json", "Emit machine-readable output")
|
|
1713
2105
|
.action(async (sessionId, options, command) => {
|
|
@@ -1717,10 +2109,12 @@ export function registerSessionCommand(program) {
|
|
|
1717
2109
|
}
|
|
1718
2110
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
1719
2111
|
const tail = parsePositiveInteger(options.tail, "tail", 20);
|
|
2112
|
+
const beforeSequence = parseOptionalPositiveInteger(options.beforeSequence, "before-sequence");
|
|
1720
2113
|
const emitJson = shouldEmitJson(options, command);
|
|
1721
2114
|
|
|
1722
2115
|
let hydration = null;
|
|
1723
2116
|
let remoteTail = null;
|
|
2117
|
+
let remoteActions = null;
|
|
1724
2118
|
if (options.remote) {
|
|
1725
2119
|
const authSession = await resolveActiveAuthSession({
|
|
1726
2120
|
cwd: targetPath,
|
|
@@ -1736,9 +2130,17 @@ export function registerSessionCommand(program) {
|
|
|
1736
2130
|
});
|
|
1737
2131
|
remoteTail = await pollSessionEventsBefore(normalizedSessionId, {
|
|
1738
2132
|
targetPath,
|
|
2133
|
+
beforeSequence,
|
|
1739
2134
|
limit: tail,
|
|
1740
2135
|
timeoutMs: 15_000,
|
|
1741
2136
|
});
|
|
2137
|
+
if (options.actions !== false) {
|
|
2138
|
+
remoteActions = await listSessionMessageActions(normalizedSessionId, {
|
|
2139
|
+
targetPath,
|
|
2140
|
+
limit: 500,
|
|
2141
|
+
timeoutMs: 15_000,
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
1742
2144
|
if (!emitJson) {
|
|
1743
2145
|
if (hydration.ok) {
|
|
1744
2146
|
console.log(
|
|
@@ -1760,6 +2162,13 @@ export function registerSessionCommand(program) {
|
|
|
1760
2162
|
),
|
|
1761
2163
|
);
|
|
1762
2164
|
}
|
|
2165
|
+
if (remoteActions && !remoteActions.ok) {
|
|
2166
|
+
console.log(
|
|
2167
|
+
pc.yellow(
|
|
2168
|
+
`Remote message actions skipped (${remoteActions.reason}); showing events only.`,
|
|
2169
|
+
),
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
1763
2172
|
}
|
|
1764
2173
|
}
|
|
1765
2174
|
|
|
@@ -1795,7 +2204,10 @@ export function registerSessionCommand(program) {
|
|
|
1795
2204
|
}
|
|
1796
2205
|
}
|
|
1797
2206
|
}
|
|
1798
|
-
const
|
|
2207
|
+
const actionEvents = remoteActions?.ok
|
|
2208
|
+
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
2209
|
+
: [];
|
|
2210
|
+
const events = mergeSessionActionEvents(displayEvents, actionEvents).slice(-tail);
|
|
1799
2211
|
const remoteVerified = Boolean(
|
|
1800
2212
|
options.remote &&
|
|
1801
2213
|
((hydration && hydration.ok) || (remoteTail && remoteTail.ok))
|
|
@@ -1805,6 +2217,7 @@ export function registerSessionCommand(program) {
|
|
|
1805
2217
|
targetPath,
|
|
1806
2218
|
sessionId: normalizedSessionId,
|
|
1807
2219
|
tail,
|
|
2220
|
+
beforeSequence,
|
|
1808
2221
|
count: events.length,
|
|
1809
2222
|
events,
|
|
1810
2223
|
displaySource: !options.remote
|
|
@@ -1825,11 +2238,24 @@ export function registerSessionCommand(program) {
|
|
|
1825
2238
|
reason: remoteTail.reason || "",
|
|
1826
2239
|
count: Array.isArray(remoteTail.events) ? remoteTail.events.length : 0,
|
|
1827
2240
|
cursor: remoteTail.cursor || null,
|
|
2241
|
+
beforeSequence: remoteTail.beforeSequence || null,
|
|
1828
2242
|
verified: Boolean(remoteTail.ok),
|
|
1829
2243
|
appended: remoteTailAppended,
|
|
1830
2244
|
displayedOnly: remoteTailDisplayedOnly,
|
|
1831
2245
|
}
|
|
1832
2246
|
: null,
|
|
2247
|
+
actions: remoteActions
|
|
2248
|
+
? {
|
|
2249
|
+
ok: Boolean(remoteActions.ok),
|
|
2250
|
+
reason: remoteActions.reason || "",
|
|
2251
|
+
count: Array.isArray(remoteActions.actions)
|
|
2252
|
+
? remoteActions.actions.length
|
|
2253
|
+
: 0,
|
|
2254
|
+
actions: remoteActions.actions || [],
|
|
2255
|
+
syntheticEventCount: actionEvents.length,
|
|
2256
|
+
projection: remoteActions.projection || null,
|
|
2257
|
+
}
|
|
2258
|
+
: null,
|
|
1833
2259
|
}
|
|
1834
2260
|
: hydration,
|
|
1835
2261
|
};
|
|
@@ -1906,6 +2332,57 @@ export function registerSessionCommand(program) {
|
|
|
1906
2332
|
}
|
|
1907
2333
|
});
|
|
1908
2334
|
|
|
2335
|
+
session
|
|
2336
|
+
.command("search <sessionId> <query>")
|
|
2337
|
+
.description("Search durable API session events by text, event type, or agent")
|
|
2338
|
+
.option("--before-sequence <n>", "Return matches older than this sequence id")
|
|
2339
|
+
.option("--limit <n>", "Maximum search results (default 20, max 50)", "20")
|
|
2340
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
2341
|
+
.option("--json", "Emit machine-readable output")
|
|
2342
|
+
.action(async (sessionId, query, options, command) => {
|
|
2343
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
2344
|
+
if (!normalizedSessionId) {
|
|
2345
|
+
throw new Error("session id is required.");
|
|
2346
|
+
}
|
|
2347
|
+
const normalizedQuery = normalizeString(query);
|
|
2348
|
+
if (normalizedQuery.length < 2) {
|
|
2349
|
+
throw new Error("query must be at least 2 characters.");
|
|
2350
|
+
}
|
|
2351
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
2352
|
+
const beforeSequence = parseOptionalPositiveInteger(options.beforeSequence, "before-sequence");
|
|
2353
|
+
const limit = parsePositiveInteger(options.limit, "limit", 20);
|
|
2354
|
+
const result = await searchSessionEvents(normalizedSessionId, {
|
|
2355
|
+
query: normalizedQuery,
|
|
2356
|
+
targetPath,
|
|
2357
|
+
beforeSequence,
|
|
2358
|
+
limit,
|
|
2359
|
+
timeoutMs: 15_000,
|
|
2360
|
+
});
|
|
2361
|
+
const payload = {
|
|
2362
|
+
command: "session search",
|
|
2363
|
+
targetPath,
|
|
2364
|
+
sessionId: normalizedSessionId,
|
|
2365
|
+
...result,
|
|
2366
|
+
nextBeforeSequence: result.nextBeforeSequence || null,
|
|
2367
|
+
};
|
|
2368
|
+
if (shouldEmitJson(options, command)) {
|
|
2369
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (!result.ok) {
|
|
2373
|
+
throw new Error(`Session search failed (${result.reason || "unknown"}).`);
|
|
2374
|
+
}
|
|
2375
|
+
for (const item of result.results || []) {
|
|
2376
|
+
const event = item.event || {};
|
|
2377
|
+
const sequence = item.sequenceId ? `#${item.sequenceId}` : "";
|
|
2378
|
+
const snippet = normalizeString(item.snippet);
|
|
2379
|
+
console.log(`${sequence} ${formatEventLine(event)}${snippet ? ` | ${snippet}` : ""}`);
|
|
2380
|
+
}
|
|
2381
|
+
if (result.hasMore && result.nextBeforeSequence) {
|
|
2382
|
+
console.log(pc.gray(`More results before sequence ${result.nextBeforeSequence}.`));
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
|
|
1909
2386
|
session
|
|
1910
2387
|
.command("sync <sessionId>")
|
|
1911
2388
|
.description(
|
|
@@ -2238,6 +2715,11 @@ export function registerSessionCommand(program) {
|
|
|
2238
2715
|
"json",
|
|
2239
2716
|
)
|
|
2240
2717
|
.option("--out <file>", "Write to file instead of stdout")
|
|
2718
|
+
.option(
|
|
2719
|
+
"--remote",
|
|
2720
|
+
"Hydrate from the SentinelLayer API before exporting and include remote message actions",
|
|
2721
|
+
)
|
|
2722
|
+
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
2241
2723
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2242
2724
|
.action(async (sessionId, options) => {
|
|
2243
2725
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -2249,6 +2731,21 @@ export function registerSessionCommand(program) {
|
|
|
2249
2731
|
if (format !== "json" && format !== "ndjson") {
|
|
2250
2732
|
throw new Error(`--format must be 'json' or 'ndjson' (received '${format}').`);
|
|
2251
2733
|
}
|
|
2734
|
+
let hydration = null;
|
|
2735
|
+
let remoteActions = null;
|
|
2736
|
+
if (options.remote) {
|
|
2737
|
+
hydration = await hydrateSessionFromRemote({
|
|
2738
|
+
sessionId: normalizedSessionId,
|
|
2739
|
+
targetPath,
|
|
2740
|
+
}).catch((error) => ({ ok: false, reason: error?.message || "hydrate_failed" }));
|
|
2741
|
+
if (options.actions !== false) {
|
|
2742
|
+
remoteActions = await listSessionMessageActions(normalizedSessionId, {
|
|
2743
|
+
targetPath,
|
|
2744
|
+
limit: 500,
|
|
2745
|
+
timeoutMs: 15_000,
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2252
2749
|
|
|
2253
2750
|
const sessionPayload = await getSession(normalizedSessionId, { targetPath });
|
|
2254
2751
|
if (!sessionPayload) {
|
|
@@ -2269,13 +2766,31 @@ export function registerSessionCommand(program) {
|
|
|
2269
2766
|
limit: 5_000,
|
|
2270
2767
|
}),
|
|
2271
2768
|
]);
|
|
2769
|
+
const actionEvents = remoteActions?.ok
|
|
2770
|
+
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
2771
|
+
: [];
|
|
2772
|
+
const exportEvents = mergeSessionActionEvents(events, actionEvents);
|
|
2773
|
+
const stats = computeTranscriptStats({
|
|
2774
|
+
sessionMeta: sessionPayload,
|
|
2775
|
+
events: exportEvents,
|
|
2776
|
+
});
|
|
2777
|
+
const participants = buildSessionParticipants({
|
|
2778
|
+
statsAgents: stats.agents,
|
|
2779
|
+
registeredAgents: agents,
|
|
2780
|
+
});
|
|
2272
2781
|
|
|
2273
2782
|
let output;
|
|
2274
2783
|
if (format === "ndjson") {
|
|
2275
2784
|
const lines = [];
|
|
2276
2785
|
lines.push(JSON.stringify({ kind: "session", value: sessionPayload }));
|
|
2277
2786
|
for (const agent of agents) lines.push(JSON.stringify({ kind: "agent", value: agent }));
|
|
2278
|
-
for (const
|
|
2787
|
+
for (const participant of participants) {
|
|
2788
|
+
lines.push(JSON.stringify({ kind: "participant", value: participant }));
|
|
2789
|
+
}
|
|
2790
|
+
for (const action of remoteActions?.actions || []) {
|
|
2791
|
+
lines.push(JSON.stringify({ kind: "action", value: action }));
|
|
2792
|
+
}
|
|
2793
|
+
for (const event of exportEvents) lines.push(JSON.stringify({ kind: "event", value: event }));
|
|
2279
2794
|
for (const task of tasks.tasks || []) lines.push(JSON.stringify({ kind: "task", value: task }));
|
|
2280
2795
|
output = `${lines.join("\n")}\n`;
|
|
2281
2796
|
} else {
|
|
@@ -2285,13 +2800,35 @@ export function registerSessionCommand(program) {
|
|
|
2285
2800
|
exportedAt: new Date().toISOString(),
|
|
2286
2801
|
session: sessionPayload,
|
|
2287
2802
|
agents,
|
|
2288
|
-
|
|
2803
|
+
participants,
|
|
2804
|
+
actions: remoteActions?.actions || [],
|
|
2805
|
+
actionProjection: remoteActions?.projection || null,
|
|
2806
|
+
actionEvents,
|
|
2807
|
+
events: exportEvents,
|
|
2289
2808
|
tasks: tasks.tasks || [],
|
|
2809
|
+
remote: {
|
|
2810
|
+
hydration,
|
|
2811
|
+
actions: remoteActions
|
|
2812
|
+
? {
|
|
2813
|
+
ok: Boolean(remoteActions.ok),
|
|
2814
|
+
reason: remoteActions.reason || "",
|
|
2815
|
+
count: Array.isArray(remoteActions.actions) ? remoteActions.actions.length : 0,
|
|
2816
|
+
syntheticEventCount: actionEvents.length,
|
|
2817
|
+
}
|
|
2818
|
+
: null,
|
|
2819
|
+
},
|
|
2290
2820
|
counts: {
|
|
2291
|
-
agents:
|
|
2292
|
-
|
|
2821
|
+
agents: participants.length,
|
|
2822
|
+
participants: participants.length,
|
|
2823
|
+
derivedAgents: stats.agents.length,
|
|
2824
|
+
registeredAgents: agents.length,
|
|
2825
|
+
events: exportEvents.length,
|
|
2826
|
+
rawEvents: events.length,
|
|
2827
|
+
actions: Array.isArray(remoteActions?.actions) ? remoteActions.actions.length : 0,
|
|
2828
|
+
actionEvents: actionEvents.length,
|
|
2293
2829
|
tasks: (tasks.tasks || []).length,
|
|
2294
2830
|
},
|
|
2831
|
+
totals: stats.totals,
|
|
2295
2832
|
},
|
|
2296
2833
|
null,
|
|
2297
2834
|
2,
|
|
@@ -2305,7 +2842,7 @@ export function registerSessionCommand(program) {
|
|
|
2305
2842
|
await fsp.writeFile(outPath, output, "utf-8");
|
|
2306
2843
|
console.log(
|
|
2307
2844
|
pc.gray(
|
|
2308
|
-
`Exported ${
|
|
2845
|
+
`Exported ${exportEvents.length} events / ${participants.length} participants (${agents.length} registered agents) / ${
|
|
2309
2846
|
(tasks.tasks || []).length
|
|
2310
2847
|
} tasks โ ${outPath}`,
|
|
2311
2848
|
),
|
|
@@ -2329,6 +2866,7 @@ export function registerSessionCommand(program) {
|
|
|
2329
2866
|
"--remote",
|
|
2330
2867
|
"Hydrate from the SentinelLayer API before rendering (pulls web-posted messages into the local NDJSON)",
|
|
2331
2868
|
)
|
|
2869
|
+
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
2332
2870
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2333
2871
|
.option("--json", "Emit machine-readable output")
|
|
2334
2872
|
.action(async (sessionId, options, command) => {
|
|
@@ -2340,11 +2878,19 @@ export function registerSessionCommand(program) {
|
|
|
2340
2878
|
const emitJson = shouldEmitJson(options, command);
|
|
2341
2879
|
|
|
2342
2880
|
let hydration = null;
|
|
2881
|
+
let remoteActions = null;
|
|
2343
2882
|
if (options.remote) {
|
|
2344
2883
|
hydration = await hydrateSessionFromRemote({
|
|
2345
2884
|
sessionId: normalizedSessionId,
|
|
2346
2885
|
targetPath,
|
|
2347
2886
|
}).catch((error) => ({ ok: false, reason: error?.message || "hydrate_failed" }));
|
|
2887
|
+
if (options.actions !== false) {
|
|
2888
|
+
remoteActions = await listSessionMessageActions(normalizedSessionId, {
|
|
2889
|
+
targetPath,
|
|
2890
|
+
limit: 500,
|
|
2891
|
+
timeoutMs: 15_000,
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2348
2894
|
}
|
|
2349
2895
|
|
|
2350
2896
|
const sessionPayload = await getSession(normalizedSessionId, { targetPath });
|
|
@@ -2356,6 +2902,10 @@ export function registerSessionCommand(program) {
|
|
|
2356
2902
|
listAgents(normalizedSessionId, { targetPath, includeInactive: true }),
|
|
2357
2903
|
readStream(normalizedSessionId, { targetPath, tail: 0 }),
|
|
2358
2904
|
]);
|
|
2905
|
+
const actionEvents = remoteActions?.ok
|
|
2906
|
+
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
2907
|
+
: [];
|
|
2908
|
+
const transcriptEvents = mergeSessionActionEvents(events, actionEvents);
|
|
2359
2909
|
|
|
2360
2910
|
// Pull GitHub/Google avatar + display name from the active auth
|
|
2361
2911
|
// session so any human-id seen in the stream renders with the
|
|
@@ -2391,7 +2941,7 @@ export function registerSessionCommand(program) {
|
|
|
2391
2941
|
createdAt: sessionPayload.createdAt,
|
|
2392
2942
|
status: sessionPayload.status,
|
|
2393
2943
|
},
|
|
2394
|
-
events,
|
|
2944
|
+
events: transcriptEvents,
|
|
2395
2945
|
agents,
|
|
2396
2946
|
speakerProfiles,
|
|
2397
2947
|
options: {
|
|
@@ -2399,6 +2949,10 @@ export function registerSessionCommand(program) {
|
|
|
2399
2949
|
includeSystemEvents: options.systemEvents !== false,
|
|
2400
2950
|
},
|
|
2401
2951
|
});
|
|
2952
|
+
const participants = buildSessionParticipants({
|
|
2953
|
+
statsAgents: stats.agents,
|
|
2954
|
+
registeredAgents: agents,
|
|
2955
|
+
});
|
|
2402
2956
|
|
|
2403
2957
|
const outArg = normalizeString(options.out);
|
|
2404
2958
|
const outPath = outArg
|
|
@@ -2412,12 +2966,30 @@ export function registerSessionCommand(program) {
|
|
|
2412
2966
|
sessionId: normalizedSessionId,
|
|
2413
2967
|
outPath,
|
|
2414
2968
|
bytes: Buffer.byteLength(markdown, "utf-8"),
|
|
2415
|
-
eventCount:
|
|
2416
|
-
|
|
2969
|
+
eventCount: transcriptEvents.length,
|
|
2970
|
+
rawEventCount: events.length,
|
|
2971
|
+
actionCount: Array.isArray(remoteActions?.actions) ? remoteActions.actions.length : 0,
|
|
2972
|
+
actionEventCount: actionEvents.length,
|
|
2973
|
+
agentCount: participants.length,
|
|
2974
|
+
participantCount: participants.length,
|
|
2975
|
+
derivedAgentCount: stats.agents.length,
|
|
2976
|
+
registeredAgentCount: agents.length,
|
|
2977
|
+
participants,
|
|
2417
2978
|
sessionLiveSeconds: stats.sessionLiveSeconds,
|
|
2418
2979
|
sentiActions: stats.sentiActions,
|
|
2419
2980
|
totals: stats.totals,
|
|
2420
|
-
remote:
|
|
2981
|
+
remote: {
|
|
2982
|
+
hydration,
|
|
2983
|
+
actions: remoteActions
|
|
2984
|
+
? {
|
|
2985
|
+
ok: Boolean(remoteActions.ok),
|
|
2986
|
+
reason: remoteActions.reason || "",
|
|
2987
|
+
count: Array.isArray(remoteActions.actions) ? remoteActions.actions.length : 0,
|
|
2988
|
+
syntheticEventCount: actionEvents.length,
|
|
2989
|
+
projection: remoteActions.projection || null,
|
|
2990
|
+
}
|
|
2991
|
+
: null,
|
|
2992
|
+
},
|
|
2421
2993
|
};
|
|
2422
2994
|
if (emitJson) {
|
|
2423
2995
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -2426,7 +2998,7 @@ export function registerSessionCommand(program) {
|
|
|
2426
2998
|
console.log(pc.bold(`Downloaded session ${normalizedSessionId} โ ${outPath}`));
|
|
2427
2999
|
console.log(
|
|
2428
3000
|
pc.gray(
|
|
2429
|
-
`${
|
|
3001
|
+
`${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)}`,
|
|
2430
3002
|
),
|
|
2431
3003
|
);
|
|
2432
3004
|
});
|
|
@@ -156,14 +156,19 @@ async function writeAgentSnapshot(snapshotPath, snapshot) {
|
|
|
156
156
|
await fsp.rename(tmpPath, snapshotPath);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
async function emitAgentEvent(
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
package/src/session/recap.js
CHANGED
|
@@ -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
|
|
package/src/session/stream.js
CHANGED
|
@@ -337,7 +337,12 @@ function filterBySince(events = [], since) {
|
|
|
337
337
|
export async function appendToStream(
|
|
338
338
|
sessionId,
|
|
339
339
|
event,
|
|
340
|
-
{
|
|
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
|
-
|
|
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;
|
package/src/session/sync.js
CHANGED
|
@@ -15,6 +15,8 @@ const HUMAN_MESSAGE_LIMIT_PER_MINUTE = 10;
|
|
|
15
15
|
const HUMAN_MESSAGE_MAX_LENGTH = 2_000;
|
|
16
16
|
const HUMAN_MESSAGE_FETCH_LIMIT = 50;
|
|
17
17
|
const SESSION_EVENT_FETCH_LIMIT = 200;
|
|
18
|
+
const SESSION_ACTION_FETCH_LIMIT = 500;
|
|
19
|
+
const SESSION_SEARCH_FETCH_LIMIT = 50;
|
|
18
20
|
|
|
19
21
|
// Audit ยง2.9: crash-recovery contract for in-memory circuit state.
|
|
20
22
|
// Persist outbound/inbound circuit state to disk so a process restart
|
|
@@ -1238,6 +1240,347 @@ export async function pollSessionEventsBefore(
|
|
|
1238
1240
|
}
|
|
1239
1241
|
|
|
1240
1242
|
|
|
1243
|
+
export async function listSessionMessageActions(
|
|
1244
|
+
sessionId,
|
|
1245
|
+
{
|
|
1246
|
+
targetPath = process.cwd(),
|
|
1247
|
+
targetSequenceId = null,
|
|
1248
|
+
limit = SESSION_ACTION_FETCH_LIMIT,
|
|
1249
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1250
|
+
forceCircuitProbe = false,
|
|
1251
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1252
|
+
fetchImpl = fetchWithTimeout,
|
|
1253
|
+
nowMs = Date.now,
|
|
1254
|
+
} = {}
|
|
1255
|
+
) {
|
|
1256
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1257
|
+
if (!normalizedSessionId) {
|
|
1258
|
+
return {
|
|
1259
|
+
ok: false,
|
|
1260
|
+
reason: "invalid_session_id",
|
|
1261
|
+
actions: [],
|
|
1262
|
+
count: 0,
|
|
1263
|
+
projection: null,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1267
|
+
if (!forceCircuitProbe && isCircuitOpen(inboundCircuit, normalizedNowMs)) {
|
|
1268
|
+
return {
|
|
1269
|
+
ok: false,
|
|
1270
|
+
reason: "circuit_breaker_open",
|
|
1271
|
+
actions: [],
|
|
1272
|
+
count: 0,
|
|
1273
|
+
projection: null,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
let session = null;
|
|
1278
|
+
try {
|
|
1279
|
+
session = await resolveAuthSession({
|
|
1280
|
+
cwd: targetPath,
|
|
1281
|
+
env: process.env,
|
|
1282
|
+
autoRotate: false,
|
|
1283
|
+
});
|
|
1284
|
+
} catch {
|
|
1285
|
+
return { ok: false, reason: "no_session", actions: [], count: 0, projection: null };
|
|
1286
|
+
}
|
|
1287
|
+
if (!session || !session.token) {
|
|
1288
|
+
return { ok: false, reason: "not_authenticated", actions: [], count: 0, projection: null };
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1292
|
+
const query = new URLSearchParams();
|
|
1293
|
+
const normalizedTargetSequence = Number(targetSequenceId);
|
|
1294
|
+
if (Number.isFinite(normalizedTargetSequence) && normalizedTargetSequence > 0) {
|
|
1295
|
+
query.set("targetSequenceId", String(Math.floor(normalizedTargetSequence)));
|
|
1296
|
+
}
|
|
1297
|
+
query.set(
|
|
1298
|
+
"limit",
|
|
1299
|
+
String(Math.max(1, Math.min(SESSION_ACTION_FETCH_LIMIT, normalizePositiveInteger(limit, 200))))
|
|
1300
|
+
);
|
|
1301
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/actions?${query.toString()}`;
|
|
1302
|
+
|
|
1303
|
+
try {
|
|
1304
|
+
const response = await fetchImpl(
|
|
1305
|
+
endpoint,
|
|
1306
|
+
{
|
|
1307
|
+
method: "GET",
|
|
1308
|
+
headers: { Authorization: `Bearer ${session.token}` },
|
|
1309
|
+
},
|
|
1310
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
|
|
1311
|
+
);
|
|
1312
|
+
if (!response || !response.ok) {
|
|
1313
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1314
|
+
return {
|
|
1315
|
+
ok: false,
|
|
1316
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1317
|
+
actions: [],
|
|
1318
|
+
count: 0,
|
|
1319
|
+
projection: null,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
const payload = await response.json().catch(() => ({}));
|
|
1323
|
+
recordCircuitSuccess(inboundCircuit);
|
|
1324
|
+
const actions = Array.isArray(payload?.actions) ? payload.actions : [];
|
|
1325
|
+
return {
|
|
1326
|
+
ok: true,
|
|
1327
|
+
reason: "",
|
|
1328
|
+
sessionId: normalizeString(payload?.sessionId) || normalizedSessionId,
|
|
1329
|
+
actions,
|
|
1330
|
+
count: Number(payload?.count ?? actions.length) || actions.length,
|
|
1331
|
+
projection: payload?.projection && typeof payload.projection === "object"
|
|
1332
|
+
? payload.projection
|
|
1333
|
+
: null,
|
|
1334
|
+
};
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1337
|
+
return {
|
|
1338
|
+
ok: false,
|
|
1339
|
+
reason: normalizeString(error?.message) || "actions_read_failed",
|
|
1340
|
+
actions: [],
|
|
1341
|
+
count: 0,
|
|
1342
|
+
projection: null,
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export async function createSessionMessageAction(
|
|
1348
|
+
sessionId,
|
|
1349
|
+
{
|
|
1350
|
+
actionType,
|
|
1351
|
+
targetPath = process.cwd(),
|
|
1352
|
+
targetSequenceId = null,
|
|
1353
|
+
targetCursor = "",
|
|
1354
|
+
note = "",
|
|
1355
|
+
metadata = {},
|
|
1356
|
+
idempotencyKey = "",
|
|
1357
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1358
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1359
|
+
fetchImpl = fetchWithTimeout,
|
|
1360
|
+
nowMs = Date.now,
|
|
1361
|
+
} = {}
|
|
1362
|
+
) {
|
|
1363
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1364
|
+
const normalizedActionType = normalizeString(actionType).toLowerCase();
|
|
1365
|
+
if (!normalizedSessionId || !normalizedActionType) {
|
|
1366
|
+
return { ok: false, reason: "invalid_input", action: null };
|
|
1367
|
+
}
|
|
1368
|
+
if (String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1") {
|
|
1369
|
+
return { ok: false, reason: "remote_sync_disabled_env", action: null };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1373
|
+
if (isCircuitOpen(outboundCircuit, normalizedNowMs)) {
|
|
1374
|
+
return { ok: false, reason: "circuit_breaker_open", action: null };
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
let session = null;
|
|
1378
|
+
try {
|
|
1379
|
+
session = await resolveAuthSession({
|
|
1380
|
+
cwd: targetPath,
|
|
1381
|
+
env: process.env,
|
|
1382
|
+
autoRotate: false,
|
|
1383
|
+
});
|
|
1384
|
+
} catch {
|
|
1385
|
+
return { ok: false, reason: "no_session", action: null };
|
|
1386
|
+
}
|
|
1387
|
+
if (!session || !session.token) {
|
|
1388
|
+
return { ok: false, reason: "not_authenticated", action: null };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1392
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/actions`;
|
|
1393
|
+
const body = {
|
|
1394
|
+
actionType: normalizedActionType,
|
|
1395
|
+
metadata: metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {},
|
|
1396
|
+
};
|
|
1397
|
+
const normalizedTargetSequence = Number(targetSequenceId);
|
|
1398
|
+
if (Number.isFinite(normalizedTargetSequence) && normalizedTargetSequence > 0) {
|
|
1399
|
+
body.targetSequenceId = Math.floor(normalizedTargetSequence);
|
|
1400
|
+
}
|
|
1401
|
+
const normalizedTargetCursor = normalizeString(targetCursor);
|
|
1402
|
+
if (normalizedTargetCursor) {
|
|
1403
|
+
body.targetCursor = normalizedTargetCursor;
|
|
1404
|
+
}
|
|
1405
|
+
const normalizedNote = normalizeString(note);
|
|
1406
|
+
if (normalizedNote) {
|
|
1407
|
+
body.note = normalizedNote;
|
|
1408
|
+
}
|
|
1409
|
+
const normalizedIdempotencyKey = normalizeString(idempotencyKey);
|
|
1410
|
+
if (normalizedIdempotencyKey) {
|
|
1411
|
+
body.idempotencyKey = normalizedIdempotencyKey;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
try {
|
|
1415
|
+
const response = await fetchImpl(
|
|
1416
|
+
endpoint,
|
|
1417
|
+
{
|
|
1418
|
+
method: "POST",
|
|
1419
|
+
headers: {
|
|
1420
|
+
"Content-Type": "application/json",
|
|
1421
|
+
Authorization: `Bearer ${session.token}`,
|
|
1422
|
+
},
|
|
1423
|
+
body: JSON.stringify(body),
|
|
1424
|
+
},
|
|
1425
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
|
|
1426
|
+
);
|
|
1427
|
+
if (!response || !response.ok) {
|
|
1428
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1429
|
+
return {
|
|
1430
|
+
ok: false,
|
|
1431
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1432
|
+
action: null,
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
const payload = await response.json().catch(() => ({}));
|
|
1436
|
+
recordCircuitSuccess(outboundCircuit);
|
|
1437
|
+
return {
|
|
1438
|
+
ok: Boolean(payload?.ok ?? true),
|
|
1439
|
+
reason: "",
|
|
1440
|
+
duplicate: Boolean(payload?.duplicate),
|
|
1441
|
+
action: payload?.action && typeof payload.action === "object" ? payload.action : null,
|
|
1442
|
+
};
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1445
|
+
return {
|
|
1446
|
+
ok: false,
|
|
1447
|
+
reason: normalizeString(error?.message) || "action_write_failed",
|
|
1448
|
+
action: null,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export async function searchSessionEvents(
|
|
1454
|
+
sessionId,
|
|
1455
|
+
{
|
|
1456
|
+
query,
|
|
1457
|
+
targetPath = process.cwd(),
|
|
1458
|
+
beforeSequence = null,
|
|
1459
|
+
limit = 20,
|
|
1460
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1461
|
+
forceCircuitProbe = false,
|
|
1462
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1463
|
+
fetchImpl = fetchWithTimeout,
|
|
1464
|
+
nowMs = Date.now,
|
|
1465
|
+
} = {}
|
|
1466
|
+
) {
|
|
1467
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1468
|
+
const normalizedQuery = normalizeString(query);
|
|
1469
|
+
if (!normalizedSessionId || normalizedQuery.length < 2) {
|
|
1470
|
+
return {
|
|
1471
|
+
ok: false,
|
|
1472
|
+
reason: "invalid_input",
|
|
1473
|
+
query: normalizedQuery,
|
|
1474
|
+
results: [],
|
|
1475
|
+
count: 0,
|
|
1476
|
+
hasMore: false,
|
|
1477
|
+
nextBeforeSequence: null,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1481
|
+
if (!forceCircuitProbe && isCircuitOpen(inboundCircuit, normalizedNowMs)) {
|
|
1482
|
+
return {
|
|
1483
|
+
ok: false,
|
|
1484
|
+
reason: "circuit_breaker_open",
|
|
1485
|
+
query: normalizedQuery,
|
|
1486
|
+
results: [],
|
|
1487
|
+
count: 0,
|
|
1488
|
+
hasMore: false,
|
|
1489
|
+
nextBeforeSequence: null,
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
let session = null;
|
|
1494
|
+
try {
|
|
1495
|
+
session = await resolveAuthSession({
|
|
1496
|
+
cwd: targetPath,
|
|
1497
|
+
env: process.env,
|
|
1498
|
+
autoRotate: false,
|
|
1499
|
+
});
|
|
1500
|
+
} catch {
|
|
1501
|
+
return {
|
|
1502
|
+
ok: false,
|
|
1503
|
+
reason: "no_session",
|
|
1504
|
+
query: normalizedQuery,
|
|
1505
|
+
results: [],
|
|
1506
|
+
count: 0,
|
|
1507
|
+
hasMore: false,
|
|
1508
|
+
nextBeforeSequence: null,
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
if (!session || !session.token) {
|
|
1512
|
+
return {
|
|
1513
|
+
ok: false,
|
|
1514
|
+
reason: "not_authenticated",
|
|
1515
|
+
query: normalizedQuery,
|
|
1516
|
+
results: [],
|
|
1517
|
+
count: 0,
|
|
1518
|
+
hasMore: false,
|
|
1519
|
+
nextBeforeSequence: null,
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1524
|
+
const queryParams = new URLSearchParams();
|
|
1525
|
+
queryParams.set("q", normalizedQuery);
|
|
1526
|
+
const normalizedBeforeSequence = Number(beforeSequence);
|
|
1527
|
+
if (Number.isFinite(normalizedBeforeSequence) && normalizedBeforeSequence > 0) {
|
|
1528
|
+
queryParams.set("beforeSequence", String(Math.floor(normalizedBeforeSequence)));
|
|
1529
|
+
}
|
|
1530
|
+
queryParams.set(
|
|
1531
|
+
"limit",
|
|
1532
|
+
String(Math.max(1, Math.min(SESSION_SEARCH_FETCH_LIMIT, normalizePositiveInteger(limit, 20))))
|
|
1533
|
+
);
|
|
1534
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/events/search?${queryParams.toString()}`;
|
|
1535
|
+
|
|
1536
|
+
try {
|
|
1537
|
+
const response = await fetchImpl(
|
|
1538
|
+
endpoint,
|
|
1539
|
+
{
|
|
1540
|
+
method: "GET",
|
|
1541
|
+
headers: { Authorization: `Bearer ${session.token}` },
|
|
1542
|
+
},
|
|
1543
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
|
|
1544
|
+
);
|
|
1545
|
+
if (!response || !response.ok) {
|
|
1546
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1547
|
+
return {
|
|
1548
|
+
ok: false,
|
|
1549
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1550
|
+
query: normalizedQuery,
|
|
1551
|
+
results: [],
|
|
1552
|
+
count: 0,
|
|
1553
|
+
hasMore: false,
|
|
1554
|
+
nextBeforeSequence: null,
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
const payload = await response.json().catch(() => ({}));
|
|
1558
|
+
recordCircuitSuccess(inboundCircuit);
|
|
1559
|
+
const results = Array.isArray(payload?.results) ? payload.results : [];
|
|
1560
|
+
return {
|
|
1561
|
+
ok: true,
|
|
1562
|
+
reason: "",
|
|
1563
|
+
query: normalizeString(payload?.query) || normalizedQuery,
|
|
1564
|
+
results,
|
|
1565
|
+
count: Number(payload?.count ?? results.length) || results.length,
|
|
1566
|
+
hasMore: Boolean(payload?.has_more ?? payload?.hasMore),
|
|
1567
|
+
nextBeforeSequence: Number(payload?.next_before_sequence ?? payload?.nextBeforeSequence) || null,
|
|
1568
|
+
};
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1571
|
+
return {
|
|
1572
|
+
ok: false,
|
|
1573
|
+
reason: normalizeString(error?.message) || "search_failed",
|
|
1574
|
+
query: normalizedQuery,
|
|
1575
|
+
results: [],
|
|
1576
|
+
count: 0,
|
|
1577
|
+
hasMore: false,
|
|
1578
|
+
nextBeforeSequence: null,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
|
|
1241
1584
|
/**
|
|
1242
1585
|
* List sessions owned by the active user via `GET /api/v1/sessions`.
|
|
1243
1586
|
*
|
|
@@ -41,6 +41,9 @@ const TRANSCRIPT_EVENT_KINDS = new Set([
|
|
|
41
41
|
"session_message",
|
|
42
42
|
"session_say",
|
|
43
43
|
"agent_response",
|
|
44
|
+
"session_action",
|
|
45
|
+
"session_reply",
|
|
46
|
+
"session_reaction",
|
|
44
47
|
"session_usage",
|
|
45
48
|
"human_relay",
|
|
46
49
|
"agent_join",
|
|
@@ -359,26 +362,32 @@ export function buildTranscriptMarkdown({
|
|
|
359
362
|
`| ${avatarMd(identity)} | **${agent.displayName}** \`${agent.agentId}\` | ${agent.family} | ${formatDuration(agent.activeSeconds)} | ${agent.eventCount} | ${agent.tokens.toLocaleString("en-US")} | $${agent.costUsd.toFixed(4)} |`,
|
|
360
363
|
);
|
|
361
364
|
}
|
|
362
|
-
if (stats.agents.length === 0) {
|
|
363
|
-
lines.push("| ๐ค | (no agents joined) | โ | 0s | 0 | 0 | $0.00 |");
|
|
364
|
-
}
|
|
365
365
|
// Surface registered-but-silent agents at the bottom of the table so
|
|
366
366
|
// the participants list is comprehensive even if they never emitted
|
|
367
367
|
// a stream event.
|
|
368
368
|
const seenIds = new Set(stats.agents.map((a) => a.agentId));
|
|
369
|
+
const silentRegisteredAgents = [];
|
|
369
370
|
for (const registered of agents || []) {
|
|
370
371
|
const id = normalize(registered?.agentId);
|
|
371
372
|
if (!id || seenIds.has(id)) continue;
|
|
373
|
+
seenIds.add(id);
|
|
372
374
|
const profile = speakerProfiles.get(id) || null;
|
|
373
375
|
const identity = resolveSpeakerIdentity({
|
|
374
376
|
agentId: id,
|
|
375
377
|
agentModel: registered.model || "",
|
|
376
378
|
profile,
|
|
377
379
|
});
|
|
380
|
+
silentRegisteredAgents.push({ id, identity });
|
|
381
|
+
}
|
|
382
|
+
if (stats.agents.length === 0 && silentRegisteredAgents.length === 0) {
|
|
383
|
+
lines.push("| ๐ค | (no agents joined) | โ | 0s | 0 | 0 | $0.00 |");
|
|
384
|
+
}
|
|
385
|
+
for (const registered of silentRegisteredAgents) {
|
|
378
386
|
lines.push(
|
|
379
|
-
`| ${avatarMd(identity)} | **${identity.displayName}** \`${id}\` | ${identity.family} | 0s ยท idle | 0 | 0 | $0.0000 |`,
|
|
387
|
+
`| ${avatarMd(registered.identity)} | **${registered.identity.displayName}** \`${registered.id}\` | ${registered.identity.family} | 0s ยท idle | 0 | 0 | $0.0000 |`,
|
|
380
388
|
);
|
|
381
389
|
}
|
|
390
|
+
stats.participantCount = stats.agents.length + silentRegisteredAgents.length;
|
|
382
391
|
lines.push("");
|
|
383
392
|
|
|
384
393
|
// Conversation
|