@camstack/addon-terminal 0.1.23 → 0.1.25

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 (3) hide show
  1. package/dist/addon.js +186 -27
  2. package/dist/addon.mjs +186 -27
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -31,7 +31,7 @@ let node_module = require("node:module");
31
31
  let sharp = require("sharp");
32
32
  sharp = __toESM(sharp);
33
33
  let node_http = require("node:http");
34
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
34
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
35
35
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
36
36
  EventCategory["SystemBoot"] = "system.boot";
37
37
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -48,9 +48,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
48
48
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
49
49
  /**
50
50
  * A newer addon or server-root package version was found by the
51
- * authoritative registry check. Emitted once per
52
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
53
- * polling of the same result is deduplicated by the checker.
51
+ * authoritative registry check. Emitted once when any observed
52
+ * `latestVersion` changes (or a package/node first appears behind);
53
+ * the payload carries the full currently-available list. Repeated
54
+ * polling of the same latests is silent.
54
55
  */
55
56
  EventCategory["UpdateAvailable"] = "update.available";
56
57
  /**
@@ -69,6 +70,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
69
70
  EventCategory["AddonInstalled"] = "addon.installed";
70
71
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
71
72
  EventCategory["AddonCrashed"] = "addon.crashed";
73
+ /**
74
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
75
+ *
76
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
77
+ * respawned"); this is the one that never resolves itself. Emitted exactly
78
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
79
+ * addons it hosted and the crash count that tripped the breaker.
80
+ *
81
+ * It exists because a runner marked terminally `failed` used to be SILENT:
82
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
83
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
84
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
85
+ * operator's phone told him. The Notification Center's system-event intake
86
+ * consumes this category and maps it to the `addon-crash-loop` kind.
87
+ */
88
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
72
89
  EventCategory["AddonError"] = "addon.error";
73
90
  EventCategory["AddonPageReady"] = "addon.page-ready";
74
91
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7647,6 +7664,10 @@ var RecordingBandSchema = object({
7647
7664
  preBufferSec: number().min(0).optional(),
7648
7665
  postBufferSec: number().min(0).optional()
7649
7666
  });
7667
+ ({
7668
+ preBufferSec: 10,
7669
+ postBufferSec: 30
7670
+ }).postBufferSec * 1e3;
7650
7671
  /**
7651
7672
  * Per-device retention overrides. Every field is optional; an unset or `0`
7652
7673
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7694,6 +7715,12 @@ var RecordingConfigSchema = object({
7694
7715
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7695
7716
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7696
7717
  mode: RecordingStorageModeSchema.optional(),
7718
+ /**
7719
+ * Which assigned broker slots to record. Absent / empty = {@link
7720
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7721
+ * camera's currently assigned slots — never `mid` unless the operator
7722
+ * picks it, and never a slot the broker has not assigned.
7723
+ */
7697
7724
  profiles: array(CamProfileSchema).optional(),
7698
7725
  segmentSeconds: number().int().positive().optional(),
7699
7726
  /**
@@ -7774,7 +7801,11 @@ var RelocateFootageInputSchema = object({
7774
7801
  profiles: array(string()).optional(),
7775
7802
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7776
7803
  * never allowed to starve live writers. */
7777
- throttleMbps: number().min(1).max(1e3).optional()
7804
+ throttleMbps: number().min(1).max(1e3).optional(),
7805
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7806
+ * pile. Used when a full drain is too expensive and the operator only
7807
+ * wants the recent window on the new disk. */
7808
+ sinceMs: number().int().optional()
7778
7809
  });
7779
7810
  /** Internal, lease-scoped participant operation. It is intentionally separate
7780
7811
  * from persistent recording settings: a migration never changes
@@ -14584,6 +14615,7 @@ var NcSystemEventKindSchema = _enum([
14584
14615
  "node-offline",
14585
14616
  "node-inference-unavailable",
14586
14617
  "detection-blind",
14618
+ "addon-crash-loop",
14587
14619
  "addon-update-available",
14588
14620
  "server-update-available",
14589
14621
  "alarm-triggered",
@@ -14591,6 +14623,9 @@ var NcSystemEventKindSchema = _enum([
14591
14623
  "alarm-disarmed",
14592
14624
  "alarm-arming",
14593
14625
  "alarm-arm-refused",
14626
+ "addon-updated",
14627
+ "server-updated",
14628
+ "export-completed",
14594
14629
  "camera-online",
14595
14630
  "camera-offline",
14596
14631
  "camera-disabled",
@@ -16484,13 +16519,19 @@ var RetrainStatusSchema = _enum([
16484
16519
  * "never marked" from "already trained" must read `retrainStatus`.
16485
16520
  *
16486
16521
  * `debug` does NOT pin; it is attention, not durability.
16522
+ *
16523
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16524
+ * A favourited track is skipped by retention the same way `staging` is, but
16525
+ * it does not enter `none|staging|trained` and has no staging budget.
16487
16526
  */
