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.
Files changed (62) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -3
  3. package/src/agents/ai-governance/tools/base.js +19 -166
  4. package/src/agents/backend/tools/base.js +33 -186
  5. package/src/agents/code-quality/tools/base.js +27 -154
  6. package/src/agents/data-layer/tools/base.js +35 -180
  7. package/src/agents/documentation/tools/base.js +18 -146
  8. package/src/agents/infrastructure/tools/base.js +17 -164
  9. package/src/agents/observability/tools/base.js +35 -180
  10. package/src/agents/performance/tools/base.js +35 -205
  11. package/src/agents/release/tools/base.js +35 -180
  12. package/src/agents/reliability/tools/base.js +35 -180
  13. package/src/agents/security/tools/base.js +33 -187
  14. package/src/agents/shared-tools/domain-base.js +246 -0
  15. package/src/agents/supply-chain/tools/base.js +15 -144
  16. package/src/agents/testing/tools/base.js +33 -184
  17. package/src/cli.js +5 -0
  18. package/src/commands/mcp.js +1 -1
  19. package/src/commands/ring-owner.js +58 -0
  20. package/src/commands/session.js +703 -263
  21. package/src/engram/governance.js +67 -0
  22. package/src/engram/index.js +64 -0
  23. package/src/engram/namespace.js +89 -0
  24. package/src/engram/observations.js +34 -0
  25. package/src/engram/sla.js +104 -0
  26. package/src/engram/store.js +115 -0
  27. package/src/engram/tools.js +146 -0
  28. package/src/engram/trust.js +105 -0
  29. package/src/legacy-cli.js +6 -3
  30. package/src/mcp/registry.js +27 -10
  31. package/src/mcp/session-stdio-server.js +209 -18
  32. package/src/mcp/smoke.js +9 -8
  33. package/src/pocket/ring-owner.js +97 -0
  34. package/src/review/local-review.js +9 -5
  35. package/src/review/omargate-orchestrator.js +111 -14
  36. package/src/review/spec-binding.js +16 -1
  37. package/src/scan/generator.js +1 -1
  38. package/src/session/agent-registry.js +22 -63
  39. package/src/session/checkpoints.js +194 -0
  40. package/src/session/coordination-guidance.js +6 -6
  41. package/src/session/daemon.js +19 -124
  42. package/src/session/file-lease-integrations.js +1300 -0
  43. package/src/session/file-locks.js +967 -464
  44. package/src/session/listener.js +243 -20
  45. package/src/session/listeners.js +71 -80
  46. package/src/session/paths.js +2 -0
  47. package/src/session/recall/embedder.js +181 -0
  48. package/src/session/recall/entities.js +0 -0
  49. package/src/session/recall/index-build.js +38 -0
  50. package/src/session/recall/index-core.js +108 -0
  51. package/src/session/recall/index.js +131 -0
  52. package/src/session/recall/needle-eval.js +279 -0
  53. package/src/session/recall/observation-core.js +183 -0
  54. package/src/session/recall/observations.js +21 -0
  55. package/src/session/recall/provenance.js +153 -0
  56. package/src/session/recall/retrieve.js +374 -0
  57. package/src/session/recall/text.js +0 -0
  58. package/src/session/recall/token-stats.js +81 -0
  59. package/src/session/recap.js +29 -23
  60. package/src/session/remote-hydrate.js +5 -1
  61. package/src/session/sync.js +661 -11
  62. package/src/session/wake/pump.js +8 -3
