@camstack/addon-pipeline 1.1.61 → 1.1.63
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-p_JqOxtk.js → dist-B2jTt7Lq.js} +377 -3
- package/dist/{dist-D16_dBsr.mjs → dist-BzVZ2I4P.mjs} +372 -4
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +126 -3
- package/dist/pipeline-runner/index.mjs +126 -3
- package/dist/recorder/index.js +161 -20
- package/dist/recorder/index.mjs +161 -20
- package/dist/{step-definitions-DxAcpUV-.mjs → step-definitions-B07M9qhj.mjs} +68 -1
- package/dist/{step-definitions-CoxF5G31.js → step-definitions-DXkY5Zor.js} +68 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Wu4TlSuq.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-dTMki_1D.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-bAh96WNb.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CV2WbUaO.mjs +26 -0
- package/dist/stream-broker/{hostInit-D_dSciqF.mjs → hostInit-BY2oQAEX.mjs} +3 -3
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BldL9_Ee.js → MaskShapeCanvas-DI4BY7W2-C03fx7Sp.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BgWqMe4f.js → MotionZonesSettings-NcxxQN8r-BTxzn1rQ.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-YBgaXffP.js → PrivacyMaskSettings-APgPLF7p-GKO8O7rC.js} +1 -1
- package/embed-dist/assets/index-Bm9LaQPH.css +2 -0
- package/embed-dist/assets/{index-e0bQ8yep.js → index-QdssILAA.js} +10 -10
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BPujpbZ0.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DGxcX3Wf.mjs +0 -26
- package/embed-dist/assets/index-Bmpl9O1K.css +0 -2
|
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-B2jTt7Lq.js");
|
|
7
7
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
8
8
|
const require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
|
|
9
9
|
const require_worker_protocol = require("../worker-protocol-PP4jKHHJ.js");
|
|
10
|
-
const require_step_definitions = require("../step-definitions-
|
|
10
|
+
const require_step_definitions = require("../step-definitions-DXkY5Zor.js");
|
|
11
11
|
let node_child_process = require("node:child_process");
|
|
12
12
|
let node_url = require("node:url");
|
|
13
13
|
let sharp = require("sharp");
|
|
@@ -106,6 +106,109 @@ var pipelineRunnerBenchActions = require_dist.defineCustomActions({
|
|
|
106
106
|
runSyntheticBench: require_dist.customAction(RunSyntheticBenchInputSchema, RunSyntheticBenchResultSchema, { kind: "mutation" })
|
|
107
107
|
});
|
|
108
108
|
//#endregion
|
|
109
|
+
//#region src/pipeline-runner/motion-settle-governor.ts
|
|
110
|
+
/**
|
|
111
|
+
* MotionSettleGovernor — decides WHEN the root-plane `package-detection` step
|
|
112
|
+
* should run, so it is NOT executed on every frame.
|
|
113
|
+
*
|
|
114
|
+
* A delivered package is STATIONARY: per-frame inference on it is wasted cost.
|
|
115
|
+
* The interesting moments are the EDGES — someone just set a parcel down, or
|
|
116
|
+
* just picked one up — which coincide with motion in the package zone
|
|
117
|
+
* SETTLING (transitioning from moving to still). This governor fires:
|
|
118
|
+
*
|
|
119
|
+
* - on a **settle edge**: motion was observed, then has been absent for
|
|
120
|
+
* `settleMs` (kills flicker between motion frames and the "bag briefly
|
|
121
|
+
* set down then snatched back" case), and
|
|
122
|
+
* - on a **low-cadence safety poll** (`pollIntervalMs`, ~30–60s): a
|
|
123
|
+
* backstop so a delivery whose motion was never observed (a session-gap
|
|
124
|
+
* camera that only decodes intermittently) is still eventually caught.
|
|
125
|
+
*
|
|
126
|
+
* Pure logic with an injected clock (every method takes `now`), mirroring
|
|
127
|
+
* `FrameGovernor` / `recomputeGovernor(now)` — no timers, no `Date.now()`
|
|
128
|
+
* inside, fully unit-testable. Per-device state lives in a Map; the caller
|
|
129
|
+
* consults it only while the step is enabled and calls `forget()` on detach.
|
|
130
|
+
*
|
|
131
|
+
* NOTE: today `motionInZone` is the whole-frame analyzer motion signal the
|
|
132
|
+
* runner already tracks (`lastAnalyzerDetected`). Scoping it to the package
|
|
133
|
+
* zone specifically (intersecting motion regions with the `package`-stage
|
|
134
|
+
* zones) is a refinement that lands with the crop-zone frame-prep follow-up,
|
|
135
|
+
* where the package zones first become available at the frame layer. The
|
|
136
|
+
* governor contract does not change when that happens — only the boolean the
|
|
137
|
+
* caller feeds it.
|
|
138
|
+
*
|
|
139
|
+
* See docs/superpowers/specs/2026-07-17-package-zones-design.md
|
|
140
|
+
* (ADDENDUM 2026-07-17 — motion-settle trigger).
|
|
141
|
+
*/
|
|
142
|
+
/** The step id (`PipelineStepInput.addonId`) the governor gates. */
|
|
143
|
+
var PACKAGE_DETECTION_STEP_ID = "package-detection";
|
|
144
|
+
var DEFAULT_MOTION_SETTLE_CONFIG = {
|
|
145
|
+
settleMs: 3e3,
|
|
146
|
+
pollIntervalMs: 45e3
|
|
147
|
+
};
|
|
148
|
+
var INITIAL_STATE = {
|
|
149
|
+
lastMotionAt: null,
|
|
150
|
+
armed: false,
|
|
151
|
+
lastRunAt: Number.NEGATIVE_INFINITY
|
|
152
|
+
};
|
|
153
|
+
var NO_RUN = {
|
|
154
|
+
run: false,
|
|
155
|
+
reason: null
|
|
156
|
+
};
|
|
157
|
+
var MotionSettleGovernor = class {
|
|
158
|
+
config;
|
|
159
|
+
state = /* @__PURE__ */ new Map();
|
|
160
|
+
constructor(config = DEFAULT_MOTION_SETTLE_CONFIG) {
|
|
161
|
+
this.config = config;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Decide whether `package-detection` runs for this device this frame, and
|
|
165
|
+
* advance the per-device state immutably. Never throws.
|
|
166
|
+
*/
|
|
167
|
+
evaluate(sample) {
|
|
168
|
+
const prev = this.state.get(sample.deviceId) ?? INITIAL_STATE;
|
|
169
|
+
const lastMotionAt = sample.motionInZone ? sample.now : prev.lastMotionAt;
|
|
170
|
+
const armed = sample.motionInZone ? true : prev.armed;
|
|
171
|
+
const settleReady = armed && !sample.motionInZone && lastMotionAt !== null && sample.now - lastMotionAt >= this.config.settleMs;
|
|
172
|
+
const pollReady = sample.now - prev.lastRunAt >= this.config.pollIntervalMs;
|
|
173
|
+
if (settleReady) {
|
|
174
|
+
this.state.set(sample.deviceId, {
|
|
175
|
+
lastMotionAt,
|
|
176
|
+
armed: false,
|
|
177
|
+
lastRunAt: sample.now
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
run: true,
|
|
181
|
+
reason: "settle"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (pollReady) {
|
|
185
|
+
this.state.set(sample.deviceId, {
|
|
186
|
+
lastMotionAt,
|
|
187
|
+
armed,
|
|
188
|
+
lastRunAt: sample.now
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
run: true,
|
|
192
|
+
reason: "poll"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
this.state.set(sample.deviceId, {
|
|
196
|
+
lastMotionAt,
|
|
197
|
+
armed,
|
|
198
|
+
lastRunAt: prev.lastRunAt
|
|
199
|
+
});
|
|
200
|
+
return NO_RUN;
|
|
201
|
+
}
|
|
202
|
+
/** Drop a device's state (call on detach / step-disable) to avoid a leak. */
|
|
203
|
+
forget(deviceId) {
|
|
204
|
+
this.state.delete(deviceId);
|
|
205
|
+
}
|
|
206
|
+
/** Clear all state (e.g. runner teardown). */
|
|
207
|
+
reset() {
|
|
208
|
+
this.state.clear();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
//#endregion
|
|
109
212
|
//#region src/pipeline-runner/frame-governor.ts
|
|
110
213
|
var FrameGovernor = class {
|
|
111
214
|
config;
|
|
@@ -2401,6 +2504,13 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2401
2504
|
* only (otherwise we'd emit on every analyzer frame). */
|
|
2402
2505
|
lastAnalyzerDetected = /* @__PURE__ */ new Map();
|
|
2403
2506
|
/**
|
|
2507
|
+
* Gates the root-plane `package-detection` step to motion-settle edges +
|
|
2508
|
+
* a low-cadence safety poll instead of every frame (packages are
|
|
2509
|
+
* stationary — per-frame parcel inference is wasted cost). Consulted in
|
|
2510
|
+
* `runInference` only when the step is enabled; state dropped on detach.
|
|
2511
|
+
*/
|
|
2512
|
+
packageGovernor = new MotionSettleGovernor();
|
|
2513
|
+
/**
|
|
2404
2514
|
* Last positive motion timestamp per device — preserved across the
|
|
2405
2515
|
* OFF transition so the motion runtime-state slice keeps a stable
|
|
2406
2516
|
* `lastDetectedAt` after the cooldown closes the phase. Cleared on
|
|
@@ -2584,6 +2694,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
2584
2694
|
this.unsubMotionEvents = null;
|
|
2585
2695
|
}
|
|
2586
2696
|
this.lastAnalyzerDetected.clear();
|
|
2697
|
+
this.packageGovernor.reset();
|
|
2587
2698
|
for (const deviceId of [...this.onboardAnalyzerTeardownTimers.keys(), ...this.onboardAnalyzerSubs.keys()]) this.clearOnboardAnalyzer(deviceId);
|
|
2588
2699
|
if (this.runner) {
|
|
2589
2700
|
this.runner.stop();
|
|
@@ -3197,6 +3308,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3197
3308
|
this.lastMotionFrameAt.delete(deviceId);
|
|
3198
3309
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
3199
3310
|
this.thumbSampler?.forget(deviceId);
|
|
3311
|
+
this.packageGovernor.forget(deviceId);
|
|
3200
3312
|
this.attached.delete(deviceId);
|
|
3201
3313
|
this.lastMotionAt.delete(deviceId);
|
|
3202
3314
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
@@ -3615,6 +3727,17 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3615
3727
|
return null;
|
|
3616
3728
|
}
|
|
3617
3729
|
if (steps.length === 0) return null;
|
|
3730
|
+
let dispatchSteps = steps;
|
|
3731
|
+
if (steps.some((s) => s.addonId === "package-detection" && s.enabled !== false)) {
|
|
3732
|
+
if (!this.packageGovernor.evaluate({
|
|
3733
|
+
deviceId,
|
|
3734
|
+
motionInZone: this.lastAnalyzerDetected.get(deviceId) ?? false,
|
|
3735
|
+
now: Date.now()
|
|
3736
|
+
}).run) {
|
|
3737
|
+
dispatchSteps = steps.filter((s) => s.addonId !== PACKAGE_DETECTION_STEP_ID);
|
|
3738
|
+
if (dispatchSteps.length === 0) return null;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3618
3741
|
let image;
|
|
3619
3742
|
if (!this.executorCoLocated()) {
|
|
3620
3743
|
if (frame.format === "rgb") try {
|
|
@@ -3629,7 +3752,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
|
|
|
3629
3752
|
}
|
|
3630
3753
|
try {
|
|
3631
3754
|
return await api.pipelineExecutor.runPipeline.mutate({
|
|
3632
|
-
steps: [...
|
|
3755
|
+
steps: [...dispatchSteps],
|
|
3633
3756
|
...image !== void 0 ? { image } : { frame },
|
|
3634
3757
|
deviceId,
|
|
3635
3758
|
plane: "frame"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as errMsg, G as createEvent, V as BaseAddon, dt as EventCategory, et as _enum, it as lazy, j as pipelineRunnerCapability, k as nodePin, lt as string, nt as boolean, ot as number, st as object, tt as array, x as defineCustomActions, y as customAction } from "../dist-BzVZ2I4P.mjs";
|
|
2
2
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
3
3
|
import { r as resolveHubHostname } from "../sheet-geometry-DcpuGTCm.mjs";
|
|
4
4
|
import { t as isWorkerReply } from "../worker-protocol-CyVJTZEO.mjs";
|
|
5
|
-
import { a as getStepDefinition } from "../step-definitions-
|
|
5
|
+
import { a as getStepDefinition } from "../step-definitions-B07M9qhj.mjs";
|
|
6
6
|
import { fork } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import sharp from "sharp";
|
|
@@ -100,6 +100,109 @@ var pipelineRunnerBenchActions = defineCustomActions({
|
|
|
100
100
|
runSyntheticBench: customAction(RunSyntheticBenchInputSchema, RunSyntheticBenchResultSchema, { kind: "mutation" })
|
|
101
101
|
});
|
|
102
102
|
//#endregion
|
|
103
|
+
//#region src/pipeline-runner/motion-settle-governor.ts
|
|
104
|
+
/**
|
|
105
|
+
* MotionSettleGovernor — decides WHEN the root-plane `package-detection` step
|
|
106
|
+
* should run, so it is NOT executed on every frame.
|
|
107
|
+
*
|
|
108
|
+
* A delivered package is STATIONARY: per-frame inference on it is wasted cost.
|
|
109
|
+
* The interesting moments are the EDGES — someone just set a parcel down, or
|
|
110
|
+
* just picked one up — which coincide with motion in the package zone
|
|
111
|
+
* SETTLING (transitioning from moving to still). This governor fires:
|
|
112
|
+
*
|
|
113
|
+
* - on a **settle edge**: motion was observed, then has been absent for
|
|
114
|
+
* `settleMs` (kills flicker between motion frames and the "bag briefly
|
|
115
|
+
* set down then snatched back" case), and
|
|
116
|
+
* - on a **low-cadence safety poll** (`pollIntervalMs`, ~30–60s): a
|
|
117
|
+
* backstop so a delivery whose motion was never observed (a session-gap
|
|
118
|
+
* camera that only decodes intermittently) is still eventually caught.
|
|
119
|
+
*
|
|
120
|
+
* Pure logic with an injected clock (every method takes `now`), mirroring
|
|
121
|
+
* `FrameGovernor` / `recomputeGovernor(now)` — no timers, no `Date.now()`
|
|
122
|
+
* inside, fully unit-testable. Per-device state lives in a Map; the caller
|
|
123
|
+
* consults it only while the step is enabled and calls `forget()` on detach.
|
|
124
|
+
*
|
|
125
|
+
* NOTE: today `motionInZone` is the whole-frame analyzer motion signal the
|
|
126
|
+
* runner already tracks (`lastAnalyzerDetected`). Scoping it to the package
|
|
127
|
+
* zone specifically (intersecting motion regions with the `package`-stage
|
|
128
|
+
* zones) is a refinement that lands with the crop-zone frame-prep follow-up,
|
|
129
|
+
* where the package zones first become available at the frame layer. The
|
|
130
|
+
* governor contract does not change when that happens — only the boolean the
|
|
131
|
+
* caller feeds it.
|
|
132
|
+
*
|
|
133
|
+
* See docs/superpowers/specs/2026-07-17-package-zones-design.md
|
|
134
|
+
* (ADDENDUM 2026-07-17 — motion-settle trigger).
|
|
135
|
+
*/
|
|
136
|
+
/** The step id (`PipelineStepInput.addonId`) the governor gates. */
|
|
137
|
+
var PACKAGE_DETECTION_STEP_ID = "package-detection";
|
|
138
|
+
var DEFAULT_MOTION_SETTLE_CONFIG = {
|
|
139
|
+
settleMs: 3e3,
|
|
140
|
+
pollIntervalMs: 45e3
|
|
141
|
+
};
|
|
142
|
+
var INITIAL_STATE = {
|
|
143
|
+
lastMotionAt: null,
|
|
144
|
+
armed: false,
|
|
145
|
+
lastRunAt: Number.NEGATIVE_INFINITY
|
|
146
|
+
};
|
|
147
|
+
var NO_RUN = {
|
|
148
|
+
run: false,
|
|
149
|
+
reason: null
|
|
150
|
+
};
|
|
151
|
+
var MotionSettleGovernor = class {
|
|
152
|
+
config;
|
|
153
|
+
state = /* @__PURE__ */ new Map();
|
|
154
|
+
constructor(config = DEFAULT_MOTION_SETTLE_CONFIG) {
|
|
155
|
+
this.config = config;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Decide whether `package-detection` runs for this device this frame, and
|
|
159
|
+
* advance the per-device state immutably. Never throws.
|
|
160
|
+
*/
|
|
161
|
+
evaluate(sample) {
|
|
162
|
+
const prev = this.state.get(sample.deviceId) ?? INITIAL_STATE;
|
|
163
|
+
const lastMotionAt = sample.motionInZone ? sample.now : prev.lastMotionAt;
|
|
164
|
+
const armed = sample.motionInZone ? true : prev.armed;
|
|
165
|
+
const settleReady = armed && !sample.motionInZone && lastMotionAt !== null && sample.now - lastMotionAt >= this.config.settleMs;
|
|
166
|
+
const pollReady = sample.now - prev.lastRunAt >= this.config.pollIntervalMs;
|
|
167
|
+
if (settleReady) {
|
|
168
|
+
this.state.set(sample.deviceId, {
|
|
169
|
+
lastMotionAt,
|
|
170
|
+
armed: false,
|
|
171
|
+
lastRunAt: sample.now
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
run: true,
|
|
175
|
+
reason: "settle"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (pollReady) {
|
|
179
|
+
this.state.set(sample.deviceId, {
|
|
180
|
+
lastMotionAt,
|
|
181
|
+
armed,
|
|
182
|
+
lastRunAt: sample.now
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
run: true,
|
|
186
|
+
reason: "poll"
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
this.state.set(sample.deviceId, {
|
|
190
|
+
lastMotionAt,
|
|
191
|
+
armed,
|
|
192
|
+
lastRunAt: prev.lastRunAt
|
|
193
|
+
});
|
|
194
|
+
return NO_RUN;
|
|
195
|
+
}
|
|
196
|
+
/** Drop a device's state (call on detach / step-disable) to avoid a leak. */
|
|
197
|
+
forget(deviceId) {
|
|
198
|
+
this.state.delete(deviceId);
|
|
199
|
+
}
|
|
200
|
+
/** Clear all state (e.g. runner teardown). */
|
|
201
|
+
reset() {
|
|
202
|
+
this.state.clear();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
103
206
|
//#region src/pipeline-runner/frame-governor.ts
|
|
104
207
|
var FrameGovernor = class {
|
|
105
208
|
config;
|
|
@@ -2395,6 +2498,13 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2395
2498
|
* only (otherwise we'd emit on every analyzer frame). */
|
|
2396
2499
|
lastAnalyzerDetected = /* @__PURE__ */ new Map();
|
|
2397
2500
|
/**
|
|
2501
|
+
* Gates the root-plane `package-detection` step to motion-settle edges +
|
|
2502
|
+
* a low-cadence safety poll instead of every frame (packages are
|
|
2503
|
+
* stationary — per-frame parcel inference is wasted cost). Consulted in
|
|
2504
|
+
* `runInference` only when the step is enabled; state dropped on detach.
|
|
2505
|
+
*/
|
|
2506
|
+
packageGovernor = new MotionSettleGovernor();
|
|
2507
|
+
/**
|
|
2398
2508
|
* Last positive motion timestamp per device — preserved across the
|
|
2399
2509
|
* OFF transition so the motion runtime-state slice keeps a stable
|
|
2400
2510
|
* `lastDetectedAt` after the cooldown closes the phase. Cleared on
|
|
@@ -2578,6 +2688,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2578
2688
|
this.unsubMotionEvents = null;
|
|
2579
2689
|
}
|
|
2580
2690
|
this.lastAnalyzerDetected.clear();
|
|
2691
|
+
this.packageGovernor.reset();
|
|
2581
2692
|
for (const deviceId of [...this.onboardAnalyzerTeardownTimers.keys(), ...this.onboardAnalyzerSubs.keys()]) this.clearOnboardAnalyzer(deviceId);
|
|
2582
2693
|
if (this.runner) {
|
|
2583
2694
|
this.runner.stop();
|
|
@@ -3191,6 +3302,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3191
3302
|
this.lastMotionFrameAt.delete(deviceId);
|
|
3192
3303
|
this.lastDecodeUnexpectedEndAt.delete(deviceId);
|
|
3193
3304
|
this.thumbSampler?.forget(deviceId);
|
|
3305
|
+
this.packageGovernor.forget(deviceId);
|
|
3194
3306
|
this.attached.delete(deviceId);
|
|
3195
3307
|
this.lastMotionAt.delete(deviceId);
|
|
3196
3308
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
@@ -3609,6 +3721,17 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3609
3721
|
return null;
|
|
3610
3722
|
}
|
|
3611
3723
|
if (steps.length === 0) return null;
|
|
3724
|
+
let dispatchSteps = steps;
|
|
3725
|
+
if (steps.some((s) => s.addonId === "package-detection" && s.enabled !== false)) {
|
|
3726
|
+
if (!this.packageGovernor.evaluate({
|
|
3727
|
+
deviceId,
|
|
3728
|
+
motionInZone: this.lastAnalyzerDetected.get(deviceId) ?? false,
|
|
3729
|
+
now: Date.now()
|
|
3730
|
+
}).run) {
|
|
3731
|
+
dispatchSteps = steps.filter((s) => s.addonId !== PACKAGE_DETECTION_STEP_ID);
|
|
3732
|
+
if (dispatchSteps.length === 0) return null;
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3612
3735
|
let image;
|
|
3613
3736
|
if (!this.executorCoLocated()) {
|
|
3614
3737
|
if (frame.format === "rgb") try {
|
|
@@ -3623,7 +3746,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3623
3746
|
}
|
|
3624
3747
|
try {
|
|
3625
3748
|
return await api.pipelineExecutor.runPipeline.mutate({
|
|
3626
|
-
steps: [...
|
|
3749
|
+
steps: [...dispatchSteps],
|
|
3627
3750
|
...image !== void 0 ? { image } : { frame },
|
|
3628
3751
|
deviceId,
|
|
3629
3752
|
plane: "frame"
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-B2jTt7Lq.js");
|
|
3
3
|
const require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
|
|
4
4
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
|
|
5
5
|
let node_child_process = require("node:child_process");
|
|
@@ -1005,6 +1005,34 @@ function segmentsByLocation(rows) {
|
|
|
1005
1005
|
return out;
|
|
1006
1006
|
}
|
|
1007
1007
|
/**
|
|
1008
|
+
* Evict a set of victim segments through the SegmentStore, grouped by their
|
|
1009
|
+
* storage location, and return the reclaimed bytes + deleted-bucket count.
|
|
1010
|
+
* Per-location isolated: one location's evict failure is logged and never aborts
|
|
1011
|
+
* the others. Shared by `pruneFootage` (retention) and `deleteFootprint`
|
|
1012
|
+
* (manual).
|
|
1013
|
+
*/
|
|
1014
|
+
async function evictVictims(deps, deviceId, victims) {
|
|
1015
|
+
let reclaimedBytes = 0;
|
|
1016
|
+
let deletedBuckets = 0;
|
|
1017
|
+
for (const [locationId, locVictims] of segmentsByLocation(victims)) try {
|
|
1018
|
+
const freed = await deps.segmentStore.evict(locationId, locVictims);
|
|
1019
|
+
reclaimedBytes += freed;
|
|
1020
|
+
if (freed > 0) deletedBuckets += locVictims.length;
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
deps.logger.warn("recorder evict failed for location", {
|
|
1023
|
+
tags: { deviceId },
|
|
1024
|
+
meta: {
|
|
1025
|
+
locationId,
|
|
1026
|
+
error: require_dist.errMsg(err)
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
reclaimedBytes,
|
|
1032
|
+
deletedBuckets
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1008
1036
|
* Build the `IRecordingProvider`. The provider is a thin façade over the v2
|
|
1009
1037
|
* core; all storage I/O flows through the injected deps.
|
|
1010
1038
|
*/
|
|
@@ -1101,37 +1129,63 @@ function buildRecordingProvider(deps) {
|
|
|
1101
1129
|
rescanStorage: async ({ deviceId }) => {
|
|
1102
1130
|
const locations = await deps.refreshLocations();
|
|
1103
1131
|
await deps.hydrateDevice(deviceId, locations);
|
|
1104
|
-
|
|
1132
|
+
const config = await loadDeviceConfig(deps.configStore, deviceId);
|
|
1133
|
+
await deps.opsLog?.append({
|
|
1134
|
+
op: "rescan",
|
|
1135
|
+
reason: "operator",
|
|
1136
|
+
deviceId,
|
|
1137
|
+
itemsAffected: deps.index.segments(deviceId).length,
|
|
1138
|
+
bytesReclaimed: 0,
|
|
1139
|
+
detail: "rescanned footage from disk"
|
|
1140
|
+
});
|
|
1141
|
+
return statusFor(deps, deviceId, config);
|
|
1105
1142
|
},
|
|
1106
|
-
pruneFootage: async ({ deviceId }) => {
|
|
1143
|
+
pruneFootage: async ({ deviceId, reason }) => {
|
|
1107
1144
|
const retention = (await loadDeviceConfig(deps.configStore, deviceId)).retention;
|
|
1108
1145
|
const nowMs = Date.now();
|
|
1109
|
-
const
|
|
1146
|
+
const { reclaimedBytes, deletedBuckets } = await evictVictims(deps, deviceId, selectEvictions(deps.index.segments(deviceId), {
|
|
1110
1147
|
maxAgeDays: retention?.maxAgeDays,
|
|
1111
1148
|
maxSizeGb: retention?.maxSizeGb,
|
|
1112
1149
|
nowMs
|
|
1150
|
+
}));
|
|
1151
|
+
const floorMs = deps.index.accounting(deviceId).oldestMs ?? (retention?.maxAgeDays ? nowMs - retention.maxAgeDays * DAY_MS : null);
|
|
1152
|
+
const opReason = reason ?? "retention";
|
|
1153
|
+
await deps.opsLog?.append({
|
|
1154
|
+
op: "prune",
|
|
1155
|
+
reason: opReason,
|
|
1156
|
+
deviceId,
|
|
1157
|
+
itemsAffected: deletedBuckets,
|
|
1158
|
+
bytesReclaimed: reclaimedBytes,
|
|
1159
|
+
detail: floorMs === null ? "no footage remaining" : `floor moved to ${floorMs}`
|
|
1160
|
+
});
|
|
1161
|
+
return {
|
|
1162
|
+
floorMs,
|
|
1163
|
+
deletedBuckets,
|
|
1164
|
+
reclaimedBytes
|
|
1165
|
+
};
|
|
1166
|
+
},
|
|
1167
|
+
deleteFootprint: async ({ deviceId, fromMs, toMs }) => {
|
|
1168
|
+
const lo = fromMs ?? Number.NEGATIVE_INFINITY;
|
|
1169
|
+
const hi = toMs ?? Number.POSITIVE_INFINITY;
|
|
1170
|
+
const { reclaimedBytes, deletedBuckets } = await evictVictims(deps, deviceId, deps.index.segments(deviceId).filter((s) => s.startMs < hi && s.startMs + s.durMs > lo));
|
|
1171
|
+
const windowed = fromMs !== void 0 || toMs !== void 0;
|
|
1172
|
+
await deps.opsLog?.append({
|
|
1173
|
+
op: "manual-delete",
|
|
1174
|
+
reason: "manual",
|
|
1175
|
+
deviceId,
|
|
1176
|
+
itemsAffected: deletedBuckets,
|
|
1177
|
+
bytesReclaimed: reclaimedBytes,
|
|
1178
|
+
detail: windowed ? `deleted footage in [${fromMs ?? "−∞"}, ${toMs ?? "+∞"})` : "deleted all footage"
|
|
1113
1179
|
});
|
|
1114
|
-
let reclaimedBytes = 0;
|
|
1115
|
-
let deletedBuckets = 0;
|
|
1116
|
-
for (const [locationId, locVictims] of segmentsByLocation(victims)) try {
|
|
1117
|
-
const freed = await deps.segmentStore.evict(locationId, locVictims);
|
|
1118
|
-
reclaimedBytes += freed;
|
|
1119
|
-
if (freed > 0) deletedBuckets += locVictims.length;
|
|
1120
|
-
} catch (err) {
|
|
1121
|
-
deps.logger.warn("recorder pruneFootage evict failed for location", {
|
|
1122
|
-
tags: { deviceId },
|
|
1123
|
-
meta: {
|
|
1124
|
-
locationId,
|
|
1125
|
-
error: require_dist.errMsg(err)
|
|
1126
|
-
}
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
1180
|
return {
|
|
1130
|
-
floorMs: deps.index.accounting(deviceId).oldestMs ?? (retention?.maxAgeDays ? nowMs - retention.maxAgeDays * DAY_MS : null),
|
|
1131
1181
|
deletedBuckets,
|
|
1132
1182
|
reclaimedBytes
|
|
1133
1183
|
};
|
|
1134
1184
|
},
|
|
1185
|
+
listOpsLog: async ({ deviceId, limit }) => await deps.opsLog?.list({
|
|
1186
|
+
deviceId,
|
|
1187
|
+
limit
|
|
1188
|
+
}) ?? [],
|
|
1135
1189
|
getDeviceSettingsContribution: async ({ deviceId }) => {
|
|
1136
1190
|
if (deps.isCameraDevice && !await deps.isCameraDevice(deviceId)) return null;
|
|
1137
1191
|
return require_dist.hydrateSchema(buildRecordingDeviceSchema(), {});
|
|
@@ -1208,6 +1262,81 @@ async function listExportsFor(state, deviceId) {
|
|
|
1208
1262
|
return [...(await readExports(state)).values()].filter((r) => deviceId === void 0 || r.deviceId === deviceId).toSorted((a, b) => b.createdAt - a.createdAt);
|
|
1209
1263
|
}
|
|
1210
1264
|
//#endregion
|
|
1265
|
+
//#region src/recorder/addon/ops-log-store.ts
|
|
1266
|
+
/**
|
|
1267
|
+
* Recordings ops-log persistence for the recorder addon.
|
|
1268
|
+
*
|
|
1269
|
+
* A bounded, append-only audit ring lives in ONE addon-store blob (same
|
|
1270
|
+
* DurableState pattern as `export-store.ts` / `config-store.ts`) — no SQLite in
|
|
1271
|
+
* the recorder. The whole Zod-validated array round-trips on every read/write,
|
|
1272
|
+
* so no row is silently dropped on persist. Appends evict the oldest rows once
|
|
1273
|
+
* the ring exceeds its cap, so the blob stays bounded regardless of churn. A
|
|
1274
|
+
* corrupt blob reads back as EMPTY rather than crashing boot.
|
|
1275
|
+
*
|
|
1276
|
+
* Writes are BEST-EFFORT: `createRecordingOpsLogSink().append` never throws — a
|
|
1277
|
+
* failed audit write must not fail the operation it records.
|
|
1278
|
+
*/
|
|
1279
|
+
/** The addon-store key holding the bounded ops-log ring. */
|
|
1280
|
+
var RECORDING_OPS_LOG_KEY = "recordingOpsLog";
|
|
1281
|
+
/** Persisted shape: a flat array of ops-log rows (oldest-first on disk). */
|
|
1282
|
+
var RecordingOpsLogBlobSchema = require_dist.array(require_dist.OpsLogEntrySchema);
|
|
1283
|
+
/**
|
|
1284
|
+
* Append one row and persist, evicting the oldest rows so the ring never grows
|
|
1285
|
+
* past `maxEntries`. Validates at the boundary. Returns the stored row. Rows are
|
|
1286
|
+
* kept oldest-first on disk (the tail is newest); {@link listOpsLog} reverses.
|
|
1287
|
+
*/
|
|
1288
|
+
async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
1289
|
+
const validated = require_dist.OpsLogEntrySchema.parse(entry);
|
|
1290
|
+
const next = [...await state.get(), validated];
|
|
1291
|
+
const bounded = next.length > maxEntries ? next.slice(next.length - maxEntries) : next;
|
|
1292
|
+
await state.set(bounded);
|
|
1293
|
+
return validated;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* List ops-log rows newest-first, optionally scoped to one device, capped at
|
|
1297
|
+
* `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
1298
|
+
*/
|
|
1299
|
+
async function listOpsLog(state, query) {
|
|
1300
|
+
const rows = await state.get();
|
|
1301
|
+
return (query.deviceId === void 0 ? rows : rows.filter((r) => r.deviceId === query.deviceId)).toSorted((a, b) => b.at - a.at).slice(0, query.limit ?? 200);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Build the recorder's ops-log sink. `append` stamps `domain:'recording'`,
|
|
1305
|
+
* `nodeId`, `id` and `at`, then persists best-effort — a failed write is logged,
|
|
1306
|
+
* never thrown, so it cannot fail the operation being audited.
|
|
1307
|
+
*/
|
|
1308
|
+
function createRecordingOpsLogSink(deps) {
|
|
1309
|
+
return {
|
|
1310
|
+
append: async (input) => {
|
|
1311
|
+
const entry = {
|
|
1312
|
+
id: deps.newId(),
|
|
1313
|
+
at: deps.now(),
|
|
1314
|
+
domain: "recording",
|
|
1315
|
+
op: input.op,
|
|
1316
|
+
reason: input.reason,
|
|
1317
|
+
deviceId: input.deviceId,
|
|
1318
|
+
nodeId: deps.nodeId,
|
|
1319
|
+
itemsAffected: input.itemsAffected,
|
|
1320
|
+
bytesReclaimed: input.bytesReclaimed,
|
|
1321
|
+
detail: input.detail ?? null,
|
|
1322
|
+
actor: input.actor ?? "operator"
|
|
1323
|
+
};
|
|
1324
|
+
try {
|
|
1325
|
+
await appendOpsLog(deps.state, entry, deps.maxEntries);
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
deps.logger.warn("recorder: ops-log append failed (best-effort)", {
|
|
1328
|
+
tags: { deviceId: input.deviceId ?? void 0 },
|
|
1329
|
+
meta: {
|
|
1330
|
+
op: input.op,
|
|
1331
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
list: (query) => listOpsLog(deps.state, query)
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
//#endregion
|
|
1211
1340
|
//#region src/recorder/addon/export-ffmpeg-args.ts
|
|
1212
1341
|
/** Widest [min,max] speed a single ffmpeg `atempo` filter accepts. Outside this
|
|
1213
1342
|
* band audio is dropped rather than chained (kept simple + predictable). */
|
|
@@ -3582,12 +3711,20 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3582
3711
|
}),
|
|
3583
3712
|
brokerCall: makeBrokerCall(brokerHandle)
|
|
3584
3713
|
});
|
|
3714
|
+
const opsLog = createRecordingOpsLogSink({
|
|
3715
|
+
state: this.opsLogStore(),
|
|
3716
|
+
nodeId: this.nodeId,
|
|
3717
|
+
now: () => Date.now(),
|
|
3718
|
+
newId: () => (0, node_crypto.randomUUID)(),
|
|
3719
|
+
logger: this.ctx.logger.child("ops-log")
|
|
3720
|
+
});
|
|
3585
3721
|
const recordingDeps = {
|
|
3586
3722
|
index: this.index,
|
|
3587
3723
|
segmentStore: this.segmentStore,
|
|
3588
3724
|
nodeId: this.nodeId,
|
|
3589
3725
|
logger: this.ctx.logger,
|
|
3590
3726
|
configStore: this.configStore(),
|
|
3727
|
+
opsLog,
|
|
3591
3728
|
locations: () => this.resolvedLocations,
|
|
3592
3729
|
refreshLocations: () => this.refreshLocations(),
|
|
3593
3730
|
capacity: (root) => this.locationCapacity(root),
|
|
@@ -3926,6 +4063,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3926
4063
|
exportsStore() {
|
|
3927
4064
|
return this.state(RECORDING_EXPORTS_KEY, RecordingExportsBlobSchema, {});
|
|
3928
4065
|
}
|
|
4066
|
+
/** Durable handle over the bounded recordings ops-log ring. */
|
|
4067
|
+
opsLogStore() {
|
|
4068
|
+
return this.state(RECORDING_OPS_LOG_KEY, RecordingOpsLogBlobSchema, []);
|
|
4069
|
+
}
|
|
3929
4070
|
/** Retention deps shared by the sweep and the download-complete hook. */
|
|
3930
4071
|
exportJanitorDeps() {
|
|
3931
4072
|
return {
|