@telora/daemon 0.22.13 → 0.22.42
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/build-info.json +5 -3
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/index.js +1 -0
- package/dist/backends/index.js.map +1 -1
- package/dist/ci/ci-status.d.ts.map +1 -1
- package/dist/ci/ci-status.js +1 -0
- package/dist/ci/ci-status.js.map +1 -1
- package/dist/completion/detector.d.ts +9 -0
- package/dist/completion/detector.d.ts.map +1 -1
- package/dist/completion/detector.js +18 -0
- package/dist/completion/detector.js.map +1 -1
- package/dist/completion/event-phases.d.ts +17 -0
- package/dist/completion/event-phases.d.ts.map +1 -1
- package/dist/completion/event-phases.js +34 -13
- package/dist/completion/event-phases.js.map +1 -1
- package/dist/completion/event.d.ts.map +1 -1
- package/dist/completion/event.js +1 -0
- package/dist/completion/event.js.map +1 -1
- package/dist/completion/liveness-firing.d.ts +27 -0
- package/dist/completion/liveness-firing.d.ts.map +1 -0
- package/dist/completion/liveness-firing.js +79 -0
- package/dist/completion/liveness-firing.js.map +1 -0
- package/dist/completion/stdin-turn-tracker.d.ts +19 -0
- package/dist/completion/stdin-turn-tracker.d.ts.map +1 -0
- package/dist/completion/stdin-turn-tracker.js +57 -0
- package/dist/completion/stdin-turn-tracker.js.map +1 -0
- package/dist/completion/supervision-firing-record.d.ts +190 -0
- package/dist/completion/supervision-firing-record.d.ts.map +1 -0
- package/dist/completion/supervision-firing-record.js +232 -0
- package/dist/completion/supervision-firing-record.js.map +1 -0
- package/dist/completion/team-lifecycle.d.ts.map +1 -1
- package/dist/completion/team-lifecycle.js +3 -0
- package/dist/completion/team-lifecycle.js.map +1 -1
- package/dist/directive/directive-dispatch.d.ts +7 -1
- package/dist/directive/directive-dispatch.d.ts.map +1 -1
- package/dist/directive/directive-dispatch.js +75 -12
- package/dist/directive/directive-dispatch.js.map +1 -1
- package/dist/directive/directive-executor.d.ts +1 -1
- package/dist/directive/directive-executor.d.ts.map +1 -1
- package/dist/directive/directive-executor.js +4 -1
- package/dist/directive/directive-executor.js.map +1 -1
- package/dist/directive/directive-queue.d.ts +35 -9
- package/dist/directive/directive-queue.d.ts.map +1 -1
- package/dist/directive/directive-queue.js +15 -8
- package/dist/directive/directive-queue.js.map +1 -1
- package/dist/directive/directive-verify-hold-gate.d.ts +20 -0
- package/dist/directive/directive-verify-hold-gate.d.ts.map +1 -0
- package/dist/directive/directive-verify-hold-gate.js +47 -0
- package/dist/directive/directive-verify-hold-gate.js.map +1 -0
- package/dist/drift/staleness-sensor.d.ts +62 -67
- package/dist/drift/staleness-sensor.d.ts.map +1 -1
- package/dist/drift/staleness-sensor.js +233 -68
- package/dist/drift/staleness-sensor.js.map +1 -1
- package/dist/drift/staleness.d.ts +8 -0
- package/dist/drift/staleness.d.ts.map +1 -1
- package/dist/drift/staleness.js +7 -1
- package/dist/drift/staleness.js.map +1 -1
- package/dist/focus/focus-engine.js +1 -1
- package/dist/focus/focus-engine.js.map +1 -1
- package/dist/focus/focus-team-state.d.ts.map +1 -1
- package/dist/focus/focus-team-state.js +1 -0
- package/dist/focus/focus-team-state.js.map +1 -1
- package/dist/focus/focus-worktree-state.d.ts.map +1 -1
- package/dist/focus/focus-worktree-state.js +4 -2
- package/dist/focus/focus-worktree-state.js.map +1 -1
- package/dist/focus/listener-spawn.d.ts +37 -0
- package/dist/focus/listener-spawn.d.ts.map +1 -1
- package/dist/focus/listener-spawn.js +121 -24
- package/dist/focus/listener-spawn.js.map +1 -1
- package/dist/focus/listener.d.ts.map +1 -1
- package/dist/focus/listener.js +20 -11
- package/dist/focus/listener.js.map +1 -1
- package/dist/focus/team-actionable-work.d.ts +22 -0
- package/dist/focus/team-actionable-work.d.ts.map +1 -1
- package/dist/focus/team-actionable-work.js +43 -5
- package/dist/focus/team-actionable-work.js.map +1 -1
- package/dist/nexus/return.d.ts +60 -1
- package/dist/nexus/return.d.ts.map +1 -1
- package/dist/nexus/return.js +78 -2
- package/dist/nexus/return.js.map +1 -1
- package/dist/shell/heartbeat-metadata.d.ts +91 -0
- package/dist/shell/heartbeat-metadata.d.ts.map +1 -0
- package/dist/shell/heartbeat-metadata.js +169 -0
- package/dist/shell/heartbeat-metadata.js.map +1 -0
- package/dist/shell/heartbeat.d.ts +7 -22
- package/dist/shell/heartbeat.d.ts.map +1 -1
- package/dist/shell/heartbeat.js +120 -89
- package/dist/shell/heartbeat.js.map +1 -1
- package/dist/shell/unified-shell.d.ts.map +1 -1
- package/dist/shell/unified-shell.js +21 -12
- package/dist/shell/unified-shell.js.map +1 -1
- package/dist/spawn/helpers.d.ts.map +1 -1
- package/dist/spawn/helpers.js +6 -1
- package/dist/spawn/helpers.js.map +1 -1
- package/dist/spawn/process-spawn.d.ts +32 -1
- package/dist/spawn/process-spawn.d.ts.map +1 -1
- package/dist/spawn/process-spawn.js +43 -5
- package/dist/spawn/process-spawn.js.map +1 -1
- package/dist/testing/fixtures/index.js +1 -1
- package/dist/types/index.js +1 -1
- package/dist/update/auto-update-idle.d.ts +22 -0
- package/dist/update/auto-update-idle.d.ts.map +1 -1
- package/dist/update/auto-update-idle.js +8 -1
- package/dist/update/auto-update-idle.js.map +1 -1
- package/dist/update/auto-update.d.ts +0 -6
- package/dist/update/auto-update.d.ts.map +1 -1
- package/dist/update/auto-update.js +20 -6
- package/dist/update/auto-update.js.map +1 -1
- package/dist/update/version-check.d.ts +73 -0
- package/dist/update/version-check.d.ts.map +1 -1
- package/dist/update/version-check.js +107 -11
- package/dist/update/version-check.js.map +1 -1
- package/dist/worktree/verify-interlock.d.ts.map +1 -1
- package/dist/worktree/verify-interlock.js +2 -1
- package/dist/worktree/verify-interlock.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { DaemonConfig } from '../types/index.js';
|
|
2
|
+
import { computeBranchStatus, type BranchStatusPayload } from './branch-status.js';
|
|
3
|
+
import { type ProductBranchConfig } from '../queries/products.js';
|
|
4
|
+
import { type VersionDistance } from '../update/version-check.js';
|
|
5
|
+
/** Injectable seam for buildBranchStatusByProduct (unit tests inject fakes). */
|
|
6
|
+
export interface BranchStatusProducerDeps {
|
|
7
|
+
computeBranchStatus: typeof computeBranchStatus;
|
|
8
|
+
getBranchConfig: (productId: string) => Promise<ProductBranchConfig>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Publish branch status per product ({ [productId]: BranchStatusPayload }),
|
|
12
|
+
* mirroring the verificationPostures shape. Each product's payload is
|
|
13
|
+
* computed against ITS OWN repository (configForProduct) with ITS OWN
|
|
14
|
+
* mainBranch/branchMonitors resolved from its own product record — the root
|
|
15
|
+
* config always mirrors products[0] and must never be used to describe the
|
|
16
|
+
* others. One product failing is non-fatal to the rest (its entry is omitted).
|
|
17
|
+
*
|
|
18
|
+
* Exported (with injectable deps) so tests can pin the per-product claims
|
|
19
|
+
* without module mocks; doHeartbeat calls it with the real dependencies.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildBranchStatusByProduct(config: DaemonConfig, deps?: BranchStatusProducerDeps): Promise<Record<string, BranchStatusPayload>>;
|
|
22
|
+
/** The versionLag heartbeat field shape (running-vs-latest lag). */
|
|
23
|
+
export interface VersionLagField {
|
|
24
|
+
running: string;
|
|
25
|
+
latest: string;
|
|
26
|
+
/** The registry the `latest` reading was consulted from, or null when unknown
|
|
27
|
+
* (no reading yet / legacy). Names WHICH registry the reading came from. */
|
|
28
|
+
registry: string | null;
|
|
29
|
+
/**
|
|
30
|
+
* True only when the reading is ATTRIBUTABLE to the registry npm resolves for
|
|
31
|
+
* an install -- only then are behind/distance a trustworthy measurement. When
|
|
32
|
+
* false the field is UNMEASURED: the platform must NOT read behind:false as
|
|
33
|
+
* "in sync" (it may be agreement against the wrong registry -- the mars
|
|
34
|
+
* false-negative). Silence (no field) means no reading yet; measured:false
|
|
35
|
+
* means a reading exists but cannot be attributed to the install registry.
|
|
36
|
+
*/
|
|
37
|
+
measured: boolean;
|
|
38
|
+
/** True when `latest` is strictly newer than `running`. */
|
|
39
|
+
behind: boolean;
|
|
40
|
+
distance: VersionDistance;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the versionLag metadata field: the running daemon vs the install
|
|
44
|
+
* registry's latest. Returns null when it cannot be computed (latest unknown, or
|
|
45
|
+
* the running version unstamped) so the key is simply absent; `behind:false` when
|
|
46
|
+
* the daemon is current. Carries the registry the reading came from and whether
|
|
47
|
+
* it is attributable to `installRegistry` (the registry npm resolves for an
|
|
48
|
+
* install) -- an unattributable reading is measured:false (UNMEASURED), never a
|
|
49
|
+
* silent false "in sync". The fields make a fix that has SHIPPED but is not
|
|
50
|
+
* RUNNING distinguishable from one that is running -- the mars 0.22.4-vs-0.22.13
|
|
51
|
+
* incident, visible from the platform without host access. Pure + testable.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildVersionLagField(running: string, latest: string | null, readingRegistry: string | null, installRegistry: string): VersionLagField | null;
|
|
54
|
+
/** Version-observability state performHeartbeat samples from the daemon module. */
|
|
55
|
+
export interface HeartbeatVersionState {
|
|
56
|
+
daemonVersion: string;
|
|
57
|
+
daemonCommitSha: string;
|
|
58
|
+
latestAvailableVersion: string | null;
|
|
59
|
+
latestAvailableRegistry: string | null;
|
|
60
|
+
installRegistry: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Assemble the heartbeat metadata object (version observability, verification
|
|
64
|
+
* postures, available backends, branch status, focus-scoped fields, health
|
|
65
|
+
* events, read-only mode). Split out of heartbeat.ts (at its file-size floor);
|
|
66
|
+
* the daemon's volatile version state and the two heartbeat-internal helpers
|
|
67
|
+
* (focus-scoped fields, read-only field) are passed in so this stays a pure
|
|
68
|
+
* composer with no back-import into the hot-path module. No behavior change.
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildHeartbeatMetadata(args: {
|
|
71
|
+
cfg: DaemonConfig;
|
|
72
|
+
version: HeartbeatVersionState;
|
|
73
|
+
probeBackends: (cfg: DaemonConfig) => Promise<unknown>;
|
|
74
|
+
buildBranchStatus: (cfg: DaemonConfig) => Promise<Record<string, BranchStatusPayload>>;
|
|
75
|
+
applyFocusScopedMetadata: (metadata: Record<string, unknown>) => void;
|
|
76
|
+
readOnlyMetadataField: () => Record<string, unknown> | null;
|
|
77
|
+
}): Promise<Record<string, unknown>>;
|
|
78
|
+
/** One mechanism's heartbeat falsifiability reading. */
|
|
79
|
+
export interface SupervisionFiringRate {
|
|
80
|
+
total: number;
|
|
81
|
+
falsePositives: number;
|
|
82
|
+
falsePositiveRate: number | null;
|
|
83
|
+
/** True when the mechanism is DEMOTED to observe-only (zero true positives over
|
|
84
|
+
* a meaningful sample): it still records firings but its action sites no longer
|
|
85
|
+
* kill/hold/respawn (enforced via isMechanismDemoted). The visible half of the
|
|
86
|
+
* block-to-observe policy -- the demotion IS the reading (AC/FRT #10). */
|
|
87
|
+
demotionRecommended: boolean;
|
|
88
|
+
}
|
|
89
|
+
/** Per-mechanism falsifiability reading (rate + demotion recommendation) from the store. */
|
|
90
|
+
export declare function buildSupervisionFiringRates(): Record<string, SupervisionFiringRate>;
|
|
91
|
+
//# sourceMappingURL=heartbeat-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat-metadata.d.ts","sourceRoot":"","sources":["../../src/shell/heartbeat-metadata.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,mBAAmB,EAA4B,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC7G,OAAO,EAA0B,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,EAA0D,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAoB1H,gFAAgF;AAChF,MAAM,WAAW,wBAAwB;IACvC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACtE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE,wBAGL,GACA,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAc9C;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf;iFAC6E;IAC7E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,eAAe,EAAE,MAAM,GACtB,eAAe,GAAG,IAAI,CAUxB;AAED,mFAAmF;AACnF,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,GAAG,EAAE,YAAY,CAAC;IAClB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,aAAa,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD,iBAAiB,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC;IACvF,wBAAwB,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,qBAAqB,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7D,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsEnC;AAED,wDAAwD;AACxD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;+EAG2E;IAC3E,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,4FAA4F;AAC5F,wBAAgB,2BAA2B,IAAI,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAYnF"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// PINNED-BY-BEGIN (generated by gen:pinned-by -- do not hand-edit; checked by lint:pinned-by)
|
|
2
|
+
// Pinned by: packages/daemon/src/shell/heartbeat-per-product-branch-status.test.ts,
|
|
3
|
+
// packages/daemon/src/shell/heartbeat-version-lag.test.ts,
|
|
4
|
+
// packages/daemon/src/shell/supervision-firing-rates.test.ts
|
|
5
|
+
// PINNED-BY-END
|
|
6
|
+
/**
|
|
7
|
+
* Heartbeat metadata builders.
|
|
8
|
+
*
|
|
9
|
+
* Pure/near-pure producers of individual heartbeat metadata fields, split out of
|
|
10
|
+
* heartbeat.ts (which sits at its file-size floor) so a new field can be added
|
|
11
|
+
* without growing the hot-path module. Each builder is independently testable;
|
|
12
|
+
* doHeartbeat composes them into the metadata object.
|
|
13
|
+
*/
|
|
14
|
+
import { healthEvents } from '@telora/daemon-core';
|
|
15
|
+
import { configForProduct } from './config.js';
|
|
16
|
+
import { computeBranchStatus } from './branch-status.js';
|
|
17
|
+
import { getProductBranchConfig } from '../queries/products.js';
|
|
18
|
+
import { isNewerVersion, versionDistance, isReadingAttributable } from '../update/version-check.js';
|
|
19
|
+
import { buildProductPostureMap } from '../verify/verification-config.js';
|
|
20
|
+
import { firingMechanisms, firingStats, isDemotionRecommended } from '../completion/supervision-firing-record.js';
|
|
21
|
+
// ── Product branch config cache (per product) ────────────────────────
|
|
22
|
+
const branchConfigCache = new Map();
|
|
23
|
+
const BRANCH_CONFIG_REFRESH_MS = 300_000; // 5 minutes
|
|
24
|
+
/** Fetch (with 5-minute cache) the branch config for one product. */
|
|
25
|
+
async function getCachedBranchConfig(productId) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const cached = branchConfigCache.get(productId);
|
|
28
|
+
if (cached && now - cached.fetchedAt <= BRANCH_CONFIG_REFRESH_MS) {
|
|
29
|
+
return cached.config;
|
|
30
|
+
}
|
|
31
|
+
const fresh = await getProductBranchConfig(productId);
|
|
32
|
+
branchConfigCache.set(productId, { config: fresh, fetchedAt: now });
|
|
33
|
+
return fresh;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Publish branch status per product ({ [productId]: BranchStatusPayload }),
|
|
37
|
+
* mirroring the verificationPostures shape. Each product's payload is
|
|
38
|
+
* computed against ITS OWN repository (configForProduct) with ITS OWN
|
|
39
|
+
* mainBranch/branchMonitors resolved from its own product record — the root
|
|
40
|
+
* config always mirrors products[0] and must never be used to describe the
|
|
41
|
+
* others. One product failing is non-fatal to the rest (its entry is omitted).
|
|
42
|
+
*
|
|
43
|
+
* Exported (with injectable deps) so tests can pin the per-product claims
|
|
44
|
+
* without module mocks; doHeartbeat calls it with the real dependencies.
|
|
45
|
+
*/
|
|
46
|
+
export async function buildBranchStatusByProduct(config, deps = {
|
|
47
|
+
computeBranchStatus,
|
|
48
|
+
getBranchConfig: getCachedBranchConfig,
|
|
49
|
+
}) {
|
|
50
|
+
const branchStatusByProduct = {};
|
|
51
|
+
for (const product of config.products) {
|
|
52
|
+
try {
|
|
53
|
+
const productConfig = configForProduct(config, product);
|
|
54
|
+
const branchConfig = await deps.getBranchConfig(product.id);
|
|
55
|
+
const branchOpts = {
|
|
56
|
+
mainBranch: branchConfig.mainBranch,
|
|
57
|
+
branchMonitors: branchConfig.branchMonitors,
|
|
58
|
+
};
|
|
59
|
+
branchStatusByProduct[product.id] = deps.computeBranchStatus(productConfig, branchOpts);
|
|
60
|
+
}
|
|
61
|
+
catch { /* non-fatal — omit this product's entry */ }
|
|
62
|
+
}
|
|
63
|
+
return branchStatusByProduct;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the versionLag metadata field: the running daemon vs the install
|
|
67
|
+
* registry's latest. Returns null when it cannot be computed (latest unknown, or
|
|
68
|
+
* the running version unstamped) so the key is simply absent; `behind:false` when
|
|
69
|
+
* the daemon is current. Carries the registry the reading came from and whether
|
|
70
|
+
* it is attributable to `installRegistry` (the registry npm resolves for an
|
|
71
|
+
* install) -- an unattributable reading is measured:false (UNMEASURED), never a
|
|
72
|
+
* silent false "in sync". The fields make a fix that has SHIPPED but is not
|
|
73
|
+
* RUNNING distinguishable from one that is running -- the mars 0.22.4-vs-0.22.13
|
|
74
|
+
* incident, visible from the platform without host access. Pure + testable.
|
|
75
|
+
*/
|
|
76
|
+
export function buildVersionLagField(running, latest, readingRegistry, installRegistry) {
|
|
77
|
+
if (!latest || running === 'unknown')
|
|
78
|
+
return null;
|
|
79
|
+
return {
|
|
80
|
+
running,
|
|
81
|
+
latest,
|
|
82
|
+
registry: readingRegistry,
|
|
83
|
+
measured: isReadingAttributable(readingRegistry, installRegistry),
|
|
84
|
+
behind: isNewerVersion(latest, running),
|
|
85
|
+
distance: versionDistance(running, latest),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Assemble the heartbeat metadata object (version observability, verification
|
|
90
|
+
* postures, available backends, branch status, focus-scoped fields, health
|
|
91
|
+
* events, read-only mode). Split out of heartbeat.ts (at its file-size floor);
|
|
92
|
+
* the daemon's volatile version state and the two heartbeat-internal helpers
|
|
93
|
+
* (focus-scoped fields, read-only field) are passed in so this stays a pure
|
|
94
|
+
* composer with no back-import into the hot-path module. No behavior change.
|
|
95
|
+
*/
|
|
96
|
+
export async function buildHeartbeatMetadata(args) {
|
|
97
|
+
const { cfg, version, probeBackends, buildBranchStatus, applyFocusScopedMetadata, readOnlyMetadataField } = args;
|
|
98
|
+
const metadata = {};
|
|
99
|
+
if (version.daemonCommitSha !== 'unknown') {
|
|
100
|
+
metadata.commitSha = version.daemonCommitSha;
|
|
101
|
+
}
|
|
102
|
+
if (version.latestAvailableVersion) {
|
|
103
|
+
metadata.latestAvailableVersion = version.latestAvailableVersion;
|
|
104
|
+
}
|
|
105
|
+
// Version lag: the running daemon vs the install registry's latest, carried so
|
|
106
|
+
// a lagging host (a fix shipped but NOT running -- the mars 0.22.4-vs-0.22.13
|
|
107
|
+
// incident) is visible from the platform without host access. It names the
|
|
108
|
+
// registry the reading came from and marks measured:false (UNMEASURED) when the
|
|
109
|
+
// reading cannot be attributed to the install registry, so the platform never
|
|
110
|
+
// reads a false "in sync" against the wrong registry.
|
|
111
|
+
const versionLag = buildVersionLagField(version.daemonVersion, version.latestAvailableVersion, version.latestAvailableRegistry, version.installRegistry);
|
|
112
|
+
if (versionLag) {
|
|
113
|
+
metadata.versionLag = versionLag;
|
|
114
|
+
}
|
|
115
|
+
metadata.maxTotalSessions = cfg.maxTotalSessions;
|
|
116
|
+
// Publish per-product verification posture ({ [productId]: { posture, command } })
|
|
117
|
+
// so the web app's product settings can surface how each product's deliveries
|
|
118
|
+
// are gated -- posture is local daemon.json config the app cannot otherwise see.
|
|
119
|
+
// Declarative state, derived at read time; never an escalation.
|
|
120
|
+
metadata.verificationPostures = buildProductPostureMap(cfg.products, cfg.defaultVerification);
|
|
121
|
+
// Publish which engines are installed + authenticated on this host so the
|
|
122
|
+
// app's engine picker can grey out unavailable ones (probe is cached).
|
|
123
|
+
try {
|
|
124
|
+
metadata.availableBackends = await probeBackends(cfg);
|
|
125
|
+
}
|
|
126
|
+
catch { /* non-fatal -- omit availability rather than fail the heartbeat */ }
|
|
127
|
+
const branchStatusByProduct = await buildBranchStatus(cfg);
|
|
128
|
+
if (Object.keys(branchStatusByProduct).length > 0) {
|
|
129
|
+
metadata.branchStatus = branchStatusByProduct;
|
|
130
|
+
}
|
|
131
|
+
applyFocusScopedMetadata(metadata);
|
|
132
|
+
// Include health events in metadata for dashboard visibility
|
|
133
|
+
const events = healthEvents.getEvents();
|
|
134
|
+
if (events.length > 0) {
|
|
135
|
+
metadata.healthEvents = events;
|
|
136
|
+
}
|
|
137
|
+
// Surface read-only mode (schema-version gate, warn path) so the dashboard can
|
|
138
|
+
// mark stuck foci/deliveries and show the recovery path. The heartbeat is
|
|
139
|
+
// allowlisted in read-only mode, so this write always lands. Only emit when
|
|
140
|
+
// active -- in normal operation the key is absent.
|
|
141
|
+
const readOnlyField = readOnlyMetadataField();
|
|
142
|
+
if (readOnlyField) {
|
|
143
|
+
Object.assign(metadata, readOnlyField);
|
|
144
|
+
}
|
|
145
|
+
// Supervision falsifiability: the per-mechanism false-positive rate from the
|
|
146
|
+
// firing-record store, so a chronically-wrong kill/hold/respawn mechanism is a
|
|
147
|
+
// READING on the heartbeat -- not something a human hand-greps from the log
|
|
148
|
+
// after the cost is paid. Omitted (key absent) until a mechanism has fired.
|
|
149
|
+
const firingRates = buildSupervisionFiringRates();
|
|
150
|
+
if (Object.keys(firingRates).length > 0) {
|
|
151
|
+
metadata.supervisionFiringRates = firingRates;
|
|
152
|
+
}
|
|
153
|
+
return metadata;
|
|
154
|
+
}
|
|
155
|
+
/** Per-mechanism falsifiability reading (rate + demotion recommendation) from the store. */
|
|
156
|
+
export function buildSupervisionFiringRates() {
|
|
157
|
+
const rates = {};
|
|
158
|
+
for (const mechanism of firingMechanisms()) {
|
|
159
|
+
const s = firingStats(mechanism);
|
|
160
|
+
rates[mechanism] = {
|
|
161
|
+
total: s.total,
|
|
162
|
+
falsePositives: s.falsePositives,
|
|
163
|
+
falsePositiveRate: s.falsePositiveRate,
|
|
164
|
+
demotionRecommended: isDemotionRecommended(mechanism),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return rates;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=heartbeat-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat-metadata.js","sourceRoot":"","sources":["../../src/shell/heartbeat-metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,8 +3,7 @@ import { sendHeartbeat, sendDisconnect } from '../queries/daemon-connection.js';
|
|
|
3
3
|
import { type RetentionBeatObservation, type RetirementPreconditionVerdict } from '../drift/retention-observation.js';
|
|
4
4
|
import { recordSessionCompleted, recordActivity, getLastActivityAt } from './agent-state.js';
|
|
5
5
|
import type { ReadOnlyModeState } from '@telora/daemon-core';
|
|
6
|
-
import {
|
|
7
|
-
import { type ProductBranchConfig } from '../queries/products.js';
|
|
6
|
+
import { type BranchStatusPayload } from './branch-status.js';
|
|
8
7
|
/**
|
|
9
8
|
* Compute a stable, display-meaningful repo identity from a product set: the
|
|
10
9
|
* sorted, deduplicated basenames of each product's repo path, joined by ', '.
|
|
@@ -55,10 +54,13 @@ export declare function getVersions(): {
|
|
|
55
54
|
daemon: string;
|
|
56
55
|
};
|
|
57
56
|
/**
|
|
58
|
-
* Store the latest available version from the npm registry
|
|
59
|
-
*
|
|
57
|
+
* Store the latest available version from the npm registry, plus the registry it
|
|
58
|
+
* was read from. The registry lets the heartbeat ATTRIBUTE the reading to the
|
|
59
|
+
* install registry so a lagging host cannot publish a false "in sync" against the
|
|
60
|
+
* wrong registry (the mars false-negative). Called by the version check module
|
|
61
|
+
* after a successful registry query.
|
|
60
62
|
*/
|
|
61
|
-
export declare function setLatestAvailableVersion(version: string): void;
|
|
63
|
+
export declare function setLatestAvailableVersion(version: string, registry?: string): void;
|
|
62
64
|
export { recordSessionCompleted };
|
|
63
65
|
/**
|
|
64
66
|
* Record a completed issue (called when an issue transitions to Done).
|
|
@@ -77,23 +79,6 @@ export declare function recordIssueCompleted(): void;
|
|
|
77
79
|
export declare function readOnlyMetadataField(): {
|
|
78
80
|
readOnlyMode: ReadOnlyModeState;
|
|
79
81
|
} | null;
|
|
80
|
-
/** Injectable seam for buildBranchStatusByProduct (unit tests inject fakes). */
|
|
81
|
-
export interface BranchStatusProducerDeps {
|
|
82
|
-
computeBranchStatus: typeof computeBranchStatus;
|
|
83
|
-
getBranchConfig: (productId: string) => Promise<ProductBranchConfig>;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Publish branch status per product ({ [productId]: BranchStatusPayload }),
|
|
87
|
-
* mirroring the verificationPostures shape. Each product's payload is
|
|
88
|
-
* computed against ITS OWN repository (configForProduct) with ITS OWN
|
|
89
|
-
* mainBranch/branchMonitors resolved from its own product record — the root
|
|
90
|
-
* config always mirrors products[0] and must never be used to describe the
|
|
91
|
-
* others. One product failing is non-fatal to the rest (its entry is omitted).
|
|
92
|
-
*
|
|
93
|
-
* Exported (with injectable deps) so tests can pin the per-product claims
|
|
94
|
-
* without module mocks; doHeartbeat calls it with the real dependencies.
|
|
95
|
-
*/
|
|
96
|
-
export declare function buildBranchStatusByProduct(config: DaemonConfig, deps?: BranchStatusProducerDeps): Promise<Record<string, BranchStatusPayload>>;
|
|
97
82
|
/**
|
|
98
83
|
* Pure presence-gap detector for the heartbeat.
|
|
99
84
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../src/shell/heartbeat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../src/shell/heartbeat.ts"],"names":[],"mappings":"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"}
|
package/dist/shell/heartbeat.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// PINNED-BY-BEGIN (generated by gen:pinned-by -- do not hand-edit; checked by lint:pinned-by)
|
|
2
2
|
// Pinned by: packages/daemon/src/shell/activity-watchdog.test.ts,
|
|
3
3
|
// packages/daemon/src/shell/heartbeat-draining-status.test.ts,
|
|
4
|
-
// packages/daemon/src/shell/heartbeat-gap.test.ts,
|
|
5
|
-
// packages/daemon/src/shell/heartbeat-per-product-branch-status.test.ts,
|
|
6
|
-
// packages/daemon/src/shell/heartbeat-read-only.test.ts,
|
|
4
|
+
// packages/daemon/src/shell/heartbeat-gap.test.ts, packages/daemon/src/shell/heartbeat-read-only.test.ts,
|
|
7
5
|
// packages/daemon/src/shell/heartbeat-shutdown-guard.test.ts,
|
|
8
6
|
// packages/daemon/src/shell/heartbeat-stable-identity.test.ts,
|
|
9
7
|
// packages/daemon/src/shell/heartbeat-team-phases.test.ts,
|
|
@@ -31,16 +29,16 @@ import { sendHeartbeat, sendDisconnect } from '../queries/daemon-connection.js';
|
|
|
31
29
|
import { getCompletedSessionAggregates } from '../queries/sessions.js';
|
|
32
30
|
import { observeRetentionBeat, recordRetentionObservation, evaluateRetirementPrecondition, } from '../drift/retention-observation.js';
|
|
33
31
|
import { getSessionCounters, setSessionCounters, recordSessionCompleted, recordActivity, getLastActivityAt } from './agent-state.js';
|
|
34
|
-
import { getActiveTeamCount, buildTeamClockMap } from '../focus/focus-team-state.js';
|
|
32
|
+
import { getActiveTeamCount, buildTeamClockMap, hasActiveTeam } from '../focus/focus-team-state.js';
|
|
33
|
+
import { observeLivenessBadge } from '../completion/liveness-firing.js';
|
|
35
34
|
import { checkClockDivergence } from './activity-clock-registry.js';
|
|
36
35
|
import { buildTeamPhaseMap } from '../completion/team-phase.js';
|
|
37
36
|
import { safeInterval, healthEvents, getReadOnlyModeState } from '@telora/daemon-core';
|
|
38
37
|
import { isIntakeDraining } from './intake-control.js';
|
|
39
|
-
import { buildProductPostureMap } from '../verify/verification-config.js';
|
|
40
|
-
import { computeBranchStatus } from './branch-status.js';
|
|
41
|
-
import { getProductBranchConfig } from '../queries/products.js';
|
|
42
|
-
import { configForProduct } from './config.js';
|
|
43
38
|
import { probeAllBackends } from '../backends/index.js';
|
|
39
|
+
import { buildBranchStatusByProduct, buildHeartbeatMetadata } from './heartbeat-metadata.js';
|
|
40
|
+
import { sweepAgedFirings, FIRING_SETTLE_WINDOW_MS } from '../completion/supervision-firing-record.js';
|
|
41
|
+
import { resolveInstallRegistryUrl } from '../update/version-check.js';
|
|
44
42
|
import { createLogger } from './log.js';
|
|
45
43
|
const log = createLogger({ module: 'watchdog' });
|
|
46
44
|
const HEARTBEAT_INTERVAL_MS = 60_000; // 60 seconds
|
|
@@ -82,21 +80,23 @@ let retentionObservationWindow = [];
|
|
|
82
80
|
let daemonVersion = 'unknown';
|
|
83
81
|
let daemonCommitSha = 'unknown';
|
|
84
82
|
let latestAvailableVersion = null;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
// The registry the latestAvailableVersion reading came from, for attribution.
|
|
84
|
+
let latestAvailableRegistry = null;
|
|
85
|
+
// Memoized install-registry resolution (npm config shells out): refreshed on a
|
|
86
|
+
// long TTL so the 60s heartbeat does not spawn `npm config get` every beat.
|
|
87
|
+
let installRegistryCache = null;
|
|
88
|
+
const INSTALL_REGISTRY_TTL_MS = 300_000; // 5 minutes
|
|
89
|
+
/** The registry npm resolves for an install, memoized for INSTALL_REGISTRY_TTL_MS. */
|
|
90
|
+
function getInstallRegistryCached() {
|
|
91
91
|
const now = Date.now();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return cached.config;
|
|
92
|
+
if (installRegistryCache && now - installRegistryCache.at <= INSTALL_REGISTRY_TTL_MS) {
|
|
93
|
+
return installRegistryCache.url;
|
|
95
94
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
return
|
|
95
|
+
const url = resolveInstallRegistryUrl();
|
|
96
|
+
installRegistryCache = { url, at: now };
|
|
97
|
+
return url;
|
|
99
98
|
}
|
|
99
|
+
const daemonStartedAt = new Date().toISOString();
|
|
100
100
|
// ── Stable daemon repo identity ──────────────────────────────────────
|
|
101
101
|
//
|
|
102
102
|
// The heartbeat's `repo_path` field doubles as the key the edge handler
|
|
@@ -271,11 +271,15 @@ export function getVersions() {
|
|
|
271
271
|
return { daemon: daemonVersion };
|
|
272
272
|
}
|
|
273
273
|
/**
|
|
274
|
-
* Store the latest available version from the npm registry
|
|
275
|
-
*
|
|
274
|
+
* Store the latest available version from the npm registry, plus the registry it
|
|
275
|
+
* was read from. The registry lets the heartbeat ATTRIBUTE the reading to the
|
|
276
|
+
* install registry so a lagging host cannot publish a false "in sync" against the
|
|
277
|
+
* wrong registry (the mars false-negative). Called by the version check module
|
|
278
|
+
* after a successful registry query.
|
|
276
279
|
*/
|
|
277
|
-
export function setLatestAvailableVersion(version) {
|
|
280
|
+
export function setLatestAvailableVersion(version, registry) {
|
|
278
281
|
latestAvailableVersion = version;
|
|
282
|
+
latestAvailableRegistry = registry ?? null;
|
|
279
283
|
}
|
|
280
284
|
// Re-export recordSessionCompleted from agent-state for backward compatibility
|
|
281
285
|
export { recordSessionCompleted };
|
|
@@ -323,6 +327,10 @@ function applyFocusScopedMetadata(metadata) {
|
|
|
323
327
|
if (Object.keys(teamClocks).length > 0) {
|
|
324
328
|
metadata.teamClocks = teamClocks;
|
|
325
329
|
}
|
|
330
|
+
// Liveness-badge falsifiability: record when a team is executing+stale (the
|
|
331
|
+
// "unresponsive" signal) and classify it FP on recovery -- so the badge's known
|
|
332
|
+
// false positive (a legit park painted unresponsive) is a measured rate.
|
|
333
|
+
observeLivenessBadge({ teamPhases, teamClocks, isActive: hasActiveTeam, now: Date.now() });
|
|
326
334
|
// Compare the in-memory activity clock (teamClocks, the clock the daemon acts
|
|
327
335
|
// on) against the last SUCCESSFULLY-published clock per focus; a focus whose
|
|
328
336
|
// in-memory clock has led the published one past the threshold surfaces an
|
|
@@ -331,36 +339,74 @@ function applyFocusScopedMetadata(metadata) {
|
|
|
331
339
|
// metadata key is added here. Edge-triggered + quiet within threshold.
|
|
332
340
|
checkClockDivergence(teamClocks);
|
|
333
341
|
}
|
|
334
|
-
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
342
|
+
/*
|
|
343
|
+
* HEARTBEAT METADATA CLASSIFICATION (audited 2026-07-26; full audit in
|
|
344
|
+
* docs/heartbeat-metadata-audit.md).
|
|
345
|
+
*
|
|
346
|
+
* THE RULE: every NEW metadata key or counter added here must be declared
|
|
347
|
+
* product-invariant or product-scoped. Product-scoped keys MUST be published
|
|
348
|
+
* as { [productId]: ... } maps (or carry per-entry product attribution) --
|
|
349
|
+
* the app matches a connection to a product via productIds.includes(productId)
|
|
350
|
+
* (src/hooks/queries/useDaemonConnections.ts), so ANY flat value is served
|
|
351
|
+
* verbatim to EVERY product this daemon manages. A flat product-scoped value
|
|
352
|
+
* is exactly the branchStatus misattribution bug, reborn under a new key.
|
|
353
|
+
*
|
|
354
|
+
* Classification of every key currently emitted:
|
|
341
355
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
356
|
+
* metadata.commitSha invariant daemon build fact (one binary)
|
|
357
|
+
* metadata.latestAvailableVersion invariant npm registry fact about @telora/daemon
|
|
358
|
+
* metadata.versionLag invariant running-vs-latest lag + the registry
|
|
359
|
+
* the reading came from + measured
|
|
360
|
+
* (attributed to the install registry);
|
|
361
|
+
* the running daemon is one binary, so
|
|
362
|
+
* its version lag is a process-global
|
|
363
|
+
* fact (like commitSha /
|
|
364
|
+
* latestAvailableVersion)
|
|
365
|
+
* metadata.maxTotalSessions invariant BY DESIGN shared ResourceGovernor
|
|
366
|
+
* spans all products; not a defect
|
|
367
|
+
* metadata.verificationPostures SCOPED { [productId]: posture } map
|
|
368
|
+
* metadata.availableBackends invariant host-level engine install+auth probe
|
|
369
|
+
* metadata.branchStatus SCOPED { [productId]: BranchStatusPayload }
|
|
370
|
+
* map, each computed via configForProduct
|
|
371
|
+
* metadata.narrations SCOPED keyed by focusId (globally unique;
|
|
372
|
+
* product attribution flows through
|
|
373
|
+
* the focus) -- finer key than productId
|
|
374
|
+
* metadata.teamPhases SCOPED { [focusId]: FocusLifecyclePhase }
|
|
375
|
+
* map (same focusId key as narrations;
|
|
376
|
+
* product attribution flows through
|
|
377
|
+
* the focus)
|
|
378
|
+
* metadata.teamClocks SCOPED { [focusId]: epochMs } map of the
|
|
379
|
+
* per-team last-activity clock (same
|
|
380
|
+
* focusId key as teamPhases; product
|
|
381
|
+
* attribution flows through the focus)
|
|
382
|
+
* metadata.healthEvents MIXED process events + product events;
|
|
383
|
+
* product-scoped entries MUST set the
|
|
384
|
+
* per-entry productId field
|
|
385
|
+
* metadata.readOnlyMode invariant schema gate parks the whole process;
|
|
386
|
+
* every product it serves is read-only
|
|
387
|
+
*
|
|
388
|
+
* counters.activeSessions invariant active team count across all products
|
|
389
|
+
* counters.inProgressIssues invariant same aggregate as activeSessions
|
|
390
|
+
* counters.completedSessions invariant BY CHOICE bootstrapCountersFromDb
|
|
391
|
+
* counters.completedIssues invariant BY CHOICE deliberately SUMS across
|
|
392
|
+
* counters.totalTokensUsed invariant BY CHOICE products -- an aggregate,
|
|
393
|
+
* counters.totalCost invariant BY CHOICE not a misattribution
|
|
394
|
+
* counters.plannedIssues invariant placeholder (always 0)
|
|
395
|
+
* counters.productId legacy mirrors products[0].id only; kept as
|
|
396
|
+
* a matcher fallback for old rows
|
|
397
|
+
* counters.productIds invariant the daemon's product scope -- this IS
|
|
398
|
+
* the per-product match mechanism
|
|
399
|
+
* counters.healthStatus invariant scalar derived from healthEvents;
|
|
400
|
+
* note: product-scoped events fold into
|
|
401
|
+
* it, so one product's warn degrades the
|
|
402
|
+
* daemon-wide status (see audit doc)
|
|
403
|
+
* counters.daemonStartedAt invariant process fact
|
|
404
|
+
* counters.lastActivityAt invariant process fact
|
|
405
|
+
* counters.eventLoopLagMs invariant process fact
|
|
406
|
+
*
|
|
407
|
+
* Known wart (documented, not fixed here): the repoPath positional arg to
|
|
408
|
+
* sendHeartbeat is basename(config.repoPath), i.e. products[0]'s repo only.
|
|
344
409
|
*/
|
|
345
|
-
export async function buildBranchStatusByProduct(config, deps = {
|
|
346
|
-
computeBranchStatus,
|
|
347
|
-
getBranchConfig: getCachedBranchConfig,
|
|
348
|
-
}) {
|
|
349
|
-
const branchStatusByProduct = {};
|
|
350
|
-
for (const product of config.products) {
|
|
351
|
-
try {
|
|
352
|
-
const productConfig = configForProduct(config, product);
|
|
353
|
-
const branchConfig = await deps.getBranchConfig(product.id);
|
|
354
|
-
const branchOpts = {
|
|
355
|
-
mainBranch: branchConfig.mainBranch,
|
|
356
|
-
branchMonitors: branchConfig.branchMonitors,
|
|
357
|
-
};
|
|
358
|
-
branchStatusByProduct[product.id] = deps.computeBranchStatus(productConfig, branchOpts);
|
|
359
|
-
}
|
|
360
|
-
catch { /* non-fatal — omit this product's entry */ }
|
|
361
|
-
}
|
|
362
|
-
return branchStatusByProduct;
|
|
363
|
-
}
|
|
364
410
|
/**
|
|
365
411
|
* Pure presence-gap detector for the heartbeat.
|
|
366
412
|
*
|
|
@@ -438,6 +484,11 @@ async function performHeartbeat(deps) {
|
|
|
438
484
|
if (!config)
|
|
439
485
|
return;
|
|
440
486
|
const activeTeamCount = getActiveTeamCount();
|
|
487
|
+
// Settle sweep: a supervision firing that has stood past the settle window with
|
|
488
|
+
// no falsifying event (a re-ready focus for a teardown) is classified a true
|
|
489
|
+
// positive -- the prediction held. Runs on the beat cadence so the FP rate the
|
|
490
|
+
// heartbeat then publishes reflects a settled reading.
|
|
491
|
+
sweepAgedFirings({ olderThanMs: FIRING_SETTLE_WINDOW_MS });
|
|
441
492
|
// Event loop watchdog: measure and check lag
|
|
442
493
|
const eventLoopLagMs = getEventLoopLagMs();
|
|
443
494
|
checkEventLoopLag(eventLoopLagMs);
|
|
@@ -449,44 +500,24 @@ async function performHeartbeat(deps) {
|
|
|
449
500
|
`Active teams: ${activeTeamCount}. ` +
|
|
450
501
|
`Last activity: ${lastActivity.toISOString()}`);
|
|
451
502
|
}
|
|
452
|
-
// Build metadata
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
catch { /* non-fatal -- omit availability rather than fail the heartbeat */ }
|
|
472
|
-
const branchStatusByProduct = await deps.buildBranchStatus(config);
|
|
473
|
-
if (Object.keys(branchStatusByProduct).length > 0) {
|
|
474
|
-
metadata.branchStatus = branchStatusByProduct;
|
|
475
|
-
}
|
|
476
|
-
applyFocusScopedMetadata(metadata);
|
|
477
|
-
// Include health events in metadata for dashboard visibility
|
|
478
|
-
const events = healthEvents.getEvents();
|
|
479
|
-
if (events.length > 0) {
|
|
480
|
-
metadata.healthEvents = events;
|
|
481
|
-
}
|
|
482
|
-
// Surface read-only mode (schema-version gate, warn path) so the dashboard
|
|
483
|
-
// can mark stuck foci/deliveries and show the recovery path. The heartbeat
|
|
484
|
-
// is allowlisted in read-only mode, so this write always lands. Only emit
|
|
485
|
-
// when active -- in normal operation the key is absent.
|
|
486
|
-
const readOnlyField = readOnlyMetadataField();
|
|
487
|
-
if (readOnlyField) {
|
|
488
|
-
Object.assign(metadata, readOnlyField);
|
|
489
|
-
}
|
|
503
|
+
// Build metadata (version observability, postures, backends, branch status,
|
|
504
|
+
// focus-scoped fields, health events, read-only mode). Lives in
|
|
505
|
+
// heartbeat-metadata.ts; the version state and the two heartbeat-internal
|
|
506
|
+
// helpers (focus-scoped fields, read-only field) are passed in.
|
|
507
|
+
const metadata = await buildHeartbeatMetadata({
|
|
508
|
+
cfg: config,
|
|
509
|
+
version: {
|
|
510
|
+
daemonVersion,
|
|
511
|
+
daemonCommitSha,
|
|
512
|
+
latestAvailableVersion,
|
|
513
|
+
latestAvailableRegistry,
|
|
514
|
+
installRegistry: getInstallRegistryCached(),
|
|
515
|
+
},
|
|
516
|
+
probeBackends: deps.probeBackends,
|
|
517
|
+
buildBranchStatus: deps.buildBranchStatus,
|
|
518
|
+
applyFocusScopedMetadata,
|
|
519
|
+
readOnlyMetadataField,
|
|
520
|
+
});
|
|
490
521
|
// Derive health_status from event buffer severity
|
|
491
522
|
const healthStatus = healthEvents.getUnresolvedCount('error') > 0
|
|
492
523
|
? 'error'
|