@blueharford/scrypted-spatial-awareness 0.6.13 → 0.6.15

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/plugin.zip CHANGED
Binary file
@@ -36173,59 +36173,140 @@ const topology_1 = __webpack_require__(/*! ../models/topology */ "./src/models/t
36173
36173
  const spatial_reasoning_1 = __webpack_require__(/*! ./spatial-reasoning */ "./src/core/spatial-reasoning.ts");
36174
36174
  const { systemManager } = sdk_1.default;
36175
36175
  /** Scene analysis prompt for single camera */
36176
- const SCENE_ANALYSIS_PROMPT = `Analyze this security camera image and identify what you see.
36176
+ const SCENE_ANALYSIS_PROMPT = `You are analyzing a security camera image. Describe EVERYTHING you can see in detail.
36177
+
36178
+ ## INSTRUCTIONS
36179
+ Look at this image carefully and identify ALL visible objects, structures, and areas. Be thorough - even small or partially visible items are important for security awareness.
36180
+
36181
+ ## 1. LANDMARKS - List EVERY distinct object or feature you can see:
36182
+
36183
+ **Structures** (buildings, parts of buildings):
36184
+ - Houses, garages, sheds, porches, decks, patios, carports, gazebos
36185
+ - Walls, pillars, columns, railings, stairs, steps
36186
+
36187
+ **Vegetation** (plants, trees, landscaping):
36188
+ - Trees (describe type if identifiable: oak, palm, pine, etc.)
36189
+ - Bushes, shrubs, hedges
36190
+ - Flower beds, gardens, planters, potted plants
36191
+ - Grass/lawn areas, mulch beds
36192
+
36193
+ **Boundaries & Barriers**:
36194
+ - Fences (wood, chain-link, aluminum, vinyl, iron, privacy)
36195
+ - Walls (brick, stone, concrete, retaining)
36196
+ - Gates, gate posts
36197
+ - Hedges used as boundaries
36198
+
36199
+ **Access Points & Pathways**:
36200
+ - Doors (front, side, garage, screen)
36201
+ - Driveways (concrete, asphalt, gravel, pavers)
36202
+ - Walkways, sidewalks, paths, stepping stones
36203
+ - Stairs, ramps, porches
36204
+
36205
+ **Utility & Fixtures**:
36206
+ - Mailboxes, package boxes
36207
+ - Light fixtures, lamp posts, solar lights
36208
+ - A/C units, utility boxes, meters
36209
+ - Trash cans, recycling bins
36210
+ - Hoses, spigots, sprinklers
36211
+
36212
+ **Outdoor Items**:
36213
+ - Vehicles (cars, trucks, motorcycles, boats, trailers)
36214
+ - Furniture (chairs, tables, benches, swings)
36215
+ - Grills, fire pits, outdoor kitchens
36216
+ - Play equipment, trampolines, pools
36217
+ - Decorations, flags, signs
36218
+
36219
+ **Off-Property Elements** (important for security context):
36220
+ - Street, road, sidewalk
36221
+ - Neighbor's property/fence/house
36222
+ - Public areas visible
36223
+
36224
+ For EACH landmark, estimate its DISTANCE from the camera:
36225
+ - "close" = 0-10 feet (within arm's reach of camera)
36226
+ - "near" = 10-30 feet
36227
+ - "medium" = 30-60 feet
36228
+ - "far" = 60-100 feet
36229
+ - "distant" = 100+ feet (edge of property or beyond)
36230
+
36231
+ ## 2. ZONES - Identify distinct AREAS visible:
36232
+ - Front yard, backyard, side yard
36233
+ - Driveway, parking area
36234
+ - Patio, deck, porch
36235
+ - Garden area, lawn
36236
+ - Street/road
36237
+ - Neighbor's yard
36238
+
36239
+ For each zone, estimate what percentage of the image it covers (0.0 to 1.0).
36240
+
36241
+ ## 3. EDGES - What's at each edge of the frame:
36242
+ This helps understand what's just out of view.
36243
+
36244
+ ## 4. CAMERA CONTEXT:
36245
+ - Estimated mounting height (ground level, 8ft, 12ft, roofline, etc.)
36246
+ - Approximate field of view (narrow, medium, wide)
36247
+ - Facing direction if determinable (north, south, street-facing, etc.)
36177
36248
 
36178
- 1. LANDMARKS - Identify fixed features visible:
36179
- - Structures (house, garage, shed, porch, deck)
36180
- - Features (mailbox, tree, pool, garden, fountain)
36181
- - Access points (door, gate, driveway entrance, walkway)
36182
- - Boundaries (fence, wall, hedge)
36183
-
36184
- 2. ZONES - Identify area types visible:
36185
- - What type of area is this? (front yard, backyard, driveway, street, patio, walkway)
36186
- - Estimate what percentage of the frame each zone covers (0.0 to 1.0)
36187
-
36188
- 3. EDGES - What's visible at the frame edges:
36189
- - Top edge: (sky, roof, trees, etc.)
36190
- - Left edge: (fence, neighbor, street, etc.)
36191
- - Right edge: (fence, garage, etc.)
36192
- - Bottom edge: (ground, driveway, grass, etc.)
36193
-
36194
- 4. ORIENTATION - Estimate camera facing direction based on shadows, sun position, or landmarks
36195
-
36196
- Respond with ONLY valid JSON in this exact format:
36249
+ Respond with ONLY valid JSON:
36197
36250
  {
36198
36251
  "landmarks": [
36199
- {"name": "Front Door", "type": "access", "confidence": 0.9, "description": "White front door with black frame"}
36252
+ {"name": "Mailbox", "type": "feature", "distance": "medium", "confidence": 0.95, "description": "Black metal mailbox on wooden post, approximately 40 feet from camera"},
36253
+ {"name": "Aluminum Fence", "type": "boundary", "distance": "near", "confidence": 0.9, "description": "Silver aluminum fence running along left side of property, about 15-20 feet away"},
36254
+ {"name": "Large Oak Tree", "type": "feature", "distance": "far", "confidence": 0.85, "description": "Mature oak tree near property line, roughly 80 feet from camera"}
36200
36255
  ],
36201
36256
  "zones": [
36202
- {"name": "Front Yard", "type": "yard", "coverage": 0.4, "description": "Grass lawn area"}
36257
+ {"name": "Front Yard", "type": "yard", "coverage": 0.5, "description": "Grass lawn with some bare patches"},
36258
+ {"name": "Driveway", "type": "driveway", "coverage": 0.25, "description": "Concrete driveway leading to garage"}
36203
36259
  ],
36204
- "edges": {"top": "sky with clouds", "left": "fence and trees", "right": "garage wall", "bottom": "concrete walkway"},
36205
- "orientation": "north"
36206
- }`;
36260
+ "edges": {
36261
+ "top": "sky, tree canopy",
36262
+ "left": "aluminum fence, neighbor's yard beyond",
36263
+ "right": "side of house, garage door",
36264
+ "bottom": "concrete walkway, grass edge"
36265
+ },
36266
+ "cameraContext": {
36267
+ "mountHeight": "8 feet",
36268
+ "fieldOfView": "wide",
36269
+ "facingDirection": "street-facing"
36270
+ }
36271
+ }
36272
+
36273
+ BE THOROUGH. List every distinct item you can identify. A typical outdoor scene should have 5-15+ landmarks.`;
36207
36274
  /** Multi-camera correlation prompt */
36208
- const CORRELATION_PROMPT = `I have scene analyses from multiple security cameras at the same property. Help me correlate them to understand the property layout.
36275
+ const CORRELATION_PROMPT = `I have detailed scene analyses from multiple security cameras at the same property. Help me understand which landmarks appear in multiple camera views.
36209
36276
 
36210
36277
  CAMERA SCENES:
36211
36278
  {scenes}
36212
36279
 
36213
- Identify:
36214
- 1. Shared landmarks - Features that appear in multiple camera views
36215
- 2. Camera connections - How someone could move between camera views and estimated walking time
36216
- 3. Overall layout - Describe the property layout based on what you see
36280
+ ## PRIORITY ORDER (most important first):
36281
+
36282
+ ### 1. SHARED LANDMARKS (HIGHEST PRIORITY)
36283
+ Identify features that are visible from MULTIPLE cameras. This is crucial for understanding the property layout.
36284
+ - Look for the SAME fence, tree, mailbox, driveway, structure, etc. appearing in different camera views
36285
+ - Even partial visibility counts (e.g., a tree visible in full from one camera and just the edge from another)
36286
+ - Include landmarks that are at the boundary between camera views
36287
+
36288
+ ### 2. PROPERTY LAYOUT
36289
+ Based on what each camera sees and their overlapping features, describe:
36290
+ - Which areas each camera covers
36291
+ - How the cameras relate spatially (e.g., "Camera A looks toward Camera B's direction")
36292
+ - Overall property shape and features
36293
+
36294
+ ### 3. CONNECTIONS (Lower Priority)
36295
+ Only if clearly determinable, suggest walking paths between camera views.
36217
36296
 
36218
36297
  IMPORTANT: For camera references, use the EXACT device ID shown in parentheses (e.g., "device_123"), NOT the camera name.
36219
36298
 
36220
36299
  Respond with ONLY valid JSON:
36221
36300
  {
36222
36301
  "sharedLandmarks": [
36223
- {"name": "Driveway", "type": "access", "seenByCameras": ["device_123", "device_456"], "confidence": 0.8, "description": "Concrete driveway"}
36302
+ {"name": "Aluminum Fence", "type": "boundary", "seenByCameras": ["device_123", "device_456"], "confidence": 0.85, "description": "Silver aluminum fence visible on right edge of Camera A and left edge of Camera B"},
36303
+ {"name": "Large Oak Tree", "type": "feature", "seenByCameras": ["device_123", "device_789"], "confidence": 0.9, "description": "Mature oak tree in front yard, visible from both front and side cameras"},
36304
+ {"name": "Concrete Driveway", "type": "access", "seenByCameras": ["device_123", "device_456", "device_789"], "confidence": 0.95, "description": "Driveway visible from multiple angles"}
36224
36305
  ],
36225
36306
  "connections": [
36226
- {"from": "device_123", "to": "device_456", "transitSeconds": 10, "via": "driveway", "confidence": 0.7, "bidirectional": true}
36307
+ {"from": "device_123", "to": "device_456", "transitSeconds": 8, "via": "along driveway", "confidence": 0.6, "bidirectional": true}
36227
36308
  ],
36228
- "layoutDescription": "Single-story house with front yard facing street, driveway on the left side, backyard accessible through side gate"
36309
+ "layoutDescription": "Ranch-style house. Front camera covers front yard and street. Garage camera covers driveway entrance. Side camera covers side yard with aluminum fence separating from neighbor. Backyard camera shows deck and pool area."
36229
36310
  }`;
36230
36311
  class TopologyDiscoveryEngine {
36231
36312
  config;
@@ -36429,6 +36510,7 @@ class TopologyDiscoveryEngine {
36429
36510
  name: l.name || 'Unknown',
36430
36511
  type: this.mapLandmarkType(l.type),
36431
36512
  confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
36513
+ distance: this.mapDistance(l.distance),
36432
36514
  description: l.description || '',
36433
36515
  boundingBox: l.boundingBox,
36434
36516
  }));
@@ -36546,6 +36628,21 @@ class TopologyDiscoveryEngine {
36546
36628
  return 'west';
36547
36629
  return 'unknown';
36548
36630
  }
36631
+ /** Map LLM distance to our type */
36632
+ mapDistance(distance) {
36633
+ const dist = distance?.toLowerCase();
36634
+ if (dist?.includes('close'))
36635
+ return 'close';
36636
+ if (dist?.includes('near'))
36637
+ return 'near';
36638
+ if (dist?.includes('medium'))
36639
+ return 'medium';
36640
+ if (dist?.includes('far') && !dist?.includes('distant'))
36641
+ return 'far';
36642
+ if (dist?.includes('distant'))
36643
+ return 'distant';
36644
+ return 'medium'; // Default to medium if not specified
36645
+ }
36549
36646
  /** Resolve a camera reference (name or deviceId) to its deviceId */
36550
36647
  resolveCameraRef(ref) {
36551
36648
  if (!this.topology?.cameras || !ref)
@@ -36721,9 +36818,12 @@ class TopologyDiscoveryEngine {
36721
36818
  generateSuggestionsFromAnalysis(analysis) {
36722
36819
  if (!analysis.isValid)
36723
36820
  return;
36821
+ this.console.log(`[Discovery] Generating suggestions from ${analysis.landmarks.length} landmarks, ${analysis.zones.length} zones`);
36724
36822
  // Generate landmark suggestions
36725
36823
  for (const landmark of analysis.landmarks) {
36726
36824
  if (landmark.confidence >= this.config.minLandmarkConfidence) {
36825
+ // Calculate distance in feet from distance estimate
36826
+ const distanceFeet = landmark.distance ? (0, discovery_1.distanceToFeet)(landmark.distance) : 50;
36727
36827
  const suggestion = {
36728
36828
  id: `landmark_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
36729
36829
  type: 'landmark',
@@ -36736,26 +36836,30 @@ class TopologyDiscoveryEngine {
36736
36836
  type: landmark.type,
36737
36837
  description: landmark.description,
36738
36838
  visibleFromCameras: [analysis.cameraId],
36739
- // Include bounding box for positioning (will be used by applyDiscoverySuggestion)
36839
+ // Include extra metadata for positioning
36740
36840
  boundingBox: landmark.boundingBox,
36741
- }, // boundingBox is extra metadata not in Landmark interface
36841
+ distance: landmark.distance,
36842
+ distanceFeet: distanceFeet,
36843
+ }, // Extra metadata not in base Landmark interface
36742
36844
  };
