harnery 0.36.0 → 0.37.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.
Files changed (311) hide show
  1. package/dist/commander.js +4 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +87 -46
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +26 -1
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +11 -0
  8. package/dist/commands/diagnostics.d.ts +4 -0
  9. package/dist/commands/diagnostics.d.ts.map +1 -0
  10. package/dist/commands/diagnostics.js +229 -0
  11. package/dist/commands/files.d.ts +12 -0
  12. package/dist/commands/files.d.ts.map +1 -1
  13. package/dist/commands/files.js +54 -1
  14. package/dist/commands/governor.d.ts.map +1 -1
  15. package/dist/commands/governor.js +4 -0
  16. package/dist/commands/ledger-v3.d.ts.map +1 -1
  17. package/dist/commands/ledger-v3.js +71 -2
  18. package/dist/commands/qa-run.d.ts +4 -0
  19. package/dist/commands/qa-run.d.ts.map +1 -0
  20. package/dist/commands/qa-run.js +141 -0
  21. package/dist/commands/resources.d.ts +4 -0
  22. package/dist/commands/resources.d.ts.map +1 -0
  23. package/dist/commands/resources.js +29 -0
  24. package/dist/commands/supervisor.d.ts +4 -0
  25. package/dist/commands/supervisor.d.ts.map +1 -0
  26. package/dist/commands/supervisor.js +101 -0
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +17 -0
  29. package/dist/commands/workflow.d.ts.map +1 -1
  30. package/dist/commands/workflow.js +4 -0
  31. package/dist/core/agents/cli.js +7 -3
  32. package/dist/core/agents/health.d.ts +17 -0
  33. package/dist/core/agents/health.d.ts.map +1 -0
  34. package/dist/core/agents/health.js +109 -0
  35. package/dist/core/agents/mailbox.d.ts +92 -0
  36. package/dist/core/agents/mailbox.d.ts.map +1 -0
  37. package/dist/core/agents/mailbox.js +325 -0
  38. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  39. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  40. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  41. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  42. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  43. package/dist/core/agents/render/prompt-context.js +34 -13
  44. package/dist/core/agents/render/session-context.d.ts +8 -0
  45. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  46. package/dist/core/agents/render/session-context.js +34 -3
  47. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  48. package/dist/core/agents/rules/commit-conflict.js +50 -1
  49. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  50. package/dist/core/agents/rules/stop-hook.js +6 -10
  51. package/dist/core/agents/session-finalizer-v3.js +4 -1
  52. package/dist/core/artifacts/index.d.ts +26 -1
  53. package/dist/core/artifacts/index.d.ts.map +1 -1
  54. package/dist/core/artifacts/index.js +146 -6
  55. package/dist/core/config.d.ts +12 -0
  56. package/dist/core/config.d.ts.map +1 -1
  57. package/dist/core/config.js +43 -0
  58. package/dist/core/diagnostics/advice.d.ts +9 -0
  59. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  60. package/dist/core/diagnostics/advice.js +112 -0
  61. package/dist/core/diagnostics/bundle.d.ts +35 -0
  62. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  63. package/dist/core/diagnostics/bundle.js +874 -0
  64. package/dist/core/diagnostics/comparison.d.ts +5 -0
  65. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  66. package/dist/core/diagnostics/comparison.js +444 -0
  67. package/dist/core/diagnostics/contract.d.ts +245 -0
  68. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  69. package/dist/core/diagnostics/contract.js +18 -0
  70. package/dist/core/diagnostics/identity.d.ts +3 -0
  71. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  72. package/dist/core/diagnostics/identity.js +10 -0
  73. package/dist/core/diagnostics/index.d.ts +8 -0
  74. package/dist/core/diagnostics/index.d.ts.map +1 -0
  75. package/dist/core/diagnostics/index.js +7 -0
  76. package/dist/core/diagnostics/replay.d.ts +8 -0
  77. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  78. package/dist/core/diagnostics/replay.js +213 -0
  79. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  80. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  81. package/dist/core/diagnostics/sanitize.js +84 -0
  82. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  83. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  84. package/dist/core/events/legacy-storage/compression.js +107 -0
  85. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  86. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/index.js +1 -0
  88. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  89. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  90. package/dist/core/events/v3/archive-retention.js +195 -0
  91. package/dist/core/events/v3/capabilities.d.ts +1 -1
  92. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  93. package/dist/core/events/v3/capabilities.js +1 -0
  94. package/dist/core/events/v3/index.d.ts +1 -0
  95. package/dist/core/events/v3/index.d.ts.map +1 -1
  96. package/dist/core/events/v3/index.js +1 -0
  97. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  98. package/dist/core/events/v3/live-routing.js +1 -0
  99. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  100. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  101. package/dist/core/events/v3/producers/hook-base.js +13 -2
  102. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  103. package/dist/core/events/v3/producers/intake.js +1 -0
  104. package/dist/core/events/v3/producers/recorder.d.ts +16 -0
  105. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  106. package/dist/core/events/v3/producers/recorder.js +88 -1
  107. package/dist/core/hooks/adapter/output.d.ts +8 -0
  108. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  109. package/dist/core/hooks/adapter/output.js +11 -1
  110. package/dist/core/hooks/cli.js +183 -192
  111. package/dist/core/hooks/health.d.ts +59 -0
  112. package/dist/core/hooks/health.d.ts.map +1 -0
  113. package/dist/core/hooks/health.js +84 -0
  114. package/dist/core/resources/contract.d.ts +90 -0
  115. package/dist/core/resources/contract.d.ts.map +1 -0
  116. package/dist/core/resources/contract.js +2 -0
  117. package/dist/core/resources/index.d.ts +6 -0
  118. package/dist/core/resources/index.d.ts.map +1 -0
  119. package/dist/core/resources/index.js +5 -0
  120. package/dist/core/resources/sampler.d.ts +27 -0
  121. package/dist/core/resources/sampler.d.ts.map +1 -0
  122. package/dist/core/resources/sampler.js +357 -0
  123. package/dist/core/resources/service-status.d.ts +3 -0
  124. package/dist/core/resources/service-status.d.ts.map +1 -0
  125. package/dist/core/resources/service-status.js +48 -0
  126. package/dist/core/resources/service.d.ts +26 -0
  127. package/dist/core/resources/service.d.ts.map +1 -0
  128. package/dist/core/resources/service.js +292 -0
  129. package/dist/core/resources/storage.d.ts +13 -0
  130. package/dist/core/resources/storage.d.ts.map +1 -0
  131. package/dist/core/resources/storage.js +30 -0
  132. package/dist/core/storage/atomic-json.d.ts +3 -0
  133. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  134. package/dist/core/storage/atomic-json.js +15 -0
  135. package/dist/core/storage/builtins.d.ts.map +1 -1
  136. package/dist/core/storage/builtins.js +45 -6
  137. package/dist/core/storage/logger.d.ts +2 -0
  138. package/dist/core/storage/logger.d.ts.map +1 -1
  139. package/dist/core/storage/logger.js +2 -0
  140. package/dist/core/storage/query.d.ts +14 -0
  141. package/dist/core/storage/query.d.ts.map +1 -1
  142. package/dist/core/storage/query.js +51 -0
  143. package/dist/core/supervisor/activity.d.ts +4 -0
  144. package/dist/core/supervisor/activity.d.ts.map +1 -0
  145. package/dist/core/supervisor/activity.js +141 -0
  146. package/dist/core/supervisor/contract.d.ts +268 -0
  147. package/dist/core/supervisor/contract.d.ts.map +1 -0
  148. package/dist/core/supervisor/contract.js +35 -0
  149. package/dist/core/supervisor/explanations.d.ts +4 -0
  150. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  151. package/dist/core/supervisor/explanations.js +71 -0
  152. package/dist/core/supervisor/findings.d.ts +20 -0
  153. package/dist/core/supervisor/findings.d.ts.map +1 -0
  154. package/dist/core/supervisor/findings.js +399 -0
  155. package/dist/core/supervisor/history.d.ts +9 -0
  156. package/dist/core/supervisor/history.d.ts.map +1 -0
  157. package/dist/core/supervisor/history.js +46 -0
  158. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  159. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  160. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  161. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  162. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  163. package/dist/core/supervisor/hook-health-storage.js +24 -0
  164. package/dist/core/supervisor/hook-health.d.ts +72 -0
  165. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  166. package/dist/core/supervisor/hook-health.js +203 -0
  167. package/dist/core/supervisor/hooks.d.ts +10 -0
  168. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  169. package/dist/core/supervisor/hooks.js +28 -0
  170. package/dist/core/supervisor/index.d.ts +15 -0
  171. package/dist/core/supervisor/index.d.ts.map +1 -0
  172. package/dist/core/supervisor/index.js +14 -0
  173. package/dist/core/supervisor/log-feed.d.ts +9 -0
  174. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  175. package/dist/core/supervisor/log-feed.js +103 -0
  176. package/dist/core/supervisor/service.d.ts +29 -0
  177. package/dist/core/supervisor/service.d.ts.map +1 -0
  178. package/dist/core/supervisor/service.js +450 -0
  179. package/dist/core/supervisor/services.d.ts +11 -0
  180. package/dist/core/supervisor/services.d.ts.map +1 -0
  181. package/dist/core/supervisor/services.js +155 -0
  182. package/dist/core/supervisor/status.d.ts +3 -0
  183. package/dist/core/supervisor/status.d.ts.map +1 -0
  184. package/dist/core/supervisor/status.js +48 -0
  185. package/dist/core/supervisor/storage.d.ts +30 -0
  186. package/dist/core/supervisor/storage.d.ts.map +1 -0
  187. package/dist/core/supervisor/storage.js +73 -0
  188. package/dist/core/supervisor/timeline.d.ts +3 -0
  189. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  190. package/dist/core/supervisor/timeline.js +87 -0
  191. package/dist/core/workflow/admission.d.ts +23 -0
  192. package/dist/core/workflow/admission.d.ts.map +1 -0
  193. package/dist/core/workflow/admission.js +136 -0
  194. package/dist/core/workflow/engine.d.ts.map +1 -1
  195. package/dist/core/workflow/engine.js +77 -2
  196. package/dist/core/workflow/index.d.ts +3 -2
  197. package/dist/core/workflow/index.d.ts.map +1 -1
  198. package/dist/core/workflow/index.js +2 -1
  199. package/dist/core/workflow/proof.d.ts +2 -1
  200. package/dist/core/workflow/proof.d.ts.map +1 -1
  201. package/dist/core/workflow/proof.js +39 -1
  202. package/dist/core/workflow/run-state.d.ts +3 -2
  203. package/dist/core/workflow/run-state.d.ts.map +1 -1
  204. package/dist/core/workflow/run-state.js +8 -1
  205. package/dist/core/workflow/types.d.ts +33 -1
  206. package/dist/core/workflow/types.d.ts.map +1 -1
  207. package/dist/core/workflow/types.js +2 -1
  208. package/dist/lib/browser/client.d.ts +5 -0
  209. package/dist/lib/browser/client.d.ts.map +1 -1
  210. package/dist/lib/browser/client.js +1 -0
  211. package/dist/lib/browser/index.d.ts +2 -0
  212. package/dist/lib/browser/index.d.ts.map +1 -1
  213. package/dist/lib/browser/index.js +2 -0
  214. package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
  215. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  216. package/dist/lib/browser/qa-run-contracts.js +226 -0
  217. package/dist/lib/browser/qa-run.d.ts +51 -0
  218. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  219. package/dist/lib/browser/qa-run.js +684 -0
  220. package/dist/lib/coord-root-id.d.ts +9 -0
  221. package/dist/lib/coord-root-id.d.ts.map +1 -0
  222. package/dist/lib/coord-root-id.js +17 -0
  223. package/dist/lib/instructions/apply.d.ts +3 -1
  224. package/dist/lib/instructions/apply.d.ts.map +1 -1
  225. package/dist/lib/instructions/apply.js +74 -3
  226. package/dist/lib/instructions/templates.d.ts.map +1 -1
  227. package/dist/lib/instructions/templates.js +6 -0
  228. package/package.json +12 -1
  229. package/src/commander.ts +26 -0
  230. package/src/commands/agents.ts +102 -49
  231. package/src/commands/artifacts.ts +40 -2
  232. package/src/commands/browse.ts +15 -0
  233. package/src/commands/diagnostics.ts +271 -0
  234. package/src/commands/files.ts +102 -2
  235. package/src/commands/governor.ts +8 -0
  236. package/src/commands/ledger-v3.ts +82 -0
  237. package/src/commands/qa-run.ts +184 -0
  238. package/src/commands/resources.ts +38 -0
  239. package/src/commands/supervisor.ts +141 -0
  240. package/src/commands/web.ts +19 -0
  241. package/src/commands/workflow.ts +8 -0
  242. package/src/core/agents/cli.ts +7 -3
  243. package/src/core/agents/health.ts +142 -0
  244. package/src/core/agents/mailbox.ts +373 -0
  245. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  246. package/src/core/agents/render/prompt-context.ts +42 -12
  247. package/src/core/agents/render/session-context.ts +37 -3
  248. package/src/core/agents/rules/commit-conflict.ts +58 -1
  249. package/src/core/agents/rules/stop-hook.ts +6 -12
  250. package/src/core/agents/session-finalizer-v3.ts +4 -1
  251. package/src/core/artifacts/index.ts +196 -10
  252. package/src/core/config.ts +111 -2
  253. package/src/core/diagnostics/advice.ts +147 -0
  254. package/src/core/diagnostics/bundle.ts +1142 -0
  255. package/src/core/diagnostics/comparison.ts +565 -0
  256. package/src/core/diagnostics/contract.ts +311 -0
  257. package/src/core/diagnostics/identity.ts +11 -0
  258. package/src/core/diagnostics/index.ts +7 -0
  259. package/src/core/diagnostics/replay.ts +314 -0
  260. package/src/core/diagnostics/sanitize.ts +97 -0
  261. package/src/core/events/legacy-storage/compression.ts +130 -0
  262. package/src/core/events/legacy-storage/index.ts +1 -0
  263. package/src/core/events/v3/archive-retention.ts +251 -0
  264. package/src/core/events/v3/capabilities.ts +2 -0
  265. package/src/core/events/v3/index.ts +9 -0
  266. package/src/core/events/v3/live-routing.ts +1 -0
  267. package/src/core/events/v3/producers/hook-base.ts +14 -2
  268. package/src/core/events/v3/producers/intake.ts +1 -0
  269. package/src/core/events/v3/producers/recorder.ts +110 -1
  270. package/src/core/hooks/adapter/output.ts +12 -1
  271. package/src/core/hooks/cli.ts +192 -215
  272. package/src/core/hooks/health.ts +146 -0
  273. package/src/core/resources/contract.ts +98 -0
  274. package/src/core/resources/index.ts +5 -0
  275. package/src/core/resources/sampler.ts +458 -0
  276. package/src/core/resources/service-status.ts +62 -0
  277. package/src/core/resources/service.ts +337 -0
  278. package/src/core/resources/storage.ts +44 -0
  279. package/src/core/storage/atomic-json.ts +15 -0
  280. package/src/core/storage/builtins.ts +62 -6
  281. package/src/core/storage/logger.ts +2 -0
  282. package/src/core/storage/query.ts +58 -0
  283. package/src/core/supervisor/activity.ts +193 -0
  284. package/src/core/supervisor/contract.ts +312 -0
  285. package/src/core/supervisor/explanations.ts +104 -0
  286. package/src/core/supervisor/findings.ts +788 -0
  287. package/src/core/supervisor/history.ts +61 -0
  288. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  289. package/src/core/supervisor/hook-health-storage.ts +25 -0
  290. package/src/core/supervisor/hook-health.ts +301 -0
  291. package/src/core/supervisor/hooks.ts +35 -0
  292. package/src/core/supervisor/index.ts +14 -0
  293. package/src/core/supervisor/log-feed.ts +131 -0
  294. package/src/core/supervisor/service.ts +511 -0
  295. package/src/core/supervisor/services.ts +203 -0
  296. package/src/core/supervisor/status.ts +59 -0
  297. package/src/core/supervisor/storage.ts +126 -0
  298. package/src/core/supervisor/timeline.ts +133 -0
  299. package/src/core/workflow/admission.ts +215 -0
  300. package/src/core/workflow/engine.ts +93 -1
  301. package/src/core/workflow/index.ts +10 -0
  302. package/src/core/workflow/proof.ts +54 -1
  303. package/src/core/workflow/run-state.ts +13 -1
  304. package/src/core/workflow/types.ts +39 -1
  305. package/src/lib/browser/client.ts +6 -0
  306. package/src/lib/browser/index.ts +29 -0
  307. package/src/lib/browser/qa-run-contracts.ts +380 -0
  308. package/src/lib/browser/qa-run.ts +784 -0
  309. package/src/lib/coord-root-id.ts +23 -0
  310. package/src/lib/instructions/apply.ts +81 -2
  311. package/src/lib/instructions/templates.ts +6 -0
