@camstack/addon-pipeline-orchestrator 1.2.181 → 1.2.182

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.mjs CHANGED
@@ -17069,6 +17069,7 @@ var NcSystemEventKindSchema = _enum([
17069
17069
  "addon-crash-loop",
17070
17070
  "addon-update-available",
17071
17071
  "server-update-available",
17072
+ "wrapper-update-available",
17072
17073
  "alarm-triggered",
17073
17074
  "alarm-armed",
17074
17075
  "alarm-disarmed",
@@ -23327,6 +23328,53 @@ var ImageContractSchema = object({
23327
23328
  /** One operator-grade sentence: this node runs image X; the contract says Y. */
23328
23329
  message: string()
23329
23330
  });
23331
+ /**
23332
+ * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
23333
+ * produced it.
23334
+ *
23335
+ * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
23336
+ * update is an ANNOUNCEMENT. The notification body carries the exact command
23337
+ * for a `docker` shell and says the app restarts into the new build for
23338
+ * `electron`, and that sentence is chosen from `kind` alone — a boolean
23339
+ * nothing reads is the defect this repo keeps paying for.
23340
+ */
23341
+ var WrapperIdentitySchema = object({
23342
+ kind: _enum([
23343
+ "docker",
23344
+ "electron",
23345
+ "native",
23346
+ "contradictory",
23347
+ "unknown"
23348
+ ]),
23349
+ /** Every probe that ran, in a stable order. Never empty. */
23350
+ evidence: array(object({
23351
+ /** Which probe this row reports. */
23352
+ fact: _enum([
23353
+ "dockerenv-file",
23354
+ "proc-1-cgroup",
23355
+ "platform",
23356
+ "electron-app-version-env"
23357
+ ]),
23358
+ /**
23359
+ * `observed` — this process read it itself. `declared` — the shell told the
23360
+ * process and nothing here can check it. Only the Electron app version is
23361
+ * ever `declared`, and only because a child cannot see its parent.
23362
+ */
23363
+ mode: _enum(["observed", "declared"]),
23364
+ /** Did the probe support the fact it names? A `false` row is still evidence. */
23365
+ holds: boolean(),
23366
+ /** What was actually read, verbatim enough for an operator to argue with. */
23367
+ detail: string()
23368
+ })),
23369
+ /**
23370
+ * The version of the SHELL, when the shell has one it can state: the
23371
+ * Electron app version it declared, or the baked seed closure that
23372
+ * fingerprints a container / app bundle. `null` for `contradictory` and
23373
+ * `unknown` — which version belongs to the shell is precisely what is not
23374
+ * known there.
23375
+ */
23376
+ currentVersion: string().nullable()
23377
+ });
23330
23378
  var ServerRollbackInfoSchema = object({
23331
23379
  /** The version that failed (or was manually rolled back). */
23332
23380
  fromVersion: string(),
@@ -23368,7 +23416,13 @@ var ServerPackageStatusSchema = object({
23368
23416
  * Seed-vs-contract verdict (see {@link ImageContractSchema}). Optional for
23369
23417
  * version skew: an older provider's payload simply omits it.
23370
23418
  */
23371
- imageContract: ImageContractSchema.optional()
23419
+ imageContract: ImageContractSchema.optional(),
23420
+ /**
23421
+ * What this node runs INSIDE (see {@link WrapperIdentitySchema}). Optional
23422
+ * for version skew: an older provider's payload simply omits it, and an
23423
+ * absent wrapper is unknown — never `native`.
23424
+ */
23425
+ wrapper: WrapperIdentitySchema.optional()
23372
23426
  });
23373
23427
  var ServerUpdateCheckResultSchema = object({
23374
23428
  packageName: string(),
@@ -42897,6 +42951,52 @@ var LoadShedController = class LoadShedController {
42897
42951
  }
42898
42952
  };
42899
42953
  //#endregion
42954
+ //#region src/dispatch-timing.ts
42955
+ /**
42956
+ * How long the placement census waits for ONE runner's `getLocalCameras`
42957
+ * before counting that node as not having reported. A node that did not
42958
+ * report never has its assignments dropped (`reconcilePlacement` says so), so
42959
+ * skipping a slow node costs one pass of orphan cleanup on that node, not
42960
+ * correctness. The census fans out in parallel, so this bounds the whole
42961
+ * census at one budget rather than one budget per node.
42962
+ */
42963
+ var RECONCILE_NODE_BUDGET_MS = 1500;
42964
+ /**
42965
+ * Wait for `promise`, but never longer than `budgetMs`.
42966
+ *
42967
+ * The promise is NOT cancelled — it keeps running, and its eventual rejection
42968
+ * is observed here so an overrun never becomes an unhandled rejection. The
42969
+ * caller decides what an overrun means; this only says that it happened.
42970
+ */
42971
+ function raceBudget(promise, budgetMs) {
42972
+ return new Promise((resolve) => {
42973
+ let done = false;
42974
+ const timer = setTimeout(() => {
42975
+ if (done) return;
42976
+ done = true;
42977
+ resolve({ kind: "budget-exceeded" });
42978
+ }, budgetMs);
42979
+ timer.unref?.();
42980
+ promise.then((value) => {
42981
+ if (done) return;
42982
+ done = true;
42983
+ clearTimeout(timer);
42984
+ resolve({
42985
+ kind: "settled",
42986
+ value
42987
+ });
42988
+ }, (error) => {
42989
+ if (done) return;
42990
+ done = true;
42991
+ clearTimeout(timer);
42992
+ resolve({
42993
+ kind: "rejected",
42994
+ error
42995
+ });
42996
+ });
42997
+ });
42998
+ }
42999
+ //#endregion
42900
43000
  //#region src/agent-load-service.ts
42901
43001
  var AgentLoadService = class AgentLoadService {
42902
43002
  deps;
@@ -42934,28 +43034,69 @@ var AgentLoadService = class AgentLoadService {
42934
43034
  * collects every known runner, which is what the UI wants.
42935
43035
  */
42936
43036
  async collectAgentLoad(options) {
42937
- const loads = [];
42938
43037
  const onlyEnabled = options?.onlyEnabled ?? false;
42939
- for (const nodeId of this.deps.topology.knownRunnerNodeIds()) {
42940
- if (onlyEnabled && !this.deps.topology.isNodeEnabled(nodeId)) {
42941
- this.deps.logger.debug("runner excluded by enabledNodes whitelist", { tags: { nodeId } });
42942
- continue;
42943
- }
42944
- if (!this.deps.api()) continue;
42945
- try {
42946
- const load = await this.queryLocalLoadBounded(nodeId);
42947
- loads.push(load);
42948
- } catch (err) {
42949
- const msg = errMsg(err);
42950
- this.deps.logger.debug("getLocalLoad failed", {
42951
- tags: { nodeId },
42952
- meta: { error: msg }
42953
- });
42954
- }
43038
+ const nodeIds = this.deps.topology.knownRunnerNodeIds().filter((nodeId) => {
43039
+ if (!onlyEnabled || this.deps.topology.isNodeEnabled(nodeId)) return true;
43040
+ this.deps.logger.debug("runner excluded by enabledNodes whitelist", { tags: { nodeId } });
43041
+ return false;
43042
+ });
43043
+ const budgetMs = this.agentLoadBudgetMs();
43044
+ const entries = await Promise.all(nodeIds.map((nodeId) => this.censusNode(nodeId, budgetMs)));
43045
+ const loads = entries.flatMap((e) => e.load === null ? [] : [e.load]);
43046
+ const skipped = entries.filter((e) => e.load === null).map((e) => e.nodeId);
43047
+ if (skipped.length > 0) {
43048
+ const nodes = Object.fromEntries(entries.map((e) => [e.nodeId, {
43049
+ ms: e.ms,
43050
+ outcome: e.outcome,
43051
+ ...e.error === void 0 ? {} : { error: e.error }
43052
+ }]));
43053
+ this.deps.logger.warn("agent load census skipped a runner", { meta: {
43054
+ budgetMs,
43055
+ skipped,
43056
+ nodes
43057
+ } });
42955
43058
  }
42956
43059
  this.refreshAgentLoadCache(loads);
42957
43060
  return loads;
42958
43061
  }
43062
+ /**
43063
+ * One runner's load under `budgetMs`. A wedged runner (transport up enough
43064
+ * to stay in the registry, but whose call neither resolves nor rejects) is
43065
+ * counted as `budget-exceeded` and skipped exactly like an offline one; the
43066
+ * in-flight call is left to settle on its own, as before.
43067
+ */
43068
+ async censusNode(nodeId, budgetMs) {
43069
+ const api = this.deps.api();
43070
+ const startedAt = Date.now();
43071
+ if (!api) return {
43072
+ nodeId,
43073
+ ms: 0,
43074
+ outcome: "failed",
43075
+ load: null,
43076
+ error: "addon not initialized"
43077
+ };
43078
+ const outcome = await raceBudget(api.pipelineRunner.getLocalLoad.query({ nodeId }), budgetMs);
43079
+ const ms = Date.now() - startedAt;
43080
+ if (outcome.kind === "settled") return {
43081
+ nodeId,
43082
+ ms,
43083
+ outcome: "reported",
43084
+ load: outcome.value
43085
+ };
43086
+ if (outcome.kind === "rejected") return {
43087
+ nodeId,
43088
+ ms,
43089
+ outcome: "failed",
43090
+ load: null,
43091
+ error: errMsg(outcome.error)
43092
+ };
43093
+ return {
43094
+ nodeId,
43095
+ ms,
43096
+ outcome: "budget-exceeded",
43097
+ load: null
43098
+ };
43099
+ }
42959
43100
  refreshAgentLoadCache(loads) {
42960
43101
  const next = /* @__PURE__ */ new Map();
42961
43102
  for (const load of loads) next.set(load.nodeId, {
@@ -43002,25 +43143,6 @@ var AgentLoadService = class AgentLoadService {
43002
43143
  const value = (this.deps.readGlobalSettings() ?? {}).agentLoadTimeoutMs;
43003
43144
  return typeof value === "number" && value > 0 ? value : AgentLoadService.DEFAULT_AGENT_LOAD_TIMEOUT_MS;
43004
43145
  }
43005
- /**
43006
- * `getLocalLoad` for one runner node, bounded by `agentLoadBudgetMs`. On
43007
- * timeout it rejects so `collectAgentLoad`'s catch treats the node exactly
43008
- * like an offline one (logged at debug, skipped).
43009
- */
43010
- async queryLocalLoadBounded(nodeId) {
43011
- const api = this.deps.api();
43012
- if (!api) throw new Error("queryLocalLoadBounded: addon not initialized");
43013
- const budgetMs = this.agentLoadBudgetMs();
43014
- let timer;
43015
- const timeout = new Promise((_resolve, reject) => {
43016
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`getLocalLoad exceeded ${budgetMs}ms budget for ${nodeId}`)), budgetMs);
43017
- });
43018
- try {
43019
- return await Promise.race([api.pipelineRunner.getLocalLoad.query({ nodeId }), timeout]);
43020
- } finally {
43021
- if (timer) clearTimeout(timer);
43022
- }
43023
- }
43024
43146
  /** Mirrors the former `onShutdown`'s `this.cachedAgentLoad.clear()`. */
43025
43147
  dispose() {
43026
43148
  this.cachedAgentLoad.clear();
@@ -50591,19 +50713,44 @@ var ReconcileController = class ReconcileController {
50591
50713
  async runPlacementReconcile() {
50592
50714
  const api = this.deps.api();
50593
50715
  if (!api) return;
50716
+ const censusStartedAt = Date.now();
50594
50717
  const runners = [];
50595
- for (const nodeId of this.deps.topology.collectRunnerNodeIds()) try {
50596
- const attached = await api.pipelineRunner.getLocalCameras.query(void 0, nodePin(nodeId));
50597
- runners.push({
50718
+ const census = [];
50719
+ await Promise.all(this.deps.topology.collectRunnerNodeIds().map(async (nodeId) => {
50720
+ const startedAt = Date.now();
50721
+ const outcome = await raceBudget(api.pipelineRunner.getLocalCameras.query(void 0, nodePin(nodeId)), RECONCILE_NODE_BUDGET_MS);
50722
+ const ms = Date.now() - startedAt;
50723
+ if (outcome.kind === "settled") {
50724
+ runners.push({
50725
+ nodeId,
50726
+ attached: outcome.value
50727
+ });
50728
+ census.push({
50729
+ nodeId,
50730
+ ms,
50731
+ outcome: "reported"
50732
+ });
50733
+ return;
50734
+ }
50735
+ census.push({
50598
50736
  nodeId,
50599
- attached
50737
+ ms,
50738
+ outcome: outcome.kind === "rejected" ? "failed" : "budget-exceeded"
50600
50739
  });
50601
- } catch (err) {
50602
50740
  this.deps.logger.debug("placement reconcile skipped node — getLocalCameras failed", {
50603
50741
  tags: { nodeId },
50604
- meta: { error: errMsg(err) }
50742
+ meta: {
50743
+ error: outcome.kind === "rejected" ? errMsg(outcome.error) : "budget exceeded",
50744
+ ms
50745
+ }
50605
50746
  });
50606
- }
50747
+ }));
50748
+ const censusMs = Date.now() - censusStartedAt;
50749
+ if (censusMs >= 1e3 || census.some((entry) => entry.outcome !== "reported")) this.deps.logger.info("placement reconcile: runner census was slow", { meta: {
50750
+ censusMs,
50751
+ nodes: census,
50752
+ budgetMs: RECONCILE_NODE_BUDGET_MS
50753
+ } });
50607
50754
  const { assignedNodeById, dispatching } = this.deps.ledger.snapshotForReconcile();
50608
50755
  const idleSessionDeviceIds = new Set([...this.deps.activeDeviceIds()].filter((deviceId) => this.deps.isIdleSessionCamera(deviceId)));
50609
50756
  const motionWatchAttachments = /* @__PURE__ */ new Set();
@@ -50617,7 +50764,7 @@ var ReconcileController = class ReconcileController {
50617
50764
  });
50618
50765
  if (detach.length > 0) {
50619
50766
  this.deps.logger.info("placement reconcile: detaching orphans/duplicates", { meta: { detach } });
50620
- for (const { nodeId, deviceId } of detach) await this.deps.detachOn(nodeId, deviceId).catch((err) => {
50767
+ await Promise.all(detach.map(({ nodeId, deviceId }) => this.deps.detachOn(nodeId, deviceId).catch((err) => {
50621
50768
  this.deps.logger.warn("placement reconcile: detach failed", {
50622
50769
  tags: {
50623
50770
  nodeId,
@@ -50625,7 +50772,7 @@ var ReconcileController = class ReconcileController {
50625
50772
  },
50626
50773
  meta: { error: errMsg(err) }
50627
50774
  });
50628
- });
50775
+ })));
50629
50776
  }
50630
50777
  if (dropStaleAssignment.length > 0) {
50631
50778
  this.deps.logger.info("placement reconcile: dropping stale assignments", { meta: { deviceIds: dropStaleAssignment } });
@@ -51015,6 +51162,13 @@ var TtlMemo = class {
51015
51162
  * declared wrong.
51016
51163
  */
51017
51164
  generations = /* @__PURE__ */ new Map();
51165
+ /**
51166
+ * The last value ANY read produced for a key, kept across
51167
+ * {@link invalidate}. Never served by {@link get}; it is the bounded fallback
51168
+ * of {@link getWithin} alone (D441) — a caller that would rather balance on a
51169
+ * snapshot a few seconds old than wait seconds for the re-read.
51170
+ */
51171
+ lastKnown = /* @__PURE__ */ new Map();
51018
51172
  ttlMs;
51019
51173
  now;
51020
51174
  constructor(opts) {
@@ -51048,6 +51202,50 @@ var TtlMemo = class {
51048
51202
  }
51049
51203
  }
51050
51204
  /**
51205
+ * The value for `key` within `budgetMs`: from memory when fresh, from `load`
51206
+ * when the read lands in time, and otherwise the LAST KNOWN value —
51207
+ * invalidated or not — with its age (D441).
51208
+ *
51209
+ * Three properties, each pinned by a test:
51210
+ * - a read that overruns the budget is NOT abandoned: it keeps running and
51211
+ * stores its result for the next caller, so a stale answer is served at
51212
+ * most once per read;
51213
+ * - a COLD key waits for the read past the budget, because a cold cache must
51214
+ * never be the reason a camera fails to dispatch (the `get` contract);
51215
+ * - a read that FAILS with a last-known value in hand serves it as stale.
51216
+ */
51217
+ async getWithin(key, budgetMs) {
51218
+ const at = this.now();
51219
+ const entry = this.entries.get(key);
51220
+ if (entry) this.entries.set(key, {
51221
+ ...entry,
51222
+ usedAt: at
51223
+ });
51224
+ if (entry && at - entry.readAt < this.ttlMs) return {
51225
+ value: entry.value,
51226
+ stale: false,
51227
+ ageMs: at - entry.readAt
51228
+ };
51229
+ const fallback = this.lastKnown.get(key);
51230
+ const pending = this.read(key);
51231
+ if (fallback === void 0) return {
51232
+ value: await pending,
51233
+ stale: false,
51234
+ ageMs: 0
51235
+ };
51236
+ const outcome = await raceBudget(pending, budgetMs);
51237
+ if (outcome.kind === "settled") return {
51238
+ value: outcome.value,
51239
+ stale: false,
51240
+ ageMs: 0
51241
+ };
51242
+ return {
51243
+ value: fallback.value,
51244
+ stale: true,
51245
+ ageMs: this.now() - fallback.readAt
51246
+ };
51247
+ }
51248
+ /**
51051
51249
  * Declare the cached answer for `key` WRONG — not merely old.
51052
51250
  *
51053
51251
  * The caller has just changed the thing being cached (an attach or a detach
@@ -51092,6 +51290,7 @@ var TtlMemo = class {
51092
51290
  this.entries.clear();
51093
51291
  this.inFlight.clear();
51094
51292
  this.generations.clear();
51293
+ this.lastKnown.clear();
51095
51294
  }
51096
51295
  /** One shared in-flight read per key; stores the result on success. */
51097
51296
  read(key) {
@@ -51099,14 +51298,15 @@ var TtlMemo = class {
51099
51298
  if (existing) return existing;
51100
51299
  const generation = this.generations.get(key) ?? 0;
51101
51300
  const pending = this.opts.load(key).then((value) => {
51102
- if ((this.generations.get(key) ?? 0) !== generation) return value;
51103
51301
  const at = this.now();
51104
- const prior = this.entries.get(key);
51105
- this.entries.set(key, {
51302
+ const stored = {
51106
51303
  value,
51107
51304
  readAt: at,
51108
- usedAt: prior?.usedAt ?? at
51109
- });
51305
+ usedAt: this.entries.get(key)?.usedAt ?? at
51306
+ };
51307
+ this.lastKnown.set(key, stored);
51308
+ if ((this.generations.get(key) ?? 0) !== generation) return value;
51309
+ this.entries.set(key, stored);
51110
51310
  return value;
51111
51311
  }).finally(() => {
51112
51312
  if (this.inFlight.get(key) === pending) this.inFlight.delete(key);
@@ -51574,7 +51774,7 @@ var SessionDispatchController = class {
51574
51774
  return;
51575
51775
  }
51576
51776
  this.activeRefireCountByDevice.delete(deviceId);
51577
- await this.dispatchDetectionSession(deviceId, cur, trigger);
51777
+ const timings = await this.dispatchDetectionSession(deviceId, cur, trigger);
51578
51778
  if (this.sessionRegistry.has(deviceId)) this.scheduleSessionTeardown(deviceId, cooldownMs);
51579
51779
  const doneAt = Date.now();
51580
51780
  this.deps.logger.info("session motion → attach latency", {
@@ -51583,6 +51783,7 @@ var SessionDispatchController = class {
51583
51783
  ...busLagMs !== void 0 ? { busLagMs } : {},
51584
51784
  lockWaitMs: lockAcquiredAt - lockRequestedAt,
51585
51785
  dispatchMs: doneAt - lockAcquiredAt,
51786
+ ...timings,
51586
51787
  totalMs: doneAt - receivedAt,
51587
51788
  ...emittedAt !== void 0 ? { sinceMotionMs: doneAt - emittedAt } : {}
51588
51789
  }
@@ -51615,9 +51816,32 @@ var SessionDispatchController = class {
51615
51816
  */
51616
51817
  async dispatchDetectionSession(deviceId, config, trigger) {
51617
51818
  const log = this.deps.logger.withTags({ deviceId });
51618
- await this.deps.reconcilePlacementFromRunners();
51819
+ const reconcileStartedAt = Date.now();
51820
+ const census = this.deps.reconcilePlacementFromRunners();
51821
+ const censusOutcome = await raceBudget(census, 500);
51822
+ const reconcileMs = Date.now() - reconcileStartedAt;
51823
+ const reconcileBudgetExceeded = censusOutcome.kind === "budget-exceeded";
51824
+ const dispatchGuard = this.deps.ledger.markDispatching(deviceId);
51825
+ const releaseGuard = () => {
51826
+ if (!reconcileBudgetExceeded) {
51827
+ dispatchGuard.dispose();
51828
+ return;
51829
+ }
51830
+ census.finally(() => dispatchGuard.dispose());
51831
+ };
51832
+ if (reconcileBudgetExceeded) log.warn("detection session: placement census overran its budget — placing without it", { meta: { budgetMs: 500 } });
51833
+ else if (censusOutcome.kind === "rejected") log.warn("detection session: placement census failed — placing on the ledger as it is", { meta: {
51834
+ error: errMsg(censusOutcome.error),
51835
+ reconcileMs
51836
+ } });
51837
+ const balanceStartedAt = Date.now();
51619
51838
  const preferredAgent = await this.deps.readPipelinePin(deviceId);
51620
- const loads = await this.agentLoads.get(CLUSTER_KEY);
51839
+ const loadAnswer = await this.agentLoads.getWithin(CLUSTER_KEY, 500);
51840
+ const loads = loadAnswer.value;
51841
+ if (loadAnswer.stale) log.warn("detection session: agent load read overran its budget — balancing on the last known load", { meta: {
51842
+ budgetMs: 500,
51843
+ loadAgeMs: loadAnswer.ageMs
51844
+ } });
51621
51845
  const allEligible = this.deps.topology.detectionEligibleNodes(deviceId);
51622
51846
  const usableEligible = allEligible.filter((n) => this.deps.isNodeInferenceUsable(n));
51623
51847
  const eligible = usableEligible.length > 0 ? usableEligible : allEligible;
@@ -51631,9 +51855,11 @@ var SessionDispatchController = class {
51631
51855
  });
51632
51856
  if (!decision || decision.kind !== "assigned") {
51633
51857
  log.warn("detection session: no eligible node — will retry on next motion", { meta: { reason: decision?.kind === "pending" ? decision.reason : "no-runners-online" } });
51634
- return;
51858
+ releaseGuard();
51859
+ return null;
51635
51860
  }
51636
51861
  const targetNodeId = decision.agentNodeId;
51862
+ const balancedAt = Date.now();
51637
51863
  const enabledDevices = await this.eligibleInferenceDevices.get(targetNodeId);
51638
51864
  const targetLoad = loads.find((n) => n.nodeId === targetNodeId);
51639
51865
  const deviceCaps = await this.inferenceDeviceCaps.get(targetNodeId);
@@ -51663,6 +51889,7 @@ var SessionDispatchController = class {
51663
51889
  ...pinnedDeviceKey ? { pin: pinnedDeviceKey } : {}
51664
51890
  }
51665
51891
  });
51892
+ const deviceSelectedAt = Date.now();
51666
51893
  const pipelineConfig = await this.deps.settingsStore.resolvePipelineForDevice(deviceId, targetNodeId, deviceKey);
51667
51894
  const deviceBase = await this.deps.settingsStore.buildNodeInferenceDeviceSteps(targetNodeId, deviceKey ?? "");
51668
51895
  const deviceOverride = deviceKey ? await this.deps.settingsStore.readCameraDeviceOverride(deviceId, targetNodeId, deviceKey) : {};
@@ -51698,9 +51925,11 @@ var SessionDispatchController = class {
51698
51925
  preRollInferenceEnabled: config.preRollInferenceEnabled,
51699
51926
  preRollInferenceFrames: config.preRollInferenceFrames
51700
51927
  };
51701
- const dispatchGuard = this.deps.ledger.markDispatching(deviceId);
51928
+ const configuredAt = Date.now();
51929
+ let attachedAt = configuredAt;
51702
51930
  try {
51703
51931
  await this.deps.attachOn(targetNodeId, sessionConfig);
51932
+ attachedAt = Date.now();
51704
51933
  this.deps.recordAssignment(deviceId, targetNodeId, decision.reason, decision.reason === "manual");
51705
51934
  this.sessionRegistry.register(deviceId, targetNodeId, Date.now(), deviceKey);
51706
51935
  log.info("detection session: dispatched on motion", {
@@ -51716,9 +51945,18 @@ var SessionDispatchController = class {
51716
51945
  }
51717
51946
  });
51718
51947
  } finally {
51719
- dispatchGuard.dispose();
51948
+ releaseGuard();
51720
51949
  this.notePlacementChanged();
51721
51950
  }
51951
+ return {
51952
+ reconcileMs,
51953
+ reconcileBudgetExceeded,
51954
+ balanceMs: balancedAt - balanceStartedAt,
51955
+ balanceLoadStale: loadAnswer.stale,
51956
+ deviceSelectMs: deviceSelectedAt - balancedAt,
51957
+ configMs: configuredAt - deviceSelectedAt,
51958
+ attachMs: attachedAt - configuredAt
51959
+ };
51722
51960
  }
51723
51961
  /**
51724
51962
  * Schedule a session teardown after `cooldownMs` of no rising-edge motion.
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CUGRt6hQ.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-B944RKiT.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline-orchestrator",
3
- "version": "1.2.181",
3
+ "version": "1.2.182",
4
4
  "description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
5
5
  "keywords": [
6
6
  "camstack",