sentinelayer-cli 0.40.0 → 0.41.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 +4 -4
- package/package.json +2 -3
- package/src/commands/session.js +611 -232
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +64 -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 +115 -0
- package/src/engram/tools.js +146 -0
- package/src/engram/trust.js +105 -0
- package/src/legacy-cli.js +4 -1
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/session/agent-registry.js +22 -63
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon.js +19 -124
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/listener.js +243 -20
- package/src/session/listeners.js +71 -80
- 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 +29 -23
- package/src/session/remote-hydrate.js +5 -1
- package/src/session/sync.js +661 -11
- package/src/session/wake/pump.js +8 -3
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,
|
|
@@ -103,11 +112,15 @@ import {
|
|
|
103
112
|
pollHumanMessages,
|
|
104
113
|
pollSessionEvents,
|
|
105
114
|
pollSessionEventsBefore,
|
|
115
|
+
requestSessionListenerStop,
|
|
116
|
+
renewSessionPresence,
|
|
106
117
|
searchSessionEvents,
|
|
107
118
|
syncSessionEventToApi,
|
|
108
119
|
syncSessionMetadataToApi,
|
|
120
|
+
updateSessionReadCursor,
|
|
109
121
|
} from "../session/sync.js";
|
|
110
122
|
import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
123
|
+
import { runSessionRecall } from "../session/recall/index.js";
|
|
111
124
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
112
125
|
import { listenSessionEvents } from "../session/listener.js";
|
|
113
126
|
import { SESSION_LIVE_SUCCESS_TIPS } from "../session/coordination-guidance.js";
|
|
@@ -442,7 +455,9 @@ async function confirmSessionEventVisible(
|
|
|
442
455
|
}
|
|
443
456
|
lastReason = "not_visible";
|
|
444
457
|
const nextCursor = normalizeString(result.cursor);
|
|
445
|
-
|
|
458
|
+
const scanExhausted = result.scanExhausted ?? result.scan_exhausted;
|
|
459
|
+
const emptyScanCanContinue = !events.length && scanExhausted === false;
|
|
460
|
+
if (!nextCursor || nextCursor === pageCursor || (!events.length && !emptyScanCanContinue)) {
|
|
446
461
|
break;
|
|
447
462
|
}
|
|
448
463
|
pageCursor = nextCursor;
|
|
@@ -960,12 +975,12 @@ function normalizeAutoViewTimeoutMs(value) {
|
|
|
960
975
|
return Math.max(50, Math.min(5_000, Math.floor(parsed)));
|
|
961
976
|
}
|
|
962
977
|
|
|
963
|
-
async function
|
|
978
|
+
async function writeSessionReadCursor(sessionId, target = null, {
|
|
964
979
|
targetPath,
|
|
965
980
|
agentId,
|
|
966
981
|
timeoutMs = SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
967
982
|
} = {}) {
|
|
968
|
-
if (!
|
|
983
|
+
if (!target) {
|
|
969
984
|
return;
|
|
970
985
|
}
|
|
971
986
|
const totalTimeoutMs = normalizeAutoViewTimeoutMs(timeoutMs);
|
|
@@ -983,50 +998,31 @@ async function writeSessionReadViews(sessionId, writeTargets = [], {
|
|
|
983
998
|
}
|
|
984
999
|
|
|
985
1000
|
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
|
-
}
|
|
1001
|
+
const elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
1002
|
+
const remainingMs = Math.max(1, totalTimeoutMs - elapsedMs);
|
|
1003
|
+
await updateSessionReadCursor(sessionId, {
|
|
1004
|
+
targetPath,
|
|
1005
|
+
targetSequenceId: target.targetSequenceId,
|
|
1006
|
+
targetCursor: target.targetCursor,
|
|
1007
|
+
agentId,
|
|
1008
|
+
timeoutMs: remainingMs,
|
|
1009
|
+
signal: controller.signal,
|
|
1010
|
+
});
|
|
1014
1011
|
} finally {
|
|
1015
1012
|
clearTimeout(timeoutHandle);
|
|
1016
1013
|
}
|
|
1017
1014
|
}
|
|
1018
1015
|
|
|
1019
|
-
function
|
|
1016
|
+
function recordSessionReadCursor(sessionId, events = [], {
|
|
1020
1017
|
targetPath,
|
|
1021
1018
|
agentId,
|
|
1022
1019
|
enabled = false,
|
|
1023
|
-
maxTargets = 50,
|
|
1024
1020
|
timeoutMs = SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
1025
1021
|
} = {}) {
|
|
1026
|
-
const
|
|
1022
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
1027
1023
|
const summary = {
|
|
1028
1024
|
enabled: Boolean(enabled),
|
|
1029
|
-
agentId:
|
|
1025
|
+
agentId: normalizedAgentId || "cli-user",
|
|
1030
1026
|
targetCount: 0,
|
|
1031
1027
|
attempted: 0,
|
|
1032
1028
|
recorded: 0,
|
|
@@ -1042,26 +1038,22 @@ function recordSessionReadViews(sessionId, events = [], {
|
|
|
1042
1038
|
return summary;
|
|
1043
1039
|
}
|
|
1044
1040
|
|
|
1045
|
-
|
|
1046
|
-
const targets = [];
|
|
1041
|
+
let target = null;
|
|
1047
1042
|
for (const event of Array.isArray(events) ? events : []) {
|
|
1048
|
-
const
|
|
1049
|
-
if (!
|
|
1050
|
-
|
|
1043
|
+
const candidate = sessionReadViewTarget(event);
|
|
1044
|
+
if (!candidate) continue;
|
|
1045
|
+
summary.targetCount += 1;
|
|
1046
|
+
if (
|
|
1047
|
+
!target ||
|
|
1048
|
+
Number(candidate.targetSequenceId || 0) >= Number(target.targetSequenceId || 0)
|
|
1049
|
+
) {
|
|
1050
|
+
target = candidate;
|
|
1051
1051
|
}
|
|
1052
|
-
seenTargets.add(target.key);
|
|
1053
|
-
targets.push(target);
|
|
1054
1052
|
}
|
|
1055
|
-
summary.
|
|
1053
|
+
summary.skipped = Math.max(0, summary.targetCount - (target ? 1 : 0));
|
|
1054
|
+
summary.queued = target ? 1 : 0;
|
|
1056
1055
|
|
|
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) {
|
|
1056
|
+
if (!target) {
|
|
1065
1057
|
if (!summary.reason) {
|
|
1066
1058
|
summary.reason = "no_targets";
|
|
1067
1059
|
}
|
|
@@ -1069,13 +1061,13 @@ function recordSessionReadViews(sessionId, events = [], {
|
|
|
1069
1061
|
}
|
|
1070
1062
|
|
|
1071
1063
|
summary.background = true;
|
|
1072
|
-
|
|
1073
|
-
summary.reason = "queued_best_effort";
|
|
1074
|
-
}
|
|
1064
|
+
summary.reason = "queued_monotonic_upsert";
|
|
1075
1065
|
|
|
1076
|
-
void
|
|
1066
|
+
void writeSessionReadCursor(sessionId, target, {
|
|
1077
1067
|
targetPath,
|
|
1078
|
-
|
|
1068
|
+
// Omit a placeholder identity so the server can bind human reads to the
|
|
1069
|
+
// authenticated principal. Explicit agent reads still carry their grant.
|
|
1070
|
+
agentId: normalizedAgentId,
|
|
1079
1071
|
timeoutMs,
|
|
1080
1072
|
}).catch(() => {
|
|
1081
1073
|
// Best-effort view receipts must never affect transcript rendering.
|
|
@@ -1855,13 +1847,6 @@ function normalizeSessionObservationText(rawValue, fieldName, maxLength) {
|
|
|
1855
1847
|
return value;
|
|
1856
1848
|
}
|
|
1857
1849
|
|
|
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
1850
|
function compactPayload(record = {}) {
|
|
1866
1851
|
return Object.fromEntries(
|
|
1867
1852
|
Object.entries(record).filter(([, value]) => value !== undefined)
|
|
@@ -1948,11 +1933,6 @@ async function publishListenerPresenceEvent({
|
|
|
1948
1933
|
lifecycle = {},
|
|
1949
1934
|
} = {}) {
|
|
1950
1935
|
const normalizedType = normalizeString(lifecycle.type) || "heartbeat";
|
|
1951
|
-
const eventName = listenerLifecycleEventName(normalizedType);
|
|
1952
|
-
const idempotencyStep = listenerLifecycleIdempotencyStep({
|
|
1953
|
-
...lifecycle,
|
|
1954
|
-
type: normalizedType,
|
|
1955
|
-
});
|
|
1956
1936
|
const identity = inferSessionAgentIdentity({
|
|
1957
1937
|
agentId,
|
|
1958
1938
|
model: agentModel,
|
|
@@ -1960,49 +1940,23 @@ async function publishListenerPresenceEvent({
|
|
|
1960
1940
|
provider,
|
|
1961
1941
|
clientKind,
|
|
1962
1942
|
});
|
|
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
|
-
}),
|
|
1943
|
+
return renewSessionPresence(sessionId, {
|
|
1944
|
+
targetPath,
|
|
1945
|
+
agentId,
|
|
1946
|
+
state:
|
|
1947
|
+
normalizedType === "stopped"
|
|
1948
|
+
? "stopped"
|
|
1949
|
+
: normalizeString(lifecycle.state) || normalizedType,
|
|
1950
|
+
listenerId,
|
|
1951
|
+
model: normalizeString(identity.model) || "cli",
|
|
1952
|
+
displayName: normalizeString(identity.displayName) || agentId,
|
|
1953
|
+
provider: normalizeString(identity.provider),
|
|
1954
|
+
clientKind: normalizeString(identity.clientKind) || "cli",
|
|
1955
|
+
observedAt:
|
|
1956
|
+
lifecycle.stoppedAt ||
|
|
1957
|
+
lifecycle.startedAt ||
|
|
1958
|
+
new Date().toISOString(),
|
|
2004
1959
|
});
|
|
2005
|
-
return syncSessionEventToApi(sessionId, event, { targetPath });
|
|
2006
1960
|
}
|
|
2007
1961
|
|
|
2008
1962
|
function formatListenerCatchupNotice(catchup = {}) {
|
|
@@ -2400,17 +2354,6 @@ async function resolveSessionAgentEnvelope(
|
|
|
2400
2354
|
return Object.fromEntries(Object.entries(envelope).filter(([, value]) => value !== undefined));
|
|
2401
2355
|
}
|
|
2402
2356
|
|
|
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
2357
|
async function runWithConcurrency(items = [], concurrency = 1, worker = async () => null) {
|
|
2415
2358
|
const normalizedItems = Array.isArray(items) ? items : [];
|
|
2416
2359
|
const normalizedConcurrency = Math.max(
|
|
@@ -3368,7 +3311,7 @@ export function registerSessionCommand(program) {
|
|
|
3368
3311
|
.option("--name <name>", "Agent display name (legacy alias for --agent)")
|
|
3369
3312
|
.option(
|
|
3370
3313
|
"--agent <id>",
|
|
3371
|
-
"Granted agent id to
|
|
3314
|
+
"Granted agent id to register in the participant roster. Join state and onboarding are returned locally and never appended to the transcript.",
|
|
3372
3315
|
)
|
|
3373
3316
|
.option("--invite-token <token>", "Invitation token to accept before joining the session")
|
|
3374
3317
|
.option("--seat-key <key>", "Reserved session seat key to claim while accepting an invite")
|
|
@@ -3457,13 +3400,9 @@ export function registerSessionCommand(program) {
|
|
|
3457
3400
|
const model = normalizeString(options.model) || "cli";
|
|
3458
3401
|
const hasConcreteAgentIdentity = Boolean(explicitAgent || acceptedAgentId);
|
|
3459
3402
|
|
|
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).
|
|
3403
|
+
// Registration updates the local participant projection and returns
|
|
3404
|
+
// onboarding directly to this process. It never writes join, identity,
|
|
3405
|
+
// or coaching rows into the transcript.
|
|
3467
3406
|
const joined = await registerAgent(normalizedSessionId, {
|
|
3468
3407
|
targetPath,
|
|
3469
3408
|
agentId: resolvedAgentId,
|
|
@@ -3474,13 +3413,12 @@ export function registerSessionCommand(program) {
|
|
|
3474
3413
|
trackProcessExit: false,
|
|
3475
3414
|
awaitRemoteSync: hasConcreteAgentIdentity,
|
|
3476
3415
|
});
|
|
3477
|
-
const agentJoinRelayed =
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
!resolvedAgentId.startsWith("human-");
|
|
3416
|
+
const agentJoinRelayed = false;
|
|
3417
|
+
const localBriefingPayload =
|
|
3418
|
+
joined.onboarding?.contextBriefing?.event?.payload &&
|
|
3419
|
+
typeof joined.onboarding.contextBriefing.event.payload === "object"
|
|
3420
|
+
? joined.onboarding.contextBriefing.event.payload
|
|
3421
|
+
: null;
|
|
3484
3422
|
const onboardingBrief = await writeSessionOnboardingBrief(normalizedSessionId, {
|
|
3485
3423
|
targetPath,
|
|
3486
3424
|
onboarding: acceptedOnboarding,
|
|
@@ -3521,6 +3459,17 @@ export function registerSessionCommand(program) {
|
|
|
3521
3459
|
agentCount: Number.isFinite(agentCount) ? agentCount : 0,
|
|
3522
3460
|
lastActivityAt: lastActivityIso || null,
|
|
3523
3461
|
agentJoinRelayed,
|
|
3462
|
+
onboardingBriefing: localBriefingPayload
|
|
3463
|
+
? {
|
|
3464
|
+
forAgent: localBriefingPayload.forAgent || joined.agentId,
|
|
3465
|
+
message: localBriefingPayload.message || "",
|
|
3466
|
+
recap: localBriefingPayload.recap || "",
|
|
3467
|
+
rules: localBriefingPayload.rules || null,
|
|
3468
|
+
generatedAt: localBriefingPayload.generatedAt || null,
|
|
3469
|
+
delivery: "joining_process",
|
|
3470
|
+
persisted: false,
|
|
3471
|
+
}
|
|
3472
|
+
: null,
|
|
3524
3473
|
invitationAccepted: Boolean(invitationAcceptResult?.ok || invitationAccept),
|
|
3525
3474
|
invitationAccept: invitationAccept
|
|
3526
3475
|
? {
|
|
@@ -3559,6 +3508,9 @@ export function registerSessionCommand(program) {
|
|
|
3559
3508
|
if (payload.onboardingGuide?.markdownPath) {
|
|
3560
3509
|
console.log(pc.gray(`onboarding=${payload.onboardingGuide.markdownPath}`));
|
|
3561
3510
|
}
|
|
3511
|
+
if (payload.onboardingBriefing?.recap) {
|
|
3512
|
+
console.log(payload.onboardingBriefing.recap);
|
|
3513
|
+
}
|
|
3562
3514
|
});
|
|
3563
3515
|
|
|
3564
3516
|
session
|
|
@@ -4082,10 +4034,23 @@ export function registerSessionCommand(program) {
|
|
|
4082
4034
|
if (!result.ok || !result.action) {
|
|
4083
4035
|
throw new Error(`Session action failed (${result.reason || "unknown"}).`);
|
|
4084
4036
|
}
|
|
4085
|
-
const
|
|
4086
|
-
const
|
|
4087
|
-
|
|
4088
|
-
|
|
4037
|
+
const isReadCursorProjection = normalizedActionType === "view";
|
|
4038
|
+
const actionEvent = isReadCursorProjection
|
|
4039
|
+
? null
|
|
4040
|
+
: buildSessionActionEvent(normalizedSessionId, result.action);
|
|
4041
|
+
const localAppend = isReadCursorProjection
|
|
4042
|
+
? { appended: false, reason: "read_cursor_projection", event: null }
|
|
4043
|
+
: await appendActionEventIfMissing(normalizedSessionId, actionEvent, {
|
|
4044
|
+
targetPath,
|
|
4045
|
+
});
|
|
4046
|
+
const readCursorProjection = isReadCursorProjection
|
|
4047
|
+
? {
|
|
4048
|
+
updated: Boolean(result.updated),
|
|
4049
|
+
lastReadSequenceId:
|
|
4050
|
+
Number(result.lastReadSequenceId || result.action.targetSequenceId || 0) || null,
|
|
4051
|
+
targetCursor: targetCursor || null,
|
|
4052
|
+
}
|
|
4053
|
+
: null;
|
|
4089
4054
|
const payload = {
|
|
4090
4055
|
command: commandName,
|
|
4091
4056
|
targetPath,
|
|
@@ -4098,11 +4063,22 @@ export function registerSessionCommand(program) {
|
|
|
4098
4063
|
appended: Boolean(localAppend.appended),
|
|
4099
4064
|
reason: localAppend.reason || "",
|
|
4100
4065
|
},
|
|
4066
|
+
...(readCursorProjection ? { readCursorProjection } : {}),
|
|
4101
4067
|
};
|
|
4102
4068
|
if (shouldEmitJson(options, command)) {
|
|
4103
4069
|
console.log(JSON.stringify(payload, null, 2));
|
|
4104
4070
|
return payload;
|
|
4105
4071
|
}
|
|
4072
|
+
if (readCursorProjection) {
|
|
4073
|
+
const targetLabel = readCursorProjection.lastReadSequenceId
|
|
4074
|
+
? `#${readCursorProjection.lastReadSequenceId}`
|
|
4075
|
+
: readCursorProjection.targetCursor || "target";
|
|
4076
|
+
const status = readCursorProjection.updated ? "Advanced" : "Confirmed";
|
|
4077
|
+
console.log(
|
|
4078
|
+
pc.green(`${status} read cursor through ${targetLabel}; no transcript event appended.`),
|
|
4079
|
+
);
|
|
4080
|
+
return payload;
|
|
4081
|
+
}
|
|
4106
4082
|
if (localAppend.event || actionEvent) {
|
|
4107
4083
|
console.log(formatEventLine(localAppend.event || actionEvent));
|
|
4108
4084
|
} else {
|
|
@@ -4319,15 +4295,17 @@ export function registerSessionCommand(program) {
|
|
|
4319
4295
|
if (verb === "lock") {
|
|
4320
4296
|
const result = await lockFile(normalizedSessionId, identity.agentId, file, {
|
|
4321
4297
|
intent,
|
|
4298
|
+
ttlSeconds: options.ttl,
|
|
4322
4299
|
targetPath,
|
|
4323
|
-
awaitRemoteSync: true,
|
|
4324
4300
|
});
|
|
4325
4301
|
const record = {
|
|
4326
4302
|
file: result.file || file,
|
|
4327
4303
|
locked: Boolean(result.locked),
|
|
4328
4304
|
reason: result.reason || (result.locked ? "locked" : "held_by_other_agent"),
|
|
4329
4305
|
heldBy: result.heldBy || null,
|
|
4306
|
+
intent: result.lock?.intent || result.lease?.intent || null,
|
|
4330
4307
|
since: result.since || null,
|
|
4308
|
+
expiresAt: result.lock?.expiresAt || result.lease?.expiresAt || null,
|
|
4331
4309
|
};
|
|
4332
4310
|
results.push(record);
|
|
4333
4311
|
if (result.locked) {
|
|
@@ -4341,7 +4319,6 @@ export function registerSessionCommand(program) {
|
|
|
4341
4319
|
const result = await unlockFile(normalizedSessionId, identity.agentId, file, {
|
|
4342
4320
|
reason: intent || "manual_release",
|
|
4343
4321
|
targetPath,
|
|
4344
|
-
awaitRemoteSync: true,
|
|
4345
4322
|
});
|
|
4346
4323
|
const record = {
|
|
4347
4324
|
file: result.file || file,
|
|
@@ -4349,6 +4326,7 @@ export function registerSessionCommand(program) {
|
|
|
4349
4326
|
reason: result.reason || (result.unlocked ? "unlocked" : "not_locked"),
|
|
4350
4327
|
heldBy: result.heldBy || null,
|
|
4351
4328
|
since: result.since || null,
|
|
4329
|
+
expiresAt: result.lock?.expiresAt || result.lease?.expiresAt || null,
|
|
4352
4330
|
};
|
|
4353
4331
|
results.push(record);
|
|
4354
4332
|
if (result.unlocked) {
|
|
@@ -4360,12 +4338,6 @@ export function registerSessionCommand(program) {
|
|
|
4360
4338
|
}
|
|
4361
4339
|
}
|
|
4362
4340
|
|
|
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
4341
|
const payload = {
|
|
4370
4342
|
command: `session ${verb}`,
|
|
4371
4343
|
sessionId: normalizedSessionId,
|
|
@@ -4373,16 +4345,38 @@ export function registerSessionCommand(program) {
|
|
|
4373
4345
|
files: processed,
|
|
4374
4346
|
results,
|
|
4375
4347
|
skipped,
|
|
4348
|
+
failed,
|
|
4376
4349
|
};
|
|
4377
4350
|
if (shouldEmitJson(options, command)) {
|
|
4378
4351
|
console.log(JSON.stringify(payload, null, 2));
|
|
4352
|
+
if (failed.length > 0) {
|
|
4353
|
+
process.exitCode = 2;
|
|
4354
|
+
}
|
|
4355
|
+
return payload;
|
|
4356
|
+
}
|
|
4357
|
+
if (failed.length > 0) {
|
|
4358
|
+
for (const item of failed) {
|
|
4359
|
+
const details = [
|
|
4360
|
+
item.heldBy ? `held by ${item.heldBy}` : "",
|
|
4361
|
+
item.intent ? `intent: ${item.intent}` : "",
|
|
4362
|
+
item.expiresAt ? `expires ${item.expiresAt}` : "",
|
|
4363
|
+
].filter(Boolean);
|
|
4364
|
+
console.error(
|
|
4365
|
+
pc.red(
|
|
4366
|
+
`Blocked ${item.file}: ${item.reason}${details.length > 0 ? ` (${details.join(", ")})` : ""}`,
|
|
4367
|
+
),
|
|
4368
|
+
);
|
|
4369
|
+
}
|
|
4370
|
+
process.exitCode = 2;
|
|
4379
4371
|
return payload;
|
|
4380
4372
|
}
|
|
4381
|
-
const action = verb === "lock" ? "
|
|
4373
|
+
const action = verb === "lock" ? "Acquired lease for" : "Released";
|
|
4382
4374
|
console.log(pc.green(`${action} ${processed.length} file(s) as ${identity.agentId}: ${processed.join(", ")}`));
|
|
4383
4375
|
if (verb === "lock") {
|
|
4384
4376
|
console.log(
|
|
4385
|
-
pc.gray(
|
|
4377
|
+
pc.gray(
|
|
4378
|
+
"The SentinelLayer API is authoritative; leases expire by TTL and can be released with `sl session unlock`.",
|
|
4379
|
+
),
|
|
4386
4380
|
);
|
|
4387
4381
|
}
|
|
4388
4382
|
return payload;
|
|
@@ -4390,8 +4384,9 @@ export function registerSessionCommand(program) {
|
|
|
4390
4384
|
|
|
4391
4385
|
session
|
|
4392
4386
|
.command("lock <sessionId> <files...>")
|
|
4393
|
-
.description("Claim exclusive file
|
|
4387
|
+
.description("Claim exclusive API-backed file leases (fail-closed, TTL expiry)")
|
|
4394
4388
|
.option("--intent <text>", "Why you're locking these files (shown to peers)")
|
|
4389
|
+
.option("--ttl <seconds>", "Lease TTL in seconds (15-3600)", "300")
|
|
4395
4390
|
.option("--agent <id>", "Agent id claiming the lock (defaults to the joined session agent)")
|
|
4396
4391
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4397
4392
|
.option("--json", "Emit machine-readable output")
|
|
@@ -4401,7 +4396,7 @@ export function registerSessionCommand(program) {
|
|
|
4401
4396
|
|
|
4402
4397
|
session
|
|
4403
4398
|
.command("unlock <sessionId> <files...>")
|
|
4404
|
-
.description("Release file
|
|
4399
|
+
.description("Release API-backed file leases you hold")
|
|
4405
4400
|
.option("--intent <text>", "Optional note on the release")
|
|
4406
4401
|
.option("--agent <id>", "Agent id releasing the lock (defaults to the joined session agent)")
|
|
4407
4402
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
@@ -4410,9 +4405,264 @@ export function registerSessionCommand(program) {
|
|
|
4410
4405
|
await runFileLockCommand("unlock", sessionId, files, options, command);
|
|
4411
4406
|
});
|
|
4412
4407
|
|
|
4408
|
+
session
|
|
4409
|
+
.command("renew <sessionId> <files...>")
|
|
4410
|
+
.description("Renew holder-bound file leases without writing session events")
|
|
4411
|
+
.option("--ttl <seconds>", "Renewed lease TTL in seconds (15-3600)", "300")
|
|
4412
|
+
.option("--agent <id>", "Agent id renewing the lease (defaults to the joined session agent)")
|
|
4413
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
4414
|
+
.option("--json", "Emit machine-readable output")
|
|
4415
|
+
.action(async (sessionId, files, options, command) => {
|
|
4416
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
4417
|
+
if (!normalizedSessionId) {
|
|
4418
|
+
throw new Error("session id is required.");
|
|
4419
|
+
}
|
|
4420
|
+
const fileList = (Array.isArray(files) ? files : [files])
|
|
4421
|
+
.map((file) => normalizeString(file))
|
|
4422
|
+
.filter(Boolean);
|
|
4423
|
+
if (fileList.length === 0) {
|
|
4424
|
+
throw new Error("session renew requires at least one file path.");
|
|
4425
|
+
}
|
|
4426
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4427
|
+
await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
|
|
4428
|
+
const identity = await resolveMessageActionIdentity({
|
|
4429
|
+
sessionId: normalizedSessionId,
|
|
4430
|
+
optionAgent: options.agent,
|
|
4431
|
+
targetPath,
|
|
4432
|
+
env: process.env,
|
|
4433
|
+
});
|
|
4434
|
+
if (shouldBlockImplicitCliUserSessionSay(identity)) {
|
|
4435
|
+
throw new Error(
|
|
4436
|
+
identity.identityWarning ||
|
|
4437
|
+
"session renew requires an agent identity; pass --agent <id>.",
|
|
4438
|
+
);
|
|
4439
|
+
}
|
|
4440
|
+
const results = [];
|
|
4441
|
+
for (const file of fileList) {
|
|
4442
|
+
results.push(
|
|
4443
|
+
await renewFileLease(normalizedSessionId, identity.agentId, file, {
|
|
4444
|
+
ttlSeconds: options.ttl,
|
|
4445
|
+
targetPath,
|
|
4446
|
+
}),
|
|
4447
|
+
);
|
|
4448
|
+
}
|
|
4449
|
+
const payload = {
|
|
4450
|
+
command: "session renew",
|
|
4451
|
+
sessionId: normalizedSessionId,
|
|
4452
|
+
agentId: identity.agentId,
|
|
4453
|
+
allowed: results.every((result) => result.renewed),
|
|
4454
|
+
results,
|
|
4455
|
+
};
|
|
4456
|
+
if (shouldEmitJson(options, command)) {
|
|
4457
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4458
|
+
} else if (payload.allowed) {
|
|
4459
|
+
console.log(
|
|
4460
|
+
pc.green(`Renewed ${results.length} file lease(s) as ${identity.agentId}.`),
|
|
4461
|
+
);
|
|
4462
|
+
} else {
|
|
4463
|
+
console.error(
|
|
4464
|
+
pc.red("One or more file leases could not be renewed; edit access is not assured."),
|
|
4465
|
+
);
|
|
4466
|
+
}
|
|
4467
|
+
if (!payload.allowed) {
|
|
4468
|
+
process.exitCode = 2;
|
|
4469
|
+
}
|
|
4470
|
+
return payload;
|
|
4471
|
+
});
|
|
4472
|
+
|
|
4473
|
+
session
|
|
4474
|
+
.command("guard <sessionId> <files...>")
|
|
4475
|
+
.description("Fail-closed authoritative preflight for file edits")
|
|
4476
|
+
.requiredOption("--agent <id>", "Agent id presenting holder capabilities")
|
|
4477
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
4478
|
+
.option("--json", "Emit machine-readable output")
|
|
4479
|
+
.action(async (sessionId, files, options, command) => {
|
|
4480
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
4481
|
+
if (!normalizedSessionId) {
|
|
4482
|
+
throw new Error("session id is required.");
|
|
4483
|
+
}
|
|
4484
|
+
const fileList = (Array.isArray(files) ? files : [files])
|
|
4485
|
+
.map((file) => normalizeString(file))
|
|
4486
|
+
.filter(Boolean);
|
|
4487
|
+
if (fileList.length === 0) {
|
|
4488
|
+
throw new Error("session guard requires at least one file path.");
|
|
4489
|
+
}
|
|
4490
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4491
|
+
const requestedAgentId = normalizeString(options.agent).toLowerCase();
|
|
4492
|
+
let result;
|
|
4493
|
+
try {
|
|
4494
|
+
result = await guardFileLeases(
|
|
4495
|
+
normalizedSessionId,
|
|
4496
|
+
requestedAgentId,
|
|
4497
|
+
fileList,
|
|
4498
|
+
{ targetPath },
|
|
4499
|
+
);
|
|
4500
|
+
} catch (error) {
|
|
4501
|
+
const message = safeFileLeaseErrorMessage(error);
|
|
4502
|
+
result = {
|
|
4503
|
+
ok: false,
|
|
4504
|
+
allowed: false,
|
|
4505
|
+
authoritative: false,
|
|
4506
|
+
sessionId: normalizedSessionId,
|
|
4507
|
+
holderId: requestedAgentId || null,
|
|
4508
|
+
checkedAt: new Date().toISOString(),
|
|
4509
|
+
validUntil: null,
|
|
4510
|
+
files: fileList,
|
|
4511
|
+
guarded: [],
|
|
4512
|
+
denials: fileList.map((file) => ({
|
|
4513
|
+
path: file,
|
|
4514
|
+
reason: "guard_failed",
|
|
4515
|
+
})),
|
|
4516
|
+
error: {
|
|
4517
|
+
code: normalizeString(error?.code) || "FILE_LEASE_GUARD_FAILED",
|
|
4518
|
+
message,
|
|
4519
|
+
},
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
const payload = {
|
|
4523
|
+
command: "session guard",
|
|
4524
|
+
...result,
|
|
4525
|
+
};
|
|
4526
|
+
if (shouldEmitJson(options, command)) {
|
|
4527
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4528
|
+
} else if (result.allowed) {
|
|
4529
|
+
console.log(
|
|
4530
|
+
pc.green(`File-lease guard allowed ${fileList.length} edit target(s).`),
|
|
4531
|
+
);
|
|
4532
|
+
} else {
|
|
4533
|
+
if (result.error?.message) {
|
|
4534
|
+
console.error(pc.red(`Authoritative guard failed: ${result.error.message}`));
|
|
4535
|
+
}
|
|
4536
|
+
if ((result.denials || []).length > 0) {
|
|
4537
|
+
console.error(
|
|
4538
|
+
pc.red(`Edit blocked: ${formatFileLeaseDenialSummary(result)}`),
|
|
4539
|
+
);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
if (!result.allowed) {
|
|
4543
|
+
process.exitCode = 2;
|
|
4544
|
+
}
|
|
4545
|
+
return payload;
|
|
4546
|
+
});
|
|
4547
|
+
|
|
4548
|
+
session
|
|
4549
|
+
.command("guard-hook <sessionId>")
|
|
4550
|
+
.description("Claude PreToolUse hook: read tool JSON on stdin and block denied edits")
|
|
4551
|
+
.requiredOption("--agent <id>", "Agent id presenting holder capabilities")
|
|
4552
|
+
.option("--path <path>", "Workspace root", ".")
|
|
4553
|
+
.action(async (sessionId, options) => {
|
|
4554
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4555
|
+
const result = await runFileLeaseGuardHook({
|
|
4556
|
+
sessionId,
|
|
4557
|
+
agentId: options.agent,
|
|
4558
|
+
targetPath,
|
|
4559
|
+
});
|
|
4560
|
+
process.exitCode = result.exitCode;
|
|
4561
|
+
return result;
|
|
4562
|
+
});
|
|
4563
|
+
|
|
4564
|
+
session
|
|
4565
|
+
.command("guard-install <sessionId>")
|
|
4566
|
+
.description("Install Claude, terminal, and VS Code authoritative lease preflights")
|
|
4567
|
+
.requiredOption("--agent <id>", "Agent id presenting holder capabilities")
|
|
4568
|
+
.option("--path <path>", "Workspace root", ".")
|
|
4569
|
+
.option("--json", "Emit machine-readable output")
|
|
4570
|
+
.action(async (sessionId, options, command) => {
|
|
4571
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4572
|
+
const result = await installFileLeaseIntegrations(
|
|
4573
|
+
sessionId,
|
|
4574
|
+
options.agent,
|
|
4575
|
+
{ targetPath },
|
|
4576
|
+
);
|
|
4577
|
+
if (shouldEmitJson(options, command)) {
|
|
4578
|
+
console.log(JSON.stringify({
|
|
4579
|
+
command: "session guard-install",
|
|
4580
|
+
...result,
|
|
4581
|
+
}, null, 2));
|
|
4582
|
+
} else {
|
|
4583
|
+
console.log(
|
|
4584
|
+
pc.green(
|
|
4585
|
+
"Installed Claude PreToolUse blocking plus terminal and VS Code lease preflights.",
|
|
4586
|
+
),
|
|
4587
|
+
);
|
|
4588
|
+
console.log(
|
|
4589
|
+
pc.yellow(
|
|
4590
|
+
"VS Code native Save cannot be cancelled reliably; run the generated guard task before edits or make it a dependency of mutation tasks.",
|
|
4591
|
+
),
|
|
4592
|
+
);
|
|
4593
|
+
}
|
|
4594
|
+
return result;
|
|
4595
|
+
});
|
|
4596
|
+
|
|
4597
|
+
session
|
|
4598
|
+
.command("guard-uninstall <sessionId>")
|
|
4599
|
+
.description("Safely remove only fingerprinted SentinelLayer lease preflights before CLI rollback")
|
|
4600
|
+
.requiredOption("--agent <id>", "Agent id that owns the managed integration")
|
|
4601
|
+
.option("--path <path>", "Workspace root", ".")
|
|
4602
|
+
.option("--json", "Emit machine-readable output")
|
|
4603
|
+
.action(async (sessionId, options, command) => {
|
|
4604
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4605
|
+
let result;
|
|
4606
|
+
try {
|
|
4607
|
+
result = await uninstallFileLeaseIntegrations(
|
|
4608
|
+
sessionId,
|
|
4609
|
+
options.agent,
|
|
4610
|
+
{ targetPath },
|
|
4611
|
+
);
|
|
4612
|
+
} catch (error) {
|
|
4613
|
+
result = {
|
|
4614
|
+
ok: false,
|
|
4615
|
+
uninstalled: false,
|
|
4616
|
+
reason: "guard_uninstall_failed",
|
|
4617
|
+
sessionId: normalizeString(sessionId),
|
|
4618
|
+
agentId: normalizeString(options.agent),
|
|
4619
|
+
activeLeases: [],
|
|
4620
|
+
residuals: [],
|
|
4621
|
+
error: {
|
|
4622
|
+
code: normalizeString(error?.code) || "FILE_LEASE_GUARD_UNINSTALL_FAILED",
|
|
4623
|
+
message: safeFileLeaseErrorMessage(error),
|
|
4624
|
+
},
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
const payload = {
|
|
4628
|
+
command: "session guard-uninstall",
|
|
4629
|
+
...result,
|
|
4630
|
+
};
|
|
4631
|
+
if (shouldEmitJson(options, command)) {
|
|
4632
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4633
|
+
} else if (result.ok) {
|
|
4634
|
+
console.log(pc.green("Removed the managed SentinelLayer lease preflights."));
|
|
4635
|
+
} else {
|
|
4636
|
+
if (result.error?.message) {
|
|
4637
|
+
console.error(pc.red(`Guard uninstall blocked: ${result.error.message}`));
|
|
4638
|
+
}
|
|
4639
|
+
for (const lease of result.activeLeases || []) {
|
|
4640
|
+
const file = normalizeString(lease.file || lease.path) || "(unknown path)";
|
|
4641
|
+
const holder = normalizeString(lease.agentId || lease.holderId) || "unknown";
|
|
4642
|
+
const expiresAt = normalizeString(lease.expiresAt);
|
|
4643
|
+
console.error(
|
|
4644
|
+
pc.red(
|
|
4645
|
+
`Guard uninstall blocked by ${file} (held by ${holder}${expiresAt ? `, expires ${expiresAt}` : ""}).`,
|
|
4646
|
+
),
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
4649
|
+
for (const residual of result.residuals || []) {
|
|
4650
|
+
console.error(
|
|
4651
|
+
pc.red(
|
|
4652
|
+
`Managed artifact retained: ${residual.path || residual.label || residual.artifact} (${residual.reason}).`,
|
|
4653
|
+
),
|
|
4654
|
+
);
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
if (!result.ok) {
|
|
4658
|
+
process.exitCode = 2;
|
|
4659
|
+
}
|
|
4660
|
+
return payload;
|
|
4661
|
+
});
|
|
4662
|
+
|
|
4413
4663
|
session
|
|
4414
4664
|
.command("locks <sessionId>")
|
|
4415
|
-
.description("List active file
|
|
4665
|
+
.description("List authoritative active file leases for the session")
|
|
4416
4666
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4417
4667
|
.option("--json", "Emit machine-readable output")
|
|
4418
4668
|
.action(async (sessionId, options, command) => {
|
|
@@ -4451,11 +4701,11 @@ export function registerSessionCommand(program) {
|
|
|
4451
4701
|
session
|
|
4452
4702
|
.command("listeners <sessionId>")
|
|
4453
4703
|
.description(
|
|
4454
|
-
"List
|
|
4704
|
+
"List the authoritative ephemeral listener presence roster. Presence is unknown when the server capability or Redis is unavailable; transcript heartbeats are never scanned.",
|
|
4455
4705
|
)
|
|
4456
4706
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4457
|
-
.option("--limit <n>", "
|
|
4458
|
-
.option("--max-pages <n>", "
|
|
4707
|
+
.option("--limit <n>", "Deprecated compatibility option; no event rows are scanned", "200")
|
|
4708
|
+
.option("--max-pages <n>", "Deprecated compatibility option; no event rows are scanned", "5")
|
|
4459
4709
|
.option("--json", "Emit machine-readable output")
|
|
4460
4710
|
.action(async (sessionId, options, command) => {
|
|
4461
4711
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -4512,12 +4762,18 @@ export function registerSessionCommand(program) {
|
|
|
4512
4762
|
};
|
|
4513
4763
|
}
|
|
4514
4764
|
}
|
|
4515
|
-
const live = listeners.filter(
|
|
4765
|
+
const live = listeners.filter(
|
|
4766
|
+
(row) => row.status === "present" || row.status === "active" || row.status === "idle",
|
|
4767
|
+
).length;
|
|
4516
4768
|
const payload = {
|
|
4517
4769
|
command: "session listeners",
|
|
4518
4770
|
sessionId: normalizedSessionId,
|
|
4519
4771
|
ok: Boolean(result.ok),
|
|
4520
4772
|
reason: result.ok ? undefined : result.reason,
|
|
4773
|
+
authoritative: Boolean(result.authoritative),
|
|
4774
|
+
presenceStatus: result.presenceStatus || "degraded",
|
|
4775
|
+
presenceEnabled: result.enabled ?? null,
|
|
4776
|
+
retryAfterMs: result.retryAfterMs || null,
|
|
4521
4777
|
count: listeners.length,
|
|
4522
4778
|
liveCount: live,
|
|
4523
4779
|
pageCount: Number(result.pageCount || 0),
|
|
@@ -4533,17 +4789,21 @@ export function registerSessionCommand(program) {
|
|
|
4533
4789
|
return payload;
|
|
4534
4790
|
}
|
|
4535
4791
|
if (!result.ok) {
|
|
4536
|
-
console.log(
|
|
4792
|
+
console.log(
|
|
4793
|
+
pc.yellow(
|
|
4794
|
+
`Listener presence is unknown (${result.reason}); durable heartbeat history was not scanned.`,
|
|
4795
|
+
),
|
|
4796
|
+
);
|
|
4537
4797
|
return payload;
|
|
4538
4798
|
}
|
|
4539
4799
|
if (listeners.length === 0) {
|
|
4540
|
-
console.log(pc.gray("No listeners
|
|
4800
|
+
console.log(pc.gray("No listeners are currently present."));
|
|
4541
4801
|
return payload;
|
|
4542
4802
|
}
|
|
4543
4803
|
console.log(pc.bold(`Listeners (${live} live / ${listeners.length} seen)`));
|
|
4544
4804
|
for (const row of listeners) {
|
|
4545
4805
|
const line = formatListenerLine(row);
|
|
4546
|
-
if (row.status === "active") console.log(pc.green(` ${line}`));
|
|
4806
|
+
if (row.status === "present" || row.status === "active") console.log(pc.green(` ${line}`));
|
|
4547
4807
|
else if (row.status === "idle") console.log(pc.cyan(` ${line}`));
|
|
4548
4808
|
else console.log(pc.gray(` ${line}`));
|
|
4549
4809
|
}
|
|
@@ -4563,7 +4823,7 @@ export function registerSessionCommand(program) {
|
|
|
4563
4823
|
session
|
|
4564
4824
|
.command("stop-listener <sessionId>")
|
|
4565
4825
|
.description(
|
|
4566
|
-
"Ask an agent's listener to stop
|
|
4826
|
+
"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
4827
|
)
|
|
4568
4828
|
.option("--agent <id>", "Agent whose listener to stop (omit to stop all listeners in the room)")
|
|
4569
4829
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
@@ -4576,27 +4836,21 @@ export function registerSessionCommand(program) {
|
|
|
4576
4836
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4577
4837
|
const targetAgent = normalizeString(options.agent);
|
|
4578
4838
|
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
|
-
},
|
|
4839
|
+
const listenerControl = await requestSessionListenerStop(normalizedSessionId, {
|
|
4840
|
+
targetPath,
|
|
4841
|
+
targetAgentId: targetAgent,
|
|
4589
4842
|
});
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4843
|
+
if (!listenerControl?.ok || !listenerControl?.recorded) {
|
|
4844
|
+
throw new Error(
|
|
4845
|
+
`Listener stop request failed (${normalizeString(listenerControl?.reason) || "not_recorded"}).`,
|
|
4846
|
+
);
|
|
4847
|
+
}
|
|
4594
4848
|
const payload = {
|
|
4595
4849
|
command: "session stop-listener",
|
|
4596
4850
|
sessionId: normalizedSessionId,
|
|
4597
4851
|
targetAgent: targetAgent || null,
|
|
4598
4852
|
scope: targetAgent ? "agent" : "all",
|
|
4599
|
-
|
|
4853
|
+
listenerControl,
|
|
4600
4854
|
};
|
|
4601
4855
|
if (shouldEmitJson(options, command)) {
|
|
4602
4856
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -4624,8 +4878,8 @@ export function registerSessionCommand(program) {
|
|
|
4624
4878
|
.option("--interval <seconds>", "Idle polling interval in seconds (default 60)", "60")
|
|
4625
4879
|
.option(
|
|
4626
4880
|
"--active-interval <seconds>",
|
|
4627
|
-
"Polling interval after recent human activity (default
|
|
4628
|
-
"
|
|
4881
|
+
"Polling interval after recent human activity (default 60)",
|
|
4882
|
+
"60",
|
|
4629
4883
|
)
|
|
4630
4884
|
.option(
|
|
4631
4885
|
"--active-window <seconds>",
|
|
@@ -4634,17 +4888,17 @@ export function registerSessionCommand(program) {
|
|
|
4634
4888
|
)
|
|
4635
4889
|
.option(
|
|
4636
4890
|
"--presence-interval <seconds>",
|
|
4637
|
-
"Minimum seconds between
|
|
4891
|
+
"Minimum seconds between ephemeral presence renewals (default 60)",
|
|
4638
4892
|
"60",
|
|
4639
4893
|
)
|
|
4640
4894
|
.option(
|
|
4641
4895
|
"--presence-keepalive <seconds>",
|
|
4642
|
-
"Maximum seconds between unchanged
|
|
4896
|
+
"Maximum seconds between unchanged ephemeral presence renewals (default 180)",
|
|
4643
4897
|
"180",
|
|
4644
4898
|
)
|
|
4645
4899
|
.option(
|
|
4646
4900
|
"--no-presence",
|
|
4647
|
-
"Do not
|
|
4901
|
+
"Do not renew ephemeral remote presence",
|
|
4648
4902
|
)
|
|
4649
4903
|
.option(
|
|
4650
4904
|
"--model <model>",
|
|
@@ -4653,7 +4907,7 @@ export function registerSessionCommand(program) {
|
|
|
4653
4907
|
)
|
|
4654
4908
|
.option("--display-name <name>", "Human-readable listener name for presence")
|
|
4655
4909
|
.option("--emit <format>", "Output format: ndjson or text", "ndjson")
|
|
4656
|
-
.option("--transport <mode>", "Listen transport:
|
|
4910
|
+
.option("--transport <mode>", "Listen transport: poll, auto, or stream (default poll)", "poll")
|
|
4657
4911
|
.option("--limit <n>", "Maximum events to request per poll (default 200)", "200")
|
|
4658
4912
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4659
4913
|
.option("--since <cursor>", "Override the persisted listen cursor")
|
|
@@ -4705,7 +4959,7 @@ export function registerSessionCommand(program) {
|
|
|
4705
4959
|
const activeIntervalSeconds = parsePositiveInteger(
|
|
4706
4960
|
options.activeInterval,
|
|
4707
4961
|
"active-interval",
|
|
4708
|
-
|
|
4962
|
+
60,
|
|
4709
4963
|
);
|
|
4710
4964
|
const activeWindowSeconds = parsePositiveInteger(options.activeWindow, "active-window", 300);
|
|
4711
4965
|
const presenceIntervalSeconds = parsePositiveInteger(
|
|
@@ -4781,7 +5035,7 @@ export function registerSessionCommand(program) {
|
|
|
4781
5035
|
"--wake-host requires a valid host (claude|codex) and --resume-session <host-session-id>.",
|
|
4782
5036
|
);
|
|
4783
5037
|
}
|
|
4784
|
-
const requestedTransport = normalizeString(options.transport).toLowerCase() || "
|
|
5038
|
+
const requestedTransport = normalizeString(options.transport).toLowerCase() || "poll";
|
|
4785
5039
|
if (!["auto", "stream", "poll"].includes(requestedTransport)) {
|
|
4786
5040
|
throw new Error("--transport must be one of: auto, stream, poll.");
|
|
4787
5041
|
}
|
|
@@ -4865,14 +5119,15 @@ export function registerSessionCommand(program) {
|
|
|
4865
5119
|
const ac = new AbortController();
|
|
4866
5120
|
const onSigint = () => ac.abort();
|
|
4867
5121
|
process.on("SIGINT", onSigint);
|
|
4868
|
-
const
|
|
4869
|
-
const publishPresence =
|
|
5122
|
+
const remotePresenceEnabled = options.presence !== false;
|
|
5123
|
+
const publishPresence = remotePresenceEnabled && canPublishListenerPresence(agentId);
|
|
4870
5124
|
const presenceIntervalMs = Math.max(1, presenceIntervalSeconds) * 1000;
|
|
4871
5125
|
const effectivePresenceKeepaliveSeconds =
|
|
4872
5126
|
Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds, intervalSeconds, 1);
|
|
4873
5127
|
const presenceKeepaliveMs = effectivePresenceKeepaliveSeconds * 1000;
|
|
4874
5128
|
let lastPresenceHeartbeatMs = 0;
|
|
4875
5129
|
let lastPresenceHeartbeatFingerprint = "";
|
|
5130
|
+
let nextPresenceAttemptMs = 0;
|
|
4876
5131
|
|
|
4877
5132
|
// Periodic in-session success reminders (ack, claim work, reply
|
|
4878
5133
|
// in-thread). Long-running interactive listeners only — skipped under
|
|
@@ -4920,10 +5175,10 @@ export function registerSessionCommand(program) {
|
|
|
4920
5175
|
`Listening to session ${normalizedSessionId} as ${agentId}; transport=${listenTransport} idle=${intervalSeconds}s active=${activeIntervalSeconds}s/${activeWindowSeconds}s. Press Ctrl+C to stop.`,
|
|
4921
5176
|
),
|
|
4922
5177
|
);
|
|
4923
|
-
if (!
|
|
5178
|
+
if (!remotePresenceEnabled) {
|
|
4924
5179
|
console.log(
|
|
4925
5180
|
pc.gray(
|
|
4926
|
-
"Remote listener presence is disabled; no
|
|
5181
|
+
"Remote listener presence is disabled; no liveness writes will be made.",
|
|
4927
5182
|
),
|
|
4928
5183
|
);
|
|
4929
5184
|
} else if (!publishPresence) {
|
|
@@ -4974,21 +5229,17 @@ export function registerSessionCommand(program) {
|
|
|
4974
5229
|
console.log(pc.yellow(formatListenerCatchupNotice(catchup)));
|
|
4975
5230
|
}
|
|
4976
5231
|
},
|
|
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
|
-
}
|
|
5232
|
+
onControl: async (control) => {
|
|
5233
|
+
const controlType = normalizeString(
|
|
5234
|
+
control?.type || control?.action || control?.event,
|
|
5235
|
+
).toLowerCase();
|
|
5236
|
+
if (!["stop", "listener_stop"].includes(controlType)) return;
|
|
5237
|
+
if (emitFormat !== "ndjson") {
|
|
5238
|
+
console.log(pc.yellow(`Listener stop requested for ${agentId}; exiting.`));
|
|
4991
5239
|
}
|
|
5240
|
+
ac.abort();
|
|
5241
|
+
},
|
|
5242
|
+
onEvent: async (event) => {
|
|
4992
5243
|
if (emitFormat === "ndjson") {
|
|
4993
5244
|
console.log(JSON.stringify(event));
|
|
4994
5245
|
} else {
|
|
@@ -5054,13 +5305,17 @@ export function registerSessionCommand(program) {
|
|
|
5054
5305
|
}
|
|
5055
5306
|
if (!publishPresence) return;
|
|
5056
5307
|
const lifecycleType = normalizeString(lifecycle?.type);
|
|
5308
|
+
const presenceAttemptMs = Date.now();
|
|
5309
|
+
if (lifecycleType !== "stopped" && presenceAttemptMs < nextPresenceAttemptMs) {
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5057
5312
|
const publishLifecycle = {
|
|
5058
5313
|
...lifecycle,
|
|
5059
5314
|
presenceIntervalSeconds,
|
|
5060
5315
|
presenceKeepaliveSeconds: effectivePresenceKeepaliveSeconds,
|
|
5061
5316
|
};
|
|
5062
5317
|
if (lifecycleType === "heartbeat") {
|
|
5063
|
-
const nowMs =
|
|
5318
|
+
const nowMs = presenceAttemptMs;
|
|
5064
5319
|
const heartbeatDecision = shouldPublishListenerPresenceHeartbeat({
|
|
5065
5320
|
lifecycle: publishLifecycle,
|
|
5066
5321
|
nowMs,
|
|
@@ -5075,7 +5330,7 @@ export function registerSessionCommand(program) {
|
|
|
5075
5330
|
lastPresenceHeartbeatFingerprint = heartbeatDecision.fingerprint;
|
|
5076
5331
|
lastPresenceHeartbeatMs = nowMs;
|
|
5077
5332
|
}
|
|
5078
|
-
await publishListenerPresenceEvent({
|
|
5333
|
+
const presenceResult = await publishListenerPresenceEvent({
|
|
5079
5334
|
sessionId: normalizedSessionId,
|
|
5080
5335
|
targetPath,
|
|
5081
5336
|
agentId,
|
|
@@ -5086,6 +5341,15 @@ export function registerSessionCommand(program) {
|
|
|
5086
5341
|
listenerId,
|
|
5087
5342
|
lifecycle: publishLifecycle,
|
|
5088
5343
|
});
|
|
5344
|
+
if (presenceResult?.ok) {
|
|
5345
|
+
nextPresenceAttemptMs = 0;
|
|
5346
|
+
return;
|
|
5347
|
+
}
|
|
5348
|
+
const retryAfterMs = Math.max(
|
|
5349
|
+
presenceIntervalMs,
|
|
5350
|
+
Number(presenceResult?.retryAfterMs) || 0,
|
|
5351
|
+
);
|
|
5352
|
+
nextPresenceAttemptMs = presenceAttemptMs + retryAfterMs;
|
|
5089
5353
|
},
|
|
5090
5354
|
});
|
|
5091
5355
|
} finally {
|
|
@@ -5565,6 +5829,8 @@ export function registerSessionCommand(program) {
|
|
|
5565
5829
|
let hydration = null;
|
|
5566
5830
|
let remoteTail = null;
|
|
5567
5831
|
let remoteAppend = null;
|
|
5832
|
+
let listenerPresence = null;
|
|
5833
|
+
let readCursor = null;
|
|
5568
5834
|
if (options.remote) {
|
|
5569
5835
|
hydration = await hydrateSessionFromRemote({
|
|
5570
5836
|
sessionId: normalizedSessionId,
|
|
@@ -5592,12 +5858,27 @@ export function registerSessionCommand(program) {
|
|
|
5592
5858
|
remoteAppend = await appendMissingRemoteEvents(normalizedSessionId, remoteTail.events, {
|
|
5593
5859
|
targetPath,
|
|
5594
5860
|
});
|
|
5861
|
+
readCursor = recordSessionReadCursor(normalizedSessionId, remoteTail.events, {
|
|
5862
|
+
targetPath,
|
|
5863
|
+
agentId,
|
|
5864
|
+
enabled: true,
|
|
5865
|
+
});
|
|
5595
5866
|
}
|
|
5867
|
+
listenerPresence = await fetchSessionListeners(normalizedSessionId, {
|
|
5868
|
+
targetPath,
|
|
5869
|
+
}).catch((error) => ({
|
|
5870
|
+
ok: false,
|
|
5871
|
+
authoritative: false,
|
|
5872
|
+
presenceStatus: "degraded",
|
|
5873
|
+
reason: normalizeString(error?.message) || "presence_read_failed",
|
|
5874
|
+
listeners: [],
|
|
5875
|
+
}));
|
|
5596
5876
|
}
|
|
5597
5877
|
const current = await buildSessionRecap(normalizedSessionId, {
|
|
5598
5878
|
forAgentId: agentId,
|
|
5599
5879
|
maxEvents,
|
|
5600
5880
|
targetPath,
|
|
5881
|
+
listenerPresence,
|
|
5601
5882
|
});
|
|
5602
5883
|
const payload = {
|
|
5603
5884
|
command: "session recap now",
|
|
@@ -5622,6 +5903,8 @@ export function registerSessionCommand(program) {
|
|
|
5622
5903
|
}
|
|
5623
5904
|
: null,
|
|
5624
5905
|
appendedTail: remoteAppend,
|
|
5906
|
+
listenerPresence,
|
|
5907
|
+
readCursor,
|
|
5625
5908
|
}
|
|
5626
5909
|
: null,
|
|
5627
5910
|
};
|
|
@@ -5841,11 +6124,10 @@ export function registerSessionCommand(program) {
|
|
|
5841
6124
|
? filterSessionActionEventsForTranscriptWindow(visibleActionEvents, transcriptEvents)
|
|
5842
6125
|
: visibleActionEvents;
|
|
5843
6126
|
const events = mergeSessionActionEvents(transcriptEvents, actionEvents).slice(-tail);
|
|
5844
|
-
const autoView =
|
|
6127
|
+
const autoView = recordSessionReadCursor(normalizedSessionId, events, {
|
|
5845
6128
|
targetPath,
|
|
5846
6129
|
agentId: readAgentId,
|
|
5847
6130
|
enabled: Boolean(options.remote && options.view !== false),
|
|
5848
|
-
maxTargets: process.env.SENTINELAYER_SESSION_READ_VIEW_MAX_TARGETS || 50,
|
|
5849
6131
|
timeoutMs: process.env.SENTINELAYER_SESSION_READ_VIEW_TIMEOUT_MS || SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
5850
6132
|
});
|
|
5851
6133
|
const remoteVerified = Boolean(
|
|
@@ -6054,6 +6336,66 @@ export function registerSessionCommand(program) {
|
|
|
6054
6336
|
}
|
|
6055
6337
|
});
|
|
6056
6338
|
|
|
6339
|
+
session
|
|
6340
|
+
.command("recall <need>")
|
|
6341
|
+
.description(
|
|
6342
|
+
"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)",
|
|
6343
|
+
)
|
|
6344
|
+
.requiredOption("--session <id>", "Session id to recall from")
|
|
6345
|
+
.option("--role <role>", "Optional role lens folded into the query")
|
|
6346
|
+
.option("--k <n>", "Number of memories to return (default 12, max 50)", "12")
|
|
6347
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
6348
|
+
.option("--no-remote", "Skip remote hydrate; recall over the local mirror only")
|
|
6349
|
+
.option("--include-control-events", "Index control-plane events as memories too")
|
|
6350
|
+
.option("--json", "Emit machine-readable output")
|
|
6351
|
+
.action(async (need, options, command) => {
|
|
6352
|
+
const normalizedNeed = normalizeString(need);
|
|
6353
|
+
if (normalizedNeed.length < 2) {
|
|
6354
|
+
throw new Error("need must be at least 2 characters.");
|
|
6355
|
+
}
|
|
6356
|
+
const normalizedSessionId = normalizeString(options.session);
|
|
6357
|
+
if (!normalizedSessionId) {
|
|
6358
|
+
throw new Error("--session <id> is required.");
|
|
6359
|
+
}
|
|
6360
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6361
|
+
const k = Math.min(50, parsePositiveInteger(options.k, "k", 12));
|
|
6362
|
+
const emitJson = shouldEmitJson(options, command);
|
|
6363
|
+
|
|
6364
|
+
const result = await runSessionRecall({
|
|
6365
|
+
sessionId: normalizedSessionId,
|
|
6366
|
+
need: normalizedNeed,
|
|
6367
|
+
targetPath,
|
|
6368
|
+
k,
|
|
6369
|
+
role: normalizeString(options.role),
|
|
6370
|
+
remote: options.remote !== false,
|
|
6371
|
+
includeControlEvents: Boolean(options.includeControlEvents),
|
|
6372
|
+
});
|
|
6373
|
+
|
|
6374
|
+
if (emitJson) {
|
|
6375
|
+
console.log(JSON.stringify({ command: "session recall", targetPath, ...result }, null, 2));
|
|
6376
|
+
return;
|
|
6377
|
+
}
|
|
6378
|
+
if (!result.ok) {
|
|
6379
|
+
throw new Error(`Session recall failed (${result.reason || "unknown"}).`);
|
|
6380
|
+
}
|
|
6381
|
+
if (result.backfill && result.backfill.warning) {
|
|
6382
|
+
console.log(pc.yellow(result.backfill.warning));
|
|
6383
|
+
}
|
|
6384
|
+
console.log(
|
|
6385
|
+
pc.gray(
|
|
6386
|
+
`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}).`,
|
|
6387
|
+
),
|
|
6388
|
+
);
|
|
6389
|
+
result.results.forEach((item, index) => {
|
|
6390
|
+
const sequence = item.sequenceId ? `#${item.sequenceId}` : "";
|
|
6391
|
+
console.log(`${String(index + 1).padStart(2)}. ${sequence} ${item.agentId} ${item.kind} (score ${item.score})`);
|
|
6392
|
+
if (item.snippet) {
|
|
6393
|
+
console.log(` ${item.snippet}`);
|
|
6394
|
+
}
|
|
6395
|
+
console.log(pc.gray(` why: ${item.provenance}`));
|
|
6396
|
+
});
|
|
6397
|
+
});
|
|
6398
|
+
|
|
6057
6399
|
session
|
|
6058
6400
|
.command("sync <sessionId>")
|
|
6059
6401
|
.description(
|
|
@@ -6422,7 +6764,14 @@ export function registerSessionCommand(program) {
|
|
|
6422
6764
|
throw new Error(`Session '${normalizedSessionId}' was not found.`);
|
|
6423
6765
|
}
|
|
6424
6766
|
|
|
6425
|
-
const [
|
|
6767
|
+
const [
|
|
6768
|
+
agents,
|
|
6769
|
+
runtimeRuns,
|
|
6770
|
+
leases,
|
|
6771
|
+
fileLeaseStatus,
|
|
6772
|
+
activeTasks,
|
|
6773
|
+
recentEvents,
|
|
6774
|
+
] = await Promise.all([
|
|
6426
6775
|
listAgents(normalizedSessionId, {
|
|
6427
6776
|
targetPath,
|
|
6428
6777
|
includeInactive: false,
|
|
@@ -6441,10 +6790,22 @@ export function registerSessionCommand(program) {
|
|
|
6441
6790
|
includeExpired: true,
|
|
6442
6791
|
limit: 100,
|
|
6443
6792
|
}),
|
|
6444
|
-
listFileLocks(normalizedSessionId, {
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6793
|
+
listFileLocks(normalizedSessionId, { targetPath })
|
|
6794
|
+
.then((activeFileLocks) => ({
|
|
6795
|
+
ok: true,
|
|
6796
|
+
authoritative: true,
|
|
6797
|
+
activeFileLocks,
|
|
6798
|
+
error: null,
|
|
6799
|
+
}))
|
|
6800
|
+
.catch((error) => ({
|
|
6801
|
+
ok: false,
|
|
6802
|
+
authoritative: false,
|
|
6803
|
+
activeFileLocks: [],
|
|
6804
|
+
error: {
|
|
6805
|
+
code: normalizeString(error?.code) || "FILE_LEASE_LIST_FAILED",
|
|
6806
|
+
message: safeFileLeaseErrorMessage(error),
|
|
6807
|
+
},
|
|
6808
|
+
})),
|
|
6448
6809
|
listSessionTasks(normalizedSessionId, {
|
|
6449
6810
|
targetPath,
|
|
6450
6811
|
statuses: ["PENDING", "ACCEPTED"],
|
|
@@ -6455,42 +6816,51 @@ export function registerSessionCommand(program) {
|
|
|
6455
6816
|
tail: 10,
|
|
6456
6817
|
}),
|
|
6457
6818
|
]);
|
|
6819
|
+
const fileLocks = fileLeaseStatus.activeFileLocks;
|
|
6458
6820
|
|
|
6459
6821
|
const staleAgents = detectStaleAgents(agents, {});
|
|
6460
6822
|
const staleAgentIds = new Set(staleAgents.map((agent) => normalizeString(agent.agentId)));
|
|
6461
6823
|
const activeAgents = agents.filter((agent) => !staleAgentIds.has(normalizeString(agent.agentId)));
|
|
6462
|
-
|
|
6824
|
+
const materialRecentEvents = filterSessionMaterialEvents(recentEvents);
|
|
6825
|
+
const hiddenControlEventCount = recentEvents.length - materialRecentEvents.length;
|
|
6826
|
+
let listenerRows = [];
|
|
6463
6827
|
let listenerPresence = {
|
|
6464
|
-
source: "
|
|
6465
|
-
ok:
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6828
|
+
source: "remote_presence",
|
|
6829
|
+
ok: false,
|
|
6830
|
+
authoritative: false,
|
|
6831
|
+
presenceStatus: "degraded",
|
|
6832
|
+
reason: "presence_unknown",
|
|
6833
|
+
count: 0,
|
|
6834
|
+
liveCount: 0,
|
|
6835
|
+
listeners: [],
|
|
6469
6836
|
};
|
|
6470
6837
|
try {
|
|
6471
6838
|
const remoteListeners = await fetchSessionListeners(normalizedSessionId, {
|
|
6472
6839
|
targetPath,
|
|
6473
|
-
limit: 200,
|
|
6474
|
-
maxPages: 5,
|
|
6475
6840
|
});
|
|
6476
|
-
if (remoteListeners?.ok && Array.isArray(remoteListeners.listeners)
|
|
6841
|
+
if (remoteListeners?.ok && Array.isArray(remoteListeners.listeners)) {
|
|
6477
6842
|
listenerRows = remoteListeners.listeners;
|
|
6478
6843
|
listenerPresence = {
|
|
6479
|
-
source: "
|
|
6844
|
+
source: "remote_presence",
|
|
6480
6845
|
ok: true,
|
|
6846
|
+
authoritative: true,
|
|
6847
|
+
presenceStatus: remoteListeners.presenceStatus || "ok",
|
|
6481
6848
|
count: listenerRows.length,
|
|
6482
|
-
liveCount: listenerRows.filter(
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
listenerEventCount: Number(remoteListeners.listenerEventCount || 0),
|
|
6486
|
-
partial: Boolean(remoteListeners.partial),
|
|
6849
|
+
liveCount: listenerRows.filter(
|
|
6850
|
+
(row) => row.status === "present" || row.status === "active" || row.status === "idle",
|
|
6851
|
+
).length,
|
|
6487
6852
|
listeners: listenerRows,
|
|
6488
6853
|
};
|
|
6489
6854
|
} else if (remoteListeners && remoteListeners.ok === false) {
|
|
6490
|
-
listenerPresence
|
|
6855
|
+
listenerPresence = {
|
|
6856
|
+
...listenerPresence,
|
|
6857
|
+
presenceStatus: remoteListeners.presenceStatus || "degraded",
|
|
6858
|
+
reason: normalizeString(remoteListeners.reason) || "remote_unavailable",
|
|
6859
|
+
retryAfterMs: remoteListeners.retryAfterMs || null,
|
|
6860
|
+
};
|
|
6491
6861
|
}
|
|
6492
6862
|
} catch (error) {
|
|
6493
|
-
listenerPresence.
|
|
6863
|
+
listenerPresence.reason = normalizeString(error?.message) || "remote_unavailable";
|
|
6494
6864
|
}
|
|
6495
6865
|
const liveListenerCount = listenerPresence.liveCount;
|
|
6496
6866
|
const payload = {
|
|
@@ -6505,8 +6875,14 @@ export function registerSessionCommand(program) {
|
|
|
6505
6875
|
runtimeRuns,
|
|
6506
6876
|
activeLeases: leases.assignments,
|
|
6507
6877
|
activeFileLocks: fileLocks,
|
|
6878
|
+
fileLeaseAuthority: {
|
|
6879
|
+
ok: fileLeaseStatus.ok,
|
|
6880
|
+
authoritative: fileLeaseStatus.authoritative,
|
|
6881
|
+
error: fileLeaseStatus.error,
|
|
6882
|
+
},
|
|
6508
6883
|
activeTasks: activeTasks.tasks,
|
|
6509
|
-
recentEvents,
|
|
6884
|
+
recentEvents: materialRecentEvents,
|
|
6885
|
+
hiddenControlEventCount,
|
|
6510
6886
|
};
|
|
6511
6887
|
if (shouldEmitJson(options, command)) {
|
|
6512
6888
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -6514,12 +6890,15 @@ export function registerSessionCommand(program) {
|
|
|
6514
6890
|
}
|
|
6515
6891
|
|
|
6516
6892
|
console.log(pc.bold(`Session ${normalizedSessionId}`));
|
|
6893
|
+
const listenerText = listenerPresence.authoritative
|
|
6894
|
+
? `${liveListenerCount}/${listenerRows.length}`
|
|
6895
|
+
: `unknown(${listenerPresence.presenceStatus || "degraded"})`;
|
|
6517
6896
|
console.log(
|
|
6518
6897
|
pc.gray(
|
|
6519
|
-
`status=${sessionPayload.status} active=${activeAgents.length} registered=${agents.length} stale=${staleAgents.length} listeners=${
|
|
6898
|
+
`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
6899
|
)
|
|
6521
6900
|
);
|
|
6522
|
-
for (const event of
|
|
6901
|
+
for (const event of materialRecentEvents) {
|
|
6523
6902
|
console.log(formatEventLine(event));
|
|
6524
6903
|
}
|
|
6525
6904
|
});
|