@camstack/addon-post-analysis 1.2.662 → 1.2.664

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 (16) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{clip-model-registry-ELaSS9jq.mjs → clip-model-registry-Ch9Sz-Qb.mjs} +33939 -33004
  3. package/dist/{clip-model-registry-C3N1NpST.js → clip-model-registry-TeNUAAPP.js} +33939 -33004
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BvOlyiL9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CXG7YPYq.mjs} +2 -2
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CIt_RDlm.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-4TTlyJ66.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-BjZchMMh.mjs → hostInit-BZ_D3_sF.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +337 -50
  12. package/dist/pipeline-analytics/index.mjs +337 -50
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +2 -2
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D4zkNcgf.mjs +0 -26
  16. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-vaatUWY6.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-C3N1NpST.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-TeNUAAPP.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -59338,56 +59338,143 @@ function sweepOne(deps, deviceId) {
59338
59338
  }
59339
59339
  //#endregion
59340
59340
  //#region src/pipeline-analytics/pipeline/package/package-still-capture.ts
59341
- /** A held look frame older than this is not the parcel's picture any more. */
59342
- var PACKAGE_LOOK_FRAME_MAX_AGE_MS = 15e3;
59341
+ /**
59342
+ * The runner's native lease on a look frame (holdFrames 8) is released about
59343
+ * 100 ms after inference, so a handle older than this answers `native-miss` or
59344
+ * `preview-only` — promotion runs seconds to minutes after the look (D60).
59345
+ */
59346
+ var PACKAGE_NATIVE_HANDLE_MAX_AGE_MS = 1e3;
59347
+ /** Total time a promotion may spend landing its still before it ships without (D60). */
59348
+ var PACKAGE_STILL_BOUND_MS = 6e3;
59349
+ /** One live-source attempt never takes longer than this (a sleeping camera). */
59350
+ var PACKAGE_STILL_ATTEMPT_TIMEOUT_MS = 3e3;
59351
+ /** Pause between two live attempts. */
59352
+ var PACKAGE_STILL_RETRY_PAUSE_MS = 750;
59353
+ var PACKAGE_STILL_LIVE_ATTEMPTS = 3;
59354
+ var realPause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
59343
59355
  async function jpegOf(reply) {
59344
59356
  if (reply.tier === "ram-fullframe") return "preview-only";
59345
59357
  if (reply.jpegBytes !== void 0 && reply.jpegBytes.byteLength > 0) return reply.jpegBytes;
59346
59358
  if (reply.bytes !== void 0 && reply.bytes.length === reply.width * reply.height * 3) return encodeRgbCropToJpeg(reply.bytes, reply.width, reply.height);
59347
59359
  return "empty-frame";
59348
59360
  }
59361
+ /** One live read, cut off after `limitMs` so a hung camera cannot hold the delivery. */
59362
+ async function readLive(source, limitMs) {
59363
+ let timer;
59364
+ const timeout = new Promise((resolve) => {
59365
+ timer = setTimeout(() => resolve(null), limitMs);
59366
+ });
59367
+ try {
59368
+ return await Promise.race([source(), timeout]);
59369
+ } finally {
59370
+ if (timer !== void 0) clearTimeout(timer);
59371
+ }
59372
+ }
59373
+ /** Retry the live source under {@link PACKAGE_STILL_BOUND_MS} until a frame lands (D60). */
59374
+ async function liveStill(input, source) {
59375
+ const now = input.now ?? Date.now;
59376
+ const pause = input.pause ?? realPause;
59377
+ const deadline = now() + PACKAGE_STILL_BOUND_MS;
59378
+ let reason = "live-unavailable";
59379
+ let attempts = 0;
59380
+ while (attempts < PACKAGE_STILL_LIVE_ATTEMPTS) {
59381
+ const remaining = deadline - now();
59382
+ if (remaining <= 0) return {
59383
+ jpeg: null,
59384
+ reason: "bound-exhausted",
59385
+ attempts
59386
+ };
59387
+ attempts += 1;
59388
+ try {
59389
+ const jpeg = await readLive(source, Math.min(PACKAGE_STILL_ATTEMPT_TIMEOUT_MS, remaining));
59390
+ if (jpeg !== null && jpeg.byteLength > 0) return {
59391
+ jpeg,
59392
+ reason,
59393
+ attempts
59394
+ };
59395
+ reason = jpeg === null ? "live-unavailable" : "empty-frame";
59396
+ } catch {
59397
+ reason = "live-unavailable";
59398
+ }
59399
+ if (attempts < PACKAGE_STILL_LIVE_ATTEMPTS) await pause(PACKAGE_STILL_RETRY_PAUSE_MS);
59400
+ }
59401
+ return {
59402
+ jpeg: null,
59403
+ reason,
59404
+ attempts
59405
+ };
59406
+ }
59349
59407
  /**
59350
59408
  * The entry carrying its still's `keyFrameMediaId`, or the entry unchanged when
59351
59409
  * it already had one or none could be stored (said, with the reason).
59410
+ *
59411
+ * Ladder: the native handle while its lease can still be alive, then a frame
59412
+ * that exists now (retried under a bound). Every landed still is logged next to
59413
+ * every miss (D400).
59352
59414
  */
