sentinelayer-cli 0.30.12 → 0.30.14

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.
@@ -38,6 +38,10 @@ function normalizePositiveInteger(value, fallbackValue) {
38
38
  return Math.floor(normalized);
39
39
  }
40
40
 
41
+ function isListenerStopDirective(event = {}) {
42
+ return normalizeString(event?.event) === "listener_stop";
43
+ }
44
+
41
45
  function isPlainObject(value) {
42
46
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
43
47
  }
@@ -255,6 +259,8 @@ export async function listenSessionEvents({
255
259
  _readCursor = readSyncCursor,
256
260
  _writeCursor = writeSyncCursor,
257
261
  _sleep = defaultSleep,
262
+ _setInterval = setInterval,
263
+ _clearInterval = clearInterval,
258
264
  _nowMs = Date.now,
259
265
  } = {}) {
260
266
  const normalizedSessionId = normalizeString(sessionId);
@@ -394,6 +400,7 @@ export async function listenSessionEvents({
394
400
  const visibleEvents = [];
395
401
  let preStartEventCount = 0;
396
402
  for (const event of events) {
403
+ if (!isPlainObject(event)) continue;
397
404
  const timestampMs = eventTimestampMs(event);
398
405
  if (!timestampMs || timestampMs < startedAtMs) {
399
406
  preStartEventCount += 1;
@@ -402,6 +409,7 @@ export async function listenSessionEvents({
402
409
  if (isRecentActivity(activityMs, observedAtMs, activeWindowMs)) {
403
410
  lastHumanActivityMs = Math.max(lastHumanActivityMs, activityMs);
404
411
  }
412
+ if (timestampMs && timestampMs < startedAtMs && isListenerStopDirective(event)) continue;
405
413
  if (isSessionListenerLifecycleEvent(event)) continue;
406
414
  if (!eventMatchesAgent(event, normalizedAgentId)) continue;
407
415
  visibleEvents.push(event);
@@ -441,9 +449,14 @@ export async function listenSessionEvents({
441
449
  if (emittedKeys.has(key)) continue;
442
450
  matched += 1;
443
451
  if (!shouldEmitBatch && eventTimestampMs(event) < startedAtMs) continue;
444
- await onEvent(event);
445
452
  emittedKeys.add(key);
446
- emitted += 1;
453
+ try {
454
+ await onEvent(event);
455
+ emitted += 1;
456
+ } catch (error) {
457
+ emittedKeys.delete(key);
458
+ throw error;
459
+ }
447
460
  }
448
461
 
449
462
  if (nextCursor && nextCursor !== cursor) {
@@ -473,6 +486,32 @@ export async function listenSessionEvents({
473
486
  return humanActive;
474
487
  }
475
488
 
489
+ let streamHeartbeatTimer = null;
490
+ let streamHeartbeatInFlight = false;
491
+ const streamHeartbeatMs = Math.max(1_000, Math.min(idleSleepMs, 60_000));
492
+
493
+ function stopStreamHeartbeatTimer() {
494
+ if (!streamHeartbeatTimer) return;
495
+ _clearInterval(streamHeartbeatTimer);
496
+ streamHeartbeatTimer = null;
497
+ }
498
+
499
+ function startStreamHeartbeatTimer() {
500
+ stopStreamHeartbeatTimer();
501
+ streamHeartbeatTimer = _setInterval(async () => {
502
+ if (signal?.aborted || streamHeartbeatInFlight) return;
503
+ streamHeartbeatInFlight = true;
504
+ try {
505
+ await notifyHeartbeat({ nextPollMs: null });
506
+ } finally {
507
+ streamHeartbeatInFlight = false;
508
+ }
509
+ }, streamHeartbeatMs);
510
+ if (streamHeartbeatTimer && typeof streamHeartbeatTimer.unref === "function") {
511
+ streamHeartbeatTimer.unref();
512
+ }
513
+ }
514
+
476
515
  await notifyLifecycle(
477
516
  lifecycleSnapshot("started", {
478
517
  startedAt: new Date(startedAtMs).toISOString(),
@@ -484,6 +523,7 @@ export async function listenSessionEvents({
484
523
  if (normalizedTransport !== "poll") {
485
524
  streamAttempted = true;
486
525
  activeTransport = "stream";
526
+ startStreamHeartbeatTimer();
487
527
  const streamResult = await _stream(normalizedSessionId, {
488
528
  targetPath,
489
529
  since: cursor,
@@ -502,6 +542,8 @@ export async function listenSessionEvents({
502
542
  onHeartbeat: async () => {
503
543
  await notifyHeartbeat({ nextPollMs: null });
504
544
  },
545
+ }).finally(() => {
546
+ stopStreamHeartbeatTimer();
505
547
  });
506
548
  if (!streamResult?.ok) {
507
549
  streamFallbackReason = normalizeString(streamResult?.reason) || lastReason || "stream_failed";
@@ -0,0 +1,181 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import util from "node:util";
5
+
6
+ export const DEFAULT_ROTATING_LOG_MAX_BYTES = 5 * 1024 * 1024;
7
+ export const DEFAULT_ROTATING_LOG_MAX_FILES = 5;
8
+
9
+ function normalizeString(value) {
10
+ return String(value || "").trim();
11
+ }
12
+
13
+ function normalizePositiveInteger(value, fallbackValue) {
14
+ const numeric = Number(value);
15
+ if (!Number.isFinite(numeric) || numeric <= 0) {
16
+ return fallbackValue;
17
+ }
18
+ return Math.max(1, Math.floor(numeric));
19
+ }
20
+
21
+ export function normalizeRotatingLogOptions({
22
+ maxBytes = DEFAULT_ROTATING_LOG_MAX_BYTES,
23
+ maxFiles = DEFAULT_ROTATING_LOG_MAX_FILES,
24
+ } = {}) {
25
+ return {
26
+ maxBytes: normalizePositiveInteger(maxBytes, DEFAULT_ROTATING_LOG_MAX_BYTES),
27
+ maxFiles: normalizePositiveInteger(maxFiles, DEFAULT_ROTATING_LOG_MAX_FILES),
28
+ };
29
+ }
30
+
31
+ export function rotateLogFileIfNeeded(logPath, options = {}) {
32
+ const normalizedLogPath = normalizeString(logPath);
33
+ if (!normalizedLogPath) {
34
+ return { rotated: false, reason: "missing_log_path" };
35
+ }
36
+
37
+ const { maxBytes, maxFiles } = normalizeRotatingLogOptions(options);
38
+ let currentSize = 0;
39
+ try {
40
+ currentSize = fs.statSync(normalizedLogPath).size;
41
+ } catch (error) {
42
+ if (error?.code === "ENOENT") {
43
+ return { rotated: false, reason: "missing_file", maxBytes, maxFiles };
44
+ }
45
+ throw error;
46
+ }
47
+
48
+ if (currentSize < maxBytes) {
49
+ return { rotated: false, reason: "under_limit", size: currentSize, maxBytes, maxFiles };
50
+ }
51
+
52
+ fs.mkdirSync(path.dirname(normalizedLogPath), { recursive: true });
53
+ const backupCount = Math.max(0, maxFiles - 1);
54
+ if (backupCount === 0) {
55
+ fs.rmSync(normalizedLogPath, { force: true });
56
+ return { rotated: true, reason: "removed_active", size: currentSize, maxBytes, maxFiles };
57
+ }
58
+
59
+ fs.rmSync(`${normalizedLogPath}.${backupCount}`, { force: true });
60
+ for (let index = backupCount - 1; index >= 1; index -= 1) {
61
+ const from = `${normalizedLogPath}.${index}`;
62
+ const to = `${normalizedLogPath}.${index + 1}`;
63
+ if (fs.existsSync(from)) {
64
+ fs.renameSync(from, to);
65
+ }
66
+ }
67
+ fs.renameSync(normalizedLogPath, `${normalizedLogPath}.1`);
68
+ return { rotated: true, reason: "rotated", size: currentSize, maxBytes, maxFiles };
69
+ }
70
+
71
+ export function appendRotatingLogLine(logPath, line = "", options = {}) {
72
+ const normalizedLogPath = normalizeString(logPath);
73
+ if (!normalizedLogPath) {
74
+ return { written: false, reason: "missing_log_path" };
75
+ }
76
+ fs.mkdirSync(path.dirname(normalizedLogPath), { recursive: true });
77
+ const rotation = rotateLogFileIfNeeded(normalizedLogPath, options);
78
+ fs.appendFileSync(normalizedLogPath, `${redactLogSecrets(line)}\n`, "utf-8");
79
+ return { written: true, rotation };
80
+ }
81
+
82
+ function normalizeLogChunk(chunk, encoding = "utf8") {
83
+ if (Buffer.isBuffer(chunk)) {
84
+ return chunk.toString(typeof encoding === "string" ? encoding : "utf8");
85
+ }
86
+ return String(chunk ?? "");
87
+ }
88
+
89
+ export function formatConsoleLogLine(parts = []) {
90
+ return parts.map((part) => (typeof part === "string" ? part : util.inspect(part))).join(" ");
91
+ }
92
+
93
+ export function redactLogSecrets(value = "") {
94
+ return String(value ?? "")
95
+ .replace(/\bbearer\s+[a-z0-9._~+/=-]+\b/gi, "bearer [REDACTED]")
96
+ .replace(
97
+ /\b([a-z0-9_.-]*(?:token|secret|password|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token)[a-z0-9_.-]*)\b\s*[:=]\s*["']?[^"'\s,;]+["']?/gi,
98
+ "$1=[REDACTED]",
99
+ )
100
+ .replace(/\beyJ[a-z0-9_-]{8,}\.[a-z0-9_-]{10,}\.[a-z0-9_-]{10,}\b/gi, "[REDACTED_JWT]");
101
+ }
102
+
103
+ export function installRotatingConsoleLog({
104
+ logPath,
105
+ maxBytes = DEFAULT_ROTATING_LOG_MAX_BYTES,
106
+ maxFiles = DEFAULT_ROTATING_LOG_MAX_FILES,
107
+ tee = true,
108
+ now = () => new Date(),
109
+ onError = null,
110
+ captureConsoleError = true,
111
+ captureStderr = true,
112
+ } = {}) {
113
+ const normalizedLogPath = normalizeString(logPath);
114
+ if (!normalizedLogPath) {
115
+ return () => {};
116
+ }
117
+
118
+ const options = normalizeRotatingLogOptions({ maxBytes, maxFiles });
119
+ const originalLog = console.log;
120
+ const originalError = console.error;
121
+ const originalStderrWrite = process.stderr.write;
122
+ let warned = false;
123
+ const writeLogLine = (line) => {
124
+ try {
125
+ appendRotatingLogLine(
126
+ normalizedLogPath,
127
+ `${now().toISOString()} ${line}`,
128
+ options,
129
+ );
130
+ } catch (error) {
131
+ if (!warned) {
132
+ warned = true;
133
+ if (typeof onError === "function") {
134
+ onError(error);
135
+ }
136
+ }
137
+ }
138
+ };
139
+
140
+ console.log = (...parts) => {
141
+ if (tee) {
142
+ originalLog(...parts);
143
+ }
144
+ writeLogLine(formatConsoleLogLine(parts));
145
+ };
146
+
147
+ if (captureStderr) {
148
+ process.stderr.write = function rotatingStderrWrite(chunk, encoding, callback) {
149
+ const text = normalizeLogChunk(chunk, encoding).replace(/\s+$/u, "");
150
+ if (text) {
151
+ writeLogLine(`[stderr] ${text}`);
152
+ }
153
+ if (!tee) {
154
+ if (typeof encoding === "function") {
155
+ encoding();
156
+ } else if (typeof callback === "function") {
157
+ callback();
158
+ }
159
+ return true;
160
+ }
161
+ return originalStderrWrite.apply(process.stderr, arguments);
162
+ };
163
+ }
164
+
165
+ if (captureConsoleError) {
166
+ console.error = (...parts) => {
167
+ if (tee) {
168
+ originalError(...parts);
169
+ }
170
+ if (!captureStderr || !tee) {
171
+ writeLogLine(`[stderr] ${formatConsoleLogLine(parts)}`);
172
+ }
173
+ };
174
+ }
175
+
176
+ return () => {
177
+ console.log = originalLog;
178
+ console.error = originalError;
179
+ process.stderr.write = originalStderrWrite;
180
+ };
181
+ }
@@ -461,7 +461,13 @@ function abortRequestController(controller, reason) {
461
461
  function cancelResponseBody(response) {
462
462
  try {
463
463
  if (response?.body && typeof response.body.cancel === "function") {
464
- void response.body.cancel();
464
+ if (response.body.locked) return;
465
+ const cancellation = response.body.cancel();
466
+ if (cancellation && typeof cancellation.catch === "function") {
467
+ void cancellation.catch(() => {
468
+ // Best-effort socket/body cleanup only.
469
+ });
470
+ }
465
471
  }
466
472
  } catch {
467
473
  // Best-effort socket/body cleanup only.