16488
16527
  var TrackFlagFields = {
16489
16528
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16490
16529
  * `'staging'`. */
16491
16530
  markForTrain: boolean().optional(),
16492
16531
  /** Operator marked this track for diagnostic attention. */
16493
- debug: boolean().optional()
16532
+ debug: boolean().optional(),
16533
+ /** Operator favourited this track. Pins it against pruning. */
16534
+ favourited: boolean().optional()
16494
16535
  };
16495
16536
  /**
16496
16537
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16515,6 +16556,7 @@ var TrackFlagsSchema = object({
16515
16556
  trackId: string(),
16516
16557
  markForTrain: boolean(),
16517
16558
  debug: boolean(),
16559
+ favourited: boolean(),
16518
16560
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16519
16561
  * a track row) because this shape is only ever produced by the write body,
16520
16562
  * which always knows it — and a surface that has just written needs to render
@@ -17147,6 +17189,12 @@ var TrackCascadeCountsSchema = object({
17147
17189
  /** Per-track CLIP search vectors removed (best-effort). */
17148
17190
  embeddings: number().int()
17149
17191
  });
17192
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17193
+ var DiskReconcileCountsSchema = object({
17194
+ mediaDropped: number().int(),
17195
+ tracks: number().int(),
17196
+ events: number().int()
17197
+ });
17150
17198
  /** Event-store footprint for one camera. */
