@camstack/types 1.2.428 → 1.2.429

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/CHANGELOG.md CHANGED
@@ -7,11 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.429] - 2026-10-27
11
+
12
+ ### Added
13
+
14
+ - pet-feeder reports bowlFood, eating and supportsMarkRefilled
15
+
10
16
  ## [1.2.428] - 2026-10-27
11
17
 
12
18
  ### Added
13
19
 
14
20
  - The published package now includes its README and CHANGELOG.
21
+ - Cloud integrations raise one alert when their session is lost and clear it when it is back
15
22
 
16
23
  ## [1.2.427] - 2026-10-27
17
24
 
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Session health — the STANDARD "cloud connection lost" alert (D-session-health).
3
+ *
4
+ * Every addon that keeps a cloud session for an integration (an account login, a
5
+ * refresh token, a long-lived socket) reports two facts and nothing else:
6
+ *
7
+ * - `lost(integrationId, …)` the session was refused / kicked / dropped;
8
+ * - `restored(integrationId)` a call went through again.
9
+ *
10
+ * The tracker owns everything between: a debounce (a transient reconnect never
11
+ * alerts), ONE alert per integration instance (never one per device, never one
12
+ * per failed poll), the wording (provider, integration, reason), an action the
13
+ * operator can follow, and the automatic resolve when the session is back.
14
+ *
15
+ * Alerts are durable state that the next run of the addon cannot see, so the
16
+ * first `restored()` of a process resolves whatever a previous run left active.
17
+ */
18
+ import type { Alert, AlertSeverity } from '../capabilities/alerts.cap.js';
19
+ import type { IScopedLogger } from '../interfaces/logging.js';
20
+ /** Why the session ended. `kicked` = the same account signed in elsewhere. */
21
+ export type SessionLossReason = 'kicked' | 'auth-rejected' | 'token-expired' | 'connection-lost';
22
+ /** What an alert's action looks like (the `Alert.action` shape). */
23
+ export interface SessionAlertAction {
24
+ readonly label: string;
25
+ readonly href?: string;
26
+ readonly navigation?: 'app' | 'server';
27
+ }
28
+ /** The slice of `ctx.api.alerts` the tracker needs. Structural, so `ctx.api.alerts` fits. */
29
+ export interface SessionHealthAlertsPort {
30
+ readonly emit?: {
31
+ mutate(alert: Alert): Promise<unknown>;
32
+ };
33
+ readonly update?: {
34
+ mutate(input: {
35
+ readonly alertId: string;
36
+ readonly patch: Partial<Alert>;
37
+ }): Promise<unknown>;
38
+ };
39
+ }
40
+ export declare const SESSION_LOST_ALERT_CATEGORY = "integration.session-lost";
41
+ /** A transient reconnect is quiet for this long before it is worth an alert. */
42
+ export declare const DEFAULT_SESSION_LOST_DEBOUNCE_MS: number;
43
+ export declare function sessionLostAlertId(kind: string, integrationId: string): string;
44
+ export interface BuildSessionLostAlertInput {
45
+ /** Stable provider key, e.g. `petkit` — part of the alert id and source. */
46
+ readonly kind: string;
47
+ /** Display name, e.g. `PetKit`. */
48
+ readonly providerName: string;
49
+ readonly integrationId: string;
50
+ readonly integrationName: string;
51
+ readonly reason: SessionLossReason;
52
+ /** Defaults to "Open integration" (the admin integration page). */
53
+ readonly action?: SessionAlertAction;
54
+ readonly severity?: AlertSeverity;
55
+ readonly now?: number;
56
+ }
57
+ /** Pure builder, exported so an addon with its own flow can still emit the standard shape. */
58
+ export declare function buildSessionLostAlert(input: BuildSessionLostAlertInput): Alert;
59
+ export interface SessionHealthTrackerOptions {
60
+ readonly kind: string;
61
+ readonly providerName: string;
62
+ readonly logger: IScopedLogger;
63
+ /** Read lazily: `ctx.api.alerts` may be absent early in boot. */
64
+ readonly getAlerts: () => SessionHealthAlertsPort | undefined;
65
+ readonly debounceMs?: number;
66
+ }
67
+ export interface SessionLostInfo {
68
+ readonly integrationName: string;
69
+ readonly reason: SessionLossReason;
70
+ /** Re-auth route when the addon has one; otherwise "Open integration". */
71
+ readonly action?: SessionAlertAction;
72
+ }
73
+ export declare class SessionHealthTracker {
74
+ #private;
75
+ constructor(opts: SessionHealthTrackerOptions);
76
+ /** The session is gone. Idempotent: call it on every failed poll. */
77
+ lost(integrationId: string, info: SessionLostInfo): void;
78
+ /** A call went through. Idempotent: call it on every successful poll. */
79
+ restored(integrationId: string): Promise<void>;
80
+ /** The integration is gone (deleted / disabled): drop its state and its alert. */
81
+ forget(integrationId: string): Promise<void>;
82
+ /** Stop every pending timer (addon shutdown). */
83
+ dispose(): void;
84
+ }
85
+ export interface IntegrationSessionBinding {
86
+ readonly logger: IScopedLogger;
87
+ readonly getAlerts: () => SessionHealthAlertsPort | undefined;
88
+ readonly debounceMs?: number;
89
+ }
90
+ export interface IntegrationSessionReporterOptions {
91
+ readonly kind: string;
92
+ readonly providerName: string;
93
+ /**
94
+ * Is `err` the cloud ending OUR session (kicked, token refused, sign-in
95
+ * rejected)? `null` for anything else — a network blip or a per-device error
96
+ * must never read as a lost session.
97
+ */
98
+ readonly classify: (err: unknown) => SessionLossReason | null;
99
+ /** Re-auth route when the addon has one; otherwise "Open integration". */
100
+ readonly action?: (integrationId: string) => SessionAlertAction | undefined;
101
+ }
102
+ /**
103
+ * The addon-facing face of {@link SessionHealthTracker}: a process-wide object an
104
+ * integration manager AND its device classes can reach (the kernel builds a
105
+ * device from a `DeviceContext` alone, so neither can be handed a tracker).
106
+ * The addon `bind()`s it in `onInitialize`, `unbind()`s it on shutdown and
107
+ * `forget()`s an integration that vanished; everything before `bind` is a no-op.
108
+ */
109
+ export declare class IntegrationSessionReporter {
110
+ #private;
111
+ constructor(opts: IntegrationSessionReporterOptions);
112
+ bind(binding: IntegrationSessionBinding): void;
113
+ unbind(): void;
114
+ /** The manager tells us the display name once, so a device can report by id alone. */
115
+ register(integrationId: string, integrationName: string): void;
116
+ /** A call failed. Only a session-ending failure counts; anything else is ignored. */
117
+ failure(integrationId: string, err: unknown): void;
118
+ /** A call went through. */
119
+ ok(integrationId: string): Promise<void>;
120
+ /** The integration was removed. */
121
+ forget(integrationId: string): Promise<void>;
122
+ }
123
+ /** What a manager / device depends on to report. */
124
+ export type IntegrationSessionSink = Pick<IntegrationSessionReporter, 'register' | 'failure' | 'ok'>;
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-edKiOXLo.js");
2
+ const require_sleep = require("./sleep-hRhTSDlN.js");
3
3
  const require_event_category = require("./event-category-Ave2VSxY.js");