36743
36845
  this.suggestions.set(suggestion.id, suggestion);
36846
+ this.console.log(`[Discovery] Landmark suggestion: ${landmark.name} (${landmark.type}, ${landmark.distance || 'medium'}, ~${distanceFeet}ft)`);
36744
36847
  }
36745
36848
  }
36746
- // Generate zone suggestions
36849
+ // Generate zone suggestions (even for smaller coverage - 10% is enough)
36747
36850
  for (const zone of analysis.zones) {
36748
- if (zone.coverage >= 0.2) {
36851
+ if (zone.coverage >= 0.1) {
36749
36852
  const suggestion = {
36750
36853
  id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
36751
36854
  type: 'zone',
36752
36855
  timestamp: Date.now(),
36753
36856
  sourceCameras: [analysis.cameraId],
36754
- confidence: 0.7,
36857
+ confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
36755
36858
  status: 'pending',
36756
36859
  zone: zone,
36757
36860
  };
36758
36861
  this.suggestions.set(suggestion.id, suggestion);
36862
+ this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage)`);
36759
36863
  }
36760
36864
  }
36761
36865
  }
@@ -36946,6 +37050,8 @@ class TrackingEngine {
36946
37050
  // ==================== Snapshot Cache ====================
36947
37051
  /** Cached snapshots for tracked objects (for faster notifications) */
36948
37052
  snapshotCache = new Map();
37053
+ /** Pending LLM description promises (started when snapshot is captured) */
37054
+ pendingDescriptions = new Map();
36949
37055
  constructor(topology, state, alertManager, config, console) {
36950
37056
  this.topology = topology;
36951
37057
  this.state = state;
@@ -37256,13 +37362,28 @@ class TrackingEngine {
37256
37362
  // Check if we've already alerted for this object
37257
37363
  if (this.isInAlertCooldown(globalId))
37258
37364
  return;
37259
- // Use cached snapshot (captured when object was first detected)
37260
- let mediaObject = this.snapshotCache.get(globalId);
37261
- this.console.log(`[Entry Alert] Using cached snapshot: ${!!mediaObject}`);
37262
- // Generate spatial description (now async with LLM support)
37263
- this.console.log(`[Entry Alert] Calling generateEntryDescription with mediaObject=${!!mediaObject}`);
37264
- const spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
37265
- this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37365
+ // Use prefetched LLM result if available (started when snapshot was captured)
37366
+ let spatialResult;
37367
+ const pendingDescription = this.pendingDescriptions.get(globalId);
37368
+ if (pendingDescription) {
37369
+ this.console.log(`[Entry Alert] Using prefetched LLM result for ${globalId.slice(0, 8)}`);
37370
+ try {
37371
+ spatialResult = await pendingDescription;
37372
+ this.console.log(`[Entry Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37373
+ }
37374
+ catch (e) {
37375
+ this.console.warn(`[Entry Alert] Prefetch failed, generating fallback: ${e}`);
37376
+ spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId);
37377
+ }
37378
+ this.pendingDescriptions.delete(globalId);
37379
+ }
37380
+ else {
37381
+ // Fallback: generate description now (slower path)
37382
+ this.console.log(`[Entry Alert] No prefetch available, generating now`);
37383
+ const mediaObject = this.snapshotCache.get(globalId);
37384
+ spatialResult = await this.spatialReasoning.generateEntryDescription(tracked, sighting.cameraId, mediaObject);
37385
+ this.console.log(`[Entry Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37386
+ }
37266
37387
  if (isEntryPoint) {
37267
37388
  // Entry point - generate property entry alert
37268
37389
  await this.alertManager.checkAndAlert('property_entry', tracked, {
@@ -37293,8 +37414,8 @@ class TrackingEngine {
37293
37414
  this.recordAlertTime(globalId);
37294
37415
  }, this.config.loiteringThreshold);
37295
37416
  }
37296
- /** Capture and cache a snapshot for a tracked object */
37297
- async captureAndCacheSnapshot(globalId, cameraId) {
37417
+ /** Capture and cache a snapshot for a tracked object, and start LLM analysis immediately */
37418
+ async captureAndCacheSnapshot(globalId, cameraId, eventType = 'entry') {
37298
37419
  try {
37299
37420
  const camera = systemManager.getDeviceById(cameraId);
37300
37421
  if (camera?.interfaces?.includes(sdk_1.ScryptedInterface.Camera)) {
@@ -37302,6 +37423,21 @@ class TrackingEngine {
37302
37423
  if (mediaObject) {
37303
37424
  this.snapshotCache.set(globalId, mediaObject);
37304
37425
  this.console.log(`[Snapshot] Cached snapshot for ${globalId.slice(0, 8)} from ${cameraId}`);
37426
+ // Start LLM analysis immediately in parallel (don't await)
37427
+ const tracked = this.state.getObject(globalId);
37428
+ if (tracked && this.config.useLlmDescriptions) {
37429
+ this.console.log(`[LLM Prefetch] Starting ${eventType} analysis for ${globalId.slice(0, 8)}`);
37430
+ const descriptionPromise = eventType === 'exit'
37431
+ ? this.spatialReasoning.generateExitDescription(tracked, cameraId, mediaObject)
37432
+ : this.spatialReasoning.generateEntryDescription(tracked, cameraId, mediaObject);
37433
+ this.pendingDescriptions.set(globalId, descriptionPromise);
37434
+ // Log when complete (for debugging)
37435
+ descriptionPromise.then(result => {
37436
+ this.console.log(`[LLM Prefetch] ${eventType} analysis ready for ${globalId.slice(0, 8)}: "${result.description.substring(0, 40)}..."`);
37437
+ }).catch(e => {
37438
+ this.console.warn(`[LLM Prefetch] Failed for ${globalId.slice(0, 8)}: ${e}`);
37439
+ });
37440
+ }
37305
37441
  }
37306
37442
  }
37307
37443
  }
@@ -37349,8 +37485,9 @@ class TrackingEngine {
37349
37485
  // Mark as pending and set timer
37350
37486
  this.state.markPending(tracked.globalId);
37351
37487
  // Capture a fresh snapshot now while object is still visible (before they leave)
37488
+ // Also starts LLM analysis immediately in parallel
37352
37489
  if (this.config.useLlmDescriptions) {
37353
- this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId).catch(e => {
37490
+ this.captureAndCacheSnapshot(tracked.globalId, sighting.cameraId, 'exit').catch(e => {
37354
37491
  this.console.warn(`[Exit Snapshot] Failed to update snapshot: ${e}`);
37355
37492
  });
37356
37493
  }
@@ -37359,13 +37496,28 @@ class TrackingEngine {
37359
37496
  const current = this.state.getObject(tracked.globalId);
37360
37497
  if (current && current.state === 'pending') {
37361
37498
  this.state.markExited(tracked.globalId, sighting.cameraId, sighting.cameraName);
37362
- // Use cached snapshot (captured when exit was first detected, while object was still visible)
37363
- let mediaObject = this.snapshotCache.get(tracked.globalId);
37364
- this.console.log(`[Exit Alert] Using cached snapshot: ${!!mediaObject}`);
37365
- // Generate rich exit description using topology context (now async with LLM support)
37366
- this.console.log(`[Exit Alert] Calling generateExitDescription with mediaObject=${!!mediaObject}`);
37367
- const spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
37368
- this.console.log(`[Exit Alert] Object ${tracked.globalId.slice(0, 8)} exited: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37499
+ // Use prefetched LLM result if available (started when exit was first detected)
37500
+ let spatialResult;
37501
+ const pendingDescription = this.pendingDescriptions.get(tracked.globalId);
37502
+ if (pendingDescription) {
37503
+ this.console.log(`[Exit Alert] Using prefetched LLM result for ${tracked.globalId.slice(0, 8)}`);
37504
+ try {
37505
+ spatialResult = await pendingDescription;
37506
+ this.console.log(`[Exit Alert] Prefetch result: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37507
+ }
37508
+ catch (e) {
37509
+ this.console.warn(`[Exit Alert] Prefetch failed, generating fallback: ${e}`);
37510
+ spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId);
37511
+ }
37512
+ this.pendingDescriptions.delete(tracked.globalId);
37513
+ }
37514
+ else {
37515
+ // Fallback: generate description now (slower path)
37516
+ this.console.log(`[Exit Alert] No prefetch available, generating now`);
37517
+ const mediaObject = this.snapshotCache.get(tracked.globalId);
37518
+ spatialResult = await this.spatialReasoning.generateExitDescription(current, sighting.cameraId, mediaObject);
37519
+ this.console.log(`[Exit Alert] Got description: "${spatialResult.description.substring(0, 60)}...", usedLlm=${spatialResult.usedLlm}`);
37520
+ }
37369
37521
  await this.alertManager.checkAndAlert('property_exit', current, {
37370
37522
  cameraId: sighting.cameraId,
37371
37523
  cameraName: sighting.cameraName,
@@ -37374,8 +37526,9 @@ class TrackingEngine {
37374
37526
  involvedLandmarks: spatialResult.involvedLandmarks?.map(l => l.name),
37375
37527
  usedLlm: spatialResult.usedLlm,
37376
37528
  });
37377
- // Clean up cached snapshot after exit alert
37529
+ // Clean up cached snapshot and pending descriptions after exit alert
37378
37530
  this.snapshotCache.delete(tracked.globalId);
37531
+ this.pendingDescriptions.delete(tracked.globalId);
37379
37532
  }
37380
37533
  this.pendingTimers.delete(tracked.globalId);
37381
37534
  }, this.config.correlationWindow);
@@ -37391,8 +37544,9 @@ class TrackingEngine {
37391
37544
  this.state.markLost(tracked.globalId);
37392
37545
  this.console.log(`Object ${tracked.globalId.slice(0, 8)} marked as lost ` +
37393
37546
  `(not seen for ${Math.round(timeSinceSeen / 1000)}s)`);
37394
- // Clean up cached snapshot
37547
+ // Clean up cached snapshot and pending descriptions
37395
37548
  this.snapshotCache.delete(tracked.globalId);
37549
+ this.pendingDescriptions.delete(tracked.globalId);
37396
37550
  this.alertManager.checkAndAlert('lost_tracking', tracked, {
37397
37551
  objectClass: tracked.className,
37398
37552
  objectLabel: tracked.label,
@@ -41107,6 +41261,7 @@ function createAlert(type, trackedObjectId, details, severity = 'info', ruleId)
41107
41261
  */
41108
41262
  Object.defineProperty(exports, "__esModule", ({ value: true }));
41109
41263
  exports.DEFAULT_DISCOVERY_STATUS = exports.RATE_LIMIT_WARNING_THRESHOLD = exports.DEFAULT_DISCOVERY_CONFIG = void 0;
41264
+ exports.distanceToFeet = distanceToFeet;
41110
41265
  /** Default discovery configuration */
41111
41266
  exports.DEFAULT_DISCOVERY_CONFIG = {
41112
41267
  discoveryIntervalHours: 0, // Disabled by default
@@ -41116,6 +41271,17 @@ exports.DEFAULT_DISCOVERY_CONFIG = {
41116
41271
  };
41117
41272
  /** Rate limit warning thresholds (in hours) */
41118
41273
  exports.RATE_LIMIT_WARNING_THRESHOLD = 1; // Warn if interval is less than 1 hour
41274
+ /** Convert distance estimate to approximate feet */
41275
+ function distanceToFeet(distance) {
41276
+ switch (distance) {
41277
+ case 'close': return 5; // 0-10 feet
41278
+ case 'near': return 20; // 10-30 feet
41279
+ case 'medium': return 45; // 30-60 feet
41280
+ case 'far': return 80; // 60-100 feet
41281
+ case 'distant': return 150; // 100+ feet
41282
+ default: return 50;
41283
+ }
41284
+ }
41119
41285
  /** Default discovery status */
41120
41286
  exports.DEFAULT_DISCOVERY_STATUS = {
41121
41287
  isRunning: false,