sentinelayer-cli 0.39.2 → 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/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +703 -263
- 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 +6 -3
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +194 -0
- 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";
|
|
@@ -138,6 +151,7 @@ import {
|
|
|
138
151
|
generateSessionCheckpoint,
|
|
139
152
|
generateSessionCheckpointBatch,
|
|
140
153
|
listSessionCheckpoints,
|
|
154
|
+
showSessionCheckpoint,
|
|
141
155
|
} from "../session/checkpoints.js";
|
|
142
156
|
import {
|
|
143
157
|
buildCodexExecResumeInvocation,
|
|
@@ -441,7 +455,9 @@ async function confirmSessionEventVisible(
|
|
|
441
455
|
}
|
|
442
456
|
lastReason = "not_visible";
|
|
443
457
|
const nextCursor = normalizeString(result.cursor);
|
|
444
|
-
|
|
458
|
+
const scanExhausted = result.scanExhausted ?? result.scan_exhausted;
|
|
459
|
+
const emptyScanCanContinue = !events.length && scanExhausted === false;
|
|
460
|
+
if (!nextCursor || nextCursor === pageCursor || (!events.length && !emptyScanCanContinue)) {
|
|
445
461
|
break;
|
|
446
462
|
}
|
|
447
463
|
pageCursor = nextCursor;
|
|
@@ -959,12 +975,12 @@ function normalizeAutoViewTimeoutMs(value) {
|
|
|
959
975
|
return Math.max(50, Math.min(5_000, Math.floor(parsed)));
|
|
960
976
|
}
|
|
961
977
|
|
|
962
|
-
async function
|
|
978
|
+
async function writeSessionReadCursor(sessionId, target = null, {
|
|
963
979
|
targetPath,
|
|
964
980
|
agentId,
|
|
965
981
|
timeoutMs = SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
966
982
|
} = {}) {
|
|
967
|
-
if (!
|
|
983
|
+
if (!target) {
|
|
968
984
|
return;
|
|
969
985
|
}
|
|
970
986
|
const totalTimeoutMs = normalizeAutoViewTimeoutMs(timeoutMs);
|
|
@@ -982,50 +998,31 @@ async function writeSessionReadViews(sessionId, writeTargets = [], {
|
|
|
982
998
|
}
|
|
983
999
|
|
|
984
1000
|
try {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
targetCursor: target.targetCursor,
|
|
996
|
-
metadata: {
|
|
997
|
-
source: "cli_read",
|
|
998
|
-
agentId,
|
|
999
|
-
},
|
|
1000
|
-
idempotencyKey: defaultActionIdempotencyKey({
|
|
1001
|
-
actionType: "view",
|
|
1002
|
-
targetSequenceId: target.targetSequenceId,
|
|
1003
|
-
targetCursor: target.targetCursor,
|
|
1004
|
-
agentId,
|
|
1005
|
-
}),
|
|
1006
|
-
timeoutMs: remainingMs,
|
|
1007
|
-
signal: controller.signal,
|
|
1008
|
-
});
|
|
1009
|
-
if (!result?.ok) {
|
|
1010
|
-
break;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
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
|
+
});
|
|
1013
1011
|
} finally {
|
|
1014
1012
|
clearTimeout(timeoutHandle);
|
|
1015
1013
|
}
|
|
1016
1014
|
}
|
|
1017
1015
|
|
|
1018
|
-
function
|
|
1016
|
+
function recordSessionReadCursor(sessionId, events = [], {
|
|
1019
1017
|
targetPath,
|
|
1020
1018
|
agentId,
|
|
1021
1019
|
enabled = false,
|
|
1022
|
-
maxTargets = 50,
|
|
1023
1020
|
timeoutMs = SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
1024
1021
|
} = {}) {
|
|
1025
|
-
const
|
|
1022
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
1026
1023
|
const summary = {
|
|
1027
1024
|
enabled: Boolean(enabled),
|
|
1028
|
-
agentId:
|
|
1025
|
+
agentId: normalizedAgentId || "cli-user",
|
|
1029
1026
|
targetCount: 0,
|
|
1030
1027
|
attempted: 0,
|
|
1031
1028
|
recorded: 0,
|
|
@@ -1041,26 +1038,22 @@ function recordSessionReadViews(sessionId, events = [], {
|
|
|
1041
1038
|
return summary;
|
|
1042
1039
|
}
|
|
1043
1040
|
|
|
1044
|
-
|
|
1045
|
-
const targets = [];
|
|
1041
|
+
let target = null;
|
|
1046
1042
|
for (const event of Array.isArray(events) ? events : []) {
|
|
1047
|
-
const
|
|
1048
|
-
if (!
|
|
1049
|
-
|
|
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;
|
|
1050
1051
|
}
|
|
1051
|
-
seenTargets.add(target.key);
|
|
1052
|
-
targets.push(target);
|
|
1053
|
-
}
|
|
1054
|
-
summary.targetCount = targets.length;
|
|
1055
|
-
|
|
1056
|
-
const writeTargets = maxAutoViewTargets > 0 ? targets.slice(-maxAutoViewTargets) : [];
|
|
1057
|
-
summary.skipped = Math.max(0, targets.length - writeTargets.length);
|
|
1058
|
-
summary.queued = writeTargets.length;
|
|
1059
|
-
if (summary.skipped > 0) {
|
|
1060
|
-
summary.reason = "target_cap_reached";
|
|
1061
1052
|
}
|
|
1053
|
+
summary.skipped = Math.max(0, summary.targetCount - (target ? 1 : 0));
|
|
1054
|
+
summary.queued = target ? 1 : 0;
|
|
1062
1055
|
|
|
1063
|
-
if (
|
|
1056
|
+
if (!target) {
|
|
1064
1057
|
if (!summary.reason) {
|
|
1065
1058
|
summary.reason = "no_targets";
|
|
1066
1059
|
}
|
|
@@ -1068,13 +1061,13 @@ function recordSessionReadViews(sessionId, events = [], {
|
|
|
1068
1061
|
}
|
|
1069
1062
|
|
|
1070
1063
|
summary.background = true;
|
|
1071
|
-
|
|
1072
|
-
summary.reason = "queued_best_effort";
|
|
1073
|
-
}
|
|
1064
|
+
summary.reason = "queued_monotonic_upsert";
|
|
1074
1065
|
|
|
1075
|
-
void
|
|
1066
|
+
void writeSessionReadCursor(sessionId, target, {
|
|
1076
1067
|
targetPath,
|
|
1077
|
-
|
|
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,
|
|
1078
1071
|
timeoutMs,
|
|
1079
1072
|
}).catch(() => {
|
|
1080
1073
|
// Best-effort view receipts must never affect transcript rendering.
|
|
@@ -1854,13 +1847,6 @@ function normalizeSessionObservationText(rawValue, fieldName, maxLength) {
|
|
|
1854
1847
|
return value;
|
|
1855
1848
|
}
|
|
1856
1849
|
|
|
1857
|
-
function listenerLifecycleEventName(type = "") {
|
|
1858
|
-
const normalized = normalizeString(type).toLowerCase();
|
|
1859
|
-
if (normalized === "started") return "session_listener_started";
|
|
1860
|
-
if (normalized === "stopped") return "session_listener_stopped";
|
|
1861
|
-
return "session_listener_heartbeat";
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
1850
|
function compactPayload(record = {}) {
|
|
1865
1851
|
return Object.fromEntries(
|
|
1866
1852
|
Object.entries(record).filter(([, value]) => value !== undefined)
|
|
@@ -1947,11 +1933,6 @@ async function publishListenerPresenceEvent({
|
|
|
1947
1933
|
lifecycle = {},
|
|
1948
1934
|
} = {}) {
|
|
1949
1935
|
const normalizedType = normalizeString(lifecycle.type) || "heartbeat";
|
|
1950
|
-
const eventName = listenerLifecycleEventName(normalizedType);
|
|
1951
|
-
const idempotencyStep = listenerLifecycleIdempotencyStep({
|
|
1952
|
-
...lifecycle,
|
|
1953
|
-
type: normalizedType,
|
|
1954
|
-
});
|
|
1955
1936
|
const identity = inferSessionAgentIdentity({
|
|
1956
1937
|
agentId,
|
|
1957
1938
|
model: agentModel,
|
|
@@ -1959,49 +1940,23 @@ async function publishListenerPresenceEvent({
|
|
|
1959
1940
|
provider,
|
|
1960
1941
|
clientKind,
|
|
1961
1942
|
});
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
lifecycle: normalizedType,
|
|
1979
|
-
state: normalizeString(lifecycle.state) || normalizedType,
|
|
1980
|
-
active: lifecycle.active,
|
|
1981
|
-
cursor: lifecycle.cursor || null,
|
|
1982
|
-
cursorSuffix: lifecycle.cursorSuffix,
|
|
1983
|
-
cursorSource: lifecycle.cursorSource,
|
|
1984
|
-
pollCount: lifecycle.pollCount,
|
|
1985
|
-
heartbeatCount: lifecycle.heartbeatCount,
|
|
1986
|
-
matched: lifecycle.matched,
|
|
1987
|
-
emitted: lifecycle.emitted,
|
|
1988
|
-
persistedCursor: lifecycle.persistedCursor,
|
|
1989
|
-
idleIntervalSeconds: lifecycle.idleIntervalSeconds,
|
|
1990
|
-
activeIntervalSeconds: lifecycle.activeIntervalSeconds,
|
|
1991
|
-
activeWindowSeconds: lifecycle.activeWindowSeconds,
|
|
1992
|
-
presenceIntervalSeconds: lifecycle.presenceIntervalSeconds,
|
|
1993
|
-
presenceKeepaliveSeconds: lifecycle.presenceKeepaliveSeconds,
|
|
1994
|
-
lastHumanActivityAt: lifecycle.lastHumanActivityAt,
|
|
1995
|
-
lastSleepMs: lifecycle.lastSleepMs,
|
|
1996
|
-
nextPollMs: lifecycle.nextPollMs,
|
|
1997
|
-
reason: lifecycle.reason || null,
|
|
1998
|
-
startedAt: lifecycle.startedAt,
|
|
1999
|
-
stoppedAt: lifecycle.stoppedAt,
|
|
2000
|
-
aborted: lifecycle.aborted,
|
|
2001
|
-
stopping: lifecycle.stopping,
|
|
2002
|
-
}),
|
|
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(),
|
|
2003
1959
|
});
|
|
2004
|
-
return syncSessionEventToApi(sessionId, event, { targetPath });
|
|
2005
1960
|
}
|
|
2006
1961
|
|
|
2007
1962
|
function formatListenerCatchupNotice(catchup = {}) {
|
|
@@ -2317,7 +2272,7 @@ export async function resolveMessageActionIdentity({
|
|
|
2317
2272
|
return resolveSessionSayIdentity({ sessionId, agentId: agentSeed, targetPath, env });
|
|
2318
2273
|
}
|
|
2319
2274
|
|
|
2320
|
-
async function
|
|
2275
|
+
async function recordPublishedSessionAgentActivity(
|
|
2321
2276
|
sessionId,
|
|
2322
2277
|
agent = {},
|
|
2323
2278
|
{ targetPath = process.cwd() } = {},
|
|
@@ -2328,33 +2283,27 @@ async function ensureSessionSayAgentRegistered(
|
|
|
2328
2283
|
}
|
|
2329
2284
|
|
|
2330
2285
|
try {
|
|
2331
|
-
const
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
)
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2286
|
+
const snapshot = await rememberAgentIdentity(sessionId, {
|
|
2287
|
+
agentId,
|
|
2288
|
+
model: normalizeString(agent.model) || "cli",
|
|
2289
|
+
displayName: normalizeString(agent.displayName),
|
|
2290
|
+
provider: normalizeString(agent.provider),
|
|
2291
|
+
clientKind: normalizeString(agent.clientKind) || "cli",
|
|
2292
|
+
role: sessionSayRegistryRole(agent.role),
|
|
2293
|
+
targetPath,
|
|
2294
|
+
});
|
|
2295
|
+
return {
|
|
2296
|
+
persisted: true,
|
|
2297
|
+
agentId: snapshot.agentId,
|
|
2298
|
+
lastActivityAt: snapshot.lastActivityAt,
|
|
2299
|
+
};
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
return {
|
|
2302
|
+
persisted: false,
|
|
2303
|
+
reason: "activity_update_failed",
|
|
2304
|
+
detail: normalizeString(error?.message),
|
|
2305
|
+
};
|
|
2342
2306
|
}
|
|
2343
|
-
|
|
2344
|
-
const registered = await rememberAgentIdentity(sessionId, {
|
|
2345
|
-
agentId,
|
|
2346
|
-
model: normalizeString(agent.model) || "cli",
|
|
2347
|
-
displayName: normalizeString(agent.displayName),
|
|
2348
|
-
provider: normalizeString(agent.provider),
|
|
2349
|
-
clientKind: normalizeString(agent.clientKind) || "cli",
|
|
2350
|
-
role: sessionSayRegistryRole(agent.role),
|
|
2351
|
-
targetPath,
|
|
2352
|
-
});
|
|
2353
|
-
|
|
2354
|
-
return {
|
|
2355
|
-
persisted: true,
|
|
2356
|
-
agentId: registered.agentId,
|
|
2357
|
-
};
|
|
2358
2307
|
}
|
|
2359
2308
|
|
|
2360
2309
|
async function resolveSessionAgentEnvelope(
|
|
@@ -2405,17 +2354,6 @@ async function resolveSessionAgentEnvelope(
|
|
|
2405
2354
|
return Object.fromEntries(Object.entries(envelope).filter(([, value]) => value !== undefined));
|
|
2406
2355
|
}
|
|
2407
2356
|
|
|
2408
|
-
// Builds the lock/unlock say-convention directive the session daemon parses
|
|
2409
|
-
// into the authoritative file-lock registry. Kept pure + exported for testing.
|
|
2410
|
-
export function buildSessionLockDirective(verb, file, intent = "") {
|
|
2411
|
-
const normalizedFile = normalizeString(file);
|
|
2412
|
-
const normalizedIntent = normalizeString(intent);
|
|
2413
|
-
if (verb === "unlock") {
|
|
2414
|
-
return `unlock: ${normalizedFile} - ${normalizedIntent || "done"}`;
|
|
2415
|
-
}
|
|
2416
|
-
return normalizedIntent ? `lock: ${normalizedFile} - ${normalizedIntent}` : `lock: ${normalizedFile}`;
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
2357
|
async function runWithConcurrency(items = [], concurrency = 1, worker = async () => null) {
|
|
2420
2358
|
const normalizedItems = Array.isArray(items) ? items : [];
|
|
2421
2359
|
const normalizedConcurrency = Math.max(
|
|
@@ -3373,7 +3311,7 @@ export function registerSessionCommand(program) {
|
|
|
3373
3311
|
.option("--name <name>", "Agent display name (legacy alias for --agent)")
|
|
3374
3312
|
.option(
|
|
3375
3313
|
"--agent <id>",
|
|
3376
|
-
"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.",
|
|
3377
3315
|
)
|
|
3378
3316
|
.option("--invite-token <token>", "Invitation token to accept before joining the session")
|
|
3379
3317
|
.option("--seat-key <key>", "Reserved session seat key to claim while accepting an invite")
|
|
@@ -3462,13 +3400,9 @@ export function registerSessionCommand(program) {
|
|
|
3462
3400
|
const model = normalizeString(options.model) || "cli";
|
|
3463
3401
|
const hasConcreteAgentIdentity = Boolean(explicitAgent || acceptedAgentId);
|
|
3464
3402
|
|
|
3465
|
-
//
|
|
3466
|
-
//
|
|
3467
|
-
//
|
|
3468
|
-
// the spec calls for when `--agent <granted>` or an accepted reserved
|
|
3469
|
-
// seat provides a concrete agent id. We don't double-emit; we record
|
|
3470
|
-
// whether a durable agent identity path was used
|
|
3471
|
-
// 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.
|
|
3472
3406
|
const joined = await registerAgent(normalizedSessionId, {
|
|
3473
3407
|
targetPath,
|
|
3474
3408
|
agentId: resolvedAgentId,
|
|
@@ -3479,13 +3413,12 @@ export function registerSessionCommand(program) {
|
|
|
3479
3413
|
trackProcessExit: false,
|
|
3480
3414
|
awaitRemoteSync: hasConcreteAgentIdentity,
|
|
3481
3415
|
});
|
|
3482
|
-
const agentJoinRelayed =
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
!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;
|
|
3489
3422
|
const onboardingBrief = await writeSessionOnboardingBrief(normalizedSessionId, {
|
|
3490
3423
|
targetPath,
|
|
3491
3424
|
onboarding: acceptedOnboarding,
|
|
@@ -3526,6 +3459,17 @@ export function registerSessionCommand(program) {
|
|
|
3526
3459
|
agentCount: Number.isFinite(agentCount) ? agentCount : 0,
|
|
3527
3460
|
lastActivityAt: lastActivityIso || null,
|
|
3528
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,
|
|
3529
3473
|
invitationAccepted: Boolean(invitationAcceptResult?.ok || invitationAccept),
|
|
3530
3474
|
invitationAccept: invitationAccept
|
|
3531
3475
|
? {
|
|
@@ -3564,6 +3508,9 @@ export function registerSessionCommand(program) {
|
|
|
3564
3508
|
if (payload.onboardingGuide?.markdownPath) {
|
|
3565
3509
|
console.log(pc.gray(`onboarding=${payload.onboardingGuide.markdownPath}`));
|
|
3566
3510
|
}
|
|
3511
|
+
if (payload.onboardingBriefing?.recap) {
|
|
3512
|
+
console.log(payload.onboardingBriefing.recap);
|
|
3513
|
+
}
|
|
3567
3514
|
});
|
|
3568
3515
|
|
|
3569
3516
|
session
|
|
@@ -3645,9 +3592,6 @@ export function registerSessionCommand(program) {
|
|
|
3645
3592
|
role: options.role,
|
|
3646
3593
|
displayName: options.displayName,
|
|
3647
3594
|
});
|
|
3648
|
-
const agentRegistration = await ensureSessionSayAgentRegistered(normalizedSessionId, agent, {
|
|
3649
|
-
targetPath,
|
|
3650
|
-
});
|
|
3651
3595
|
const event = createAgentEvent({
|
|
3652
3596
|
event: "session_message",
|
|
3653
3597
|
agent,
|
|
@@ -3699,6 +3643,10 @@ export function registerSessionCommand(program) {
|
|
|
3699
3643
|
targetPath,
|
|
3700
3644
|
syncRemote: false,
|
|
3701
3645
|
});
|
|
3646
|
+
const agentActivity = await recordPublishedSessionAgentActivity(normalizedSessionId, agent, {
|
|
3647
|
+
targetPath,
|
|
3648
|
+
});
|
|
3649
|
+
const agentRegistration = agentActivity;
|
|
3702
3650
|
const payload = {
|
|
3703
3651
|
command: "session say",
|
|
3704
3652
|
targetPath,
|
|
@@ -3710,6 +3658,7 @@ export function registerSessionCommand(program) {
|
|
|
3710
3658
|
identitySource: identity.source,
|
|
3711
3659
|
identityWarning: identity.identityWarning || undefined,
|
|
3712
3660
|
agentRegistration,
|
|
3661
|
+
agentActivity,
|
|
3713
3662
|
remoteSync: remoteSync || undefined,
|
|
3714
3663
|
remoteConfirmationAnchor: remoteConfirmationAnchor || undefined,
|
|
3715
3664
|
remoteConfirmation: remoteConfirmation || undefined,
|
|
@@ -3822,6 +3771,9 @@ export function registerSessionCommand(program) {
|
|
|
3822
3771
|
targetPath,
|
|
3823
3772
|
syncRemote: false,
|
|
3824
3773
|
});
|
|
3774
|
+
const agentActivity = await recordPublishedSessionAgentActivity(normalizedSessionId, agent, {
|
|
3775
|
+
targetPath,
|
|
3776
|
+
});
|
|
3825
3777
|
const payload = {
|
|
3826
3778
|
command: "session post-agent",
|
|
3827
3779
|
targetPath,
|
|
@@ -3830,6 +3782,7 @@ export function registerSessionCommand(program) {
|
|
|
3830
3782
|
event: persisted,
|
|
3831
3783
|
materializedLocalSession: localSession.materialized,
|
|
3832
3784
|
refreshedLocalSession: Boolean(localSession.refreshed),
|
|
3785
|
+
agentActivity,
|
|
3833
3786
|
remoteSync,
|
|
3834
3787
|
remoteConfirmationAnchor,
|
|
3835
3788
|
remoteConfirmation,
|
|
@@ -4081,10 +4034,23 @@ export function registerSessionCommand(program) {
|
|
|
4081
4034
|
if (!result.ok || !result.action) {
|
|
4082
4035
|
throw new Error(`Session action failed (${result.reason || "unknown"}).`);
|
|
4083
4036
|
}
|
|
4084
|
-
const
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4087
|
-
|
|
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;
|
|
4088
4054
|
const payload = {
|
|
4089
4055
|
command: commandName,
|
|
4090
4056
|
targetPath,
|
|
@@ -4097,11 +4063,22 @@ export function registerSessionCommand(program) {
|
|
|
4097
4063
|
appended: Boolean(localAppend.appended),
|
|
4098
4064
|
reason: localAppend.reason || "",
|
|
4099
4065
|
},
|
|
4066
|
+
...(readCursorProjection ? { readCursorProjection } : {}),
|
|
4100
4067
|
};
|
|
4101
4068
|
if (shouldEmitJson(options, command)) {
|
|
4102
4069
|
console.log(JSON.stringify(payload, null, 2));
|
|
4103
4070
|
return payload;
|
|
4104
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
|
+
}
|
|
4105
4082
|
if (localAppend.event || actionEvent) {
|
|
4106
4083
|
console.log(formatEventLine(localAppend.event || actionEvent));
|
|
4107
4084
|
} else {
|
|
@@ -4318,15 +4295,17 @@ export function registerSessionCommand(program) {
|
|
|
4318
4295
|
if (verb === "lock") {
|
|
4319
4296
|
const result = await lockFile(normalizedSessionId, identity.agentId, file, {
|
|
4320
4297
|
intent,
|
|
4298
|
+
ttlSeconds: options.ttl,
|
|
4321
4299
|
targetPath,
|
|
4322
|
-
awaitRemoteSync: true,
|
|
4323
4300
|
});
|
|
4324
4301
|
const record = {
|
|
4325
4302
|
file: result.file || file,
|
|
4326
4303
|
locked: Boolean(result.locked),
|
|
4327
4304
|
reason: result.reason || (result.locked ? "locked" : "held_by_other_agent"),
|
|
4328
4305
|
heldBy: result.heldBy || null,
|
|
4306
|
+
intent: result.lock?.intent || result.lease?.intent || null,
|
|
4329
4307
|
since: result.since || null,
|
|
4308
|
+
expiresAt: result.lock?.expiresAt || result.lease?.expiresAt || null,
|
|
4330
4309
|
};
|
|
4331
4310
|
results.push(record);
|
|
4332
4311
|
if (result.locked) {
|
|
@@ -4340,7 +4319,6 @@ export function registerSessionCommand(program) {
|
|
|
4340
4319
|
const result = await unlockFile(normalizedSessionId, identity.agentId, file, {
|
|
4341
4320
|
reason: intent || "manual_release",
|
|
4342
4321
|
targetPath,
|
|
4343
|
-
awaitRemoteSync: true,
|
|
4344
4322
|
});
|
|
4345
4323
|
const record = {
|
|
4346
4324
|
file: result.file || file,
|
|
@@ -4348,6 +4326,7 @@ export function registerSessionCommand(program) {
|
|
|
4348
4326
|
reason: result.reason || (result.unlocked ? "unlocked" : "not_locked"),
|
|
4349
4327
|
heldBy: result.heldBy || null,
|
|
4350
4328
|
since: result.since || null,
|
|
4329
|
+
expiresAt: result.lock?.expiresAt || result.lease?.expiresAt || null,
|
|
4351
4330
|
};
|
|
4352
4331
|
results.push(record);
|
|
4353
4332
|
if (result.unlocked) {
|
|
@@ -4359,12 +4338,6 @@ export function registerSessionCommand(program) {
|
|
|
4359
4338
|
}
|
|
4360
4339
|
}
|
|
4361
4340
|
|
|
4362
|
-
if (failed.length > 0) {
|
|
4363
|
-
const summary = failed
|
|
4364
|
-
.map((item) => `${item.file}${item.heldBy ? ` held by ${item.heldBy}` : ""}`)
|
|
4365
|
-
.join(", ");
|
|
4366
|
-
throw new Error(`session ${verb} failed for ${summary}`);
|
|
4367
|
-
}
|
|
4368
4341
|
const payload = {
|
|
4369
4342
|
command: `session ${verb}`,
|
|
4370
4343
|
sessionId: normalizedSessionId,
|
|
@@ -4372,16 +4345,38 @@ export function registerSessionCommand(program) {
|
|
|
4372
4345
|
files: processed,
|
|
4373
4346
|
results,
|
|
4374
4347
|
skipped,
|
|
4348
|
+
failed,
|
|
4375
4349
|
};
|
|
4376
4350
|
if (shouldEmitJson(options, command)) {
|
|
4377
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;
|
|
4378
4371
|
return payload;
|
|
4379
4372
|
}
|
|
4380
|
-
const action = verb === "lock" ? "
|
|
4373
|
+
const action = verb === "lock" ? "Acquired lease for" : "Released";
|
|
4381
4374
|
console.log(pc.green(`${action} ${processed.length} file(s) as ${identity.agentId}: ${processed.join(", ")}`));
|
|
4382
4375
|
if (verb === "lock") {
|
|
4383
4376
|
console.log(
|
|
4384
|
-
pc.gray(
|
|
4377
|
+
pc.gray(
|
|
4378
|
+
"The SentinelLayer API is authoritative; leases expire by TTL and can be released with `sl session unlock`.",
|
|
4379
|
+
),
|
|
4385
4380
|
);
|
|
4386
4381
|
}
|
|
4387
4382
|
return payload;
|
|
@@ -4389,8 +4384,9 @@ export function registerSessionCommand(program) {
|
|
|
4389
4384
|
|
|
4390
4385
|
session
|
|
4391
4386
|
.command("lock <sessionId> <files...>")
|
|
4392
|
-
.description("Claim exclusive file
|
|
4387
|
+
.description("Claim exclusive API-backed file leases (fail-closed, TTL expiry)")
|
|
4393
4388
|
.option("--intent <text>", "Why you're locking these files (shown to peers)")
|
|
4389
|
+
.option("--ttl <seconds>", "Lease TTL in seconds (15-3600)", "300")
|
|
4394
4390
|
.option("--agent <id>", "Agent id claiming the lock (defaults to the joined session agent)")
|
|
4395
4391
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4396
4392
|
.option("--json", "Emit machine-readable output")
|
|
@@ -4400,7 +4396,7 @@ export function registerSessionCommand(program) {
|
|
|
4400
4396
|
|
|
4401
4397
|
session
|
|
4402
4398
|
.command("unlock <sessionId> <files...>")
|
|
4403
|
-
.description("Release file
|
|
4399
|
+
.description("Release API-backed file leases you hold")
|
|
4404
4400
|
.option("--intent <text>", "Optional note on the release")
|
|
4405
4401
|
.option("--agent <id>", "Agent id releasing the lock (defaults to the joined session agent)")
|
|
4406
4402
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
@@ -4409,9 +4405,264 @@ export function registerSessionCommand(program) {
|
|
|
4409
4405
|
await runFileLockCommand("unlock", sessionId, files, options, command);
|
|
4410
4406
|
});
|
|
4411
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
|
+
|
|
4412
4663
|
session
|
|
4413
4664
|
.command("locks <sessionId>")
|
|
4414
|
-
.description("List active file
|
|
4665
|
+
.description("List authoritative active file leases for the session")
|
|
4415
4666
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4416
4667
|
.option("--json", "Emit machine-readable output")
|
|
4417
4668
|
.action(async (sessionId, options, command) => {
|
|
@@ -4450,11 +4701,11 @@ export function registerSessionCommand(program) {
|
|
|
4450
4701
|
session
|
|
4451
4702
|
.command("listeners <sessionId>")
|
|
4452
4703
|
.description(
|
|
4453
|
-
"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.",
|
|
4454
4705
|
)
|
|
4455
4706
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4456
|
-
.option("--limit <n>", "
|
|
4457
|
-
.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")
|
|
4458
4709
|
.option("--json", "Emit machine-readable output")
|
|
4459
4710
|
.action(async (sessionId, options, command) => {
|
|
4460
4711
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -4511,12 +4762,18 @@ export function registerSessionCommand(program) {
|
|
|
4511
4762
|
};
|
|
4512
4763
|
}
|
|
4513
4764
|
}
|
|
4514
|
-
const live = listeners.filter(
|
|
4765
|
+
const live = listeners.filter(
|
|
4766
|
+
(row) => row.status === "present" || row.status === "active" || row.status === "idle",
|
|
4767
|
+
).length;
|
|
4515
4768
|
const payload = {
|
|
4516
4769
|
command: "session listeners",
|
|
4517
4770
|
sessionId: normalizedSessionId,
|
|
4518
4771
|
ok: Boolean(result.ok),
|
|
4519
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,
|
|
4520
4777
|
count: listeners.length,
|
|
4521
4778
|
liveCount: live,
|
|
4522
4779
|
pageCount: Number(result.pageCount || 0),
|
|
@@ -4532,17 +4789,21 @@ export function registerSessionCommand(program) {
|
|
|
4532
4789
|
return payload;
|
|
4533
4790
|
}
|
|
4534
4791
|
if (!result.ok) {
|
|
4535
|
-
console.log(
|
|
4792
|
+
console.log(
|
|
4793
|
+
pc.yellow(
|
|
4794
|
+
`Listener presence is unknown (${result.reason}); durable heartbeat history was not scanned.`,
|
|
4795
|
+
),
|
|
4796
|
+
);
|
|
4536
4797
|
return payload;
|
|
4537
4798
|
}
|
|
4538
4799
|
if (listeners.length === 0) {
|
|
4539
|
-
console.log(pc.gray("No listeners
|
|
4800
|
+
console.log(pc.gray("No listeners are currently present."));
|
|
4540
4801
|
return payload;
|
|
4541
4802
|
}
|
|
4542
4803
|
console.log(pc.bold(`Listeners (${live} live / ${listeners.length} seen)`));
|
|
4543
4804
|
for (const row of listeners) {
|
|
4544
4805
|
const line = formatListenerLine(row);
|
|
4545
|
-
if (row.status === "active") console.log(pc.green(` ${line}`));
|
|
4806
|
+
if (row.status === "present" || row.status === "active") console.log(pc.green(` ${line}`));
|
|
4546
4807
|
else if (row.status === "idle") console.log(pc.cyan(` ${line}`));
|
|
4547
4808
|
else console.log(pc.gray(` ${line}`));
|
|
4548
4809
|
}
|
|
@@ -4562,7 +4823,7 @@ export function registerSessionCommand(program) {
|
|
|
4562
4823
|
session
|
|
4563
4824
|
.command("stop-listener <sessionId>")
|
|
4564
4825
|
.description(
|
|
4565
|
-
"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.",
|
|
4566
4827
|
)
|
|
4567
4828
|
.option("--agent <id>", "Agent whose listener to stop (omit to stop all listeners in the room)")
|
|
4568
4829
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
@@ -4575,27 +4836,21 @@ export function registerSessionCommand(program) {
|
|
|
4575
4836
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
4576
4837
|
const targetAgent = normalizeString(options.agent);
|
|
4577
4838
|
await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
|
|
4578
|
-
const
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
sessionId: normalizedSessionId,
|
|
4582
|
-
payload: {
|
|
4583
|
-
// targetAgentId routes the directive to that agent's listener (an
|
|
4584
|
-
// event recipient); omitting it broadcasts to every listener.
|
|
4585
|
-
...(targetAgent ? { targetAgentId: targetAgent } : { broadcast: true }),
|
|
4586
|
-
reason: "operator_stop",
|
|
4587
|
-
},
|
|
4839
|
+
const listenerControl = await requestSessionListenerStop(normalizedSessionId, {
|
|
4840
|
+
targetPath,
|
|
4841
|
+
targetAgentId: targetAgent,
|
|
4588
4842
|
});
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4843
|
+
if (!listenerControl?.ok || !listenerControl?.recorded) {
|
|
4844
|
+
throw new Error(
|
|
4845
|
+
`Listener stop request failed (${normalizeString(listenerControl?.reason) || "not_recorded"}).`,
|
|
4846
|
+
);
|
|
4847
|
+
}
|
|
4593
4848
|
const payload = {
|
|
4594
4849
|
command: "session stop-listener",
|
|
4595
4850
|
sessionId: normalizedSessionId,
|
|
4596
4851
|
targetAgent: targetAgent || null,
|
|
4597
4852
|
scope: targetAgent ? "agent" : "all",
|
|
4598
|
-
|
|
4853
|
+
listenerControl,
|
|
4599
4854
|
};
|
|
4600
4855
|
if (shouldEmitJson(options, command)) {
|
|
4601
4856
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -4623,8 +4878,8 @@ export function registerSessionCommand(program) {
|
|
|
4623
4878
|
.option("--interval <seconds>", "Idle polling interval in seconds (default 60)", "60")
|
|
4624
4879
|
.option(
|
|
4625
4880
|
"--active-interval <seconds>",
|
|
4626
|
-
"Polling interval after recent human activity (default
|
|
4627
|
-
"
|
|
4881
|
+
"Polling interval after recent human activity (default 60)",
|
|
4882
|
+
"60",
|
|
4628
4883
|
)
|
|
4629
4884
|
.option(
|
|
4630
4885
|
"--active-window <seconds>",
|
|
@@ -4633,17 +4888,17 @@ export function registerSessionCommand(program) {
|
|
|
4633
4888
|
)
|
|
4634
4889
|
.option(
|
|
4635
4890
|
"--presence-interval <seconds>",
|
|
4636
|
-
"Minimum seconds between
|
|
4891
|
+
"Minimum seconds between ephemeral presence renewals (default 60)",
|
|
4637
4892
|
"60",
|
|
4638
4893
|
)
|
|
4639
4894
|
.option(
|
|
4640
4895
|
"--presence-keepalive <seconds>",
|
|
4641
|
-
"Maximum seconds between unchanged
|
|
4896
|
+
"Maximum seconds between unchanged ephemeral presence renewals (default 180)",
|
|
4642
4897
|
"180",
|
|
4643
4898
|
)
|
|
4644
4899
|
.option(
|
|
4645
4900
|
"--no-presence",
|
|
4646
|
-
"Do not
|
|
4901
|
+
"Do not renew ephemeral remote presence",
|
|
4647
4902
|
)
|
|
4648
4903
|
.option(
|
|
4649
4904
|
"--model <model>",
|
|
@@ -4652,7 +4907,7 @@ export function registerSessionCommand(program) {
|
|
|
4652
4907
|
)
|
|
4653
4908
|
.option("--display-name <name>", "Human-readable listener name for presence")
|
|
4654
4909
|
.option("--emit <format>", "Output format: ndjson or text", "ndjson")
|
|
4655
|
-
.option("--transport <mode>", "Listen transport:
|
|
4910
|
+
.option("--transport <mode>", "Listen transport: poll, auto, or stream (default poll)", "poll")
|
|
4656
4911
|
.option("--limit <n>", "Maximum events to request per poll (default 200)", "200")
|
|
4657
4912
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4658
4913
|
.option("--since <cursor>", "Override the persisted listen cursor")
|
|
@@ -4704,7 +4959,7 @@ export function registerSessionCommand(program) {
|
|
|
4704
4959
|
const activeIntervalSeconds = parsePositiveInteger(
|
|
4705
4960
|
options.activeInterval,
|
|
4706
4961
|
"active-interval",
|
|
4707
|
-
|
|
4962
|
+
60,
|
|
4708
4963
|
);
|
|
4709
4964
|
const activeWindowSeconds = parsePositiveInteger(options.activeWindow, "active-window", 300);
|
|
4710
4965
|
const presenceIntervalSeconds = parsePositiveInteger(
|
|
@@ -4780,7 +5035,7 @@ export function registerSessionCommand(program) {
|
|
|
4780
5035
|
"--wake-host requires a valid host (claude|codex) and --resume-session <host-session-id>.",
|
|
4781
5036
|
);
|
|
4782
5037
|
}
|
|
4783
|
-
const requestedTransport = normalizeString(options.transport).toLowerCase() || "
|
|
5038
|
+
const requestedTransport = normalizeString(options.transport).toLowerCase() || "poll";
|
|
4784
5039
|
if (!["auto", "stream", "poll"].includes(requestedTransport)) {
|
|
4785
5040
|
throw new Error("--transport must be one of: auto, stream, poll.");
|
|
4786
5041
|
}
|
|
@@ -4864,14 +5119,15 @@ export function registerSessionCommand(program) {
|
|
|
4864
5119
|
const ac = new AbortController();
|
|
4865
5120
|
const onSigint = () => ac.abort();
|
|
4866
5121
|
process.on("SIGINT", onSigint);
|
|
4867
|
-
const
|
|
4868
|
-
const publishPresence =
|
|
5122
|
+
const remotePresenceEnabled = options.presence !== false;
|
|
5123
|
+
const publishPresence = remotePresenceEnabled && canPublishListenerPresence(agentId);
|
|
4869
5124
|
const presenceIntervalMs = Math.max(1, presenceIntervalSeconds) * 1000;
|
|
4870
5125
|
const effectivePresenceKeepaliveSeconds =
|
|
4871
5126
|
Math.max(presenceIntervalSeconds, presenceKeepaliveSeconds, intervalSeconds, 1);
|
|
4872
5127
|
const presenceKeepaliveMs = effectivePresenceKeepaliveSeconds * 1000;
|
|
4873
5128
|
let lastPresenceHeartbeatMs = 0;
|
|
4874
5129
|
let lastPresenceHeartbeatFingerprint = "";
|
|
5130
|
+
let nextPresenceAttemptMs = 0;
|
|
4875
5131
|
|
|
4876
5132
|
// Periodic in-session success reminders (ack, claim work, reply
|
|
4877
5133
|
// in-thread). Long-running interactive listeners only — skipped under
|
|
@@ -4919,10 +5175,10 @@ export function registerSessionCommand(program) {
|
|
|
4919
5175
|
`Listening to session ${normalizedSessionId} as ${agentId}; transport=${listenTransport} idle=${intervalSeconds}s active=${activeIntervalSeconds}s/${activeWindowSeconds}s. Press Ctrl+C to stop.`,
|
|
4920
5176
|
),
|
|
4921
5177
|
);
|
|
4922
|
-
if (!
|
|
5178
|
+
if (!remotePresenceEnabled) {
|
|
4923
5179
|
console.log(
|
|
4924
5180
|
pc.gray(
|
|
4925
|
-
"Remote listener presence is disabled; no
|
|
5181
|
+
"Remote listener presence is disabled; no liveness writes will be made.",
|
|
4926
5182
|
),
|
|
4927
5183
|
);
|
|
4928
5184
|
} else if (!publishPresence) {
|
|
@@ -4973,21 +5229,17 @@ export function registerSessionCommand(program) {
|
|
|
4973
5229
|
console.log(pc.yellow(formatListenerCatchupNotice(catchup)));
|
|
4974
5230
|
}
|
|
4975
5231
|
},
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
if (
|
|
4982
|
-
|
|
4983
|
-
if (!target || target === agentId) {
|
|
4984
|
-
if (emitFormat !== "ndjson") {
|
|
4985
|
-
console.log(pc.yellow(`Listener stop requested for ${agentId}; exiting.`));
|
|
4986
|
-
}
|
|
4987
|
-
ac.abort();
|
|
4988
|
-
return;
|
|
4989
|
-
}
|
|
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.`));
|
|
4990
5239
|
}
|
|
5240
|
+
ac.abort();
|
|
5241
|
+
},
|
|
5242
|
+
onEvent: async (event) => {
|
|
4991
5243
|
if (emitFormat === "ndjson") {
|
|
4992
5244
|
console.log(JSON.stringify(event));
|
|
4993
5245
|
} else {
|
|
@@ -5053,13 +5305,17 @@ export function registerSessionCommand(program) {
|
|
|
5053
5305
|
}
|
|
5054
5306
|
if (!publishPresence) return;
|
|
5055
5307
|
const lifecycleType = normalizeString(lifecycle?.type);
|
|
5308
|
+
const presenceAttemptMs = Date.now();
|
|
5309
|
+
if (lifecycleType !== "stopped" && presenceAttemptMs < nextPresenceAttemptMs) {
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5056
5312
|
const publishLifecycle = {
|
|
5057
5313
|
...lifecycle,
|
|
5058
5314
|
presenceIntervalSeconds,
|
|
5059
5315
|
presenceKeepaliveSeconds: effectivePresenceKeepaliveSeconds,
|
|
5060
5316
|
};
|
|
5061
5317
|
if (lifecycleType === "heartbeat") {
|
|
5062
|
-
const nowMs =
|
|
5318
|
+
const nowMs = presenceAttemptMs;
|
|
5063
5319
|
const heartbeatDecision = shouldPublishListenerPresenceHeartbeat({
|
|
5064
5320
|
lifecycle: publishLifecycle,
|
|
5065
5321
|
nowMs,
|
|
@@ -5074,7 +5330,7 @@ export function registerSessionCommand(program) {
|
|
|
5074
5330
|
lastPresenceHeartbeatFingerprint = heartbeatDecision.fingerprint;
|
|
5075
5331
|
lastPresenceHeartbeatMs = nowMs;
|
|
5076
5332
|
}
|
|
5077
|
-
await publishListenerPresenceEvent({
|
|
5333
|
+
const presenceResult = await publishListenerPresenceEvent({
|
|
5078
5334
|
sessionId: normalizedSessionId,
|
|
5079
5335
|
targetPath,
|
|
5080
5336
|
agentId,
|
|
@@ -5085,6 +5341,15 @@ export function registerSessionCommand(program) {
|
|
|
5085
5341
|
listenerId,
|
|
5086
5342
|
lifecycle: publishLifecycle,
|
|
5087
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;
|
|
5088
5353
|
},
|
|
5089
5354
|
});
|
|
5090
5355
|
} finally {
|
|
@@ -5564,6 +5829,8 @@ export function registerSessionCommand(program) {
|
|
|
5564
5829
|
let hydration = null;
|
|
5565
5830
|
let remoteTail = null;
|
|
5566
5831
|
let remoteAppend = null;
|
|
5832
|
+
let listenerPresence = null;
|
|
5833
|
+
let readCursor = null;
|
|
5567
5834
|
if (options.remote) {
|
|
5568
5835
|
hydration = await hydrateSessionFromRemote({
|
|
5569
5836
|
sessionId: normalizedSessionId,
|
|
@@ -5591,12 +5858,27 @@ export function registerSessionCommand(program) {
|
|
|
5591
5858
|
remoteAppend = await appendMissingRemoteEvents(normalizedSessionId, remoteTail.events, {
|
|
5592
5859
|
targetPath,
|
|
5593
5860
|
});
|
|
5861
|
+
readCursor = recordSessionReadCursor(normalizedSessionId, remoteTail.events, {
|
|
5862
|
+
targetPath,
|
|
5863
|
+
agentId,
|
|
5864
|
+
enabled: true,
|
|
5865
|
+
});
|
|
5594
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
|
+
}));
|
|
5595
5876
|
}
|
|
5596
5877
|
const current = await buildSessionRecap(normalizedSessionId, {
|
|
5597
5878
|
forAgentId: agentId,
|
|
5598
5879
|
maxEvents,
|
|
5599
5880
|
targetPath,
|
|
5881
|
+
listenerPresence,
|
|
5600
5882
|
});
|
|
5601
5883
|
const payload = {
|
|
5602
5884
|
command: "session recap now",
|
|
@@ -5621,6 +5903,8 @@ export function registerSessionCommand(program) {
|
|
|
5621
5903
|
}
|
|
5622
5904
|
: null,
|
|
5623
5905
|
appendedTail: remoteAppend,
|
|
5906
|
+
listenerPresence,
|
|
5907
|
+
readCursor,
|
|
5624
5908
|
}
|
|
5625
5909
|
: null,
|
|
5626
5910
|
};
|
|
@@ -5840,11 +6124,10 @@ export function registerSessionCommand(program) {
|
|
|
5840
6124
|
? filterSessionActionEventsForTranscriptWindow(visibleActionEvents, transcriptEvents)
|
|
5841
6125
|
: visibleActionEvents;
|
|
5842
6126
|
const events = mergeSessionActionEvents(transcriptEvents, actionEvents).slice(-tail);
|
|
5843
|
-
const autoView =
|
|
6127
|
+
const autoView = recordSessionReadCursor(normalizedSessionId, events, {
|
|
5844
6128
|
targetPath,
|
|
5845
6129
|
agentId: readAgentId,
|
|
5846
6130
|
enabled: Boolean(options.remote && options.view !== false),
|
|
5847
|
-
maxTargets: process.env.SENTINELAYER_SESSION_READ_VIEW_MAX_TARGETS || 50,
|
|
5848
6131
|
timeoutMs: process.env.SENTINELAYER_SESSION_READ_VIEW_TIMEOUT_MS || SESSION_READ_AUTO_VIEW_TIMEOUT_MS,
|
|
5849
6132
|
});
|
|
5850
6133
|
const remoteVerified = Boolean(
|
|
@@ -6053,6 +6336,66 @@ export function registerSessionCommand(program) {
|
|
|
6053
6336
|
}
|
|
6054
6337
|
});
|
|
6055
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
|
+
|
|
6056
6399
|
session
|
|
6057
6400
|
.command("sync <sessionId>")
|
|
6058
6401
|
.description(
|
|
@@ -6151,7 +6494,7 @@ export function registerSessionCommand(program) {
|
|
|
6151
6494
|
|
|
6152
6495
|
const checkpoint = session
|
|
6153
6496
|
.command("checkpoint")
|
|
6154
|
-
.description("List, create, and generate durable session checkpoints");
|
|
6497
|
+
.description("List, show, create, and generate durable session checkpoints");
|
|
6155
6498
|
|
|
6156
6499
|
checkpoint
|
|
6157
6500
|
.command("list <sessionId>")
|
|
@@ -6187,6 +6530,66 @@ export function registerSessionCommand(program) {
|
|
|
6187
6530
|
}
|
|
6188
6531
|
});
|
|
6189
6532
|
|
|
6533
|
+
checkpoint
|
|
6534
|
+
.command("show <sessionId> <checkpointId>")
|
|
6535
|
+
.description("Show a checkpoint and a bounded read-only source event window")
|
|
6536
|
+
.option("--context-events <n>", "Adjacent events to include before/after the checkpoint range (default 3)", "3")
|
|
6537
|
+
.option("--max-events <n>", "Maximum events to fetch for restore context (default 120, max 200)", "120")
|
|
6538
|
+
.option("--path <path>", "Workspace path for auth/session context", ".")
|
|
6539
|
+
.option("--json", "Emit machine-readable output")
|
|
6540
|
+
.action(async (sessionId, checkpointId, options, command) => {
|
|
6541
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
6542
|
+
const normalizedCheckpointId = normalizeString(checkpointId);
|
|
6543
|
+
if (!normalizedSessionId) {
|
|
6544
|
+
throw new Error("session id is required.");
|
|
6545
|
+
}
|
|
6546
|
+
if (!normalizedCheckpointId) {
|
|
6547
|
+
throw new Error("checkpoint id is required.");
|
|
6548
|
+
}
|
|
6549
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
6550
|
+
const result = await showSessionCheckpoint(normalizedSessionId, normalizedCheckpointId, {
|
|
6551
|
+
targetPath,
|
|
6552
|
+
contextEvents: options.contextEvents,
|
|
6553
|
+
maxEvents: options.maxEvents,
|
|
6554
|
+
});
|
|
6555
|
+
const payload = {
|
|
6556
|
+
command: "session checkpoint show",
|
|
6557
|
+
targetPath,
|
|
6558
|
+
...result,
|
|
6559
|
+
};
|
|
6560
|
+
if (shouldEmitJson(options, command)) {
|
|
6561
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6564
|
+
|
|
6565
|
+
console.log(pc.bold(`Checkpoint ${result.checkpointId}`));
|
|
6566
|
+
console.log(formatCheckpointLine(result.checkpoint));
|
|
6567
|
+
const sourceRange = `#${result.window.startSequence}-${result.window.endSequence}`;
|
|
6568
|
+
const contextSummary =
|
|
6569
|
+
`${result.window.sourceEventCount}/${result.window.sourceEventCountExpected} source` +
|
|
6570
|
+
`, ${result.window.beforeContextCount} before` +
|
|
6571
|
+
`, ${result.window.afterContextCount} after`;
|
|
6572
|
+
console.log(pc.gray(`Read-only restore window ${sourceRange}: ${contextSummary}.`));
|
|
6573
|
+
if (!result.ok) {
|
|
6574
|
+
console.log(pc.yellow(`Remote event window unavailable: ${result.reason || "unknown"}`));
|
|
6575
|
+
return;
|
|
6576
|
+
}
|
|
6577
|
+
if (result.window.partial) {
|
|
6578
|
+
const missing = result.window.missingSourceEvents
|
|
6579
|
+
? ` missing=${result.window.missingSourceEvents}`
|
|
6580
|
+
: "";
|
|
6581
|
+
const truncated = result.window.truncatedByLimit ? " truncated-by-limit" : "";
|
|
6582
|
+
console.log(pc.yellow(`Partial checkpoint source range:${missing}${truncated}`.trim()));
|
|
6583
|
+
}
|
|
6584
|
+
for (const event of result.window.events) {
|
|
6585
|
+
const sequence = eventSequenceNumber(event);
|
|
6586
|
+
const inSource =
|
|
6587
|
+
sequence >= result.window.startSequence &&
|
|
6588
|
+
sequence <= result.window.endSequence;
|
|
6589
|
+
console.log(`${inSource ? "*" : " "} ${formatEventLine(event)}`);
|
|
6590
|
+
}
|
|
6591
|
+
});
|
|
6592
|
+
|
|
6190
6593
|
checkpoint
|
|
6191
6594
|
.command("create <sessionId>")
|
|
6192
6595
|
.description("Create a durable checkpoint anchored to a canonical sequence range")
|
|
@@ -6361,7 +6764,14 @@ export function registerSessionCommand(program) {
|
|
|
6361
6764
|
throw new Error(`Session '${normalizedSessionId}' was not found.`);
|
|
6362
6765
|
}
|
|
6363
6766
|
|
|
6364
|
-
const [
|
|
6767
|
+
const [
|
|
6768
|
+
agents,
|
|
6769
|
+
runtimeRuns,
|
|
6770
|
+
leases,
|
|
6771
|
+
fileLeaseStatus,
|
|
6772
|
+
activeTasks,
|
|
6773
|
+
recentEvents,
|
|
6774
|
+
] = await Promise.all([
|
|
6365
6775
|
listAgents(normalizedSessionId, {
|
|
6366
6776
|
targetPath,
|
|
6367
6777
|
includeInactive: false,
|
|
@@ -6380,10 +6790,22 @@ export function registerSessionCommand(program) {
|
|
|
6380
6790
|
includeExpired: true,
|
|
6381
6791
|
limit: 100,
|
|
6382
6792
|
}),
|
|
6383
|
-
listFileLocks(normalizedSessionId, {
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
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
|
+
})),
|
|
6387
6809
|
listSessionTasks(normalizedSessionId, {
|
|
6388
6810
|
targetPath,
|
|
6389
6811
|
statuses: ["PENDING", "ACCEPTED"],
|
|
@@ -6394,42 +6816,51 @@ export function registerSessionCommand(program) {
|
|
|
6394
6816
|
tail: 10,
|
|
6395
6817
|
}),
|
|
6396
6818
|
]);
|
|
6819
|
+
const fileLocks = fileLeaseStatus.activeFileLocks;
|
|
6397
6820
|
|
|
6398
6821
|
const staleAgents = detectStaleAgents(agents, {});
|
|
6399
6822
|
const staleAgentIds = new Set(staleAgents.map((agent) => normalizeString(agent.agentId)));
|
|
6400
6823
|
const activeAgents = agents.filter((agent) => !staleAgentIds.has(normalizeString(agent.agentId)));
|
|
6401
|
-
|
|
6824
|
+
const materialRecentEvents = filterSessionMaterialEvents(recentEvents);
|
|
6825
|
+
const hiddenControlEventCount = recentEvents.length - materialRecentEvents.length;
|
|
6826
|
+
let listenerRows = [];
|
|
6402
6827
|
let listenerPresence = {
|
|
6403
|
-
source: "
|
|
6404
|
-
ok:
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
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: [],
|
|
6408
6836
|
};
|
|
6409
6837
|
try {
|
|
6410
6838
|
const remoteListeners = await fetchSessionListeners(normalizedSessionId, {
|
|
6411
6839
|
targetPath,
|
|
6412
|
-
limit: 200,
|
|
6413
|
-
maxPages: 5,
|
|
6414
6840
|
});
|
|
6415
|
-
if (remoteListeners?.ok && Array.isArray(remoteListeners.listeners)
|
|
6841
|
+
if (remoteListeners?.ok && Array.isArray(remoteListeners.listeners)) {
|
|
6416
6842
|
listenerRows = remoteListeners.listeners;
|
|
6417
6843
|
listenerPresence = {
|
|
6418
|
-
source: "
|
|
6844
|
+
source: "remote_presence",
|
|
6419
6845
|
ok: true,
|
|
6846
|
+
authoritative: true,
|
|
6847
|
+
presenceStatus: remoteListeners.presenceStatus || "ok",
|
|
6420
6848
|
count: listenerRows.length,
|
|
6421
|
-
liveCount: listenerRows.filter(
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
listenerEventCount: Number(remoteListeners.listenerEventCount || 0),
|
|
6425
|
-
partial: Boolean(remoteListeners.partial),
|
|
6849
|
+
liveCount: listenerRows.filter(
|
|
6850
|
+
(row) => row.status === "present" || row.status === "active" || row.status === "idle",
|
|
6851
|
+
).length,
|
|
6426
6852
|
listeners: listenerRows,
|
|
6427
6853
|
};
|
|
6428
6854
|
} else if (remoteListeners && remoteListeners.ok === false) {
|
|
6429
|
-
listenerPresence
|
|
6855
|
+
listenerPresence = {
|
|
6856
|
+
...listenerPresence,
|
|
6857
|
+
presenceStatus: remoteListeners.presenceStatus || "degraded",
|
|
6858
|
+
reason: normalizeString(remoteListeners.reason) || "remote_unavailable",
|
|
6859
|
+
retryAfterMs: remoteListeners.retryAfterMs || null,
|
|
6860
|
+
};
|
|
6430
6861
|
}
|
|
6431
6862
|
} catch (error) {
|
|
6432
|
-
listenerPresence.
|
|
6863
|
+
listenerPresence.reason = normalizeString(error?.message) || "remote_unavailable";
|
|
6433
6864
|
}
|
|
6434
6865
|
const liveListenerCount = listenerPresence.liveCount;
|
|
6435
6866
|
const payload = {
|
|
@@ -6444,8 +6875,14 @@ export function registerSessionCommand(program) {
|
|
|
6444
6875
|
runtimeRuns,
|
|
6445
6876
|
activeLeases: leases.assignments,
|
|
6446
6877
|
activeFileLocks: fileLocks,
|
|
6878
|
+
fileLeaseAuthority: {
|
|
6879
|
+
ok: fileLeaseStatus.ok,
|
|
6880
|
+
authoritative: fileLeaseStatus.authoritative,
|
|
6881
|
+
error: fileLeaseStatus.error,
|
|
6882
|
+
},
|
|
6447
6883
|
activeTasks: activeTasks.tasks,
|
|
6448
|
-
recentEvents,
|
|
6884
|
+
recentEvents: materialRecentEvents,
|
|
6885
|
+
hiddenControlEventCount,
|
|
6449
6886
|
};
|
|
6450
6887
|
if (shouldEmitJson(options, command)) {
|
|
6451
6888
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -6453,12 +6890,15 @@ export function registerSessionCommand(program) {
|
|
|
6453
6890
|
}
|
|
6454
6891
|
|
|
6455
6892
|
console.log(pc.bold(`Session ${normalizedSessionId}`));
|
|
6893
|
+
const listenerText = listenerPresence.authoritative
|
|
6894
|
+
? `${liveListenerCount}/${listenerRows.length}`
|
|
6895
|
+
: `unknown(${listenerPresence.presenceStatus || "degraded"})`;
|
|
6456
6896
|
console.log(
|
|
6457
6897
|
pc.gray(
|
|
6458
|
-
`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}`
|
|
6459
6899
|
)
|
|
6460
6900
|
);
|
|
6461
|
-
for (const event of
|
|
6901
|
+
for (const event of materialRecentEvents) {
|
|
6462
6902
|
console.log(formatEventLine(event));
|
|
6463
6903
|
}
|
|
6464
6904
|
});
|