sentinelayer-cli 0.40.0 → 0.42.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/README.md +22 -4
- package/package.json +4 -4
- package/src/auth/admission-scope.js +34 -0
- package/src/auth/gate.js +6 -0
- package/src/auth/http.js +4 -0
- package/src/auth/service.js +6 -0
- package/src/board/done-gate.js +155 -0
- package/src/board/evidence-resolvers.js +138 -0
- package/src/cli.js +24 -1
- package/src/commands/board.js +116 -0
- package/src/commands/session.js +1094 -279
- package/src/engram/build-state.js +183 -0
- package/src/engram/canonical.js +75 -0
- package/src/engram/document.js +190 -0
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +73 -0
- package/src/engram/ingest-document.js +107 -0
- package/src/engram/namespace.js +89 -0
- package/src/engram/observations.js +34 -0
- package/src/engram/sla.js +104 -0
- package/src/engram/store.js +120 -0
- package/src/engram/tools.js +152 -0
- package/src/engram/trust.js +105 -0
- package/src/events/schema.js +18 -0
- package/src/legacy-cli.js +6 -3
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/net/test-egress-guard.js +158 -0
- package/src/review/investor-dd-orchestrator.js +14 -2
- package/src/session/admission-auth.js +312 -0
- package/src/session/admission-preimage.js +90 -0
- package/src/session/admission.js +620 -0
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +25 -1
- package/src/session/codebase-context.js +54 -0
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon-spawn.js +20 -2
- package/src/session/daemon.js +26 -125
- package/src/session/event-identity.js +9 -2
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/first-message.js +11 -3
- package/src/session/listener.js +246 -20
- package/src/session/listeners.js +71 -80
- package/src/session/live-source.js +47 -16
- package/src/session/message-edits.js +89 -0
- package/src/session/paths.js +2 -0
- package/src/session/recall/embedder.js +181 -0
- package/src/session/recall/entities.js +0 -0
- package/src/session/recall/index-build.js +38 -0
- package/src/session/recall/index-core.js +108 -0
- package/src/session/recall/index.js +131 -0
- package/src/session/recall/needle-eval.js +279 -0
- package/src/session/recall/observation-core.js +183 -0
- package/src/session/recall/observations.js +21 -0
- package/src/session/recall/provenance.js +153 -0
- package/src/session/recall/retrieve.js +374 -0
- package/src/session/recall/text.js +0 -0
- package/src/session/recall/token-stats.js +81 -0
- package/src/session/recap.js +54 -26
- package/src/session/remote-hydrate.js +12 -20
- package/src/session/store.js +13 -14
- package/src/session/stream.js +23 -3
- package/src/session/sync.js +789 -22
- package/src/session/tickets.js +316 -0
- package/src/session/title-sync.js +2 -1
- package/src/session/usage-report.js +41 -2
- package/src/session/wake/pump.js +8 -3
- package/src/session/wake/resolve-target.js +10 -5
package/src/commands/session.js
CHANGED
|
@@ -42,11 +42,20 @@ import {
|
|
|
42
42
|
} from "../session/daemon-spawn.js";
|
|
43
43
|
import { listRuntimeRuns } from "../session/runtime-bridge.js";
|
|
44
44
|
import {
|
|
45
|
+
guardFileLeases,
|
|
45
46
|
listFileLocks,
|
|
46
47
|
lockFile,
|
|
47
48
|
releaseFileLocksForAgent,
|
|
49
|
+
renewFileLease,
|
|
48
50
|
unlockFile,
|
|
49
51
|
} from "../session/file-locks.js";
|
|
52
|
+
import {
|
|
53
|
+
formatFileLeaseDenialSummary,
|
|
54
|
+
installFileLeaseIntegrations,
|
|
55
|
+
runFileLeaseGuardHook,
|
|
56
|
+
safeFileLeaseErrorMessage,
|
|
57
|
+
uninstallFileLeaseIntegrations,
|
|
58
|
+
} from "../session/file-lease-integrations.js";
|
|
50
59
|
import {
|
|
51
60
|
injectSessionGuides,
|
|
52
61
|
setupSessionGuides,
|
|
@@ -64,7 +73,7 @@ import {
|
|
|
64
73
|
refreshSessionCacheForRemoteActivity,
|
|
65
74
|
updateSessionTitle,
|
|
66
75
|
} from "../session/store.js";
|
|
67
|
-
import { fetchSessionListeners, formatListenerLine
|
|
76
|
+
import { fetchSessionListeners, formatListenerLine } from "../session/listeners.js";
|
|
68
77
|
import {
|
|
69
78
|
getListenerProcessStatus,
|
|
70
79
|
removeListenerPidRecord,
|
|
@@ -82,6 +91,27 @@ import {
|
|
|
82
91
|
normalizeSessionOnboarding,
|
|
83
92
|
writeSessionOnboardingBrief,
|
|
84
93
|
} from "../session/invitations.js";
|
|
94
|
+
import {
|
|
95
|
+
cancelAdmission,
|
|
96
|
+
fetchOwnAdmissionReceipt,
|
|
97
|
+
parseScope as parseAdmissionScope,
|
|
98
|
+
parseTtlSeconds as parseAdmissionTtlSeconds,
|
|
99
|
+
runAdmissionJoin,
|
|
100
|
+
} from "../session/admission.js";
|
|
101
|
+
import {
|
|
102
|
+
assertCanonicalAgentId,
|
|
103
|
+
canonicalAgentId,
|
|
104
|
+
currentAdmittedAgent,
|
|
105
|
+
withAgentAdmission,
|
|
106
|
+
} from "../session/admission-auth.js";
|
|
107
|
+
import {
|
|
108
|
+
claimTicket,
|
|
109
|
+
listTickets,
|
|
110
|
+
releaseTicketLease,
|
|
111
|
+
renewTicketLease,
|
|
112
|
+
submitTicket,
|
|
113
|
+
ticketEvents,
|
|
114
|
+
} from "../session/tickets.js";
|
|
85
115
|
import { postFirstSentiMessage } from "../session/first-message.js";
|
|
86
116
|
import { createListenerHostWake } from "../session/wake/listen-wake.js";
|
|
87
117
|
import { appendToStream, readStream, tailStream } from "../session/stream.js";
|
|
@@ -95,6 +125,8 @@ import {
|
|
|
95
125
|
import { readSessionPreview } from "../session/preview.js";
|
|
96
126
|
import {
|
|
97
127
|
createSessionMessageAction,
|
|
128
|
+
editSessionMessage,
|
|
129
|
+
fetchJsonWithFullTimeout,
|
|
98
130
|
fetchSessionUsageLedger,
|
|
99
131
|
fetchSessionPinnedMessages,
|
|
100
132
|
listSessionMessageActions,
|
|
@@ -103,11 +135,16 @@ import {
|
|
|
103
135
|
pollHumanMessages,
|
|
104
136
|
pollSessionEvents,
|
|
105
137
|
pollSessionEventsBefore,
|
|
138
|
+
requestSessionListenerStop,
|
|
139
|
+
renewSessionPresence,
|
|
106
140
|
searchSessionEvents,
|
|
107
141
|
syncSessionEventToApi,
|
|
108
142
|
syncSessionMetadataToApi,
|
|
143
|
+
updateSessionReadCursor,
|
|
109
144
|
} from "../session/sync.js";
|
|
110
145
|
import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
146
|
+
import { messageRevision, replyRevisionEvent, sessionMessageRoutingEvent } from "../session/message-edits.js";
|
|
147
|
+
import { runSessionRecall } from "../session/recall/index.js";
|
|
111
148
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
112
149
|
import { listenSessionEvents } from "../session/listener.js";
|
|
113
150
|
import { SESSION_LIVE_SUCCESS_TIPS } from "../session/coordination-guidance.js";
|
|
@@ -442,7 +479,9 @@ async function confirmSessionEventVisible(
|
|
|
442
479
|
}
|
|
443
480
|
lastReason = "not_visible";
|
|
444
481
|
const nextCursor = normalizeString(result.cursor);
|
|
445
|
-
|
|
482
|
+
const scanExhausted = result.scanExhausted ?? result.scan_exhausted;
|
|
483
|
+
const emptyScanCanContinue = !events.length && scanExhausted === false;
|
|
484
|
+
if (!nextCursor || nextCursor === pageCursor || (!events.length && !emptyScanCanContinue)) {
|
|
446
485
|
break;
|
|
447
486
|
}
|
|
448
487
|
pageCursor = nextCursor;
|
|
@@ -558,11 +597,6 @@ const SESSION_MESSAGE_ACTION_DESCRIPTIONS = Object.freeze([
|
|
|
558
597
|
command: "sl session action <id> disregard --target-sequence <n>",
|
|
559
598
|
description: "Mark a message as intentionally ignored or superseded.",
|
|
560
599
|
},
|
|
561
|
-
{
|
|
562
|
-
type: "view",
|
|
563
|
-
command: "sl session view <id> <sequence>",
|
|
564
|
-
description: "Manually backfill a read receipt for a target message; remote reads record views automatically.",
|
|
565
|
-
},
|
|
566
600
|
]);
|
|
567
601
|
|
|
568
602
|
function normalizeSessionMessageActionType(value) {
|
|
@@ -654,6 +688,11 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
654
688
|
},
|
|
655
689
|
sessionId,
|
|
656
690
|
ts: actionCreatedAt(action),
|
|
691
|
+
messageRevision: messageRevision(action),
|
|
692
|
+
editedAt: action.editedAt,
|
|
693
|
+
editedBy: action.editedBy,
|
|
694
|
+
canEdit: action.canEdit,
|
|
695
|
+
revisionEvidence: action.revisionEvidence,
|
|
657
696
|
payload: {
|
|
658
697
|
actionId: id,
|
|
659
698
|
actionType,
|
|
@@ -661,6 +700,7 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
661
700
|
targetCursor: actionTargetCursor(action) || null,
|
|
662
701
|
targetActionId: actionTargetActionId(action) || null,
|
|
663
702
|
note: normalizeString(action.note) || null,
|
|
703
|
+
metadata: action.metadata && typeof action.metadata === "object" ? action.metadata : undefined,
|
|
664
704
|
message: actionDisplayMessage(action),
|
|
665
705
|
source: "session_action",
|
|
666
706
|
},
|
|
@@ -960,12 +1000,12 @@ function normalizeAutoViewTimeoutMs(value) {
|
|
|
960
1000
|
return Math.max(50, Math.min(5_000, Math.floor(parsed)));
|
|
961
1001
|
}
|
|
962
1002
|
|
|
963
|
-
async function
|
|
1003
|
+
async function writeSessionReadCursor(sessionId, target = null, {
|
|
964
1004
|
targetPath,
|
|
965
1005
|
agentId,
|
|
966
1006
|
timeoutMs = SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
967
1007
|
} = {}) {
|
|
968
|
-
if (!
|
|
1008
|
+
if (!target) {
|
|
969
1009
|
return;
|
|
970
1010
|
}
|
|
971
1011
|
const totalTimeoutMs = normalizeAutoViewTimeoutMs(timeoutMs);
|
|
@@ -983,50 +1023,31 @@ async function writeSessionReadViews(sessionId, writeTargets = [], {
|
|
|
983
1023
|
}
|
|
984
1024
|
|
|
985
1025
|
try {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
targetCursor: target.targetCursor,
|
|
997
|
-
metadata: {
|
|
998
|
-
source: "cli_read",
|
|
999
|
-
agentId,
|
|
1000
|
-
},
|
|
1001
|
-
idempotencyKey: defaultActionIdempotencyKey({
|
|
1002
|
-
actionType: "view",
|
|
1003
|
-
targetSequenceId: target.targetSequenceId,
|
|
1004
|
-
targetCursor: target.targetCursor,
|
|
1005
|
-
agentId,
|
|
1006
|
-
}),
|
|
1007
|
-
timeoutMs: remainingMs,
|
|
1008
|
-
signal: controller.signal,
|
|
1009
|
-
});
|
|
1010
|
-
if (!result?.ok) {
|
|
1011
|
-
break;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1026
|
+
const elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
1027
|
+
const remainingMs = Math.max(1, totalTimeoutMs - elapsedMs);
|
|
1028
|
+
await updateSessionReadCursor(sessionId, {
|
|
1029
|
+
targetPath,
|
|
1030
|
+
targetSequenceId: target.targetSequenceId,
|
|
1031
|
+
targetCursor: target.targetCursor,
|
|
1032
|
+
agentId,
|
|
1033
|
+
timeoutMs: remainingMs,
|
|
1034
|
+
signal: controller.signal,
|
|
1035
|
+
});
|
|
1014
1036
|
} finally {
|
|
1015
1037
|
clearTimeout(timeoutHandle);
|
|
1016
1038
|
}
|
|
1017
1039
|
}
|
|
1018
1040
|
|
|
1019
|
-
function
|
|
1041
|
+
function recordSessionReadCursor(sessionId, events = [], {
|
|
1020
1042
|
targetPath,
|
|
1021
1043
|
agentId,
|
|
1022
1044
|
enabled = false,
|
|
1023
|
-
maxTargets = 50,
|
|
1024
1045
|
timeoutMs = SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
1025
1046
|
} = {}) {
|
|
1026
|
-
const
|
|
1047
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
1027
1048
|
const summary = {
|
|
1028
1049
|
enabled: Boolean(enabled),
|
|
1029
|
-
agentId:
|
|
1050
|
+
agentId: normalizedAgentId || "cli-user",
|
|
1030
1051
|
targetCount: 0,
|
|
1031
1052
|
attempted: 0,
|
|
1032
1053
|
recorded: 0,
|
|
@@ -1042,26 +1063,22 @@ function recordSessionReadViews(sessionId, events = [], {
|
|
|
1042
1063
|
return summary;
|
|
1043
1064
|
}
|
|
1044
1065
|
|
|
1045
|
-
|
|
1046
|
-
const targets = [];
|
|
1066
|
+
let target = null;
|
|
1047
1067
|
for (const event of Array.isArray(events) ? events : []) {
|
|
1048
|
-
const
|
|
1049
|
-
if (!
|
|
1050
|
-
|
|
1068
|
+
const candidate = sessionReadViewTarget(event);
|
|
1069
|
+
if (!candidate) continue;
|
|
1070
|
+
summary.targetCount += 1;
|
|
1071
|
+
if (
|
|
1072
|
+
!target ||
|
|
1073
|
+
Number(candidate.targetSequenceId || 0) >= Number(target.targetSequenceId || 0)
|
|
1074
|
+
) {
|
|
1075
|
+
target = candidate;
|
|
1051
1076
|
}
|
|
1052
|
-
seenTargets.add(target.key);
|
|
1053
|
-
targets.push(target);
|
|
1054
1077
|
}
|
|
1055
|
-
summary.
|
|
1078
|
+
summary.skipped = Math.max(0, summary.targetCount - (target ? 1 : 0));
|
|
1079
|
+
summary.queued = target ? 1 : 0;
|
|
1056
1080
|
|
|
1057
|
-
|
|
1058
|
-
summary.skipped = Math.max(0, targets.length - writeTargets.length);
|
|
1059
|
-
summary.queued = writeTargets.length;
|
|
1060
|
-
if (summary.skipped > 0) {
|
|
1061
|
-
summary.reason = "target_cap_reached";
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
if (writeTargets.length === 0) {
|
|
1081
|
+
if (!target) {
|
|
1065
1082
|
if (!summary.reason) {
|
|
1066
1083
|
summary.reason = "no_targets";
|
|
1067
1084
|
}
|
|
@@ -1069,13 +1086,13 @@ function recordSessionReadViews(sessionId, events = [], {
|
|
|
1069
1086
|
}
|
|
1070
1087
|
|
|
1071
1088
|
summary.background = true;
|
|
1072
|
-
|
|
1073
|
-
summary.reason = "queued_best_effort";
|
|
1074
|
-
}
|
|
1089
|
+
summary.reason = "queued_monotonic_upsert";
|
|
1075
1090
|
|
|
1076
|
-
void
|
|
1091
|
+
void writeSessionReadCursor(sessionId, target, {
|
|
1077
1092
|
targetPath,
|
|
1078
|
-
|
|
1093
|
+
// Omit a placeholder identity so the server can bind human reads to the
|
|
1094
|
+
// authenticated principal. Explicit agent reads still carry their grant.
|
|
1095
|
+
agentId: normalizedAgentId,
|
|
1079
1096
|
timeoutMs,
|
|
1080
1097
|
}).catch(() => {
|
|
1081
1098
|
// Best-effort view receipts must never affect transcript rendering.
|
|
@@ -1412,19 +1429,22 @@ async function findReusableSessionCandidate({
|
|
|
1412
1429
|
// can exercise the local materialization path without a real API.
|
|
1413
1430
|
async function fetchRemoteSessionDetail(endpoint, headers) {
|
|
1414
1431
|
let response;
|
|
1432
|
+
let payload;
|
|
1415
1433
|
try {
|
|
1416
|
-
response = await
|
|
1434
|
+
({ response, payload } = await fetchJsonWithFullTimeout(
|
|
1435
|
+
endpoint, { method: "GET", headers, redirect: "error" }, 2_000,
|
|
1436
|
+
));
|
|
1417
1437
|
} catch (err) {
|
|
1418
1438
|
return {
|
|
1419
1439
|
ok: false,
|
|
1420
1440
|
response: null,
|
|
1421
1441
|
status: 0,
|
|
1422
|
-
reason: normalizeString(err?.message) || "fetch_failed",
|
|
1442
|
+
reason: err?.name === "AbortError" ? "request_timeout" : (normalizeString(err?.message) || "fetch_failed"),
|
|
1423
1443
|
retryable: true,
|
|
1424
1444
|
};
|
|
1425
1445
|
}
|
|
1426
1446
|
if (response && response.ok) {
|
|
1427
|
-
return { ok: true,
|
|
1447
|
+
return { ok: true, payload, status: response.status, reason: "", retryable: false };
|
|
1428
1448
|
}
|
|
1429
1449
|
if (!response) {
|
|
1430
1450
|
return { ok: false, response: null, status: 0, reason: "no_response", retryable: true };
|
|
@@ -1439,8 +1459,7 @@ async function fetchRemoteSessionDetail(endpoint, headers) {
|
|
|
1439
1459
|
};
|
|
1440
1460
|
}
|
|
1441
1461
|
|
|
1442
|
-
|
|
1443
|
-
const body = await response.json().catch(() => ({}));
|
|
1462
|
+
function parseRemoteSessionDetailResponse(body) {
|
|
1444
1463
|
return body && body.session && typeof body.session === "object"
|
|
1445
1464
|
? body.session
|
|
1446
1465
|
: body && typeof body === "object"
|
|
@@ -1483,7 +1502,7 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
1483
1502
|
return {
|
|
1484
1503
|
ok: true,
|
|
1485
1504
|
source: "singleton",
|
|
1486
|
-
session:
|
|
1505
|
+
session: parseRemoteSessionDetailResponse(detail.payload),
|
|
1487
1506
|
status: detail.status,
|
|
1488
1507
|
};
|
|
1489
1508
|
}
|
|
@@ -1777,7 +1796,13 @@ export async function ensureWorkspaceSession({
|
|
|
1777
1796
|
);
|
|
1778
1797
|
let titleSync = null;
|
|
1779
1798
|
if (shouldPushTitle) {
|
|
1780
|
-
titleSync = await pushSessionTitleToApi(created.sessionId, effectiveTitle, {
|
|
1799
|
+
titleSync = await pushSessionTitleToApi(created.sessionId, effectiveTitle, {
|
|
1800
|
+
targetPath,
|
|
1801
|
+
// Startup uses the already validated credential; rotation and Retry-After
|
|
1802
|
+
// sleeps belong to explicit auth/title operations, not optional startup.
|
|
1803
|
+
autoRotate: false,
|
|
1804
|
+
maxRetries: 0,
|
|
1805
|
+
});
|
|
1781
1806
|
}
|
|
1782
1807
|
|
|
1783
1808
|
return {
|
|
@@ -1796,11 +1821,8 @@ export async function ensureWorkspaceSession({
|
|
|
1796
1821
|
}
|
|
1797
1822
|
|
|
1798
1823
|
function normalizeAgentId(value, fallbackValue = "cli-user") {
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1802
|
-
.replace(/^-+|-+$/g, "");
|
|
1803
|
-
return normalized || fallbackValue;
|
|
1824
|
+
// One canonicalisation for authorisation and execution (see admission-auth.js).
|
|
1825
|
+
return canonicalAgentId(value) || fallbackValue;
|
|
1804
1826
|
}
|
|
1805
1827
|
|
|
1806
1828
|
function canPublishListenerPresence(agentId) {
|
|
@@ -1855,13 +1877,6 @@ function normalizeSessionObservationText(rawValue, fieldName, maxLength) {
|
|
|
1855
1877
|
return value;
|
|
1856
1878
|
}
|
|
1857
1879
|
|
|
1858
|
-
function listenerLifecycleEventName(type = "") {
|
|
1859
|
-
const normalized = normalizeString(type).toLowerCase();
|
|
1860
|
-
if (normalized === "started") return "session_listener_started";
|
|
1861
|
-
if (normalized === "stopped") return "session_listener_stopped";
|
|
1862
|
-
return "session_listener_heartbeat";
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
1880
|
function compactPayload(record = {}) {
|
|
1866
1881
|
return Object.fromEntries(
|
|
1867
1882
|
Object.entries(record).filter(([, value]) => value !== undefined)
|
|
@@ -1948,11 +1963,6 @@ async function publishListenerPresenceEvent({
|
|
|
1948
1963
|
lifecycle = {},
|
|
1949
1964
|
} = {}) {
|
|
1950
1965
|
const normalizedType = normalizeString(lifecycle.type) || "heartbeat";
|
|
1951
|
-
const eventName = listenerLifecycleEventName(normalizedType);
|
|
1952
|
-
const idempotencyStep = listenerLifecycleIdempotencyStep({
|
|
1953
|
-
...lifecycle,
|
|
1954
|
-
type: normalizedType,
|
|
1955
|
-
});
|
|
1956
1966
|
const identity = inferSessionAgentIdentity({
|
|
1957
1967
|
agentId,
|
|
1958
1968
|
model: agentModel,
|
|
@@ -1960,49 +1970,23 @@ async function publishListenerPresenceEvent({
|
|
|
1960
1970
|
provider,
|
|
1961
1971
|
clientKind,
|
|
1962
1972
|
});
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
lifecycle: normalizedType,
|
|
1980
|
-
state: normalizeString(lifecycle.state) || normalizedType,
|
|
1981
|
-
active: lifecycle.active,
|
|
1982
|
-
cursor: lifecycle.cursor || null,
|
|
1983
|
-
cursorSuffix: lifecycle.cursorSuffix,
|
|
1984
|
-
cursorSource: lifecycle.cursorSource,
|
|
1985
|
-
pollCount: lifecycle.pollCount,
|
|
1986
|
-
heartbeatCount: lifecycle.heartbeatCount,
|
|
1987
|
-
matched: lifecycle.matched,
|
|
1988
|
-
emitted: lifecycle.emitted,
|
|
1989
|
-
persistedCursor: lifecycle.persistedCursor,
|
|
1990
|
-
idleIntervalSeconds: lifecycle.idleIntervalSeconds,
|
|
1991
|
-
activeIntervalSeconds: lifecycle.activeIntervalSeconds,
|
|
1992
|
-
activeWindowSeconds: lifecycle.activeWindowSeconds,
|
|
1993
|
-
presenceIntervalSeconds: lifecycle.presenceIntervalSeconds,
|
|
1994
|
-
presenceKeepaliveSeconds: lifecycle.presenceKeepaliveSeconds,
|
|
1995
|
-
lastHumanActivityAt: lifecycle.lastHumanActivityAt,
|
|
1996
|
-
lastSleepMs: lifecycle.lastSleepMs,
|
|
1997
|
-
nextPollMs: lifecycle.nextPollMs,
|
|
1998
|
-
reason: lifecycle.reason || null,
|
|
1999
|
-
startedAt: lifecycle.startedAt,
|
|
2000
|
-
stoppedAt: lifecycle.stoppedAt,
|
|
2001
|
-
aborted: lifecycle.aborted,
|
|
2002
|
-
stopping: lifecycle.stopping,
|
|
2003
|
-
}),
|
|
1973
|
+
return renewSessionPresence(sessionId, {
|
|
1974
|
+
targetPath,
|
|
1975
|
+
agentId,
|
|
1976
|
+
state:
|
|
1977
|
+
normalizedType === "stopped"
|
|
1978
|
+
? "stopped"
|
|
1979
|
+
: normalizeString(lifecycle.state) || normalizedType,
|
|
1980
|
+
listenerId,
|
|
1981
|
+
model: normalizeString(identity.model) || "cli",
|
|
1982
|
+
displayName: normalizeString(identity.displayName) || agentId,
|
|
1983
|
+
provider: normalizeString(identity.provider),
|
|
1984
|
+
clientKind: normalizeString(identity.clientKind) || "cli",
|
|
1985
|
+
observedAt:
|
|
1986
|
+
lifecycle.stoppedAt ||
|
|
1987
|
+
lifecycle.startedAt ||
|
|
1988
|
+
new Date().toISOString(),
|
|
2004
1989
|
});
|
|
2005
|
-
return syncSessionEventToApi(sessionId, event, { targetPath });
|
|
2006
1990
|
}
|
|
2007
1991
|
|
|
2008
1992
|
function formatListenerCatchupNotice(catchup = {}) {
|
|
@@ -2400,17 +2384,6 @@ async function resolveSessionAgentEnvelope(
|
|
|
2400
2384
|
return Object.fromEntries(Object.entries(envelope).filter(([, value]) => value !== undefined));
|
|
2401
2385
|
}
|
|
2402
2386
|
|
|
2403
|
-
// Builds the lock/unlock say-convention directive the session daemon parses
|
|
2404
|
-
// into the authoritative file-lock registry. Kept pure + exported for testing.
|
|
2405
|
-
export function buildSessionLockDirective(verb, file, intent = "") {
|
|
2406
|
-
const normalizedFile = normalizeString(file);
|
|
2407
|
-
const normalizedIntent = normalizeString(intent);
|
|
2408
|
-
if (verb === "unlock") {
|
|
2409
|
-
return `unlock: ${normalizedFile} - ${normalizedIntent || "done"}`;
|
|
2410
|
-
}
|
|
2411
|
-
return normalizedIntent ? `lock: ${normalizedFile} - ${normalizedIntent}` : `lock: ${normalizedFile}`;
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
2387
|
async function runWithConcurrency(items = [], concurrency = 1, worker = async () => null) {
|
|
2415
2388
|
const normalizedItems = Array.isArray(items) ? items : [];
|
|
2416
2389
|
const normalizedConcurrency = Math.max(
|
|
@@ -2446,6 +2419,11 @@ function formatEventLine(event = {}) {
|
|
|
2446
2419
|
const ts = normalizeString(event.ts || event.timestamp);
|
|
2447
2420
|
const type = normalizeString(event.event || event.type) || "event";
|
|
2448
2421
|
const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
|
|
2422
|
+
if (type === "session_message_edited") {
|
|
2423
|
+
const routed = sessionMessageRoutingEvent(event);
|
|
2424
|
+
const target = event.payload?.targetActionId ? `reply ${event.payload.targetActionId}` : `#${event.payload?.targetSequenceId || "?"}`;
|
|
2425
|
+
return `${ts} ${agentId} ${type} ${target} revision ${event.payload?.messageRevision || "?"}: ${normalizeString(routed.payload?.message || routed.payload?.text || "")}`;
|
|
2426
|
+
}
|
|
2449
2427
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
2450
2428
|
if (type === "session_action" || type === "session_reply" || type === "session_reaction") {
|
|
2451
2429
|
const actionType = normalizeString(payload.actionType) || type.replace(/^session_/, "");
|
|
@@ -2866,6 +2844,43 @@ async function emitAgentKilledEvent(sessionId, agentId, {
|
|
|
2866
2844
|
return event;
|
|
2867
2845
|
}
|
|
2868
2846
|
|
|
2847
|
+
function collectOption(value, previous) {
|
|
2848
|
+
return [...(previous || []), value];
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
/**
|
|
2852
|
+
* Wrap a command action so it runs as its agent: inside that agent's admission when
|
|
2853
|
+
* this machine holds one (every session API call then uses the admission credential
|
|
2854
|
+
* and never the human token), on the legacy path when it holds none, and refused when
|
|
2855
|
+
* what it holds is expired or unusable. `resolveIds` names the (session, agent) pair
|
|
2856
|
+
* exactly the way the action itself resolves it.
|
|
2857
|
+
*/
|
|
2858
|
+
function asAdmittedAgent(resolveIds, action) {
|
|
2859
|
+
return async (...args) => {
|
|
2860
|
+
const { sessionId, agentId } = await resolveIds(...args);
|
|
2861
|
+
return withAgentAdmission(sessionId, agentId, () => action(...args));
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
async function sessionSayAgent(sessionId, _messageParts, options) {
|
|
2866
|
+
const sid = normalizeString(sessionId);
|
|
2867
|
+
const identity = await resolveSessionSayIdentity({
|
|
2868
|
+
sessionId: sid,
|
|
2869
|
+
agentId: options.agent,
|
|
2870
|
+
targetPath: path.resolve(process.cwd(), String(options.path || ".")),
|
|
2871
|
+
});
|
|
2872
|
+
return { sessionId: sid, agentId: identity.agentId };
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
async function sessionReadAgent(sessionId, options) {
|
|
2876
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
2877
|
+
return { sessionId: normalizeString(sessionId), agentId: await defaultAgentId(options.agent, targetPath) };
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
async function sessionListenAgent(options) {
|
|
2881
|
+
return { sessionId: resolveSessionIdOption(options), agentId: normalizeAgentId(options.agent, "cli-user") };
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2869
2884
|
export function registerSessionCommand(program) {
|
|
2870
2885
|
const session = program
|
|
2871
2886
|
.command("session")
|
|
@@ -2980,8 +2995,9 @@ export function registerSessionCommand(program) {
|
|
|
2980
2995
|
};
|
|
2981
2996
|
|
|
2982
2997
|
// Best-effort admin visibility sync. Session creation remains local-first.
|
|
2998
|
+
let metadataSync = Promise.resolve(null);
|
|
2983
2999
|
if (remoteSync.attempted) {
|
|
2984
|
-
|
|
3000
|
+
metadataSync = syncSessionMetadataToApi(created.sessionId, {
|
|
2985
3001
|
targetPath,
|
|
2986
3002
|
sessionId: created.sessionId,
|
|
2987
3003
|
status: created.status,
|
|
@@ -2991,7 +3007,7 @@ export function registerSessionCommand(program) {
|
|
|
2991
3007
|
ttlSeconds,
|
|
2992
3008
|
template: created.template,
|
|
2993
3009
|
codebaseContext: created.codebaseContext,
|
|
2994
|
-
}).catch(() =>
|
|
3010
|
+
}, { targetPath, timeoutMs: 2_000 }).catch(() => null);
|
|
2995
3011
|
}
|
|
2996
3012
|
|
|
2997
3013
|
// Make the session managed by default: spawn the Senti daemon as a
|
|
@@ -3028,6 +3044,14 @@ export function registerSessionCommand(program) {
|
|
|
3028
3044
|
}).catch((error) => ({ posted: false, reason: normalizeString(error?.message) || "error" }));
|
|
3029
3045
|
}
|
|
3030
3046
|
payload.firstMessage = firstMessage;
|
|
3047
|
+
// Own the request lifecycle; do not leave a referenced fetch running
|
|
3048
|
+
// after emitting the single JSON document (even with --no-daemon).
|
|
3049
|
+
const metadataResult = await metadataSync;
|
|
3050
|
+
if (remoteSync.attempted) {
|
|
3051
|
+
remoteSync.status = metadataResult?.synced ? "synced" : "degraded";
|
|
3052
|
+
remoteSync.metadataSynced = Boolean(metadataResult?.synced);
|
|
3053
|
+
remoteSync.reason = metadataResult?.synced ? "metadata_acknowledged" : (metadataResult?.reason || "metadata_sync_failed");
|
|
3054
|
+
}
|
|
3031
3055
|
|
|
3032
3056
|
if (shouldEmitJson(options, command)) {
|
|
3033
3057
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -3368,7 +3392,7 @@ export function registerSessionCommand(program) {
|
|
|
3368
3392
|
.option("--name <name>", "Agent display name (legacy alias for --agent)")
|
|
3369
3393
|
.option(
|
|
3370
3394
|
"--agent <id>",
|
|
3371
|
-
"Granted agent id to
|
|
3395
|
+
"Granted agent id to register in the participant roster. Join state and onboarding are returned locally and never appended to the transcript.",
|
|
3372
3396
|
)
|
|
3373
3397
|
.option("--invite-token <token>", "Invitation token to accept before joining the session")
|
|
3374
3398
|
.option("--seat-key <key>", "Reserved session seat key to claim while accepting an invite")
|
|
@@ -3376,6 +3400,21 @@ export function registerSessionCommand(program) {
|
|
|
3376
3400
|
.option("--role <role>", "Agent role: coder, reviewer, tester, observer", "coder")
|
|
3377
3401
|
.option("--model <model>", "Agent model hint", "cli")
|
|
3378
3402
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3403
|
+
.option(
|
|
3404
|
+
"--goal <text>",
|
|
3405
|
+
"Request goal-and-scope admission: say what this agent is coming to do. A room owner approves it on the web; the agent then receives its own scoped, expiring identity.",
|
|
3406
|
+
)
|
|
3407
|
+
.option("--deliverable <text>", "An output this agent will produce (repeatable)", collectOption, [])
|
|
3408
|
+
.option("--stop-when <text>", "A condition that ends this agent's work (repeatable)", collectOption, [])
|
|
3409
|
+
.option(
|
|
3410
|
+
"--scope <actions>",
|
|
3411
|
+
"Comma-separated actions to request: session.read, session.post, session.react, tickets.read, tickets.work",
|
|
3412
|
+
)
|
|
3413
|
+
.option("--ttl <duration>", "How long the grant should last, e.g. 90m or 2h (5m..24h)", "2h")
|
|
3414
|
+
.option("--provider <name>", "Model provider hint for the admission request")
|
|
3415
|
+
.option("--no-wait", "Request admission and return immediately with the approval URL")
|
|
3416
|
+
.option("--wait-seconds <n>", "How long to wait for approval before returning pending", "900")
|
|
3417
|
+
.option("--cancel-admission", "Cancel this workspace's pending admission request for the session")
|
|
3379
3418
|
.option("--json", "Emit machine-readable output")
|
|
3380
3419
|
.action(async (sessionId, options, command) => {
|
|
3381
3420
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -3386,6 +3425,34 @@ export function registerSessionCommand(program) {
|
|
|
3386
3425
|
const explicitAgent = normalizeString(options.agent);
|
|
3387
3426
|
const legacyName = normalizeString(options.name);
|
|
3388
3427
|
const inviteToken = normalizeString(options.inviteToken);
|
|
3428
|
+
const emitJson = shouldEmitJson(options, command);
|
|
3429
|
+
// ONE requested identity for the whole join, validated before any side effect.
|
|
3430
|
+
// --name is the identity only when --agent is absent (its legacy alias); beside an
|
|
3431
|
+
// explicit --agent it is a free-text display label. Either way the identity is
|
|
3432
|
+
// used in its canonical spelling from here on, so the admission, its receipt,
|
|
3433
|
+
// the local registration and every later implicit command name the same agent.
|
|
3434
|
+
if (!explicitAgent) assertCanonicalAgentId(legacyName, "--name");
|
|
3435
|
+
const requestedAgent = canonicalAgentId(explicitAgent || legacyName);
|
|
3436
|
+
if (options.cancelAdmission) {
|
|
3437
|
+
const cancelled = await cancelAdmission(normalizedSessionId, {
|
|
3438
|
+
agentId: requestedAgent,
|
|
3439
|
+
targetPath,
|
|
3440
|
+
});
|
|
3441
|
+
const payload = {
|
|
3442
|
+
command: "session join",
|
|
3443
|
+
joined: false,
|
|
3444
|
+
sessionId: normalizedSessionId,
|
|
3445
|
+
admission: {
|
|
3446
|
+
status: cancelled?.status || "cancelled",
|
|
3447
|
+
admissionId: cancelled?.admissionId || null,
|
|
3448
|
+
},
|
|
3449
|
+
};
|
|
3450
|
+
if (emitJson) console.log(JSON.stringify(payload, null, 2));
|
|
3451
|
+
else console.log(`Admission request ${payload.admission.admissionId || ""} cancelled.`);
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
// Membership first. Only a member may ask for an agent to be admitted, so an
|
|
3455
|
+
// invitation is accepted BEFORE the admission request, never after.
|
|
3389
3456
|
let invitationAccept = null;
|
|
3390
3457
|
let invitationAcceptResult = null;
|
|
3391
3458
|
if (inviteToken) {
|
|
@@ -3393,7 +3460,7 @@ export function registerSessionCommand(program) {
|
|
|
3393
3460
|
targetPath,
|
|
3394
3461
|
invitationToken: inviteToken,
|
|
3395
3462
|
seatKey: options.seatKey,
|
|
3396
|
-
agentId:
|
|
3463
|
+
agentId: requestedAgent,
|
|
3397
3464
|
idempotencyKey: options.idempotencyKey,
|
|
3398
3465
|
});
|
|
3399
3466
|
invitationAcceptResult =
|
|
@@ -3401,6 +3468,55 @@ export function registerSessionCommand(program) {
|
|
|
3401
3468
|
? invitationAccept.result
|
|
3402
3469
|
: null;
|
|
3403
3470
|
}
|
|
3471
|
+
let admission = null;
|
|
3472
|
+
if (normalizeString(options.goal)) {
|
|
3473
|
+
const admissionAgent = requestedAgent;
|
|
3474
|
+
if (!admissionAgent) {
|
|
3475
|
+
throw new Error("--goal requires --agent <id>: admission is requested for one named agent.");
|
|
3476
|
+
}
|
|
3477
|
+
const waitSeconds = Number.parseInt(String(options.waitSeconds ?? "900"), 10);
|
|
3478
|
+
admission = await runAdmissionJoin(normalizedSessionId, {
|
|
3479
|
+
agentId: admissionAgent,
|
|
3480
|
+
displayName: legacyName && explicitAgent ? legacyName : "",
|
|
3481
|
+
model: options.model,
|
|
3482
|
+
provider: options.provider,
|
|
3483
|
+
goal: options.goal,
|
|
3484
|
+
deliverables: options.deliverable || [],
|
|
3485
|
+
stopConditions: options.stopWhen || [],
|
|
3486
|
+
actions: parseAdmissionScope(options.scope),
|
|
3487
|
+
ttlSeconds: parseAdmissionTtlSeconds(options.ttl),
|
|
3488
|
+
wait: options.wait !== false,
|
|
3489
|
+
waitTimeoutMs: (Number.isFinite(waitSeconds) && waitSeconds >= 0 ? waitSeconds : 900) * 1000,
|
|
3490
|
+
targetPath,
|
|
3491
|
+
onPending: ({ approveUrl }) => {
|
|
3492
|
+
// stderr, so --json stdout stays a single parseable document.
|
|
3493
|
+
const line = `Waiting for a room owner to approve this agent: ${approveUrl}`;
|
|
3494
|
+
if (emitJson) process.stderr.write(`${line}\n`);
|
|
3495
|
+
else console.log(pc.yellow(line));
|
|
3496
|
+
},
|
|
3497
|
+
});
|
|
3498
|
+
if (admission.status !== "active") {
|
|
3499
|
+
// Only an ACTIVE admission joins. Pending, denied, cancelled or expired never
|
|
3500
|
+
// registers the agent or materializes the room for it.
|
|
3501
|
+
process.exitCode = admission.status === "pending" ? 3 : 4;
|
|
3502
|
+
const payload = {
|
|
3503
|
+
command: "session join",
|
|
3504
|
+
joined: false,
|
|
3505
|
+
sessionId: normalizedSessionId,
|
|
3506
|
+
agentId: admissionAgent,
|
|
3507
|
+
invitationAccepted: Boolean(invitationAcceptResult),
|
|
3508
|
+
admission,
|
|
3509
|
+
};
|
|
3510
|
+
if (emitJson) {
|
|
3511
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3512
|
+
} else if (admission.status === "pending") {
|
|
3513
|
+
console.log(pc.yellow(`Admission pending. A room owner approves it here: ${admission.approveUrl || "(see the session page)"}`));
|
|
3514
|
+
} else {
|
|
3515
|
+
console.log(pc.red(`Admission ${admission.status}. The agent was not joined.`));
|
|
3516
|
+
}
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3404
3520
|
|
|
3405
3521
|
// PR #483 contract: verify the session exists and the caller has access
|
|
3406
3522
|
// BEFORE materializing local cache state. Without this we'd silently
|
|
@@ -3412,7 +3528,48 @@ export function registerSessionCommand(program) {
|
|
|
3412
3528
|
// before the invite is accepted the user may legitimately receive 403,
|
|
3413
3529
|
// so --invite-token performs the guarded accept mutation first and this
|
|
3414
3530
|
// verification proves membership immediately afterward.
|
|
3415
|
-
|
|
3531
|
+
// An admitted agent proves its grant is live with ITS OWN credential, via the
|
|
3532
|
+
// grant's receipt, and never with the human token. A cached "active" is not
|
|
3533
|
+
// reported until this succeeds (a revoked grant is not joined), and nothing
|
|
3534
|
+
// about the room is fetched on the human's authority.
|
|
3535
|
+
const admittedAgentId = admission ? requestedAgent : "";
|
|
3536
|
+
let admissionReceipt = null;
|
|
3537
|
+
if (admission) {
|
|
3538
|
+
try {
|
|
3539
|
+
admissionReceipt = await withAgentAdmission(normalizedSessionId, admittedAgentId, () =>
|
|
3540
|
+
fetchOwnAdmissionReceipt(normalizedSessionId, { targetPath }),
|
|
3541
|
+
);
|
|
3542
|
+
// The grant must be for EXACTLY the identity this join registers, byte for
|
|
3543
|
+
// byte: a receipt for any other spelling is a different agent's authority.
|
|
3544
|
+
const receiptAgent = String(admissionReceipt?.agentId ?? "");
|
|
3545
|
+
if (receiptAgent !== admittedAgentId) {
|
|
3546
|
+
throw new Error(
|
|
3547
|
+
`the admission is for agent "${receiptAgent || "(none)"}", not "${admittedAgentId}"`,
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
} catch (error) {
|
|
3551
|
+
process.exitCode = 4;
|
|
3552
|
+
const payload = {
|
|
3553
|
+
command: "session join",
|
|
3554
|
+
joined: false,
|
|
3555
|
+
sessionId: normalizedSessionId,
|
|
3556
|
+
agentId: admittedAgentId,
|
|
3557
|
+
admission: {
|
|
3558
|
+
...admission,
|
|
3559
|
+
status: "not_accepted",
|
|
3560
|
+
verified: false,
|
|
3561
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3562
|
+
},
|
|
3563
|
+
};
|
|
3564
|
+
if (emitJson) console.log(JSON.stringify(payload, null, 2));
|
|
3565
|
+
else console.log(pc.red(`The API no longer accepts this agent's admission; it was not joined.`));
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
admission = { ...admission, verified: true, receipt: admissionReceipt };
|
|
3569
|
+
}
|
|
3570
|
+
const verification = admission
|
|
3571
|
+
? { ok: true, source: "admission_receipt", session: { sessionId: normalizedSessionId } }
|
|
3572
|
+
: await verifyRemoteSession(normalizedSessionId, { targetPath });
|
|
3416
3573
|
if (!verification.ok) {
|
|
3417
3574
|
if (verification.status === 404 || verification.reason === "not_found") {
|
|
3418
3575
|
throw new Error(
|
|
@@ -3439,17 +3596,31 @@ export function registerSessionCommand(program) {
|
|
|
3439
3596
|
skipRemoteProbe: true,
|
|
3440
3597
|
remoteSession,
|
|
3441
3598
|
});
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3599
|
+
// History only for a grant that may read it, and then on the admission.
|
|
3600
|
+
const joinHydration = !admission
|
|
3601
|
+
? await hydrateJoinBriefingContext(normalizedSessionId, { targetPath })
|
|
3602
|
+
: (admissionReceipt?.actions || []).includes("session.read")
|
|
3603
|
+
? await withAgentAdmission(normalizedSessionId, admittedAgentId, () =>
|
|
3604
|
+
hydrateJoinBriefingContext(normalizedSessionId, { targetPath }),
|
|
3605
|
+
)
|
|
3606
|
+
: { skipped: true, reason: "the grant does not include session.read" };
|
|
3445
3607
|
|
|
3446
3608
|
const acceptedOnboarding = normalizeSessionOnboarding(
|
|
3447
3609
|
invitationAcceptResult?.onboarding,
|
|
3448
3610
|
invitationAcceptResult?.claimedSeat,
|
|
3449
3611
|
);
|
|
3450
3612
|
const acceptedAgentId = normalizeString(acceptedOnboarding?.agentId);
|
|
3451
|
-
|
|
3613
|
+
// An admitted join registers the admitted identity and nothing else. Otherwise an
|
|
3614
|
+
// invitation's onboarding agent id is the identity only when none was given, and
|
|
3615
|
+
// it is held to the same canonical rule before anything is registered locally.
|
|
3616
|
+
if (!admission && !explicitAgent && acceptedAgentId) {
|
|
3617
|
+
assertCanonicalAgentId(acceptedAgentId, "the invitation's agent id");
|
|
3618
|
+
}
|
|
3619
|
+
const agentSeed = admission ? admittedAgentId : explicitAgent || acceptedAgentId || legacyName;
|
|
3452
3620
|
const resolvedAgentId = await defaultAgentId(agentSeed, targetPath);
|
|
3621
|
+
if (admission && resolvedAgentId !== admittedAgentId) {
|
|
3622
|
+
throw new Error(`join would register "${resolvedAgentId}", not the admitted agent "${admittedAgentId}".`);
|
|
3623
|
+
}
|
|
3453
3624
|
const roleWasExplicit = optionWasSetByCli(command, "role");
|
|
3454
3625
|
const role =
|
|
3455
3626
|
normalizeString(roleWasExplicit ? options.role : acceptedOnboarding?.role || options.role) ||
|
|
@@ -3457,13 +3628,9 @@ export function registerSessionCommand(program) {
|
|
|
3457
3628
|
const model = normalizeString(options.model) || "cli";
|
|
3458
3629
|
const hasConcreteAgentIdentity = Boolean(explicitAgent || acceptedAgentId);
|
|
3459
3630
|
|
|
3460
|
-
//
|
|
3461
|
-
//
|
|
3462
|
-
//
|
|
3463
|
-
// the spec calls for when `--agent <granted>` or an accepted reserved
|
|
3464
|
-
// seat provides a concrete agent id. We don't double-emit; we record
|
|
3465
|
-
// whether a durable agent identity path was used
|
|
3466
|
-
// so the JSON output can advertise it to callers (and tests).
|
|
3631
|
+
// Registration updates the local participant projection and returns
|
|
3632
|
+
// onboarding directly to this process. It never writes join, identity,
|
|
3633
|
+
// or coaching rows into the transcript.
|
|
3467
3634
|
const joined = await registerAgent(normalizedSessionId, {
|
|
3468
3635
|
targetPath,
|
|
3469
3636
|
agentId: resolvedAgentId,
|
|
@@ -3474,13 +3641,12 @@ export function registerSessionCommand(program) {
|
|
|
3474
3641
|
trackProcessExit: false,
|
|
3475
3642
|
awaitRemoteSync: hasConcreteAgentIdentity,
|
|
3476
3643
|
});
|
|
3477
|
-
const agentJoinRelayed =
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
!resolvedAgentId.startsWith("human-");
|
|
3644
|
+
const agentJoinRelayed = false;
|
|
3645
|
+
const localBriefingPayload =
|
|
3646
|
+
joined.onboarding?.contextBriefing?.event?.payload &&
|
|
3647
|
+
typeof joined.onboarding.contextBriefing.event.payload === "object"
|
|
3648
|
+
? joined.onboarding.contextBriefing.event.payload
|
|
3649
|
+
: null;
|
|
3484
3650
|
const onboardingBrief = await writeSessionOnboardingBrief(normalizedSessionId, {
|
|
3485
3651
|
targetPath,
|
|
3486
3652
|
onboarding: acceptedOnboarding,
|
|
@@ -3521,6 +3687,17 @@ export function registerSessionCommand(program) {
|
|
|
3521
3687
|
agentCount: Number.isFinite(agentCount) ? agentCount : 0,
|
|
3522
3688
|
lastActivityAt: lastActivityIso || null,
|
|
3523
3689
|
agentJoinRelayed,
|
|
3690
|
+
onboardingBriefing: localBriefingPayload
|
|
3691
|
+
? {
|
|
3692
|
+
forAgent: localBriefingPayload.forAgent || joined.agentId,
|
|
3693
|
+
message: localBriefingPayload.message || "",
|
|
3694
|
+
recap: localBriefingPayload.recap || "",
|
|
3695
|
+
rules: localBriefingPayload.rules || null,
|
|
3696
|
+
generatedAt: localBriefingPayload.generatedAt || null,
|
|
3697
|
+
delivery: "joining_process",
|
|
3698
|
+
persisted: false,
|
|
3699
|
+
}
|
|
3700
|
+
: null,
|
|
3524
3701
|
invitationAccepted: Boolean(invitationAcceptResult?.ok || invitationAccept),
|
|
3525
3702
|
invitationAccept: invitationAccept
|
|
3526
3703
|
? {
|
|
@@ -3536,6 +3713,7 @@ export function registerSessionCommand(program) {
|
|
|
3536
3713
|
jsonPath: onboardingBrief.jsonPath,
|
|
3537
3714
|
}
|
|
3538
3715
|
: null,
|
|
3716
|
+
admission,
|
|
3539
3717
|
};
|
|
3540
3718
|
if (shouldEmitJson(options, command)) {
|
|
3541
3719
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -3556,9 +3734,29 @@ export function registerSessionCommand(program) {
|
|
|
3556
3734
|
console.log(pc.green(`Invitation accepted${seatLabel}`));
|
|
3557
3735
|
}
|
|
3558
3736
|
console.log(pc.gray(`agent=${joined.agentId} role=${joined.role} model=${joined.model}`));
|
|
3737
|
+
if (admission) {
|
|
3738
|
+
if (admission.status === "active") {
|
|
3739
|
+
const expires = new Date(admission.grant.expiresAt * 1000).toISOString();
|
|
3740
|
+
console.log(
|
|
3741
|
+
pc.green(
|
|
3742
|
+
`Admitted${admission.reused ? " (existing grant)" : ""}: ${admission.grant.actions.join(", ")} until ${expires}`,
|
|
3743
|
+
),
|
|
3744
|
+
);
|
|
3745
|
+
if (admission.identity?.subject) console.log(pc.gray(`identity=${admission.identity.subject}`));
|
|
3746
|
+
console.log(pc.gray(`credential stored at ${admission.credential.storage} (never printed)`));
|
|
3747
|
+
} else if (admission.status === "pending") {
|
|
3748
|
+
console.log(pc.yellow(`Admission pending approval: ${admission.approveUrl}`));
|
|
3749
|
+
console.log(pc.gray("Re-run the same command to resume waiting."));
|
|
3750
|
+
} else {
|
|
3751
|
+
console.log(pc.red(`Admission ${admission.status}.`));
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3559
3754
|
if (payload.onboardingGuide?.markdownPath) {
|
|
3560
3755
|
console.log(pc.gray(`onboarding=${payload.onboardingGuide.markdownPath}`));
|
|
3561
3756
|
}
|
|
3757
|
+
if (payload.onboardingBriefing?.recap) {
|
|
3758
|
+
console.log(payload.onboardingBriefing.recap);
|
|
3759
|
+
}
|
|
3562
3760
|
});
|
|
3563
3761
|
|
|
3564
3762
|
session
|
|
@@ -3592,7 +3790,7 @@ export function registerSessionCommand(program) {
|
|
|
3592
3790
|
.option("--local-only", "Append only to the local session cache without remote send confirmation")
|
|
3593
3791
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3594
3792
|
.option("--json", "Emit machine-readable output")
|
|
3595
|
-
.action(async (sessionId, messageParts, options, command) => {
|
|
3793
|
+
.action(asAdmittedAgent(sessionSayAgent, async (sessionId, messageParts, options, command) => {
|
|
3596
3794
|
const normalizedSessionId = normalizeString(sessionId);
|
|
3597
3795
|
if (!normalizedSessionId) {
|
|
3598
3796
|
throw new Error("session id is required.");
|
|
@@ -3720,7 +3918,7 @@ export function registerSessionCommand(program) {
|
|
|
3720
3918
|
console.error(pc.yellow(`Identity warning: ${identity.identityWarning}`));
|
|
3721
3919
|
}
|
|
3722
3920
|
console.log(formatEventLine(persisted));
|
|
3723
|
-
});
|
|
3921
|
+
}));
|
|
3724
3922
|
|
|
3725
3923
|
session
|
|
3726
3924
|
.command("post-agent <sessionId> <message>")
|
|
@@ -4010,7 +4208,20 @@ export function registerSessionCommand(program) {
|
|
|
4010
4208
|
console.log(formatEventLine(persisted));
|
|
4011
4209
|
});
|
|
4012
4210
|
|
|
4013
|
-
|
|
4211
|
+
// Every message-action command (react, reply, ack, ...) runs as its authoring
|
|
4212
|
+
// agent: inside that agent's admission when this machine holds one.
|
|
4213
|
+
async function runMessageActionCommand(args = {}) {
|
|
4214
|
+
const sid = normalizeString(args.sessionId);
|
|
4215
|
+
const identity = await resolveMessageActionIdentity({
|
|
4216
|
+
sessionId: sid,
|
|
4217
|
+
optionAgent: args.options?.agent,
|
|
4218
|
+
targetPath: path.resolve(process.cwd(), String(args.options?.path || ".")),
|
|
4219
|
+
env: process.env,
|
|
4220
|
+
});
|
|
4221
|
+
return withAgentAdmission(sid, identity.agentId, () => runMessageActionCommandUnscoped(args));
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
async function runMessageActionCommandUnscoped({
|
|
4014
4225
|
sessionId,
|
|
4015
4226
|
actionType,
|
|
4016
4227
|
options,
|
|
@@ -4082,10 +4293,23 @@ export function registerSessionCommand(program) {
|
|
|
4082
4293
|
if (!result.ok || !result.action) {
|
|
4083
4294
|
throw new Error(`Session action failed (${result.reason || "unknown"}).`);
|
|
4084
4295
|
}
|
|
4085
|
-
const
|
|
4086
|
-
const
|
|
4087
|
-
|
|
4088
|
-
|
|
4296
|
+
const isReadCursorProjection = normalizedActionType === "view";
|
|
4297
|
+
const actionEvent = isReadCursorProjection
|
|
4298
|
+
? null
|
|
4299
|
+
: buildSessionActionEvent(normalizedSessionId, result.action);
|
|
4300
|
+
const localAppend = isReadCursorProjection
|
|
4301
|
+
? { appended: false, reason: "read_cursor_projection", event: null }
|
|
4302
|
+
: await appendActionEventIfMissing(normalizedSessionId, actionEvent, {
|
|
4303
|
+
targetPath,
|
|
4304
|
+
});
|
|
4305
|
+
const readCursorProjection = isReadCursorProjection
|
|
4306
|
+
? {
|
|
4307
|
+
updated: Boolean(result.updated),
|
|
4308
|
+
lastReadSequenceId:
|
|
4309
|
+
Number(result.lastReadSequenceId || result.action.targetSequenceId || 0) || null,
|
|
4310
|
+
targetCursor: targetCursor || null,
|
|
4311
|
+
}
|
|
4312
|
+
: null;
|
|
4089
4313
|
const payload = {
|
|
4090
4314
|
command: commandName,
|
|
4091
4315
|
targetPath,
|
|
@@ -4098,11 +4322,22 @@ export function registerSessionCommand(program) {
|
|
|
4098
4322
|
appended: Boolean(localAppend.appended),
|
|
4099
4323
|
reason: localAppend.reason || "",
|
|
4100
4324
|
},
|
|
4325
|
+
...(readCursorProjection ? { readCursorProjection } : {}),
|
|
4101
4326
|
};
|
|
4102
4327
|
if (shouldEmitJson(options, command)) {
|
|
4103
4328
|
console.log(JSON.stringify(payload, null, 2));
|
|
4104
4329
|
return payload;
|
|
4105
4330
|
}
|
|
4331
|
+
if (readCursorProjection) {
|
|
4332
|
+
const targetLabel = readCursorProjection.lastReadSequenceId
|
|
4333
|
+
? `#${readCursorProjection.lastReadSequenceId}`
|
|
4334
|
+
: readCursorProjection.targetCursor || "target";
|
|
4335
|
+
const status = readCursorProjection.updated ? "Advanced" : "Confirmed";
|
|
4336
|
+
console.log(
|
|
4337
|
+
pc.green(`${status} read cursor through ${targetLabel}; no transcript event appended.`),
|
|
4338
|
+
);
|
|
4339
|
+
return payload;
|
|
4340
|
+
}
|
|
4106
4341
|
if (localAppend.event || actionEvent) {
|
|
4107
4342
|
console.log(formatEventLine(localAppend.event || actionEvent));
|
|
4108
4343
|
} else {
|
|
@@ -4217,20 +4452,50 @@ export function registerSessionCommand(program) {
|
|
|
4217
4452
|
});
|
|
4218
4453
|
|
|
4219
4454
|
session
|
|
4220
|
-
.command("
|
|
4221
|
-
.description("
|
|
4222
|
-
.option("--agent <id>", "
|
|
4223
|
-
.option("--
|
|
4455
|
+
.command("edit <sessionId> <target> <message...>")
|
|
4456
|
+
.description("Edit your message by sequence or threaded reply UUID; preserves its audit history")
|
|
4457
|
+
.option("--agent <id>", "Authoring agent (defaults to the joined session agent)")
|
|
4458
|
+
.option("--expected-revision <n>", "Guard against concurrent edits; otherwise fetch the current revision once")
|
|
4459
|
+
.option("--idempotency-key <key>", "Reuse an explicit key when retrying the same edit")
|
|
4224
4460
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4225
4461
|
.option("--json", "Emit machine-readable output")
|
|
4226
|
-
.action(async (sessionId,
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4462
|
+
.action(async (sessionId, target, messageParts, options, command) => {
|
|
4463
|
+
const sid = normalizeString(sessionId);
|
|
4464
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4465
|
+
const text = Array.isArray(messageParts) ? messageParts.join(" ") : String(messageParts || "");
|
|
4466
|
+
const sequence = /^\d+$/.test(target) ? Number(target) : null;
|
|
4467
|
+
if (sequence !== null && (!Number.isSafeInteger(sequence) || sequence <= 0)) throw new Error("target sequence must be a positive safe integer.");
|
|
4468
|
+
if (!sequence && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(target)) {
|
|
4469
|
+
throw new Error("Edit target must be a positive message sequence or a threaded reply UUID.");
|
|
4470
|
+
}
|
|
4471
|
+
const identity = await resolveMessageActionIdentity({ sessionId: sid, optionAgent: options.agent, targetPath, env: process.env });
|
|
4472
|
+
if (shouldBlockImplicitCliUserSessionSay(identity)) throw new Error(identity.identityWarning || "session edit requires an authoring agent identity; pass --agent <id>.");
|
|
4473
|
+
const expectedRevision = options.expectedRevision === undefined ? null : Number(options.expectedRevision);
|
|
4474
|
+
if (expectedRevision !== null && (!Number.isSafeInteger(expectedRevision) || expectedRevision <= 0)) throw new Error("expected-revision must be a positive safe integer.");
|
|
4475
|
+
return withAgentAdmission(sid, identity.agentId, async () => {
|
|
4476
|
+
const result = await editSessionMessage(sid, {
|
|
4477
|
+
targetPath,
|
|
4478
|
+
targetSequenceId: sequence,
|
|
4479
|
+
targetActionId: sequence ? "" : target,
|
|
4480
|
+
text,
|
|
4481
|
+
agentId: identity.agentId,
|
|
4482
|
+
expectedRevision,
|
|
4483
|
+
idempotencyKey: options.idempotencyKey,
|
|
4484
|
+
timeoutMs: 15_000,
|
|
4485
|
+
});
|
|
4486
|
+
if (!result.ok) throw new Error(`Session edit failed (${result.reason || "unknown"}). No local message was changed.`);
|
|
4487
|
+
let localAppend = { appended: 0, upgraded: 0, failed: 0 };
|
|
4488
|
+
try {
|
|
4489
|
+
await ensureLocalSessionForRemoteCommand(sid, { targetPath });
|
|
4490
|
+
const current = result.reply ? replyRevisionEvent(sid, result.reply) : result.event;
|
|
4491
|
+
localAppend = await appendMissingRemoteEvents(sid, [current, result.notification].filter(Boolean), { targetPath });
|
|
4492
|
+
} catch {
|
|
4493
|
+
localAppend = { appended: 0, upgraded: 0, failed: 1 };
|
|
4494
|
+
}
|
|
4495
|
+
const payload = { command: "session edit", sessionId: sid, target, ...result, localAppend, localAppendComplete: localAppend.failed === 0 };
|
|
4496
|
+
if (shouldEmitJson(options, command)) console.log(JSON.stringify(payload, null, 2));
|
|
4497
|
+
else console.log(`${result.changed ? "Edited" : "Confirmed"} ${sequence ? `#${sequence}` : `reply ${target}`} at revision ${messageRevision(result.reply || result.event)}.${localAppend.failed ? " Remote edit succeeded; local cache needs sync." : ""}`);
|
|
4498
|
+
return payload;
|
|
4234
4499
|
});
|
|
4235
4500
|
});
|
|
4236
4501
|
|
|
@@ -4319,15 +4584,17 @@ export function registerSessionCommand(program) {
|
|
|
4319
4584
|
if (verb === "lock") {
|
|
4320
4585
|
const result = await lockFile(normalizedSessionId, identity.agentId, file, {
|
|
4321
4586
|
intent,
|
|
4587
|
+
ttlSeconds: options.ttl,
|
|
4322
4588
|
targetPath,
|
|
4323
|
-
awaitRemoteSync: true,
|
|
4324
4589
|
});
|
|
4325
4590
|
const record = {
|
|
4326
4591
|
file: result.file || file,
|
|
4327
4592
|
locked: Boolean(result.locked),
|
|
4328
4593
|
reason: result.reason || (result.locked ? "locked" : "held_by_other_agent"),
|
|
4329
4594
|
heldBy: result.heldBy || null,
|
|
4595
|
+
intent: result.lock?.intent || result.lease?.intent || null,
|
|
4330
4596
|
since: result.since || null,
|
|
4597
|
+
expiresAt: result.lock?.expiresAt || result.lease?.expiresAt || null,
|
|
4331
4598
|
};
|
|
4332
4599
|
results.push(record);
|
|
4333
4600
|
if (result.locked) {
|
|
@@ -4341,7 +4608,6 @@ export function registerSessionCommand(program) {
|
|
|
4341
4608
|
const result = await unlockFile(normalizedSessionId, identity.agentId, file, {
|
|
4342
4609
|
reason: intent || "manual_release",
|
|
4343
4610
|
targetPath,
|
|
4344
|
-
awaitRemoteSync: true,
|
|
4345
4611
|
});
|
|
4346
4612
|
const record = {
|
|
4347
4613
|
file: result.file || file,
|
|
@@ -4349,6 +4615,7 @@ export function registerSessionCommand(program) {
|
|
|
4349
4615
|
reason: result.reason || (result.unlocked ? "unlocked" : "not_locked"),
|
|
4350
4616
|
heldBy: result.heldBy || null,
|
|
4351
4617
|
since: result.since || null,
|
|
4618
|
+
expiresAt: result.lock?.expiresAt || result.lease?.expiresAt || null,
|
|
4352
4619
|
};
|
|
4353
4620
|
results.push(record);
|
|
4354
4621
|
if (result.unlocked) {
|
|
@@ -4360,12 +4627,6 @@ export function registerSessionCommand(program) {
|
|
|
4360
4627
|
}
|
|
4361
4628
|
}
|
|
4362
4629
|
|
|
4363
|
-
if (failed.length > 0) {
|
|
4364
|
-
const summary = failed
|
|
4365
|
-
.map((item) => `${item.file}${item.heldBy ? ` held by ${item.heldBy}` : ""}`)
|
|
4366
|
-
.join(", ");
|
|
4367
|
-
throw new Error(`session ${verb} failed for ${summary}`);
|
|
4368
|
-
}
|
|
4369
4630
|
const payload = {
|
|
4370
4631
|
command: `session ${verb}`,
|
|
4371
4632
|
sessionId: normalizedSessionId,
|
|
@@ -4373,16 +4634,38 @@ export function registerSessionCommand(program) {
|
|
|
4373
4634
|
files: processed,
|
|
4374
4635
|
results,
|
|
4375
4636
|
skipped,
|
|
4637
|
+
failed,
|
|
4376
4638
|
};
|
|
4377
4639
|
if (shouldEmitJson(options, command)) {
|
|
4378
4640
|
console.log(JSON.stringify(payload, null, 2));
|
|
4641
|
+
if (failed.length > 0) {
|
|
4642
|
+
process.exitCode = 2;
|
|
4643
|
+
}
|
|
4644
|
+
return payload;
|
|
4645
|
+
}
|
|
4646
|
+
if (failed.length > 0) {
|
|
4647
|
+
for (const item of failed) {
|
|
4648
|
+
const details = [
|
|
4649
|
+
item.heldBy ? `held by ${item.heldBy}` : "",
|
|
4650
|
+
item.intent ? `intent: ${item.intent}` : "",
|
|
4651
|
+
item.expiresAt ? `expires ${item.expiresAt}` : "",
|
|
4652
|
+
].filter(Boolean);
|
|
4653
|
+
console.error(
|
|
4654
|
+
pc.red(
|
|
4655
|
+
`Blocked ${item.file}: ${item.reason}${details.length > 0 ? ` (${details.join(", ")})` : ""}`,
|
|
4656
|
+
),
|
|
4657
|
+
);
|
|
4658
|
+
}
|
|
4659
|
+
process.exitCode = 2;
|
|
4379
4660
|
return payload;
|
|
4380
4661
|
}
|
|
4381
|
-
const action = verb === "lock" ? "
|
|
4662
|
+
const action = verb === "lock" ? "Acquired lease for" : "Released";
|
|
4382
4663
|
console.log(pc.green(`${action} ${processed.length} file(s) as ${identity.agentId}: ${processed.join(", ")}`));
|
|
4383
4664
|
if (verb === "lock") {
|
|
4384
4665
|
console.log(
|
|
4385
|
-
pc.gray(
|
|
4666
|
+
pc.gray(
|
|
4667
|
+
"The SentinelLayer API is authoritative; leases expire by TTL and can be released with `sl session unlock`.",
|
|
4668
|
+
),
|
|
4386
4669
|
);
|
|
4387
4670
|
}
|
|
4388
4671
|
return payload;
|
|
@@ -4390,8 +4673,9 @@ export function registerSessionCommand(program) {
|
|
|
4390
4673
|
|
|
4391
4674
|
session
|
|
4392
4675
|
.command("lock <sessionId> <files...>")
|
|
4393
|
-
.description("Claim exclusive file
|
|
4676
|
+
.description("Claim exclusive API-backed file leases (fail-closed, TTL expiry)")
|
|
4394
4677
|
.option("--intent <text>", "Why you're locking these files (shown to peers)")
|
|
4678
|
+
.option("--ttl <seconds>", "Lease TTL in seconds (15-3600)", "300")
|
|
4395
4679
|
.option("--agent <id>", "Agent id claiming the lock (defaults to the joined session agent)")
|
|
4396
4680
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4397
4681
|
.option("--json", "Emit machine-readable output")
|
|
@@ -4401,7 +4685,7 @@ export function registerSessionCommand(program) {
|
|
|
4401
4685
|
|
|
4402
4686
|
session
|
|
4403
4687
|
.command("unlock <sessionId> <files...>")
|
|
4404
|
-
.description("Release file
|
|
4688
|
+
.description("Release API-backed file leases you hold")
|
|
4405
4689
|
.option("--intent <text>", "Optional note on the release")
|
|
4406
4690
|
.option("--agent <id>", "Agent id releasing the lock (defaults to the joined session agent)")
|
|
4407
4691
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
@@ -4410,9 +4694,264 @@ export function registerSessionCommand(program) {
|
|
|
4410
4694
|
await runFileLockCommand("unlock", sessionId, files, options, command);
|
|
4411
4695
|
});
|
|
4412
4696
|
|
|
4697
|
+
session
|
|
4698
|
+
.command("renew <sessionId> <files...>")
|
|
4699
|
+
.description("Renew holder-bound file leases without writing session events")
|
|
4700
|
+
.option("--ttl <seconds>", "Renewed lease TTL in seconds (15-3600)", "300")
|
|
4701
|
+
.option("--agent <id>", "Agent id renewing the lease (defaults to the joined session agent)")
|
|
4702
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
4703
|
+
.option("--json", "Emit machine-readable output")
|
|
4704
|
+
.action(async (sessionId, files, options, command) => {
|
|
4705
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
4706
|
+
if (!normalizedSessionId) {
|
|
4707
|
+
throw new Error("session id is required.");
|
|
4708
|
+
}
|
|
4709
|
+
const fileList = (Array.isArray(files) ? files : [files])
|
|
4710
|
+
.map((file) => normalizeString(file))
|
|
4711
|
+
.filter(Boolean);
|
|
4712
|
+
if (fileList.length === 0) {
|
|
4713
|
+
throw new Error("session renew requires at least one file path.");
|
|
4714
|
+
}
|
|
4715
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4716
|
+
await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
|
|
4717
|
+
const identity = await resolveMessageActionIdentity({
|
|
4718
|
+
sessionId: normalizedSessionId,
|
|
4719
|
+
optionAgent: options.agent,
|
|
4720
|
+
targetPath,
|
|
4721
|
+
env: process.env,
|
|
4722
|
+
});
|
|
4723
|
+
if (shouldBlockImplicitCliUserSessionSay(identity)) {
|
|
4724
|
+
throw new Error(
|
|
4725
|
+
identity.identityWarning ||
|
|
4726
|
+
"session renew requires an agent identity; pass --agent <id>.",
|
|
4727
|
+
);
|
|
4728
|
+
}
|
|
4729
|
+
const results = [];
|
|
4730
|
+
for (const file of fileList) {
|
|
4731
|
+
results.push(
|
|
4732
|
+
await renewFileLease(normalizedSessionId, identity.agentId, file, {
|
|
4733
|
+
ttlSeconds: options.ttl,
|
|
4734
|
+
targetPath,
|
|
4735
|
+
}),
|
|
4736
|
+
);
|
|
4737
|
+
}
|
|
4738
|
+
const payload = {
|
|
4739
|
+
command: "session renew",
|
|
4740
|
+
sessionId: normalizedSessionId,
|
|
4741
|
+
agentId: identity.agentId,
|
|
4742
|
+
allowed: results.every((result) => result.renewed),
|
|
4743
|
+
results,
|
|
4744
|
+
};
|
|
4745
|
+
if (shouldEmitJson(options, command)) {
|
|
4746
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4747
|
+
} else if (payload.allowed) {
|
|
4748
|
+
console.log(
|
|
4749
|
+
pc.green(`Renewed ${results.length} file lease(s) as ${identity.agentId}.`),
|
|
4750
|
+
);
|
|
4751
|
+
} else {
|
|
4752
|
+
console.error(
|
|
4753
|
+
pc.red("One or more file leases could not be renewed; edit access is not assured."),
|
|
4754
|
+
);
|
|
4755
|
+
}
|
|
4756
|
+
if (!payload.allowed) {
|
|
4757
|
+
process.exitCode = 2;
|
|
4758
|
+
}
|
|
4759
|
+
return payload;
|
|
4760
|
+
});
|
|
4761
|
+
|
|
4762
|
+
session
|
|
4763
|
+
.command("guard <sessionId> <files...>")
|
|
4764
|
+
.description("Fail-closed authoritative preflight for file edits")
|
|
4765
|
+
.requiredOption("--agent <id>", "Agent id presenting holder capabilities")
|
|
4766
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
4767
|
+
.option("--json", "Emit machine-readable output")
|
|
4768
|
+
.action(async (sessionId, files, options, command) => {
|
|
4769
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
4770
|
+
if (!normalizedSessionId) {
|
|
4771
|
+
throw new Error("session id is required.");
|
|
4772
|
+
}
|
|
4773
|
+
const fileList = (Array.isArray(files) ? files : [files])
|
|
4774
|
+
.map((file) => normalizeString(file))
|
|
4775
|
+
.filter(Boolean);
|
|
4776
|
+
if (fileList.length === 0) {
|
|
4777
|
+
throw new Error("session guard requires at least one file path.");
|
|
4778
|
+
}
|
|
4779
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4780
|
+
const requestedAgentId = normalizeString(options.agent).toLowerCase();
|
|
4781
|
+
let result;
|
|
4782
|
+
try {
|
|
4783
|
+
result = await guardFileLeases(
|
|
4784
|
+
normalizedSessionId,
|
|
4785
|
+
requestedAgentId,
|
|
4786
|
+
fileList,
|
|
4787
|
+
{ targetPath },
|
|
4788
|
+
);
|
|
4789
|
+
} catch (error) {
|
|
4790
|
+
const message = safeFileLeaseErrorMessage(error);
|
|
4791
|
+
result = {
|
|
4792
|
+
ok: false,
|
|
4793
|
+
allowed: false,
|
|
4794
|
+
authoritative: false,
|
|
4795
|
+
sessionId: normalizedSessionId,
|
|
4796
|
+
holderId: requestedAgentId || null,
|
|
4797
|
+
checkedAt: new Date().toISOString(),
|
|
4798
|
+
validUntil: null,
|
|
4799
|
+
files: fileList,
|
|
4800
|
+
guarded: [],
|
|
4801
|
+
denials: fileList.map((file) => ({
|
|
4802
|
+
path: file,
|
|
4803
|
+
reason: "guard_failed",
|
|
4804
|
+
})),
|
|
4805
|
+
error: {
|
|
4806
|
+
code: normalizeString(error?.code) || "FILE_LEASE_GUARD_FAILED",
|
|
4807
|
+
message,
|
|
4808
|
+
},
|
|
4809
|
+
};
|
|
4810
|
+
}
|
|
4811
|
+
const payload = {
|
|
4812
|
+
command: "session guard",
|
|
4813
|
+
...result,
|
|
4814
|
+
};
|
|
4815
|
+
if (shouldEmitJson(options, command)) {
|
|
4816
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4817
|
+
} else if (result.allowed) {
|
|
4818
|
+
console.log(
|
|
4819
|
+
pc.green(`File-lease guard allowed ${fileList.length} edit target(s).`),
|
|
4820
|
+
);
|
|
4821
|
+
} else {
|
|
4822
|
+
if (result.error?.message) {
|
|
4823
|
+
console.error(pc.red(`Authoritative guard failed: ${result.error.message}`));
|
|
4824
|
+
}
|
|
4825
|
+
if ((result.denials || []).length > 0) {
|
|
4826
|
+
console.error(
|
|
4827
|
+
pc.red(`Edit blocked: ${formatFileLeaseDenialSummary(result)}`),
|
|
4828
|
+
);
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
if (!result.allowed) {
|
|
4832
|
+
process.exitCode = 2;
|
|
4833
|
+
}
|
|
4834
|
+
return payload;
|
|
4835
|
+
});
|
|
4836
|
+
|
|
4837
|
+
session
|
|
4838
|
+
.command("guard-hook <sessionId>")
|
|
4839
|
+
.description("Claude PreToolUse hook: read tool JSON on stdin and block denied edits")
|
|
4840
|
+
.requiredOption("--agent <id>", "Agent id presenting holder capabilities")
|
|
4841
|
+
.option("--path <path>", "Workspace root", ".")
|
|
4842
|
+
.action(async (sessionId, options) => {
|
|
4843
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4844
|
+
const result = await runFileLeaseGuardHook({
|
|
4845
|
+
sessionId,
|
|
4846
|
+
agentId: options.agent,
|
|
4847
|
+
targetPath,
|
|
4848
|
+
});
|
|
4849
|
+
process.exitCode = result.exitCode;
|
|
4850
|
+
return result;
|
|
4851
|
+
});
|
|
4852
|
+
|
|
4853
|
+
session
|
|
4854
|
+
.command("guard-install <sessionId>")
|
|
4855
|
+
.description("Install Claude, terminal, and VS Code authoritative lease preflights")
|
|
4856
|
+
.requiredOption("--agent <id>", "Agent id presenting holder capabilities")
|
|
4857
|
+
.option("--path <path>", "Workspace root", ".")
|
|
4858
|
+
.option("--json", "Emit machine-readable output")
|
|
4859
|
+
.action(async (sessionId, options, command) => {
|
|
4860
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4861
|
+
const result = await installFileLeaseIntegrations(
|
|
4862
|
+
sessionId,
|
|
4863
|
+
options.agent,
|
|
4864
|
+
{ targetPath },
|
|
4865
|
+
);
|
|
4866
|
+
if (shouldEmitJson(options, command)) {
|
|
4867
|
+
console.log(JSON.stringify({
|
|
4868
|
+
command: "session guard-install",
|
|
4869
|
+
...result,
|
|
4870
|
+
}, null, 2));
|
|
4871
|
+
} else {
|
|
4872
|
+
console.log(
|
|
4873
|
+
pc.green(
|
|
4874
|
+
"Installed Claude PreToolUse blocking plus terminal and VS Code lease preflights.",
|
|
4875
|
+
),
|
|
4876
|
+
);
|
|
4877
|
+
console.log(
|
|
4878
|
+
pc.yellow(
|
|
4879
|
+
"VS Code native Save cannot be cancelled reliably; run the generated guard task before edits or make it a dependency of mutation tasks.",
|
|
4880
|
+
),
|
|
4881
|
+
);
|
|
4882
|
+
}
|
|
4883
|
+
return result;
|
|
4884
|
+
});
|
|
4885
|
+
|
|
4886
|
+
session
|
|
4887
|
+
.command("guard-uninstall <sessionId>")
|
|
4888
|
+
.description("Safely remove only fingerprinted SentinelLayer lease preflights before CLI rollback")
|
|
4889
|
+
.requiredOption("--agent <id>", "Agent id that owns the managed integration")
|
|
4890
|
+
.option("--path <path>", "Workspace root", ".")
|
|
4891
|
+
.option("--json", "Emit machine-readable output")
|
|
4892
|
+
.action(async (sessionId, options, command) => {
|
|
4893
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4894
|
+
let result;
|
|
4895
|
+
try {
|
|
4896
|
+
result = await uninstallFileLeaseIntegrations(
|
|
4897
|
+
sessionId,
|
|
4898
|
+
options.agent,
|
|
4899
|
+
{ targetPath },
|
|
4900
|
+
);
|
|
4901
|
+
} catch (error) {
|
|
4902
|
+
result = {
|
|
4903
|
+
ok: false,
|
|
4904
|
+
uninstalled: false,
|
|
4905
|
+
reason: "guard_uninstall_failed",
|
|
4906
|
+
sessionId: normalizeString(sessionId),
|
|
4907
|
+
agentId: normalizeString(options.agent),
|
|
4908
|
+
activeLeases: [],
|
|
4909
|
+
residuals: [],
|
|
4910
|
+
error: {
|
|
4911
|
+
code: normalizeString(error?.code) || "FILE_LEASE_GUARD_UNINSTALL_FAILED",
|
|
4912
|
+
message: safeFileLeaseErrorMessage(error),
|
|
4913
|
+
},
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4916
|
+
const payload = {
|
|
4917
|
+
command: "session guard-uninstall",
|
|
4918
|
+
...result,
|
|
4919
|
+
};
|
|
4920
|
+
if (shouldEmitJson(options, command)) {
|
|
4921
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4922
|
+
} else if (result.ok) {
|
|
4923
|
+
console.log(pc.green("Removed the managed SentinelLayer lease preflights."));
|
|
4924
|
+
} else {
|
|
4925
|
+
if (result.error?.message) {
|
|
4926
|
+
console.error(pc.red(`Guard uninstall blocked: ${result.error.message}`));
|
|
4927
|
+
}
|
|
4928
|
+
for (const lease of result.activeLeases || []) {
|
|
4929
|
+
const file = normalizeString(lease.file || lease.path) || "(unknown path)";
|
|
4930
|
+
const holder = normalizeString(lease.agentId || lease.holderId) || "unknown";
|
|
4931
|
+
const expiresAt = normalizeString(lease.expiresAt);
|
|
4932
|
+
console.error(
|
|
4933
|
+
pc.red(
|
|
4934
|
+
`Guard uninstall blocked by ${file} (held by ${holder}${expiresAt ? `, expires ${expiresAt}` : ""}).`,
|
|
4935
|
+
),
|
|
4936
|
+
);
|
|
4937
|
+
}
|
|
4938
|
+
for (const residual of result.residuals || []) {
|
|
4939
|
+
console.error(
|
|
4940
|
+
pc.red(
|
|
4941
|
+
`Managed artifact retained: ${residual.path || residual.label || residual.artifact} (${residual.reason}).`,
|
|
4942
|
+
),
|
|
4943
|
+
);
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
if (!result.ok) {
|
|
4947
|
+
process.exitCode = 2;
|
|
4948
|
+
}
|
|
4949
|
+
return payload;
|
|
4950
|
+
});
|
|
4951
|
+
|
|
4413
4952
|
session
|
|
4414
4953
|
.command("locks <sessionId>")
|
|
4415
|
-
.description("List active file
|
|
4954
|
+
.description("List authoritative active file leases for the session")
|
|
4416
4955
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4417
4956
|
.option("--json", "Emit machine-readable output")
|
|
4418
4957
|
.action(async (sessionId, options, command) => {
|
|
@@ -4451,11 +4990,11 @@ export function registerSessionCommand(program) {
|
|
|
4451
4990
|
session
|
|
4452
4991
|
.command("listeners <sessionId>")
|
|
4453
4992
|
.description(
|
|
4454
|
-
"List
|
|
4993
|
+
"List the authoritative ephemeral listener presence roster. Presence is unknown when the server capability or Redis is unavailable; transcript heartbeats are never scanned.",
|
|
4455
4994
|
)
|
|
4456
4995
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4457
|
-
.option("--limit <n>", "
|
|
4458
|
-
.option("--max-pages <n>", "
|
|
4996
|
+
.option("--limit <n>", "Deprecated compatibility option; no event rows are scanned", "200")
|
|
4997
|
+
.option("--max-pages <n>", "Deprecated compatibility option; no event rows are scanned", "5")
|
|
4459
4998
|
.option("--json", "Emit machine-readable output")
|
|
4460
4999
|
.action(async (sessionId, options, command) => {
|
|
4461
5000
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -4512,12 +5051,18 @@ export function registerSessionCommand(program) {
|
|
|
4512
5051
|
};
|
|
4513
5052
|
}
|
|
4514
5053
|
}
|
|
4515
|
-
const live = listeners.filter(
|
|
5054
|
+
const live = listeners.filter(
|
|
5055
|
+
(row) => row.status === "present" || row.status === "active" || row.status === "idle",
|
|
5056
|
+
).length;
|
|
4516
5057
|
const payload = {
|
|
4517
5058
|
command: "session listeners",
|
|
4518
5059
|
sessionId: normalizedSessionId,
|
|
4519
5060
|
ok: Boolean(result.ok),
|
|
4520
5061
|
reason: result.ok ? undefined : result.reason,
|
|
5062
|
+
authoritative: Boolean(result.authoritative),
|
|
5063
|
+
presenceStatus: result.presenceStatus || "degraded",
|
|
5064
|
+
presenceEnabled: result.enabled ?? null,
|
|
5065
|
+
retryAfterMs: result.retryAfterMs || null,
|
|
4521
5066
|
count: listeners.length,
|
|
4522
5067
|
liveCount: live,
|
|
4523
5068
|
pageCount: Number(result.pageCount || 0),
|
|
@@ -4533,17 +5078,21 @@ export function registerSessionCommand(program) {
|
|
|
4533
5078
|
return payload;
|
|
4534
5079
|
}
|
|
4535
5080
|
if (!result.ok) {
|
|
4536
|
-
console.log(
|
|
5081
|
+
console.log(
|
|
5082
|
+
pc.yellow(
|
|
5083
|
+
`Listener presence is unknown (${result.reason}); durable heartbeat history was not scanned.`,
|
|
5084
|
+
),
|
|
5085
|
+
);
|
|
4537
5086
|
return payload;
|
|
4538
5087
|
}
|
|
4539
5088
|
if (listeners.length === 0) {
|
|
4540
|
-
console.log(pc.gray("No listeners
|
|
5089
|
+
console.log(pc.gray("No listeners are currently present."));
|
|
4541
5090
|
return payload;
|
|
4542
5091
|
}
|
|
4543
5092
|
console.log(pc.bold(`Listeners (${live} live / ${listeners.length} seen)`));
|
|
4544
5093
|
for (const row of listeners) {
|
|
4545
5094
|
const line = formatListenerLine(row);
|
|
4546
|
-
if (row.status === "active") console.log(pc.green(` ${line}`));
|
|
5095
|
+
if (row.status === "present" || row.status === "active") console.log(pc.green(` ${line}`));
|
|
4547
5096
|
else if (row.status === "idle") console.log(pc.cyan(` ${line}`));
|
|
4548
5097
|
else console.log(pc.gray(` ${line}`));
|
|
4549
5098
|
}
|
|
@@ -4563,7 +5112,7 @@ export function registerSessionCommand(program) {
|
|
|
4563
5112
|
session
|
|
4564
5113
|
.command("stop-listener <sessionId>")
|
|
4565
5114
|
.description(
|
|
4566
|
-
"Ask an agent's listener to stop
|
|
5115
|
+
"Ask an agent's listener to stop through the ephemeral listener-control plane. Targets one agent with --agent; omit it to stop every listener in the room.",
|
|
4567
5116
|
)
|
|
4568
5117
|
.option("--agent <id>", "Agent whose listener to stop (omit to stop all listeners in the room)")
|
|
4569
5118
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
@@ -4576,27 +5125,21 @@ export function registerSessionCommand(program) {
|
|
|
4576
5125
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4577
5126
|
const targetAgent = normalizeString(options.agent);
|
|
4578
5127
|
await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
|
|
4579
|
-
const
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
sessionId: normalizedSessionId,
|
|
4583
|
-
payload: {
|
|
4584
|
-
// targetAgentId routes the directive to that agent's listener (an
|
|
4585
|
-
// event recipient); omitting it broadcasts to every listener.
|
|
4586
|
-
...(targetAgent ? { targetAgentId: targetAgent } : { broadcast: true }),
|
|
4587
|
-
reason: "operator_stop",
|
|
4588
|
-
},
|
|
5128
|
+
const listenerControl = await requestSessionListenerStop(normalizedSessionId, {
|
|
5129
|
+
targetPath,
|
|
5130
|
+
targetAgentId: targetAgent,
|
|
4589
5131
|
});
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
5132
|
+
if (!listenerControl?.ok || !listenerControl?.recorded) {
|
|
5133
|
+
throw new Error(
|
|
5134
|
+
`Listener stop request failed (${normalizeString(listenerControl?.reason) || "not_recorded"}).`,
|
|
5135
|
+
);
|
|
5136
|
+
}
|
|
4594
5137
|
const payload = {
|
|
4595
5138
|
command: "session stop-listener",
|
|
4596
5139
|
sessionId: normalizedSessionId,
|
|
4597
5140
|
targetAgent: targetAgent || null,
|
|
4598
5141
|
scope: targetAgent ? "agent" : "all",
|
|
4599
|
-
|
|
5142
|
+
listenerControl,
|
|
4600
5143
|
};
|
|
4601
5144
|
if (shouldEmitJson(options, command)) {
|
|
4602
5145
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -4624,8 +5167,8 @@ export function registerSessionCommand(program) {
|
|
|
4624
5167
|
.option("--interval <seconds>", "Idle polling interval in seconds (default 60)", "60")
|
|
4625
5168
|
.option(
|
|
4626
5169
|
"--active-interval <seconds>",
|
|
4627
|
-
"Polling interval after recent human activity (default
|
|
4628
|
-
"
|
|
5170
|
+
"Polling interval after recent human activity (default 60)",
|
|
5171
|
+
"60",
|
|
4629
5172
|
)
|
|
4630
5173
|
.option(
|
|
4631
5174
|
"--active-window <seconds>",
|
|
@@ -4634,17 +5177,17 @@ export function registerSessionCommand(program) {
|
|
|
4634
5177
|
)
|
|
4635
5178
|
.option(
|
|
4636
5179
|
"--presence-interval <seconds>",
|
|
4637
|
-
"Minimum seconds between
|
|
5180
|
+
"Minimum seconds between ephemeral presence renewals (default 60)",
|
|
4638
5181
|
"60",
|
|
4639
5182
|
)
|
|
4640
5183
|
.option(
|
|
4641
5184
|
"--presence-keepalive <seconds>",
|
|
4642
|
-
"Maximum seconds between unchanged
|
|
5185
|
+
"Maximum seconds between unchanged ephemeral presence renewals (default 180)",
|
|
4643
5186
|
"180",
|
|
4644
5187
|
)
|
|
4645
5188
|
.option(
|
|
4646
5189
|
"--no-presence",
|
|
4647
|
-
"Do not
|
|
5190
|
+
"Do not renew ephemeral remote presence",
|
|
4648
5191
|
)
|
|
4649
5192
|
.option(
|
|
4650
5193
|
"--model <model>",
|
|
@@ -4653,7 +5196,7 @@ export function registerSessionCommand(program) {
|
|
|
4653
5196
|
)
|
|
4654
5197
|
.option("--display-name <name>", "Human-readable listener name for presence")
|
|
4655
5198
|
.option("--emit <format>", "Output format: ndjson or text", "ndjson")
|
|
4656
|
-
.option("--transport <mode>", "Listen transport:
|
|
5199
|
+
.option("--transport <mode>", "Listen transport: poll, auto, or stream (default poll)", "poll")
|
|
4657
5200
|
.option("--limit <n>", "Maximum events to request per poll (default 200)", "200")
|
|
4658
5201
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4659
5202
|
.option("--since <cursor>", "Override the persisted listen cursor")
|
|
@@ -4697,7 +5240,7 @@ export function registerSessionCommand(program) {
|
|
|
4697
5240
|
`Total log files to retain including the active file (default ${DEFAULT_ROTATING_LOG_MAX_FILES})`,
|
|
4698
5241
|
String(DEFAULT_ROTATING_LOG_MAX_FILES),
|
|
4699
5242
|
)
|
|
4700
|
-
.action(async (options) => {
|
|
5243
|
+
.action(asAdmittedAgent(sessionListenAgent, async (options) => {
|
|
4701
5244
|
const normalizedSessionId = resolveSessionIdOption(options);
|
|
4702
5245
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4703
5246
|
const agentId = normalizeAgentId(options.agent, "cli-user");
|
|
@@ -4705,7 +5248,7 @@ export function registerSessionCommand(program) {
|
|
|
4705
5248
|
const activeIntervalSeconds = parsePositiveInteger(
|
|
4706
5249
|
options.activeInterval,
|
|
4707
5250
|
"active-interval",
|
|
4708
|
-
|
|
5251
|
+
60,
|
|
4709
5252
|
);
|
|
4710
5253
|
const activeWindowSeconds = parsePositiveInteger(options.activeWindow, "active-window", 300);
|
|
4711
5254
|
const presenceIntervalSeconds = parsePositiveInteger(
|
|
@@ -4781,7 +5324,7 @@ export function registerSessionCommand(program) {
|
|
|
4781
5324
|
"--wake-host requires a valid host (claude|codex) and --resume-session <host-session-id>.",
|
|
4782
5325
|
);
|
|
4783
5326
|
}
|
|
4784
|
-
const requestedTransport = normalizeString(options.transport).toLowerCase() || "
|
|
5327
|
+
const requestedTransport = normalizeString(options.transport).toLowerCase() || "poll";
|
|
4785
5328
|
if (!["auto", "stream", "poll"].includes(requestedTransport)) {
|
|
4786
5329
|
throw new Error("--transport must be one of: auto, stream, poll.");
|
|
4787
5330
|
}
|
|
@@ -4789,9 +5332,15 @@ export function registerSessionCommand(program) {
|
|
|
4789
5332
|
options.maxPolls === undefined
|
|
4790
5333
|
? null
|
|
4791
5334
|
: parsePositiveInteger(options.maxPolls, "max-polls", 1);
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
5335
|
+
if (currentAdmittedAgent() && requestedTransport === "stream") {
|
|
5336
|
+
throw new Error(
|
|
5337
|
+
"An admitted agent listens by polling: the event stream is not part of an admission grant, so a revoke never has to chase an open stream. Use --transport poll.",
|
|
5338
|
+
);
|
|
5339
|
+
}
|
|
5340
|
+
const listenTransport =
|
|
5341
|
+
requestedTransport === "auto" && (maxPolls !== null || currentAdmittedAgent())
|
|
5342
|
+
? "poll"
|
|
5343
|
+
: requestedTransport;
|
|
4795
5344
|
const since = options.since === undefined ? undefined : String(options.since);
|
|
4796
5345
|
if (options.fromNow && options.since !== undefined) {
|
|
4797
5346
|
throw new Error("Use either --from-now or --since, not both.");
|
|
@@ -4865,14 +5414,15 @@ export function registerSessionCommand(program) {
|
|
|
4865
5414
|
const ac = new AbortController();
|
|
4866
5415
|
const onSigint = () => ac.abort();
|
|
4867
5416
|
process.on("SIGINT", onSigint);
|
|
4868
|
-
const
|
|
4869
|
-
const publishPresence =
|
|
5417
|
+
const remotePresenceEnabled = options.presence !== false;
|
|
5418
|
+
const publishPresence = remotePresenceEnabled && canPublishListenerPresence(agentId);
|
|
4870
5419
|
const presenceIntervalMs = Math.max(1, presenceIntervalSeconds) * 1000;
|
|
4871
5420
|
const effectivePresenceKeepaliveSeconds =
|
|
4872
5421
|
Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds, intervalSeconds, 1);
|
|
4873
5422
|
const presenceKeepaliveMs = effectivePresenceKeepaliveSeconds * 1000;
|
|
4874
5423
|
let lastPresenceHeartbeatMs = 0;
|
|
4875
5424
|
let lastPresenceHeartbeatFingerprint = "";
|
|
5425
|
+
let nextPresenceAttemptMs = 0;
|
|
4876
5426
|
|
|
4877
5427
|
// Periodic in-session success reminders (ack, claim work, reply
|
|
4878
5428
|
// in-thread). Long-running interactive listeners only — skipped under
|
|
@@ -4920,10 +5470,10 @@ export function registerSessionCommand(program) {
|
|
|
4920
5470
|
`Listening to session ${normalizedSessionId} as ${agentId}; transport=${listenTransport} idle=${intervalSeconds}s active=${activeIntervalSeconds}s/${activeWindowSeconds}s. Press Ctrl+C to stop.`,
|
|
4921
5471
|
),
|
|
4922
5472
|
);
|
|
4923
|
-
if (!
|
|
5473
|
+
if (!remotePresenceEnabled) {
|
|
4924
5474
|
console.log(
|
|
4925
5475
|
pc.gray(
|
|
4926
|
-
"Remote listener presence is disabled; no
|
|
5476
|
+
"Remote listener presence is disabled; no liveness writes will be made.",
|
|
4927
5477
|
),
|
|
4928
5478
|
);
|
|
4929
5479
|
} else if (!publishPresence) {
|
|
@@ -4974,21 +5524,17 @@ export function registerSessionCommand(program) {
|
|
|
4974
5524
|
console.log(pc.yellow(formatListenerCatchupNotice(catchup)));
|
|
4975
5525
|
}
|
|
4976
5526
|
},
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
if (
|
|
4983
|
-
|
|
4984
|
-
if (!target || target === agentId) {
|
|
4985
|
-
if (emitFormat !== "ndjson") {
|
|
4986
|
-
console.log(pc.yellow(`Listener stop requested for ${agentId}; exiting.`));
|
|
4987
|
-
}
|
|
4988
|
-
ac.abort();
|
|
4989
|
-
return;
|
|
4990
|
-
}
|
|
5527
|
+
onControl: async (control) => {
|
|
5528
|
+
const controlType = normalizeString(
|
|
5529
|
+
control?.type || control?.action || control?.event,
|
|
5530
|
+
).toLowerCase();
|
|
5531
|
+
if (!["stop", "listener_stop"].includes(controlType)) return;
|
|
5532
|
+
if (emitFormat !== "ndjson") {
|
|
5533
|
+
console.log(pc.yellow(`Listener stop requested for ${agentId}; exiting.`));
|
|
4991
5534
|
}
|
|
5535
|
+
ac.abort();
|
|
5536
|
+
},
|
|
5537
|
+
onEvent: async (event) => {
|
|
4992
5538
|
if (emitFormat === "ndjson") {
|
|
4993
5539
|
console.log(JSON.stringify(event));
|
|
4994
5540
|
} else {
|
|
@@ -5054,13 +5600,17 @@ export function registerSessionCommand(program) {
|
|
|
5054
5600
|
}
|
|
5055
5601
|
if (!publishPresence) return;
|
|
5056
5602
|
const lifecycleType = normalizeString(lifecycle?.type);
|
|
5603
|
+
const presenceAttemptMs = Date.now();
|
|
5604
|
+
if (lifecycleType !== "stopped" && presenceAttemptMs < nextPresenceAttemptMs) {
|
|
5605
|
+
return;
|
|
5606
|
+
}
|
|
5057
5607
|
const publishLifecycle = {
|
|
5058
5608
|
...lifecycle,
|
|
5059
5609
|
presenceIntervalSeconds,
|
|
5060
5610
|
presenceKeepaliveSeconds: effectivePresenceKeepaliveSeconds,
|
|
5061
5611
|
};
|
|
5062
5612
|
if (lifecycleType === "heartbeat") {
|
|
5063
|
-
const nowMs =
|
|
5613
|
+
const nowMs = presenceAttemptMs;
|
|
5064
5614
|
const heartbeatDecision = shouldPublishListenerPresenceHeartbeat({
|
|
5065
5615
|
lifecycle: publishLifecycle,
|
|
5066
5616
|
nowMs,
|
|
@@ -5075,7 +5625,7 @@ export function registerSessionCommand(program) {
|
|
|
5075
5625
|
lastPresenceHeartbeatFingerprint = heartbeatDecision.fingerprint;
|
|
5076
5626
|
lastPresenceHeartbeatMs = nowMs;
|
|
5077
5627
|
}
|
|
5078
|
-
await publishListenerPresenceEvent({
|
|
5628
|
+
const presenceResult = await publishListenerPresenceEvent({
|
|
5079
5629
|
sessionId: normalizedSessionId,
|
|
5080
5630
|
targetPath,
|
|
5081
5631
|
agentId,
|
|
@@ -5086,6 +5636,15 @@ export function registerSessionCommand(program) {
|
|
|
5086
5636
|
listenerId,
|
|
5087
5637
|
lifecycle: publishLifecycle,
|
|
5088
5638
|
});
|
|
5639
|
+
if (presenceResult?.ok) {
|
|
5640
|
+
nextPresenceAttemptMs = 0;
|
|
5641
|
+
return;
|
|
5642
|
+
}
|
|
5643
|
+
const retryAfterMs = Math.max(
|
|
5644
|
+
presenceIntervalMs,
|
|
5645
|
+
Number(presenceResult?.retryAfterMs) || 0,
|
|
5646
|
+
);
|
|
5647
|
+
nextPresenceAttemptMs = presenceAttemptMs + retryAfterMs;
|
|
5089
5648
|
},
|
|
5090
5649
|
});
|
|
5091
5650
|
} finally {
|
|
@@ -5103,7 +5662,7 @@ export function registerSessionCommand(program) {
|
|
|
5103
5662
|
}).catch(() => {});
|
|
5104
5663
|
}
|
|
5105
5664
|
}
|
|
5106
|
-
});
|
|
5665
|
+
}));
|
|
5107
5666
|
|
|
5108
5667
|
session
|
|
5109
5668
|
.command("daemon [sessionId]")
|
|
@@ -5565,6 +6124,8 @@ export function registerSessionCommand(program) {
|
|
|
5565
6124
|
let hydration = null;
|
|
5566
6125
|
let remoteTail = null;
|
|
5567
6126
|
let remoteAppend = null;
|
|
6127
|
+
let listenerPresence = null;
|
|
6128
|
+
let readCursor = null;
|
|
5568
6129
|
if (options.remote) {
|
|
5569
6130
|
hydration = await hydrateSessionFromRemote({
|
|
5570
6131
|
sessionId: normalizedSessionId,
|
|
@@ -5592,12 +6153,27 @@ export function registerSessionCommand(program) {
|
|
|
5592
6153
|
remoteAppend = await appendMissingRemoteEvents(normalizedSessionId, remoteTail.events, {
|
|
5593
6154
|
targetPath,
|
|
5594
6155
|
});
|
|
6156
|
+
readCursor = recordSessionReadCursor(normalizedSessionId, remoteTail.events, {
|
|
6157
|
+
targetPath,
|
|
6158
|
+
agentId,
|
|
6159
|
+
enabled: true,
|
|
6160
|
+
});
|
|
5595
6161
|
}
|
|
6162
|
+
listenerPresence = await fetchSessionListeners(normalizedSessionId, {
|
|
6163
|
+
targetPath,
|
|
6164
|
+
}).catch((error) => ({
|
|
6165
|
+
ok: false,
|
|
6166
|
+
authoritative: false,
|
|
6167
|
+
presenceStatus: "degraded",
|
|
6168
|
+
reason: normalizeString(error?.message) || "presence_read_failed",
|
|
6169
|
+
listeners: [],
|
|
6170
|
+
}));
|
|
5596
6171
|
}
|
|
5597
6172
|
const current = await buildSessionRecap(normalizedSessionId, {
|
|
5598
6173
|
forAgentId: agentId,
|
|
5599
6174
|
maxEvents,
|
|
5600
6175
|
targetPath,
|
|
6176
|
+
listenerPresence,
|
|
5601
6177
|
});
|
|
5602
6178
|
const payload = {
|
|
5603
6179
|
command: "session recap now",
|
|
@@ -5622,6 +6198,8 @@ export function registerSessionCommand(program) {
|
|
|
5622
6198
|
}
|
|
5623
6199
|
: null,
|
|
5624
6200
|
appendedTail: remoteAppend,
|
|
6201
|
+
listenerPresence,
|
|
6202
|
+
readCursor,
|
|
5625
6203
|
}
|
|
5626
6204
|
: null,
|
|
5627
6205
|
};
|
|
@@ -5656,7 +6234,7 @@ export function registerSessionCommand(program) {
|
|
|
5656
6234
|
.option("--include-control-events", "Include listener lifecycle/control-plane events in transcript output")
|
|
5657
6235
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
5658
6236
|
.option("--json", "Emit machine-readable output")
|
|
5659
|
-
.action(async (sessionId, options, command) => {
|
|
6237
|
+
.action(asAdmittedAgent(sessionReadAgent, async (sessionId, options, command) => {
|
|
5660
6238
|
const normalizedSessionId = normalizeString(sessionId);
|
|
5661
6239
|
if (!normalizedSessionId) {
|
|
5662
6240
|
throw new Error("session id is required.");
|
|
@@ -5841,11 +6419,10 @@ export function registerSessionCommand(program) {
|
|
|
5841
6419
|
? filterSessionActionEventsForTranscriptWindow(visibleActionEvents, transcriptEvents)
|
|
5842
6420
|
: visibleActionEvents;
|
|
5843
6421
|
const events = mergeSessionActionEvents(transcriptEvents, actionEvents).slice(-tail);
|
|
5844
|
-
const autoView =
|
|
6422
|
+
const autoView = recordSessionReadCursor(normalizedSessionId, events, {
|
|
5845
6423
|
targetPath,
|
|
5846
6424
|
agentId: readAgentId,
|
|
5847
6425
|
enabled: Boolean(options.remote && options.view !== false),
|
|
5848
|
-
maxTargets: process.env.SENTINELAYER_SESSION_READ_VIEW_MAX_TARGETS || 50,
|
|
5849
6426
|
timeoutMs: process.env.SENTINELAYER_SESSION_READ_VIEW_TIMEOUT_MS || SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
5850
6427
|
});
|
|
5851
6428
|
const remoteVerified = Boolean(
|
|
@@ -6001,7 +6578,7 @@ export function registerSessionCommand(program) {
|
|
|
6001
6578
|
console.log(formatEventLine(event));
|
|
6002
6579
|
}
|
|
6003
6580
|
}
|
|
6004
|
-
});
|
|
6581
|
+
}));
|
|
6005
6582
|
|
|
6006
6583
|
session
|
|
6007
6584
|
.command("search <sessionId> <query>")
|
|
@@ -6054,6 +6631,66 @@ export function registerSessionCommand(program) {
|
|
|
6054
6631
|
}
|
|
6055
6632
|
});
|
|
6056
6633
|
|
|
6634
|
+
session
|
|
6635
|
+
.command("recall <need>")
|
|
6636
|
+
.description(
|
|
6637
|
+
"Hydrate a session by RETRIEVAL, not full-tail replay: return the top-K relevant memories (dense int8 exact-scan + BM25 + RRF + graph diffusion + ACT-R) each with a one-line provenance path (ENGRAM §1)",
|
|
6638
|
+
)
|
|
6639
|
+
.requiredOption("--session <id>", "Session id to recall from")
|
|
6640
|
+
.option("--role <role>", "Optional role lens folded into the query")
|
|
6641
|
+
.option("--k <n>", "Number of memories to return (default 12, max 50)", "12")
|
|
6642
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6643
|
+
.option("--no-remote", "Skip remote hydrate; recall over the local mirror only")
|
|
6644
|
+
.option("--include-control-events", "Index control-plane events as memories too")
|
|
6645
|
+
.option("--json", "Emit machine-readable output")
|
|
6646
|
+
.action(async (need, options, command) => {
|
|
6647
|
+
const normalizedNeed = normalizeString(need);
|
|
6648
|
+
if (normalizedNeed.length < 2) {
|
|
6649
|
+
throw new Error("need must be at least 2 characters.");
|
|
6650
|
+
}
|
|
6651
|
+
const normalizedSessionId = normalizeString(options.session);
|
|
6652
|
+
if (!normalizedSessionId) {
|
|
6653
|
+
throw new Error("--session <id> is required.");
|
|
6654
|
+
}
|
|
6655
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6656
|
+
const k = Math.min(50, parsePositiveInteger(options.k, "k", 12));
|
|
6657
|
+
const emitJson = shouldEmitJson(options, command);
|
|
6658
|
+
|
|
6659
|
+
const result = await runSessionRecall({
|
|
6660
|
+
sessionId: normalizedSessionId,
|
|
6661
|
+
need: normalizedNeed,
|
|
6662
|
+
targetPath,
|
|
6663
|
+
k,
|
|
6664
|
+
role: normalizeString(options.role),
|
|
6665
|
+
remote: options.remote !== false,
|
|
6666
|
+
includeControlEvents: Boolean(options.includeControlEvents),
|
|
6667
|
+
});
|
|
6668
|
+
|
|
6669
|
+
if (emitJson) {
|
|
6670
|
+
console.log(JSON.stringify({ command: "session recall", targetPath, ...result }, null, 2));
|
|
6671
|
+
return;
|
|
6672
|
+
}
|
|
6673
|
+
if (!result.ok) {
|
|
6674
|
+
throw new Error(`Session recall failed (${result.reason || "unknown"}).`);
|
|
6675
|
+
}
|
|
6676
|
+
if (result.backfill && result.backfill.warning) {
|
|
6677
|
+
console.log(pc.yellow(result.backfill.warning));
|
|
6678
|
+
}
|
|
6679
|
+
console.log(
|
|
6680
|
+
pc.gray(
|
|
6681
|
+
`Recalled top ${result.results.length} of ${result.meta.observationCount} memories · pool ${result.poolSize} · ~${result.tokenCut.reductionRatio}x fewer tokens than full replay (${result.tokenCut.recallPackTokens} vs ${result.tokenCut.fullReplayTokens}).`,
|
|
6682
|
+
),
|
|
6683
|
+
);
|
|
6684
|
+
result.results.forEach((item, index) => {
|
|
6685
|
+
const sequence = item.sequenceId ? `#${item.sequenceId}` : "";
|
|
6686
|
+
console.log(`${String(index + 1).padStart(2)}. ${sequence} ${item.agentId} ${item.kind} (score ${item.score})`);
|
|
6687
|
+
if (item.snippet) {
|
|
6688
|
+
console.log(` ${item.snippet}`);
|
|
6689
|
+
}
|
|
6690
|
+
console.log(pc.gray(` why: ${item.provenance}`));
|
|
6691
|
+
});
|
|
6692
|
+
});
|
|
6693
|
+
|
|
6057
6694
|
session
|
|
6058
6695
|
.command("sync <sessionId>")
|
|
6059
6696
|
.description(
|
|
@@ -6150,6 +6787,147 @@ export function registerSessionCommand(program) {
|
|
|
6150
6787
|
}
|
|
6151
6788
|
});
|
|
6152
6789
|
|
|
6790
|
+
// ---------------------------------------------------------------- tickets
|
|
6791
|
+
// The room's shared backlog (API "Ticket Lease Contract (T2)"). Every subcommand is
|
|
6792
|
+
// an ACTOR command: the runCli choke point runs it on the agent's admission.
|
|
6793
|
+
const ticket = session
|
|
6794
|
+
.command("ticket")
|
|
6795
|
+
.description("The room's shared backlog: list, claim, renew, release and submit tickets");
|
|
6796
|
+
|
|
6797
|
+
// Ticket work is attributed by the CREDENTIAL, never by a label: the lease routes take the
|
|
6798
|
+
// holder from authentication alone. So an agent identity is allowed only with that agent's
|
|
6799
|
+
// live admission in scope; without it, the request would go out on the human token and
|
|
6800
|
+
// claim as the PERSON while this command reported the agent. Refused before any request.
|
|
6801
|
+
async function ticketIdentity(sessionId, options, targetPath) {
|
|
6802
|
+
const identity = await resolveSessionSayIdentity({ sessionId, agentId: options.agent, targetPath });
|
|
6803
|
+
const agent = normalizeString(identity.agentId);
|
|
6804
|
+
if (!agent || agent === "cli-user") return "human";
|
|
6805
|
+
const admitted = currentAdmittedAgent();
|
|
6806
|
+
if (!admitted || admitted.sessionId !== sessionId || admitted.agentId !== agent) {
|
|
6807
|
+
throw new Error(
|
|
6808
|
+
`Agent "${agent}" has no live admission in this room. Ticket work is attributed by the credential, so ` +
|
|
6809
|
+
`without one this would act as you, not as "${agent}". Admit it first: ` +
|
|
6810
|
+
`sl session join ${sessionId} --agent ${agent} --goal "<what it is here to do>"`
|
|
6811
|
+
);
|
|
6812
|
+
}
|
|
6813
|
+
return agent;
|
|
6814
|
+
}
|
|
6815
|
+
|
|
6816
|
+
function ticketOutput(options, command, payload, line) {
|
|
6817
|
+
if (shouldEmitJson(options, command)) {
|
|
6818
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
6819
|
+
return;
|
|
6820
|
+
}
|
|
6821
|
+
console.log(line);
|
|
6822
|
+
}
|
|
6823
|
+
|
|
6824
|
+
ticket
|
|
6825
|
+
.command("list <sessionId>")
|
|
6826
|
+
.description("List the room's tickets (--available: only those claimable now)")
|
|
6827
|
+
.option("--available", "Only tickets that can be claimed now (never blocked work)")
|
|
6828
|
+
.option("--agent <id>", "Agent id reading the backlog (defaults to the joined session agent)")
|
|
6829
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6830
|
+
.option("--json", "Emit machine-readable output")
|
|
6831
|
+
.action(async (sessionId, options, command) => {
|
|
6832
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6833
|
+
const result = await listTickets(normalizeString(sessionId), { targetPath, available: Boolean(options.available) });
|
|
6834
|
+
const lines = result.items.map((t) => {
|
|
6835
|
+
const holder = t.lease ? ` · ${t.lease.live ? "leased by" : "lease EXPIRED, was"} ${t.lease.holder}` : "";
|
|
6836
|
+
const done = t.completion && !t.completion.verified ? " (manual, unverified)" : "";
|
|
6837
|
+
return `${t.id} ${t.status}${done} v${t.version} ${t.title}${holder}`;
|
|
6838
|
+
});
|
|
6839
|
+
ticketOutput(options, command, { command: "session ticket list", ...result }, lines.join("\n") || "No tickets.");
|
|
6840
|
+
});
|
|
6841
|
+
|
|
6842
|
+
ticket
|
|
6843
|
+
.command("claim <sessionId> <ticketId>")
|
|
6844
|
+
.description("Claim a ticket: take its fenced lease")
|
|
6845
|
+
.option("--expected-version <n>", "Refuse if the ticket has changed since this version")
|
|
6846
|
+
.option("--agent <id>", "Agent id claiming the ticket (defaults to the joined session agent)")
|
|
6847
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6848
|
+
.option("--json", "Emit machine-readable output")
|
|
6849
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
6850
|
+
const sid = normalizeString(sessionId);
|
|
6851
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6852
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
6853
|
+
const expectedVersion =
|
|
6854
|
+
options.expectedVersion === undefined ? null : parsePositiveInteger(options.expectedVersion, "expected-version", 1);
|
|
6855
|
+
const result = await claimTicket(sid, normalizeString(ticketId), { identity, expectedVersion, targetPath });
|
|
6856
|
+
ticketOutput(options, command, { command: "session ticket claim", identity, ...result },
|
|
6857
|
+
`Claimed ${ticketId} (fence ${result.lease.fence}, until ${result.lease.expiresAt}).`);
|
|
6858
|
+
});
|
|
6859
|
+
|
|
6860
|
+
ticket
|
|
6861
|
+
.command("renew <sessionId> <ticketId>")
|
|
6862
|
+
.description("Renew the lease this identity holds on a ticket")
|
|
6863
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
6864
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6865
|
+
.option("--json", "Emit machine-readable output")
|
|
6866
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
6867
|
+
const sid = normalizeString(sessionId);
|
|
6868
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6869
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
6870
|
+
const result = await renewTicketLease(sid, normalizeString(ticketId), { identity, targetPath });
|
|
6871
|
+
ticketOutput(options, command, { command: "session ticket renew", identity, ...result },
|
|
6872
|
+
`Renewed ${ticketId} until ${result.lease.expiresAt}.`);
|
|
6873
|
+
});
|
|
6874
|
+
|
|
6875
|
+
ticket
|
|
6876
|
+
.command("release <sessionId> <ticketId>")
|
|
6877
|
+
.description("Give a ticket back: it returns to open for the next claimant")
|
|
6878
|
+
.option("--reason <text>", "Why it is being released")
|
|
6879
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
6880
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6881
|
+
.option("--json", "Emit machine-readable output")
|
|
6882
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
6883
|
+
const sid = normalizeString(sessionId);
|
|
6884
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6885
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
6886
|
+
const result = await releaseTicketLease(sid, normalizeString(ticketId), {
|
|
6887
|
+
identity, targetPath, reason: normalizeString(options.reason) || null,
|
|
6888
|
+
});
|
|
6889
|
+
ticketOutput(options, command, { command: "session ticket release", identity, ...result }, `Released ${ticketId}.`);
|
|
6890
|
+
});
|
|
6891
|
+
|
|
6892
|
+
ticket
|
|
6893
|
+
.command("submit <sessionId> <ticketId>")
|
|
6894
|
+
.description("Submit the work for review (in_review). Completion needs verification; this is not done.")
|
|
6895
|
+
.option("--sha <sha>", "The implementation commit")
|
|
6896
|
+
.option("--evidence-file <path>", "JSON array of re-checkable evidence claims")
|
|
6897
|
+
.option("--agent <id>", "Agent id holding the lease (defaults to the joined session agent)")
|
|
6898
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6899
|
+
.option("--json", "Emit machine-readable output")
|
|
6900
|
+
.action(async (sessionId, ticketId, options, command) => {
|
|
6901
|
+
const sid = normalizeString(sessionId);
|
|
6902
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6903
|
+
const identity = await ticketIdentity(sid, options, targetPath);
|
|
6904
|
+
let evidence = [];
|
|
6905
|
+
if (options.evidenceFile) {
|
|
6906
|
+
evidence = JSON.parse(await fsp.readFile(path.resolve(targetPath, String(options.evidenceFile)), "utf8"));
|
|
6907
|
+
if (!Array.isArray(evidence)) throw new Error("--evidence-file must contain a JSON array.");
|
|
6908
|
+
}
|
|
6909
|
+
const result = await submitTicket(sid, normalizeString(ticketId), {
|
|
6910
|
+
identity, targetPath, implementationSha: normalizeString(options.sha) || null, evidence,
|
|
6911
|
+
});
|
|
6912
|
+
ticketOutput(options, command, { command: "session ticket submit", identity, ...result },
|
|
6913
|
+
`Submitted ${ticketId} for review.`);
|
|
6914
|
+
});
|
|
6915
|
+
|
|
6916
|
+
ticket
|
|
6917
|
+
.command("events <sessionId>")
|
|
6918
|
+
.description("The room's ticket events after a cursor, in commit order")
|
|
6919
|
+
.option("--after <cursor>", "Only events after this cursor", "0")
|
|
6920
|
+
.option("--agent <id>", "Agent id reading the feed (defaults to the joined session agent)")
|
|
6921
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6922
|
+
.option("--json", "Emit machine-readable output")
|
|
6923
|
+
.action(async (sessionId, options, command) => {
|
|
6924
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6925
|
+
const after = Number.parseInt(String(options.after ?? "0"), 10) || 0;
|
|
6926
|
+
const result = await ticketEvents(normalizeString(sessionId), { after, targetPath });
|
|
6927
|
+
const lines = (result.events || []).map((e) => `#${e.cursor} ${e.kind} ${e.ticketId} v${e.ticketVersion} by ${e.actorId}`);
|
|
6928
|
+
ticketOutput(options, command, { command: "session ticket events", ...result }, lines.join("\n") || "No new ticket events.");
|
|
6929
|
+
});
|
|
6930
|
+
|
|
6153
6931
|
const checkpoint = session
|
|
6154
6932
|
.command("checkpoint")
|
|
6155
6933
|
.description("List, show, create, and generate durable session checkpoints");
|
|
@@ -6422,7 +7200,14 @@ export function registerSessionCommand(program) {
|
|
|
6422
7200
|
throw new Error(`Session '${normalizedSessionId}' was not found.`);
|
|
6423
7201
|
}
|
|
6424
7202
|
|
|
6425
|
-
const [
|
|
7203
|
+
const [
|
|
7204
|
+
agents,
|
|
7205
|
+
runtimeRuns,
|
|
7206
|
+
leases,
|
|
7207
|
+
fileLeaseStatus,
|
|
7208
|
+
activeTasks,
|
|
7209
|
+
recentEvents,
|
|
7210
|
+
] = await Promise.all([
|
|
6426
7211
|
listAgents(normalizedSessionId, {
|
|
6427
7212
|
targetPath,
|
|
6428
7213
|
includeInactive: false,
|
|
@@ -6441,10 +7226,22 @@ export function registerSessionCommand(program) {
|
|
|
6441
7226
|
includeExpired: true,
|
|
6442
7227
|
limit: 100,
|
|
6443
7228
|
}),
|
|
6444
|
-
listFileLocks(normalizedSessionId, {
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
7229
|
+
listFileLocks(normalizedSessionId, { targetPath })
|
|
7230
|
+
.then((activeFileLocks) => ({
|
|
7231
|
+
ok: true,
|
|
7232
|
+
authoritative: true,
|
|
7233
|
+
activeFileLocks,
|
|
7234
|
+
error: null,
|
|
7235
|
+
}))
|
|
7236
|
+
.catch((error) => ({
|
|
7237
|
+
ok: false,
|
|
7238
|
+
authoritative: false,
|
|
7239
|
+
activeFileLocks: [],
|
|
7240
|
+
error: {
|
|
7241
|
+
code: normalizeString(error?.code) || "FILE_LEASE_LIST_FAILED",
|
|
7242
|
+
message: safeFileLeaseErrorMessage(error),
|
|
7243
|
+
},
|
|
7244
|
+
})),
|
|
6448
7245
|
listSessionTasks(normalizedSessionId, {
|
|
6449
7246
|
targetPath,
|
|
6450
7247
|
statuses: ["PENDING", "ACCEPTED"],
|
|
@@ -6455,42 +7252,51 @@ export function registerSessionCommand(program) {
|
|
|
6455
7252
|
tail: 10,
|
|
6456
7253
|
}),
|
|
6457
7254
|
]);
|
|
7255
|
+
const fileLocks = fileLeaseStatus.activeFileLocks;
|
|
6458
7256
|
|
|
6459
7257
|
const staleAgents = detectStaleAgents(agents, {});
|
|
6460
7258
|
const staleAgentIds = new Set(staleAgents.map((agent) => normalizeString(agent.agentId)));
|
|
6461
7259
|
const activeAgents = agents.filter((agent) => !staleAgentIds.has(normalizeString(agent.agentId)));
|
|
6462
|
-
|
|
7260
|
+
const materialRecentEvents = filterSessionMaterialEvents(recentEvents);
|
|
7261
|
+
const hiddenControlEventCount = recentEvents.length - materialRecentEvents.length;
|
|
7262
|
+
let listenerRows = [];
|
|
6463
7263
|
let listenerPresence = {
|
|
6464
|
-
source: "
|
|
6465
|
-
ok:
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
7264
|
+
source: "remote_presence",
|
|
7265
|
+
ok: false,
|
|
7266
|
+
authoritative: false,
|
|
7267
|
+
presenceStatus: "degraded",
|
|
7268
|
+
reason: "presence_unknown",
|
|
7269
|
+
count: 0,
|
|
7270
|
+
liveCount: 0,
|
|
7271
|
+
listeners: [],
|
|
6469
7272
|
};
|
|
6470
7273
|
try {
|
|
6471
7274
|
const remoteListeners = await fetchSessionListeners(normalizedSessionId, {
|
|
6472
7275
|
targetPath,
|
|
6473
|
-
limit: 200,
|
|
6474
|
-
maxPages: 5,
|
|
6475
7276
|
});
|
|
6476
|
-
if (remoteListeners?.ok && Array.isArray(remoteListeners.listeners)
|
|
7277
|
+
if (remoteListeners?.ok && Array.isArray(remoteListeners.listeners)) {
|
|
6477
7278
|
listenerRows = remoteListeners.listeners;
|
|
6478
7279
|
listenerPresence = {
|
|
6479
|
-
source: "
|
|
7280
|
+
source: "remote_presence",
|
|
6480
7281
|
ok: true,
|
|
7282
|
+
authoritative: true,
|
|
7283
|
+
presenceStatus: remoteListeners.presenceStatus || "ok",
|
|
6481
7284
|
count: listenerRows.length,
|
|
6482
|
-
liveCount: listenerRows.filter(
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
listenerEventCount: Number(remoteListeners.listenerEventCount || 0),
|
|
6486
|
-
partial: Boolean(remoteListeners.partial),
|
|
7285
|
+
liveCount: listenerRows.filter(
|
|
7286
|
+
(row) => row.status === "present" || row.status === "active" || row.status === "idle",
|
|
7287
|
+
).length,
|
|
6487
7288
|
listeners: listenerRows,
|
|
6488
7289
|
};
|
|
6489
7290
|
} else if (remoteListeners && remoteListeners.ok === false) {
|
|
6490
|
-
listenerPresence
|
|
7291
|
+
listenerPresence = {
|
|
7292
|
+
...listenerPresence,
|
|
7293
|
+
presenceStatus: remoteListeners.presenceStatus || "degraded",
|
|
7294
|
+
reason: normalizeString(remoteListeners.reason) || "remote_unavailable",
|
|
7295
|
+
retryAfterMs: remoteListeners.retryAfterMs || null,
|
|
7296
|
+
};
|
|
6491
7297
|
}
|
|
6492
7298
|
} catch (error) {
|
|
6493
|
-
listenerPresence.
|
|
7299
|
+
listenerPresence.reason = normalizeString(error?.message) || "remote_unavailable";
|
|
6494
7300
|
}
|
|
6495
7301
|
const liveListenerCount = listenerPresence.liveCount;
|
|
6496
7302
|
const payload = {
|
|
@@ -6505,8 +7311,14 @@ export function registerSessionCommand(program) {
|
|
|
6505
7311
|
runtimeRuns,
|
|
6506
7312
|
activeLeases: leases.assignments,
|
|
6507
7313
|
activeFileLocks: fileLocks,
|
|
7314
|
+
fileLeaseAuthority: {
|
|
7315
|
+
ok: fileLeaseStatus.ok,
|
|
7316
|
+
authoritative: fileLeaseStatus.authoritative,
|
|
7317
|
+
error: fileLeaseStatus.error,
|
|
7318
|
+
},
|
|
6508
7319
|
activeTasks: activeTasks.tasks,
|
|
6509
|
-
recentEvents,
|
|
7320
|
+
recentEvents: materialRecentEvents,
|
|
7321
|
+
hiddenControlEventCount,
|
|
6510
7322
|
};
|
|
6511
7323
|
if (shouldEmitJson(options, command)) {
|
|
6512
7324
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -6514,12 +7326,15 @@ export function registerSessionCommand(program) {
|
|
|
6514
7326
|
}
|
|
6515
7327
|
|
|
6516
7328
|
console.log(pc.bold(`Session ${normalizedSessionId}`));
|
|
7329
|
+
const listenerText = listenerPresence.authoritative
|
|
7330
|
+
? `${liveListenerCount}/${listenerRows.length}`
|
|
7331
|
+
: `unknown(${listenerPresence.presenceStatus || "degraded"})`;
|
|
6517
7332
|
console.log(
|
|
6518
7333
|
pc.gray(
|
|
6519
|
-
`status=${sessionPayload.status} active=${activeAgents.length} registered=${agents.length} stale=${staleAgents.length} listeners=${
|
|
7334
|
+
`status=${sessionPayload.status} active=${activeAgents.length} registered=${agents.length} stale=${staleAgents.length} listeners=${listenerText} runs=${runtimeRuns.length} leases=${leases.assignments.length} locks=${fileLocks.length} tasks=${activeTasks.tasks.length}`
|
|
6520
7335
|
)
|
|
6521
7336
|
);
|
|
6522
|
-
for (const event of
|
|
7337
|
+
for (const event of materialRecentEvents) {
|
|
6523
7338
|
console.log(formatEventLine(event));
|
|
6524
7339
|
}
|
|
6525
7340
|
});
|