59353
59415
  async function withParcelStill(input) {
59354
59416
  const { entry, logger } = input;
59355
59417
  if (entry.keyFrameMediaId !== void 0) return entry;
59356
- const miss = (reason, detail) => {
59418
+ const tags = { deviceId: entry.deviceId };
59419
+ const miss = (reason, extra = {}) => {
59357
59420
  logger.warn("package still NOT stored — the delivery ships without a picture of the parcel", {
59358
- tags: { deviceId: entry.deviceId },
59421
+ tags,
59359
59422
  meta: {
59360
59423
  entryId: entry.id,
59361
59424
  reason,
59362
- ...detail !== void 0 ? { error: detail } : {}
59425
+ ...extra
59363
59426
  }
59364
59427
  });
59365
59428
  return entry;
59366
59429
  };
59367
- if (input.handle === null) return miss("no-frame-handle");
59368
59430
  if (input.writer === null) return miss("no-media-store");
59431
+ const now = input.now ?? Date.now;
59432
+ let jpeg = null;
59433
+ let source = "native";
59434
+ let stamp = input.timestamp;
59435
+ let nativeMiss = "no-frame-handle";
59436
+ let liveReason = null;
59437
+ let attempts = 0;
59369
59438
  try {
59370
- const reply = await input.query({
59371
- handle: input.handle,
59372
- bbox: {
59373
- x: 0,
59374
- y: 0,
59375
- w: entry.frameWidth,
59376
- h: entry.frameHeight
59377
- },
59378
- acceptJpegBytes: true,
59379
- encodeJpeg: true,
59380
- pin: require_clip_model_registry.nodePin(input.handle.nodeId)
59439
+ if (input.handle !== null) {
59440
+ const reply = await input.query({
59441
+ handle: input.handle,
59442
+ bbox: {
59443
+ x: 0,
59444
+ y: 0,
59445
+ w: entry.frameWidth,
59446
+ h: entry.frameHeight
59447
+ },
59448
+ acceptJpegBytes: true,
59449
+ encodeJpeg: true,
59450
+ pin: require_clip_model_registry.nodePin(input.handle.nodeId)
59451
+ });
59452
+ if (reply === null) nativeMiss = "native-miss";
59453
+ else {
59454
+ const out = await jpegOf(reply);
59455
+ if (typeof out === "string") nativeMiss = out;
59456
+ else jpeg = out;
59457
+ }
59458
+ }
59459
+ if (jpeg === null && input.live !== null) {
59460
+ const live = await liveStill(input, input.live);
59461
+ attempts = live.attempts;
59462
+ if (live.jpeg !== null) {
59463
+ jpeg = live.jpeg;
59464
+ source = "live";
59465
+ stamp = now();
59466
+ } else liveReason = live.reason;
59467
+ }
59468
+ if (jpeg === null) return miss(liveReason ?? nativeMiss, {
59469
+ ...input.handle !== null ? { nativeMiss } : {},
59470
+ ...liveReason !== null ? { liveAttempts: attempts } : {}
59381
59471
  });
59382
- if (reply === null) return miss("native-miss");
59383
- const jpeg = await jpegOf(reply);
59384
- if (typeof jpeg === "string") return miss(jpeg);
59385
59472
  const keyFrameMediaId = await input.writer.putReplacing({
59386
59473
  deviceId: entry.deviceId,
59387
59474
  ownerKind: "track",
59388
59475
  ownerId: entry.id,
59389
59476
  kind: "keyFrame",
59390
- timestamp: input.timestamp,
59477
+ timestamp: stamp,
59391
59478
  data: Buffer.from(jpeg),
59392
59479
  subjectBox: {
59393
59480
  x: entry.bbox.x,
@@ -59398,12 +59485,20 @@ async function withParcelStill(input) {
59398
59485
  frameHeight: entry.frameHeight
59399
59486
  }
59400
59487
  });
59488
+ logger.info("package still stored", {
59489
+ tags,
59490
+ meta: {
59491
+ entryId: entry.id,
59492
+ mediaId: keyFrameMediaId,
59493
+ source
59494
+ }
59495
+ });
59401
59496
  return {
59402
59497
  ...entry,
59403
59498
  keyFrameMediaId
59404
59499
  };
59405
59500
  } catch (err) {
59406
- return miss("write-failed", require_clip_model_registry.errMsg(err));
59501
+ return miss("write-failed", { error: require_clip_model_registry.errMsg(err) });
59407
59502
  }
59408
59503
  }
59409
59504
  /**
@@ -70790,6 +70885,7 @@ Object.freeze({
70790
70885
  "pageSize": 50
70791
70886
  } },
70792
70887
  "device-runs": { "listRuns": { "limit": 50 } },
70888
+ "fan-presets": { "capturePreset": { "steps": [] } },
70793
70889
  "llm": { "upsertProfile": { "profile": { "__nested__": {
70794
70890
  "timeoutMs": 6e4,
70795
70891
  "connectTimeoutMs": 1e4,
@@ -71030,6 +71126,10 @@ Object.freeze({
71030
71126
  "speed": 2
71031
71127
  }
71032
71128
  },
71129
+ "system": { "debugWatch": {
71130
+ "channels": [],
71131
+ "deviceIds": []
71132
+ } },
71033
71133
  "terminal-session": { "pullOutput": { "waitMs": 0 } },
71034
71134
  "training": {
71035
71135
  "planJob": { "selection": { "__nested__": { "workState": "complete" } } },
@@ -71076,6 +71176,7 @@ Object.freeze({
71076
71176
  ],
71077
71177
  "device-removal-hook": ["listDeviceData"],
71078
71178
  "failure-contribution": ["list"],
71179
+ "integration-instances": ["listInstances"],
71079
71180
  "llm": [
71080
71181
  "getDefaults",
71081
71182
  "getUsage",
@@ -71117,6 +71218,7 @@ Object.freeze({
71117
71218
  "listSupportedEntityKinds",
71118
71219
  "unexposeEntity"
71119
71220
  ],
71221
+ "integration-instances": ["testInstance"],
71120
71222
  "log-destination": [
71121
71223
  "cancelRead",
71122
71224
  "describeRead",
@@ -85215,6 +85317,9 @@ async function renderMosaic(request, logger) {
85215
85317
  * one composition are decisions about the set, and none of them belongs in a
85216
85318
  * shelf that photographs one camera for an absence.
85217
85319
  *
85320
+ * A SLEEPING battery camera is woken by one forced capture when the rule leaves
85321
+ * it allowed (D998) — bounded, once per gap per camera, never by a poll.
85322
+ *
85218
85323
  * Two reads can only ever ADD a camera back, never take one away on their own
85219
85324
  * failure (D49): a wrapper-state read (run in parallel with the capture) that
85220
85325
  * misses or throws keeps the tile, and a freshness read that misses or throws
@@ -85228,6 +85333,15 @@ var NC_LINKED_CAMERA_PREFIX = "nc-linked:";
85228
85333
  function isLinkedStillId(id) {
85229
85334
  return id.startsWith(NC_LINKED_MOSAIC_PREFIX) || id.startsWith(NC_LINKED_CAMERA_PREFIX);
85230
85335
  }
85336
+ /**
85337
+ * D998 — the one forced capture that wakes a sleeping battery camera for a
85338
+ * sensor notification: the bound it may take, the pause between re-polls of a
85339
+ * camera another wake already started, and the gap per camera before it is
85340
+ * woken again (a flapping contact must not drain a battery).
85341
+ */
85342
+ var NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS = 15e3;
85343
+ var NC_LINKED_WAKE_POLL_MS = 1e3;
85344
+ var NC_LINKED_WAKE_MIN_GAP_MS = 6e4;
85231
85345
  /** Per camera — the marker's own bound (`SENSOR_MARKER_SNAPSHOT_TIMEOUT_MS`). */
85232
85346
  var SENSOR_LINKED_CAPTURE_TIMEOUT_MS = 2500;
85233
85347
  /** The capture-free wrapper-state read, per camera. It runs IN PARALLEL with
@@ -85260,11 +85374,14 @@ var NcLinkedStillShelf = class {
85260
85374
  deps;
85261
85375
  sets = /* @__PURE__ */ new Map();
85262
85376
  owners = /* @__PURE__ */ new Map();
85377
+ /** setId → when its in-flight wake gives up (the dispatcher waits that long). */
85378
+ pendingWakes = /* @__PURE__ */ new Map();
85379
+ lastWakeAt = /* @__PURE__ */ new Map();
85263
85380
  constructor(deps) {
85264
85381
  this.deps = deps;
85265
85382
  }
85266
85383
  /** SYNCHRONOUS: the evaluation that follows names these owners. */
85267
- captureSet(sensorId, cameraIds, atMs) {
85384
+ captureSet(sensorId, cameraIds, atMs, options = { wakeSleeping: false }) {
85268
85385
  const now = this.deps.now();
85269
85386
  this.prune(now);
85270
85387
  const setId = (0, node_crypto.randomUUID)();
@@ -85292,7 +85409,7 @@ var NcLinkedStillShelf = class {
85292
85409
  camera: cameraId
85293
85410
  });
85294
85411
  });
85295
- this.run(setId, stills, atMs, now).catch((err) => {
85412
+ this.run(setId, stills, atMs, now, options).catch((err) => {
85296
85413
  this.deps.logger.warn("sensor notification pictures failed — it ships text-only", {
85297
85414
  tags: { deviceId: sensorId },
85298
85415
  meta: { error: String(err) }
@@ -85300,6 +85417,15 @@ var NcLinkedStillShelf = class {
85300
85417
  });
85301
85418
  return stills;
85302
85419
  }
85420
+ /**
85421
+ * While a wake of one of this owner's cameras is in flight: the instant it
85422
+ * gives up (its bound plus one poll). `undefined` otherwise — the dispatcher
85423
+ * then keeps its ordinary still-wait.
85424
+ */
85425
+ pendingUntil(ownerId) {
85426
+ const ref = this.owners.get(ownerId);
85427
+ return ref === void 0 ? void 0 : this.pendingWakes.get(ref.setId);
85428
+ }
85303
85429
  /** `undefined` = not an owner this shelf minted (or pruned); `[]` = nothing (yet). */
85304
85430
  get(ownerId) {
85305
85431
  const ref = this.owners.get(ownerId);
@@ -85321,20 +85447,37 @@ var NcLinkedStillShelf = class {
85321
85447
  clear() {
85322
85448
  this.sets.clear();
85323
85449
  this.owners.clear();
85450
+ this.pendingWakes.clear();
85324
85451
  }
85325
- async run(setId, stills, atMs, startedAt) {
85326
- const results = await Promise.all(stills.cameraIds.map((id) => this.captureOne(id, startedAt)));
85452
+ async run(setId, stills, atMs, startedAt, options) {
85453
+ const ctx = {
85454
+ setId,
85455
+ sensorId: stills.sensorId,
85456
+ startedAt,
85457
+ options
85458
+ };
85459
+ let results;
85460
+ try {
85461
+ results = await Promise.all(stills.cameraIds.map((id) => this.captureOne(id, ctx)));
85462
+ } finally {
85463
+ this.pendingWakes.delete(setId);
85464
+ }
85327
85465
  const judged = await this.judgeFreshness(stills.sensorId, results, atMs);
85328
85466
  for (const r of judged) this.logTile(stills.sensorId, r);
85329
85467
  const landed = judged.filter(isLandedTile);
85330
85468
  const composed = await this.compose(stills.sensorId, landed, atMs);
85331
85469
  const undrawable = landed.length - composed.drawn;
85470
+ const woken = judged.filter((r) => r.woken === true).map((r) => r.cameraId);
85332
85471
  this.deps.logger.info(composed.mosaic.length === 0 ? "sensor notification has no linked picture — it ships text-only" : "sensor notification pictures ready", {
85333
85472
  tags: { deviceId: stills.sensorId },
85334
85473
  meta: {
85335
85474
  cameras: stills.cameraIds.length,
85336
85475
  landed: composed.drawn,
85337
85476
  ...undrawable > 0 ? { undrawable } : {},
85477
+ ...woken.length > 0 ? {
85478
+ woken,
85479
+ waitedMs: this.deps.now() - startedAt
85480
+ } : {},
85338
85481
  dropped: Object.fromEntries(judged.filter((r) => r.outcome !== "landed").map((r) => [String(r.cameraId), r.outcome])),
85339
85482
  tookMs: this.deps.now() - startedAt
85340
85483
  }
@@ -85347,21 +85490,25 @@ var NcLinkedStillShelf = class {
85347
85490
  mosaic: composed.mosaic
85348
85491
  });
85349
85492
  }
85350
- async captureOne(cameraId, startedAt) {
85351
- const took = () => this.deps.now() - startedAt;
85493
+ async captureOne(cameraId, ctx) {
85494
+ const took = () => this.deps.now() - ctx.startedAt;
85352
85495
  if (this.deps.isOnline(cameraId) === false) return {
85353
85496
  cameraId,
85354
85497
  outcome: "excluded",
85355
85498
  reason: "offline",
85356
85499
  tookMs: took()
85357
85500
  };
85358
- const [verdict, capture] = await Promise.all([this.wrapperVerdict(cameraId), this.captureStill(cameraId)]);
85359
- if (verdict.reason !== null) return {
85501
+ const [verdict, capture] = await Promise.all([this.wrapperVerdict(cameraId), this.captureStill(cameraId, false, SENSOR_LINKED_CAPTURE_TIMEOUT_MS)]);
85502
+ const excluded = (reason) => ({
85360
85503
  cameraId,
85361
85504
  outcome: "excluded",
85362
- reason: verdict.reason,
85505
+ reason,
85363
85506
  tookMs: took()
85364
- };
85507
+ });
85508
+ if (verdict.reason === "sleeping") return this.wakeOne(cameraId, ctx);
85509
+ if (verdict.reason === "waking" && ctx.options.wakeSleeping) return this.awaitWaking(cameraId, ctx);
85510
+ if (verdict.reason === "unreachable" && ctx.options.wakeSleeping) return excluded("wake-skipped:unreachable");
85511
+ if (verdict.reason !== null) return excluded(verdict.reason);
85365
85512
  return {
85366
85513
  cameraId,
85367
85514
  ...capture,
@@ -85369,9 +85516,84 @@ var NcLinkedStillShelf = class {
85369
85516
  tookMs: took()
85370
85517
  };
85371
85518
  }
85372
- async captureStill(cameraId) {
85519
+ /** D998: one forced capture, by the rule's leave, at most once per gap per camera. */
85520
+ async wakeOne(cameraId, ctx) {
85521
+ const took = () => this.deps.now() - ctx.startedAt;
85522
+ const skipped = (reason) => ({
85523
+ cameraId,
85524
+ outcome: "excluded",
85525
+ reason,
85526
+ tookMs: took()
85527
+ });
85528
+ if (!ctx.options.wakeSleeping) return skipped("wake-skipped:policy");
85529
+ const last = this.lastWakeAt.get(cameraId);
85530
+ if (last !== void 0 && ctx.startedAt - last < NC_LINKED_WAKE_MIN_GAP_MS) return skipped("wake-skipped:recent-wake");
85531
+ this.lastWakeAt.set(cameraId, ctx.startedAt);
85532
+ this.markPending(ctx);
85533
+ this.deps.logger.info("linked battery camera woken for the sensor notification", {
85534
+ tags: { deviceId: cameraId },
85535
+ meta: {
85536
+ sensorId: ctx.sensorId,
85537
+ ruleIds: ctx.options.ruleIds ?? [],
85538
+ boundMs: NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS
85539
+ }
85540
+ });
85541
+ const capture = await this.captureStill(cameraId, true, NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS);
85542
+ return {
85543
+ cameraId,
85544
+ ...this.asWakeResult(capture),
85545
+ woken: true,
85546
+ tookMs: took()
85547
+ };
85548
+ }
85549
+ /** A camera another wake already started: never forced, re-polled within the bound. */
85550
+ async awaitWaking(cameraId, ctx) {
85551
+ const took = () => this.deps.now() - ctx.startedAt;
85552
+ this.markPending(ctx);
85553
+ const polls = Math.floor(NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS / NC_LINKED_WAKE_POLL_MS);
85554
+ for (let i = 0; i < polls; i++) {
85555
+ await new Promise((resolve) => setTimeout(resolve, NC_LINKED_WAKE_POLL_MS));
85556
+ const verdict = await this.wrapperVerdict(cameraId);
85557
+ if (verdict.reason === "waking") continue;
85558
+ if (verdict.reason !== null) return {
85559
+ cameraId,
85560
+ outcome: "excluded",
85561
+ reason: verdict.reason,
85562
+ tookMs: took()
85563
+ };
85564
+ const capture = await this.captureStill(cameraId, false, SENSOR_LINKED_CAPTURE_TIMEOUT_MS);
85565
+ return {
85566
+ cameraId,
85567
+ ...this.asWakeResult(capture),
85568
+ woken: true,
85569
+ tookMs: took()
85570
+ };
85571
+ }
85572
+ return {
85573
+ cameraId,
85574
+ outcome: "wake-timeout",
85575
+ woken: true,
85576
+ tookMs: took()
85577
+ };
85578
+ }
85579
+ markPending(ctx) {
85580
+ const until = ctx.startedAt + NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS + NC_LINKED_WAKE_POLL_MS;
85581
+ this.pendingWakes.set(ctx.setId, Math.max(this.pendingWakes.get(ctx.setId) ?? 0, until));
85582
+ }
85583
+ /** A wake that brought nothing back names itself, not the generic `empty`. */
85584
+ asWakeResult(capture) {
85585
+ if (capture.outcome === "timeout") return {
85586
+ ...capture,
85587
+ outcome: "wake-timeout"
85588
+ };
85589
+ return capture.outcome === "empty" || capture.outcome === "error" && capture.reason !== "undecodable" ? {
85590
+ ...capture,
85591
+ outcome: "wake-refused"
85592
+ } : capture;
85593
+ }
85594
+ async captureStill(cameraId, force, timeoutMs) {
85373
85595
  try {
85374
- const shot = await withTimeout$2(this.deps.getSnapshot(cameraId), SENSOR_LINKED_CAPTURE_TIMEOUT_MS, "snapshot cap");
85596
+ const shot = await withTimeout$2(force ? this.deps.getSnapshot(cameraId, { force: true }) : this.deps.getSnapshot(cameraId), timeoutMs, "snapshot cap");
85375
85597
  if (shot === null || shot.base64.length === 0) return { outcome: "empty" };
85376
85598
  const jpeg = await this.decodableStill(Buffer.from(shot.base64, "base64"));
85377
85599
  if (jpeg === null) return {
@@ -86930,6 +87152,13 @@ var NC_RECORDED_FOOTAGE_WAIT_MS = 3e4;
86930
87152
  var NC_STILL_WAIT_MS = 3e3;
86931
87153
  var NC_STILL_WAIT_INTERVAL_MS = 500;
86932
87154
  /**
87155
+ * D998 — the longest a sensor notification holds for a linked battery camera
87156
+ * that is being woken (`stillDeadline`). A `sent` row is final, so the picture
87157
+ * cannot follow the push: it is waited for here, bounded, or never attached.
87158
+ * Owners with no wake in flight keep {@link NC_STILL_WAIT_MS}.
87159
+ */
87160
+ var NC_LINKED_WAKE_WAIT_MS = 15e3;
87161
+ /**
86933
87162
  * How far a still's OWN instant may sit from the trigger and still count as
86934
87163
  * being ABOUT the trigger, for a `delivery: 'immediate'` rule.
86935
87164
  *
@@ -88146,7 +88375,15 @@ var NcDispatcher = class {
88146
88375
  const firstLook = groupTiles !== null ? null : await this.resolveAttachment(entry, canStillGainMedia(entry) ? "trigger-only" : "any");
88147
88376
  const footagePending = this.renderFootage(entry);
88148
88377
  const mosaic = groupView !== void 0 && groupTiles !== null ? await this.groupedMosaic(entry, groupView, groupTiles) : null;
88149
- const still = mosaic !== null ? mosaic.attachment : firstLook !== null || entry.payload.media === "none" ? firstLook : await this.waitForStill(entry);
88378
+ let waitedMs = 0;
88379
+ let still;
88380
+ if (mosaic !== null) still = mosaic.attachment;
88381
+ else if (firstLook !== null || entry.payload.media === "none") still = firstLook;
88382
+ else {
88383
+ const waited = await this.waitForStill(entry);
88384
+ still = waited.still;
88385
+ waitedMs = waited.waitedMs;
88386
+ }
88150
88387
  const grouped = groupView !== void 0 && groupMembers !== null ? {
88151
88388
  windowKey: groupView.key,
88152
88389
  members: groupView.members.length,
@@ -88171,6 +88408,7 @@ var NcDispatcher = class {
88171
88408
  recordKind: entry.recordKind,
88172
88409
  recordId: entry.recordId,
88173
88410
  policy: entry.payload.media,
88411
+ waitedMs,
88174
88412
  ...entry.payload.mediaImage !== void 0 ? { image: entry.payload.mediaImage } : {},
88175
88413
  ...memberTrackIds.length > 1 ? { memberTrackIds } : {},
88176
88414
  owners: [...entry.payload.subject.eventId !== void 0 ? [`object:${entry.payload.subject.eventId}`] : [], ...entry.payload.subject.trackId !== void 0 ? [`track:${entry.payload.subject.trackId}`] : []]
@@ -88197,7 +88435,13 @@ var NcDispatcher = class {
88197
88435
  const subject = entry.payload.subject;
88198
88436
  const out = [];
88199
88437
  for (const { ownerId, cameraId } of subject.extraStills ?? []) {
88200
- const file = await this.linkedStillFile(entry, ownerId, cameraId);
88438
+ let file = await this.linkedStillFile(entry, ownerId, cameraId);
88439
+ for (let polls = 0; file === void 0 && polls < NC_LINKED_WAKE_WAIT_MS / NC_STILL_WAIT_INTERVAL_MS; polls += 1) {
88440
+ const deadline = this.deps.stillDeadline?.(ownerId);
88441
+ if (deadline === void 0 || this.now() >= deadline) break;
88442
+ await this.sleep(NC_STILL_WAIT_INTERVAL_MS);
88443
+ file = await this.linkedStillFile(entry, ownerId, cameraId);
88444
+ }
88201
88445
  const bytes = file === void 0 ? null : decodeStillBytes(file.base64);
88202
88446
  if (file === void 0 || bytes === null) {
88203
88447
  this.deps.logger.info("linked camera picture missing at send — skipped", {
@@ -88684,11 +88928,19 @@ var NcDispatcher = class {
88684
88928
  */
88685
88929
  async waitForStill(entry) {
88686
88930
  const subject = entry.payload.subject;
88687
- if (this.now() - subject.timestamp > NC_STILL_WAIT_MAX_AGE_MS) return this.resolveAttachment(entry, "any");
88688
- if (subject.eventId === void 0 && subject.trackId === void 0) return null;
88689
- const attempts = Math.floor(NC_STILL_WAIT_MS / NC_STILL_WAIT_INTERVAL_MS);
88690
- for (let i = 0; i < attempts; i += 1) {
88931
+ if (this.now() - subject.timestamp > NC_STILL_WAIT_MAX_AGE_MS) return {
88932
+ still: await this.resolveAttachment(entry, "any"),
88933
+ waitedMs: 0
88934
+ };
88935
+ if (subject.eventId === void 0 && subject.trackId === void 0) return {
88936
+ still: null,
88937
+ waitedMs: 0
88938
+ };
88939
+ const startedAt = this.now();
88940
+ let waitedMs = 0;
88941
+ for (let i = 0; waitedMs < this.stillWaitBoundMs(entry, startedAt); i += 1) {
88691
88942
  await this.sleep(NC_STILL_WAIT_INTERVAL_MS);
88943
+ waitedMs += NC_STILL_WAIT_INTERVAL_MS;
88692
88944
  const still = await this.resolveAttachment(entry, "trigger-only");
88693
88945
  if (still !== null) {
88694
88946
  this.deps.logger.info("the still landed during the bounded wait", {
@@ -88700,10 +88952,32 @@ var NcDispatcher = class {
88700
88952
  kind: still.name
88701
88953
  }
88702
88954
  });
88703
- return still;
88955
+ return {
88956
+ still,
88957
+ waitedMs
88958
+ };
88704
88959
  }
88705
88960
  }
88706
- return this.resolveAttachment(entry, "any");
88961
+ return {
88962
+ still: await this.resolveAttachment(entry, "any"),
88963
+ waitedMs
88964
+ };
88965
+ }
88966
+ /**
88967
+ * How long this row's still poll may run: {@link NC_STILL_WAIT_MS}, or — while
88968
+ * the shelf says a linked camera of this row is being woken — until that
88969
+ * wake gives up, at most {@link NC_LINKED_WAKE_WAIT_MS} (D998).
88970
+ */
88971
+ stillWaitBoundMs(entry, startedAt) {
88972
+ const subject = entry.payload.subject;
88973
+ const owners = [...subject.trackId !== void 0 ? [subject.trackId] : [], ...(subject.extraStills ?? []).map((extra) => extra.ownerId)];
88974
+ let deadline;
88975
+ for (const ownerId of owners) {
88976
+ const until = this.deps.stillDeadline?.(ownerId);
88977
+ if (until !== void 0 && (deadline === void 0 || until > deadline)) deadline = until;
88978
+ }
88979
+ if (deadline === void 0) return NC_STILL_WAIT_MS;
88980
+ return Math.max(NC_STILL_WAIT_MS, Math.min(NC_LINKED_WAKE_WAIT_MS, deadline - startedAt));
88707
88981
  }
88708
88982
  /**
88709
88983
  * The grouped MOSAIC (D692 §6.2): one tile per member, at most
@@ -88851,8 +89125,8 @@ var NcDispatcher = class {
88851
89125
  return ts !== void 0 && this.now() - ts <= NC_STILL_WAIT_MAX_AGE_MS;
88852
89126
  });
88853
89127
  await look("trigger-only", pollable());
88854
- const attempts = Math.floor(NC_STILL_WAIT_MS / NC_STILL_WAIT_INTERVAL_MS);
88855
- for (let i = 0; i < attempts && pollable().length > 0; i += 1) {
89128
+ const startedAt = this.now();
89129
+ for (let waited = 0; waited < this.stillWaitBoundMs(entry, startedAt) && pollable().length > 0; waited += NC_STILL_WAIT_INTERVAL_MS) {
88856
89130
  await this.sleep(NC_STILL_WAIT_INTERVAL_MS);
88857
89131
  await look("trigger-only", pollable());
88858
89132
  }
@@ -104372,7 +104646,7 @@ function beginLinkedStills(host, request) {
104372
104646
  if (rowCameraIds.length > 0 && rowShown.length === 0) return void 0;
104373
104647
  const shown = host.unmutedCameras(cameraIds);
104374
104648
  const involved = [sensorId, ...rowShown];
104375
- if (!host.rules.listEnabled("device-event").some((rule) => {
104649
+ const wanting = host.rules.listEnabled("device-event").filter((rule) => {
104376
104650
  if (ruleShipsNoStill(rule)) return false;
104377
104651
  if (!deviceEventRuleMayFireOnKind(rule, sensorKind, sensorRaised)) return false;
104378
104652
  const excluded = rule.conditions.devicesExclude;
@@ -104383,9 +104657,14 @@ function beginLinkedStills(host, request) {
104383
104657
  })) return false;
104384
104658
  const devices = rule.conditions.devices;
104385
104659
  return devices === void 0 || devices.length === 0 || devices.some((id) => involved.includes(id));
104386
- })) return void 0;
104660
+ });
104661
+ if (wanting.length === 0) return void 0;
104662
+ const wakeSleeping = wanting.some((rule) => rule.media?.wakeSleepingCamera !== false);
104387
104663
  for (const cameraId of cameraIds) if (!shown.includes(cameraId)) host.reportLinkedCameraMuted(cameraId, sensorId);
104388
- return shown.length > 0 ? shelf.captureSet(sensorId, shown, atMs) : void 0;
104664
+ return shown.length > 0 ? shelf.captureSet(sensorId, shown, atMs, {
104665
+ wakeSleeping,
104666
+ ruleIds: wanting.map((rule) => rule.id)
104667
+ }) : void 0;
104389
104668
  }
104390
104669
  /**
104391
104670
  * A muted camera left out of a linked set — one INFO line per camera per
@@ -107108,6 +107387,7 @@ var NotificationCenter = class {
107108
107387
  ...deps.dispatcher,
107109
107388
  logger: this.logger.child("dispatch"),
107110
107389
  texts: this.texts,
107390
+ stillDeadline: (ownerId) => isLinkedStillId(ownerId) ? this.linkedStills?.pendingUntil(ownerId) : void 0,
107111
107391
  getMediaForOwner: async (ownerKind, ownerId, eventScope) => {
107112
107392
  const held = isSyntheticId(ownerId) ? this.syntheticMedia.get(ownerId) : void 0;
107113
107393
  if (held !== void 0) return held.files;
@@ -109691,7 +109971,10 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
109691
109971
  },
109692
109972
  readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
109693
109973
  emitBusEvent: (event) => this.ctx.eventBus.emit(event),
109694
- getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
109974
+ getSnapshot: (deviceId, opts) => api.snapshot.getSnapshot.query(opts?.force === true ? {
109975
+ deviceId,
109976
+ force: true
109977
+ } : { deviceId }),
109695
109978
  recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
109696
109979
  className,
109697
109980
  ...labelKind !== void 0 ? { labelKind } : {},
@@ -128056,11 +128339,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128056
128339
  const lookFrame = this.frameState.packageLookFrames.get(input.deviceId);
128057
128340
  promoteWithStill({
128058
128341
  entry,
128059
- handle: lookFrame !== void 0 && input.timestamp - lookFrame.at <= PACKAGE_LOOK_FRAME_MAX_AGE_MS ? lookFrame.handle : null,
128342
+ handle: lookFrame !== void 0 && input.timestamp - lookFrame.at <= PACKAGE_NATIVE_HANDLE_MAX_AGE_MS ? lookFrame.handle : null,
128060
128343
  timestamp: lookFrame?.at ?? input.timestamp,
128061
128344
  query: (request) => this.queryPackageSceneCrop(request),
128062
128345
  writer: this.mediaStore,
128063
128346
  logger: this.ctx.logger,
128347
+ live: async () => {
128348
+ const snap = await this.ctx.api.snapshot.getSnapshot.query({ deviceId: input.deviceId });
128349
+ return snap === null ? null : Uint8Array.from(Buffer.from(snap.base64, "base64"));
128350
+ },
128064
128351
  detector: this.packageDropDetector
128065
128352
  }).catch((err) => {
128066
128353
  this.ctx.logger.warn("package promotion failed", {