@upx-us/shield 0.7.13 → 0.8.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/dist/index.js CHANGED
@@ -39,6 +39,7 @@ exports.maskPluginConfigForLogs = maskPluginConfigForLogs;
39
39
  exports.createSingleflightRunner = createSingleflightRunner;
40
40
  exports.createStartGuard = createStartGuard;
41
41
  exports.getStatusWarnings = getStatusWarnings;
42
+ exports._resetForTesting = _resetForTesting;
42
43
  const config_1 = require("./src/config");
43
44
  const log_1 = require("./src/log");
44
45
  const log = __importStar(require("./src/log"));
@@ -231,6 +232,61 @@ function flushAllTimeStats() {
231
232
  }
232
233
  let _stateDirty = true;
233
234
  function markStateDirty() { _stateDirty = true; }
235
+ function createInitialState() {
236
+ return {
237
+ activated: false,
238
+ running: false,
239
+ startedAt: 0,
240
+ lastPollAt: 0,
241
+ lastSuccessfulPollAt: 0,
242
+ eventsProcessed: 0,
243
+ quarantineCount: 0,
244
+ consecutiveFailures: 0,
245
+ telemetryConsecutiveFailures: 0,
246
+ instanceId: '',
247
+ lastCaptureAt: 0,
248
+ captureSeenSinceLastSync: false,
249
+ lastSync: null,
250
+ lastTelemetryAt: 0,
251
+ lastSuccessfulTelemetryAt: 0,
252
+ lastTelemetryError: null,
253
+ lastPollError: null,
254
+ lastLifecyclePhase: 'idle',
255
+ lastStopReason: 'none',
256
+ lastError: null,
257
+ lastStartupCheckpoint: null,
258
+ sessionDirCount: 0,
259
+ eventsRetainedForRetry: 0,
260
+ lastDeliveryIssue: null,
261
+ lastDeliveryIssueAt: 0,
262
+ };
263
+ }
264
+ function setLifecyclePhase(phase, checkpoint) {
265
+ state.lastLifecyclePhase = phase;
266
+ if (checkpoint !== undefined)
267
+ state.lastStartupCheckpoint = checkpoint;
268
+ markStateDirty();
269
+ }
270
+ function setLastError(message) {
271
+ state.lastError = message;
272
+ markStateDirty();
273
+ }
274
+ function setStopReason(reason) {
275
+ state.lastStopReason = reason;
276
+ markStateDirty();
277
+ }
278
+ function hasMeaningfulRuntimeState(snapshot) {
279
+ return Boolean(snapshot.running ||
280
+ snapshot.startedAt ||
281
+ snapshot.lastPollAt ||
282
+ snapshot.lastSuccessfulPollAt ||
283
+ snapshot.eventsProcessed ||
284
+ snapshot.quarantineCount ||
285
+ snapshot.lastTelemetryAt ||
286
+ snapshot.lastSuccessfulTelemetryAt ||
287
+ snapshot.lastStartupCheckpoint ||
288
+ snapshot.instanceId);
289
+ }
234
290
  function persistState(extra = {}) {
235
291
  flushAllTimeStats();
236
292
  if (!_stateDirty)
@@ -264,15 +320,27 @@ function persistState(extra = {}) {
264
320
  function readPersistedState() {
265
321
  try {
266
322
  const d = (0, safe_io_1.readJsonSafe)(STATUS_FILE, null, 'status');
267
- if (!d)
268
- return null;
323
+ if (!d) {
324
+ if (hasMeaningfulRuntimeState(state)) {
325
+ return { data: state, source: 'runtime', message: null };
326
+ }
327
+ return { data: null, source: 'missing', message: 'No persisted Shield runtime state found yet.' };
328
+ }
269
329
  const age = Date.now() - (Number(d.updatedAt) || 0);
270
- if (age > 10 * 60 * 1000)
271
- return null;
272
- return d;
330
+ if (age > 10 * 60 * 1000) {
331
+ return {
332
+ data: d,
333
+ source: 'stale',
334
+ message: `Last persisted Shield state is stale (${Math.floor(age / 60_000)}m old).`,
335
+ };
336
+ }
337
+ return { data: d, source: 'persisted', message: null };
273
338
  }
274
339
  catch {
275
- return null;
340
+ if (hasMeaningfulRuntimeState(state)) {
341
+ return { data: state, source: 'runtime', message: null };
342
+ }
343
+ return { data: null, source: 'missing', message: 'Shield status state could not be read.' };
276
344
  }
277
345
  }
278
346
  const STALE_POLL_WARN_MS = 2 * 60 * 1000;
@@ -304,19 +372,7 @@ function getStatusWarnings(input) {
304
372
  }
305
373
  return warnings;
306
374
  }
307
- const state = {
308
- activated: false,
309
- running: false,
310
- startedAt: 0,
311
- lastPollAt: 0,
312
- eventsProcessed: 0,
313
- quarantineCount: 0,
314
- consecutiveFailures: 0,
315
- instanceId: '',
316
- lastCaptureAt: 0,
317
- captureSeenSinceLastSync: false,
318
- lastSync: null,
319
- };
375
+ const state = createInitialState();
320
376
  let firstEventDelivered = false;
321
377
  let teardownPreviousRuntime = null;
322
378
  let pendingTeardown = null;
@@ -330,6 +386,26 @@ function getBackoffInterval(baseMs) {
330
386
  const backoff = baseMs * Math.pow(2, Math.min(state.consecutiveFailures, 10));
331
387
  return Math.min(backoff, MAX_BACKOFF_MS);
332
388
  }
389
+ function describeSendFailure(kind, statusCode, body) {
390
+ switch (kind) {
391
+ case 'missing_credentials':
392
+ return 'Missing Shield credentials';
393
+ case 'pending_namespace':
394
+ return 'Namespace allocation pending';
395
+ case 'needs_registration':
396
+ return 'Instance registration invalidated by platform';
397
+ case 'quota_exceeded':
398
+ return 'Subscription inactive or quota exhausted';
399
+ case 'circuit_breaker':
400
+ return 'Circuit breaker opened after repeated batch failures';
401
+ case 'network_error':
402
+ return `Network error while sending events${body ? `: ${body}` : ''}`;
403
+ case 'http_error':
404
+ return `HTTP ${statusCode ?? 0} while sending events${body ? `: ${body}` : ''}`;
405
+ default:
406
+ return body || `HTTP ${statusCode ?? 0} while sending events`;
407
+ }
408
+ }
333
409
  function printNotActivatedStatus() {
334
410
  console.log(`OpenClaw Shield — v${version_1.VERSION}`);
335
411
  console.log('');
@@ -344,42 +420,54 @@ function printNotActivatedStatus() {
344
420
  console.log(' Get your key at: https://uss.upx.com → APPS → OpenClaw Shield');
345
421
  }
346
422
  function printActivatedStatus() {
347
- const s = readPersistedState() ?? state;
423
+ const snapshot = readPersistedState();
424
+ const s = snapshot.data;
425
+ const fmtTime = (ms) => ms < 60_000 ? `${Math.round(ms / 1000)}s ago`
426
+ : ms < 3_600_000 ? `${Math.floor(ms / 60_000)}m ago`
427
+ : `${(ms / 3_600_000).toFixed(1)}h ago`;
428
+ if (!s) {
429
+ console.log(`OpenClaw Shield — v${version_1.VERSION}`);
430
+ console.log('');
431
+ console.log('── Plugin Health ─────────────────────────────');
432
+ console.log(' Connection: ⚠️ Status unavailable');
433
+ console.log(` Version: ${version_1.VERSION}`);
434
+ console.log(` Warning: ${snapshot.message ?? 'Shield has not persisted runtime state yet.'}`);
435
+ console.log(' - This usually means the gateway has not started the plugin yet, or state persistence is unavailable.');
436
+ console.log(' - If this persists, run: openclaw gateway restart');
437
+ return;
438
+ }
348
439
  const isRunning = Boolean(s.running);
349
440
  const ageMs = s.updatedAt ? Date.now() - s.updatedAt : null;
350
- const ageLabel = ageMs != null
351
- ? (ageMs < 60_000 ? `${Math.round(ageMs / 1000)}s ago`
352
- : ageMs < 3_600_000 ? `${Math.floor(ageMs / 60_000)}m ago`
353
- : `${(ageMs / 3_600_000).toFixed(1)}h ago`)
354
- : '';
441
+ const ageLabel = ageMs != null ? fmtTime(ageMs) : '';
355
442
  const lastPollMs = s.lastPollAt ? Date.now() - s.lastPollAt : null;
356
- const lastPollLabel = s.lastPollAt
357
- ? (lastPollMs < 60_000 ? `${Math.round(lastPollMs / 1000)}s ago`
358
- : lastPollMs < 3_600_000 ? `${Math.floor(lastPollMs / 60_000)}m ago`
359
- : `${(lastPollMs / 3_600_000).toFixed(1)}h ago`)
360
- : 'never';
443
+ const lastPollLabel = s.lastPollAt ? fmtTime(lastPollMs) : 'never';
361
444
  const instanceId = s.instanceId;
362
445
  const shortId = instanceId ? `${instanceId.slice(0, 8)}…` : '';
446
+ const lifecyclePhase = s.lastLifecyclePhase ?? 'unknown';
447
+ const stopReason = s.lastStopReason ?? 'unknown';
363
448
  console.log(`OpenClaw Shield — v${s.version ?? version_1.VERSION}${ageLabel ? ` (${ageLabel})` : ''}`);
364
449
  console.log('');
365
450
  console.log('── Plugin Health ─────────────────────────────');
366
- console.log(` Connection: ${isRunning ? '✅ Connected' : '❌ Disconnected'}`);
451
+ console.log(` Connection: ${isRunning ? '✅ Connected' : snapshot.source === 'stale' ? '⚠️ Status stale' : '❌ Disconnected'}`);
367
452
  console.log(` Version: ${s.version ?? version_1.VERSION}`);
368
453
  if (shortId)
369
454
  console.log(` Instance: ${shortId}`);
370
455
  console.log(` Last poll: ${lastPollLabel}`);
371
456
  const lastCaptureMs = s.lastCaptureAt ? Date.now() - s.lastCaptureAt : null;
372
- const lastCaptureLabel = s.lastCaptureAt
373
- ? (lastCaptureMs < 60_000 ? `${Math.round(lastCaptureMs / 1000)}s ago`
374
- : lastCaptureMs < 3_600_000 ? `${Math.floor(lastCaptureMs / 60_000)}m ago`
375
- : `${(lastCaptureMs / 3_600_000).toFixed(1)}h ago`)
376
- : null;
457
+ const lastCaptureLabel = s.lastCaptureAt ? fmtTime(lastCaptureMs) : null;
377
458
  if (lastCaptureLabel)
378
459
  console.log(` Last capture: ${lastCaptureLabel}`);
460
+ const updateState = (0, updater_1.loadUpdateState)();
379
461
  const allTime = (s.allTime ?? readAllTimeStats());
380
462
  console.log(` Events sent: ${allTime.eventsProcessed.toLocaleString()} (all-time)`);
381
463
  console.log(` Quarantine: ${allTime.quarantineCount.toLocaleString()} (all-time)`);
382
- console.log(` Failures: ${s.consecutiveFailures ?? 0} (consecutive)`);
464
+ console.log(` Failures: ${s.consecutiveFailures ?? 0} (poll)`);
465
+ if (s.telemetryConsecutiveFailures != null) {
466
+ console.log(` Telemetry: ${s.telemetryConsecutiveFailures ?? 0} (consecutive failures)`);
467
+ }
468
+ console.log(` Phase: ${lifecyclePhase}`);
469
+ if (stopReason && stopReason !== 'none')
470
+ console.log(` Stop reason: ${stopReason}`);
383
471
  if (s.pid) {
384
472
  let pidAlive = false;
385
473
  try {
@@ -389,6 +477,28 @@ function printActivatedStatus() {
389
477
  catch { }
390
478
  console.log(` Daemon PID: ${s.pid}${pidAlive ? '' : ' ⚠️ stale (process not running)'}`);
391
479
  }
480
+ if (snapshot.message) {
481
+ console.log(` Status note: ${snapshot.message}`);
482
+ }
483
+ if (s.lastStartupCheckpoint) {
484
+ console.log(` Checkpoint: ${s.lastStartupCheckpoint}`);
485
+ }
486
+ if (s.lastError) {
487
+ console.log(` Last error: ${s.lastError}`);
488
+ }
489
+ if (updateState.pendingRestart || updateState.lastFailureStage || updateState.rollbackPending) {
490
+ console.log(` Update: ${updateState.pendingRestart ? 'pending restart' : updateState.rollbackPending ? 'rollback needed' : 'attention required'}`);
491
+ if (updateState.lastFailureStage)
492
+ console.log(` Update stage: ${updateState.lastFailureStage}`);
493
+ if (updateState.lastError)
494
+ console.log(` Update error: ${updateState.lastError}`);
495
+ }
496
+ if (s.lastDeliveryIssue) {
497
+ console.log(` Delivery: ${s.lastDeliveryIssue}`);
498
+ }
499
+ if (s.eventsRetainedForRetry && Number(s.eventsRetainedForRetry) > 0) {
500
+ console.log(` Retry backlog:${String(s.eventsRetainedForRetry).padStart(3)} event(s) retained for retry`);
501
+ }
392
502
  const statusWarnings = getStatusWarnings({
393
503
  running: isRunning,
394
504
  lastPollAt: s.lastPollAt ?? null,
@@ -397,11 +507,21 @@ function printActivatedStatus() {
397
507
  captureSeenSinceLastSync: Boolean(s.captureSeenSinceLastSync ?? false),
398
508
  consecutiveFailures: s.consecutiveFailures ?? 0,
399
509
  });
400
- if (statusWarnings.length > 0) {
510
+ const telemetryWarnings = [];
511
+ if (s.telemetryConsecutiveFailures && Number(s.telemetryConsecutiveFailures) >= CONSECUTIVE_FAILURES_WARN) {
512
+ telemetryWarnings.push(`Instance telemetry is failing (${s.telemetryConsecutiveFailures} consecutive attempts), but capture remains active.`);
513
+ }
514
+ if (s.lastTelemetryError) {
515
+ telemetryWarnings.push(`Last telemetry error: ${s.lastTelemetryError}`);
516
+ }
517
+ if (statusWarnings.length > 0 || telemetryWarnings.length > 0 || snapshot.source === 'stale') {
401
518
  console.log(' Warning: ⚠️ Capture health degraded');
402
- for (const warning of statusWarnings) {
519
+ for (const warning of [...statusWarnings, ...telemetryWarnings]) {
403
520
  console.log(` - ${warning}`);
404
521
  }
522
+ if (snapshot.source === 'stale') {
523
+ console.log(' - Persisted status is stale; the gateway may have restarted or status writes may be blocked.');
524
+ }
405
525
  console.log(' - If this persists, run: openclaw gateway restart');
406
526
  }
407
527
  const startedAt = s.startedAt;
@@ -422,9 +542,6 @@ function printActivatedStatus() {
422
542
  const filled = Math.max(1, Math.round((count / max) * BAR_MAX));
423
543
  return BAR_CHARS.slice(0, filled);
424
544
  };
425
- const fmtTime = (ms) => ms < 60_000 ? `${Math.round(ms / 1000)}s ago`
426
- : ms < 3_600_000 ? `${Math.floor(ms / 60_000)}m ago`
427
- : `${(ms / 3_600_000).toFixed(1)}h ago`;
428
545
  const lastSync = s.lastSync;
429
546
  console.log('');
430
547
  if (lastSync && lastSync.at) {
@@ -437,7 +554,7 @@ function printActivatedStatus() {
437
554
  }
438
555
  else {
439
556
  console.log('📡 Last sync');
440
- console.log(' No sync yet. Bridge will send on the next poll cycle.');
557
+ console.log(` ${snapshot.source === 'stale' ? 'Last known sync is unavailable because persisted state is stale.' : 'No sync yet. Bridge will send on the next poll cycle.'}`);
441
558
  }
442
559
  const counters = (s.counters ?? {});
443
560
  const sessionEvents = counters.totalEvents ?? 0;
@@ -472,6 +589,16 @@ function printActivatedStatus() {
472
589
  console.log(' (original values never stored or transmitted)');
473
590
  }
474
591
  }
592
+ function _resetForTesting() {
593
+ Object.assign(state, createInitialState());
594
+ _allTimeStats = null;
595
+ _allTimeStatsDirty = false;
596
+ _stateDirty = true;
597
+ firstEventDelivered = false;
598
+ teardownPreviousRuntime = null;
599
+ pendingTeardown = null;
600
+ serviceStartFn = null;
601
+ }
475
602
  exports.default = {
476
603
  id: 'shield',
477
604
  name: 'OpenClaw Shield',
@@ -561,6 +688,8 @@ exports.default = {
561
688
  const markStopped = opts?.markStopped ?? true;
562
689
  if (markStopped) {
563
690
  state.running = false;
691
+ state.lastLifecyclePhase = opts?.finalPhase ?? 'stopped';
692
+ state.lastStopReason = opts?.stopReason ?? 'unknown';
564
693
  markStateDirty();
565
694
  persistState();
566
695
  }
@@ -569,7 +698,7 @@ exports.default = {
569
698
  }
570
699
  if (opts?.flushRedactor) {
571
700
  try {
572
- const { flush: flushRedactor } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
701
+ const { flush: flushRedactor } = require('./src/redactor');
573
702
  flushRedactor();
574
703
  }
575
704
  catch { }
@@ -580,6 +709,17 @@ exports.default = {
580
709
  .catch((err) => log.warn('shield', `Runtime cleanup before re-register failed: ${err instanceof Error ? err.message : String(err)}`));
581
710
  }
582
711
  teardownPreviousRuntime = () => cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: false });
712
+ const deactivateForRegistrationInvalidation = async (checkpoint, message) => {
713
+ setLifecyclePhase('deactivated', checkpoint);
714
+ setStopReason('registration_invalidated');
715
+ setLastError(message);
716
+ await cleanupRuntime({
717
+ markStopped: true,
718
+ stopReason: 'registration_invalidated',
719
+ finalPhase: 'deactivated',
720
+ flushRedactor: true,
721
+ });
722
+ };
583
723
  const serviceDefinition = {
584
724
  id: 'shield-monitor',
585
725
  async start() {
@@ -594,6 +734,11 @@ exports.default = {
594
734
  try {
595
735
  await cleanupRuntime({ markStopped: false, resetGuard: false, flushRedactor: false });
596
736
  const activeGeneration = ++runtimeGeneration;
737
+ Object.assign(state, createInitialState());
738
+ setLastError(null);
739
+ setStopReason('none');
740
+ setLifecyclePhase('starting', 'checkpoint:1');
741
+ persistState();
597
742
  log.info('shield', `[checkpoint:1] Service start() entered — generation=${activeGeneration}`);
598
743
  let credentials = (0, config_1.loadCredentials)();
599
744
  let validCreds = hasValidCredentials(credentials);
@@ -603,6 +748,10 @@ exports.default = {
603
748
  const autoCreds = await performAutoRegistration(installationKey);
604
749
  if (!autoCreds) {
605
750
  log.error('shield', 'Activation failed. Verify your Installation Key and try again.');
751
+ setLifecyclePhase('startup_failed', 'checkpoint:activation_failed');
752
+ setStopReason('activation_failed');
753
+ setLastError('Activation failed. Verify your Installation Key and try again.');
754
+ persistState();
606
755
  startGuard.endFailure();
607
756
  return;
608
757
  }
@@ -617,11 +766,17 @@ exports.default = {
617
766
  log.warn('shield', ' Activate via CLI: openclaw shield activate <YOUR_KEY>');
618
767
  log.warn('shield', ' Or set plugins.entries.shield.config.installationKey in openclaw.json and restart.');
619
768
  log.warn('shield', ' Get your key at: https://uss.upx.com → APPS → OpenClaw Shield');
769
+ setLifecyclePhase('stopped', 'checkpoint:not_activated');
770
+ setStopReason('not_activated');
771
+ setLastError('Shield is not activated.');
772
+ persistState();
620
773
  startGuard.endFailure();
621
774
  return;
622
775
  }
623
776
  state.activated = true;
624
777
  state.startedAt = Date.now();
778
+ setLastError(null);
779
+ setLifecyclePhase('starting', 'checkpoint:3');
625
780
  const config = (0, config_1.loadConfig)({
626
781
  credentials,
627
782
  dryRun: dryRunVal,
@@ -630,25 +785,33 @@ exports.default = {
630
785
  collectHostMetrics: hostMetricsVal,
631
786
  });
632
787
  state.instanceId = config.credentials.instanceId ?? '';
788
+ state.sessionDirCount = config.sessionDirs.length;
789
+ if (config.sessionDirs.length === 0) {
790
+ state.lastDeliveryIssue = 'No OpenClaw session directories discovered yet — Shield is loaded but has no event sources.';
791
+ state.lastDeliveryIssueAt = Date.now();
792
+ }
633
793
  const persistedStats = readAllTimeStats();
634
794
  if (persistedStats.lastSync)
635
795
  state.lastSync = persistedStats.lastSync;
636
796
  log.info('shield', `[checkpoint:3] Config loaded — sessionDirs=${config.sessionDirs.length} poll=${config.pollIntervalMs}ms dryRun=${config.dryRun}`);
637
797
  log.info('shield', `Starting Shield v${version_1.VERSION} (poll: ${config.pollIntervalMs}ms, dryRun: ${config.dryRun})`);
798
+ persistState();
638
799
  (0, exclusions_1.initExclusions)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'));
639
800
  log.info('shield', '[checkpoint:4] Exclusions initialized');
640
801
  (0, case_monitor_1.initCaseMonitor)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'));
641
802
  log.info('shield', '[checkpoint:5] Case monitor initialized');
803
+ state.lastStartupCheckpoint = 'checkpoint:5';
804
+ persistState();
642
805
  if (config.localEventBuffer) {
643
806
  (0, event_store_1.initEventStore)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'), { maxEvents: config.localEventLimit });
644
807
  }
645
808
  const runtime = api.runtime;
646
- if (runtime?.system?.enqueueSystemEvent && runtime?.system?.requestHeartbeatNow) {
647
- const config = api.config;
809
+ const openclawConfig = api.config;
810
+ if (typeof runtime?.system?.enqueueSystemEvent === 'function' && typeof runtime?.system?.requestHeartbeatNow === 'function') {
648
811
  (0, case_monitor_1.setCaseNotificationDispatcher)({
649
812
  enqueueSystemEvent: runtime.system.enqueueSystemEvent,
650
813
  requestHeartbeatNow: runtime.system.requestHeartbeatNow,
651
- agentId: config?.agents?.default ?? 'main',
814
+ agentId: openclawConfig?.agents?.default ?? 'main',
652
815
  });
653
816
  }
654
817
  else {
@@ -656,7 +819,6 @@ exports.default = {
656
819
  }
657
820
  try {
658
821
  const channels = runtime?.channel;
659
- const cfg = api.config;
660
822
  const channelCandidates = [
661
823
  { name: 'telegram', sendFn: channels?.telegram?.sendMessageTelegram, configPath: 'telegram' },
662
824
  { name: 'discord', sendFn: channels?.discord?.sendMessageDiscord, configPath: 'discord' },
@@ -667,7 +829,7 @@ exports.default = {
667
829
  for (const candidate of channelCandidates) {
668
830
  if (typeof candidate.sendFn !== 'function')
669
831
  continue;
670
- const channelCfg = cfg?.channels?.[candidate.configPath];
832
+ const channelCfg = openclawConfig?.channels?.[candidate.configPath];
671
833
  if (!channelCfg?.enabled && channelCfg?.enabled !== undefined)
672
834
  continue;
673
835
  let targetId = null;
@@ -694,6 +856,9 @@ exports.default = {
694
856
  break;
695
857
  }
696
858
  }
859
+ if (!channels) {
860
+ log.debug('case-monitor', 'Direct send runtime channels unavailable — direct message notifications disabled');
861
+ }
697
862
  }
698
863
  catch (err) {
699
864
  log.debug('case-monitor', `Direct send setup failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
@@ -702,7 +867,7 @@ exports.default = {
702
867
  const updateState = (0, updater_1.loadUpdateState)();
703
868
  const previousVersion = updateState.rollbackVersion ?? null;
704
869
  const startReason = previousVersion ? 'update' : 'normal';
705
- const { reportLifecycleEvent } = await Promise.resolve().then(() => __importStar(require('./src/sender')));
870
+ const { reportLifecycleEvent } = require('./src/sender');
706
871
  void reportLifecycleEvent('plugin_started', {
707
872
  version: version_1.VERSION,
708
873
  reason: startReason,
@@ -712,14 +877,21 @@ exports.default = {
712
877
  catch { }
713
878
  const autoUpdateMode = pluginConfig.autoUpdate ?? true;
714
879
  const _bootState = (0, updater_1.loadUpdateState)();
880
+ state.lastStartupCheckpoint = 'checkpoint:6';
881
+ persistState();
715
882
  log.info('shield', `[checkpoint:6] Pre-update-check — autoUpdate=${autoUpdateMode} pendingRestart=${_bootState.pendingRestart} updateAvailable=${_bootState.updateAvailable} latestVersion=${_bootState.latestVersion}`);
716
883
  log.info('updater', `Startup update check (autoUpdate=${autoUpdateMode}, current=${version_1.VERSION}, pendingRestart=${_bootState.pendingRestart})`);
717
884
  const startupUpdate = (0, updater_1.performAutoUpdate)(autoUpdateMode, _bootState.pendingRestart ? undefined : 0);
885
+ state.lastStartupCheckpoint = 'checkpoint:7';
886
+ persistState();
718
887
  log.info('shield', `[checkpoint:7] Update check done — action=${startupUpdate.action}`);
719
888
  if (startupUpdate.action === 'updated') {
720
889
  log.info('updater', startupUpdate.message);
721
890
  const restarted = (0, updater_1.requestGatewayRestart)();
722
891
  if (restarted) {
892
+ setLifecyclePhase('stopped', 'checkpoint:update_restart');
893
+ setStopReason('update_restart');
894
+ persistState();
723
895
  startGuard.endFailure();
724
896
  return;
725
897
  }
@@ -736,21 +908,22 @@ exports.default = {
736
908
  }
737
909
  try {
738
910
  const inv = (0, inventory_1.collectInventory)();
739
- const { setCachedInventory } = await Promise.resolve().then(() => __importStar(require('./src/inventory')));
911
+ const { setCachedInventory } = require('./src/inventory');
740
912
  setCachedInventory(inv);
741
913
  }
742
914
  catch (err) {
743
915
  log.warn('shield', `Inventory collection failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
744
916
  }
745
- const { fetchNewEntries, commitCursors } = await Promise.resolve().then(() => __importStar(require('./src/fetcher')));
746
- const { transformEntries, generateHostTelemetry, resolveOpenClawVersion, resolveAgentLabel } = await Promise.resolve().then(() => __importStar(require('./src/transformer')));
747
- const { sendEvents, reportInstance } = await Promise.resolve().then(() => __importStar(require('./src/sender')));
748
- const { init: initRedactor, flush: flushRedactor, redactEvent } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
749
- const { validate } = await Promise.resolve().then(() => __importStar(require('./src/validator')));
917
+ const { fetchNewEntries, commitCursors } = require('./src/fetcher');
918
+ const { transformEntries, generateHostTelemetry, resolveOpenClawVersion, resolveAgentLabel } = require('./src/transformer');
919
+ const { sendEvents, reportInstance } = require('./src/sender');
920
+ const { init: initRedactor, flush: flushRedactor, redactEvent } = require('./src/redactor');
921
+ const { validate } = require('./src/validator');
750
922
  log.info('shield', '[checkpoint:8] Dynamic imports loaded');
751
923
  if (config.redactionEnabled)
752
924
  initRedactor();
753
925
  state.running = true;
926
+ setLifecyclePhase('running', 'checkpoint:9');
754
927
  persistState();
755
928
  log.info('shield', '[checkpoint:9] state.running=true — entering poll loop');
756
929
  const runTelemetry = async () => {
@@ -779,19 +952,31 @@ exports.default = {
779
952
  const result = await reportInstance(instancePayload, config.credentials);
780
953
  if (activeGeneration !== runtimeGeneration)
781
954
  return;
955
+ state.lastTelemetryAt = Date.now();
782
956
  log.info('shield', `Instance report → Platform: success=${result.ok}`);
783
957
  if (result.ok) {
784
- state.consecutiveFailures = 0;
958
+ state.telemetryConsecutiveFailures = 0;
959
+ state.lastSuccessfulTelemetryAt = Date.now();
960
+ state.lastTelemetryError = null;
961
+ if (state.lastLifecyclePhase === 'degraded' && state.running) {
962
+ state.lastLifecyclePhase = 'running';
963
+ }
785
964
  }
786
965
  else {
787
- state.consecutiveFailures++;
788
- if (state.consecutiveFailures >= MAX_REGISTRATION_FAILURES) {
789
- log.error('shield', `Instance report failed ${state.consecutiveFailures} consecutive times — Shield deactivated. Re-run: openclaw shield activate <KEY>`);
790
- state.running = false;
791
- markStateDirty();
792
- persistState();
966
+ state.telemetryConsecutiveFailures++;
967
+ state.lastTelemetryError = result.error ? String(result.error).slice(0, 200) : `HTTP ${result.statusCode ?? 0}`;
968
+ if (result.needsRegistration) {
969
+ log.error('shield', 'Instance report says this Shield instance is no longer registered — deactivating monitoring.');
970
+ await deactivateForRegistrationInvalidation('checkpoint:telemetry_registration_invalid', 'Instance registration invalidated by platform.');
971
+ return;
972
+ }
973
+ if (state.telemetryConsecutiveFailures >= MAX_REGISTRATION_FAILURES) {
974
+ log.error('shield', `Instance telemetry failed ${state.telemetryConsecutiveFailures} consecutive times — capture remains active but health is degraded.`);
975
+ setLifecyclePhase('degraded');
793
976
  }
794
977
  }
978
+ markStateDirty();
979
+ persistState();
795
980
  };
796
981
  const runTelemetrySingleflight = createSingleflightRunner(runTelemetry);
797
982
  log.info('shield', '[checkpoint:9a] Firing initial telemetry');
@@ -828,7 +1013,10 @@ exports.default = {
828
1013
  if (entries.length === 0) {
829
1014
  commitCursors(config, []);
830
1015
  state.consecutiveFailures = 0;
1016
+ state.lastPollError = null;
831
1017
  state.lastPollAt = Date.now();
1018
+ state.lastSuccessfulPollAt = state.lastPollAt;
1019
+ state.eventsRetainedForRetry = 0;
832
1020
  markStateDirty();
833
1021
  persistState();
834
1022
  const idlePlatformConfig = { apiUrl: config.credentials.apiUrl, instanceId: config.credentials.instanceId, hmacSecret: config.credentials.hmacSecret };
@@ -858,8 +1046,7 @@ exports.default = {
858
1046
  const needsReg = results.some(r => r.needsRegistration);
859
1047
  if (needsReg) {
860
1048
  log.error('shield', 'Instance not registered on platform — Shield deactivated.');
861
- state.running = false;
862
- markStateDirty();
1049
+ await deactivateForRegistrationInvalidation('checkpoint:poll_registration_invalid', 'Instance registration invalidated by platform.');
863
1050
  return;
864
1051
  }
865
1052
  const pending = results.find(r => r.pendingNamespace);
@@ -867,6 +1054,10 @@ exports.default = {
867
1054
  const waitMs = Math.min(pending.retryAfterMs ?? 300_000, MAX_BACKOFF_MS);
868
1055
  log.warn('shield', `Namespace allocation in progress — holding events, backing off ${Math.round(waitMs / 1000)}s`);
869
1056
  state.lastPollAt = Date.now();
1057
+ state.lastPollError = null;
1058
+ state.lastDeliveryIssue = describeSendFailure(pending.failureKind, pending.statusCode, pending.body);
1059
+ state.lastDeliveryIssueAt = Date.now();
1060
+ state.eventsRetainedForRetry = pending.eventCount;
870
1061
  markStateDirty();
871
1062
  persistState();
872
1063
  await new Promise(r => setTimeout(r, waitMs));
@@ -875,7 +1066,9 @@ exports.default = {
875
1066
  return;
876
1067
  }
877
1068
  const accepted = results.reduce((sum, r) => sum + (r.success ? r.eventCount : 0), 0);
878
- if (accepted > 0) {
1069
+ const allBatchesSucceeded = results.length > 0 && results.every(r => r.success);
1070
+ const firstFailure = results.find(r => !r.success);
1071
+ if (allBatchesSucceeded && accepted > 0) {
879
1072
  (0, case_monitor_1.notifyCaseMonitorActivity)();
880
1073
  if (!firstEventDelivered) {
881
1074
  firstEventDelivered = true;
@@ -885,6 +1078,14 @@ exports.default = {
885
1078
  flushRedactor();
886
1079
  state.eventsProcessed += accepted;
887
1080
  state.consecutiveFailures = 0;
1081
+ state.lastPollError = null;
1082
+ state.lastDeliveryIssue = null;
1083
+ state.lastDeliveryIssueAt = 0;
1084
+ state.eventsRetainedForRetry = 0;
1085
+ state.lastSuccessfulPollAt = Date.now();
1086
+ if (state.lastLifecyclePhase === 'degraded') {
1087
+ state.lastLifecyclePhase = 'running';
1088
+ }
888
1089
  markStateDirty();
889
1090
  const syncEventTypes = {};
890
1091
  for (const env of envelopes) {
@@ -908,8 +1109,30 @@ exports.default = {
908
1109
  (0, event_store_1.appendEvents)(summaries);
909
1110
  }
910
1111
  }
1112
+ else if (accepted > 0) {
1113
+ state.consecutiveFailures++;
1114
+ state.lastPollError = 'Partial delivery detected — retaining cursor position to retry all events safely.';
1115
+ state.lastDeliveryIssue = firstFailure
1116
+ ? `${describeSendFailure(firstFailure.failureKind, firstFailure.statusCode, firstFailure.body)}. Retrying entire poll window without advancing cursors.`
1117
+ : 'Partial delivery detected — retrying without advancing cursors.';
1118
+ state.lastDeliveryIssueAt = Date.now();
1119
+ state.eventsRetainedForRetry = envelopes.length;
1120
+ setLifecyclePhase('degraded');
1121
+ log.warn('shield', 'Partial delivery detected — not advancing cursors so events can be retried safely. Some already-accepted events may be resent.');
1122
+ markStateDirty();
1123
+ }
911
1124
  else {
912
1125
  state.consecutiveFailures++;
1126
+ state.lastPollError = firstFailure
1127
+ ? describeSendFailure(firstFailure.failureKind, firstFailure.statusCode, firstFailure.body)
1128
+ : 'Event delivery failed';
1129
+ state.lastDeliveryIssue = state.lastPollError;
1130
+ state.lastDeliveryIssueAt = Date.now();
1131
+ state.eventsRetainedForRetry = envelopes.length;
1132
+ state.lastLifecyclePhase = 'degraded';
1133
+ if (firstFailure?.failureKind === 'quota_exceeded') {
1134
+ setLifecyclePhase('degraded');
1135
+ }
913
1136
  markStateDirty();
914
1137
  }
915
1138
  state.lastPollAt = Date.now();
@@ -924,8 +1147,13 @@ exports.default = {
924
1147
  if (activeGeneration !== runtimeGeneration)
925
1148
  return;
926
1149
  state.consecutiveFailures++;
1150
+ state.lastPollError = err instanceof Error ? err.message : String(err);
1151
+ state.lastDeliveryIssue = state.lastPollError;
1152
+ state.lastDeliveryIssueAt = Date.now();
1153
+ state.lastLifecyclePhase = 'degraded';
927
1154
  markStateDirty();
928
1155
  log.error('shield', `Poll error: ${err instanceof Error ? err.message : String(err)}`);
1156
+ persistState();
929
1157
  }
930
1158
  };
931
1159
  const runPollSingleflight = createSingleflightRunner(poll);
@@ -957,7 +1185,8 @@ exports.default = {
957
1185
  if (!state.running)
958
1186
  return;
959
1187
  log.info('shield', '[checkpoint:SIGTERM] SIGTERM received — shutting down');
960
- await cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: true });
1188
+ setLifecyclePhase('stopping', 'checkpoint:SIGTERM');
1189
+ await cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: true, stopReason: 'signal' });
961
1190
  log.info('shield', 'Service stopped (signal)');
962
1191
  };
963
1192
  process.once('SIGTERM', onSignalHandler);
@@ -966,13 +1195,18 @@ exports.default = {
966
1195
  startGuard.endSuccess();
967
1196
  }
968
1197
  catch (err) {
1198
+ setLifecyclePhase('startup_failed');
1199
+ setStopReason('startup_failed');
1200
+ setLastError(err instanceof Error ? err.message : String(err));
1201
+ persistState();
969
1202
  startGuard.endFailure();
970
1203
  throw err;
971
1204
  }
972
1205
  },
973
1206
  async stop() {
974
1207
  const wasRunning = state.running;
975
- await cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: true });
1208
+ setLifecyclePhase('stopping');
1209
+ await cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: true, stopReason: 'service_stop' });
976
1210
  if (wasRunning)
977
1211
  log.info('shield', 'Service stopped');
978
1212
  },
@@ -984,6 +1218,7 @@ exports.default = {
984
1218
  const activated = state.activated || hasValidCredentials(creds);
985
1219
  const caseStatus = (0, case_monitor_1.getCaseMonitorStatus)();
986
1220
  const monitorHealth = (0, case_monitor_1.getMonitorHealth)();
1221
+ const updateState = (0, updater_1.loadUpdateState)();
987
1222
  const caseMonitorDisplay = monitorHealth.status === 'degraded'
988
1223
  ? `⚠️ Case Monitor: DEGRADED — ${monitorHealth.consecutiveFailures} consecutive failures since ${monitorHealth.degradedSinceMs ? new Date(monitorHealth.degradedSinceMs).toISOString() : 'unknown'}. Last error: ${monitorHealth.lastErrorMessage}`
989
1224
  : `✅ Case Monitor: ok`;
@@ -1005,18 +1240,47 @@ exports.default = {
1005
1240
  }
1006
1241
  }
1007
1242
  const hasSecret = !!creds?.hmacSecret && !PLACEHOLDER_VALUES.has((creds.hmacSecret || '').trim().toLowerCase());
1008
- const stateField = hasSecret ? (activated ? 'connected' : 'pending') : 'unconfigured';
1243
+ const stateField = !hasSecret
1244
+ ? 'unconfigured'
1245
+ : !activated
1246
+ ? 'pending'
1247
+ : state.running
1248
+ ? (state.lastLifecyclePhase === 'degraded' ? 'degraded' : 'connected')
1249
+ : state.lastLifecyclePhase;
1009
1250
  respond(true, {
1010
1251
  activated,
1011
1252
  state: stateField,
1012
1253
  running: state.running,
1013
1254
  lastPollAt: state.lastPollAt,
1255
+ lastSuccessfulPollAt: state.lastSuccessfulPollAt,
1014
1256
  lastCaptureAt: state.lastCaptureAt,
1015
1257
  captureSeenSinceLastSync: state.captureSeenSinceLastSync,
1016
1258
  eventsProcessed: state.eventsProcessed,
1017
1259
  quarantineCount: state.quarantineCount,
1018
1260
  consecutiveFailures: state.consecutiveFailures,
1261
+ telemetryConsecutiveFailures: state.telemetryConsecutiveFailures,
1262
+ lastTelemetryAt: state.lastTelemetryAt,
1263
+ lastSuccessfulTelemetryAt: state.lastSuccessfulTelemetryAt,
1264
+ lastTelemetryError: state.lastTelemetryError,
1265
+ lastPollError: state.lastPollError,
1266
+ lastLifecyclePhase: state.lastLifecyclePhase,
1267
+ lastStopReason: state.lastStopReason,
1268
+ lastError: state.lastError,
1269
+ lastStartupCheckpoint: state.lastStartupCheckpoint,
1270
+ sessionDirCount: state.sessionDirCount,
1271
+ eventsRetainedForRetry: state.eventsRetainedForRetry,
1272
+ lastDeliveryIssue: state.lastDeliveryIssue,
1273
+ lastDeliveryIssueAt: state.lastDeliveryIssueAt,
1019
1274
  version: version_1.VERSION,
1275
+ update: {
1276
+ pendingRestart: updateState.pendingRestart,
1277
+ restartAttempts: updateState.restartAttempts,
1278
+ latestVersion: updateState.latestVersion,
1279
+ updateAvailable: updateState.updateAvailable,
1280
+ lastError: updateState.lastError,
1281
+ lastFailureStage: updateState.lastFailureStage,
1282
+ rollbackPending: updateState.rollbackPending,
1283
+ },
1020
1284
  caseMonitor: {
1021
1285
  intervalMs: caseStatus.intervalMs,
1022
1286
  nextCheckInMs: caseStatus.nextCheckIn,
@@ -1071,34 +1335,39 @@ exports.default = {
1071
1335
  hmacSecret: rpcCreds?.hmacSecret || '',
1072
1336
  };
1073
1337
  (0, rpc_1.registerAllRpcs)(api, platformApiConfig);
1074
- api.registerCommand({
1075
- name: 'shieldcases',
1076
- description: 'Show pending Shield security cases',
1077
- requireAuth: true,
1078
- handler: () => {
1079
- const { getPendingCases } = require('./src/case-monitor');
1080
- const cases = getPendingCases();
1081
- if (cases.length === 0) {
1082
- return { text: '✅ No pending Shield security cases.' };
1083
- }
1084
- const severityEmoji = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
1085
- const lines = cases.map((c) => {
1086
- const emoji = severityEmoji[c.severity] || '⚠️';
1087
- const age = Math.floor((Date.now() - new Date(c.created_at).getTime()) / 60000);
1088
- const ageStr = age < 60 ? `${age}m` : `${Math.floor(age / 60)}h`;
1089
- return `${emoji} **${c.rule_title}** (${c.severity || 'unknown'}) ${c.event_count} events — ${ageStr} ago`;
1090
- });
1091
- return {
1092
- text: [
1093
- `🛡️ **Shield — ${cases.length} Pending Case${cases.length > 1 ? 's' : ''}**`,
1094
- '',
1095
- ...lines,
1096
- '',
1097
- 'Use `openclaw shield cases show <id>` for details.',
1098
- ].join('\n'),
1099
- };
1100
- },
1101
- });
1338
+ if (typeof api.registerCommand === 'function') {
1339
+ api.registerCommand({
1340
+ name: 'shieldcases',
1341
+ description: 'Show pending Shield security cases',
1342
+ requireAuth: true,
1343
+ handler: () => {
1344
+ const { getPendingCases } = require('./src/case-monitor');
1345
+ const cases = getPendingCases();
1346
+ if (cases.length === 0) {
1347
+ return { text: '✅ No pending Shield security cases.' };
1348
+ }
1349
+ const severityEmoji = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
1350
+ const lines = cases.map((c) => {
1351
+ const emoji = severityEmoji[c.severity] || '⚠️';
1352
+ const age = Math.floor((Date.now() - new Date(c.created_at).getTime()) / 60000);
1353
+ const ageStr = age < 60 ? `${age}m` : `${Math.floor(age / 60)}h`;
1354
+ return `${emoji} **${c.rule_title}** (${c.severity || 'unknown'}) — ${c.event_count} events — ${ageStr} ago`;
1355
+ });
1356
+ return {
1357
+ text: [
1358
+ `🛡️ **Shield — ${cases.length} Pending Case${cases.length > 1 ? 's' : ''}**`,
1359
+ '',
1360
+ ...lines,
1361
+ '',
1362
+ 'Use `openclaw shield cases show <id>` for details.',
1363
+ ].join('\n'),
1364
+ };
1365
+ },
1366
+ });
1367
+ }
1368
+ else {
1369
+ log.debug('shield', 'registerCommand not available — auto-reply command /shieldcases disabled on this OpenClaw runtime');
1370
+ }
1102
1371
  api.registerCli(({ program }) => {
1103
1372
  const shield = program.command('shield');
1104
1373
  (0, cli_cases_1.registerCasesCli)(shield);
@@ -1237,10 +1506,9 @@ exports.default = {
1237
1506
  step('Stale updateAvailable cleared');
1238
1507
  }
1239
1508
  step('Resetting session state...');
1240
- state.running = false;
1241
- state.consecutiveFailures = 0;
1242
- state.lastPollAt = 0;
1243
- state.captureSeenSinceLastSync = false;
1509
+ Object.assign(state, createInitialState());
1510
+ state.lastLifecyclePhase = 'stopping';
1511
+ state.lastStopReason = 'manual_restart';
1244
1512
  markStateDirty();
1245
1513
  persistState();
1246
1514
  step('State reset');
@@ -1332,7 +1600,7 @@ exports.default = {
1332
1600
  console.log(` agent:${agentHash} workspace:${wsHash} identity=${identityLabel} bootstrapped=${bootstrapLabel}`);
1333
1601
  }
1334
1602
  try {
1335
- const { initVault, getAllMappings } = await Promise.resolve().then(() => __importStar(require('./src/redactor/vault')));
1603
+ const { initVault, getAllMappings } = require('./src/redactor/vault');
1336
1604
  initVault();
1337
1605
  const mappings = getAllMappings();
1338
1606
  const tokens = Object.keys(mappings);