@camstack/addon-post-analysis 1.2.228 → 1.2.229

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.
@@ -35511,6 +35511,71 @@ function pairRiders(detections) {
35511
35511
  }
35512
35512
  return pairs;
35513
35513
  }
35514
+ //#endregion
35515
+ //#region src/pipeline-analytics/pipeline/track-id.ts
35516
+ /**
35517
+ * A track's identity: short, random, and mintable inside a frame.
35518
+ *
35519
+ * ## Why not a counter, and why not the database
35520
+ *
35521
+ * The operator asked for an incremental id. It cannot be a database
35522
+ * auto-increment: the id is minted by the tracker MID-FRAME and is used
35523
+ * immediately — in the event, in the media key, as the `object-clip` vector's
35524
+ * row id — long before anything is written, so there is no rowid to wait for
35525
+ * and no await to spend on the hot path (D49: a one-shot never gates on a
35526
+ * fallible read).
35527
+ *
35528
+ * A local counter was already tried and reverted. `sort-tracker.ts` still
35529
+ * records what it did: "A bare per-process counter (`track-${n++}`) reset to 1
35530
+ * on every runner restart and collided across restarts AND devices — a
35531
+ * post-reboot track reused an old id and UPSERT-clobbered the old persisted row
35532
+ * (wrong 'now' dates, stale bestEventId thumbnails, cross-session media in the
35533
+ * detail view)."
35534
+ *
35535
+ * A durable counter is possible — the hub hands each node a BLOCK of ids ahead
35536
+ * of need, and the bookmark advances before they are used, so a crash leaves a
35537
+ * gap and never a duplicate. It was designed and NOT built: agents hold no
35538
+ * sqlite of their own (the hub is the only store), so the whole mechanism is a
35539
+ * cluster allocator with a degrade path, for a property nobody reads. The
35540
+ * operator chose the smaller thing.
35541
+ *
35542
+ * So: still random, and only shorter. 36 characters become 10.
35543
+ *
35544
+ * ## Is 10 enough
35545
+ *
35546
+ * Arithmetic, not optimism. The alphabet is 32 characters, so an id is 50 bits
35547
+ * and the space is 32^10 ≈ 1.13e15. The live table held 17 870 track rows under
35548
+ * a 7-day window (measured, `piano-riduzione-database`), so call the population
35549
+ * 20 000 ids that must not collide with each other. Birthday:
35550
+ *
35551
+ * p ≈ n² / 2N = 4e8 / 2.25e15 ≈ 1.8e-7 per retention window
35552
+ *
35553
+ * ~1e-5 per year at the current fleet. A collision is not cosmetic — two tracks
35554
+ * sharing an id UPSERT over each other, which is precisely the old counter's
35555
+ * failure — so the margin is deliberately three orders of magnitude wider than
35556
+ * "probably fine", and it is the reason this is 10 characters and not 6.
35557
+ *
35558
+ * ## The alphabet
35559
+ *
35560
+ * Crockford-style base32, lower case: the digits plus the letters, minus
35561
+ * `i`, `l`, `o` and `u`. The first three are misread as `1`/`1`/`0` by a human
35562
+ * copying an id out of a log; `u` goes with them so the set is exactly 32,
35563
+ * which is what lets five raw bits map to one character with NO modulo bias.
35564
+ * A `% 36` over a byte would make the first 16 characters ~1.5× more likely and
35565
+ * silently shrink the space the arithmetic above depends on.
35566
+ */
35567
+ /** 32 characters: `0-9a-z` minus `i`, `l`, `o`, `u`. A power of two, on purpose. */
35568
+ var TRACK_ID_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
35569
+ /**
35570
+ * A fresh track id. Synchronous, no I/O, no shared state — safe to call from
35571
+ * inside the tracker's per-frame loop, which is the only place that calls it.
35572
+ */
35573
+ function newTrackId() {
35574
+ const bytes = (0, node_crypto.randomBytes)(10);
35575
+ let out = "";
35576
+ for (let i = 0; i < 10; i += 1) out += TRACK_ID_ALPHABET[(bytes[i] ?? 0) & 31];
35577
+ return out;
35578
+ }
35514
35579
  /** Per-(stage, gate) counter + sampling decision. One instance per tracker, so
35515
35580
  * the counts are per camera and per detection source. */