@@ -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, summarizeListeners } from "../session/listeners.js";
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
- if (!events.length || !nextCursor || nextCursor === pageCursor) {
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 writeSessionReadViews(sessionId, writeTargets = [], {
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 (!Array.isArray(writeTargets) || writeTargets.length === 0) {
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
- for (const target of writeTargets) {
986
- if (controller.signal.aborted) {
987
- break;
988
- }
989
- const elapsedMs = Math.max(0, Date.now() - startedAt);
990
- const remainingMs = Math.max(1, totalTimeoutMs - elapsedMs);
991
- const result = await createSessionMessageAction(sessionId, {
992
- actionType: "view",
993
- targetPath,
994
- targetSequenceId: target.targetSequenceId,
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 recordSessionReadViews(sessionId, events = [], {
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 maxAutoViewTargets = Math.max(0, Math.min(200, Number(maxTargets) || 0));
1022
+ const normalizedAgentId = normalizeString(agentId);
1026
1023
  const summary = {
1027
1024
  enabled: Boolean(enabled),
1028
- agentId: normalizeString(agentId) || "cli-user",
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
- const seenTargets = new Set();
1045
- const targets = [];
1041
+ let target = null;
1046
1042
  for (const event of Array.isArray(events) ? events : []) {
1047
- const target = sessionReadViewTarget(event);
1048
- if (!target || seenTargets.has(target.key)) {
1049
- continue;
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 (writeTargets.length === 0) {
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
- if (!summary.reason) {
1072
- summary.reason = "queued_best_effort";
1073
- }
1064
+ summary.reason = "queued_monotonic_upsert";
1074
1065
 
1075
- void writeSessionReadViews(sessionId, writeTargets, {
1066
+ void writeSessionReadCursor(sessionId, target, {
1076
1067
  targetPath,
1077
- agentId: summary.agentId,
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
- const event = createAgentEvent({
1963
- event: eventName,
1964
- sessionId,
1965
- agent: {
1966
- id: agentId,
1967
- model: normalizeString(identity.model) || "cli",
1968
- role: "listener",
1969
- displayName: normalizeString(identity.displayName) || agentId,
1970
- provider: normalizeString(identity.provider) || undefined,
1971
- clientKind: normalizeString(identity.clientKind) || "cli",
1972
- },
1973
- eventId: `session-listener-${listenerId}-${normalizedType}-${idempotencyStep}`,
1974
- idempotencyToken: `session-listener:${listenerId}:${normalizedType}:${idempotencyStep}`,
1975
- payload: compactPayload({
1976
- source: "session_listen",
1977
- listenerId,
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 ensureSessionSayAgentRegistered(
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 activeAgents = await listAgents(sessionId, { targetPath, includeInactive: false });
2332
- if (
2333
- activeAgents.some(
2334
- (existing) => normalizeString(existing.agentId).toLowerCase() === agentId.toLowerCase(),
2335
- )
2336
- ) {
2337
- return { persisted: false, reason: "already_registered" };
2338
- }
2339
- } catch {
2340
- // If the local registry is unreadable, let rememberAgentIdentity surface the
2341
- // filesystem problem with its normal error message.
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 emit an agent_join event as. Behaves like post-agent for human/placeholder ids — those are recorded in the local registry only.",
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
- // `registerAgent` already writes the canonical `agent_join` event to the
3466
- // local NDJSON and best-effort relays it to /events via appendToStream
3467
- // → syncSessionEventToApi. That gives us the exact `post-agent` parity
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
- joined.emittedJoinEvent !== false &&
3484
- hasConcreteAgentIdentity &&
3485
- Boolean(resolvedAgentId) &&
3486
- resolvedAgentId !== "cli-user" &&
3487
- resolvedAgentId !== "unknown" &&
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 actionEvent = buildSessionActionEvent(normalizedSessionId, result.action);
4085
- const localAppend = await appendActionEventIfMissing(normalizedSessionId, actionEvent, {
4086
- targetPath,
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" ? "Requested lock on" : "Released";
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("Senti enforces fail-closed; locks auto-release on TTL. Release with `sl session unlock`."),
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 locks via Senti (fail-closed, TTL auto-release)")
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 locks you hold (Senti only lets the holder release)")
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 locks for the session (who holds what, and when they expire)")
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 who is actively listening to the session and at what poll cadence (active/idle/stale/stopped), derived from listener presence heartbeats. Mirrors the web roster.",
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>", "Recent events to scan for heartbeats (default 200)", "200")
4457
- .option("--max-pages <n>", "Maximum older event pages to scan for heartbeats (default 5)", "5")
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((row) => row.status === "active" || row.status === "idle").length;
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(pc.yellow(`Could not read listeners (${result.reason}).`));
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 detected (no recent presence heartbeats)."));
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 (save energy). Posts a listener_stop directive the listener honors on its next poll, then exits cleanly. Targets one agent with --agent; omit it to stop every listener in the room.",
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 event = createAgentEvent({
4579
- event: "listener_stop",
4580
- agent: { id: "session-control", model: "control", persona: "Session Control" },
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
- const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, { targetPath }).catch(
4590
- (error) => ({ synced: false, reason: normalizeString(error?.message) || "sync_failed" }),
4591
- );
4592
- await appendToStream(normalizedSessionId, event, { targetPath, syncRemote: false }).catch(() => {});
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
- remoteSync: remoteSync || undefined,
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 5)",
4627
- "5",
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 remote listener heartbeat events (default 60)",
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 remote listener heartbeat events (default 180)",
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 publish durable listener lifecycle/heartbeat events",
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: auto, stream, or poll (default auto)", "auto")
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
- 5,
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() || "auto";
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 durablePresenceEnabled = options.presence !== false;
4868
- const publishPresence = durablePresenceEnabled && canPublishListenerPresence(agentId);
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 (!durablePresenceEnabled) {
5178
+ if (!remotePresenceEnabled) {
4923
5179
  console.log(
4924
5180
  pc.gray(
4925
- "Remote listener presence is disabled; no durable lifecycle events will be written.",
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
- onEvent: async (event) => {
4977
- // Cut-listener: a `listener_stop` directive addressed to this
4978
- // agent (from the web "stop listening" control or
4979
- // `sl session stop-listener`) cleanly exits this listener to save
4980
- // energy. Untargeted (no targetAgentId) stops every listener.
4981
- if (normalizeString(event?.event) === "listener_stop") {
4982
- const target = normalizeString(event?.payload?.targetAgentId);
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 = Date.now();
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 = recordSessionReadViews(normalizedSessionId, events, {
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 [agents, runtimeRuns, leases, fileLocks, activeTasks, recentEvents] = await Promise.all([
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
- targetPath,
6385
- emitExpiredEvents: false,
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
- let listenerRows = summarizeListeners(recentEvents);
6824
+ const materialRecentEvents = filterSessionMaterialEvents(recentEvents);
6825
+ const hiddenControlEventCount = recentEvents.length - materialRecentEvents.length;
6826
+ let listenerRows = [];
6402
6827
  let listenerPresence = {
6403
- source: "local_recent_events",
6404
- ok: true,
6405
- count: listenerRows.length,
6406
- liveCount: listenerRows.filter((row) => row.status === "active" || row.status === "idle").length,
6407
- listeners: listenerRows,
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) && remoteListeners.listeners.length > 0) {
6841
+ if (remoteListeners?.ok && Array.isArray(remoteListeners.listeners)) {
6416
6842
  listenerRows = remoteListeners.listeners;
6417
6843
  listenerPresence = {
6418
- source: "remote_events",
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((row) => row.status === "active" || row.status === "idle").length,
6422
- pageCount: Number(remoteListeners.pageCount || 0),
6423
- scannedEventCount: Number(remoteListeners.scannedEventCount || 0),
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.remoteReason = normalizeString(remoteListeners.reason) || "remote_unavailable";
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.remoteReason = normalizeString(error?.message) || "remote_unavailable";
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=${liveListenerCount}/${listenerRows.length} runs=${runtimeRuns.length} leases=${leases.assignments.length} locks=${fileLocks.length} tasks=${activeTasks.tasks.length}`
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 recentEvents) {
6901
+ for (const event of materialRecentEvents) {
6462
6902
  console.log(formatEventLine(event));
6463
6903
  }
6464
6904
  });