@indigoai-us/hq-cli 5.106.3 → 5.107.0

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.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Versions & sync health — the doctor family the registry docblock always
3
+ * anticipated (client-sync-health-control-plane US-015).
4
+ *
5
+ * Reports, per install:
6
+ * - the CLI / Core / desktop (hq-sync menubar) component versions, from the
7
+ * same collectors the feedback and client-health paths use (US-003), so
8
+ * `hq doctor --json` shows exactly what a heartbeat would report;
9
+ * - whether a newer hq-core release is known to be available — read OFFLINE
10
+ * from the cache stamped by the `check-hq-update` SessionStart hook
11
+ * (`workspace/.hq-update-check/last-check.json`), never from the network,
12
+ * preserving the doctor's offline contract (no cache → UNTESTED, not PASS);
13
+ * - per-company sync journal staleness via the engine's `listJournals()` —
14
+ * the ONLY correct enumeration of per-scope journal shards
15
+ * (single-path reconstruction regressed before: feedback_9fbf1f82 /
16
+ * feedback_46288b7b).
17
+ *
18
+ * Caution paid for in blood (bridge-health false positives): a machine with no
19
+ * journals at all is NA, not WARN — CLI-only installs never sync locally and
20
+ * must not read as degraded. Staleness warns only on a corroborated signal: a
21
+ * journal that EXISTS and carries a parseable, old `lastSync`.
22
+ *
23
+ * Every dependency is injectable so the family is unit-testable without an HQ
24
+ * tree, a state dir, or the wall clock.
25
+ */
26
+ import * as fs from "node:fs";
27
+ import * as path from "node:path";
28
+ import { listJournals } from "@indigoai-us/hq-cloud";
29
+ import { CLI_VERSION } from "../../../cli-version.js";
30
+ import { readSyncVersion } from "../../../utils/feedback-versions.js";
31
+ import { readHqVersion } from "../../../utils/pack-contributions.js";
32
+ /** The id of the versions/sync family. */
33
+ export const SYNC_FAMILY_ID = "sync";
34
+ /** Human title for grouped output. */
35
+ export const SYNC_FAMILY_TITLE = "Versions & sync";
36
+ /**
37
+ * A journal shard older than this is reported stale. Seven days: long enough
38
+ * that a laptop shut over a weekend never warns, short enough that a silently
39
+ * dead sync runner surfaces well before data divergence becomes painful.
40
+ */
41
+ export const STALE_JOURNAL_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
42
+ /** The offline update cache written by the check-hq-update SessionStart hook. */
43
+ export const UPDATE_CACHE_RELPATH = path.join("workspace", ".hq-update-check", "last-check.json");
44
+ function defaultVersions(hqRoot) {
45
+ return {
46
+ cli: CLI_VERSION,
47
+ core: safeReadCore(hqRoot),
48
+ desktop: safeReadDesktop(),
49
+ };
50
+ }
51
+ function safeReadCore(hqRoot) {
52
+ try {
53
+ return readHqVersion(hqRoot);
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ function safeReadDesktop() {
60
+ try {
61
+ return readSyncVersion();
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ const DEFAULT_DEPS = {
68
+ versions: defaultVersions,
69
+ // Deliberately NOT wrapped in a try/catch: an enumeration failure must
70
+ // surface as UNKNOWN in `journalResults`, never be collapsed into the
71
+ // empty-list (NA, "cloud sync not in use") case. False healthy is worse
72
+ // than no check.
73
+ journals: () => listJournals(),
74
+ now: () => new Date(),
75
+ };
76
+ /** The versions/sync check family. Registered in `createDefaultRegistry`. */
77
+ export const syncHealthFamily = {
78
+ id: SYNC_FAMILY_ID,
79
+ title: SYNC_FAMILY_TITLE,
80
+ run: (context) => Promise.resolve(checkSyncHealth(context)),
81
+ };
82
+ /** Run every versions/sync check. A thrown check degrades to UNKNOWN. */
83
+ export function checkSyncHealth(context, deps = DEFAULT_DEPS) {
84
+ try {
85
+ return [
86
+ ...versionResults(context, deps),
87
+ ...updateAvailabilityResult(context, deps),
88
+ ...journalResults(deps),
89
+ ];
90
+ }
91
+ catch (error) {
92
+ return [
93
+ {
94
+ status: "UNKNOWN",
95
+ checkId: "sync.error",
96
+ message: `versions/sync checks could not run: ${error.message}`,
97
+ },
98
+ ];
99
+ }
100
+ }
101
+ // ─── Component versions ──────────────────────────────────────────────────────
102
+ function versionResults(context, deps) {
103
+ const versions = deps.versions(context.hqRoot);
104
+ const results = [
105
+ {
106
+ status: "PASS",
107
+ checkId: "sync.versions.cli",
108
+ message: `hq-cli ${versions.cli}.`,
109
+ },
110
+ ];
111
+ if (versions.core) {
112
+ results.push({
113
+ status: "PASS",
114
+ checkId: "sync.versions.core",
115
+ message: `hq-core ${versions.core} (core/core.yaml hqVersion).`,
116
+ });
117
+ }
118
+ else {
119
+ results.push({
120
+ status: "WARN",
121
+ checkId: "sync.versions.core",
122
+ target: path.join(context.hqRoot, "core", "core.yaml"),
123
+ message: "hq-core version could not be read from core/core.yaml — the scaffold may be missing or pre-v12.",
124
+ remediation: "Run /update-hq to (re)install the hq-core scaffold.",
125
+ });
126
+ }
127
+ // Desktop absence is NA, never WARN: hq-sync is optional (CLI-only and CI
128
+ // machines legitimately run without it), so "not installed" is not degraded.
129
+ results.push(versions.desktop
130
+ ? {
131
+ status: "PASS",
132
+ checkId: "sync.versions.desktop",
133
+ message: `hq-sync desktop ${versions.desktop} (~/.hq/sync-version.json).`,
134
+ }
135
+ : {
136
+ status: "NA",
137
+ checkId: "sync.versions.desktop",
138
+ message: "hq-sync desktop app not detected (~/.hq/sync-version.json absent) — not required on CLI-only installs.",
139
+ });
140
+ return results;
141
+ }
142
+ // ─── Update availability (offline, from the SessionStart hook's cache) ───────
143
+ function updateAvailabilityResult(context, deps) {
144
+ const cachePath = path.join(context.hqRoot, UPDATE_CACHE_RELPATH);
145
+ let latest = null;
146
+ try {
147
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
148
+ const value = parsed?.latest;
149
+ if (typeof value === "string" && /^\d+\.\d+\.\d+$/.test(value)) {
150
+ latest = value;
151
+ }
152
+ }
153
+ catch {
154
+ // Missing or malformed cache: fall through to UNTESTED.
155
+ }
156
+ if (!latest) {
157
+ return [
158
+ {
159
+ status: "UNTESTED",
160
+ checkId: "sync.update.core",
161
+ target: cachePath,
162
+ message: "Latest hq-core release unknown — no update-check cache yet (written by the check-hq-update SessionStart hook; the doctor never goes to the network for it).",
163
+ },
164
+ ];
165
+ }
166
+ const core = deps.versions(context.hqRoot).core;
167
+ if (!core) {
168
+ return [
169
+ {
170
+ status: "UNKNOWN",
171
+ checkId: "sync.update.core",
172
+ message: `Latest hq-core release is v${latest}, but the local core version could not be read to compare.`,
173
+ },
174
+ ];
175
+ }
176
+ if (semverGt(latest, core)) {
177
+ return [
178
+ {
179
+ status: "WARN",
180
+ checkId: "sync.update.core",
181
+ message: `hq-core update available: local v${core}, latest v${latest}.`,
182
+ remediation: "Run /update-hq in a fresh session to upgrade.",
183
+ },
184
+ ];
185
+ }
186
+ return [
187
+ {
188
+ status: "PASS",
189
+ checkId: "sync.update.core",
190
+ message: `hq-core is up to date (local v${core}, latest known v${latest}).`,
191
+ },
192
+ ];
193
+ }
194
+ /** True when `a` > `b` for plain X.Y.Z versions. Non-numeric parts compare 0. */
195
+ export function semverGt(a, b) {
196
+ const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
197
+ const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
198
+ for (let i = 0; i < 3; i++) {
199
+ const da = pa[i] ?? 0;
200
+ const db = pb[i] ?? 0;
201
+ if (da !== db)
202
+ return da > db;
203
+ }
204
+ return false;
205
+ }
206
+ // ─── Per-journal staleness ───────────────────────────────────────────────────
207
+ function journalResults(deps) {
208
+ // Enumeration failure (throw / IO error) is UNKNOWN — which FAILS per the
209
+ // doctor's exit-code contract — never NA: a broken or unreadable journal
210
+ // store must not read as "cloud sync not in use" (a false healthy). Only a
211
+ // SUCCESSFUL enumeration that finds nothing is the benign CLI-only case.
212
+ let journals;
213
+ try {
214
+ journals = deps.journals();
215
+ }
216
+ catch (error) {
217
+ return [
218
+ {
219
+ status: "UNKNOWN",
220
+ checkId: "sync.journals",
221
+ message: `Sync journal enumeration failed: ${error instanceof Error ? error.message : String(error)}`,
222
+ remediation: "Check that the HQ state directory is readable, then re-run `hq doctor`.",
223
+ },
224
+ ];
225
+ }
226
+ // No journals is NA, never WARN: a CLI-only install has nothing to sync
227
+ // locally and must not read as degraded (bridge-health false-positive
228
+ // lesson — warn only on corroborated signals).
229
+ if (journals.length === 0) {
230
+ return [
231
+ {
232
+ status: "NA",
233
+ checkId: "sync.journals",
234
+ message: "No local sync journals found — HQ cloud sync is not in use on this machine.",
235
+ },
236
+ ];
237
+ }
238
+ const nowMs = deps.now().getTime();
239
+ return journals.map((entry) => {
240
+ const lastSync = entry.journal?.lastSync;
241
+ const checkId = `sync.journal.${entry.slug}`;
242
+ if (typeof lastSync !== "string" || lastSync.length === 0) {
243
+ return {
244
+ status: "WARN",
245
+ checkId,
246
+ target: entry.path,
247
+ message: `Sync journal '${entry.slug}' exists but has never recorded a successful sync.`,
248
+ remediation: "Run /hq-sync (or `hq sync`) to complete a first sync.",
249
+ };
250
+ }
251
+ const parsed = Date.parse(lastSync);
252
+ if (!Number.isFinite(parsed)) {
253
+ return {
254
+ status: "UNKNOWN",
255
+ checkId,
256
+ target: entry.path,
257
+ message: `Sync journal '${entry.slug}' has an unparseable lastSync (${lastSync}).`,
258
+ };
259
+ }
260
+ const ageMs = nowMs - parsed;
261
+ if (ageMs > STALE_JOURNAL_THRESHOLD_MS) {
262
+ const days = Math.floor(ageMs / 86_400_000);
263
+ return {
264
+ status: "WARN",
265
+ checkId,
266
+ target: entry.path,
267
+ message: `Sync journal '${entry.slug}' is stale: last successful sync ${days} day${days === 1 ? "" : "s"} ago (${lastSync}).`,
268
+ remediation: "Run /hq-sync (or `hq sync`) and check the sync runner.",
269
+ };
270
+ }
271
+ return {
272
+ status: "PASS",
273
+ checkId,
274
+ target: entry.path,
275
+ message: `Sync journal '${entry.slug}' is fresh (last sync ${lastSync}).`,
276
+ };
277
+ });
278
+ }
279
+ //# sourceMappingURL=sync-health.js.map
@@ -20,6 +20,7 @@ import { checkGrokWiring } from "./checks/grok-wiring.js";
20
20
  import { checkRuntimeProbe } from "./checks/runtime-probe.js";
21
21
  import { runtimeHealthFamily } from "./checks/runtime-health.js";
22
22
  import { integrationsFamily } from "./checks/integrations.js";
23
+ import { syncHealthFamily } from "./checks/sync-health.js";
23
24
  import { fixtureCoverageFamily } from "./fixtures/discover.js";
24
25
  import { checkClaudeWiring } from "./checks/claude-wiring.js";
25
26
  /**
@@ -181,6 +182,11 @@ export function createDefaultRegistry() {
181
182
  // The engine remains family-agnostic: integrations is one bounded,
182
183
  // read-only inventory family, registered alongside all other checks.
183
184
  registry.register(integrationsFamily);
185
+ // Versions & sync (US-015): CLI/Core/desktop component versions, offline
186
+ // update availability, and per-company sync journal staleness — the family
187
+ // this registry's docblock always anticipated. Local-only reads, so the
188
+ // doctor's offline contract is preserved.
189
+ registry.register(syncHealthFamily);
184
190
  return registry;
185
191
  }
186
192
  //# sourceMappingURL=registry.js.map
package/dist/main.js CHANGED
@@ -90,6 +90,7 @@ import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
90
90
  import { autoUpdateAndReexec } from "./utils/self-update.js";
91
91
  import { CLI_VERSION } from "./cli-version.js";
92
92
  import { emitCliSessionStarted } from "./utils/cli-telemetry.js";
93
+ import { reportCliClientHealthInvocation } from "./utils/client-health.js";
93
94
  import { settleWithin } from "./utils/settle-with-timeout.js";
94
95
  import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
95
96
  import { isPackageRootResolutionError, packageRootCaptureContext, } from "./utils/package-root-diagnostics.js";
@@ -289,7 +290,12 @@ registerDoctorCommand(program);
289
290
  // from `hq doctor` (hook guardrails). Does not start MQTT listen.
290
291
  registerMeshCommand(program);
291
292
  program.hook("preAction", async () => {
292
- await emitCliSessionStarted();
293
+ // Both are best-effort, bounded (1.2s), and fully swallowed: neither can
294
+ // delay past its bound or change the command's result or exit code.
295
+ await Promise.all([
296
+ emitCliSessionStarted(),
297
+ reportCliClientHealthInvocation(),
298
+ ]);
293
299
  });
294
300
  export async function runCli() {
295
301
  // Set when a self-update re-exec'd this command on a newer CLI: the child
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Client-health wire contract (client-sync-health-control-plane US-000).
3
+ *
4
+ * ONE versioned contract shared in spirit across four adapters:
5
+ *
6
+ * - hq-pro src/sync/server/client-health-contract.ts (reference)
7
+ * - hq-desktop-app crates/hq-desktop-core/src/client_health.rs
8
+ * - hq-cli src/utils/client-health-contract.ts (this file)
9
+ * - indigo-gtm-hq src/lib/client-health.ts
10
+ *
11
+ * Every adapter uses the SAME field names and the SAME enum values, verified
12
+ * per-repo against byte-equivalent canonical fixtures (here embedded in
13
+ * src/utils/client-health-contract.test.ts). The interface, not repository
14
+ * internals, is the shared test surface.
15
+ *
16
+ * Design rules (PRD notes + securityNotes):
17
+ *
18
+ * - ADDITIVE + tolerant of older clients: every non-identity field is
19
+ * optional-friendly, and unknown EXTRA fields are ignored (a newer client
20
+ * talking to an older server must not fail). Absence of `updaterState`
21
+ * means "an older client that never reported it"; the closed value
22
+ * "unchecked" means "the updater has not run yet" — the two are distinct
23
+ * and must survive the wire.
24
+ * - FAIL CLOSED on values: enums are closed sets. Free-form shell text,
25
+ * customer file paths, secret-shaped values, raw logs, and unknown repair
26
+ * kinds are rejected with a typed {@link ClientHealthContractError}.
27
+ * - Device identity: clients send only the stable random `installationId`.
28
+ * HMAC'ing of device identifiers happens server-side (US-001, modeled on
29
+ * scope-sync-mode-repository.ts) and never crosses this wire contract.
30
+ * - Monotonic `sequence`: the server keeps the highest sequence seen per
31
+ * installation and drops older/replayed heartbeats
32
+ * ({@link shouldApplyHeartbeat}).
33
+ */
34
+ export declare const CLIENT_HEALTH_CONTRACT_VERSION = 1;
35
+ export declare const CLIENT_HEALTH_PLATFORMS: readonly ["macos", "windows", "linux"];
36
+ export type ClientHealthPlatform = (typeof CLIENT_HEALTH_PLATFORMS)[number];
37
+ export declare const CLIENT_HEALTH_ARCHS: readonly ["x64", "arm64"];
38
+ export type ClientHealthArch = (typeof CLIENT_HEALTH_ARCHS)[number];
39
+ /** Which client produced the heartbeat. Desktop is always-on; CLI contributes per invocation. */
40
+ export declare const CLIENT_HEALTH_SOURCES: readonly ["desktop", "cli"];
41
+ export type ClientHealthSource = (typeof CLIENT_HEALTH_SOURCES)[number];
42
+ /**
43
+ * Current sync state of the installation. `paused` and `conflict_blocked` are
44
+ * first-class states (not failures folded into `error`) because support treats
45
+ * them differently: pause may be intentional and conflicts are user-owned.
46
+ */
47
+ export declare const CLIENT_HEALTH_SYNC_STATES: readonly ["idle", "syncing", "paused", "conflict_blocked", "error", "never_synced"];
48
+ export type ClientHealthSyncState = (typeof CLIENT_HEALTH_SYNC_STATES)[number];
49
+ /**
50
+ * Updater state. ABSENCE of the field means the client is too old to report
51
+ * it; `"unchecked"` means the updater exists but has not checked yet. Do not
52
+ * collapse the two (US-000 acceptance: Unchecked vs Absent must survive).
53
+ */
54
+ export declare const CLIENT_HEALTH_UPDATER_STATES: readonly ["unchecked", "up_to_date", "update_available", "update_downloading", "update_ready", "update_failed", "unsupported"];
55
+ export type ClientHealthUpdaterState = (typeof CLIENT_HEALTH_UPDATER_STATES)[number];
56
+ /** Closed failure/blocker reason codes — the ONLY reasons that cross the wire. */
57
+ export declare const CLIENT_HEALTH_FAILURE_REASONS: readonly ["SYNC_PAUSED", "CONFLICT_BLOCKED", "DESKTOP_OUTDATED", "CLI_OUTDATED", "CORE_OUTDATED", "AUTH_EXPIRED", "UPDATE_FAILED", "RUNNER_FAILED", "PERMISSION_DENIED", "DISK_FULL", "HEARTBEAT_STALE"];
58
+ export type ClientHealthFailureReason = (typeof CLIENT_HEALTH_FAILURE_REASONS)[number];
59
+ /**
60
+ * Repair/diagnostic command allowlist (US-006/US-009 consume these shapes).
61
+ * A desired-state interface, never a remote shell — any kind outside this set
62
+ * fails closed.
63
+ */
64
+ export declare const CLIENT_HEALTH_REPAIR_KINDS: readonly ["CHECK_NOW", "RETRY_SYNC", "RESUME_SYNC", "REPAIR_CLI", "UPDATE_CORE", "APPLY_DESKTOP_UPDATE", "RESTART_APP"];
65
+ export type ClientHealthRepairKind = (typeof CLIENT_HEALTH_REPAIR_KINDS)[number];
66
+ /** Command/receipt lifecycle states (queued → acknowledged → running → terminal). */
67
+ export declare const CLIENT_HEALTH_COMMAND_STATES: readonly ["queued", "acknowledged", "running", "succeeded", "failed", "expired"];
68
+ export type ClientHealthCommandState = (typeof CLIENT_HEALTH_COMMAND_STATES)[number];
69
+ /** Closed diagnostic probe identifiers (US-007). */
70
+ export declare const CLIENT_HEALTH_DIAGNOSTIC_CHECKS: readonly ["auth", "runner", "cli", "core", "updater", "sync", "conflicts", "storage", "permissions"];
71
+ export type ClientHealthDiagnosticCheck = (typeof CLIENT_HEALTH_DIAGNOSTIC_CHECKS)[number];
72
+ export declare const CLIENT_HEALTH_CHECK_STATUSES: readonly ["pass", "fail", "skip"];
73
+ export type ClientHealthCheckStatus = (typeof CLIENT_HEALTH_CHECK_STATUSES)[number];
74
+ export declare const CLIENT_HEALTH_MAX_STRING_LENGTH = 64;
75
+ export declare const CLIENT_HEALTH_MAX_CONSECUTIVE_FAILURES = 100000;
76
+ export declare const CLIENT_HEALTH_MAX_CONFLICT_COUNT = 100000;
77
+ export declare const CLIENT_HEALTH_MAX_CHECKS = 16;
78
+ /**
79
+ * The four client versions. All optional: a CLI-only installation has no
80
+ * desktop/syncRunner version, and older clients may omit any of them.
81
+ */
82
+ export interface ClientHealthVersions {
83
+ desktop?: string;
84
+ cli?: string;
85
+ core?: string;
86
+ syncRunner?: string;
87
+ }
88
+ export interface ClientHealthHeartbeat {
89
+ contractVersion: number;
90
+ /** Stable random installation identity — NOT a hardware fingerprint. */
91
+ installationId: string;
92
+ source: ClientHealthSource;
93
+ platform: ClientHealthPlatform;
94
+ arch: ClientHealthArch;
95
+ /** Client-side emit time (liveness); the server also stamps receive time. */
96
+ sentAt: string;
97
+ /** Monotonic per-installation sequence — older/replayed values never overwrite newer state. */
98
+ sequence: number;
99
+ versions: ClientHealthVersions;
100
+ syncState: ClientHealthSyncState;
101
+ /** Last time a sync RUN started — distinct from success. */
102
+ lastSyncAttemptAt?: string;
103
+ /** Advances only on genuine success (including no-change runs). */
104
+ lastSyncSuccessAt?: string;
105
+ consecutiveFailures: number;
106
+ conflictCount?: number;
107
+ updaterState?: ClientHealthUpdaterState;
108
+ failureReason?: ClientHealthFailureReason;
109
+ }
110
+ export interface ClientHealthCheckResult {
111
+ check: ClientHealthDiagnosticCheck;
112
+ status: ClientHealthCheckStatus;
113
+ /** Present only on `fail` — a closed reason code, never prose. */
114
+ reason?: ClientHealthFailureReason;
115
+ }
116
+ /**
117
+ * Receipt for a diagnostic or repair command (US-006+ store these). `checks`
118
+ * is only meaningful for `CHECK_NOW`.
119
+ */
120
+ export interface ClientHealthCommandReceipt {
121
+ contractVersion: number;
122
+ commandId: string;
123
+ installationId: string;
124
+ kind: ClientHealthRepairKind;
125
+ state: ClientHealthCommandState;
126
+ /** Monotonic per-command revision — out-of-order receipt updates fail closed downstream. */
127
+ revision: number;
128
+ occurredAt: string;
129
+ checks?: ClientHealthCheckResult[];
130
+ failureReason?: ClientHealthFailureReason;
131
+ }
132
+ export type ClientHealthContractErrorCode = "MISSING_FIELD" | "INVALID_TYPE" | "UNKNOWN_ENUM_VALUE" | "UNSAFE_VALUE" | "OUT_OF_BOUNDS" | "UNSUPPORTED_CONTRACT_VERSION";
133
+ export declare class ClientHealthContractError extends Error {
134
+ readonly code: ClientHealthContractErrorCode;
135
+ readonly field: string;
136
+ constructor(code: ClientHealthContractErrorCode, field: string, detail?: string);
137
+ }
138
+ /**
139
+ * Parse + validate one heartbeat. Unknown extra fields are ignored (additive
140
+ * tolerance); every consumed value fails closed on unsafe content.
141
+ */
142
+ export declare function parseClientHealthHeartbeat(input: unknown): ClientHealthHeartbeat;
143
+ /** Parse + validate one diagnostic/repair command receipt. Unknown kinds fail closed. */
144
+ export declare function parseClientHealthCommandReceipt(input: unknown): ClientHealthCommandReceipt;
145
+ /**
146
+ * True when an incoming heartbeat sequence may replace the stored snapshot.
147
+ * Equal or older sequences are late deliveries/replays: drop them (the server
148
+ * answers idempotently and the current snapshot remains unchanged).
149
+ */
150
+ export declare function shouldApplyHeartbeat(storedSequence: number | undefined, incomingSequence: number): boolean;
151
+ //# sourceMappingURL=client-health-contract.d.ts.map