4
4
  const require_err_msg = require("./err-msg-U7lLUMNz.js");
5
5
  //#region src/generated/cap-input-defaults.ts
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as expandCapMethods, $n as REVALIDATE_AFTER_MS, $r as isSecretMarker, $t as RecordFieldPathSchema, A as HF_REPO, An as RecordUniqueViolationError, Ar as buildSecretRef, B as withoutCallerStamp, Bn as RECORD_CLASSES, C as addonSliceHandleApi, Cn as InvalidRecordError, Dn as RecordNotFoundError, En as RECORDS_ERROR_CODES, Fn as UnknownRecordKindError, Fr as parseSecretRef, G as adminUiCapability, Gn as RECORD_MAX_DATA_BYTES, Hn as RECORD_DEFAULT_MAX_DATA_BYTES, I as deviceOpsCapability, In as asRecordsError, Jn as RECORD_QUERY_MAX, Jt as defineEntryKind, Kn as RECORD_MAX_INDEXES, L as viewerUiCapability, Ln as recordsErrorFromMessage, M as CAP_ADVANCE_FIELDS, Mn as RevConflictError, Mr as globalSecretScope, Nn as TransactRefusedError, Nr as isSecretRef, On as RecordSecretUnavailableError, Or as SECRET_REF_SCHEME, P as CAP_STATE_LEAVES, Pn as UnindexedFilterError, Pr as normalizeSecretOwner, Qn as resolveSecretViewsIn, Qr as isSecretFieldView, Qt as EntryDescriptorSchema, R as DEVICE_CHILDREN_BATCH_MAX, Rn as RECORD_BATCH_MAX, Rr as createEvent, Sn as ForeignSecretMarkerError, Tn as PlaintextSecretError, U as describeCustomActions, Un as RECORD_DELETE_BATCH, Vn as RECORD_DATABASES, Wn as RECORD_ID_MAX_LENGTH, Xn as RECORD_SETTINGS_MAX_ROWS, Xr as classifySecretWrite, Xt as createRecordsAccess, Yn as RECORD_SECRET_COLLECTION_PREFIX, Yr as SECRET_CLEAR, Yt as entryBody, Zn as RECORD_SWEEP_INTERVAL_MS, Zr as isSecretClear, Zt as assertTransactScope, _ as readErrorI18n, _n as collectZodSecretPaths, an as assertValidDeclaration, ar as registerKnownSecret, b as isDeviceScopedCap, bn as DeclarationRefusedError, c as asString, cn as REMOVE_LEAF, cr as PEER_BYTES_CHUNK_BYTES, ct as readNodePin, d as parseJsonUnknown, dn as parseSecretPath, dr as PEER_BYTES_TICKET_TTL_MS, dt as ReadinessRegistry, ei as isSecretMarkerLike, en as RecordIndexSchema, er as createSecretsAccess, f as hfModelUrl, fn as recordArrayItemPath, ft as ReadinessTimeoutError, g as ErrorI18nPayloadSchema, gr as BaseAddon, hn as secretArrayPrefixes, hr as DisposerChain, ht as scopeKey, in as StoredRecordKindDeclarationSchema, ir as knownSecretCount, jn as RecordsError, jr as formatSecretScope, kn as RecordTooLargeError, kr as arrayItemFieldPath, ln as collectSecretLeaves, lr as PEER_BYTES_MAX_OUTSTANDING, mn as recordSecretRef, ni as toSecretFieldView, nn as RecordKindNameSchema, nr as clearKnownSecrets, o as asJsonObject, on as declarationsOf, or as scrubKnownSecrets, p as BOOT_RECOVERY_BACKOFF_MS, pn as recordSecretCollection, pr as DATAPLANE_SECRET_HEADER, qn as RECORD_MAX_INDEX_FIELDS, qt as makeInMemoryRecords, r as maskUrlCredentials, rn as RecordRetentionSchema, rr as forgetKnownSecret, sn as defineRecordKinds, sr as shortSecretRefHash, st as nodePin, t as sleep, ti as makeSecretMarker, tn as RecordKindDeclarationSchema, tr as MIN_SCRUBBED_LENGTH, un as mapSecretLeaves, ur as PEER_BYTES_STALL_MS, ut as MODEL_FORMATS, vn as isZodSecret, vr as normalizeAddonInitResult, w as createEventBusSliceSource, wn as NoCallerError, x as createDeviceProxy, xn as FilterRequiredError, y as DEVICE_SCOPED_CAPS, yn as BatchTooLargeError, z as CALLER_STAMPED_CAPABILITIES, zn as RECORD_BATCH_MAX_BYTES, zr as emitReadiness } from "./sleep--jXS9hWo.mjs";
1
+ import { $ as expandCapMethods, $n as REVALIDATE_AFTER_MS, $r as isSecretMarker, $t as RecordFieldPathSchema, A as HF_REPO, An as RecordUniqueViolationError, Ar as buildSecretRef, B as withoutCallerStamp, Bn as RECORD_CLASSES, C as addonSliceHandleApi, Cn as InvalidRecordError, Dn as RecordNotFoundError, En as RECORDS_ERROR_CODES, Fn as UnknownRecordKindError, Fr as parseSecretRef, G as adminUiCapability, Gn as RECORD_MAX_DATA_BYTES, Hn as RECORD_DEFAULT_MAX_DATA_BYTES, I as deviceOpsCapability, In as asRecordsError, Jn as RECORD_QUERY_MAX, Jt as defineEntryKind, Kn as RECORD_MAX_INDEXES, L as viewerUiCapability, Ln as recordsErrorFromMessage, M as CAP_ADVANCE_FIELDS, Mn as RevConflictError, Mr as globalSecretScope, Nn as TransactRefusedError, Nr as isSecretRef, On as RecordSecretUnavailableError, Or as SECRET_REF_SCHEME, P as CAP_STATE_LEAVES, Pn as UnindexedFilterError, Pr as normalizeSecretOwner, Qn as resolveSecretViewsIn, Qr as isSecretFieldView, Qt as EntryDescriptorSchema, R as DEVICE_CHILDREN_BATCH_MAX, Rn as RECORD_BATCH_MAX, Rr as createEvent, Sn as ForeignSecretMarkerError, Tn as PlaintextSecretError, U as describeCustomActions, Un as RECORD_DELETE_BATCH, Vn as RECORD_DATABASES, Wn as RECORD_ID_MAX_LENGTH, Xn as RECORD_SETTINGS_MAX_ROWS, Xr as classifySecretWrite, Xt as createRecordsAccess, Yn as RECORD_SECRET_COLLECTION_PREFIX, Yr as SECRET_CLEAR, Yt as entryBody, Zn as RECORD_SWEEP_INTERVAL_MS, Zr as isSecretClear, Zt as assertTransactScope, _ as readErrorI18n, _n as collectZodSecretPaths, an as assertValidDeclaration, ar as registerKnownSecret, b as isDeviceScopedCap, bn as DeclarationRefusedError, c as asString, cn as REMOVE_LEAF, cr as PEER_BYTES_CHUNK_BYTES, ct as readNodePin, d as parseJsonUnknown, dn as parseSecretPath, dr as PEER_BYTES_TICKET_TTL_MS, dt as ReadinessRegistry, ei as isSecretMarkerLike, en as RecordIndexSchema, er as createSecretsAccess, f as hfModelUrl, fn as recordArrayItemPath, ft as ReadinessTimeoutError, g as ErrorI18nPayloadSchema, gr as BaseAddon, hn as secretArrayPrefixes, hr as DisposerChain, ht as scopeKey, in as StoredRecordKindDeclarationSchema, ir as knownSecretCount, jn as RecordsError, jr as formatSecretScope, kn as RecordTooLargeError, kr as arrayItemFieldPath, ln as collectSecretLeaves, lr as PEER_BYTES_MAX_OUTSTANDING, mn as recordSecretRef, ni as toSecretFieldView, nn as RecordKindNameSchema, nr as clearKnownSecrets, o as asJsonObject, on as declarationsOf, or as scrubKnownSecrets, p as BOOT_RECOVERY_BACKOFF_MS, pn as recordSecretCollection, pr as DATAPLANE_SECRET_HEADER, qn as RECORD_MAX_INDEX_FIELDS, qt as makeInMemoryRecords, r as maskUrlCredentials, rn as RecordRetentionSchema, rr as forgetKnownSecret, sn as defineRecordKinds, sr as shortSecretRefHash, st as nodePin, t as sleep, ti as makeSecretMarker, tn as RecordKindDeclarationSchema, tr as MIN_SCRUBBED_LENGTH, un as mapSecretLeaves, ur as PEER_BYTES_STALL_MS, ut as MODEL_FORMATS, vn as isZodSecret, vr as normalizeAddonInitResult, w as createEventBusSliceSource, wn as NoCallerError, x as createDeviceProxy, xn as FilterRequiredError, y as DEVICE_SCOPED_CAPS, yn as BatchTooLargeError, z as CALLER_STAMPED_CAPABILITIES, zn as RECORD_BATCH_MAX_BYTES, zr as emitReadiness } from "./sleep-BE7IAR2p.mjs";
2
2
  import { t as EventCategory } from "./event-category-CT-hz5Rb.mjs";