35516
35581
  var LooseBindSampler = class {
@@ -36789,7 +36854,7 @@ var SortTracker = class SortTracker {
36789
36854
  used.add(di);
36790
36855
  continue;
36791
36856
  }
36792
- const spawnedId = (0, node_crypto.randomUUID)();
36857
+ const spawnedId = newTrackId();
36793
36858
  this.reportSpawnBesideCoasting(spawnedId, det, surviving.filter((t) => t.lastSeen !== timestamp), detections, frameArea, timestamp);
36794
36859
  surviving.push({
36795
36860
  id: spawnedId,
@@ -60826,6 +60891,95 @@ async function reconcileGallerySamples(deps, options) {
60826
60891
  };
60827
60892
  }
60828
60893
  //#endregion
60894
+ //#region src/pipeline-analytics/summary/summary-membership.ts
60895
+ /**
60896
+ * Who belongs to a summary, and which member is its primary.
60897
+ *
60898
+ * ## What this corrects
60899
+ *
60900
+ * `openOrAbsorb` absorbed ANY track born while a summary was open on the
60901
+ * camera, and `primary` went to whoever opened it. Both were measured wrong on
60902
+ * the live hub on 2026-09-13: synthetic sensor tracks (`pa-synth-…`) sat inside
60903
+ * summaries describing "who was here together", and a dog that walked in ahead
60904
+ * of its owner outranked the person behind it.
60905
+ *
60906
+ * The operator's rule: "persona animale veicolo la priorità. Gli eventi extra
60907
+ * sono sempre separati come eventi sintetici di sensori e pacchi."
60908
+ *
60909
+ * ## Why exclusion and priority live together
60910
+ *
60911
+ * They are the same question asked twice — "what is a subject in a group" —
60912
+ * and answering it in two files is how the two drift. A class that stops being
60913
+ * a member must not keep a rank, and a class that gains a rank must be a
60914
+ * member.
60915
+ *
60916
+ * Pure, so the surprising cases are pinned rather than discovered: the dog that
60917
+ * arrives first, the tie between two people, and the member whose class is not
60918
+ * known yet.
60919
+ */
60920
+ /** The synthetic marker. `synthetic-track.ts` mints these and nothing parses it
60921
+ * except this — which is the one place it must be parsed. */
60922
+ var SYNTHETIC_TRACK_PREFIX = "pa-synth-";
60923
+ /**
60924
+ * Classes the operator keeps OUT of a group, by name.
60925
+ *
60926
+ * A delivered package is an event about a PLACE, not a member of something that
60927
+ * arrived together — the operator named it alongside the sensors.
60928
+ */
60929
+ var NON_MEMBER_CLASSES = new Set(["package"]);
60930
+ /**
60931
+ * Is this track a member of a group at all?
60932
+ *
60933
+ * An unknown class is NOT a reason to refuse. The grouping is the point and the
60934
+ * label is decoration, and nothing revisits this decision — a member dropped
60935
+ * here is dropped for the life of the summary.
60936
+ */
60937
+ function isSummaryEligible(trackId, className) {
60938
+ if (trackId.startsWith(SYNTHETIC_TRACK_PREFIX)) return false;
60939
+ return className === null || className === void 0 || !NON_MEMBER_CLASSES.has(className);
60940
+ }
60941
+ /**
60942
+ * The operator's order: person › animal › vehicle › everything else.
60943
+ *
60944
+ * Higher is more important. Everything unnamed — including a class that has not
60945
+ * resolved yet — sits below all three rather than above them: promoting an
60946
+ * unknown would let a momentary mis-read take the group's identity, and the
60947
+ * group shot with it.
60948
+ */
60949
+ var CLASS_RANK = new Map([
60950
+ ["person", 3],
60951
+ ["animal", 2],
60952
+ ["vehicle", 1]
60953
+ ]);
60954
+ function classPriority(className) {
60955
+ if (className === null || className === void 0) return 0;
60956
+ return CLASS_RANK.get(className) ?? 0;
60957
+ }
60958
+ /**
60959
+ * Which member is the primary: highest class rank, ties broken by EARLIEST
60960
+ * arrival.
60961
+ *
60962
+ * The tie-break matters more than it looks. Two people are equal, so without it
60963
+ * the primary would flip on every join between equals — and the primary is what
60964
+ * a consumer reads to name the group, so it would rename itself for no reason
60965
+ * an operator could see.
60966
+ *
60967
+ * `null` for no members, never a fabricated id.
60968
+ */
60969
+ function primaryTrackId(members) {
60970
+ let best = null;
60971
+ for (const m of members) {
60972
+ if (best === null) {
60973
+ best = m;
60974
+ continue;
60975
+ }
60976
+ const rank = classPriority(m.className);
60977
+ const bestRank = classPriority(best.className);
60978
+ if (rank > bestRank || rank === bestRank && m.joinedAt < best.joinedAt) best = m;
60979
+ }
60980
+ return best?.trackId ?? null;
60981
+ }
60982
+ //#endregion
60829
60983
  //#region src/pipeline-analytics/summary/summary-store.ts
60830
60984
  /**
60831
60985
  * @durable class=ledger owner=pipeline-analytics
@@ -61090,7 +61244,18 @@ var SummaryStore = class {
61090
61244
  async absorb(summary, input) {
61091
61245
  const members = await this.membersOf(summary.id);
61092
61246
  if (members.some((m) => m.trackId === input.trackId)) return summary;
61093
- await this.addMember(summary.id, input, "sibling");
61247
+ const joined = {
61248
+ trackId: input.trackId,
61249
+ className: input.className ?? null,
61250
+ joinedAt: input.at
61251
+ };
61252
+ const primary = primaryTrackId([...members.map((m) => ({
61253
+ trackId: m.trackId,
61254
+ className: m.className,
61255
+ joinedAt: m.joinedAt
61256
+ })), joined]);
61257
+ await this.addMember(summary.id, input, primary === input.trackId ? "primary" : "sibling");
61258
+ if (primary === input.trackId) await this.demoteOtherPrimaries(summary.id, input.trackId, members);
61094
61259
  const memberCount = members.length + 1;
61095
61260
  const lastActiveAt = Math.max(summary.lastActiveAt, input.at);
61096
61261
  await this.store.updateWhere.mutate({
@@ -61110,6 +61275,31 @@ var SummaryStore = class {
61110
61275
  lastActiveAt
61111
61276
  };
61112
61277
  }
61278
+ /**
61279
+ * Put every OTHER member back to `sibling` after a new primary took the role.
61280
+ *
61281
+ * Best-effort per row and loud about a row it could not move: two primaries
61282
+ * is a wrong a consumer can see, but it is smaller than a lost member, so a
61283
+ * failure here never propagates to the join.
61284
+ */
61285
+ async demoteOtherPrimaries(summaryIdValue, primaryTrack, previous) {
61286
+ for (const m of previous) {
61287
+ if (m.trackId === primaryTrack || m.role !== "primary") continue;
61288
+ try {
61289
+ await this.store.update.mutate({
61290
+ collection: SUMMARY_MEMBERS_COLLECTION,
61291
+ id: memberRowId(summaryIdValue, m.trackId),
61292
+ data: { role: "sibling" }
61293
+ });
61294
+ } catch (err) {
61295
+ this.logger.warn("summary: could not demote the previous primary — two remain", { meta: {
61296
+ summaryId: summaryIdValue,
61297
+ trackId: m.trackId,
61298
+ error: String(err)
61299
+ } });
61300
+ }
61301
+ }
61302
+ }
61113
61303
  async addMember(summaryIdValue, input, role) {
61114
61304
  await this.store.insertMany.mutate({
61115
61305
  collection: SUMMARY_MEMBERS_COLLECTION,
@@ -68454,7 +68644,7 @@ var SyntheticTrackMaterializer = class {
68454
68644
  this.deps = deps;
68455
68645
  this.debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
68456
68646
  this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS;
68457
- this.makeId = deps.makeId ?? (() => `pa-synth-${(0, node_crypto.randomUUID)()}`);
68647
+ this.makeId = deps.makeId ?? (() => `pa-synth-${newTrackId()}`);
68458
68648
  this.now = deps.now ?? Date.now;
68459
68649
  }
68460
68650
  /**
@@ -71436,6 +71626,16 @@ var SummarySealer = class {
71436
71626
  }
71437
71627
  async onTrackOpened(input) {
71438
71628
  try {
71629
+ if (!isSummaryEligible(input.trackId, input.className)) {
71630
+ this.deps.logger.debug("summary: track is not a group member — left standalone", {
71631
+ tags: { deviceId: input.deviceId },
71632
+ meta: {
71633
+ trackId: input.trackId,
71634
+ className: input.className ?? null
71635
+ }
71636
+ });
71637
+ return;
71638
+ }
71439
71639
  const row = await this.deps.summaries.openOrAbsorb({
71440
71640
  deviceId: input.deviceId,
71441
71641
  trackId: input.trackId,
@@ -4,7 +4,7 @@ import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import path from "node:path";
7
- import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
7
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
8
8
  import { execFile } from "node:child_process";
9
9
  import { startEventLoopStallMonitor } from "@camstack/system/addon-utils";
10
10
  import sharp from "sharp";
@@ -35486,6 +35486,71 @@ function pairRiders(detections) {
35486
35486
  }
35487
35487
  return pairs;
35488
35488
  }
35489
+ //#endregion
35490
+ //#region src/pipeline-analytics/pipeline/track-id.ts
35491
+ /**
35492
+ * A track's identity: short, random, and mintable inside a frame.
35493
+ *
35494
+ * ## Why not a counter, and why not the database
35495
+ *
35496
+ * The operator asked for an incremental id. It cannot be a database
35497
+ * auto-increment: the id is minted by the tracker MID-FRAME and is used
35498
+ * immediately — in the event, in the media key, as the `object-clip` vector's
35499
+ * row id — long before anything is written, so there is no rowid to wait for
35500
+ * and no await to spend on the hot path (D49: a one-shot never gates on a
35501
+ * fallible read).
35502
+ *
35503
+ * A local counter was already tried and reverted. `sort-tracker.ts` still
35504
+ * records what it did: "A bare per-process counter (`track-${n++}`) reset to 1
35505
+ * on every runner restart and collided across restarts AND devices — a
35506
+ * post-reboot track reused an old id and UPSERT-clobbered the old persisted row
35507
+ * (wrong 'now' dates, stale bestEventId thumbnails, cross-session media in the
35508
+ * detail view)."
35509
+ *
35510
+ * A durable counter is possible — the hub hands each node a BLOCK of ids ahead
35511
+ * of need, and the bookmark advances before they are used, so a crash leaves a
35512
+ * gap and never a duplicate. It was designed and NOT built: agents hold no
35513
+ * sqlite of their own (the hub is the only store), so the whole mechanism is a
35514
+ * cluster allocator with a degrade path, for a property nobody reads. The
35515
+ * operator chose the smaller thing.
35516
+ *
35517
+ * So: still random, and only shorter. 36 characters become 10.
35518
+ *
35519
+ * ## Is 10 enough
35520
+ *
35521
+ * Arithmetic, not optimism. The alphabet is 32 characters, so an id is 50 bits
35522
+ * and the space is 32^10 ≈ 1.13e15. The live table held 17 870 track rows under
35523
+ * a 7-day window (measured, `piano-riduzione-database`), so call the population
35524
+ * 20 000 ids that must not collide with each other. Birthday:
35525
+ *
35526
+ * p ≈ n² / 2N = 4e8 / 2.25e15 ≈ 1.8e-7 per retention window
35527
+ *
35528
+ * ~1e-5 per year at the current fleet. A collision is not cosmetic — two tracks
35529
+ * sharing an id UPSERT over each other, which is precisely the old counter's
35530
+ * failure — so the margin is deliberately three orders of magnitude wider than
35531
+ * "probably fine", and it is the reason this is 10 characters and not 6.
35532
+ *
35533
+ * ## The alphabet
35534
+ *
35535
+ * Crockford-style base32, lower case: the digits plus the letters, minus
35536
+ * `i`, `l`, `o` and `u`. The first three are misread as `1`/`1`/`0` by a human
35537
+ * copying an id out of a log; `u` goes with them so the set is exactly 32,
35538
+ * which is what lets five raw bits map to one character with NO modulo bias.
35539
+ * A `% 36` over a byte would make the first 16 characters ~1.5× more likely and
35540
+ * silently shrink the space the arithmetic above depends on.
35541
+ */
35542
+ /** 32 characters: `0-9a-z` minus `i`, `l`, `o`, `u`. A power of two, on purpose. */
35543
+ var TRACK_ID_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
35544
+ /**
35545
+ * A fresh track id. Synchronous, no I/O, no shared state — safe to call from
35546
+ * inside the tracker's per-frame loop, which is the only place that calls it.
35547
+ */
35548
+ function newTrackId() {
35549
+ const bytes = randomBytes(10);
35550
+ let out = "";
35551
+ for (let i = 0; i < 10; i += 1) out += TRACK_ID_ALPHABET[(bytes[i] ?? 0) & 31];
35552
+ return out;
35553
+ }
35489
35554
  /** Per-(stage, gate) counter + sampling decision. One instance per tracker, so
35490
35555
  * the counts are per camera and per detection source. */
35491
35556
  var LooseBindSampler = class {
@@ -36764,7 +36829,7 @@ var SortTracker = class SortTracker {
36764
36829
  used.add(di);
36765
36830
  continue;
36766
36831
  }
36767
- const spawnedId = randomUUID();
36832
+ const spawnedId = newTrackId();
36768
36833
  this.reportSpawnBesideCoasting(spawnedId, det, surviving.filter((t) => t.lastSeen !== timestamp), detections, frameArea, timestamp);
36769
36834
  surviving.push({
36770
36835
  id: spawnedId,
@@ -60752,6 +60817,95 @@ async function reconcileGallerySamples(deps, options) {
60752
60817
  };
60753
60818
  }
60754
60819
  //#endregion
60820
+ //#region src/pipeline-analytics/summary/summary-membership.ts
60821
+ /**
60822
+ * Who belongs to a summary, and which member is its primary.
60823
+ *
60824
+ * ## What this corrects
60825
+ *
60826
+ * `openOrAbsorb` absorbed ANY track born while a summary was open on the
60827
+ * camera, and `primary` went to whoever opened it. Both were measured wrong on
60828
+ * the live hub on 2026-09-13: synthetic sensor tracks (`pa-synth-…`) sat inside
60829
+ * summaries describing "who was here together", and a dog that walked in ahead
60830
+ * of its owner outranked the person behind it.
60831
+ *
60832
+ * The operator's rule: "persona animale veicolo la priorità. Gli eventi extra
60833
+ * sono sempre separati come eventi sintetici di sensori e pacchi."
60834
+ *
60835
+ * ## Why exclusion and priority live together
60836
+ *
60837
+ * They are the same question asked twice — "what is a subject in a group" —
60838
+ * and answering it in two files is how the two drift. A class that stops being
60839
+ * a member must not keep a rank, and a class that gains a rank must be a
60840
+ * member.
60841
+ *
60842
+ * Pure, so the surprising cases are pinned rather than discovered: the dog that
60843
+ * arrives first, the tie between two people, and the member whose class is not
60844
+ * known yet.
60845
+ */
60846
+ /** The synthetic marker. `synthetic-track.ts` mints these and nothing parses it
60847
+ * except this — which is the one place it must be parsed. */
60848
+ var SYNTHETIC_TRACK_PREFIX = "pa-synth-";
60849
+ /**
60850
+ * Classes the operator keeps OUT of a group, by name.
60851
+ *
60852
+ * A delivered package is an event about a PLACE, not a member of something that
60853
+ * arrived together — the operator named it alongside the sensors.
60854
+ */
60855
+ var NON_MEMBER_CLASSES = new Set(["package"]);
60856
+ /**
60857
+ * Is this track a member of a group at all?
60858
+ *
60859
+ * An unknown class is NOT a reason to refuse. The grouping is the point and the
60860
+ * label is decoration, and nothing revisits this decision — a member dropped
60861
+ * here is dropped for the life of the summary.
60862
+ */
60863
+ function isSummaryEligible(trackId, className) {
60864
+ if (trackId.startsWith(SYNTHETIC_TRACK_PREFIX)) return false;
60865
+ return className === null || className === void 0 || !NON_MEMBER_CLASSES.has(className);
60866
+ }
60867
+ /**
60868
+ * The operator's order: person › animal › vehicle › everything else.
60869
+ *
60870
+ * Higher is more important. Everything unnamed — including a class that has not
60871
+ * resolved yet — sits below all three rather than above them: promoting an
60872
+ * unknown would let a momentary mis-read take the group's identity, and the
60873
+ * group shot with it.
60874
+ */
60875
+ var CLASS_RANK = new Map([
60876
+ ["person", 3],
60877
+ ["animal", 2],
60878
+ ["vehicle", 1]
60879
+ ]);
60880
+ function classPriority(className) {
60881
+ if (className === null || className === void 0) return 0;
60882
+ return CLASS_RANK.get(className) ?? 0;
60883
+ }
60884
+ /**
60885
+ * Which member is the primary: highest class rank, ties broken by EARLIEST
60886
+ * arrival.
60887
+ *
60888
+ * The tie-break matters more than it looks. Two people are equal, so without it
60889
+ * the primary would flip on every join between equals — and the primary is what
60890
+ * a consumer reads to name the group, so it would rename itself for no reason
60891
+ * an operator could see.
60892
+ *
60893
+ * `null` for no members, never a fabricated id.
60894
+ */
60895
+ function primaryTrackId(members) {
60896
+ let best = null;
60897
+ for (const m of members) {
60898
+ if (best === null) {
60899
+ best = m;
60900
+ continue;
60901
+ }
60902
+ const rank = classPriority(m.className);
60903
+ const bestRank = classPriority(best.className);
60904
+ if (rank > bestRank || rank === bestRank && m.joinedAt < best.joinedAt) best = m;
60905
+ }
60906
+ return best?.trackId ?? null;
60907
+ }
60908
+ //#endregion
60755
60909
  //#region src/pipeline-analytics/summary/summary-store.ts
60756
60910
  /**
60757
60911
  * @durable class=ledger owner=pipeline-analytics
@@ -61016,7 +61170,18 @@ var SummaryStore = class {
61016
61170
  async absorb(summary, input) {
61017
61171
  const members = await this.membersOf(summary.id);
61018
61172
  if (members.some((m) => m.trackId === input.trackId)) return summary;
61019
- await this.addMember(summary.id, input, "sibling");
61173
+ const joined = {
61174
+ trackId: input.trackId,
61175
+ className: input.className ?? null,
61176
+ joinedAt: input.at
61177
+ };
61178
+ const primary = primaryTrackId([...members.map((m) => ({
61179
+ trackId: m.trackId,
61180
+ className: m.className,
61181
+ joinedAt: m.joinedAt
61182
+ })), joined]);
61183
+ await this.addMember(summary.id, input, primary === input.trackId ? "primary" : "sibling");
61184
+ if (primary === input.trackId) await this.demoteOtherPrimaries(summary.id, input.trackId, members);
61020
61185
  const memberCount = members.length + 1;
61021
61186
  const lastActiveAt = Math.max(summary.lastActiveAt, input.at);
61022
61187
  await this.store.updateWhere.mutate({
@@ -61036,6 +61201,31 @@ var SummaryStore = class {
61036
61201
  lastActiveAt
61037
61202
  };
61038
61203
  }
61204
+ /**
61205
+ * Put every OTHER member back to `sibling` after a new primary took the role.
61206
+ *
61207
+ * Best-effort per row and loud about a row it could not move: two primaries
61208
+ * is a wrong a consumer can see, but it is smaller than a lost member, so a
61209
+ * failure here never propagates to the join.
61210
+ */
61211
+ async demoteOtherPrimaries(summaryIdValue, primaryTrack, previous) {
61212
+ for (const m of previous) {
61213
+ if (m.trackId === primaryTrack || m.role !== "primary") continue;
61214
+ try {
61215
+ await this.store.update.mutate({
61216
+ collection: SUMMARY_MEMBERS_COLLECTION,
61217
+ id: memberRowId(summaryIdValue, m.trackId),
61218
+ data: { role: "sibling" }
61219
+ });
61220
+ } catch (err) {
61221
+ this.logger.warn("summary: could not demote the previous primary — two remain", { meta: {
61222
+ summaryId: summaryIdValue,
61223
+ trackId: m.trackId,
61224
+ error: String(err)
61225
+ } });
61226
+ }
61227
+ }
61228
+ }
61039
61229
  async addMember(summaryIdValue, input, role) {
61040
61230
  await this.store.insertMany.mutate({
61041
61231
  collection: SUMMARY_MEMBERS_COLLECTION,
@@ -68380,7 +68570,7 @@ var SyntheticTrackMaterializer = class {
68380
68570
  this.deps = deps;
68381
68571
  this.debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
68382
68572
  this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS;
68383
- this.makeId = deps.makeId ?? (() => `pa-synth-${randomUUID()}`);
68573
+ this.makeId = deps.makeId ?? (() => `pa-synth-${newTrackId()}`);
68384
68574
  this.now = deps.now ?? Date.now;
68385
68575
  }
68386
68576
  /**
@@ -71362,6 +71552,16 @@ var SummarySealer = class {
71362
71552
  }
71363
71553
  async onTrackOpened(input) {
71364
71554
  try {
71555
+ if (!isSummaryEligible(input.trackId, input.className)) {
71556
+ this.deps.logger.debug("summary: track is not a group member — left standalone", {
71557
+ tags: { deviceId: input.deviceId },
71558
+ meta: {
71559
+ trackId: input.trackId,
71560
+ className: input.className ?? null
71561
+ }
71562
+ });
71563
+ return;
71564
+ }
71365
71565
  const row = await this.deps.summaries.openOrAbsorb({
71366
71566
  deviceId: input.deviceId,
71367
71567
  trackId: input.trackId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.228",
3
+ "version": "1.2.229",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",