sentinelayer-cli 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/session/recap.js +164 -2
package/package.json
CHANGED
package/src/session/recap.js
CHANGED
|
@@ -13,6 +13,14 @@ const DEFAULT_RECAP_MAX_EVENTS = 100;
|
|
|
13
13
|
const DEFAULT_RECAP_INTERVAL_MS = 300_000;
|
|
14
14
|
const DEFAULT_RECAP_INACTIVITY_MS = 600_000;
|
|
15
15
|
const DEFAULT_RECAP_ACTIVITY_THRESHOLD = 5;
|
|
16
|
+
const DEFAULT_TASK_SUMMARY_LIMIT = 3;
|
|
17
|
+
const ACTIVE_TASK_STATUSES = new Set(["PENDING", "ACCEPTED", "BLOCKED"]);
|
|
18
|
+
const TASK_STATUS_KEYS = {
|
|
19
|
+
PENDING: "pending",
|
|
20
|
+
ACCEPTED: "accepted",
|
|
21
|
+
COMPLETED: "completed",
|
|
22
|
+
BLOCKED: "blocked",
|
|
23
|
+
};
|
|
16
24
|
|
|
17
25
|
const ACTIVE_RECAP_EMITTERS = new Map();
|
|
18
26
|
|
|
@@ -160,6 +168,117 @@ async function readPendingTasks(sessionId, { forAgentId = "", targetPath = proce
|
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
|
|
171
|
+
function normalizeTaskStatus(value) {
|
|
172
|
+
const normalized = normalizeString(value).toUpperCase();
|
|
173
|
+
return Object.prototype.hasOwnProperty.call(TASK_STATUS_KEYS, normalized)
|
|
174
|
+
? normalized
|
|
175
|
+
: "PENDING";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function taskOwner(task = {}) {
|
|
179
|
+
return (
|
|
180
|
+
normalizeString(task.toAgentId) ||
|
|
181
|
+
normalizeString(task.requestedToAgentId) ||
|
|
182
|
+
(normalizeString(task.roleFilter) ? `role:${normalizeString(task.roleFilter)}` : "") ||
|
|
183
|
+
"unassigned"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function shortTaskText(value) {
|
|
188
|
+
const text = normalizeString(value).replace(/\s+/g, " ");
|
|
189
|
+
if (text.length <= 80) {
|
|
190
|
+
return text;
|
|
191
|
+
}
|
|
192
|
+
return `${text.slice(0, 77)}...`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function emptyTaskLedgerSummary() {
|
|
196
|
+
return {
|
|
197
|
+
total: 0,
|
|
198
|
+
active: 0,
|
|
199
|
+
pending: 0,
|
|
200
|
+
accepted: 0,
|
|
201
|
+
blocked: 0,
|
|
202
|
+
completed: 0,
|
|
203
|
+
owners: [],
|
|
204
|
+
recent: [],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function summarizeTaskLedger(tasks = [], { limit = DEFAULT_TASK_SUMMARY_LIMIT } = {}) {
|
|
209
|
+
const summary = emptyTaskLedgerSummary();
|
|
210
|
+
const owners = new Map();
|
|
211
|
+
const normalizedTasks = [];
|
|
212
|
+
|
|
213
|
+
for (const task of Array.isArray(tasks) ? tasks : []) {
|
|
214
|
+
if (!task || typeof task !== "object") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const status = normalizeTaskStatus(task.status);
|
|
218
|
+
const statusKey = TASK_STATUS_KEYS[status];
|
|
219
|
+
const owner = taskOwner(task);
|
|
220
|
+
const priority = normalizeString(task.priority) || "when-free";
|
|
221
|
+
const taskId = normalizeString(task.taskId) || "task";
|
|
222
|
+
const updatedAt = normalizeIsoTimestamp(
|
|
223
|
+
task.updatedAt || task.completedAt || task.acceptedAt || task.createdAt,
|
|
224
|
+
new Date().toISOString(),
|
|
225
|
+
);
|
|
226
|
+
const record = {
|
|
227
|
+
taskId,
|
|
228
|
+
status,
|
|
229
|
+
priority,
|
|
230
|
+
owner,
|
|
231
|
+
task: shortTaskText(task.task),
|
|
232
|
+
updatedAt,
|
|
233
|
+
};
|
|
234
|
+
normalizedTasks.push(record);
|
|
235
|
+
summary.total += 1;
|
|
236
|
+
summary[statusKey] += 1;
|
|
237
|
+
|
|
238
|
+
if (ACTIVE_TASK_STATUSES.has(status)) {
|
|
239
|
+
summary.active += 1;
|
|
240
|
+
const ownerRecord = owners.get(owner) || {
|
|
241
|
+
agentId: owner,
|
|
242
|
+
active: 0,
|
|
243
|
+
pending: 0,
|
|
244
|
+
accepted: 0,
|
|
245
|
+
blocked: 0,
|
|
246
|
+
};
|
|
247
|
+
ownerRecord.active += 1;
|
|
248
|
+
ownerRecord[statusKey] += 1;
|
|
249
|
+
owners.set(owner, ownerRecord);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
summary.owners = [...owners.values()]
|
|
254
|
+
.sort((left, right) => {
|
|
255
|
+
if (right.active !== left.active) return right.active - left.active;
|
|
256
|
+
return left.agentId.localeCompare(right.agentId);
|
|
257
|
+
})
|
|
258
|
+
.slice(0, Math.max(1, limit));
|
|
259
|
+
summary.recent = normalizedTasks
|
|
260
|
+
.sort((left, right) => toEpoch(right.updatedAt) - toEpoch(left.updatedAt))
|
|
261
|
+
.slice(0, Math.max(1, limit));
|
|
262
|
+
return summary;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function readTaskLedgerSummary(
|
|
266
|
+
sessionId,
|
|
267
|
+
{ targetPath = process.cwd(), limit = DEFAULT_TASK_SUMMARY_LIMIT } = {},
|
|
268
|
+
) {
|
|
269
|
+
const paths = resolveSessionPaths(sessionId, { targetPath });
|
|
270
|
+
try {
|
|
271
|
+
const raw = await fsp.readFile(paths.tasksPath, "utf-8");
|
|
272
|
+
const parsed = JSON.parse(raw);
|
|
273
|
+
return summarizeTaskLedger(parsed?.tasks || [], { limit });
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
276
|
+
return emptyTaskLedgerSummary();
|
|
277
|
+
}
|
|
278
|
+
return emptyTaskLedgerSummary();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
163
282
|
function buildElapsedMinutes(events = [], nowIso = new Date().toISOString()) {
|
|
164
283
|
if (!Array.isArray(events) || events.length === 0) {
|
|
165
284
|
return 0;
|
|
@@ -181,6 +300,7 @@ function buildRecapText({
|
|
|
181
300
|
totalFindings = 0,
|
|
182
301
|
activeLocks = 0,
|
|
183
302
|
pendingTasks = 0,
|
|
303
|
+
taskLedger = emptyTaskLedgerSummary(),
|
|
184
304
|
snippets = [],
|
|
185
305
|
} = {}) {
|
|
186
306
|
const agentText =
|
|
@@ -191,13 +311,49 @@ function buildRecapText({
|
|
|
191
311
|
const lockText = `${activeLocks} file lock${activeLocks === 1 ? "" : "s"} active`;
|
|
192
312
|
const pendingText =
|
|
193
313
|
pendingTasks > 0 ? `You have ${pendingTasks} pending task${pendingTasks === 1 ? "" : "s"}.` : "";
|
|
314
|
+
const taskText = buildTaskLedgerText(taskLedger);
|
|
194
315
|
const snippetText = snippets.length > 0 ? `Recent: ${snippets.join(" | ")}` : "";
|
|
195
|
-
return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${snippetText}`.replace(
|
|
316
|
+
return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${snippetText}`.replace(
|
|
196
317
|
/\s+/g,
|
|
197
318
|
" "
|
|
198
319
|
).trim();
|
|
199
320
|
}
|
|
200
321
|
|
|
322
|
+
function buildTaskLedgerText(taskLedger = emptyTaskLedgerSummary()) {
|
|
323
|
+
const total = Number(taskLedger.total || 0);
|
|
324
|
+
if (!total) {
|
|
325
|
+
return "Tasks: none queued";
|
|
326
|
+
}
|
|
327
|
+
const active = Number(taskLedger.active || 0);
|
|
328
|
+
const counts = [
|
|
329
|
+
`${Number(taskLedger.pending || 0)} pending`,
|
|
330
|
+
`${Number(taskLedger.accepted || 0)} accepted`,
|
|
331
|
+
`${Number(taskLedger.blocked || 0)} blocked`,
|
|
332
|
+
`${Number(taskLedger.completed || 0)} done`,
|
|
333
|
+
].join(", ");
|
|
334
|
+
const ownerText =
|
|
335
|
+
Array.isArray(taskLedger.owners) && taskLedger.owners.length > 0
|
|
336
|
+
? `Owners: ${taskLedger.owners
|
|
337
|
+
.map((owner) => {
|
|
338
|
+
const parts = [];
|
|
339
|
+
if (owner.pending) parts.push(`${owner.pending} pending`);
|
|
340
|
+
if (owner.accepted) parts.push(`${owner.accepted} accepted`);
|
|
341
|
+
if (owner.blocked) parts.push(`${owner.blocked} blocked`);
|
|
342
|
+
return `${owner.agentId} (${parts.join("/") || `${owner.active || 0} active`})`;
|
|
343
|
+
})
|
|
344
|
+
.join("; ")}`
|
|
345
|
+
: "Owners: none active";
|
|
346
|
+
const recentText =
|
|
347
|
+
Array.isArray(taskLedger.recent) && taskLedger.recent.length > 0
|
|
348
|
+
? `Recent tasks: ${taskLedger.recent
|
|
349
|
+
.map((task) => `${task.priority} ${task.status} ${task.owner}: ${task.task}`)
|
|
350
|
+
.join(" | ")}`
|
|
351
|
+
: "";
|
|
352
|
+
return [`Tasks: ${active} active of ${total} total (${counts})`, ownerText, recentText]
|
|
353
|
+
.filter(Boolean)
|
|
354
|
+
.join(". ");
|
|
355
|
+
}
|
|
356
|
+
|
|
201
357
|
// Multi-agent session etiquette + read-path rules surfaced in the
|
|
202
358
|
// context_briefing payload an agent receives on first join. Web
|
|
203
359
|
// renders this as markdown (see sentinelayer-web Session.tsx
|
|
@@ -235,7 +391,8 @@ function buildPeriodicText(recap = {}) {
|
|
|
235
391
|
const activeLocks = Number(summary.activeLocks || 0);
|
|
236
392
|
const lastActor = normalizeString(summary.lastActorId);
|
|
237
393
|
const actorText = lastActor ? `${lastActor} active` : "no active actor";
|
|
238
|
-
|
|
394
|
+
const taskText = buildTaskLedgerText(summary.taskLedger);
|
|
395
|
+
return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${actorText}.`;
|
|
239
396
|
}
|
|
240
397
|
|
|
241
398
|
export async function buildSessionRecap(
|
|
@@ -288,6 +445,9 @@ export async function buildSessionRecap(
|
|
|
288
445
|
forAgentId: normalizedForAgentId,
|
|
289
446
|
targetPath: normalizedTargetPath,
|
|
290
447
|
});
|
|
448
|
+
const taskLedger = await readTaskLedgerSummary(normalizedSessionId, {
|
|
449
|
+
targetPath: normalizedTargetPath,
|
|
450
|
+
});
|
|
291
451
|
const snippets = summarizeRecentActivity(visibleEvents, {
|
|
292
452
|
forAgentId: normalizedForAgentId,
|
|
293
453
|
limit: 2,
|
|
@@ -299,6 +459,7 @@ export async function buildSessionRecap(
|
|
|
299
459
|
totalFindings: totalFindingsCount,
|
|
300
460
|
activeLocks,
|
|
301
461
|
pendingTasks,
|
|
462
|
+
taskLedger,
|
|
302
463
|
snippets,
|
|
303
464
|
});
|
|
304
465
|
|
|
@@ -317,6 +478,7 @@ export async function buildSessionRecap(
|
|
|
317
478
|
totalFindingsCount,
|
|
318
479
|
activeLocks,
|
|
319
480
|
pendingTasksForAgent: pendingTasks,
|
|
481
|
+
taskLedger,
|
|
320
482
|
snippets,
|
|
321
483
|
elapsedMinutes,
|
|
322
484
|
lastActorId: normalizeString(latestEvent?.agent?.id || latestEvent?.agentId) || null,
|