@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.
Files changed (116) hide show
  1. package/build-info.json +5 -3
  2. package/dist/backends/index.d.ts.map +1 -1
  3. package/dist/backends/index.js +1 -0
  4. package/dist/backends/index.js.map +1 -1
  5. package/dist/ci/ci-status.d.ts.map +1 -1
  6. package/dist/ci/ci-status.js +1 -0
  7. package/dist/ci/ci-status.js.map +1 -1
  8. package/dist/completion/detector.d.ts +9 -0
  9. package/dist/completion/detector.d.ts.map +1 -1
  10. package/dist/completion/detector.js +18 -0
  11. package/dist/completion/detector.js.map +1 -1
  12. package/dist/completion/event-phases.d.ts +17 -0
  13. package/dist/completion/event-phases.d.ts.map +1 -1
  14. package/dist/completion/event-phases.js +34 -13
  15. package/dist/completion/event-phases.js.map +1 -1
  16. package/dist/completion/event.d.ts.map +1 -1
  17. package/dist/completion/event.js +1 -0
  18. package/dist/completion/event.js.map +1 -1
  19. package/dist/completion/liveness-firing.d.ts +27 -0
  20. package/dist/completion/liveness-firing.d.ts.map +1 -0
  21. package/dist/completion/liveness-firing.js +79 -0
  22. package/dist/completion/liveness-firing.js.map +1 -0
  23. package/dist/completion/stdin-turn-tracker.d.ts +19 -0
  24. package/dist/completion/stdin-turn-tracker.d.ts.map +1 -0
  25. package/dist/completion/stdin-turn-tracker.js +57 -0
  26. package/dist/completion/stdin-turn-tracker.js.map +1 -0
  27. package/dist/completion/supervision-firing-record.d.ts +190 -0
  28. package/dist/completion/supervision-firing-record.d.ts.map +1 -0
  29. package/dist/completion/supervision-firing-record.js +232 -0
  30. package/dist/completion/supervision-firing-record.js.map +1 -0
  31. package/dist/completion/team-lifecycle.d.ts.map +1 -1
  32. package/dist/completion/team-lifecycle.js +3 -0
  33. package/dist/completion/team-lifecycle.js.map +1 -1
  34. package/dist/directive/directive-dispatch.d.ts +7 -1
  35. package/dist/directive/directive-dispatch.d.ts.map +1 -1
  36. package/dist/directive/directive-dispatch.js +75 -12
  37. package/dist/directive/directive-dispatch.js.map +1 -1
  38. package/dist/directive/directive-executor.d.ts +1 -1
  39. package/dist/directive/directive-executor.d.ts.map +1 -1
  40. package/dist/directive/directive-executor.js +4 -1
  41. package/dist/directive/directive-executor.js.map +1 -1
  42. package/dist/directive/directive-queue.d.ts +35 -9
  43. package/dist/directive/directive-queue.d.ts.map +1 -1
  44. package/dist/directive/directive-queue.js +15 -8
  45. package/dist/directive/directive-queue.js.map +1 -1
  46. package/dist/directive/directive-verify-hold-gate.d.ts +20 -0
  47. package/dist/directive/directive-verify-hold-gate.d.ts.map +1 -0
  48. package/dist/directive/directive-verify-hold-gate.js +47 -0
  49. package/dist/directive/directive-verify-hold-gate.js.map +1 -0
  50. package/dist/drift/staleness-sensor.d.ts +62 -67
  51. package/dist/drift/staleness-sensor.d.ts.map +1 -1
  52. package/dist/drift/staleness-sensor.js +233 -68
  53. package/dist/drift/staleness-sensor.js.map +1 -1
  54. package/dist/drift/staleness.d.ts +8 -0
  55. package/dist/drift/staleness.d.ts.map +1 -1
  56. package/dist/drift/staleness.js +7 -1
  57. package/dist/drift/staleness.js.map +1 -1
  58. package/dist/focus/focus-engine.js +1 -1
  59. package/dist/focus/focus-engine.js.map +1 -1
  60. package/dist/focus/focus-team-state.d.ts.map +1 -1
  61. package/dist/focus/focus-team-state.js +1 -0
  62. package/dist/focus/focus-team-state.js.map +1 -1
  63. package/dist/focus/focus-worktree-state.d.ts.map +1 -1
  64. package/dist/focus/focus-worktree-state.js +4 -2
  65. package/dist/focus/focus-worktree-state.js.map +1 -1
  66. package/dist/focus/listener-spawn.d.ts +37 -0
  67. package/dist/focus/listener-spawn.d.ts.map +1 -1
  68. package/dist/focus/listener-spawn.js +121 -24
  69. package/dist/focus/listener-spawn.js.map +1 -1
  70. package/dist/focus/listener.d.ts.map +1 -1
  71. package/dist/focus/listener.js +20 -11
  72. package/dist/focus/listener.js.map +1 -1
  73. package/dist/focus/team-actionable-work.d.ts +22 -0
  74. package/dist/focus/team-actionable-work.d.ts.map +1 -1
  75. package/dist/focus/team-actionable-work.js +43 -5
  76. package/dist/focus/team-actionable-work.js.map +1 -1
  77. package/dist/nexus/return.d.ts +60 -1
  78. package/dist/nexus/return.d.ts.map +1 -1
  79. package/dist/nexus/return.js +78 -2
  80. package/dist/nexus/return.js.map +1 -1
  81. package/dist/shell/heartbeat-metadata.d.ts +91 -0
  82. package/dist/shell/heartbeat-metadata.d.ts.map +1 -0
  83. package/dist/shell/heartbeat-metadata.js +169 -0
  84. package/dist/shell/heartbeat-metadata.js.map +1 -0
  85. package/dist/shell/heartbeat.d.ts +7 -22
  86. package/dist/shell/heartbeat.d.ts.map +1 -1
  87. package/dist/shell/heartbeat.js +120 -89
  88. package/dist/shell/heartbeat.js.map +1 -1
  89. package/dist/shell/unified-shell.d.ts.map +1 -1
  90. package/dist/shell/unified-shell.js +21 -12
  91. package/dist/shell/unified-shell.js.map +1 -1
  92. package/dist/spawn/helpers.d.ts.map +1 -1
  93. package/dist/spawn/helpers.js +6 -1
  94. package/dist/spawn/helpers.js.map +1 -1
  95. package/dist/spawn/process-spawn.d.ts +32 -1
  96. package/dist/spawn/process-spawn.d.ts.map +1 -1
  97. package/dist/spawn/process-spawn.js +43 -5
  98. package/dist/spawn/process-spawn.js.map +1 -1
  99. package/dist/testing/fixtures/index.js +1 -1
  100. package/dist/types/index.js +1 -1
  101. package/dist/update/auto-update-idle.d.ts +22 -0
  102. package/dist/update/auto-update-idle.d.ts.map +1 -1
  103. package/dist/update/auto-update-idle.js +8 -1
  104. package/dist/update/auto-update-idle.js.map +1 -1
  105. package/dist/update/auto-update.d.ts +0 -6
  106. package/dist/update/auto-update.d.ts.map +1 -1
  107. package/dist/update/auto-update.js +20 -6
  108. package/dist/update/auto-update.js.map +1 -1
  109. package/dist/update/version-check.d.ts +73 -0
  110. package/dist/update/version-check.d.ts.map +1 -1
  111. package/dist/update/version-check.js +107 -11
  112. package/dist/update/version-check.js.map +1 -1
  113. package/dist/worktree/verify-interlock.d.ts.map +1 -1
  114. package/dist/worktree/verify-interlock.js +2 -1
  115. package/dist/worktree/verify-interlock.js.map +1 -1
  116. 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":"AAAA,8FAA8F;AAC9F,oFAAoF;AACpF,6DAA6D;AAC7D,+DAA+D;AAC/D,gBAAgB;AAEhB;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAsD,MAAM,oBAAoB,CAAC;AAC7G,OAAO,EAAE,sBAAsB,EAA4B,MAAM,wBAAwB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,qBAAqB,EAAwB,MAAM,4BAA4B,CAAC;AAC1H,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAElH,wEAAwE;AACxE,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAA8D,CAAC;AAChG,MAAM,wBAAwB,GAAG,OAAO,CAAC,CAAC,YAAY;AAEtD,qEAAqE;AACrE,KAAK,UAAU,qBAAqB,CAAC,SAAiB;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAChD,IAAI,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,IAAI,wBAAwB,EAAE,CAAC;QACjE,OAAO,MAAM,CAAC,MAAM,CAAC;IACvB,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,sBAAsB,CAAC,SAAS,CAAC,CAAC;IACtD,iBAAiB,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC;AACf,CAAC;AAQD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,MAAoB,EACpB,OAAiC;IAC/B,mBAAmB;IACnB,eAAe,EAAE,qBAAqB;CACvC;IAED,MAAM,qBAAqB,GAAwC,EAAE,CAAC;IACtE,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,aAAa,GAAG,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YACxD,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC5D,MAAM,UAAU,GAAwB;gBACtC,UAAU,EAAE,YAAY,CAAC,UAAU;gBACnC,cAAc,EAAE,YAAY,CAAC,cAAc;aAC5C,CAAC;YACF,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,mBAAmB,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QAC1F,CAAC;QAAC,MAAM,CAAC,CAAC,2CAA2C,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,qBAAqB,CAAC;AAC/B,CAAC;AAuBD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,MAAqB,EACrB,eAA8B,EAC9B,eAAuB;IAEvB,IAAI,CAAC,MAAM,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAClD,OAAO;QACL,OAAO;QACP,MAAM;QACN,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,qBAAqB,CAAC,eAAe,EAAE,eAAe,CAAC;QACjE,MAAM,EAAE,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC;QACvC,QAAQ,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC;KAC3C,CAAC;AACJ,CAAC;AAWD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,IAO5C;IACC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,GAAG,IAAI,CAAC;IACjH,MAAM,QAAQ,GAA4B,EAAE,CAAC;IAC7C,IAAI,OAAO,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QAC1C,QAAQ,CAAC,SAAS,GAAG,OAAO,CAAC,eAAe,CAAC;IAC/C,CAAC;IACD,IAAI,OAAO,CAAC,sBAAsB,EAAE,CAAC;QACnC,QAAQ,CAAC,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IACnE,CAAC;IACD,+EAA+E;IAC/E,8EAA8E;IAC9E,2EAA2E;IAC3E,gFAAgF;IAChF,8EAA8E;IAC9E,sDAAsD;IACtD,MAAM,UAAU,GAAG,oBAAoB,CACrC,OAAO,CAAC,aAAa,EACrB,OAAO,CAAC,sBAAsB,EAC9B,OAAO,CAAC,uBAAuB,EAC/B,OAAO,CAAC,eAAe,CACxB,CAAC;IACF,IAAI,UAAU,EAAE,CAAC;QACf,QAAQ,CAAC,UAAU,GAAG,UAAU,CAAC;IACnC,CAAC;IACD,QAAQ,CAAC,gBAAgB,GAAG,GAAG,CAAC,gBAAgB,CAAC;IAEjD,mFAAmF;IACnF,8EAA8E;IAC9E,iFAAiF;IACjF,gEAAgE;IAChE,QAAQ,CAAC,oBAAoB,GAAG,sBAAsB,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAE9F,0EAA0E;IAC1E,uEAAuE;IACvE,IAAI,CAAC;QACH,QAAQ,CAAC,iBAAiB,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC,CAAC,mEAAmE,CAAC,CAAC;IAE/E,MAAM,qBAAqB,GAAG,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAC3D,IAAI,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,QAAQ,CAAC,YAAY,GAAG,qBAAqB,CAAC;IAChD,CAAC;IAED,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAEnC,6DAA6D;IAC7D,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,EAAE,CAAC;IACxC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,QAAQ,CAAC,YAAY,GAAG,MAAM,CAAC;IACjC,CAAC;IAED,+EAA+E;IAC/E,0EAA0E;IAC1E,4EAA4E;IAC5E,mDAAmD;IACnD,MAAM,aAAa,GAAG,qBAAqB,EAAE,CAAC;IAC9C,IAAI,aAAa,EAAE,CAAC;QAClB,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IACzC,CAAC;IAED,6EAA6E;IAC7E,+EAA+E;IAC/E,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,WAAW,GAAG,2BAA2B,EAAE,CAAC;IAClD,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxC,QAAQ,CAAC,sBAAsB,GAAG,WAAW,CAAC;IAChD,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAcD,4FAA4F;AAC5F,MAAM,UAAU,2BAA2B;IACzC,MAAM,KAAK,GAA0C,EAAE,CAAC;IACxD,KAAK,MAAM,SAAS,IAAI,gBAAgB,EAAE,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;QACjC,KAAK,CAAC,SAAS,CAAC,GAAG;YACjB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,cAAc,EAAE,CAAC,CAAC,cAAc;YAChC,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;YACtC,mBAAmB,EAAE,qBAAqB,CAAC,SAAS,CAAC;SACtD,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -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 { computeBranchStatus, type BranchStatusPayload } from './branch-status.js';
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
- * Called by the version check module after a successful registry query.
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":"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"}
1
+ {"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../src/shell/heartbeat.ts"],"names":[],"mappings":"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"}
@@ -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
- const daemonStartedAt = new Date().toISOString();
86
- // ── Product branch config cache (per product) ────────────────────────
87
- const branchConfigCache = new Map();
88
- const BRANCH_CONFIG_REFRESH_MS = 300_000; // 5 minutes
89
- /** Fetch (with 5-minute cache) the branch config for one product. */
90
- async function getCachedBranchConfig(productId) {
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
- const cached = branchConfigCache.get(productId);
93
- if (cached && now - cached.fetchedAt <= BRANCH_CONFIG_REFRESH_MS) {
94
- return cached.config;
92
+ if (installRegistryCache && now - installRegistryCache.at <= INSTALL_REGISTRY_TTL_MS) {
93
+ return installRegistryCache.url;
95
94
  }
96
- const fresh = await getProductBranchConfig(productId);
97
- branchConfigCache.set(productId, { config: fresh, fetchedAt: now });
98
- return fresh;
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
- * Called by the version check module after a successful registry query.
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
- * Publish branch status per product ({ [productId]: BranchStatusPayload }),
336
- * mirroring the verificationPostures shape. Each product's payload is
337
- * computed against ITS OWN repository (configForProduct) with ITS OWN
338
- * mainBranch/branchMonitors resolved from its own product record — the root
339
- * config always mirrors products[0] and must never be used to describe the
340
- * others. One product failing is non-fatal to the rest (its entry is omitted).
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
- * Exported (with injectable deps) so tests can pin the per-product claims
343
- * without module mocks; doHeartbeat calls it with the real dependencies.
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 with version observability fields
453
- const metadata = {};
454
- if (daemonCommitSha !== 'unknown') {
455
- metadata.commitSha = daemonCommitSha;
456
- }
457
- if (latestAvailableVersion) {
458
- metadata.latestAvailableVersion = latestAvailableVersion;
459
- }
460
- metadata.maxTotalSessions = config.maxTotalSessions;
461
- // Publish per-product verification posture ({ [productId]: { posture, command } })
462
- // so the web app's product settings can surface how each product's deliveries
463
- // are gated -- posture is local daemon.json config the app cannot otherwise see.
464
- // Declarative state, derived at read time; never an escalation.
465
- metadata.verificationPostures = buildProductPostureMap(config.products, config.defaultVerification);
466
- // Publish which engines are installed + authenticated on this host so the
467
- // app's engine picker can grey out unavailable ones (probe is cached).
468
- try {
469
- metadata.availableBackends = await deps.probeBackends(config);
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'