3
3
  import { a as DeviceType, t as errMsg } from "./err-msg-DIorGYeC.mjs";
4
4
  //#region src/generated/cap-input-defaults.ts
@@ -46,6 +46,9 @@ export declare const PetFeederDeviceStatusSchema: z.ZodEnum<{
46
46
  export type PetFeederDeviceStatus = z.infer<typeof PetFeederDeviceStatusSchema>;
47
47
  export declare const PetFeederStatusSchema: z.ZodObject<{
48
48
  foodLevel: z.ZodNullable<z.ZodNumber>;
49
+ bowlFood: z.ZodNullable<z.ZodNumber>;
50
+ eating: z.ZodBoolean;
51
+ supportsMarkRefilled: z.ZodBoolean;
49
52
  food1: z.ZodNullable<z.ZodNumber>;
50
53
  food2: z.ZodNullable<z.ZodNumber>;
51
54
  lowFood: z.ZodBoolean;
@@ -95,7 +98,9 @@ export declare const petFeederCapability: {
95
98
  readonly resetDesiccant: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
96
99
  deviceId: z.ZodNumber;
97
100
  }, z.core.$strip>, z.ZodVoid, "mutation">;
98
- /** Mark a hopper as refilled (D4H/D4S/D4SH). */
101
+ /** Tell the PetKit cloud a hopper was refilled so it stops its low-food
102
+ * reminders (D4H/D4S/D4SH only — see `supportsMarkRefilled`). It does not
103
+ * change any sensor reading. */
99
104
  readonly markFoodReplenished: import("./capability-definition.js").CapabilityMethodSchema<z.ZodObject<{
100
105
  deviceId: z.ZodNumber;
101
106
  }, z.core.$strip>, z.ZodVoid, "mutation">;
@@ -132,6 +137,9 @@ export declare const petFeederCapability: {
132
137
  readonly status: {
133
138
  readonly schema: z.ZodObject<{
134
139
  foodLevel: z.ZodNullable<z.ZodNumber>;
140
+ bowlFood: z.ZodNullable<z.ZodNumber>;
141
+ eating: z.ZodBoolean;
142
+ supportsMarkRefilled: z.ZodBoolean;
135
143
  food1: z.ZodNullable<z.ZodNumber>;
136
144
  food2: z.ZodNullable<z.ZodNumber>;
137
145
  lowFood: z.ZodBoolean;
@@ -161,6 +169,9 @@ export declare const petFeederCapability: {
161
169
  */
162
170
  readonly runtimeState: z.ZodObject<{
163
171
  foodLevel: z.ZodNullable<z.ZodNumber>;
172
+ bowlFood: z.ZodNullable<z.ZodNumber>;
173
+ eating: z.ZodBoolean;
174
+ supportsMarkRefilled: z.ZodBoolean;
164
175
  food1: z.ZodNullable<z.ZodNumber>;
165
176
  food2: z.ZodNullable<z.ZodNumber>;
166
177
  lowFood: z.ZodBoolean;
@@ -196,6 +207,9 @@ export declare const petFeederCapability: {
196
207
  readonly foodLevel: {
197
208
  readonly kind: 'gauge';
198
209
  };
210
+ readonly bowlFood: {
211
+ readonly kind: 'gauge';
212
+ };
199
213
  readonly food1: {
200
214
  readonly kind: 'gauge';
201
215
  };
@@ -5,7 +5,7 @@
5
5
  * `ALL_CAPABILITY_DEFINITIONS` (the whole Zod schema graph, ~+100 MB heap per
6
6
  * runner). See docs/decisions/adr-0028.md.
7
7
  *
8
- * Coverage: 1 advance-derived caps, 77 caps with 353 state leaves.
8
+ * Coverage: 1 advance-derived caps, 77 caps with 356 state leaves.
9
9
  */
10
10
  /** A bindable scalar in a capability's runtime state. */
11
11
  export interface CapStateLeaf {
@@ -1,7 +1,7 @@
1
1
  import type { CapabilityHistoryField, CapabilityHistoryItems } from '../capabilities/capability-definition.js';
2
2
  /**
3
3
  * Cap history catalog, projected from every `*.cap.ts` `history` block.
4
- * 42 caps with series (76 series), 33 caps answered `'none'`.
4
+ * 42 caps with series (77 series), 33 caps answered `'none'`.
5
5
  * Field keys are `runtimeState` paths (`a.b` into fixed objects only).
6
6
  */
7
7
  export declare const HISTORY_CATALOG: Readonly<Record<string, Readonly<Record<string, CapabilityHistoryField>>>>;
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export { BaseAddon, classifyStoreError, normalizeAddonInitResult } from './addon
6
6
  export { normalizeNodeId } from './addon/per-node-store.js';
7
7
  export type { AddonRouteInvokeRequest, AddonRouteReplyEnvelope, } from './addon/build-addon-route-provider.js';
8
8
  export { buildAddonRouteProvider } from './addon/build-addon-route-provider.js';
9
+ export type { BuildSessionLostAlertInput, IntegrationSessionBinding, IntegrationSessionReporterOptions, IntegrationSessionSink, SessionAlertAction, SessionHealthAlertsPort, SessionHealthTrackerOptions, SessionLossReason, SessionLostInfo, } from './addon/session-health.js';
10
+ export { buildSessionLostAlert, DEFAULT_SESSION_LOST_DEBOUNCE_MS, IntegrationSessionReporter, SESSION_LOST_ALERT_CATEGORY, SessionHealthTracker, sessionLostAlertId, } from './addon/session-health.js';
9
11
  export type { DurableState, DurableStateDeps } from './addon/durable-state.js';
10
12
  export { createDurableState } from './addon/durable-state.js';
11
13
  export type { IAuthProvider } from './capabilities/auth-provider.cap.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-edKiOXLo.js");
2
+ const require_sleep = require("./sleep-hRhTSDlN.js");
3
3
  const require_event_category = require("./event-category-Ave2VSxY.js");
4
4
  const require_composition = require("./composition-CpAc0YfQ.js");
5
5
  const require_node_color = require("./node-color.js");
@@ -289,6 +289,254 @@ function buildCapturingReply(envelope) {
289
289
  return wrapper;
290
290
  }
291
291
  //#endregion
292
+ //#region src/addon/session-health.ts
293
+ var SESSION_LOST_ALERT_CATEGORY = "integration.session-lost";
294
+ var SESSION_EPISODE_KEY = "session-lost";
295
+ /** A transient reconnect is quiet for this long before it is worth an alert. */
296
+ var DEFAULT_SESSION_LOST_DEBOUNCE_MS = 2 * 6e4;
297
+ var REASON_TEXT = {
298
+ kicked: "the account was signed in on another device, which ended this session",
299
+ "auth-rejected": "the cloud rejected the sign-in (check the credentials)",
300
+ "token-expired": "the session token expired and could not be renewed",
301
+ "connection-lost": "the connection to the cloud was lost"
302
+ };
303
+ function sessionLostAlertId(kind, integrationId) {
304
+ return `session:${kind}:${integrationId}`;
305
+ }
306
+ /** Pure builder, exported so an addon with its own flow can still emit the standard shape. */
307
+ function buildSessionLostAlert(input) {
308
+ const now = input.now ?? Date.now();
309
+ return {
310
+ id: sessionLostAlertId(input.kind, input.integrationId),
311
+ category: SESSION_LOST_ALERT_CATEGORY,
312
+ severity: input.severity ?? "error",
313
+ title: `${input.providerName} connection lost`,
314
+ message: `${input.integrationName} lost its ${input.providerName} session: ${REASON_TEXT[input.reason]}.`,
315
+ action: input.action ?? {
316
+ label: "Open integration",
317
+ href: `/integrations/${encodeURIComponent(input.integrationId)}`
318
+ },
319
+ status: "active",
320
+ read: false,
321
+ createdAt: now,
322
+ updatedAt: now,
323
+ source: {
324
+ type: input.kind,
325
+ id: input.integrationId
326
+ },
327
+ metadata: {
328
+ episodeKey: SESSION_EPISODE_KEY,
329
+ reason: input.reason
330
+ }
331
+ };
332
+ }
333
+ var SessionHealthTracker = class {
334
+ #opts;
335
+ #debounceMs;
336
+ #entries = /* @__PURE__ */ new Map();
337
+ constructor(opts) {
338
+ this.#opts = opts;
339
+ this.#debounceMs = opts.debounceMs ?? 12e4;
340
+ }
341
+ /** The session is gone. Idempotent: call it on every failed poll. */
342
+ lost(integrationId, info) {
343
+ const tags = { integrationId };
344
+ const entry = this.#entries.get(integrationId);
345
+ if (entry === void 0 || entry.phase === "ok") {
346
+ const timer = setTimeout(() => void this.#fire(integrationId), this.#debounceMs);
347
+ this.#entries.set(integrationId, {
348
+ phase: "pending",
349
+ info,
350
+ timer
351
+ });
352
+ this.#opts.logger.info(`${this.#opts.providerName} session lost — alert after ${this.#debounceMs}ms unless it recovers`, {
353
+ tags,
354
+ meta: { reason: info.reason }
355
+ });
356
+ return;
357
+ }
358
+ entry.info = info;
359
+ if (entry.phase === "pending") {
360
+ this.#opts.logger.debug(`${this.#opts.providerName} session still lost — alert pending`, {
361
+ tags,
362
+ meta: { reason: info.reason }
363
+ });
364
+ return;
365
+ }
366
+ if (entry.phase === "unsent") {
367
+ this.#emit(integrationId, entry);
368
+ return;
369
+ }
370
+ this.#opts.logger.debug(`${this.#opts.providerName} session still lost — alert already active`, {
371
+ tags,
372
+ meta: { reason: info.reason }
373
+ });
374
+ }
375
+ /** A call went through. Idempotent: call it on every successful poll. */
376
+ async restored(integrationId) {
377
+ const tags = { integrationId };
378
+ const entry = this.#entries.get(integrationId);
379
+ if (entry === void 0) {
380
+ this.#entries.set(integrationId, {
381
+ phase: "ok",
382
+ info: EMPTY_INFO,
383
+ timer: null
384
+ });
385
+ if (!await this.#resolve(integrationId, "startup reconcile")) this.#entries.delete(integrationId);
386
+ return;
387
+ }
388
+ if (entry.phase === "ok") return;
389
+ this.#clearTimer(entry);
390
+ const previous = entry.phase;
391
+ entry.phase = "ok";
392
+ if (previous === "pending" || previous === "unsent") {
393
+ this.#opts.logger.info(`${this.#opts.providerName} session recovered inside the debounce — no alert`, { tags });
394
+ return;
395
+ }
396
+ await this.#resolve(integrationId, "session restored");
397
+ }
398
+ /** The integration is gone (deleted / disabled): drop its state and its alert. */
399
+ async forget(integrationId) {
400
+ const entry = this.#entries.get(integrationId);
401
+ if (entry === void 0) return;
402
+ this.#clearTimer(entry);
403
+ this.#entries.delete(integrationId);
404
+ if (entry.phase === "alerted") await this.#resolve(integrationId, "integration removed");
405
+ }
406
+ /** Stop every pending timer (addon shutdown). */
407
+ dispose() {
408
+ for (const entry of this.#entries.values()) this.#clearTimer(entry);
409
+ this.#entries.clear();
410
+ }
411
+ #clearTimer(entry) {
412
+ if (entry.timer !== null) clearTimeout(entry.timer);
413
+ entry.timer = null;
414
+ }
415
+ async #fire(integrationId) {
416
+ const entry = this.#entries.get(integrationId);
417
+ if (entry === void 0 || entry.phase !== "pending") return;
418
+ entry.timer = null;
419
+ await this.#emit(integrationId, entry);
420
+ }
421
+ async #emit(integrationId, entry) {
422
+ const tags = { integrationId };
423
+ const emit = this.#opts.getAlerts()?.emit;
424
+ if (emit === void 0) {
425
+ entry.phase = "unsent";
426
+ this.#opts.logger.warn(`${this.#opts.providerName} session alert skipped — alerts capability missing`, { tags });
427
+ return;
428
+ }
429
+ try {
430
+ await emit.mutate(buildSessionLostAlert({
431
+ kind: this.#opts.kind,
432
+ providerName: this.#opts.providerName,
433
+ integrationId,
434
+ integrationName: entry.info.integrationName,
435
+ reason: entry.info.reason,
436
+ ...entry.info.action !== void 0 ? { action: entry.info.action } : {}
437
+ }));
438
+ entry.phase = "alerted";
439
+ this.#opts.logger.warn(`${this.#opts.providerName} session lost — alert raised`, {
440
+ tags,
441
+ meta: { reason: entry.info.reason }
442
+ });
443
+ } catch (err) {
444
+ entry.phase = "unsent";
445
+ this.#opts.logger.warn(`${this.#opts.providerName} session alert emit failed`, {
446
+ tags,
447
+ meta: { error: err instanceof Error ? err.message : String(err) }
448
+ });
449
+ }
450
+ }
451
+ async #resolve(integrationId, why) {
452
+ const tags = { integrationId };
453
+ const update = this.#opts.getAlerts()?.update;
454
+ if (update === void 0) {
455
+ this.#opts.logger.debug(`${this.#opts.providerName} session alert not resolved — no alerts cap`, { tags });
456
+ return false;
457
+ }
458
+ try {
459
+ await update.mutate({
460
+ alertId: sessionLostAlertId(this.#opts.kind, integrationId),
461
+ patch: {
462
+ status: "completed",
463
+ read: true
464
+ }
465
+ });
466
+ this.#opts.logger.info(`${this.#opts.providerName} session alert resolved`, {
467
+ tags,
468
+ meta: { why }
469
+ });
470
+ } catch (err) {
471
+ this.#opts.logger.debug(`${this.#opts.providerName} session alert resolve skipped`, {
472
+ tags,
473
+ meta: {
474
+ why,
475
+ error: err instanceof Error ? err.message : String(err)
476
+ }
477
+ });
478
+ }
479
+ return true;
480
+ }
481
+ };
482
+ var EMPTY_INFO = {
483
+ integrationName: "",
484
+ reason: "connection-lost"
485
+ };
486
+ /**
487
+ * The addon-facing face of {@link SessionHealthTracker}: a process-wide object an
488
+ * integration manager AND its device classes can reach (the kernel builds a
489
+ * device from a `DeviceContext` alone, so neither can be handed a tracker).
490
+ * The addon `bind()`s it in `onInitialize`, `unbind()`s it on shutdown and
491
+ * `forget()`s an integration that vanished; everything before `bind` is a no-op.
492
+ */
493
+ var IntegrationSessionReporter = class {
494
+ #opts;
495
+ #tracker = null;
496
+ #names = /* @__PURE__ */ new Map();
497
+ constructor(opts) {
498
+ this.#opts = opts;
499
+ }
500
+ bind(binding) {
501
+ this.#tracker?.dispose();
502
+ this.#tracker = new SessionHealthTracker({
503
+ kind: this.#opts.kind,
504
+ providerName: this.#opts.providerName,
505
+ logger: binding.logger,
506
+ getAlerts: binding.getAlerts,
507
+ ...binding.debounceMs !== void 0 ? { debounceMs: binding.debounceMs } : {}
508
+ });
509
+ }
510
+ unbind() {
511
+ this.#tracker?.dispose();
512
+ this.#tracker = null;
513
+ }
514
+ /** The manager tells us the display name once, so a device can report by id alone. */
515
+ register(integrationId, integrationName) {
516
+ this.#names.set(integrationId, integrationName);
517
+ }
518
+ /** A call failed. Only a session-ending failure counts; anything else is ignored. */
519
+ failure(integrationId, err) {
520
+ const reason = this.#opts.classify(err);
521
+ if (reason === null) return;
522
+ const action = this.#opts.action?.(integrationId);
523
+ this.#tracker?.lost(integrationId, {
524
+ integrationName: this.#names.get(integrationId) ?? integrationId,
525
+ reason,
526
+ ...action !== void 0 ? { action } : {}
527
+ });
528
+ }
529
+ /** A call went through. */
530
+ async ok(integrationId) {
531
+ await this.#tracker?.restored(integrationId);
532
+ }
533
+ /** The integration was removed. */
534
+ async forget(integrationId) {
535
+ this.#names.delete(integrationId);
536
+ await this.#tracker?.forget(integrationId);
537
+ }
538
+ };
539
+ //#endregion
292
540
  //#region src/encode-profile.ts