17151
17199
  var EventStoreDeviceFootprintSchema = object({
17152
17200
  deviceId: number(),
@@ -17323,6 +17371,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17323
17371
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17324
17372
  kind: "mutation",
17325
17373
  auth: "admin"
17374
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17375
+ kind: "mutation",
17376
+ auth: "admin"
17326
17377
  }), method(object({
17327
17378
  deviceId: number(),
17328
17379
  trackIds: array(string()).min(1)
@@ -18873,6 +18924,24 @@ var CameraStatusDegradationSchema = object({
18873
18924
  *
18874
18925
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18875
18926
  */
18927
+ var DiskReconcileJobSchema = object({
18928
+ state: _enum([
18929
+ "idle",
18930
+ "running",
18931
+ "done",
18932
+ "error"
18933
+ ]),
18934
+ total: number().int().nonnegative(),
18935
+ completed: number().int().nonnegative(),
18936
+ currentDeviceId: number().int().nullable(),
18937
+ failed: array(number().int()).readonly(),
18938
+ mediaDropped: number().int().nonnegative(),
18939
+ tracks: number().int().nonnegative(),
18940
+ events: number().int().nonnegative(),
18941
+ startedAtMs: number().int().nullable(),
18942
+ finishedAtMs: number().int().nullable(),
18943
+ error: string().nullable()
18944
+ });
18876
18945
  var CameraStatusSchema = object({
18877
18946
  deviceId: number(),
18878
18947
  assignment: CameraAssignmentStatusSchema,
@@ -19104,7 +19173,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19104
19173
  }), CameraSwitchGroupSchema, {
19105
19174
  kind: "mutation",
19106
19175
  auth: "admin"
19107
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19176
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19177
+ kind: "mutation",
19178
+ auth: "admin"
19179
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19108
19180
  name: string(),
19109
19181
  description: string().optional(),
19110
19182
  config: CameraPipelineConfigSchema
@@ -20324,20 +20396,6 @@ var VectorStatsResultSchema = object({
20324
20396
  exact: boolean()
20325
20397
  });
20326
20398
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20327
- /**
20328
- * `videoclips` — the unified, navigable-clip surface for a camera.
20329
- *
20330
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20331
- * provider per device, substitutable. The DEFAULT provider (registered by
20332
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20333
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20334
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20335
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20336
- * wrapper provider for its device and serve its own clip catalog + URLs.
20337
- *
20338
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20339
- * temporal overlap, so the API never decides `continuous` vs `events`.
20340
- */
20341
20399
  var ClipSchema = object({
20342
20400
  /** Opaque, provider-namespaced id. The default provider encodes the time
20343
20401
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20354,8 +20412,65 @@ var ClipSchema = object({
20354
20412
  startMs: number(),
20355
20413
  endMs: number()
20356
20414
  }),
20357
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20358
- thumbnail: string().optional()
20415
+ /**
20416
+ * Distinct object classes attached to this visit (`person`, `car`, …),
20417
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
20418
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
20419
+ * did not. Absent (never empty) when the visit attached no classified object
20420
+ * event, so a motion/audio-only visit keeps its kind label.
20421
+ */
20422
+ labels: array(string()).max(3).optional(),
20423
+ /**
20424
+ * Lazy thumbnail URL, never inlined.
20425
+ *
20426
+ * Recording-derived clips (events-mode keep-window, and the prepared
20427
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20428
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20429
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20430
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20431
+ * mint their own stills.
20432
+ *
20433
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20434
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20435
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20436
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20437
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20438
+ * A read that FAILS drops the claim; it never invents it.
20439
+ */
20440
+ thumbnail: string().optional(),
20441
+ /**
20442
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20443
+ * can be decoded. Present whenever the visit came from recorded availability;
20444
+ * absent on a per-event padded window (there is no footage to promise).
20445
+ *
20446
+ * This is not a thumbnail and not a second byte path: it is the argument to
20447
+ * the recorder's existing still route. The surface — never the provider —
20448
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20449
+ * contiguous range, not of the visit: a visit spans its holes by
20450
+ * construction, so a naive midpoint lands in dead air.
20451
+ */
20452
+ stillAtMs: number().optional(),
20453
+ /**
20454
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20455
+ * first within kind (object → motion → audio), capped at
20456
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20457
+ *
20458
+ * Bounded because it is not a payload the surface pages through: one visit on
20459
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20460
+ * camera-day. Read {@link eventCount} for the true total.
20461
+ */
20462
+ eventIds: array(string()).optional(),
20463
+ /** How many analytics events actually overlap this visit. Differs from
20464
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20465
+ * list is never mistaken for a quiet visit. */
20466
+ eventCount: number().int().nonnegative().optional(),
20467
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20468
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20469
+ * bar keeps showing them via `recording.getAvailability`. */
20470
+ holes: array(object({
20471
+ startMs: number(),
20472
+ endMs: number()
20473
+ })).optional()
20359
20474
  });
20360
20475
  var ClipPlaybackSchema = object({
20361
20476
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20819,7 +20934,14 @@ var SearchResultSchema = object({
20819
20934
  });
20820
20935
  var AutoUpdateSettingsSchema = object({
20821
20936
  channel: ChannelSchema,
20822
- intervalSeconds: number()
20937
+ intervalSeconds: number(),
20938
+ /**
20939
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20940
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20941
+ * about a publish and installing it are different decisions. Clamped
20942
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20943
+ */
20944
+ updateCheckIntervalSeconds: number()
20823
20945
  });
20824
20946
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20825
20947
  var RestartAddonResultSchema = unknown();
@@ -20960,7 +21082,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20960
21082
  auth: "admin"
20961
21083
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20962
21084
  channel: ChannelSchema,
20963
- intervalSeconds: number().min(300).max(86400).optional()
21085
+ intervalSeconds: number().min(300).max(86400).optional(),
21086
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
21087
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20964
21088
  }), unknown(), {
20965
21089
  kind: "mutation",
20966
21090
  auth: "admin"
@@ -26641,7 +26765,9 @@ var ExportOptionsSchema = object({
26641
26765
  includeAudio: boolean(),
26642
26766
  maxLifeMs: number().int().positive(),
26643
26767
  deleteAfterDownload: boolean(),
26644
- title: string().max(200).optional()
26768
+ title: string().max(200).optional(),
26769
+ /** Notification-output target ids to ping when this export becomes ready. */
26770
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26645
26771
  }).superRefine((v, ctx) => {
26646
26772
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26647
26773
  code: ZodIssueCode.custom,
@@ -26700,10 +26826,18 @@ var ExportBytesSchema = object({
26700
26826
  });
26701
26827
  method(object({
26702
26828
  deviceId: number(),
26703
- profile: string(),
26829
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26830
+ profile: string().optional(),
26831
+ profiles: array(string()).min(1).optional(),
26704
26832
  fromMs: number(),
26705
26833
  toMs: number(),
26706
26834
  options: ExportOptionsSchema
26835
+ }).superRefine((v, ctx) => {
26836
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26837
+ code: ZodIssueCode.custom,
26838
+ message: "pass profiles[] (min 1) or legacy profile",
26839
+ path: ["profiles"]
26840
+ });
26707
26841
  }), ExportRecordSchema, {
26708
26842
  kind: "mutation",
26709
26843
  auth: "protected"
@@ -32970,6 +33104,12 @@ Object.freeze({
32970
33104
  addonId: null,
32971
33105
  access: "create"
32972
33106
  },
33107
+ "pipelineAnalytics.reconcileFromDisk": {
33108
+ capName: "pipeline-analytics",
33109
+ capScope: "device",
33110
+ addonId: null,
33111
+ access: "create"
33112
+ },
32973
33113
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32974
33114
  capName: "pipeline-analytics",
32975
33115
  capScope: "device",
@@ -33372,6 +33512,12 @@ Object.freeze({
33372
33512
  addonId: null,
33373
33513
  access: "view"
33374
33514
  },
33515
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33516
+ capName: "pipeline-orchestrator",
33517
+ capScope: "system",
33518
+ addonId: null,
33519
+ access: "view"
33520
+ },
33375
33521
  "pipelineOrchestrator.listAgentSettings": {
33376
33522
  capName: "pipeline-orchestrator",
33377
33523
  capScope: "system",
@@ -33396,6 +33542,12 @@ Object.freeze({
33396
33542
  addonId: null,
33397
33543
  access: "create"
33398
33544
  },
33545
+ "pipelineOrchestrator.reconcileFromDisk": {
33546
+ capName: "pipeline-orchestrator",
33547
+ capScope: "system",
33548
+ addonId: null,
33549
+ access: "create"
33550
+ },
33399
33551
  "pipelineOrchestrator.removeAgentSettings": {
33400
33552
  capName: "pipeline-orchestrator",
33401
33553
  capScope: "system",
@@ -36387,6 +36539,11 @@ Object.freeze({
36387
36539
  form: "single",
36388
36540
  optional: true
36389
36541
  }],
36542
+ "pipelineAnalytics.reconcileFromDisk": [{
36543
+ name: "deviceId",
36544
+ form: "single",
36545
+ optional: false
36546
+ }],
36390
36547
  "pipelineAnalytics.restageRetrainTrack": [{
36391
36548
  name: "deviceId",
36392
36549
  form: "single",
@@ -37452,6 +37609,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
37452
37609
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
37453
37610
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
37454
37611
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
37612
+ var MB = 1024 * 1024;
37613
+ 1024 * MB, 3072 * MB;
37455
37614
  //#endregion
37456
37615
  //#region src/curses-shim.ts
37457
37616
  /**
package/dist/addon.mjs CHANGED
@@ -8,7 +8,7 @@ import { createServer } from "node:http";
8
8
  //#region \0rolldown/runtime.js
9
9
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
10
10
  //#endregion
11
- //#region ../types/dist/event-category-Bxo5yJjt.mjs
11
+ //#region ../types/dist/event-category-XfKNtfCc.mjs
12
12
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
13
13
  EventCategory["SystemBoot"] = "system.boot";
14
14
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -25,9 +25,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
25
25
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
26
26
  /**
27
27
  * A newer addon or server-root package version was found by the
28
- * authoritative registry check. Emitted once per
29
- * `(target, packageName, currentVersion, latestVersion)` transition; repeated
30
- * polling of the same result is deduplicated by the checker.
28
+ * authoritative registry check. Emitted once when any observed
29
+ * `latestVersion` changes (or a package/node first appears behind);
30
+ * the payload carries the full currently-available list. Repeated
31
+ * polling of the same latests is silent.
31
32
  */
32
33
  EventCategory["UpdateAvailable"] = "update.available";
33
34
  /**
@@ -46,6 +47,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
46
47
  EventCategory["AddonInstalled"] = "addon.installed";
47
48
  EventCategory["AddonUninstalled"] = "addon.uninstalled";
48
49
  EventCategory["AddonCrashed"] = "addon.crashed";
50
+ /**
51
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
52
+ *
53
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
54
+ * respawned"); this is the one that never resolves itself. Emitted exactly
55
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
56
+ * addons it hosted and the crash count that tripped the breaker.
57
+ *
58
+ * It exists because a runner marked terminally `failed` used to be SILENT:
59
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
60
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
61
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
62
+ * operator's phone told him. The Notification Center's system-event intake
63
+ * consumes this category and maps it to the `addon-crash-loop` kind.
64
+ */
65
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
49
66
  EventCategory["AddonError"] = "addon.error";
50
67
  EventCategory["AddonPageReady"] = "addon.page-ready";
51
68
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -7624,6 +7641,10 @@ var RecordingBandSchema = object({
7624
7641
  preBufferSec: number().min(0).optional(),
7625
7642
  postBufferSec: number().min(0).optional()
7626
7643
  });
7644
+ ({
7645
+ preBufferSec: 10,
7646
+ postBufferSec: 30
7647
+ }).postBufferSec * 1e3;
7627
7648
  /**
7628
7649
  * Per-device retention overrides. Every field is optional; an unset or `0`
7629
7650
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -7671,6 +7692,12 @@ var RecordingConfigSchema = object({
7671
7692
  /** DERIVED summary of `bands`, stamped by the recorder on every save.
7672
7693
  * Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
7673
7694
  mode: RecordingStorageModeSchema.optional(),
7695
+ /**
7696
+ * Which assigned broker slots to record. Absent / empty = {@link
7697
+ * DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
7698
+ * camera's currently assigned slots — never `mid` unless the operator
7699
+ * picks it, and never a slot the broker has not assigned.
7700
+ */
7674
7701
  profiles: array(CamProfileSchema).optional(),
7675
7702
  segmentSeconds: number().int().positive().optional(),
7676
7703
  /**
@@ -7751,7 +7778,11 @@ var RelocateFootageInputSchema = object({
7751
7778
  profiles: array(string()).optional(),
7752
7779
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7753
7780
  * never allowed to starve live writers. */
7754
- throttleMbps: number().min(1).max(1e3).optional()
7781
+ throttleMbps: number().min(1).max(1e3).optional(),
7782
+ /** Move only segments whose startMs is >= this. Absent = the whole source
7783
+ * pile. Used when a full drain is too expensive and the operator only
7784
+ * wants the recent window on the new disk. */
7785
+ sinceMs: number().int().optional()
7755
7786
  });
7756
7787
  /** Internal, lease-scoped participant operation. It is intentionally separate
7757
7788
  * from persistent recording settings: a migration never changes
@@ -14561,6 +14592,7 @@ var NcSystemEventKindSchema = _enum([
14561
14592
  "node-offline",
14562
14593
  "node-inference-unavailable",
14563
14594
  "detection-blind",
14595
+ "addon-crash-loop",
14564
14596
  "addon-update-available",
14565
14597
  "server-update-available",
14566
14598
  "alarm-triggered",
@@ -14568,6 +14600,9 @@ var NcSystemEventKindSchema = _enum([
14568
14600
  "alarm-disarmed",
14569
14601
  "alarm-arming",
14570
14602
  "alarm-arm-refused",
14603
+ "addon-updated",
14604
+ "server-updated",
14605
+ "export-completed",
14571
14606
  "camera-online",
14572
14607
  "camera-offline",
14573
14608
  "camera-disabled",
@@ -16461,13 +16496,19 @@ var RetrainStatusSchema = _enum([
16461
16496
  * "never marked" from "already trained" must read `retrainStatus`.
16462
16497
  *
16463
16498
  * `debug` does NOT pin; it is attention, not durability.
16499
+ *
16500
+ * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
16501
+ * A favourited track is skipped by retention the same way `staging` is, but
16502
+ * it does not enter `none|staging|trained` and has no staging budget.
16464
16503
  */
16465
16504
  var TrackFlagFields = {
16466
16505
  /** Operator marked this track as training material — i.e. `retrainStatus` is
16467
16506
  * `'staging'`. */
16468
16507
  markForTrain: boolean().optional(),
16469
16508
  /** Operator marked this track for diagnostic attention. */
16470
- debug: boolean().optional()
16509
+ debug: boolean().optional(),
16510
+ /** Operator favourited this track. Pins it against pruning. */
16511
+ favourited: boolean().optional()
16471
16512
  };
16472
16513
  /**
16473
16514
  * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
@@ -16492,6 +16533,7 @@ var TrackFlagsSchema = object({
16492
16533
  trackId: string(),
16493
16534
  markForTrain: boolean(),
16494
16535
  debug: boolean(),
16536
+ favourited: boolean(),
16495
16537
  /** The lifecycle state the boolean was derived from. Required here (unlike on
16496
16538
  * a track row) because this shape is only ever produced by the write body,
16497
16539
  * which always knows it — and a surface that has just written needs to render
@@ -17124,6 +17166,12 @@ var TrackCascadeCountsSchema = object({
17124
17166
  /** Per-track CLIP search vectors removed (best-effort). */
17125
17167
  embeddings: number().int()
17126
17168
  });
17169
+ /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17170
+ var DiskReconcileCountsSchema = object({
17171
+ mediaDropped: number().int(),
17172
+ tracks: number().int(),
17173
+ events: number().int()
17174
+ });
17127
17175
  /** Event-store footprint for one camera. */
17128
17176
  var EventStoreDeviceFootprintSchema = object({
17129
17177
  deviceId: number(),
@@ -17300,6 +17348,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17300
17348
  }), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
17301
17349
  kind: "mutation",
17302
17350
  auth: "admin"
17351
+ }), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
17352
+ kind: "mutation",
17353
+ auth: "admin"
17303
17354
  }), method(object({
17304
17355
  deviceId: number(),
17305
17356
  trackIds: array(string()).min(1)
@@ -18850,6 +18901,24 @@ var CameraStatusDegradationSchema = object({
18850
18901
  *
18851
18902
  * See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
18852
18903
  */
18904
+ var DiskReconcileJobSchema = object({
18905
+ state: _enum([
18906
+ "idle",
18907
+ "running",
18908
+ "done",
18909
+ "error"
18910
+ ]),
18911
+ total: number().int().nonnegative(),
18912
+ completed: number().int().nonnegative(),
18913
+ currentDeviceId: number().int().nullable(),
18914
+ failed: array(number().int()).readonly(),
18915
+ mediaDropped: number().int().nonnegative(),
18916
+ tracks: number().int().nonnegative(),
18917
+ events: number().int().nonnegative(),
18918
+ startedAtMs: number().int().nullable(),
18919
+ finishedAtMs: number().int().nullable(),
18920
+ error: string().nullable()
18921
+ });
18853
18922
  var CameraStatusSchema = object({
18854
18923
  deviceId: number(),
18855
18924
  assignment: CameraAssignmentStatusSchema,
@@ -19081,7 +19150,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19081
19150
  }), CameraSwitchGroupSchema, {
19082
19151
  kind: "mutation",
19083
19152
  auth: "admin"
19084
- }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19153
+ }), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
19154
+ kind: "mutation",
19155
+ auth: "admin"
19156
+ }), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
19085
19157
  name: string(),
19086
19158
  description: string().optional(),
19087
19159
  config: CameraPipelineConfigSchema
@@ -20301,20 +20373,6 @@ var VectorStatsResultSchema = object({
20301
20373
  exact: boolean()
20302
20374
  });
20303
20375
  method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
20304
- /**
20305
- * `videoclips` — the unified, navigable-clip surface for a camera.
20306
- *
20307
- * A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
20308
- * provider per device, substitutable. The DEFAULT provider (registered by
20309
- * `addon-post-analysis`, `defaultActive: true`) composes the analytics event
20310
- * log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
20311
- * is a time-WINDOW over existing footage, never a separate file. A camera that
20312
- * exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
20313
- * wrapper provider for its device and serve its own clip catalog + URLs.
20314
- *
20315
- * A `Clip` is purely time-based (subtree-blind): playback resolves segments by
20316
- * temporal overlap, so the API never decides `continuous` vs `events`.
20317
- */
20318
20376
  var ClipSchema = object({
20319
20377
  /** Opaque, provider-namespaced id. The default provider encodes the time
20320
20378
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -20331,8 +20389,65 @@ var ClipSchema = object({
20331
20389
  startMs: number(),
20332
20390
  endMs: number()
20333
20391
  }),
20334
- /** Thumbnail URL (lazy; e.g. analytics `getEventMedia`). Never inlined. */
20335
- thumbnail: string().optional()
20392
+ /**
20393
+ * Distinct object classes attached to this visit (`person`, `car`, …),
20394
+ * dominant first, capped at {@link MAX_CLIP_LABELS}. The ribbon renders these
20395
+ * instead of the bare kind — "Object" tells the operator nothing a colour bar
20396
+ * did not. Absent (never empty) when the visit attached no classified object
20397
+ * event, so a motion/audio-only visit keeps its kind label.
20398
+ */
20399
+ labels: array(string()).max(3).optional(),
20400
+ /**
20401
+ * Lazy thumbnail URL, never inlined.
20402
+ *
20403
+ * Recording-derived clips (events-mode keep-window, and the prepared
20404
+ * continuous event+fragment visit) MUST use the snapshot of the **main
20405
+ * event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
20406
+ * of the event that owns `kind` (object > motion > audio). Do not extract
20407
+ * a keyframe from the recorded segments. Other providers (onboard, HKSV)
20408
+ * mint their own stills.
20409
+ *
20410
+ * **VOUCHED, never fabricated.** A provider emits this only for an event it
20411
+ * has CONFIRMED owns at least one media row (its own, or its owning track's).
20412
+ * Absent is meaningful — "this visit has no event still" — never "we did not
20413
+ * look". Stamping it from a URL template made 35% of one camera's clips point
20414
+ * at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
20415
+ * A read that FAILS drops the claim; it never invents it.
20416
+ */
20417
+ thumbnail: string().optional(),
20418
+ /**
20419
+ * An instant INSIDE this visit's footage, hole-safe, where a recorded still
20420
+ * can be decoded. Present whenever the visit came from recorded availability;
20421
+ * absent on a per-event padded window (there is no footage to promise).
20422
+ *
20423
+ * This is not a thumbnail and not a second byte path: it is the argument to
20424
+ * the recorder's existing still route. The surface — never the provider —
20425
+ * decides whether to use it. It is the midpoint of the visit's LONGEST
20426
+ * contiguous range, not of the visit: a visit spans its holes by
20427
+ * construction, so a naive midpoint lands in dead air.
20428
+ */
20429
+ stillAtMs: number().optional(),
20430
+ /**
20431
+ * Analytics event ids that overlap this visit — a BOUNDED sample, newest
20432
+ * first within kind (object → motion → audio), capped at
20433
+ * {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
20434
+ *
20435
+ * Bounded because it is not a payload the surface pages through: one visit on
20436
+ * device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
20437
+ * camera-day. Read {@link eventCount} for the true total.
20438
+ */
20439
+ eventIds: array(string()).optional(),
20440
+ /** How many analytics events actually overlap this visit. Differs from
20441
+ * `eventIds.length` exactly when the sample was capped — so a truncated
20442
+ * list is never mistaken for a quiet visit. */
20443
+ eventCount: number().int().nonnegative().optional(),
20444
+ /** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
20445
+ * than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
20446
+ * bar keeps showing them via `recording.getAvailability`. */
20447
+ holes: array(object({
20448
+ startMs: number(),
20449
+ endMs: number()
20450
+ })).optional()
20336
20451
  });
20337
20452
  var ClipPlaybackSchema = object({
20338
20453
  /** HLS master URL through the hub data-plane (Range + token in path). */
@@ -20796,7 +20911,14 @@ var SearchResultSchema = object({
20796
20911
  });
20797
20912
  var AutoUpdateSettingsSchema = object({
20798
20913
  channel: ChannelSchema,
20799
- intervalSeconds: number()
20914
+ intervalSeconds: number(),
20915
+ /**
20916
+ * Cadence of the "an update exists" POLLER, in seconds. Independent of
20917
+ * `channel`: the poller runs while auto-apply is `off`, because being told
20918
+ * about a publish and installing it are different decisions. Clamped
20919
+ * server-side to 900 s … 604800 s; defaults to 21600 s (6 h).
20920
+ */
20921
+ updateCheckIntervalSeconds: number()
20800
20922
  });
20801
20923
  var AddonAutoUpdateSchema = ChannelWithInheritSchema;
20802
20924
  var RestartAddonResultSchema = unknown();
@@ -20937,7 +21059,9 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
20937
21059
  auth: "admin"
20938
21060
  }), method(_void(), AutoUpdateSettingsSchema, { auth: "admin" }), method(object({
20939
21061
  channel: ChannelSchema,
20940
- intervalSeconds: number().min(300).max(86400).optional()
21062
+ intervalSeconds: number().min(300).max(86400).optional(),
21063
+ /** Availability-poll cadence; see AutoUpdateSettingsSchema. */
21064
+ updateCheckIntervalSeconds: number().min(900).max(604800).optional()
20941
21065
  }), unknown(), {
20942
21066
  kind: "mutation",
20943
21067
  auth: "admin"
@@ -26618,7 +26742,9 @@ var ExportOptionsSchema = object({
26618
26742
  includeAudio: boolean(),
26619
26743
  maxLifeMs: number().int().positive(),
26620
26744
  deleteAfterDownload: boolean(),
26621
- title: string().max(200).optional()
26745
+ title: string().max(200).optional(),
26746
+ /** Notification-output target ids to ping when this export becomes ready. */
26747
+ notifyTargetIds: array(string().min(1)).max(20).optional()
26622
26748
  }).superRefine((v, ctx) => {
26623
26749
  if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
26624
26750
  code: ZodIssueCode.custom,
@@ -26677,10 +26803,18 @@ var ExportBytesSchema = object({
26677
26803
  });
26678
26804
  method(object({
26679
26805
  deviceId: number(),
26680
- profile: string(),
26806
+ /** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
26807
+ profile: string().optional(),
26808
+ profiles: array(string()).min(1).optional(),
26681
26809
  fromMs: number(),
26682
26810
  toMs: number(),
26683
26811
  options: ExportOptionsSchema
26812
+ }).superRefine((v, ctx) => {
26813
+ if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
26814
+ code: ZodIssueCode.custom,
26815
+ message: "pass profiles[] (min 1) or legacy profile",
26816
+ path: ["profiles"]
26817
+ });
26684
26818
  }), ExportRecordSchema, {
26685
26819
  kind: "mutation",
26686
26820
  auth: "protected"
@@ -32947,6 +33081,12 @@ Object.freeze({
32947
33081
  addonId: null,
32948
33082
  access: "create"
32949
33083
  },
33084
+ "pipelineAnalytics.reconcileFromDisk": {
33085
+ capName: "pipeline-analytics",
33086
+ capScope: "device",
33087
+ addonId: null,
33088
+ access: "create"
33089
+ },
32950
33090
  "pipelineAnalytics.refreshStorageLocationsForMigration": {
32951
33091
  capName: "pipeline-analytics",
32952
33092
  capScope: "device",
@@ -33349,6 +33489,12 @@ Object.freeze({
33349
33489
  addonId: null,
33350
33490
  access: "view"
33351
33491
  },
33492
+ "pipelineOrchestrator.getReconcileFromDiskStatus": {
33493
+ capName: "pipeline-orchestrator",
33494
+ capScope: "system",
33495
+ addonId: null,
33496
+ access: "view"
33497
+ },
33352
33498
  "pipelineOrchestrator.listAgentSettings": {
33353
33499
  capName: "pipeline-orchestrator",
33354
33500
  capScope: "system",
@@ -33373,6 +33519,12 @@ Object.freeze({
33373
33519
  addonId: null,
33374
33520
  access: "create"
33375
33521
  },
33522
+ "pipelineOrchestrator.reconcileFromDisk": {
33523
+ capName: "pipeline-orchestrator",
33524
+ capScope: "system",
33525
+ addonId: null,
33526
+ access: "create"
33527
+ },
33376
33528
  "pipelineOrchestrator.removeAgentSettings": {
33377
33529
  capName: "pipeline-orchestrator",
33378
33530
  capScope: "system",
@@ -36364,6 +36516,11 @@ Object.freeze({
36364
36516
  form: "single",
36365
36517
  optional: true
36366
36518
  }],
36519
+ "pipelineAnalytics.reconcileFromDisk": [{
36520
+ name: "deviceId",
36521
+ form: "single",
36522
+ optional: false
36523
+ }],
36367
36524
  "pipelineAnalytics.restageRetrainTrack": [{
36368
36525
  name: "deviceId",
36369
36526
  form: "single",
@@ -37429,6 +37586,8 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
37429
37586
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
37430
37587
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
37431
37588
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
37589
+ var MB = 1024 * 1024;
37590
+ 1024 * MB, 3072 * MB;
37432
37591
  //#endregion
37433
37592
  //#region src/curses-shim.ts
37434
37593
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",