sentinelayer-cli 0.19.0 → 0.20.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/ai/identity-lifecycle.js +14 -2
- package/src/commands/mcp.js +60 -0
- package/src/commands/session.js +1255 -25
- package/src/legacy-cli.js +16 -11
- package/src/mcp/registry.js +151 -0
- package/src/mcp/session-stdio-server.js +977 -0
- package/src/scan/generator.js +3 -2
- package/src/session/agent-registry.js +118 -0
- package/src/session/checkpoints.js +71 -1
- package/src/session/coordination-guidance.js +3 -2
- package/src/session/listener.js +302 -68
- package/src/session/pricing-ledger.js +34 -4
- package/src/session/recap.js +4 -2
- package/src/session/sync.js +278 -0
- package/src/session/transcript.js +86 -36
- package/src/session/usage.js +5 -5
- package/src/session/wake/claude.js +175 -0
- package/src/session/wake/codex.js +394 -0
- package/src/session/wake/cursor-store.js +69 -0
- package/src/session/wake/dispatcher.js +184 -0
- package/src/session/wake/pump.js +135 -0
- package/src/session/wake/registry.js +80 -0
- package/src/session/wake/resolve-target.js +146 -0
- package/src/session/wake/sentid.js +103 -0
package/src/commands/session.js
CHANGED
|
@@ -1,6 +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 { setTimeout as sleep } from "node:timers/promises";
|
|
4
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
|
|
6
7
|
import pc from "picocolors";
|
|
@@ -26,6 +27,7 @@ import { createAgentEvent } from "../events/schema.js";
|
|
|
26
27
|
import {
|
|
27
28
|
detectStaleAgents,
|
|
28
29
|
listAgents,
|
|
30
|
+
rememberAgentIdentity,
|
|
29
31
|
registerAgent,
|
|
30
32
|
unregisterAgent,
|
|
31
33
|
} from "../session/agent-registry.js";
|
|
@@ -64,6 +66,7 @@ import {
|
|
|
64
66
|
listSessionMessageActions,
|
|
65
67
|
listSessionsFromApi,
|
|
66
68
|
probeSessionAccess,
|
|
69
|
+
pollSessionEvents,
|
|
67
70
|
pollSessionEventsBefore,
|
|
68
71
|
searchSessionEvents,
|
|
69
72
|
syncSessionEventToApi,
|
|
@@ -85,8 +88,16 @@ import {
|
|
|
85
88
|
import {
|
|
86
89
|
createSessionCheckpoint,
|
|
87
90
|
generateSessionCheckpoint,
|
|
91
|
+
generateSessionCheckpointBatch,
|
|
88
92
|
listSessionCheckpoints,
|
|
89
93
|
} from "../session/checkpoints.js";
|
|
94
|
+
import {
|
|
95
|
+
buildCodexExecResumeInvocation,
|
|
96
|
+
buildCodexWakePrompt,
|
|
97
|
+
recordCodexWakeRegistration,
|
|
98
|
+
runCodexExecResume,
|
|
99
|
+
} from "../session/wake/codex.js";
|
|
100
|
+
import createSentid from "../session/wake/sentid.js";
|
|
90
101
|
import { authLoginHint, preferredCliCommand } from "../ui/command-hints.js";
|
|
91
102
|
import { parseCsvTokens } from "./ai/shared.js";
|
|
92
103
|
|
|
@@ -101,6 +112,164 @@ function normalizeString(value) {
|
|
|
101
112
|
return String(value || "").trim();
|
|
102
113
|
}
|
|
103
114
|
|
|
115
|
+
const SESSION_SAY_CONFIRM_ATTEMPTS = 3;
|
|
116
|
+
const SESSION_SAY_CONFIRM_DELAY_MS = 250;
|
|
117
|
+
const SESSION_SAY_CONFIRM_PAGE_LIMIT = 200;
|
|
118
|
+
const SESSION_SAY_CONFIRM_MAX_PAGES = 10;
|
|
119
|
+
const SESSION_SAY_LOCAL_ONLY_REASONS = new Set([
|
|
120
|
+
"no_session",
|
|
121
|
+
"not_authenticated",
|
|
122
|
+
"remote_sync_disabled_env",
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
function isLocalOnlySessionSayReason(reason) {
|
|
126
|
+
return SESSION_SAY_LOCAL_ONLY_REASONS.has(normalizeString(reason));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sessionEventMatchesClientMessageId(event, clientMessageId) {
|
|
130
|
+
const normalizedClientMessageId = normalizeString(clientMessageId);
|
|
131
|
+
if (!normalizedClientMessageId || !event || typeof event !== "object") {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
135
|
+
const candidates = [
|
|
136
|
+
event.id,
|
|
137
|
+
event.eventId,
|
|
138
|
+
event.event_id,
|
|
139
|
+
event.idempotencyToken,
|
|
140
|
+
event.idempotency_token,
|
|
141
|
+
event.clientMessageId,
|
|
142
|
+
event.client_message_id,
|
|
143
|
+
payload.id,
|
|
144
|
+
payload.messageId,
|
|
145
|
+
payload.message_id,
|
|
146
|
+
payload.eventId,
|
|
147
|
+
payload.event_id,
|
|
148
|
+
payload.idempotencyToken,
|
|
149
|
+
payload.idempotency_token,
|
|
150
|
+
payload.clientMessageId,
|
|
151
|
+
payload.client_message_id,
|
|
152
|
+
];
|
|
153
|
+
return candidates.some((candidate) => normalizeString(candidate) === normalizedClientMessageId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function readSessionConfirmationAnchor(sessionId, { targetPath } = {}) {
|
|
157
|
+
const result = await pollSessionEventsBefore(sessionId, {
|
|
158
|
+
targetPath,
|
|
159
|
+
limit: 1,
|
|
160
|
+
forceCircuitProbe: true,
|
|
161
|
+
});
|
|
162
|
+
if (!result?.ok) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
reason: normalizeString(result?.reason) || "confirmation_anchor_failed",
|
|
166
|
+
cursor: null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
170
|
+
const lastEvent = events[events.length - 1] || null;
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
reason: "",
|
|
174
|
+
cursor: normalizeString(lastEvent?.cursor) || normalizeString(result.cursor) || null,
|
|
175
|
+
sequenceId: Number(lastEvent?.sequenceId ?? lastEvent?.sequence_id) || null,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function confirmSessionEventVisible(sessionId, clientMessageId, { targetPath, anchorCursor = null } = {}) {
|
|
180
|
+
let lastReason = "not_visible";
|
|
181
|
+
let checked = 0;
|
|
182
|
+
let pages = 0;
|
|
183
|
+
const normalizedAnchorCursor = normalizeString(anchorCursor) || null;
|
|
184
|
+
for (let attempt = 1; attempt <= SESSION_SAY_CONFIRM_ATTEMPTS; attempt += 1) {
|
|
185
|
+
let pageCursor = normalizedAnchorCursor;
|
|
186
|
+
for (let page = 1; page <= SESSION_SAY_CONFIRM_MAX_PAGES; page += 1) {
|
|
187
|
+
pages += 1;
|
|
188
|
+
const result = await pollSessionEvents(sessionId, {
|
|
189
|
+
targetPath,
|
|
190
|
+
since: pageCursor,
|
|
191
|
+
limit: SESSION_SAY_CONFIRM_PAGE_LIMIT,
|
|
192
|
+
forceCircuitProbe: true,
|
|
193
|
+
});
|
|
194
|
+
if (!result?.ok) {
|
|
195
|
+
lastReason = normalizeString(result?.reason) || "confirmation_poll_failed";
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
199
|
+
checked += events.length;
|
|
200
|
+
const confirmedEvent = events.find((candidate) => sessionEventMatchesClientMessageId(candidate, clientMessageId));
|
|
201
|
+
if (confirmedEvent) {
|
|
202
|
+
return {
|
|
203
|
+
confirmed: true,
|
|
204
|
+
reason: "",
|
|
205
|
+
checked,
|
|
206
|
+
pages,
|
|
207
|
+
anchorCursor: normalizedAnchorCursor,
|
|
208
|
+
event: confirmedEvent,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
lastReason = "not_visible";
|
|
212
|
+
const nextCursor = normalizeString(result.cursor);
|
|
213
|
+
if (!events.length || !nextCursor || nextCursor === pageCursor) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
pageCursor = nextCursor;
|
|
217
|
+
}
|
|
218
|
+
if (attempt < SESSION_SAY_CONFIRM_ATTEMPTS) {
|
|
219
|
+
await sleep(SESSION_SAY_CONFIRM_DELAY_MS);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
confirmed: false,
|
|
224
|
+
reason: lastReason,
|
|
225
|
+
checked,
|
|
226
|
+
pages,
|
|
227
|
+
anchorCursor: normalizedAnchorCursor,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function formatSessionListText(value, { maxLength = 80 } = {}) {
|
|
232
|
+
const normalized = normalizeString(value)
|
|
233
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
234
|
+
.replace(/\s+/g, " ")
|
|
235
|
+
.replace(/"/g, "'");
|
|
236
|
+
if (!normalized) {
|
|
237
|
+
return "";
|
|
238
|
+
}
|
|
239
|
+
const limit = Math.max(8, Number(maxLength) || 80);
|
|
240
|
+
return normalized.length > limit
|
|
241
|
+
? `${normalized.slice(0, Math.max(0, limit - 3)).trimEnd()}...`
|
|
242
|
+
: normalized;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function sessionListCodebaseLabel(value) {
|
|
246
|
+
const normalized = formatSessionListText(value, { maxLength: 120 });
|
|
247
|
+
if (!normalized) {
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
const segments = normalized.split(/[\\/]+/).filter(Boolean);
|
|
251
|
+
if (segments.length <= 2) {
|
|
252
|
+
return formatSessionListText(normalized, { maxLength: 80 });
|
|
253
|
+
}
|
|
254
|
+
return formatSessionListText(`.../${segments.slice(-2).join("/")}`, {
|
|
255
|
+
maxLength: 80,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function formatSessionListLine(item = {}) {
|
|
260
|
+
const sessionId = normalizeString(item.sessionId) || "unknown-session";
|
|
261
|
+
const status = normalizeString(item.status) || "unknown";
|
|
262
|
+
const archiveStatus = normalizeString(item.archiveStatus);
|
|
263
|
+
const archive = archiveStatus ? ` archive=${archiveStatus}` : "";
|
|
264
|
+
const title = formatSessionListText(item.title || item.summaryText, { maxLength: 72 });
|
|
265
|
+
const codebase = sessionListCodebaseLabel(item.codebasePath);
|
|
266
|
+
const titlePart = title ? ` title="${title}"` : "";
|
|
267
|
+
const codebasePart = codebase ? ` codebase="${codebase}"` : "";
|
|
268
|
+
const created = item.createdAt || "?";
|
|
269
|
+
const lastActivity = item.lastActivityAt ? ` last=${item.lastActivityAt}` : "";
|
|
270
|
+
return `${sessionId} status=${status}${archive}${titlePart}${codebasePart} created=${created}${lastActivity}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
104
273
|
const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
105
274
|
"ack",
|
|
106
275
|
"working_on",
|
|
@@ -150,7 +319,7 @@ const SESSION_MESSAGE_ACTION_DESCRIPTIONS = Object.freeze([
|
|
|
150
319
|
{
|
|
151
320
|
type: "view",
|
|
152
321
|
command: "sl session view <id> <sequence>",
|
|
153
|
-
description: "
|
|
322
|
+
description: "Manually backfill a read receipt for a target message; remote reads record views automatically.",
|
|
154
323
|
},
|
|
155
324
|
]);
|
|
156
325
|
|
|
@@ -219,6 +388,7 @@ function actionDisplayMessage(action = {}) {
|
|
|
219
388
|
function buildSessionActionEvent(sessionId, action = {}) {
|
|
220
389
|
const actionType = normalizeString(action.actionType ?? action.action_type).toLowerCase();
|
|
221
390
|
if (!SESSION_MESSAGE_ACTION_TYPES.has(actionType)) return null;
|
|
391
|
+
if (actionType === "view") return null;
|
|
222
392
|
const id =
|
|
223
393
|
normalizeString(action.id) ||
|
|
224
394
|
shortSha256(
|
|
@@ -265,6 +435,91 @@ function buildSessionActionEvents(sessionId, actions = []) {
|
|
|
265
435
|
.filter(Boolean);
|
|
266
436
|
}
|
|
267
437
|
|
|
438
|
+
function readProjectionUnacknowledgedHumanMessages(remoteActions = null) {
|
|
439
|
+
const messages = remoteActions?.projection?.unacknowledgedHumanMessages;
|
|
440
|
+
return Array.isArray(messages)
|
|
441
|
+
? messages
|
|
442
|
+
.filter((event) => event && typeof event === "object")
|
|
443
|
+
.sort((left, right) => humanAskSortValue(right) - humanAskSortValue(left))
|
|
444
|
+
: [];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function messageActionCreatedMs(action = {}) {
|
|
448
|
+
const epoch = Date.parse(normalizeString(action.createdAt || action.created_at || action.ts || action.timestamp));
|
|
449
|
+
return Number.isFinite(epoch) ? epoch : 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function messageActionActorId(action = {}) {
|
|
453
|
+
return normalizeString(action.actorId || action.actor_id || action.agentId || action.agent_id) || "unknown";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function isHumanMessageAction(action = {}) {
|
|
457
|
+
if (action?.isHumanActivity === true) return true;
|
|
458
|
+
if (normalizeString(action.actorKind || action.actor_kind).toLowerCase() === "human") return true;
|
|
459
|
+
return messageActionActorId(action).startsWith("human-");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function readProjectionRecentHumanActivity(remoteActions = null) {
|
|
463
|
+
const projected = remoteActions?.projection?.recentActivity;
|
|
464
|
+
const source = Array.isArray(projected) && projected.length > 0 ? projected : remoteActions?.actions;
|
|
465
|
+
return Array.isArray(source)
|
|
466
|
+
? source
|
|
467
|
+
.filter((action) => action && typeof action === "object" && isHumanMessageAction(action))
|
|
468
|
+
.sort((left, right) => {
|
|
469
|
+
const timeDiff = messageActionCreatedMs(right) - messageActionCreatedMs(left);
|
|
470
|
+
if (timeDiff !== 0) return timeDiff;
|
|
471
|
+
return normalizeString(right.id).localeCompare(normalizeString(left.id));
|
|
472
|
+
})
|
|
473
|
+
: [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function formatMessageActionActivityLine(action = {}) {
|
|
477
|
+
const actionType = normalizeString(action.actionType || action.action_type) || "action";
|
|
478
|
+
const targetSequence = Number(action.targetSequenceId ?? action.target_sequence_id ?? 0);
|
|
479
|
+
const targetActionId = normalizeString(action.targetActionId || action.target_action_id);
|
|
480
|
+
const target = targetActionId
|
|
481
|
+
? `action:${targetActionId}`
|
|
482
|
+
: targetSequence > 0
|
|
483
|
+
? `#${Math.floor(targetSequence)}`
|
|
484
|
+
: normalizeString(action.targetCursor || action.target_cursor) || "unknown-target";
|
|
485
|
+
const ts = normalizeString(action.createdAt || action.created_at || action.ts || action.timestamp);
|
|
486
|
+
const note = normalizeString(action.note || action.message || "");
|
|
487
|
+
const shortNote = note.length > 220 ? `${note.slice(0, 217)}...` : note;
|
|
488
|
+
const suffix = shortNote ? `: ${shortNote}` : "";
|
|
489
|
+
return `${actionType} ${target} by ${messageActionActorId(action)}${ts ? ` ${ts}` : ""}${suffix}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function humanAskSequence(event = {}) {
|
|
493
|
+
const value = Number(event.sequenceId ?? event.sequence_id ?? event.sequence ?? 0);
|
|
494
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function humanAskAgentId(event = {}) {
|
|
498
|
+
return normalizeString(event.agent?.id || event.agentId || event.agent_id) || "human";
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function humanAskMessage(event = {}) {
|
|
502
|
+
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
503
|
+
return normalizeString(payload.message || payload.note || payload.reason || "");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function humanAskSortValue(event = {}) {
|
|
507
|
+
const sequence = humanAskSequence(event);
|
|
508
|
+
if (sequence) return sequence;
|
|
509
|
+
const epoch = Date.parse(normalizeString(event.ts || event.timestamp || event.createdAt || event.created_at));
|
|
510
|
+
return Number.isFinite(epoch) ? epoch : 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function formatHumanAskLine(event = {}) {
|
|
514
|
+
const sequence = humanAskSequence(event);
|
|
515
|
+
const sequenceLabel = sequence ? `#${sequence}` : normalizeString(event.cursor) || "unsequenced";
|
|
516
|
+
const ts = normalizeString(event.ts || event.timestamp || event.createdAt || event.created_at);
|
|
517
|
+
const message = humanAskMessage(event);
|
|
518
|
+
const shortMessage = message.length > 220 ? `${message.slice(0, 217)}...` : message;
|
|
519
|
+
const suffix = shortMessage ? `: ${shortMessage}` : "";
|
|
520
|
+
return `${sequenceLabel} ${humanAskAgentId(event)}${ts ? ` ${ts}` : ""}${suffix}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
268
523
|
function eventTimestampMs(event = {}) {
|
|
269
524
|
for (const key of ["ts", "timestamp", "createdAt", "at"]) {
|
|
270
525
|
const epoch = Date.parse(normalizeString(event?.[key]));
|
|
@@ -329,6 +584,99 @@ function defaultActionIdempotencyKey({
|
|
|
329
584
|
return `cli:${normalizeString(actionType).toLowerCase()}:${target}:${actor}:${noteHash}`;
|
|
330
585
|
}
|
|
331
586
|
|
|
587
|
+
function sessionReadViewTarget(event = {}) {
|
|
588
|
+
const eventType = normalizeString(event.event || event.type).toLowerCase();
|
|
589
|
+
if (eventType !== "session_message" || isSessionControlEvent(event)) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const targetSequenceId = eventSequenceNumber(event);
|
|
593
|
+
const targetCursor = normalizeString(event.cursor);
|
|
594
|
+
if (!targetSequenceId && !targetCursor) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
key: targetSequenceId ? `seq:${targetSequenceId}` : `cursor:${targetCursor}`,
|
|
599
|
+
targetSequenceId: targetSequenceId || null,
|
|
600
|
+
targetCursor,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function recordSessionReadViews(sessionId, events = [], {
|
|
605
|
+
targetPath,
|
|
606
|
+
agentId,
|
|
607
|
+
enabled = false,
|
|
608
|
+
maxTargets = 50,
|
|
609
|
+
} = {}) {
|
|
610
|
+
const maxAutoViewTargets = Math.max(0, Math.min(200, Number(maxTargets) || 0));
|
|
611
|
+
const summary = {
|
|
612
|
+
enabled: Boolean(enabled),
|
|
613
|
+
agentId: normalizeString(agentId) || "cli-user",
|
|
614
|
+
targetCount: 0,
|
|
615
|
+
attempted: 0,
|
|
616
|
+
recorded: 0,
|
|
617
|
+
duplicates: 0,
|
|
618
|
+
failed: 0,
|
|
619
|
+
skipped: 0,
|
|
620
|
+
reason: "",
|
|
621
|
+
};
|
|
622
|
+
if (!summary.enabled) {
|
|
623
|
+
summary.reason = "disabled";
|
|
624
|
+
return summary;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const seenTargets = new Set();
|
|
628
|
+
const targets = [];
|
|
629
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
630
|
+
const target = sessionReadViewTarget(event);
|
|
631
|
+
if (!target || seenTargets.has(target.key)) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
seenTargets.add(target.key);
|
|
635
|
+
targets.push(target);
|
|
636
|
+
}
|
|
637
|
+
summary.targetCount = targets.length;
|
|
638
|
+
|
|
639
|
+
const writeTargets = maxAutoViewTargets > 0 ? targets.slice(-maxAutoViewTargets) : [];
|
|
640
|
+
summary.skipped = Math.max(0, targets.length - writeTargets.length);
|
|
641
|
+
if (summary.skipped > 0) {
|
|
642
|
+
summary.reason = "target_cap_reached";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
for (const target of writeTargets) {
|
|
646
|
+
const result = await createSessionMessageAction(sessionId, {
|
|
647
|
+
actionType: "view",
|
|
648
|
+
targetPath,
|
|
649
|
+
targetSequenceId: target.targetSequenceId,
|
|
650
|
+
targetCursor: target.targetCursor,
|
|
651
|
+
metadata: {
|
|
652
|
+
source: "cli_read",
|
|
653
|
+
agentId: summary.agentId,
|
|
654
|
+
},
|
|
655
|
+
idempotencyKey: defaultActionIdempotencyKey({
|
|
656
|
+
actionType: "view",
|
|
657
|
+
targetSequenceId: target.targetSequenceId,
|
|
658
|
+
targetCursor: target.targetCursor,
|
|
659
|
+
agentId: summary.agentId,
|
|
660
|
+
}),
|
|
661
|
+
timeoutMs: 15_000,
|
|
662
|
+
});
|
|
663
|
+
summary.attempted += 1;
|
|
664
|
+
if (result?.ok && result.action) {
|
|
665
|
+
summary.recorded += 1;
|
|
666
|
+
if (result.duplicate) {
|
|
667
|
+
summary.duplicates += 1;
|
|
668
|
+
}
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
summary.failed += 1;
|
|
672
|
+
summary.reason = normalizeString(result?.reason) || "view_write_failed";
|
|
673
|
+
summary.skipped = Math.max(0, targets.length - summary.attempted);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return summary;
|
|
678
|
+
}
|
|
679
|
+
|
|
332
680
|
function compareIsoDesc(left = "", right = "") {
|
|
333
681
|
return normalizeString(right).localeCompare(normalizeString(left));
|
|
334
682
|
}
|
|
@@ -402,6 +750,10 @@ function parsePositiveInteger(rawValue, field, fallbackValue) {
|
|
|
402
750
|
return Math.floor(normalized);
|
|
403
751
|
}
|
|
404
752
|
|
|
753
|
+
function parsePositiveMillisecondsFromSeconds(rawValue, field, fallbackSeconds) {
|
|
754
|
+
return parsePositiveInteger(rawValue, field, fallbackSeconds) * 1000;
|
|
755
|
+
}
|
|
756
|
+
|
|
405
757
|
function normalizeComparablePath(value) {
|
|
406
758
|
return String(value || "")
|
|
407
759
|
.trim()
|
|
@@ -961,6 +1313,140 @@ function normalizeAgentId(value, fallbackValue = "cli-user") {
|
|
|
961
1313
|
return normalized || fallbackValue;
|
|
962
1314
|
}
|
|
963
1315
|
|
|
1316
|
+
function canPublishListenerPresence(agentId) {
|
|
1317
|
+
const normalized = normalizeAgentId(agentId, "");
|
|
1318
|
+
if (!normalized) return false;
|
|
1319
|
+
if (["cli-user", "unknown", "human", "user", "operator"].includes(normalized)) return false;
|
|
1320
|
+
return !(
|
|
1321
|
+
normalized.startsWith("human-") ||
|
|
1322
|
+
normalized.startsWith("user-") ||
|
|
1323
|
+
normalized.startsWith("guest-")
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function listenerLifecycleEventName(type = "") {
|
|
1328
|
+
const normalized = normalizeString(type).toLowerCase();
|
|
1329
|
+
if (normalized === "started") return "session_listener_started";
|
|
1330
|
+
if (normalized === "stopped") return "session_listener_stopped";
|
|
1331
|
+
return "session_listener_heartbeat";
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function compactPayload(record = {}) {
|
|
1335
|
+
return Object.fromEntries(
|
|
1336
|
+
Object.entries(record).filter(([, value]) => value !== undefined)
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
async function publishListenerPresenceEvent({
|
|
1341
|
+
sessionId,
|
|
1342
|
+
targetPath,
|
|
1343
|
+
agentId,
|
|
1344
|
+
agentModel = "cli",
|
|
1345
|
+
displayName = "",
|
|
1346
|
+
listenerId,
|
|
1347
|
+
lifecycle = {},
|
|
1348
|
+
} = {}) {
|
|
1349
|
+
const normalizedType = normalizeString(lifecycle.type) || "heartbeat";
|
|
1350
|
+
const eventName = listenerLifecycleEventName(normalizedType);
|
|
1351
|
+
const event = createAgentEvent({
|
|
1352
|
+
event: eventName,
|
|
1353
|
+
sessionId,
|
|
1354
|
+
agent: {
|
|
1355
|
+
id: agentId,
|
|
1356
|
+
model: normalizeString(agentModel) || "cli",
|
|
1357
|
+
role: "listener",
|
|
1358
|
+
displayName: normalizeString(displayName) || agentId,
|
|
1359
|
+
clientKind: "cli",
|
|
1360
|
+
},
|
|
1361
|
+
eventId: `session-listener-${listenerId}-${normalizedType}-${lifecycle.pollCount ?? 0}`,
|
|
1362
|
+
idempotencyToken: `session-listener:${listenerId}:${normalizedType}:${lifecycle.pollCount ?? 0}`,
|
|
1363
|
+
payload: compactPayload({
|
|
1364
|
+
source: "session_listen",
|
|
1365
|
+
listenerId,
|
|
1366
|
+
lifecycle: normalizedType,
|
|
1367
|
+
state: normalizeString(lifecycle.state) || normalizedType,
|
|
1368
|
+
active: lifecycle.active,
|
|
1369
|
+
cursor: lifecycle.cursor || null,
|
|
1370
|
+
cursorSuffix: lifecycle.cursorSuffix,
|
|
1371
|
+
cursorSource: lifecycle.cursorSource,
|
|
1372
|
+
pollCount: lifecycle.pollCount,
|
|
1373
|
+
matched: lifecycle.matched,
|
|
1374
|
+
emitted: lifecycle.emitted,
|
|
1375
|
+
persistedCursor: lifecycle.persistedCursor,
|
|
1376
|
+
idleIntervalSeconds: lifecycle.idleIntervalSeconds,
|
|
1377
|
+
activeIntervalSeconds: lifecycle.activeIntervalSeconds,
|
|
1378
|
+
activeWindowSeconds: lifecycle.activeWindowSeconds,
|
|
1379
|
+
lastHumanActivityAt: lifecycle.lastHumanActivityAt,
|
|
1380
|
+
lastSleepMs: lifecycle.lastSleepMs,
|
|
1381
|
+
nextPollMs: lifecycle.nextPollMs,
|
|
1382
|
+
reason: lifecycle.reason || null,
|
|
1383
|
+
startedAt: lifecycle.startedAt,
|
|
1384
|
+
stoppedAt: lifecycle.stoppedAt,
|
|
1385
|
+
aborted: lifecycle.aborted,
|
|
1386
|
+
stopping: lifecycle.stopping,
|
|
1387
|
+
}),
|
|
1388
|
+
});
|
|
1389
|
+
return syncSessionEventToApi(sessionId, event, { targetPath });
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function formatListenerCatchupNotice(catchup = {}) {
|
|
1393
|
+
const eventCount = Number(catchup.eventCount || 0);
|
|
1394
|
+
const matchingEventCount = Number(catchup.matchingEventCount || 0);
|
|
1395
|
+
const range = catchup.oldestEventAt && catchup.newestEventAt
|
|
1396
|
+
? ` (${catchup.oldestEventAt} -> ${catchup.newestEventAt})`
|
|
1397
|
+
: "";
|
|
1398
|
+
return [
|
|
1399
|
+
`Listener catch-up from stored cursor ${catchup.cursor || "<none>"}:`,
|
|
1400
|
+
`${eventCount} event${eventCount === 1 ? "" : "s"} in this page`,
|
|
1401
|
+
`${matchingEventCount} addressed/broadcast to this agent${range}.`,
|
|
1402
|
+
"Use --from-now only when you intentionally want to skip old backlog.",
|
|
1403
|
+
].join(" ");
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function buildListenerCatchupEvent({
|
|
1407
|
+
sessionId,
|
|
1408
|
+
agentId,
|
|
1409
|
+
agentModel = "cli",
|
|
1410
|
+
displayName = "",
|
|
1411
|
+
listenerId,
|
|
1412
|
+
catchup = {},
|
|
1413
|
+
} = {}) {
|
|
1414
|
+
const message = formatListenerCatchupNotice(catchup);
|
|
1415
|
+
const pollCount = Number(catchup.pollCount || 0);
|
|
1416
|
+
return createAgentEvent({
|
|
1417
|
+
event: "session_listen_catchup",
|
|
1418
|
+
sessionId,
|
|
1419
|
+
agent: {
|
|
1420
|
+
id: agentId,
|
|
1421
|
+
model: normalizeString(agentModel) || "cli",
|
|
1422
|
+
role: "listener",
|
|
1423
|
+
displayName: normalizeString(displayName) || agentId,
|
|
1424
|
+
clientKind: "cli",
|
|
1425
|
+
},
|
|
1426
|
+
eventId: `session-listener-${listenerId}-catchup-${pollCount}`,
|
|
1427
|
+
idempotencyToken: `session-listener:${listenerId}:catchup:${pollCount}`,
|
|
1428
|
+
payload: compactPayload({
|
|
1429
|
+
source: "session_listen",
|
|
1430
|
+
listenerId,
|
|
1431
|
+
lifecycle: "catchup",
|
|
1432
|
+
state: "catching_up",
|
|
1433
|
+
message,
|
|
1434
|
+
cursor: catchup.cursor || null,
|
|
1435
|
+
candidateCursor: catchup.candidateCursor || null,
|
|
1436
|
+
cursorSuffix: catchup.cursorSuffix,
|
|
1437
|
+
cursorSource: catchup.cursorSource,
|
|
1438
|
+
pollCount,
|
|
1439
|
+
eventCount: Number(catchup.eventCount || 0),
|
|
1440
|
+
matchingEventCount: Number(catchup.matchingEventCount || 0),
|
|
1441
|
+
preStartEventCount: Number(catchup.preStartEventCount || 0),
|
|
1442
|
+
limit: Number(catchup.limit || 0) || undefined,
|
|
1443
|
+
replay: Boolean(catchup.replay),
|
|
1444
|
+
oldestEventAt: catchup.oldestEventAt || null,
|
|
1445
|
+
newestEventAt: catchup.newestEventAt || null,
|
|
1446
|
+
}),
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
|
|
964
1450
|
// Preserve the literal default identity for `session say`. This command is
|
|
965
1451
|
// often used by agents as a low-friction relay; silently rewriting the default
|
|
966
1452
|
// `cli-user` to the authenticated human makes a forgotten --agent flag look
|
|
@@ -973,6 +1459,121 @@ async function defaultAgentId(value, _targetPath) {
|
|
|
973
1459
|
return resolveSessionSayAgentId(value);
|
|
974
1460
|
}
|
|
975
1461
|
|
|
1462
|
+
function formatAgentIdList(agentIds = []) {
|
|
1463
|
+
const normalized = agentIds.map((agentId) => normalizeString(agentId)).filter(Boolean);
|
|
1464
|
+
if (!normalized.length) return "";
|
|
1465
|
+
if (normalized.length <= 3) return normalized.join(", ");
|
|
1466
|
+
return `${normalized.slice(0, 3).join(", ")} +${normalized.length - 3} more`;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
export function sessionSayRegistryRole(value) {
|
|
1470
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
1471
|
+
if (["coder", "reviewer", "tester", "daemon", "observer", "persona"].includes(normalized)) {
|
|
1472
|
+
return normalized;
|
|
1473
|
+
}
|
|
1474
|
+
return "coder";
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
export async function resolveSessionSayIdentity({
|
|
1478
|
+
sessionId,
|
|
1479
|
+
agentId = "",
|
|
1480
|
+
targetPath = process.cwd(),
|
|
1481
|
+
env = process.env,
|
|
1482
|
+
} = {}) {
|
|
1483
|
+
const explicitAgentId = normalizeString(agentId);
|
|
1484
|
+
if (explicitAgentId) {
|
|
1485
|
+
return {
|
|
1486
|
+
agentId: resolveSessionSayAgentId(explicitAgentId),
|
|
1487
|
+
source: "option",
|
|
1488
|
+
identityWarning: "",
|
|
1489
|
+
candidateAgentIds: [],
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const envAgentId = normalizeString(env?.SENTINELAYER_AGENT_ID || env?.SENTI_AGENT_ID);
|
|
1494
|
+
if (envAgentId && canPublishListenerPresence(envAgentId)) {
|
|
1495
|
+
return {
|
|
1496
|
+
agentId: resolveSessionSayAgentId(envAgentId),
|
|
1497
|
+
source: "env",
|
|
1498
|
+
identityWarning: "",
|
|
1499
|
+
candidateAgentIds: [],
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
let candidateAgentIds = [];
|
|
1504
|
+
try {
|
|
1505
|
+
const agents = await listAgents(sessionId, { targetPath, includeInactive: false });
|
|
1506
|
+
candidateAgentIds = agents
|
|
1507
|
+
.map((agent) => normalizeString(agent.agentId))
|
|
1508
|
+
.filter((candidate) => canPublishListenerPresence(candidate));
|
|
1509
|
+
} catch {
|
|
1510
|
+
candidateAgentIds = [];
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (candidateAgentIds.length === 1) {
|
|
1514
|
+
return {
|
|
1515
|
+
agentId: resolveSessionSayAgentId(candidateAgentIds[0]),
|
|
1516
|
+
source: "local-agent",
|
|
1517
|
+
identityWarning: "",
|
|
1518
|
+
candidateAgentIds,
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const warningReason = candidateAgentIds.length > 1
|
|
1523
|
+
? `multiple local joined agents are active (${formatAgentIdList(candidateAgentIds)})`
|
|
1524
|
+
: envAgentId && !canPublishListenerPresence(envAgentId)
|
|
1525
|
+
? `configured SENTINELAYER_AGENT_ID '${envAgentId}' is reserved or human-scoped`
|
|
1526
|
+
: "no agent identity is configured";
|
|
1527
|
+
|
|
1528
|
+
return {
|
|
1529
|
+
agentId: "cli-user",
|
|
1530
|
+
source: "fallback",
|
|
1531
|
+
identityWarning: `session say is sending as cli-user because ${warningReason}; pass --agent <id>, set SENTINELAYER_AGENT_ID, or run session join --agent <id> before posting.`,
|
|
1532
|
+
candidateAgentIds,
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
export function shouldBlockImplicitCliUserSessionSay(identity = {}) {
|
|
1537
|
+
return identity?.source === "fallback" && normalizeString(identity?.agentId) === "cli-user";
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
async function ensureSessionSayAgentRegistered(
|
|
1541
|
+
sessionId,
|
|
1542
|
+
agent = {},
|
|
1543
|
+
{ targetPath = process.cwd() } = {},
|
|
1544
|
+
) {
|
|
1545
|
+
const agentId = normalizeString(agent.id);
|
|
1546
|
+
if (!canPublishListenerPresence(agentId)) {
|
|
1547
|
+
return { persisted: false, reason: "placeholder_agent" };
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
try {
|
|
1551
|
+
const activeAgents = await listAgents(sessionId, { targetPath, includeInactive: false });
|
|
1552
|
+
if (
|
|
1553
|
+
activeAgents.some(
|
|
1554
|
+
(existing) => normalizeString(existing.agentId).toLowerCase() === agentId.toLowerCase(),
|
|
1555
|
+
)
|
|
1556
|
+
) {
|
|
1557
|
+
return { persisted: false, reason: "already_registered" };
|
|
1558
|
+
}
|
|
1559
|
+
} catch {
|
|
1560
|
+
// If the local registry is unreadable, let rememberAgentIdentity surface the
|
|
1561
|
+
// filesystem problem with its normal error message.
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const registered = await rememberAgentIdentity(sessionId, {
|
|
1565
|
+
agentId,
|
|
1566
|
+
model: normalizeString(agent.model) || "cli",
|
|
1567
|
+
role: sessionSayRegistryRole(agent.role),
|
|
1568
|
+
targetPath,
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
return {
|
|
1572
|
+
persisted: true,
|
|
1573
|
+
agentId: registered.agentId,
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
|
|
976
1577
|
async function resolveSessionAgentEnvelope(
|
|
977
1578
|
sessionId,
|
|
978
1579
|
agentId,
|
|
@@ -1062,6 +1663,13 @@ function formatEventLine(event = {}) {
|
|
|
1062
1663
|
return `${ts} ${agentId} ${type}`;
|
|
1063
1664
|
}
|
|
1064
1665
|
|
|
1666
|
+
function isSessionControlEvent(event = {}) {
|
|
1667
|
+
const payload = event?.payload && typeof event.payload === "object" ? event.payload : {};
|
|
1668
|
+
const type = normalizeString(event.event || event.type).toLowerCase();
|
|
1669
|
+
if (type.startsWith("session_listener_")) return true;
|
|
1670
|
+
return normalizeString(payload.source).toLowerCase() === "session_listen";
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1065
1673
|
function checkpointSequenceRange(checkpoint = {}) {
|
|
1066
1674
|
const start = Number(checkpoint.startSequence || 0);
|
|
1067
1675
|
const end = Number(checkpoint.endSequence || 0);
|
|
@@ -1926,6 +2534,7 @@ export function registerSessionCommand(program) {
|
|
|
1926
2534
|
awaitRemoteSync: Boolean(explicitAgent),
|
|
1927
2535
|
});
|
|
1928
2536
|
const agentJoinRelayed =
|
|
2537
|
+
joined.emittedJoinEvent !== false &&
|
|
1929
2538
|
Boolean(explicitAgent) &&
|
|
1930
2539
|
Boolean(resolvedAgentId) &&
|
|
1931
2540
|
resolvedAgentId !== "cli-user" &&
|
|
@@ -1980,7 +2589,10 @@ export function registerSessionCommand(program) {
|
|
|
1980
2589
|
session
|
|
1981
2590
|
.command("say <sessionId> <message>")
|
|
1982
2591
|
.description("Send a message to the session")
|
|
1983
|
-
.option(
|
|
2592
|
+
.option(
|
|
2593
|
+
"--agent <id>",
|
|
2594
|
+
"Agent id to emit from; defaults to SENTINELAYER_AGENT_ID, then the sole local joined agent, then cli-user",
|
|
2595
|
+
)
|
|
1984
2596
|
.option(
|
|
1985
2597
|
"--model <model>",
|
|
1986
2598
|
"Agent model/provider hint; defaults to local joined agent metadata or SENTINELAYER_AGENT_MODEL",
|
|
@@ -1999,6 +2611,8 @@ export function registerSessionCommand(program) {
|
|
|
1999
2611
|
.option("--to <agent>", "Direct the message to a specific agent id")
|
|
2000
2612
|
.option("--reply-to <sequence>", "Mark this message as a reply to a target sequence id")
|
|
2001
2613
|
.option("--reply-cursor <cursor>", "Mark this message as a reply to a target event cursor")
|
|
2614
|
+
.option("--force-cli-user", "Allow fallback sends as cli-user when no agent identity can be resolved")
|
|
2615
|
+
.option("--local-only", "Append only to the local session cache without remote send confirmation")
|
|
2002
2616
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2003
2617
|
.option("--json", "Emit machine-readable output")
|
|
2004
2618
|
.action(async (sessionId, message, options, command) => {
|
|
@@ -2011,16 +2625,28 @@ export function registerSessionCommand(program) {
|
|
|
2011
2625
|
throw new Error("message is required.");
|
|
2012
2626
|
}
|
|
2013
2627
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
2014
|
-
const
|
|
2628
|
+
const identity = await resolveSessionSayIdentity({
|
|
2629
|
+
sessionId: normalizedSessionId,
|
|
2630
|
+
agentId: options.agent,
|
|
2631
|
+
targetPath,
|
|
2632
|
+
});
|
|
2633
|
+
const agentId = identity.agentId;
|
|
2634
|
+
if (shouldBlockImplicitCliUserSessionSay(identity) && !options.forceCliUser) {
|
|
2635
|
+
throw new Error(
|
|
2636
|
+
`${identity.identityWarning} Re-run with --force-cli-user only for intentional anonymous/operator relay posts.`,
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2015
2639
|
const localSession = await ensureLocalSessionForRemoteCommand(normalizedSessionId, {
|
|
2016
2640
|
targetPath,
|
|
2017
2641
|
});
|
|
2018
2642
|
const to = normalizeString(options.to);
|
|
2019
2643
|
const replyToSequenceId = parseOptionalPositiveInteger(options.replyTo, "reply-to");
|
|
2020
2644
|
const replyToCursor = normalizeString(options.replyCursor);
|
|
2645
|
+
const clientMessageId = `cli-${randomUUID()}`;
|
|
2021
2646
|
const eventPayload = {
|
|
2022
2647
|
message: normalizedMessage,
|
|
2023
2648
|
channel: "session",
|
|
2649
|
+
clientMessageId,
|
|
2024
2650
|
};
|
|
2025
2651
|
if (to) {
|
|
2026
2652
|
eventPayload.to = to;
|
|
@@ -2031,13 +2657,15 @@ export function registerSessionCommand(program) {
|
|
|
2031
2657
|
if (replyToCursor) {
|
|
2032
2658
|
eventPayload.replyToCursor = replyToCursor;
|
|
2033
2659
|
}
|
|
2034
|
-
const clientMessageId = `cli-${randomUUID()}`;
|
|
2035
2660
|
const agent = await resolveSessionAgentEnvelope(normalizedSessionId, agentId, {
|
|
2036
2661
|
targetPath,
|
|
2037
2662
|
model: options.model,
|
|
2038
2663
|
role: options.role,
|
|
2039
2664
|
displayName: options.displayName,
|
|
2040
2665
|
});
|
|
2666
|
+
const agentRegistration = await ensureSessionSayAgentRegistered(normalizedSessionId, agent, {
|
|
2667
|
+
targetPath,
|
|
2668
|
+
});
|
|
2041
2669
|
const event = createAgentEvent({
|
|
2042
2670
|
event: "session_message",
|
|
2043
2671
|
agent,
|
|
@@ -2047,7 +2675,22 @@ export function registerSessionCommand(program) {
|
|
|
2047
2675
|
event.eventId = clientMessageId;
|
|
2048
2676
|
event.idempotencyToken = clientMessageId;
|
|
2049
2677
|
let remoteSync = null;
|
|
2050
|
-
|
|
2678
|
+
let remoteConfirmation = null;
|
|
2679
|
+
let remoteConfirmationAnchor = null;
|
|
2680
|
+
let localOnly = Boolean(options.localOnly) || remoteSessionLookupDisabled();
|
|
2681
|
+
if (!localOnly) {
|
|
2682
|
+
remoteConfirmationAnchor = await readSessionConfirmationAnchor(normalizedSessionId, { targetPath });
|
|
2683
|
+
if (!remoteConfirmationAnchor?.ok) {
|
|
2684
|
+
if (!localSession.materialized && isLocalOnlySessionSayReason(remoteConfirmationAnchor?.reason)) {
|
|
2685
|
+
localOnly = true;
|
|
2686
|
+
} else {
|
|
2687
|
+
throw new Error(
|
|
2688
|
+
`Remote send confirmation anchor failed (${remoteConfirmationAnchor?.reason || "unknown"}); local cache was not updated. Use --local-only only when you intentionally want an offline local note.`,
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (!localOnly) {
|
|
2051
2694
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
2052
2695
|
remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
|
|
2053
2696
|
targetPath,
|
|
@@ -2056,13 +2699,23 @@ export function registerSessionCommand(program) {
|
|
|
2056
2699
|
}
|
|
2057
2700
|
if (!remoteSync?.synced) {
|
|
2058
2701
|
throw new Error(
|
|
2059
|
-
`Remote send failed (${remoteSync?.reason || "unknown"}); local cache was not updated.`,
|
|
2702
|
+
`Remote send failed (${remoteSync?.reason || "unknown"}); local cache was not updated. Use --local-only only when you intentionally want an offline local note.`,
|
|
2060
2703
|
);
|
|
2704
|
+
} else {
|
|
2705
|
+
remoteConfirmation = await confirmSessionEventVisible(normalizedSessionId, clientMessageId, {
|
|
2706
|
+
targetPath,
|
|
2707
|
+
anchorCursor: remoteConfirmationAnchor?.cursor,
|
|
2708
|
+
});
|
|
2709
|
+
if (!remoteConfirmation?.confirmed) {
|
|
2710
|
+
throw new Error(
|
|
2711
|
+
`Remote send was accepted but not visible in canonical session events (${remoteConfirmation?.reason || "not_visible"}); local cache was not updated.`,
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2061
2714
|
}
|
|
2062
2715
|
}
|
|
2063
2716
|
const persisted = await appendToStream(normalizedSessionId, event, {
|
|
2064
2717
|
targetPath,
|
|
2065
|
-
syncRemote:
|
|
2718
|
+
syncRemote: false,
|
|
2066
2719
|
});
|
|
2067
2720
|
const payload = {
|
|
2068
2721
|
command: "session say",
|
|
@@ -2072,12 +2725,21 @@ export function registerSessionCommand(program) {
|
|
|
2072
2725
|
event: persisted,
|
|
2073
2726
|
materializedLocalSession: localSession.materialized,
|
|
2074
2727
|
refreshedLocalSession: Boolean(localSession.refreshed),
|
|
2728
|
+
identitySource: identity.source,
|
|
2729
|
+
identityWarning: identity.identityWarning || undefined,
|
|
2730
|
+
agentRegistration,
|
|
2075
2731
|
remoteSync: remoteSync || undefined,
|
|
2732
|
+
remoteConfirmationAnchor: remoteConfirmationAnchor || undefined,
|
|
2733
|
+
remoteConfirmation: remoteConfirmation || undefined,
|
|
2734
|
+
localOnly,
|
|
2076
2735
|
};
|
|
2077
2736
|
if (shouldEmitJson(options, command)) {
|
|
2078
2737
|
console.log(JSON.stringify(payload, null, 2));
|
|
2079
2738
|
return;
|
|
2080
2739
|
}
|
|
2740
|
+
if (identity.identityWarning) {
|
|
2741
|
+
console.error(pc.yellow(`Identity warning: ${identity.identityWarning}`));
|
|
2742
|
+
}
|
|
2081
2743
|
console.log(formatEventLine(persisted));
|
|
2082
2744
|
});
|
|
2083
2745
|
|
|
@@ -2109,11 +2771,13 @@ export function registerSessionCommand(program) {
|
|
|
2109
2771
|
targetPath,
|
|
2110
2772
|
});
|
|
2111
2773
|
const to = normalizeString(options.to);
|
|
2774
|
+
const clientMessageId = `cli-agent-${randomUUID()}`;
|
|
2112
2775
|
const eventPayload = {
|
|
2113
2776
|
message: normalizedMessage,
|
|
2114
2777
|
channel: "session",
|
|
2115
2778
|
source: "agent",
|
|
2116
2779
|
clientKind: "cli",
|
|
2780
|
+
clientMessageId,
|
|
2117
2781
|
};
|
|
2118
2782
|
if (to) {
|
|
2119
2783
|
eventPayload.to = to;
|
|
@@ -2125,7 +2789,6 @@ export function registerSessionCommand(program) {
|
|
|
2125
2789
|
role: normalizeString(options.role) || "coder",
|
|
2126
2790
|
clientKind: "cli",
|
|
2127
2791
|
};
|
|
2128
|
-
const clientMessageId = `cli-agent-${randomUUID()}`;
|
|
2129
2792
|
const event = createAgentEvent({
|
|
2130
2793
|
event: "session_message",
|
|
2131
2794
|
agent,
|
|
@@ -2135,6 +2798,19 @@ export function registerSessionCommand(program) {
|
|
|
2135
2798
|
event.eventId = clientMessageId;
|
|
2136
2799
|
event.idempotencyToken = clientMessageId;
|
|
2137
2800
|
|
|
2801
|
+
const remoteConfirmationAnchor = await readSessionConfirmationAnchor(normalizedSessionId, {
|
|
2802
|
+
targetPath,
|
|
2803
|
+
});
|
|
2804
|
+
if (!remoteConfirmationAnchor?.ok) {
|
|
2805
|
+
if (remoteConfirmationAnchor?.reason === "api_403") {
|
|
2806
|
+
throw new Error(
|
|
2807
|
+
`Agent post failed (api_403). Ensure this user has an active grant for '${agentId}'.`,
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
throw new Error(
|
|
2811
|
+
`Agent post confirmation anchor failed (${remoteConfirmationAnchor?.reason || "unknown"}); local cache was not updated.`,
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2138
2814
|
const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
|
|
2139
2815
|
targetPath,
|
|
2140
2816
|
});
|
|
@@ -2143,6 +2819,15 @@ export function registerSessionCommand(program) {
|
|
|
2143
2819
|
`Agent post failed (${remoteSync?.reason || "unknown"}). Ensure this user has an active grant for '${agentId}'.`,
|
|
2144
2820
|
);
|
|
2145
2821
|
}
|
|
2822
|
+
const remoteConfirmation = await confirmSessionEventVisible(normalizedSessionId, clientMessageId, {
|
|
2823
|
+
targetPath,
|
|
2824
|
+
anchorCursor: remoteConfirmationAnchor?.cursor,
|
|
2825
|
+
});
|
|
2826
|
+
if (!remoteConfirmation?.confirmed) {
|
|
2827
|
+
throw new Error(
|
|
2828
|
+
`Agent post was accepted but not visible in canonical session events (${remoteConfirmation?.reason || "not_visible"}); local cache was not updated.`,
|
|
2829
|
+
);
|
|
2830
|
+
}
|
|
2146
2831
|
|
|
2147
2832
|
const persisted = await appendToStream(normalizedSessionId, event, {
|
|
2148
2833
|
targetPath,
|
|
@@ -2157,6 +2842,8 @@ export function registerSessionCommand(program) {
|
|
|
2157
2842
|
materializedLocalSession: localSession.materialized,
|
|
2158
2843
|
refreshedLocalSession: Boolean(localSession.refreshed),
|
|
2159
2844
|
remoteSync,
|
|
2845
|
+
remoteConfirmationAnchor,
|
|
2846
|
+
remoteConfirmation,
|
|
2160
2847
|
};
|
|
2161
2848
|
if (shouldEmitJson(options, command)) {
|
|
2162
2849
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -2242,7 +2929,12 @@ export function registerSessionCommand(program) {
|
|
|
2242
2929
|
console.log(JSON.stringify(payload, null, 2));
|
|
2243
2930
|
return payload;
|
|
2244
2931
|
}
|
|
2245
|
-
|
|
2932
|
+
if (localAppend.event || actionEvent) {
|
|
2933
|
+
console.log(formatEventLine(localAppend.event || actionEvent));
|
|
2934
|
+
} else {
|
|
2935
|
+
const targetLabel = targetSequenceId ? `#${targetSequenceId}` : targetCursor || targetActionId || "target";
|
|
2936
|
+
console.log(pc.green(`Recorded ${normalizedActionType} on ${targetLabel}.`));
|
|
2937
|
+
}
|
|
2246
2938
|
return payload;
|
|
2247
2939
|
}
|
|
2248
2940
|
|
|
@@ -2352,7 +3044,7 @@ export function registerSessionCommand(program) {
|
|
|
2352
3044
|
|
|
2353
3045
|
session
|
|
2354
3046
|
.command("view <sessionId> <targetSequenceId>")
|
|
2355
|
-
.description("
|
|
3047
|
+
.description("Manually backfill a read receipt for a target session event")
|
|
2356
3048
|
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
2357
3049
|
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
2358
3050
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
@@ -2388,10 +3080,27 @@ export function registerSessionCommand(program) {
|
|
|
2388
3080
|
"Seconds after a human message to keep the active interval (default 300)",
|
|
2389
3081
|
"300",
|
|
2390
3082
|
)
|
|
3083
|
+
.option(
|
|
3084
|
+
"--presence-interval <seconds>",
|
|
3085
|
+
"Minimum seconds between remote listener heartbeat events (default 60)",
|
|
3086
|
+
"60",
|
|
3087
|
+
)
|
|
3088
|
+
.option(
|
|
3089
|
+
"--no-presence",
|
|
3090
|
+
"Do not publish durable listener lifecycle/heartbeat events",
|
|
3091
|
+
)
|
|
3092
|
+
.option(
|
|
3093
|
+
"--model <model>",
|
|
3094
|
+
"Model/provider label to publish with listener presence",
|
|
3095
|
+
process.env.SENTINELAYER_AGENT_MODEL || process.env.SENTINELAYER_MODEL || "cli",
|
|
3096
|
+
)
|
|
3097
|
+
.option("--display-name <name>", "Human-readable listener name for presence")
|
|
2391
3098
|
.option("--emit <format>", "Output format: ndjson or text", "ndjson")
|
|
3099
|
+
.option("--transport <mode>", "Listen transport: auto, stream, or poll (default auto)", "auto")
|
|
2392
3100
|
.option("--limit <n>", "Maximum events to request per poll (default 200)", "200")
|
|
2393
3101
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2394
3102
|
.option("--since <cursor>", "Override the persisted listen cursor")
|
|
3103
|
+
.option("--from-now", "Advance the listen cursor to the latest durable event before polling")
|
|
2395
3104
|
.option("--replay", "Emit matching historical events on the first poll")
|
|
2396
3105
|
.option("--max-polls <n>", "Stop after N poll cycles (useful for tests and smoke checks)")
|
|
2397
3106
|
.action(async (options) => {
|
|
@@ -2405,26 +3114,64 @@ export function registerSessionCommand(program) {
|
|
|
2405
3114
|
5,
|
|
2406
3115
|
);
|
|
2407
3116
|
const activeWindowSeconds = parsePositiveInteger(options.activeWindow, "active-window", 300);
|
|
3117
|
+
const presenceIntervalSeconds = parsePositiveInteger(
|
|
3118
|
+
options.presenceInterval,
|
|
3119
|
+
"presence-interval",
|
|
3120
|
+
60,
|
|
3121
|
+
);
|
|
3122
|
+
const agentModel = normalizeString(options.model) || "cli";
|
|
3123
|
+
const displayName = normalizeString(options.displayName) || agentId;
|
|
2408
3124
|
const limit = parsePositiveInteger(options.limit, "limit", 200);
|
|
2409
3125
|
const emitFormat = normalizeString(options.emit).toLowerCase() || "ndjson";
|
|
2410
3126
|
if (!["ndjson", "text"].includes(emitFormat)) {
|
|
2411
3127
|
throw new Error("--emit must be one of: ndjson, text.");
|
|
2412
3128
|
}
|
|
3129
|
+
const requestedTransport = normalizeString(options.transport).toLowerCase() || "auto";
|
|
3130
|
+
if (!["auto", "stream", "poll"].includes(requestedTransport)) {
|
|
3131
|
+
throw new Error("--transport must be one of: auto, stream, poll.");
|
|
3132
|
+
}
|
|
2413
3133
|
const maxPolls =
|
|
2414
3134
|
options.maxPolls === undefined
|
|
2415
3135
|
? null
|
|
2416
3136
|
: parsePositiveInteger(options.maxPolls, "max-polls", 1);
|
|
3137
|
+
const listenTransport = requestedTransport === "auto" && maxPolls !== null
|
|
3138
|
+
? "poll"
|
|
3139
|
+
: requestedTransport;
|
|
2417
3140
|
const since = options.since === undefined ? undefined : String(options.since);
|
|
3141
|
+
if (options.fromNow && options.since !== undefined) {
|
|
3142
|
+
throw new Error("Use either --from-now or --since, not both.");
|
|
3143
|
+
}
|
|
2418
3144
|
const ac = new AbortController();
|
|
2419
3145
|
const onSigint = () => ac.abort();
|
|
2420
3146
|
process.on("SIGINT", onSigint);
|
|
3147
|
+
const listenerId = `listener-${agentId}-${randomUUID()}`;
|
|
3148
|
+
const durablePresenceEnabled = options.presence !== false;
|
|
3149
|
+
const publishPresence = durablePresenceEnabled && canPublishListenerPresence(agentId);
|
|
3150
|
+
const presenceIntervalMs = Math.max(1, presenceIntervalSeconds) * 1000;
|
|
3151
|
+
let lastPresenceHeartbeatMs = 0;
|
|
2421
3152
|
|
|
2422
3153
|
if (emitFormat === "text") {
|
|
2423
3154
|
console.log(
|
|
2424
3155
|
pc.gray(
|
|
2425
|
-
`Listening to session ${normalizedSessionId} as ${agentId}; idle=${intervalSeconds}s active=${activeIntervalSeconds}s/${activeWindowSeconds}s. Press Ctrl+C to stop.`,
|
|
3156
|
+
`Listening to session ${normalizedSessionId} as ${agentId}; transport=${listenTransport} idle=${intervalSeconds}s active=${activeIntervalSeconds}s/${activeWindowSeconds}s. Press Ctrl+C to stop.`,
|
|
2426
3157
|
),
|
|
2427
3158
|
);
|
|
3159
|
+
if (!durablePresenceEnabled) {
|
|
3160
|
+
console.log(
|
|
3161
|
+
pc.gray(
|
|
3162
|
+
"Remote listener presence is disabled; no durable lifecycle events will be written.",
|
|
3163
|
+
),
|
|
3164
|
+
);
|
|
3165
|
+
} else if (!publishPresence) {
|
|
3166
|
+
console.log(
|
|
3167
|
+
pc.gray(
|
|
3168
|
+
"Listener presence is local-only for placeholder, human, and guest agent ids.",
|
|
3169
|
+
),
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
if (options.fromNow) {
|
|
3173
|
+
console.log(pc.gray("Priming listener from the latest durable event; old backlog will be skipped."));
|
|
3174
|
+
}
|
|
2428
3175
|
}
|
|
2429
3176
|
|
|
2430
3177
|
try {
|
|
@@ -2438,8 +3185,29 @@ export function registerSessionCommand(program) {
|
|
|
2438
3185
|
limit,
|
|
2439
3186
|
since,
|
|
2440
3187
|
replay: Boolean(options.replay),
|
|
3188
|
+
fromNow: Boolean(options.fromNow),
|
|
3189
|
+
persistStartCursor: Boolean(options.fromNow),
|
|
3190
|
+
transport: listenTransport,
|
|
2441
3191
|
maxPolls,
|
|
2442
3192
|
signal: ac.signal,
|
|
3193
|
+
onCatchup: async (catchup) => {
|
|
3194
|
+
if (emitFormat === "ndjson") {
|
|
3195
|
+
console.log(
|
|
3196
|
+
JSON.stringify(
|
|
3197
|
+
buildListenerCatchupEvent({
|
|
3198
|
+
sessionId: normalizedSessionId,
|
|
3199
|
+
agentId,
|
|
3200
|
+
agentModel,
|
|
3201
|
+
displayName,
|
|
3202
|
+
listenerId,
|
|
3203
|
+
catchup,
|
|
3204
|
+
}),
|
|
3205
|
+
),
|
|
3206
|
+
);
|
|
3207
|
+
} else {
|
|
3208
|
+
console.log(pc.yellow(formatListenerCatchupNotice(catchup)));
|
|
3209
|
+
}
|
|
3210
|
+
},
|
|
2443
3211
|
onEvent: async (event) => {
|
|
2444
3212
|
if (emitFormat === "ndjson") {
|
|
2445
3213
|
console.log(JSON.stringify(event));
|
|
@@ -2467,12 +3235,394 @@ export function registerSessionCommand(program) {
|
|
|
2467
3235
|
console.log(pc.yellow(`Listen poll skipped (${reason}).`));
|
|
2468
3236
|
}
|
|
2469
3237
|
},
|
|
3238
|
+
onLifecycle: async (lifecycle) => {
|
|
3239
|
+
if (!publishPresence) return;
|
|
3240
|
+
const lifecycleType = normalizeString(lifecycle?.type);
|
|
3241
|
+
if (lifecycleType === "heartbeat") {
|
|
3242
|
+
const nowMs = Date.now();
|
|
3243
|
+
if (lastPresenceHeartbeatMs > 0 && nowMs - lastPresenceHeartbeatMs < presenceIntervalMs) {
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
lastPresenceHeartbeatMs = nowMs;
|
|
3247
|
+
}
|
|
3248
|
+
await publishListenerPresenceEvent({
|
|
3249
|
+
sessionId: normalizedSessionId,
|
|
3250
|
+
targetPath,
|
|
3251
|
+
agentId,
|
|
3252
|
+
agentModel,
|
|
3253
|
+
displayName,
|
|
3254
|
+
listenerId,
|
|
3255
|
+
lifecycle,
|
|
3256
|
+
});
|
|
3257
|
+
},
|
|
2470
3258
|
});
|
|
2471
3259
|
} finally {
|
|
2472
3260
|
process.removeListener("SIGINT", onSigint);
|
|
2473
3261
|
}
|
|
2474
3262
|
});
|
|
2475
3263
|
|
|
3264
|
+
session
|
|
3265
|
+
.command("daemon [sessionId]")
|
|
3266
|
+
.description("Run the Senti session daemon: hydrate events, emit recaps, and generate checkpoints")
|
|
3267
|
+
.option("--session <id>", "Session id to monitor")
|
|
3268
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
3269
|
+
.option("--tick-interval <seconds>", "Seconds between health ticks (default 30)", "30")
|
|
3270
|
+
.option("--stale-agent-seconds <seconds>", "Seconds before an inactive agent is flagged stale (default 90)", "90")
|
|
3271
|
+
.option("--recap-interval <seconds>", "Seconds between periodic recaps when activity continues (default 300)", "300")
|
|
3272
|
+
.option("--recap-inactivity <seconds>", "Seconds of inactivity before a recap closeout (default 600)", "600")
|
|
3273
|
+
.option("--recap-event-threshold <n>", "Meaningful events required to force a recap before interval (default 5)", "5")
|
|
3274
|
+
.option("--checkpoint-interval <seconds>", "Minimum seconds between checkpoint attempts (default 60)", "60")
|
|
3275
|
+
.option("--checkpoint-min-events <n>", "Minimum events for generated checkpoint windows (default 20)", "20")
|
|
3276
|
+
.option("--checkpoint-max-events <n>", "Maximum events per generated checkpoint window (default 80)", "80")
|
|
3277
|
+
.option("--checkpoint-event-threshold <n>", "Meaningful events required to attempt a checkpoint (default 20)", "20")
|
|
3278
|
+
.option("--checkpoint-idle <seconds>", "Seconds after latest source event before idle checkpoint attempt (default 600)", "600")
|
|
3279
|
+
.option("--no-checkpoints", "Disable durable checkpoint generation")
|
|
3280
|
+
.option("--no-checkpoint-closeout", "Skip the final closeout checkpoint when the daemon stops")
|
|
3281
|
+
.option("--once", "Run one Senti health tick and exit (CI/dogfood smoke)")
|
|
3282
|
+
.option("--json", "Emit machine-readable output")
|
|
3283
|
+
.action(async (sessionId, options, command) => {
|
|
3284
|
+
const emitJson = shouldEmitJson(options, command);
|
|
3285
|
+
const normalizedSessionId = normalizeString(sessionId) || resolveSessionIdOption(options);
|
|
3286
|
+
if (!normalizedSessionId) {
|
|
3287
|
+
throw new Error("session daemon requires a session id (positional or --session).");
|
|
3288
|
+
}
|
|
3289
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
3290
|
+
const tickIntervalMs = parsePositiveMillisecondsFromSeconds(
|
|
3291
|
+
options.tickInterval,
|
|
3292
|
+
"tick-interval",
|
|
3293
|
+
30,
|
|
3294
|
+
);
|
|
3295
|
+
const daemon = await startSenti(normalizedSessionId, {
|
|
3296
|
+
targetPath,
|
|
3297
|
+
autoStart: false,
|
|
3298
|
+
tickIntervalMs,
|
|
3299
|
+
staleAgentSeconds: parsePositiveInteger(options.staleAgentSeconds, "stale-agent-seconds", 90),
|
|
3300
|
+
recapIntervalMs: parsePositiveMillisecondsFromSeconds(options.recapInterval, "recap-interval", 300),
|
|
3301
|
+
recapInactivityMs: parsePositiveMillisecondsFromSeconds(options.recapInactivity, "recap-inactivity", 600),
|
|
3302
|
+
recapActivityThreshold: parsePositiveInteger(options.recapEventThreshold, "recap-event-threshold", 5),
|
|
3303
|
+
checkpointGenerator: options.checkpoints === false ? null : undefined,
|
|
3304
|
+
checkpointIntervalMs: parsePositiveMillisecondsFromSeconds(options.checkpointInterval, "checkpoint-interval", 60),
|
|
3305
|
+
checkpointMinEvents: parsePositiveInteger(options.checkpointMinEvents, "checkpoint-min-events", 20),
|
|
3306
|
+
checkpointMaxEvents: parsePositiveInteger(options.checkpointMaxEvents, "checkpoint-max-events", 80),
|
|
3307
|
+
checkpointEventThreshold: parsePositiveInteger(options.checkpointEventThreshold, "checkpoint-event-threshold", 20),
|
|
3308
|
+
checkpointIdleMs: parsePositiveMillisecondsFromSeconds(options.checkpointIdle, "checkpoint-idle", 600),
|
|
3309
|
+
checkpointCloseoutOnStop: options.once ? false : options.checkpointCloseout !== false,
|
|
3310
|
+
});
|
|
3311
|
+
|
|
3312
|
+
const runTickAndBuildPayload = async () => {
|
|
3313
|
+
const summary = await daemon.runTick(new Date().toISOString());
|
|
3314
|
+
return {
|
|
3315
|
+
command: "session daemon",
|
|
3316
|
+
sessionId: normalizedSessionId,
|
|
3317
|
+
targetPath,
|
|
3318
|
+
once: Boolean(options.once),
|
|
3319
|
+
running: daemon.isRunning(),
|
|
3320
|
+
summary,
|
|
3321
|
+
state: daemon.getState(),
|
|
3322
|
+
};
|
|
3323
|
+
};
|
|
3324
|
+
|
|
3325
|
+
if (options.once) {
|
|
3326
|
+
const payload = await runTickAndBuildPayload();
|
|
3327
|
+
const stopped = await daemon.stop("once_complete");
|
|
3328
|
+
payload.running = false;
|
|
3329
|
+
payload.stopped = {
|
|
3330
|
+
stopped: Boolean(stopped?.stopped),
|
|
3331
|
+
reason: stopped?.reason || "once_complete",
|
|
3332
|
+
checkpointCloseout: stopped?.checkpointCloseout || null,
|
|
3333
|
+
};
|
|
3334
|
+
if (emitJson) {
|
|
3335
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
const checkpoint = payload.summary?.checkpoint || {};
|
|
3339
|
+
const recap = payload.summary?.recap || {};
|
|
3340
|
+
console.log(
|
|
3341
|
+
pc.green(
|
|
3342
|
+
`senti tick: relayed=${Number(payload.summary?.humanMessages?.relayed || 0)} recap=${recap.emitted ? recap.mode || "emitted" : recap.reason || "none"} checkpoint=${checkpoint.created ? checkpoint.checkpointId || "created" : checkpoint.reason || "none"}`,
|
|
3343
|
+
),
|
|
3344
|
+
);
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
const controller = new AbortController();
|
|
3349
|
+
const stop = () => controller.abort();
|
|
3350
|
+
process.once("SIGINT", stop);
|
|
3351
|
+
process.once("SIGTERM", stop);
|
|
3352
|
+
const waitForNextTick = () =>
|
|
3353
|
+
new Promise((resolve) => {
|
|
3354
|
+
let timer = null;
|
|
3355
|
+
const cleanup = () => {
|
|
3356
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
3357
|
+
if (timer) {
|
|
3358
|
+
clearTimeout(timer);
|
|
3359
|
+
timer = null;
|
|
3360
|
+
}
|
|
3361
|
+
};
|
|
3362
|
+
const finish = () => {
|
|
3363
|
+
cleanup();
|
|
3364
|
+
resolve();
|
|
3365
|
+
};
|
|
3366
|
+
const onAbort = () => {
|
|
3367
|
+
finish();
|
|
3368
|
+
};
|
|
3369
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
3370
|
+
timer = setTimeout(finish, tickIntervalMs);
|
|
3371
|
+
});
|
|
3372
|
+
|
|
3373
|
+
try {
|
|
3374
|
+
if (!emitJson) {
|
|
3375
|
+
console.log(
|
|
3376
|
+
pc.green(
|
|
3377
|
+
`senti daemon: monitoring session ${normalizedSessionId}; tick=${Math.round(tickIntervalMs / 1000)}s. Ctrl-C to stop.`,
|
|
3378
|
+
),
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
let payload = await runTickAndBuildPayload();
|
|
3382
|
+
if (emitJson) {
|
|
3383
|
+
console.log(JSON.stringify(payload));
|
|
3384
|
+
} else {
|
|
3385
|
+
console.log(pc.gray(`tick ${payload.summary.generatedAt}: recap=${payload.summary.recap.reason || payload.summary.recap.mode || "ok"} checkpoint=${payload.summary.checkpoint.reason || payload.summary.checkpoint.checkpointId || "ok"}`));
|
|
3386
|
+
}
|
|
3387
|
+
while (!controller.signal.aborted) {
|
|
3388
|
+
await waitForNextTick();
|
|
3389
|
+
if (controller.signal.aborted) break;
|
|
3390
|
+
payload = await runTickAndBuildPayload();
|
|
3391
|
+
if (emitJson) {
|
|
3392
|
+
console.log(JSON.stringify(payload));
|
|
3393
|
+
} else {
|
|
3394
|
+
console.log(pc.gray(`tick ${payload.summary.generatedAt}: recap=${payload.summary.recap.reason || payload.summary.recap.mode || "ok"} checkpoint=${payload.summary.checkpoint.reason || payload.summary.checkpoint.checkpointId || "ok"}`));
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
} finally {
|
|
3398
|
+
process.removeListener("SIGINT", stop);
|
|
3399
|
+
process.removeListener("SIGTERM", stop);
|
|
3400
|
+
const stopped = await daemon.stop("signal");
|
|
3401
|
+
if (emitJson) {
|
|
3402
|
+
console.log(
|
|
3403
|
+
JSON.stringify({
|
|
3404
|
+
command: "session daemon",
|
|
3405
|
+
sessionId: normalizedSessionId,
|
|
3406
|
+
targetPath,
|
|
3407
|
+
stopped: {
|
|
3408
|
+
stopped: Boolean(stopped?.stopped),
|
|
3409
|
+
reason: stopped?.reason || "signal",
|
|
3410
|
+
checkpointCloseout: stopped?.checkpointCloseout || null,
|
|
3411
|
+
},
|
|
3412
|
+
}),
|
|
3413
|
+
);
|
|
3414
|
+
} else {
|
|
3415
|
+
console.log(pc.gray(`senti daemon stopped (${stopped?.reason || "signal"}).`));
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3419
|
+
|
|
3420
|
+
const wake = session
|
|
3421
|
+
.command("wake")
|
|
3422
|
+
.description("Wake or register host CLI sessions for the Senti notification bus");
|
|
3423
|
+
|
|
3424
|
+
wake
|
|
3425
|
+
.command("codex [sessionId]")
|
|
3426
|
+
.description("Resume a Codex CLI session with a Senti wake prompt")
|
|
3427
|
+
.option("--session <id>", "Senti session id")
|
|
3428
|
+
.option("--codex-session <id>", "Codex rollout/session id to resume")
|
|
3429
|
+
.option("--last", "Resume the most recent Codex session instead of a specific id")
|
|
3430
|
+
.option("--message <text>", "Senti message body to inject into Codex")
|
|
3431
|
+
.option("--message-file <path>", "Read Senti message body from a file")
|
|
3432
|
+
.option("--from <id>", "Senti sender id", "senti")
|
|
3433
|
+
.option("--sequence <n>", "Senti source sequence id")
|
|
3434
|
+
.option("--cursor <cursor>", "Senti source cursor")
|
|
3435
|
+
.option("--priority <level>", "Senti source priority")
|
|
3436
|
+
.option("--dashboard-url <url>", "Dashboard URL for the Senti session")
|
|
3437
|
+
.option("--cwd <path>", "Workspace cwd for codex exec", ".")
|
|
3438
|
+
.option("--codex-bin <path>", "Codex executable", "codex")
|
|
3439
|
+
.option("--model <model>", "Optional Codex model override")
|
|
3440
|
+
.option("--codex-json", "Pass --json through to codex exec resume")
|
|
3441
|
+
.option("--skip-git-repo-check", "Pass --skip-git-repo-check through to codex")
|
|
3442
|
+
.option(
|
|
3443
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
3444
|
+
"Pass Codex's dangerous no-approval/no-sandbox flag through to the resumed process",
|
|
3445
|
+
)
|
|
3446
|
+
.option("--timeout-ms <n>", "Wake process timeout in milliseconds", "600000")
|
|
3447
|
+
.option("--dry-run", "Print the resume invocation without spawning Codex")
|
|
3448
|
+
.option("--json", "Emit machine-readable output")
|
|
3449
|
+
.action(async (sessionId, options, command) => {
|
|
3450
|
+
const emitJson = shouldEmitJson(options, command);
|
|
3451
|
+
const normalizedSessionId = normalizeString(sessionId) || resolveSessionIdOption(options);
|
|
3452
|
+
const targetPath = path.resolve(process.cwd(), String(options.cwd || "."));
|
|
3453
|
+
if (options.message && options.messageFile) {
|
|
3454
|
+
throw new Error("Use either --message or --message-file, not both.");
|
|
3455
|
+
}
|
|
3456
|
+
const message = options.messageFile
|
|
3457
|
+
? await fsp.readFile(path.resolve(process.cwd(), String(options.messageFile)), "utf-8")
|
|
3458
|
+
: normalizeString(options.message);
|
|
3459
|
+
const prompt = buildCodexWakePrompt({
|
|
3460
|
+
sentiSessionId: normalizedSessionId,
|
|
3461
|
+
message,
|
|
3462
|
+
from: options.from,
|
|
3463
|
+
sequenceId: parseOptionalPositiveInteger(options.sequence, "sequence"),
|
|
3464
|
+
cursor: options.cursor,
|
|
3465
|
+
priority: options.priority,
|
|
3466
|
+
dashboardUrl: options.dashboardUrl,
|
|
3467
|
+
});
|
|
3468
|
+
const invocation = buildCodexExecResumeInvocation({
|
|
3469
|
+
codexSessionId: options.codexSession,
|
|
3470
|
+
prompt,
|
|
3471
|
+
cwd: targetPath,
|
|
3472
|
+
codexBin: options.codexBin,
|
|
3473
|
+
useLast: Boolean(options.last),
|
|
3474
|
+
json: Boolean(options.codexJson),
|
|
3475
|
+
model: options.model,
|
|
3476
|
+
skipGitRepoCheck: Boolean(options.skipGitRepoCheck),
|
|
3477
|
+
dangerouslyBypassApprovalsAndSandbox: Boolean(options.dangerouslyBypassApprovalsAndSandbox),
|
|
3478
|
+
});
|
|
3479
|
+
const payload = {
|
|
3480
|
+
command: "session wake codex",
|
|
3481
|
+
sessionId: normalizedSessionId,
|
|
3482
|
+
dryRun: Boolean(options.dryRun),
|
|
3483
|
+
invocation,
|
|
3484
|
+
prompt,
|
|
3485
|
+
};
|
|
3486
|
+
if (options.dryRun) {
|
|
3487
|
+
if (emitJson) {
|
|
3488
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
console.log(pc.green("Codex wake dry run"));
|
|
3492
|
+
console.log(`${payload.invocation.command} ${payload.invocation.args.map((arg) => JSON.stringify(arg)).join(" ")}`);
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
const result = await runCodexExecResume({
|
|
3496
|
+
invocation,
|
|
3497
|
+
timeoutMs: parsePositiveInteger(options.timeoutMs, "timeout-ms", 600000),
|
|
3498
|
+
});
|
|
3499
|
+
const output = {
|
|
3500
|
+
...payload,
|
|
3501
|
+
result,
|
|
3502
|
+
};
|
|
3503
|
+
if (emitJson) {
|
|
3504
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
const color = result.exitCode === 0 ? pc.green : pc.yellow;
|
|
3508
|
+
console.log(color(`Codex wake completed with exit code ${result.exitCode}.`));
|
|
3509
|
+
if (normalizeString(result.stderr)) {
|
|
3510
|
+
console.log(pc.gray(result.stderr.trim()));
|
|
3511
|
+
}
|
|
3512
|
+
if (result.exitCode !== 0) {
|
|
3513
|
+
process.exitCode = Number(result.exitCode) || 1;
|
|
3514
|
+
}
|
|
3515
|
+
});
|
|
3516
|
+
|
|
3517
|
+
wake
|
|
3518
|
+
.command("codex-notify [sessionId] [notificationJson]")
|
|
3519
|
+
.description("Record Codex notify payloads so sentid can resume the correct Codex rollout")
|
|
3520
|
+
.option("--session <id>", "Senti session id")
|
|
3521
|
+
.option("--agent <id>", "Senti agent id", process.env.SENTINELAYER_AGENT_ID || "codex")
|
|
3522
|
+
.option("--notification <json>", "Notification JSON override")
|
|
3523
|
+
.option("--path <path>", "Workspace path for the Senti session", ".")
|
|
3524
|
+
.option("--json", "Emit machine-readable output")
|
|
3525
|
+
.action(async (sessionId, notificationJson, options, command) => {
|
|
3526
|
+
const emitJson = shouldEmitJson(options, command);
|
|
3527
|
+
const normalizedSessionId = normalizeString(sessionId) || resolveSessionIdOption(options);
|
|
3528
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
3529
|
+
const payload = normalizeString(options.notification) || normalizeString(notificationJson);
|
|
3530
|
+
const result = await recordCodexWakeRegistration({
|
|
3531
|
+
sessionId: normalizedSessionId,
|
|
3532
|
+
agentId: options.agent,
|
|
3533
|
+
notificationPayload: payload,
|
|
3534
|
+
targetPath,
|
|
3535
|
+
});
|
|
3536
|
+
const output = {
|
|
3537
|
+
command: "session wake codex-notify",
|
|
3538
|
+
sessionId: normalizedSessionId,
|
|
3539
|
+
agentId: normalizeString(options.agent) || "codex",
|
|
3540
|
+
...result,
|
|
3541
|
+
};
|
|
3542
|
+
if (emitJson) {
|
|
3543
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
if (!result.registered) {
|
|
3547
|
+
console.log(pc.gray(`Ignored Codex notification: ${result.reason}.`));
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
console.log(pc.green(`Registered Codex wake target: ${result.registryPath}`));
|
|
3551
|
+
});
|
|
3552
|
+
|
|
3553
|
+
wake
|
|
3554
|
+
.command("daemon [sessionId]")
|
|
3555
|
+
.description("Run the sentid wake daemon: watch the session stream and wake this agent on new messages")
|
|
3556
|
+
.option("--session <id>", "Senti session id")
|
|
3557
|
+
.option("--agent <id>", "Local agent this daemon wakes", process.env.SENTINELAYER_AGENT_ID || "")
|
|
3558
|
+
.option("--host <name>", "Host adapter to wake (claude|codex)", "claude")
|
|
3559
|
+
.option("--resume-session <id>", "Host session id to resume on wake")
|
|
3560
|
+
.option("--cwd <path>", "Workspace cwd", ".")
|
|
3561
|
+
.option("--idle-ms <n>", "Idle poll backoff in milliseconds", "1500")
|
|
3562
|
+
.option("--max-attempts <n>", "Wake retries before dead-letter", "5")
|
|
3563
|
+
.option("--once", "Run a single fetch/dispatch tick and exit (dogfood/CI)")
|
|
3564
|
+
.option("--json", "Emit machine-readable output")
|
|
3565
|
+
.action(async (sessionId, options, command) => {
|
|
3566
|
+
const emitJson = shouldEmitJson(options, command);
|
|
3567
|
+
const normalizedSessionId = normalizeString(sessionId) || resolveSessionIdOption(options);
|
|
3568
|
+
if (!normalizedSessionId) {
|
|
3569
|
+
throw new Error("session wake daemon requires a Senti session id (positional or --session).");
|
|
3570
|
+
}
|
|
3571
|
+
const agentId = normalizeString(options.agent);
|
|
3572
|
+
if (!agentId) {
|
|
3573
|
+
throw new Error("session wake daemon requires --agent (the local agent to wake).");
|
|
3574
|
+
}
|
|
3575
|
+
const host = normalizeString(options.host) || "claude";
|
|
3576
|
+
const resumeSessionId = normalizeString(options.resumeSession);
|
|
3577
|
+
if (!resumeSessionId) {
|
|
3578
|
+
throw new Error("session wake daemon requires --resume-session (the host session id to resume).");
|
|
3579
|
+
}
|
|
3580
|
+
const targetPath = path.resolve(process.cwd(), String(options.cwd || "."));
|
|
3581
|
+
const sentid = createSentid({
|
|
3582
|
+
sessionId: normalizedSessionId,
|
|
3583
|
+
agentId,
|
|
3584
|
+
host,
|
|
3585
|
+
resumeSessionId,
|
|
3586
|
+
targetPath,
|
|
3587
|
+
idleMs: parseOptionalPositiveInteger(options.idleMs, "idle-ms") || 1500,
|
|
3588
|
+
maxAttempts: parseOptionalPositiveInteger(options.maxAttempts, "max-attempts") || 5,
|
|
3589
|
+
logger: emitJson
|
|
3590
|
+
? undefined
|
|
3591
|
+
: (level, msg, meta) => console.error(`[sentid:${level}] ${msg}${meta ? ` ${JSON.stringify(meta)}` : ""}`),
|
|
3592
|
+
});
|
|
3593
|
+
|
|
3594
|
+
if (options.once) {
|
|
3595
|
+
const tick = await sentid.tickOnce({ fetchCursor: null });
|
|
3596
|
+
const out = {
|
|
3597
|
+
command: "session wake daemon",
|
|
3598
|
+
once: true,
|
|
3599
|
+
sessionId: normalizedSessionId,
|
|
3600
|
+
agent: agentId,
|
|
3601
|
+
host,
|
|
3602
|
+
cursor: sentid.getCursor(),
|
|
3603
|
+
dispatched: Array.isArray(tick.results) ? tick.results.length : 0,
|
|
3604
|
+
idle: Boolean(tick.idle),
|
|
3605
|
+
};
|
|
3606
|
+
console.log(
|
|
3607
|
+
emitJson
|
|
3608
|
+
? JSON.stringify(out, null, 2)
|
|
3609
|
+
: pc.green(`sentid tick: cursor=${out.cursor} dispatched=${out.dispatched}${out.idle ? " (idle)" : ""}`),
|
|
3610
|
+
);
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
const ac = new AbortController();
|
|
3615
|
+
const stop = () => ac.abort();
|
|
3616
|
+
process.once("SIGINT", stop);
|
|
3617
|
+
process.once("SIGTERM", stop);
|
|
3618
|
+
if (!emitJson) {
|
|
3619
|
+
console.log(
|
|
3620
|
+
pc.green(`sentid daemon: waking ${agentId} (host=${host}) on session ${normalizedSessionId}. Ctrl-C to stop.`),
|
|
3621
|
+
);
|
|
3622
|
+
}
|
|
3623
|
+
await sentid.start({ signal: ac.signal });
|
|
3624
|
+
});
|
|
3625
|
+
|
|
2476
3626
|
const recap = session
|
|
2477
3627
|
.command("recap")
|
|
2478
3628
|
.description("Build deterministic Senti session recaps");
|
|
@@ -2574,6 +3724,9 @@ export function registerSessionCommand(program) {
|
|
|
2574
3724
|
)
|
|
2575
3725
|
.option("--before-sequence <n>", "Remote page ending before this sequence id")
|
|
2576
3726
|
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
3727
|
+
.option("--agent <id>", "Agent id for automatic view receipts", process.env.SENTINELAYER_AGENT_ID || "cli-user")
|
|
3728
|
+
.option("--no-view", "Do not record automatic view receipts for displayed remote messages")
|
|
3729
|
+
.option("--include-control-events", "Include listener lifecycle/control-plane events in transcript output")
|
|
2577
3730
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2578
3731
|
.option("--json", "Emit machine-readable output")
|
|
2579
3732
|
.action(async (sessionId, options, command) => {
|
|
@@ -2585,6 +3738,11 @@ export function registerSessionCommand(program) {
|
|
|
2585
3738
|
const tail = parsePositiveInteger(options.tail, "tail", 20);
|
|
2586
3739
|
const beforeSequence = parseOptionalPositiveInteger(options.beforeSequence, "before-sequence");
|
|
2587
3740
|
const emitJson = shouldEmitJson(options, command);
|
|
3741
|
+
const includeControlEvents = Boolean(options.includeControlEvents);
|
|
3742
|
+
const remoteTailLimit = includeControlEvents
|
|
3743
|
+
? tail
|
|
3744
|
+
: Math.min(200, Math.max(tail, tail + 20));
|
|
3745
|
+
const readAgentId = await defaultAgentId(options.agent, targetPath);
|
|
2588
3746
|
|
|
2589
3747
|
let hydration = null;
|
|
2590
3748
|
let remoteTail = null;
|
|
@@ -2605,7 +3763,7 @@ export function registerSessionCommand(program) {
|
|
|
2605
3763
|
remoteTail = await pollSessionEventsBefore(normalizedSessionId, {
|
|
2606
3764
|
targetPath,
|
|
2607
3765
|
beforeSequence,
|
|
2608
|
-
limit:
|
|
3766
|
+
limit: remoteTailLimit,
|
|
2609
3767
|
timeoutMs: 15_000,
|
|
2610
3768
|
});
|
|
2611
3769
|
if (options.actions !== false) {
|
|
@@ -2681,7 +3839,23 @@ export function registerSessionCommand(program) {
|
|
|
2681
3839
|
const actionEvents = remoteActions?.ok
|
|
2682
3840
|
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
2683
3841
|
: [];
|
|
2684
|
-
const
|
|
3842
|
+
const unacknowledgedHumanMessages = remoteActions?.ok
|
|
3843
|
+
? readProjectionUnacknowledgedHumanMessages(remoteActions)
|
|
3844
|
+
: [];
|
|
3845
|
+
const recentHumanActivity = remoteActions?.ok
|
|
3846
|
+
? readProjectionRecentHumanActivity(remoteActions)
|
|
3847
|
+
: [];
|
|
3848
|
+
const transcriptEvents = includeControlEvents
|
|
3849
|
+
? displayEvents
|
|
3850
|
+
: displayEvents.filter((event) => !isSessionControlEvent(event));
|
|
3851
|
+
const hiddenControlEventCount = displayEvents.length - transcriptEvents.length;
|
|
3852
|
+
const events = mergeSessionActionEvents(transcriptEvents, actionEvents).slice(-tail);
|
|
3853
|
+
const autoView = await recordSessionReadViews(normalizedSessionId, events, {
|
|
3854
|
+
targetPath,
|
|
3855
|
+
agentId: readAgentId,
|
|
3856
|
+
enabled: Boolean(options.remote && options.view !== false),
|
|
3857
|
+
maxTargets: process.env.SENTINELAYER_SESSION_READ_VIEW_MAX_TARGETS || 50,
|
|
3858
|
+
});
|
|
2685
3859
|
const remoteVerified = Boolean(
|
|
2686
3860
|
options.remote &&
|
|
2687
3861
|
((hydration && hydration.ok) || (remoteTail && remoteTail.ok))
|
|
@@ -2694,6 +3868,13 @@ export function registerSessionCommand(program) {
|
|
|
2694
3868
|
beforeSequence,
|
|
2695
3869
|
count: events.length,
|
|
2696
3870
|
events,
|
|
3871
|
+
includeControlEvents,
|
|
3872
|
+
hiddenControlEventCount,
|
|
3873
|
+
unacknowledgedHumanMessageCount: unacknowledgedHumanMessages.length,
|
|
3874
|
+
unacknowledgedHumanMessages,
|
|
3875
|
+
recentHumanActivityCount: recentHumanActivity.length,
|
|
3876
|
+
recentHumanActivity,
|
|
3877
|
+
autoView,
|
|
2697
3878
|
displaySource: !options.remote
|
|
2698
3879
|
? "local"
|
|
2699
3880
|
: remoteTail?.ok
|
|
@@ -2737,6 +3918,18 @@ export function registerSessionCommand(program) {
|
|
|
2737
3918
|
console.log(JSON.stringify(payload, null, 2));
|
|
2738
3919
|
return;
|
|
2739
3920
|
}
|
|
3921
|
+
if (unacknowledgedHumanMessages.length > 0) {
|
|
3922
|
+
console.log(pc.yellow(`Unacknowledged human asks: ${unacknowledgedHumanMessages.length}`));
|
|
3923
|
+
for (const event of unacknowledgedHumanMessages.slice(0, 3)) {
|
|
3924
|
+
console.log(pc.yellow(`- ${formatHumanAskLine(event)}`));
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
if (recentHumanActivity.length > 0) {
|
|
3928
|
+
console.log(pc.yellow(`Recent human activity: ${recentHumanActivity.length}`));
|
|
3929
|
+
for (const action of recentHumanActivity.slice(0, 3)) {
|
|
3930
|
+
console.log(pc.yellow(`- ${formatMessageActionActivityLine(action)}`));
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
2740
3933
|
for (const event of events) {
|
|
2741
3934
|
console.log(formatEventLine(event));
|
|
2742
3935
|
}
|
|
@@ -3048,6 +4241,8 @@ export function registerSessionCommand(program) {
|
|
|
3048
4241
|
.description("Generate a checkpoint from the next uncheckpointed durable event window")
|
|
3049
4242
|
.option("--min-events <n>", "Minimum source events required before creating (default 20)", "20")
|
|
3050
4243
|
.option("--max-events <n>", "Maximum source events to summarize (default 80, max 200)", "80")
|
|
4244
|
+
.option("--catch-up", "Generate multiple consecutive checkpoint windows until caught up or capped")
|
|
4245
|
+
.option("--max-checkpoints <n>", "Maximum checkpoint windows to create with --catch-up (default 5, max 50)", "5")
|
|
3051
4246
|
.option("--operation-id <key>", "Explicit retry key for this generate invocation")
|
|
3052
4247
|
.option("--agent <id>", "Optional agent id recorded as checkpoint creator")
|
|
3053
4248
|
.option("--path <path>", "Workspace path for auth/session context", ".")
|
|
@@ -3061,6 +4256,49 @@ export function registerSessionCommand(program) {
|
|
|
3061
4256
|
const agentId = normalizeString(options.agent)
|
|
3062
4257
|
? await defaultAgentId(options.agent, targetPath)
|
|
3063
4258
|
: "";
|
|
4259
|
+
if (options.catchUp) {
|
|
4260
|
+
const result = await generateSessionCheckpointBatch(normalizedSessionId, {
|
|
4261
|
+
targetPath,
|
|
4262
|
+
minEvents: options.minEvents,
|
|
4263
|
+
maxEvents: options.maxEvents,
|
|
4264
|
+
maxCheckpoints: options.maxCheckpoints,
|
|
4265
|
+
idempotencyKey: options.operationId,
|
|
4266
|
+
createdByAgentId: agentId,
|
|
4267
|
+
});
|
|
4268
|
+
const hydration = result.createdCount > 0
|
|
4269
|
+
? await hydrateAfterCheckpointMutation(normalizedSessionId, { targetPath })
|
|
4270
|
+
: null;
|
|
4271
|
+
const payload = {
|
|
4272
|
+
command: "session checkpoint generate",
|
|
4273
|
+
targetPath,
|
|
4274
|
+
...result,
|
|
4275
|
+
hydration: hydration || undefined,
|
|
4276
|
+
};
|
|
4277
|
+
if (shouldEmitJson(options, command)) {
|
|
4278
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
if (result.createdCount > 0) {
|
|
4282
|
+
console.log(pc.bold(`checkpoint catch-up generated ${result.createdCount} checkpoint${result.createdCount === 1 ? "" : "s"}`));
|
|
4283
|
+
for (const item of result.results) {
|
|
4284
|
+
if (item.checkpoint) {
|
|
4285
|
+
console.log(formatCheckpointLine(item.checkpoint));
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
console.log(pc.gray(`Stopped: ${result.stoppedReason || "complete"} after ${result.attemptedCount} attempt${result.attemptedCount === 1 ? "" : "s"}.`));
|
|
4289
|
+
if (hydration && !hydration.ok) {
|
|
4290
|
+
console.log(pc.gray(`Local hydrate skipped: ${hydration.reason || "unknown"}`));
|
|
4291
|
+
}
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
4294
|
+
const last = result.lastResult || {};
|
|
4295
|
+
console.log(
|
|
4296
|
+
pc.gray(
|
|
4297
|
+
`No checkpoint created: ${normalizeString(last.reason || result.stoppedReason) || "not_needed"} (${Number(last.eventCount || 0)} events, min ${Number(last.minEvents || options.minEvents || 0)}).`,
|
|
4298
|
+
),
|
|
4299
|
+
);
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
3064
4302
|
const result = await generateSessionCheckpoint(normalizedSessionId, {
|
|
3065
4303
|
targetPath,
|
|
3066
4304
|
minEvents: options.minEvents,
|
|
@@ -3582,14 +4820,7 @@ export function registerSessionCommand(program) {
|
|
|
3582
4820
|
return;
|
|
3583
4821
|
}
|
|
3584
4822
|
for (const item of trimmed) {
|
|
3585
|
-
|
|
3586
|
-
const created = item.createdAt || "?";
|
|
3587
|
-
const lastActivity = item.lastActivityAt
|
|
3588
|
-
? ` last=${item.lastActivityAt}`
|
|
3589
|
-
: "";
|
|
3590
|
-
console.log(
|
|
3591
|
-
`${item.sessionId} status=${item.status}${archive} created=${created}${lastActivity}`,
|
|
3592
|
-
);
|
|
4823
|
+
console.log(formatSessionListLine(item));
|
|
3593
4824
|
}
|
|
3594
4825
|
if (remote.count > trimmed.length || remote.hasMore) {
|
|
3595
4826
|
console.log(
|
|
@@ -3637,10 +4868,9 @@ export function registerSessionCommand(program) {
|
|
|
3637
4868
|
return;
|
|
3638
4869
|
}
|
|
3639
4870
|
for (const item of trimmed) {
|
|
3640
|
-
const
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
);
|
|
4871
|
+
const line = formatSessionListLine(item);
|
|
4872
|
+
const expires = item.expiresAt ? ` expires=${item.expiresAt}` : "";
|
|
4873
|
+
console.log(`${line}${expires}`);
|
|
3644
4874
|
}
|
|
3645
4875
|
if (sessions.length > trimmed.length) {
|
|
3646
4876
|
console.log(
|