293
541
  /**
294
542
  * Shared video / audio encode profile schemas used by every consumer
@@ -13871,7 +14119,7 @@ async function runFanOut(calls, invoke) {
13871
14119
  //#region src/generated/history-catalog.ts
13872
14120
  /**
13873
14121
  * Cap history catalog, projected from every `*.cap.ts` `history` block.
13874
- * 42 caps with series (76 series), 33 caps answered `'none'`.
14122
+ * 42 caps with series (77 series), 33 caps answered `'none'`.
13875
14123
  * Field keys are `runtimeState` paths (`a.b` into fixed objects only).
13876
14124
  */
13877
14125
  var HISTORY_CATALOG = {
@@ -14098,6 +14346,7 @@ var HISTORY_CATALOG = {
14098
14346
  } },
14099
14347
  "pet-feeder": {
14100
14348
  "foodLevel": { kind: "gauge" },
14349
+ "bowlFood": { kind: "gauge" },
14101
14350
  "food1": { kind: "gauge" },
14102
14351
  "food2": { kind: "gauge" },
14103
14352
  "batteryPower": {
@@ -70222,10 +70471,20 @@ var PetFeederDeviceStatusSchema = zod.z.enum([
70222
70471
  ]);
70223
70472
  var gramsPortion = zod.z.number().int().min(4).max(200);
70224
70473
  var PetFeederStatusSchema = zod.z.object({
70225
- /** Food currently in the bowl (grams). Null when the device has not
70226
- * reported a reading yet. On dual-hopper models this is the combined
70227
- * bowl reading; per-hopper levels live in `food1`/`food2`. */
70474
+ /** Food in the TANK (hopper), as the feeder's own sensor reports it
70475
+ * (`state.food`). A coarse level code, not grams — `0` is empty. The
70476
+ * amount in the bowl is `bowlFood`. On dual-hopper models this is the
70477
+ * combined reading; per-hopper levels live in `food1`/`food2`. Null when
70478
+ * the device has not reported yet. */
70228
70479
  foodLevel: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
70480
+ /** Food in the BOWL as the feeder reports it (`state.bowl`), in the
70481
+ * feeder's own units. Null when the model reports none. */
70482
+ bowlFood: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
70483
+ /** True while the pet is eating (`state.eating`). */
70484
+ eating: zod.z.boolean(),
70485
+ /** Whether `markFoodReplenished` does anything on this model. When false
70486
+ * the client must not offer it. */
70487
+ supportsMarkRefilled: zod.z.boolean(),
70229
70488
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
70230
70489
  * single-hopper models. */
70231
70490
  food1: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
@@ -70233,8 +70492,8 @@ var PetFeederStatusSchema = zod.z.object({
70233
70492
  * single-hopper models. */
70234
70493
  food2: zod.z.number().nullable().meta(require_sleep.readingMeta("none")),
70235
70494
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
70236
- * (`device_class: problem`, on = low). True when the bowl is empty /
70237
- * below the feeder's low threshold. */
70495
+ * (`device_class: problem`, on = low). True when the tank reports empty,
70496
+ * or, on a dual-hopper model, when EITHER hopper is empty. */
70238
70497
  lowFood: zod.z.boolean(),
70239
70498
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
70240
70499
  * device has no battery reading. */
@@ -70302,7 +70561,9 @@ var petFeederCapability = {
70302
70561
  kind: "mutation",
70303
70562
  auth: "protected"
70304
70563
  }),
70305
- /** Mark a hopper as refilled (D4H/D4S/D4SH). */
70564
+ /** Tell the PetKit cloud a hopper was refilled so it stops its low-food
70565
+ * reminders (D4H/D4S/D4SH only — see `supportsMarkRefilled`). It does not
70566
+ * change any sensor reading. */
70306
70567
  markFoodReplenished: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.void(), {
70307
70568
  kind: "mutation",
70308
70569
  auth: "protected"
@@ -70375,6 +70636,7 @@ var petFeederCapability = {
70375
70636
  */
70376
70637
  history: { fields: {
70377
70638
  foodLevel: { kind: "gauge" },
70639
+ bowlFood: { kind: "gauge" },
70378
70640
  food1: { kind: "gauge" },
70379
70641
  food2: { kind: "gauge" },
70380
70642
  batteryPower: {
@@ -117307,6 +117569,7 @@ exports.DEFAULT_POOL_MEMORY_POLICY = DEFAULT_POOL_MEMORY_POLICY;
117307
117569
  exports.DEFAULT_RECORDING_PROFILES = DEFAULT_RECORDING_PROFILES;
117308
117570
  exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
117309
117571
  exports.DEFAULT_RUNTIME_STATE_DURABILITY = require_sleep.DEFAULT_RUNTIME_STATE_DURABILITY;
117572
+ exports.DEFAULT_SESSION_LOST_DEBOUNCE_MS = DEFAULT_SESSION_LOST_DEBOUNCE_MS;
117310
117573
  exports.DEFAULT_SESSION_PLACEMENT_POLICY = DEFAULT_SESSION_PLACEMENT_POLICY;
117311
117574
  exports.DEFAULT_SHARE_SOURCE = DEFAULT_SHARE_SOURCE;
117312
117575
  exports.DEFAULT_TIMELAPSE_PREVIEW_TEXT = DEFAULT_TIMELAPSE_PREVIEW_TEXT;
@@ -117652,6 +117915,7 @@ exports.IngestOwnerSchema = IngestOwnerSchema;
117652
117915
  exports.InstalledPackageSchema = InstalledPackageSchema;
117653
117916
  exports.IntegralHelperSpecSchema = require_composition.IntegralHelperSpecSchema;
117654
117917
  exports.IntegrationLiteSchema = IntegrationLiteSchema;
117918
+ exports.IntegrationSessionReporter = IntegrationSessionReporter;
117655
117919
  exports.IntegrationWithStateSchema = IntegrationWithStateSchema;
117656
117920
  exports.IntercomAbilitySchema = IntercomAbilitySchema;
117657
117921
  exports.IntercomStatusSchema = IntercomStatusSchema;
@@ -118556,6 +118820,7 @@ exports.SERIAL_CAPACITY_CAMERA_FPS = SERIAL_CAPACITY_CAMERA_FPS;
118556
118820
  exports.SERIES_KINDS = require_composition.SERIES_KINDS;
118557
118821
  exports.SERVER_NAME_MAX_LENGTH = SERVER_NAME_MAX_LENGTH;
118558
118822
  exports.SERVER_NAME_SETTING_KEY = SERVER_NAME_SETTING_KEY;
118823
+ exports.SESSION_LOST_ALERT_CATEGORY = SESSION_LOST_ALERT_CATEGORY;
118559
118824
  exports.SESSION_PLACEMENT_SETTING_KEY = SESSION_PLACEMENT_SETTING_KEY;
118560
118825
  exports.SHARE_AFFINITY_TOLERANCE_SESSIONS = SHARE_AFFINITY_TOLERANCE_SESSIONS;
118561
118826
  exports.SHARE_SOURCE_SETTING_KEY = SHARE_SOURCE_SETTING_KEY;
@@ -118612,6 +118877,7 @@ exports.ServerUpdateActionResultSchema = ServerUpdateActionResultSchema;
118612
118877
  exports.ServerUpdateCheckResultSchema = ServerUpdateCheckResultSchema;
118613
118878
  exports.ServerUpdateOutcomeSchema = ServerUpdateOutcomeSchema;
118614
118879
  exports.ServerUpdateStateSchema = ServerUpdateStateSchema;
118880
+ exports.SessionHealthTracker = SessionHealthTracker;
118615
118881
  exports.SessionPlacementPolicySchema = SessionPlacementPolicySchema;
118616
118882
  exports.SessionTriggerSchema = SessionTriggerSchema;
118617
118883
  exports.SetCameraSensorsInputSchema = SetCameraSensorsInputSchema;
@@ -118995,6 +119261,7 @@ exports.buildNcTaxonomy = buildNcTaxonomy;
118995
119261
  exports.buildPlusCatalogClassMap = buildPlusCatalogClassMap;
118996
119262
  exports.buildRoleScopes = buildRoleScopes;
118997
119263
  exports.buildSecretRef = require_sleep.buildSecretRef;
119264
+ exports.buildSessionLostAlert = buildSessionLostAlert;
118998
119265
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
118999
119266
  exports.buildSubjectPath = buildSubjectPath;
119000
119267
  exports.buildVideoArgs = require_composition.buildVideoArgs;
@@ -119795,6 +120062,7 @@ exports.sensorTransitionNeverFires = sensorTransitionNeverFires;
119795
120062
  exports.sensorTransitionOffered = sensorTransitionOffered;
119796
120063
  exports.servedTemplateVars = servedTemplateVars;
119797
120064
  exports.serverManagementCapability = serverManagementCapability;
120065
+ exports.sessionLostAlertId = sessionLostAlertId;
119798
120066
  exports.sessionsOfDeviceCell = sessionsOfDeviceCell;
119799
120067
  exports.setByPath = setByPath;
119800
120068
  exports.setFormulaExpr = setFormulaExpr;