@@ -0,0 +1,61 @@
1
+ import type { ResourceSnapshot } from "../resources/contract.ts";
2
+ import {
3
+ SUPERVISOR_HISTORY_SCHEMA_VERSION,
4
+ type SupervisorHistory,
5
+ type SupervisorHistoryPoint,
6
+ } from "./contract.ts";
7
+
8
+ export const SUPERVISOR_HISTORY_INTERVAL_MS = 10_000;
9
+ export const SUPERVISOR_HISTORY_MAX_POINTS = 90;
10
+
11
+ export function updateSupervisorHistory(
12
+ previous: SupervisorHistory | undefined,
13
+ resource: ResourceSnapshot,
14
+ ): { history: SupervisorHistory; changed: boolean } {
15
+ const prior = validHistory(previous) ? previous.points : [];
16
+ const lastAt = prior.length > 0 ? Date.parse(prior[prior.length - 1]!.sampled_at) : Number.NaN;
17
+ const sampledAt = Date.parse(resource.sampled_at);
18
+ if (
19
+ Number.isFinite(lastAt) &&
20
+ Number.isFinite(sampledAt) &&
21
+ sampledAt - lastAt < SUPERVISOR_HISTORY_INTERVAL_MS
22
+ ) {
23
+ return {
24
+ history: {
25
+ schema_version: SUPERVISOR_HISTORY_SCHEMA_VERSION,
26
+ interval_ms: SUPERVISOR_HISTORY_INTERVAL_MS,
27
+ max_points: SUPERVISOR_HISTORY_MAX_POINTS,
28
+ points: prior,
29
+ },
30
+ changed: false,
31
+ };
32
+ }
33
+ const point: SupervisorHistoryPoint = {
34
+ sampled_at: resource.sampled_at,
35
+ machine: {
36
+ cpu_percent: resource.machine.cpu_percent,
37
+ memory_percent: resource.machine.memory_percent,
38
+ memory_used_bytes: resource.machine.memory_used_bytes,
39
+ swap_used_bytes: resource.machine.swap_used_bytes,
40
+ process_count: resource.machine.process_count,
41
+ },
42
+ groups: resource.groups.map((group) => ({ ...group, root_pids: [...group.root_pids] })),
43
+ };
44
+ return {
45
+ history: {
46
+ schema_version: SUPERVISOR_HISTORY_SCHEMA_VERSION,
47
+ interval_ms: SUPERVISOR_HISTORY_INTERVAL_MS,
48
+ max_points: SUPERVISOR_HISTORY_MAX_POINTS,
49
+ points: [...prior, point].slice(-SUPERVISOR_HISTORY_MAX_POINTS),
50
+ },
51
+ changed: true,
52
+ };
53
+ }
54
+
55
+ function validHistory(value: SupervisorHistory | undefined): value is SupervisorHistory {
56
+ return (
57
+ value?.schema_version === SUPERVISOR_HISTORY_SCHEMA_VERSION &&
58
+ Array.isArray(value.points) &&
59
+ value.points.length <= SUPERVISOR_HISTORY_MAX_POINTS
60
+ );
61
+ }
@@ -0,0 +1,108 @@
1
+ import type { SupervisorFindingEvidence, SupervisorFindingSeverity } from "./contract.ts";
2
+ import {
3
+ type CompletedHookHealth,
4
+ type HookHealthAggregate,
5
+ SUPERVISOR_HIGH_COMPLETED_HOOK_RSS_BYTES,
6
+ SUPERVISOR_HOOK_RETRY_CLUSTER_COUNT,
7
+ SUPERVISOR_SLOW_COMPLETED_HOOK_MS,
8
+ type SupervisorHookHealth,
9
+ } from "./hook-health.ts";
10
+
11
+ export interface HookHealthAlert {
12
+ finding_kind:
13
+ | "hook.execution-degraded"
14
+ | "hook.execution-slow"
15
+ | "hook.memory-heavy"
16
+ | "hook.retry-cluster";
17
+ severity: SupervisorFindingSeverity;
18
+ scope_id: string;
19
+ summary: string;
20
+ observed_value: number;
21
+ unit: SupervisorFindingEvidence["unit"];
22
+ source: CompletedHookHealth;
23
+ owner_id?: string;
24
+ }
25
+
26
+ export function evaluateHookHealthAlerts(health: SupervisorHookHealth): readonly HookHealthAlert[] {
27
+ if (health.capability.state === "unavailable") return [];
28
+ const alerts: HookHealthAlert[] = [];
29
+ for (const aggregate of health.aggregates) {
30
+ const receipts = health.recent.filter(
31
+ (receipt) => `${receipt.adapter}:${receipt.hook_name}` === aggregate.key,
32
+ );
33
+ const problem = receipts.find(
34
+ (receipt) => receipt.outcome === "faulted" || receipt.outcome === "degraded",
35
+ );
36
+ if (problem) alerts.push(problemAlert(aggregate, problem));
37
+ const slow = maxReceipt(receipts, (receipt) => receipt.duration_ms);
38
+ if (slow && slow.duration_ms >= SUPERVISOR_SLOW_COMPLETED_HOOK_MS) {
39
+ alerts.push({
40
+ finding_kind: "hook.execution-slow",
41
+ severity: "warning",
42
+ scope_id: aggregate.key,
43
+ summary: `${aggregate.hook_name} completed in ${slow.duration_ms} milliseconds.`,
44
+ observed_value: slow.duration_ms,
45
+ unit: "milliseconds",
46
+ source: slow,
47
+ ...(slow.owner_id ? { owner_id: slow.owner_id } : {}),
48
+ });
49
+ }
50
+ const heavy = maxReceipt(receipts, (receipt) => receipt.rss_end_bytes);
51
+ if (heavy && heavy.rss_end_bytes >= SUPERVISOR_HIGH_COMPLETED_HOOK_RSS_BYTES) {
52
+ alerts.push({
53
+ finding_kind: "hook.memory-heavy",
54
+ severity: "warning",
55
+ scope_id: aggregate.key,
56
+ summary: `${aggregate.hook_name} completed with ${heavy.rss_end_bytes} resident bytes.`,
57
+ observed_value: heavy.rss_end_bytes,
58
+ unit: "bytes",
59
+ source: heavy,
60
+ ...(heavy.owner_id ? { owner_id: heavy.owner_id } : {}),
61
+ });
62
+ }
63
+ }
64
+ if (health.summary.retry_count >= SUPERVISOR_HOOK_RETRY_CLUSTER_COUNT) {
65
+ const latestRetry = health.recent.find((receipt) => receipt.retry_worker);
66
+ if (latestRetry) {
67
+ alerts.push({
68
+ finding_kind: "hook.retry-cluster",
69
+ severity: "warning",
70
+ scope_id: `${latestRetry.adapter}:runtime-context-retry`,
71
+ summary: `${health.summary.retry_count} hook retry workers ran inside five minutes.`,
72
+ observed_value: health.summary.retry_count,
73
+ unit: "count",
74
+ source: latestRetry,
75
+ ...(latestRetry.owner_id ? { owner_id: latestRetry.owner_id } : {}),
76
+ });
77
+ }
78
+ }
79
+ return alerts.slice(0, 12);
80
+ }
81
+
82
+ function problemAlert(
83
+ aggregate: HookHealthAggregate,
84
+ receipt: CompletedHookHealth,
85
+ ): HookHealthAlert {
86
+ const severity: SupervisorFindingSeverity =
87
+ receipt.outcome === "faulted" ? "critical" : "warning";
88
+ return {
89
+ finding_kind: "hook.execution-degraded",
90
+ severity,
91
+ scope_id: aggregate.key,
92
+ summary: `${aggregate.hook_name} completed with internal outcome ${receipt.outcome}.`,
93
+ observed_value: aggregate.degraded_count + aggregate.faulted_count,
94
+ unit: "count",
95
+ source: receipt,
96
+ ...(receipt.owner_id ? { owner_id: receipt.owner_id } : {}),
97
+ };
98
+ }
99
+
100
+ function maxReceipt(
101
+ receipts: readonly CompletedHookHealth[],
102
+ value: (receipt: CompletedHookHealth) => number,
103
+ ): CompletedHookHealth | undefined {
104
+ return receipts.reduce<CompletedHookHealth | undefined>(
105
+ (current, receipt) => (!current || value(receipt) > value(current) ? receipt : current),
106
+ undefined,
107
+ );
108
+ }
@@ -0,0 +1,25 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { writePrivateJsonAtomic } from "../storage/atomic-json.ts";
4
+ import { SUPERVISOR_HOOK_HEALTH_SCHEMA_VERSION, type SupervisorHookHealth } from "./hook-health.ts";
5
+
6
+ export function supervisorHookHealthPath(coordRoot: string): string {
7
+ return join(resolve(coordRoot), ".harnery", "supervisor", "hook-health.json");
8
+ }
9
+
10
+ export function writeSupervisorHookHealth(coordRoot: string, value: SupervisorHookHealth): void {
11
+ writePrivateJsonAtomic(supervisorHookHealthPath(coordRoot), value);
12
+ }
13
+
14
+ export function readSupervisorHookHealth(coordRoot: string): SupervisorHookHealth | undefined {
15
+ const path = supervisorHookHealthPath(coordRoot);
16
+ if (!existsSync(path)) return undefined;
17
+ try {
18
+ const value = JSON.parse(readFileSync(path, "utf8")) as Partial<SupervisorHookHealth>;
19
+ return value?.schema_version === SUPERVISOR_HOOK_HEALTH_SCHEMA_VERSION
20
+ ? (value as SupervisorHookHealth)
21
+ : undefined;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
@@ -0,0 +1,301 @@
1
+ import {
2
+ HOOK_HEALTH_EVENT,
3
+ HOOK_HEALTH_RECEIPT_VERSION,
4
+ type HookHealthOutcome,
5
+ } from "../hooks/health.ts";
6
+ import type { HarneryLogRecordV1 } from "../storage/jsonl.ts";
7
+ import type { SupervisorLogFeed } from "./contract.ts";
8
+
9
+ export const SUPERVISOR_HOOK_HEALTH_SCHEMA_VERSION = 1 as const;
10
+ export const SUPERVISOR_HOOK_HEALTH_RECENT_LIMIT = 50;
11
+ export const SUPERVISOR_HOOK_HEALTH_AGGREGATE_LIMIT = 40;
12
+ export const SUPERVISOR_SLOW_COMPLETED_HOOK_MS = 30_000;
13
+ export const SUPERVISOR_HIGH_COMPLETED_HOOK_RSS_BYTES = 512 * 1024 * 1024;
14
+ export const SUPERVISOR_HOOK_RETRY_CLUSTER_COUNT = 3;
15
+ export const SUPERVISOR_HOOK_RETRY_CLUSTER_WINDOW_MS = 5 * 60_000;
16
+
17
+ export interface CompletedHookHealth {
18
+ id: string;
19
+ observed_at: string;
20
+ hook_name: string;
21
+ adapter: "claude-code" | "codex" | "cursor" | "unknown";
22
+ outcome: HookHealthOutcome;
23
+ duration_ms: number;
24
+ rss_start_bytes: number;
25
+ rss_end_bytes: number;
26
+ rss_delta_bytes: number;
27
+ retry_worker: boolean;
28
+ error_count: number;
29
+ error_phases: readonly string[];
30
+ payload_bytes: number;
31
+ pid: number;
32
+ owner_id?: string;
33
+ skipped_reason?: string;
34
+ v3_state?: string;
35
+ }
36
+
37
+ export interface HookHealthAggregate {
38
+ key: string;
39
+ hook_name: string;
40
+ adapter: CompletedHookHealth["adapter"];
41
+ invocation_count: number;
42
+ completed_count: number;
43
+ skipped_count: number;
44
+ degraded_count: number;
45
+ faulted_count: number;
46
+ retry_count: number;
47
+ duration_p50_ms: number;
48
+ duration_p95_ms: number;
49
+ duration_max_ms: number;
50
+ rss_end_max_bytes: number;
51
+ rss_delta_max_bytes: number;
52
+ latest_observed_at: string;
53
+ owner_ids: readonly string[];
54
+ }
55
+
56
+ export interface SupervisorHookHealth {
57
+ schema_version: typeof SUPERVISOR_HOOK_HEALTH_SCHEMA_VERSION;
58
+ captured_at: string;
59
+ capability: {
60
+ source_kind: "hook.terminal-log";
61
+ state: "supported" | "partial" | "unavailable";
62
+ reason?: string;
63
+ };
64
+ source_record_count: number;
65
+ malformed_record_count: number;
66
+ truncated: boolean;
67
+ summary: {
68
+ invocation_count: number;
69
+ degraded_count: number;
70
+ faulted_count: number;
71
+ slow_count: number;
72
+ high_memory_count: number;
73
+ retry_count: number;
74
+ };
75
+ aggregates: readonly HookHealthAggregate[];
76
+ recent: readonly CompletedHookHealth[];
77
+ }
78
+
79
+ export function projectHookHealth(feed: SupervisorLogFeed, now = new Date()): SupervisorHookHealth {
80
+ const lane = feed.lanes.find((candidate) => candidate.family_id === "agent-hook-debug-log");
81
+ if (!lane) return unavailable(now, "hook-log-family-not-visible");
82
+ if (lane.error) return unavailable(now, "hook-log-family-unavailable");
83
+
84
+ const parsed: CompletedHookHealth[] = [];
85
+ let malformed = 0;
86
+ for (const record of lane.records) {
87
+ if (record.event !== HOOK_HEALTH_EVENT) continue;
88
+ const receipt = parseCompletedHookHealth(record);
89
+ if (receipt) parsed.push(receipt);
90
+ else malformed += 1;
91
+ }
92
+ parsed.sort((left, right) => Date.parse(right.observed_at) - Date.parse(left.observed_at));
93
+ const recent = parsed.slice(0, SUPERVISOR_HOOK_HEALTH_RECENT_LIMIT);
94
+ const aggregates = aggregateHookHealth(parsed).slice(0, SUPERVISOR_HOOK_HEALTH_AGGREGATE_LIMIT);
95
+ const retryCutoff = now.getTime() - SUPERVISOR_HOOK_RETRY_CLUSTER_WINDOW_MS;
96
+ return {
97
+ schema_version: SUPERVISOR_HOOK_HEALTH_SCHEMA_VERSION,
98
+ captured_at: now.toISOString(),
99
+ capability: {
100
+ source_kind: "hook.terminal-log",
101
+ state: lane.truncated || malformed > 0 ? "partial" : "supported",
102
+ ...(lane.truncated
103
+ ? { reason: "bounded-log-window-truncated" }
104
+ : malformed > 0
105
+ ? { reason: "malformed-terminal-records-ignored" }
106
+ : {}),
107
+ },
108
+ source_record_count: parsed.length,
109
+ malformed_record_count: malformed,
110
+ truncated: lane.truncated || parsed.length > SUPERVISOR_HOOK_HEALTH_RECENT_LIMIT,
111
+ summary: {
112
+ invocation_count: parsed.length,
113
+ degraded_count: parsed.filter((receipt) => receipt.outcome === "degraded").length,
114
+ faulted_count: parsed.filter((receipt) => receipt.outcome === "faulted").length,
115
+ slow_count: parsed.filter(
116
+ (receipt) => receipt.duration_ms >= SUPERVISOR_SLOW_COMPLETED_HOOK_MS,
117
+ ).length,
118
+ high_memory_count: parsed.filter(
119
+ (receipt) => receipt.rss_end_bytes >= SUPERVISOR_HIGH_COMPLETED_HOOK_RSS_BYTES,
120
+ ).length,
121
+ retry_count: parsed.filter(
122
+ (receipt) => receipt.retry_worker && Date.parse(receipt.observed_at) >= retryCutoff,
123
+ ).length,
124
+ },
125
+ aggregates,
126
+ recent,
127
+ };
128
+ }
129
+
130
+ export function parseCompletedHookHealth(
131
+ record: HarneryLogRecordV1,
132
+ ): CompletedHookHealth | undefined {
133
+ if (
134
+ record.kind !== "record" ||
135
+ record.component_id !== "agent-hook" ||
136
+ record.event !== HOOK_HEALTH_EVENT ||
137
+ record.fields.receipt_version !== HOOK_HEALTH_RECEIPT_VERSION ||
138
+ record.fields.exit_contract !== "always-zero" ||
139
+ record.fields.exit_code !== 0
140
+ ) {
141
+ return undefined;
142
+ }
143
+ const hookName = token(record.fields.hook_name);
144
+ const adapter = adapterValue(record.fields.adapter);
145
+ const outcome = outcomeValue(record.fields.outcome);
146
+ const duration = integer(record.fields.duration_ms);
147
+ const rssStart = integer(record.fields.rss_start_bytes);
148
+ const rssEnd = integer(record.fields.rss_end_bytes);
149
+ const rssDelta = signedInteger(record.fields.rss_delta_bytes);
150
+ const payloadBytes = integer(record.fields.payload_bytes);
151
+ const pid = integer(record.fields.pid);
152
+ const errorCount = integer(record.fields.error_count);
153
+ const retryWorker = record.fields.retry_worker;
154
+ const errorPhases = tokenArray(record.fields.error_phases);
155
+ if (
156
+ !hookName ||
157
+ !adapter ||
158
+ !outcome ||
159
+ duration === undefined ||
160
+ rssStart === undefined ||
161
+ rssEnd === undefined ||
162
+ rssDelta === undefined ||
163
+ payloadBytes === undefined ||
164
+ pid === undefined ||
165
+ errorCount === undefined ||
166
+ typeof retryWorker !== "boolean" ||
167
+ !errorPhases ||
168
+ !Number.isFinite(Date.parse(record.emitted_at))
169
+ ) {
170
+ return undefined;
171
+ }
172
+ return {
173
+ id: `${record.writer_id}:${record.writer_seq}`,
174
+ observed_at: record.emitted_at,
175
+ hook_name: hookName,
176
+ adapter,
177
+ outcome,
178
+ duration_ms: duration,
179
+ rss_start_bytes: rssStart,
180
+ rss_end_bytes: rssEnd,
181
+ rss_delta_bytes: rssDelta,
182
+ retry_worker: retryWorker,
183
+ error_count: errorCount,
184
+ error_phases: errorPhases,
185
+ payload_bytes: payloadBytes,
186
+ pid,
187
+ ...optionalToken("owner_id", record.fields.owner_id),
188
+ ...optionalToken("skipped_reason", record.fields.skipped_reason),
189
+ ...optionalToken("v3_state", record.fields.v3_state),
190
+ };
191
+ }
192
+
193
+ function aggregateHookHealth(receipts: readonly CompletedHookHealth[]): HookHealthAggregate[] {
194
+ const grouped = new Map<string, CompletedHookHealth[]>();
195
+ for (const receipt of receipts) {
196
+ const key = `${receipt.adapter}:${receipt.hook_name}`;
197
+ const group = grouped.get(key) ?? [];
198
+ group.push(receipt);
199
+ grouped.set(key, group);
200
+ }
201
+ return [...grouped.entries()]
202
+ .map(([key, group]) => {
203
+ const durations = group.map((receipt) => receipt.duration_ms).sort((a, b) => a - b);
204
+ return {
205
+ key,
206
+ hook_name: group[0]!.hook_name,
207
+ adapter: group[0]!.adapter,
208
+ invocation_count: group.length,
209
+ completed_count: countOutcome(group, "completed"),
210
+ skipped_count: countOutcome(group, "skipped"),
211
+ degraded_count: countOutcome(group, "degraded"),
212
+ faulted_count: countOutcome(group, "faulted"),
213
+ retry_count: group.filter((receipt) => receipt.retry_worker).length,
214
+ duration_p50_ms: percentile(durations, 0.5),
215
+ duration_p95_ms: percentile(durations, 0.95),
216
+ duration_max_ms: durations.at(-1) ?? 0,
217
+ rss_end_max_bytes: Math.max(...group.map((receipt) => receipt.rss_end_bytes)),
218
+ rss_delta_max_bytes: Math.max(...group.map((receipt) => receipt.rss_delta_bytes)),
219
+ latest_observed_at: group[0]!.observed_at,
220
+ owner_ids: [...new Set(group.flatMap((receipt) => receipt.owner_id ?? []))].slice(0, 8),
221
+ };
222
+ })
223
+ .sort(
224
+ (left, right) =>
225
+ Date.parse(right.latest_observed_at) - Date.parse(left.latest_observed_at) ||
226
+ left.key.localeCompare(right.key),
227
+ );
228
+ }
229
+
230
+ function countOutcome(
231
+ receipts: readonly CompletedHookHealth[],
232
+ outcome: HookHealthOutcome,
233
+ ): number {
234
+ return receipts.filter((receipt) => receipt.outcome === outcome).length;
235
+ }
236
+
237
+ function percentile(sorted: readonly number[], fraction: number): number {
238
+ if (sorted.length === 0) return 0;
239
+ return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * fraction) - 1))]!;
240
+ }
241
+
242
+ function unavailable(now: Date, reason: string): SupervisorHookHealth {
243
+ return {
244
+ schema_version: SUPERVISOR_HOOK_HEALTH_SCHEMA_VERSION,
245
+ captured_at: now.toISOString(),
246
+ capability: { source_kind: "hook.terminal-log", state: "unavailable", reason },
247
+ source_record_count: 0,
248
+ malformed_record_count: 0,
249
+ truncated: false,
250
+ summary: {
251
+ invocation_count: 0,
252
+ degraded_count: 0,
253
+ faulted_count: 0,
254
+ slow_count: 0,
255
+ high_memory_count: 0,
256
+ retry_count: 0,
257
+ },
258
+ aggregates: [],
259
+ recent: [],
260
+ };
261
+ }
262
+
263
+ function token(value: unknown): string | undefined {
264
+ return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,127}$/.test(value)
265
+ ? value
266
+ : undefined;
267
+ }
268
+
269
+ function adapterValue(value: unknown): CompletedHookHealth["adapter"] | undefined {
270
+ return value === "claude-code" || value === "codex" || value === "cursor" || value === "unknown"
271
+ ? value
272
+ : undefined;
273
+ }
274
+
275
+ function outcomeValue(value: unknown): HookHealthOutcome | undefined {
276
+ return value === "completed" || value === "skipped" || value === "degraded" || value === "faulted"
277
+ ? value
278
+ : undefined;
279
+ }
280
+
281
+ function integer(value: unknown): number | undefined {
282
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
283
+ }
284
+
285
+ function signedInteger(value: unknown): number | undefined {
286
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
287
+ }
288
+
289
+ function tokenArray(value: unknown): readonly string[] | undefined {
290
+ return Array.isArray(value) && value.length <= 8 && value.every((item) => token(item))
291
+ ? (value as string[])
292
+ : undefined;
293
+ }
294
+
295
+ function optionalToken<Key extends "owner_id" | "skipped_reason" | "v3_state">(
296
+ key: Key,
297
+ value: unknown,
298
+ ): Partial<Record<Key, string>> {
299
+ const parsed = token(value);
300
+ return parsed ? ({ [key]: parsed } as Partial<Record<Key, string>>) : {};
301
+ }
@@ -0,0 +1,35 @@
1
+ import type { ResourceSnapshot } from "../resources/contract.ts";
2
+ import type { ObservedHookHealth } from "./contract.ts";
3
+
4
+ export const SUPERVISOR_LONG_HOOK_SECONDS = 120;
5
+
6
+ /**
7
+ * Command text may label a process only after PID ancestry proved its agent
8
+ * owner. It never establishes ownership by itself.
9
+ */
10
+ export function collectHookHealth(resource: ResourceSnapshot): ObservedHookHealth[] {
11
+ return resource.processes
12
+ .filter(
13
+ (row) =>
14
+ row.owner_kind === "agent" &&
15
+ row.owner_id !== null &&
16
+ exactHookEntrypoint(row.name, row.command),
17
+ )
18
+ .map((row) => ({
19
+ pid: row.pid,
20
+ owner_id: row.owner_id!,
21
+ state: row.state,
22
+ age_seconds: row.age_seconds,
23
+ cpu_percent: row.cpu_percent,
24
+ rss_bytes: row.rss_bytes,
25
+ command: row.command,
26
+ long_running: row.age_seconds >= SUPERVISOR_LONG_HOOK_SECONDS,
27
+ evidence: "validated-owner-and-exact-entrypoint" as const,
28
+ }))
29
+ .sort((left, right) => right.age_seconds - left.age_seconds || left.pid - right.pid);
30
+ }
31
+
32
+ export function exactHookEntrypoint(name: string, command: string): boolean {
33
+ if (name === "agent-hook" || name === "agent-hook.exe") return true;
34
+ return /(?:^|[\\/\s])agent-hook(?:\.exe)?(?:\s|$)/.test(command);
35
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./contract.ts";
2
+ export * from "./explanations.ts";
3
+ export * from "./findings.ts";
4
+ export * from "./history.ts";
5
+ export * from "./hook-health.ts";
6
+ export * from "./hook-health-alerts.ts";
7
+ export * from "./hook-health-storage.ts";
8
+ export * from "./hooks.ts";
9
+ export * from "./log-feed.ts";
10
+ export * from "./service.ts";
11
+ export * from "./services.ts";
12
+ export * from "./status.ts";
13
+ export * from "./storage.ts";
14
+ export * from "./timeline.ts";
@@ -0,0 +1,131 @@
1
+ import { createStorageCatalog } from "../storage/catalog.ts";
2
+ import {
3
+ HARNERY_STRUCTURED_LOG_PROVIDER_ID,
4
+ type HarneryRegisteredStorageFamily,
5
+ } from "../storage/contract.ts";
6
+ import type { HarneryLogRecordV1 } from "../storage/jsonl.ts";
7
+ import {
8
+ type HarneryLogFollowCursor,
9
+ readLogFollow,
10
+ readRecentActiveLogs,
11
+ rotationFollowCursor,
12
+ } from "../storage/query.ts";
13
+ import {
14
+ SUPERVISOR_LOG_FEED_SCHEMA_VERSION,
15
+ type SupervisorLogFeed,
16
+ type SupervisorLogLane,
17
+ } from "./contract.ts";
18
+
19
+ const MAX_RECORDS_PER_FAMILY = 100;
20
+ const MAX_SEED_BYTES_PER_FAMILY = 256 * 1024;
21
+ const MAX_FOLLOW_BYTES_PER_FAMILY = 64 * 1024;
22
+
23
+ interface LaneState {
24
+ family: HarneryRegisteredStorageFamily;
25
+ cursor: HarneryLogFollowCursor;
26
+ records: HarneryLogRecordV1[];
27
+ truncated: boolean;
28
+ error?: string;
29
+ }
30
+
31
+ export class SupervisorLogCollector {
32
+ readonly #lanes: LaneState[];
33
+ #sequence = 0;
34
+
35
+ constructor(coordRoot: string) {
36
+ const catalog = createStorageCatalog({ coord_root: coordRoot, project_root: coordRoot });
37
+ this.#lanes = catalog.families.filter(isStructuredLogFamily).map((family) => seedLane(family));
38
+ }
39
+
40
+ async collect(now = new Date()): Promise<SupervisorLogFeed> {
41
+ let changed = this.#sequence === 0;
42
+ for (const lane of this.#lanes) {
43
+ try {
44
+ const followed = await readLogFollow(lane.family, lane.cursor, MAX_FOLLOW_BYTES_PER_FAMILY);
45
+ lane.cursor = followed.cursor;
46
+ if (followed.records.length > 0 || followed.rotated || followed.history_expired) {
47
+ const merged = dedupe([...lane.records, ...followed.records]);
48
+ lane.records = merged.slice(-MAX_RECORDS_PER_FAMILY);
49
+ lane.truncated =
50
+ lane.truncated || followed.history_expired || merged.length > MAX_RECORDS_PER_FAMILY;
51
+ changed = true;
52
+ }
53
+ if (lane.error) changed = true;
54
+ lane.error = undefined;
55
+ } catch (error) {
56
+ const next = error instanceof Error ? error.message : String(error);
57
+ if (next !== lane.error) changed = true;
58
+ lane.error = next;
59
+ }
60
+ }
61
+ if (changed) this.#sequence += 1;
62
+ const lanes: SupervisorLogLane[] = this.#lanes
63
+ .map((lane) => ({
64
+ family_id: lane.family.id,
65
+ owner: lane.family.owner,
66
+ storage_class: lane.family.storage_class as "operational-log" | "debug-log",
67
+ records: lane.records,
68
+ truncated: lane.truncated,
69
+ ...(lane.error ? { error: lane.error } : {}),
70
+ }))
71
+ .sort((left, right) => {
72
+ const activeDelta = Number(right.records.length > 0) - Number(left.records.length > 0);
73
+ return activeDelta || left.family_id.localeCompare(right.family_id);
74
+ });
75
+ return {
76
+ schema_version: SUPERVISOR_LOG_FEED_SCHEMA_VERSION,
77
+ captured_at: now.toISOString(),
78
+ sequence: this.#sequence,
79
+ lanes,
80
+ total_records: lanes.reduce((sum, lane) => sum + lane.records.length, 0),
81
+ unavailable_families: lanes.filter((lane) => lane.error).length,
82
+ };
83
+ }
84
+ }
85
+
86
+ export function recentSupervisorLogs(feed: SupervisorLogFeed, limit = 20): HarneryLogRecordV1[] {
87
+ return feed.lanes
88
+ .flatMap((lane) => lane.records)
89
+ .sort((left, right) => Date.parse(left.emitted_at) - Date.parse(right.emitted_at))
90
+ .slice(-Math.max(0, limit));
91
+ }
92
+
93
+ function seedLane(family: HarneryRegisteredStorageFamily): LaneState {
94
+ try {
95
+ const seed = readRecentActiveLogs(family, {
96
+ max_records: MAX_RECORDS_PER_FAMILY,
97
+ max_bytes: MAX_SEED_BYTES_PER_FAMILY,
98
+ });
99
+ return {
100
+ family,
101
+ cursor: rotationFollowCursor(family),
102
+ records: [...seed.records],
103
+ truncated: seed.truncated,
104
+ };
105
+ } catch (error) {
106
+ return {
107
+ family,
108
+ cursor: rotationFollowCursor(family),
109
+ records: [],
110
+ truncated: false,
111
+ error: error instanceof Error ? error.message : String(error),
112
+ };
113
+ }
114
+ }
115
+
116
+ function isStructuredLogFamily(family: HarneryRegisteredStorageFamily): boolean {
117
+ return (
118
+ (family.storage_class === "operational-log" || family.storage_class === "debug-log") &&
119
+ family.provider.provider_id === HARNERY_STRUCTURED_LOG_PROVIDER_ID
120
+ );
121
+ }
122
+
123
+ function dedupe(records: readonly HarneryLogRecordV1[]): HarneryLogRecordV1[] {
124
+ const keyed = new Map<string, HarneryLogRecordV1>();
125
+ for (const record of records) {
126
+ keyed.set(`${record.family_id}:${record.writer_id}:${record.writer_seq}`, record);
127
+ }
128
+ return [...keyed.values()].sort(
129
+ (left, right) => Date.parse(left.emitted_at) - Date.parse(right.emitted_at),
130
+ );
131
+ }