sentinelayer-cli 0.9.3 → 0.9.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 +1 -1
- package/src/commands/session.js +132 -0
- package/src/session/recap.js +234 -8
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
69
69
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
70
70
|
import { listenSessionEvents } from "../session/listener.js";
|
|
71
|
+
import { buildSessionRecap } from "../session/recap.js";
|
|
71
72
|
import { deriveSessionTitle } from "../session/senti-naming.js";
|
|
72
73
|
import { pushSessionTitleToApi } from "../session/title-sync.js";
|
|
73
74
|
import {
|
|
@@ -560,6 +561,51 @@ function formatEventLine(event = {}) {
|
|
|
560
561
|
return `${ts} ${agentId} ${type}`;
|
|
561
562
|
}
|
|
562
563
|
|
|
564
|
+
async function appendMissingRemoteEvents(sessionId, remoteEvents = [], { targetPath } = {}) {
|
|
565
|
+
const events = Array.isArray(remoteEvents) ? remoteEvents : [];
|
|
566
|
+
if (events.length === 0) {
|
|
567
|
+
return {
|
|
568
|
+
appended: 0,
|
|
569
|
+
skipped: 0,
|
|
570
|
+
failed: 0,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const knownKeys = new Set();
|
|
574
|
+
const localEvents = await readStream(sessionId, {
|
|
575
|
+
targetPath,
|
|
576
|
+
tail: 0,
|
|
577
|
+
});
|
|
578
|
+
for (const event of localEvents) {
|
|
579
|
+
addSessionEventIdentityKeys(knownKeys, event);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let appended = 0;
|
|
583
|
+
let skipped = 0;
|
|
584
|
+
let failed = 0;
|
|
585
|
+
for (const event of events) {
|
|
586
|
+
if (sessionEventHasKnownIdentity(event, knownKeys)) {
|
|
587
|
+
skipped += 1;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const persisted = await appendToStream(sessionId, event, {
|
|
592
|
+
targetPath,
|
|
593
|
+
syncRemote: false,
|
|
594
|
+
});
|
|
595
|
+
addSessionEventIdentityKeys(knownKeys, persisted);
|
|
596
|
+
appended += 1;
|
|
597
|
+
} catch {
|
|
598
|
+
addSessionEventIdentityKeys(knownKeys, event);
|
|
599
|
+
failed += 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
appended,
|
|
604
|
+
skipped,
|
|
605
|
+
failed,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
563
609
|
function formatTemplateLaunchLine(slot = {}) {
|
|
564
610
|
const terminal = Number(slot.terminal || 0);
|
|
565
611
|
const role = normalizeString(slot.role) || "agent";
|
|
@@ -1479,6 +1525,92 @@ export function registerSessionCommand(program) {
|
|
|
1479
1525
|
}
|
|
1480
1526
|
});
|
|
1481
1527
|
|
|
1528
|
+
const recap = session
|
|
1529
|
+
.command("recap")
|
|
1530
|
+
.description("Build deterministic Senti session recaps");
|
|
1531
|
+
|
|
1532
|
+
recap
|
|
1533
|
+
.command("now [sessionId]")
|
|
1534
|
+
.description("Summarize current session activity, peers, findings, locks, and task ownership")
|
|
1535
|
+
.option("--session <id>", "Session id to recap")
|
|
1536
|
+
.option(
|
|
1537
|
+
"--remote",
|
|
1538
|
+
"Hydrate the latest durable API events before building the recap",
|
|
1539
|
+
)
|
|
1540
|
+
.option(
|
|
1541
|
+
"--agent <id>",
|
|
1542
|
+
"Agent id requesting the recap; self-authored events are omitted from recent snippets",
|
|
1543
|
+
process.env.SENTINELAYER_AGENT_ID || "",
|
|
1544
|
+
)
|
|
1545
|
+
.option("--max-events <n>", "Maximum recent local events to inspect (default 100)", "100")
|
|
1546
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1547
|
+
.option("--json", "Emit machine-readable output")
|
|
1548
|
+
.action(async (sessionId, options, command) => {
|
|
1549
|
+
const normalizedSessionId = normalizeString(sessionId) || resolveSessionIdOption(options);
|
|
1550
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
1551
|
+
const agentId = normalizeAgentId(options.agent, "");
|
|
1552
|
+
const maxEvents = parsePositiveInteger(options.maxEvents, "max-events", 100);
|
|
1553
|
+
let hydration = null;
|
|
1554
|
+
let remoteTail = null;
|
|
1555
|
+
let remoteAppend = null;
|
|
1556
|
+
if (options.remote) {
|
|
1557
|
+
hydration = await hydrateSessionFromRemote({
|
|
1558
|
+
sessionId: normalizedSessionId,
|
|
1559
|
+
targetPath,
|
|
1560
|
+
});
|
|
1561
|
+
remoteTail = await pollSessionEventsBefore(normalizedSessionId, {
|
|
1562
|
+
targetPath,
|
|
1563
|
+
limit: maxEvents,
|
|
1564
|
+
timeoutMs: 15_000,
|
|
1565
|
+
});
|
|
1566
|
+
if (remoteTail?.ok && Array.isArray(remoteTail.events) && remoteTail.events.length > 0) {
|
|
1567
|
+
remoteAppend = await appendMissingRemoteEvents(normalizedSessionId, remoteTail.events, {
|
|
1568
|
+
targetPath,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const current = await buildSessionRecap(normalizedSessionId, {
|
|
1573
|
+
forAgentId: agentId,
|
|
1574
|
+
maxEvents,
|
|
1575
|
+
targetPath,
|
|
1576
|
+
});
|
|
1577
|
+
const payload = {
|
|
1578
|
+
command: "session recap now",
|
|
1579
|
+
targetPath,
|
|
1580
|
+
sessionId: normalizedSessionId,
|
|
1581
|
+
agentId: current.forAgentId,
|
|
1582
|
+
maxEvents,
|
|
1583
|
+
generatedAt: current.generatedAt,
|
|
1584
|
+
ephemeral: current.ephemeral,
|
|
1585
|
+
style: current.style,
|
|
1586
|
+
recap: current.text,
|
|
1587
|
+
summary: current.summary,
|
|
1588
|
+
remote: options.remote
|
|
1589
|
+
? {
|
|
1590
|
+
hydration,
|
|
1591
|
+
tailProbe: remoteTail
|
|
1592
|
+
? {
|
|
1593
|
+
ok: Boolean(remoteTail.ok),
|
|
1594
|
+
reason: remoteTail.reason || "",
|
|
1595
|
+
count: Array.isArray(remoteTail.events) ? remoteTail.events.length : 0,
|
|
1596
|
+
cursor: remoteTail.cursor || null,
|
|
1597
|
+
}
|
|
1598
|
+
: null,
|
|
1599
|
+
appendedTail: remoteAppend,
|
|
1600
|
+
}
|
|
1601
|
+
: null,
|
|
1602
|
+
};
|
|
1603
|
+
if (shouldEmitJson(options, command)) {
|
|
1604
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
console.log(pc.bold(`Recap for session ${normalizedSessionId}`));
|
|
1608
|
+
if (payload.agentId) {
|
|
1609
|
+
console.log(pc.gray(`for agent=${payload.agentId}`));
|
|
1610
|
+
}
|
|
1611
|
+
console.log(current.text);
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1482
1614
|
session
|
|
1483
1615
|
.command("read <sessionId>")
|
|
1484
1616
|
.description("Read recent session messages")
|
package/src/session/recap.js
CHANGED
|
@@ -3,8 +3,10 @@ import path from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
|
|
5
5
|
import { createAgentEvent } from "../events/schema.js";
|
|
6
|
+
import { dedupeSessionEvents } from "./event-identity.js";
|
|
6
7
|
import { resolveSessionPaths } from "./paths.js";
|
|
7
8
|
import { appendToStream, readStream } from "./stream.js";
|
|
9
|
+
import { getSession } from "./store.js";
|
|
8
10
|
|
|
9
11
|
const SENTI_AGENT_ID = "senti";
|
|
10
12
|
const SENTI_MODEL = "gpt-5.4-mini";
|
|
@@ -13,6 +15,14 @@ const DEFAULT_RECAP_MAX_EVENTS = 100;
|
|
|
13
15
|
const DEFAULT_RECAP_INTERVAL_MS = 300_000;
|
|
14
16
|
const DEFAULT_RECAP_INACTIVITY_MS = 600_000;
|
|
15
17
|
const DEFAULT_RECAP_ACTIVITY_THRESHOLD = 5;
|
|
18
|
+
const DEFAULT_TASK_SUMMARY_LIMIT = 3;
|
|
19
|
+
const ACTIVE_TASK_STATUSES = new Set(["PENDING", "ACCEPTED", "BLOCKED"]);
|
|
20
|
+
const TASK_STATUS_KEYS = {
|
|
21
|
+
PENDING: "pending",
|
|
22
|
+
ACCEPTED: "accepted",
|
|
23
|
+
COMPLETED: "completed",
|
|
24
|
+
BLOCKED: "blocked",
|
|
25
|
+
};
|
|
16
26
|
|
|
17
27
|
const ACTIVE_RECAP_EMITTERS = new Map();
|
|
18
28
|
|
|
@@ -160,11 +170,123 @@ async function readPendingTasks(sessionId, { forAgentId = "", targetPath = proce
|
|
|
160
170
|
}
|
|
161
171
|
}
|
|
162
172
|
|
|
163
|
-
function
|
|
164
|
-
|
|
173
|
+
function normalizeTaskStatus(value) {
|
|
174
|
+
const normalized = normalizeString(value).toUpperCase();
|
|
175
|
+
return Object.prototype.hasOwnProperty.call(TASK_STATUS_KEYS, normalized)
|
|
176
|
+
? normalized
|
|
177
|
+
: "PENDING";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function taskOwner(task = {}) {
|
|
181
|
+
return (
|
|
182
|
+
normalizeString(task.toAgentId) ||
|
|
183
|
+
normalizeString(task.requestedToAgentId) ||
|
|
184
|
+
(normalizeString(task.roleFilter) ? `role:${normalizeString(task.roleFilter)}` : "") ||
|
|
185
|
+
"unassigned"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function shortTaskText(value) {
|
|
190
|
+
const text = normalizeString(value).replace(/\s+/g, " ");
|
|
191
|
+
if (text.length <= 80) {
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
return `${text.slice(0, 77)}...`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function emptyTaskLedgerSummary() {
|
|
198
|
+
return {
|
|
199
|
+
total: 0,
|
|
200
|
+
active: 0,
|
|
201
|
+
pending: 0,
|
|
202
|
+
accepted: 0,
|
|
203
|
+
blocked: 0,
|
|
204
|
+
completed: 0,
|
|
205
|
+
owners: [],
|
|
206
|
+
recent: [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function summarizeTaskLedger(tasks = [], { limit = DEFAULT_TASK_SUMMARY_LIMIT } = {}) {
|
|
211
|
+
const summary = emptyTaskLedgerSummary();
|
|
212
|
+
const owners = new Map();
|
|
213
|
+
const normalizedTasks = [];
|
|
214
|
+
|
|
215
|
+
for (const task of Array.isArray(tasks) ? tasks : []) {
|
|
216
|
+
if (!task || typeof task !== "object") {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const status = normalizeTaskStatus(task.status);
|
|
220
|
+
const statusKey = TASK_STATUS_KEYS[status];
|
|
221
|
+
const owner = taskOwner(task);
|
|
222
|
+
const priority = normalizeString(task.priority) || "when-free";
|
|
223
|
+
const taskId = normalizeString(task.taskId) || "task";
|
|
224
|
+
const updatedAt = normalizeIsoTimestamp(
|
|
225
|
+
task.updatedAt || task.completedAt || task.acceptedAt || task.createdAt,
|
|
226
|
+
new Date().toISOString(),
|
|
227
|
+
);
|
|
228
|
+
const record = {
|
|
229
|
+
taskId,
|
|
230
|
+
status,
|
|
231
|
+
priority,
|
|
232
|
+
owner,
|
|
233
|
+
task: shortTaskText(task.task),
|
|
234
|
+
updatedAt,
|
|
235
|
+
};
|
|
236
|
+
normalizedTasks.push(record);
|
|
237
|
+
summary.total += 1;
|
|
238
|
+
summary[statusKey] += 1;
|
|
239
|
+
|
|
240
|
+
if (ACTIVE_TASK_STATUSES.has(status)) {
|
|
241
|
+
summary.active += 1;
|
|
242
|
+
const ownerRecord = owners.get(owner) || {
|
|
243
|
+
agentId: owner,
|
|
244
|
+
active: 0,
|
|
245
|
+
pending: 0,
|
|
246
|
+
accepted: 0,
|
|
247
|
+
blocked: 0,
|
|
248
|
+
};
|
|
249
|
+
ownerRecord.active += 1;
|
|
250
|
+
ownerRecord[statusKey] += 1;
|
|
251
|
+
owners.set(owner, ownerRecord);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
summary.owners = [...owners.values()]
|
|
256
|
+
.sort((left, right) => {
|
|
257
|
+
if (right.active !== left.active) return right.active - left.active;
|
|
258
|
+
return left.agentId.localeCompare(right.agentId);
|
|
259
|
+
})
|
|
260
|
+
.slice(0, Math.max(1, limit));
|
|
261
|
+
summary.recent = normalizedTasks
|
|
262
|
+
.sort((left, right) => toEpoch(right.updatedAt) - toEpoch(left.updatedAt))
|
|
263
|
+
.slice(0, Math.max(1, limit));
|
|
264
|
+
return summary;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function readTaskLedgerSummary(
|
|
268
|
+
sessionId,
|
|
269
|
+
{ targetPath = process.cwd(), limit = DEFAULT_TASK_SUMMARY_LIMIT } = {},
|
|
270
|
+
) {
|
|
271
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
272
|
+
try {
|
|
273
|
+
const raw = await fsp.readFile(paths.tasksPath, "utf-8");
|
|
274
|
+
const parsed = JSON.parse(raw);
|
|
275
|
+
return summarizeTaskLedger(parsed?.tasks || [], { limit });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
278
|
+
return emptyTaskLedgerSummary();
|
|
279
|
+
}
|
|
280
|
+
return emptyTaskLedgerSummary();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function elapsedMinutesBetween(startIso, nowIso = new Date().toISOString()) {
|
|
285
|
+
const normalizedStartIso = normalizeString(startIso);
|
|
286
|
+
if (!normalizedStartIso) {
|
|
165
287
|
return 0;
|
|
166
288
|
}
|
|
167
|
-
const firstEpoch = toEpoch(
|
|
289
|
+
const firstEpoch = toEpoch(normalizedStartIso, nowIso);
|
|
168
290
|
const nowEpoch = toEpoch(nowIso, nowIso);
|
|
169
291
|
if (!Number.isFinite(firstEpoch) || !Number.isFinite(nowEpoch) || nowEpoch <= firstEpoch) {
|
|
170
292
|
return 0;
|
|
@@ -172,6 +294,51 @@ function buildElapsedMinutes(events = [], nowIso = new Date().toISOString()) {
|
|
|
172
294
|
return Math.max(0, Math.floor((nowEpoch - firstEpoch) / 60_000));
|
|
173
295
|
}
|
|
174
296
|
|
|
297
|
+
function earliestIso(values = [], fallbackIso = new Date().toISOString()) {
|
|
298
|
+
const validEpochs = values
|
|
299
|
+
.map((value) => normalizeString(value))
|
|
300
|
+
.filter(Boolean)
|
|
301
|
+
.map((value) => Date.parse(value))
|
|
302
|
+
.filter((value) => Number.isFinite(value));
|
|
303
|
+
if (validEpochs.length === 0) {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
validEpochs.sort((left, right) => left - right);
|
|
307
|
+
return normalizeIsoTimestamp(new Date(validEpochs[0]).toISOString(), fallbackIso);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildElapsedMinutes(events = [], nowIso = new Date().toISOString(), { startedAt = "" } = {}) {
|
|
311
|
+
const eventStart = Array.isArray(events) && events.length > 0 ? events[0]?.ts || events[0]?.timestamp : "";
|
|
312
|
+
const startIso = earliestIso([startedAt, eventStart], nowIso);
|
|
313
|
+
return elapsedMinutesBetween(startIso, nowIso);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function eventSequenceNumber(event = {}) {
|
|
317
|
+
for (const value of [event.sequenceId, event.sequence, event.seq, event.payload?.sequenceId]) {
|
|
318
|
+
const normalized = Number(value);
|
|
319
|
+
if (Number.isFinite(normalized)) {
|
|
320
|
+
return normalized;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function sortEventsByConversationTime(events = [], fallbackIso = new Date().toISOString()) {
|
|
327
|
+
return [...(Array.isArray(events) ? events : [])].sort((left, right) => {
|
|
328
|
+
const leftEpoch = toEpoch(left?.ts || left?.timestamp, fallbackIso);
|
|
329
|
+
const rightEpoch = toEpoch(right?.ts || right?.timestamp, fallbackIso);
|
|
330
|
+
if (leftEpoch !== rightEpoch) {
|
|
331
|
+
return leftEpoch - rightEpoch;
|
|
332
|
+
}
|
|
333
|
+
const leftSequence = eventSequenceNumber(left);
|
|
334
|
+
const rightSequence = eventSequenceNumber(right);
|
|
335
|
+
if (leftSequence !== rightSequence) {
|
|
336
|
+
return leftSequence - rightSequence;
|
|
337
|
+
}
|
|
338
|
+
return normalizeString(left?.cursor).localeCompare(normalizeString(right?.cursor));
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
175
342
|
function buildRecapKey(sessionId, targetPath) {
|
|
176
343
|
return `${path.resolve(String(targetPath || "."))}::${normalizeString(sessionId)}`;
|
|
177
344
|
}
|
|
@@ -181,6 +348,7 @@ function buildRecapText({
|
|
|
181
348
|
totalFindings = 0,
|
|
182
349
|
activeLocks = 0,
|
|
183
350
|
pendingTasks = 0,
|
|
351
|
+
taskLedger = emptyTaskLedgerSummary(),
|
|
184
352
|
snippets = [],
|
|
185
353
|
} = {}) {
|
|
186
354
|
const agentText =
|
|
@@ -191,13 +359,49 @@ function buildRecapText({
|
|
|
191
359
|
const lockText = `${activeLocks} file lock${activeLocks === 1 ? "" : "s"} active`;
|
|
192
360
|
const pendingText =
|
|
193
361
|
pendingTasks > 0 ? `You have ${pendingTasks} pending task${pendingTasks === 1 ? "" : "s"}.` : "";
|
|
362
|
+
const taskText = buildTaskLedgerText(taskLedger);
|
|
194
363
|
const snippetText = snippets.length > 0 ? `Recent: ${snippets.join(" | ")}` : "";
|
|
195
|
-
return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${snippetText}`.replace(
|
|
364
|
+
return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${snippetText}`.replace(
|
|
196
365
|
/\s+/g,
|
|
197
366
|
" "
|
|
198
367
|
).trim();
|
|
199
368
|
}
|
|
200
369
|
|
|
370
|
+
function buildTaskLedgerText(taskLedger = emptyTaskLedgerSummary()) {
|
|
371
|
+
const total = Number(taskLedger.total || 0);
|
|
372
|
+
if (!total) {
|
|
373
|
+
return "Tasks: none queued";
|
|
374
|
+
}
|
|
375
|
+
const active = Number(taskLedger.active || 0);
|
|
376
|
+
const counts = [
|
|
377
|
+
`${Number(taskLedger.pending || 0)} pending`,
|
|
378
|
+
`${Number(taskLedger.accepted || 0)} accepted`,
|
|
379
|
+
`${Number(taskLedger.blocked || 0)} blocked`,
|
|
380
|
+
`${Number(taskLedger.completed || 0)} done`,
|
|
381
|
+
].join(", ");
|
|
382
|
+
const ownerText =
|
|
383
|
+
Array.isArray(taskLedger.owners) && taskLedger.owners.length > 0
|
|
384
|
+
? `Owners: ${taskLedger.owners
|
|
385
|
+
.map((owner) => {
|
|
386
|
+
const parts = [];
|
|
387
|
+
if (owner.pending) parts.push(`${owner.pending} pending`);
|
|
388
|
+
if (owner.accepted) parts.push(`${owner.accepted} accepted`);
|
|
389
|
+
if (owner.blocked) parts.push(`${owner.blocked} blocked`);
|
|
390
|
+
return `${owner.agentId} (${parts.join("/") || `${owner.active || 0} active`})`;
|
|
391
|
+
})
|
|
392
|
+
.join("; ")}`
|
|
393
|
+
: "Owners: none active";
|
|
394
|
+
const recentText =
|
|
395
|
+
Array.isArray(taskLedger.recent) && taskLedger.recent.length > 0
|
|
396
|
+
? `Recent tasks: ${taskLedger.recent
|
|
397
|
+
.map((task) => `${task.priority} ${task.status} ${task.owner}: ${task.task}`)
|
|
398
|
+
.join(" | ")}`
|
|
399
|
+
: "";
|
|
400
|
+
return [`Tasks: ${active} active of ${total} total (${counts})`, ownerText, recentText]
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.join(". ");
|
|
403
|
+
}
|
|
404
|
+
|
|
201
405
|
// Multi-agent session etiquette + read-path rules surfaced in the
|
|
202
406
|
// context_briefing payload an agent receives on first join. Web
|
|
203
407
|
// renders this as markdown (see sentinelayer-web Session.tsx
|
|
@@ -235,7 +439,8 @@ function buildPeriodicText(recap = {}) {
|
|
|
235
439
|
const activeLocks = Number(summary.activeLocks || 0);
|
|
236
440
|
const lastActor = normalizeString(summary.lastActorId);
|
|
237
441
|
const actorText = lastActor ? `${lastActor} active` : "no active actor";
|
|
238
|
-
|
|
442
|
+
const taskText = buildTaskLedgerText(summary.taskLedger);
|
|
443
|
+
return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${actorText}.`;
|
|
239
444
|
}
|
|
240
445
|
|
|
241
446
|
export async function buildSessionRecap(
|
|
@@ -256,10 +461,19 @@ export async function buildSessionRecap(
|
|
|
256
461
|
const normalizedMaxEvents = normalizePositiveInteger(maxEvents, DEFAULT_RECAP_MAX_EVENTS);
|
|
257
462
|
const normalizedForAgentId = normalizeString(forAgentId);
|
|
258
463
|
|
|
259
|
-
const
|
|
464
|
+
const allEvents = await readStream(normalizedSessionId, {
|
|
260
465
|
targetPath: normalizedTargetPath,
|
|
261
|
-
tail:
|
|
466
|
+
tail: 0,
|
|
262
467
|
});
|
|
468
|
+
let sessionMetadata = null;
|
|
469
|
+
try {
|
|
470
|
+
sessionMetadata = await getSession(normalizedSessionId, { targetPath: normalizedTargetPath });
|
|
471
|
+
} catch {
|
|
472
|
+
sessionMetadata = null;
|
|
473
|
+
}
|
|
474
|
+
const events = sortEventsByConversationTime(dedupeSessionEvents(allEvents), normalizedNow).slice(
|
|
475
|
+
-normalizedMaxEvents,
|
|
476
|
+
);
|
|
263
477
|
const visibleEvents = (Array.isArray(events) ? events : []).filter((event) => {
|
|
264
478
|
const agentId = normalizeString(event.agent?.id || event.agentId);
|
|
265
479
|
if (!agentId) {
|
|
@@ -288,17 +502,24 @@ export async function buildSessionRecap(
|
|
|
288
502
|
forAgentId: normalizedForAgentId,
|
|
289
503
|
targetPath: normalizedTargetPath,
|
|
290
504
|
});
|
|
505
|
+
const taskLedger = await readTaskLedgerSummary(normalizedSessionId, {
|
|
506
|
+
targetPath: normalizedTargetPath,
|
|
507
|
+
});
|
|
291
508
|
const snippets = summarizeRecentActivity(visibleEvents, {
|
|
292
509
|
forAgentId: normalizedForAgentId,
|
|
293
510
|
limit: 2,
|
|
294
511
|
});
|
|
295
|
-
const
|
|
512
|
+
const windowElapsedMinutes = buildElapsedMinutes(visibleEvents, normalizedNow);
|
|
513
|
+
const elapsedMinutes = buildElapsedMinutes(visibleEvents, normalizedNow, {
|
|
514
|
+
startedAt: sessionMetadata?.createdAt,
|
|
515
|
+
});
|
|
296
516
|
const latestEvent = visibleEvents.length > 0 ? visibleEvents[visibleEvents.length - 1] : null;
|
|
297
517
|
const recapText = buildRecapText({
|
|
298
518
|
activeAgents,
|
|
299
519
|
totalFindings: totalFindingsCount,
|
|
300
520
|
activeLocks,
|
|
301
521
|
pendingTasks,
|
|
522
|
+
taskLedger,
|
|
302
523
|
snippets,
|
|
303
524
|
});
|
|
304
525
|
|
|
@@ -317,8 +538,13 @@ export async function buildSessionRecap(
|
|
|
317
538
|
totalFindingsCount,
|
|
318
539
|
activeLocks,
|
|
319
540
|
pendingTasksForAgent: pendingTasks,
|
|
541
|
+
taskLedger,
|
|
320
542
|
snippets,
|
|
321
543
|
elapsedMinutes,
|
|
544
|
+
windowElapsedMinutes,
|
|
545
|
+
sessionStartedAt: sessionMetadata?.createdAt
|
|
546
|
+
? normalizeIsoTimestamp(sessionMetadata.createdAt, normalizedNow)
|
|
547
|
+
: null,
|
|
322
548
|
lastActorId: normalizeString(latestEvent?.agent?.id || latestEvent?.agentId) || null,
|
|
323
549
|
lastEventAt: latestEvent ? normalizeIsoTimestamp(latestEvent.ts, normalizedNow) : null,
|
|
324
550
|
},
|