pi-smart-router 0.1.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 (310) hide show
  1. package/.pi/extensions/smart-router/command-formatters.ts +167 -0
  2. package/.pi/extensions/smart-router/commands.ts +242 -0
  3. package/.pi/extensions/smart-router/dataset-export.ts +142 -0
  4. package/.pi/extensions/smart-router/delegate-stream.ts +105 -0
  5. package/.pi/extensions/smart-router/delegation-runtime.ts +355 -0
  6. package/.pi/extensions/smart-router/extension-setup.ts +115 -0
  7. package/.pi/extensions/smart-router/fleet-bootstrap.ts +244 -0
  8. package/.pi/extensions/smart-router/index.ts +115 -0
  9. package/.pi/extensions/smart-router/package.json +3 -0
  10. package/.pi/extensions/smart-router/pi-model-scope.ts +55 -0
  11. package/.pi/extensions/smart-router/pricing-lifecycle.ts +43 -0
  12. package/.pi/extensions/smart-router/route-and-delegate.ts +562 -0
  13. package/.pi/extensions/smart-router/routing-context.ts +171 -0
  14. package/.pi/extensions/smart-router/routing-outcomes.ts +44 -0
  15. package/.pi/extensions/smart-router/session-lifecycle.ts +140 -0
  16. package/.pi/extensions/smart-router/stream-delegation.ts +75 -0
  17. package/.pi/extensions/smart-router/types.ts +90 -0
  18. package/.pi/extensions/smart-router/utils.ts +58 -0
  19. package/README.md +593 -0
  20. package/bin/pi-smart-router.mjs +79 -0
  21. package/config/.gitkeep +0 -0
  22. package/config/hydra-projection-weights.json.example +1169 -0
  23. package/config/models.yaml.example +58 -0
  24. package/config/p-success-weights.json.example +19 -0
  25. package/config/routing-calibration.json.example +1605 -0
  26. package/config/routing-centroids.json.example +1570 -0
  27. package/config/routing-clusters.yaml.example +41 -0
  28. package/data/contrib/example.json +25 -0
  29. package/dist/api/explain/router-explain.d.ts +47 -0
  30. package/dist/api/explain/router-explain.d.ts.map +1 -0
  31. package/dist/api/explain/router-explain.js +101 -0
  32. package/dist/api/explain/router-explain.js.map +1 -0
  33. package/dist/api/middleware/pi-router-middleware.d.ts +86 -0
  34. package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -0
  35. package/dist/api/middleware/pi-router-middleware.js +83 -0
  36. package/dist/api/middleware/pi-router-middleware.js.map +1 -0
  37. package/dist/cli/smart-router-cli.d.ts +85 -0
  38. package/dist/cli/smart-router-cli.d.ts.map +1 -0
  39. package/dist/cli/smart-router-cli.js +293 -0
  40. package/dist/cli/smart-router-cli.js.map +1 -0
  41. package/dist/config/defaults.d.ts +7 -0
  42. package/dist/config/defaults.d.ts.map +1 -0
  43. package/dist/config/defaults.js +33 -0
  44. package/dist/config/defaults.js.map +1 -0
  45. package/dist/config/models-loader.d.ts +22 -0
  46. package/dist/config/models-loader.d.ts.map +1 -0
  47. package/dist/config/models-loader.js +51 -0
  48. package/dist/config/models-loader.js.map +1 -0
  49. package/dist/config/pi-model-mapper.d.ts +40 -0
  50. package/dist/config/pi-model-mapper.d.ts.map +1 -0
  51. package/dist/config/pi-model-mapper.js +216 -0
  52. package/dist/config/pi-model-mapper.js.map +1 -0
  53. package/dist/config/routing-clusters-loader.d.ts +35 -0
  54. package/dist/config/routing-clusters-loader.d.ts.map +1 -0
  55. package/dist/config/routing-clusters-loader.js +100 -0
  56. package/dist/config/routing-clusters-loader.js.map +1 -0
  57. package/dist/domain/delegation/delegation-context.d.ts +23 -0
  58. package/dist/domain/delegation/delegation-context.d.ts.map +1 -0
  59. package/dist/domain/delegation/delegation-context.js +73 -0
  60. package/dist/domain/delegation/delegation-context.js.map +1 -0
  61. package/dist/domain/delegation/execution-ledger.d.ts +19 -0
  62. package/dist/domain/delegation/execution-ledger.d.ts.map +1 -0
  63. package/dist/domain/delegation/execution-ledger.js +23 -0
  64. package/dist/domain/delegation/execution-ledger.js.map +1 -0
  65. package/dist/domain/delegation/output-headroom.d.ts +39 -0
  66. package/dist/domain/delegation/output-headroom.d.ts.map +1 -0
  67. package/dist/domain/delegation/output-headroom.js +78 -0
  68. package/dist/domain/delegation/output-headroom.js.map +1 -0
  69. package/dist/domain/matching/cluster-matcher.d.ts +69 -0
  70. package/dist/domain/matching/cluster-matcher.d.ts.map +1 -0
  71. package/dist/domain/matching/cluster-matcher.js +294 -0
  72. package/dist/domain/matching/cluster-matcher.js.map +1 -0
  73. package/dist/domain/matching/embedding-provider.d.ts +21 -0
  74. package/dist/domain/matching/embedding-provider.d.ts.map +1 -0
  75. package/dist/domain/matching/embedding-provider.js +42 -0
  76. package/dist/domain/matching/embedding-provider.js.map +1 -0
  77. package/dist/domain/matching/hydra-input.d.ts +18 -0
  78. package/dist/domain/matching/hydra-input.d.ts.map +1 -0
  79. package/dist/domain/matching/hydra-input.js +40 -0
  80. package/dist/domain/matching/hydra-input.js.map +1 -0
  81. package/dist/domain/matching/hydra-matcher.d.ts +103 -0
  82. package/dist/domain/matching/hydra-matcher.d.ts.map +1 -0
  83. package/dist/domain/matching/hydra-matcher.js +275 -0
  84. package/dist/domain/matching/hydra-matcher.js.map +1 -0
  85. package/dist/domain/pinning/cache-economics.d.ts +53 -0
  86. package/dist/domain/pinning/cache-economics.d.ts.map +1 -0
  87. package/dist/domain/pinning/cache-economics.js +72 -0
  88. package/dist/domain/pinning/cache-economics.js.map +1 -0
  89. package/dist/domain/pinning/loop-escalation.d.ts +41 -0
  90. package/dist/domain/pinning/loop-escalation.d.ts.map +1 -0
  91. package/dist/domain/pinning/loop-escalation.js +137 -0
  92. package/dist/domain/pinning/loop-escalation.js.map +1 -0
  93. package/dist/domain/pinning/session-pinner.d.ts +81 -0
  94. package/dist/domain/pinning/session-pinner.d.ts.map +1 -0
  95. package/dist/domain/pinning/session-pinner.js +237 -0
  96. package/dist/domain/pinning/session-pinner.js.map +1 -0
  97. package/dist/domain/pinning/sub-route-policy.d.ts +41 -0
  98. package/dist/domain/pinning/sub-route-policy.d.ts.map +1 -0
  99. package/dist/domain/pinning/sub-route-policy.js +72 -0
  100. package/dist/domain/pinning/sub-route-policy.js.map +1 -0
  101. package/dist/domain/pipeline/router-pipeline.d.ts +180 -0
  102. package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -0
  103. package/dist/domain/pipeline/router-pipeline.js +807 -0
  104. package/dist/domain/pipeline/router-pipeline.js.map +1 -0
  105. package/dist/domain/pipeline/safe-default.d.ts +25 -0
  106. package/dist/domain/pipeline/safe-default.d.ts.map +1 -0
  107. package/dist/domain/pipeline/safe-default.js +41 -0
  108. package/dist/domain/pipeline/safe-default.js.map +1 -0
  109. package/dist/domain/refactor-guardrails.d.ts +48 -0
  110. package/dist/domain/refactor-guardrails.d.ts.map +1 -0
  111. package/dist/domain/refactor-guardrails.js +129 -0
  112. package/dist/domain/refactor-guardrails.js.map +1 -0
  113. package/dist/domain/routing/context-fit.d.ts +53 -0
  114. package/dist/domain/routing/context-fit.d.ts.map +1 -0
  115. package/dist/domain/routing/context-fit.js +142 -0
  116. package/dist/domain/routing/context-fit.js.map +1 -0
  117. package/dist/domain/routing/expected-cost.d.ts +63 -0
  118. package/dist/domain/routing/expected-cost.d.ts.map +1 -0
  119. package/dist/domain/routing/expected-cost.js +177 -0
  120. package/dist/domain/routing/expected-cost.js.map +1 -0
  121. package/dist/domain/routing/p-success-classifier.d.ts +130 -0
  122. package/dist/domain/routing/p-success-classifier.d.ts.map +1 -0
  123. package/dist/domain/routing/p-success-classifier.js +350 -0
  124. package/dist/domain/routing/p-success-classifier.js.map +1 -0
  125. package/dist/domain/routing/tier-features.d.ts +70 -0
  126. package/dist/domain/routing/tier-features.d.ts.map +1 -0
  127. package/dist/domain/routing/tier-features.js +174 -0
  128. package/dist/domain/routing/tier-features.js.map +1 -0
  129. package/dist/domain/routing/tool-history-guard.d.ts +32 -0
  130. package/dist/domain/routing/tool-history-guard.d.ts.map +1 -0
  131. package/dist/domain/routing/tool-history-guard.js +110 -0
  132. package/dist/domain/routing/tool-history-guard.js.map +1 -0
  133. package/dist/domain/scoring/multi-objective.d.ts +48 -0
  134. package/dist/domain/scoring/multi-objective.d.ts.map +1 -0
  135. package/dist/domain/scoring/multi-objective.js +116 -0
  136. package/dist/domain/scoring/multi-objective.js.map +1 -0
  137. package/dist/domain/triage/triage-engine.d.ts +43 -0
  138. package/dist/domain/triage/triage-engine.d.ts.map +1 -0
  139. package/dist/domain/triage/triage-engine.js +317 -0
  140. package/dist/domain/triage/triage-engine.js.map +1 -0
  141. package/dist/domain/triage/turn-envelope.d.ts +17 -0
  142. package/dist/domain/triage/turn-envelope.d.ts.map +1 -0
  143. package/dist/domain/triage/turn-envelope.js +81 -0
  144. package/dist/domain/triage/turn-envelope.js.map +1 -0
  145. package/dist/domain/types/entities.d.ts +303 -0
  146. package/dist/domain/types/entities.d.ts.map +1 -0
  147. package/dist/domain/types/entities.js +6 -0
  148. package/dist/domain/types/entities.js.map +1 -0
  149. package/dist/domain/types/index.d.ts +4 -0
  150. package/dist/domain/types/index.d.ts.map +1 -0
  151. package/dist/domain/types/index.js +2 -0
  152. package/dist/domain/types/index.js.map +1 -0
  153. package/dist/domain/types/schemas.d.ts +346 -0
  154. package/dist/domain/types/schemas.d.ts.map +1 -0
  155. package/dist/domain/types/schemas.js +241 -0
  156. package/dist/domain/types/schemas.js.map +1 -0
  157. package/dist/domain/types/store-port.d.ts +44 -0
  158. package/dist/domain/types/store-port.d.ts.map +1 -0
  159. package/dist/domain/types/store-port.js +6 -0
  160. package/dist/domain/types/store-port.js.map +1 -0
  161. package/dist/index.d.ts +39 -0
  162. package/dist/index.d.ts.map +1 -0
  163. package/dist/index.js +36 -0
  164. package/dist/index.js.map +1 -0
  165. package/dist/infra/gemini-provider.d.ts +28 -0
  166. package/dist/infra/gemini-provider.d.ts.map +1 -0
  167. package/dist/infra/gemini-provider.js +112 -0
  168. package/dist/infra/gemini-provider.js.map +1 -0
  169. package/dist/infra/telemetry.d.ts +51 -0
  170. package/dist/infra/telemetry.d.ts.map +1 -0
  171. package/dist/infra/telemetry.js +100 -0
  172. package/dist/infra/telemetry.js.map +1 -0
  173. package/dist/infrastructure/delegation/provider-error.d.ts +44 -0
  174. package/dist/infrastructure/delegation/provider-error.d.ts.map +1 -0
  175. package/dist/infrastructure/delegation/provider-error.js +179 -0
  176. package/dist/infrastructure/delegation/provider-error.js.map +1 -0
  177. package/dist/infrastructure/gateway/circuit-breaker.d.ts +68 -0
  178. package/dist/infrastructure/gateway/circuit-breaker.d.ts.map +1 -0
  179. package/dist/infrastructure/gateway/circuit-breaker.js +150 -0
  180. package/dist/infrastructure/gateway/circuit-breaker.js.map +1 -0
  181. package/dist/infrastructure/gateway/gateway-dispatch.d.ts +138 -0
  182. package/dist/infrastructure/gateway/gateway-dispatch.d.ts.map +1 -0
  183. package/dist/infrastructure/gateway/gateway-dispatch.js +308 -0
  184. package/dist/infrastructure/gateway/gateway-dispatch.js.map +1 -0
  185. package/dist/infrastructure/hardware/hardware-probe.d.ts +32 -0
  186. package/dist/infrastructure/hardware/hardware-probe.d.ts.map +1 -0
  187. package/dist/infrastructure/hardware/hardware-probe.js +199 -0
  188. package/dist/infrastructure/hardware/hardware-probe.js.map +1 -0
  189. package/dist/infrastructure/local/local-zero-tier.d.ts +47 -0
  190. package/dist/infrastructure/local/local-zero-tier.d.ts.map +1 -0
  191. package/dist/infrastructure/local/local-zero-tier.js +92 -0
  192. package/dist/infrastructure/local/local-zero-tier.js.map +1 -0
  193. package/dist/infrastructure/persistence/memory-store.d.ts +31 -0
  194. package/dist/infrastructure/persistence/memory-store.d.ts.map +1 -0
  195. package/dist/infrastructure/persistence/memory-store.js +87 -0
  196. package/dist/infrastructure/persistence/memory-store.js.map +1 -0
  197. package/dist/infrastructure/persistence/sqlite-store.d.ts +81 -0
  198. package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -0
  199. package/dist/infrastructure/persistence/sqlite-store.js +658 -0
  200. package/dist/infrastructure/persistence/sqlite-store.js.map +1 -0
  201. package/dist/infrastructure/pricing/litellm-fetch.d.ts +46 -0
  202. package/dist/infrastructure/pricing/litellm-fetch.d.ts.map +1 -0
  203. package/dist/infrastructure/pricing/litellm-fetch.js +145 -0
  204. package/dist/infrastructure/pricing/litellm-fetch.js.map +1 -0
  205. package/dist/infrastructure/pricing/price-broker.d.ts +56 -0
  206. package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -0
  207. package/dist/infrastructure/pricing/price-broker.js +133 -0
  208. package/dist/infrastructure/pricing/price-broker.js.map +1 -0
  209. package/dist/infrastructure/pricing/pricing-monitor.d.ts +27 -0
  210. package/dist/infrastructure/pricing/pricing-monitor.d.ts.map +1 -0
  211. package/dist/infrastructure/pricing/pricing-monitor.js +46 -0
  212. package/dist/infrastructure/pricing/pricing-monitor.js.map +1 -0
  213. package/dist/infrastructure/telemetry/dataset-limits.d.ts +13 -0
  214. package/dist/infrastructure/telemetry/dataset-limits.d.ts.map +1 -0
  215. package/dist/infrastructure/telemetry/dataset-limits.js +28 -0
  216. package/dist/infrastructure/telemetry/dataset-limits.js.map +1 -0
  217. package/dist/infrastructure/telemetry/dataset-recorder.d.ts +48 -0
  218. package/dist/infrastructure/telemetry/dataset-recorder.d.ts.map +1 -0
  219. package/dist/infrastructure/telemetry/dataset-recorder.js +154 -0
  220. package/dist/infrastructure/telemetry/dataset-recorder.js.map +1 -0
  221. package/dist/infrastructure/telemetry/outcome-limits.d.ts +12 -0
  222. package/dist/infrastructure/telemetry/outcome-limits.d.ts.map +1 -0
  223. package/dist/infrastructure/telemetry/outcome-limits.js +28 -0
  224. package/dist/infrastructure/telemetry/outcome-limits.js.map +1 -0
  225. package/dist/infrastructure/telemetry/outcome-recorder.d.ts +29 -0
  226. package/dist/infrastructure/telemetry/outcome-recorder.d.ts.map +1 -0
  227. package/dist/infrastructure/telemetry/outcome-recorder.js +49 -0
  228. package/dist/infrastructure/telemetry/outcome-recorder.js.map +1 -0
  229. package/dist/infrastructure/telemetry/routing-telemetry.d.ts +115 -0
  230. package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -0
  231. package/dist/infrastructure/telemetry/routing-telemetry.js +529 -0
  232. package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -0
  233. package/dist/infrastructure/telemetry/telemetry-limits.d.ts +16 -0
  234. package/dist/infrastructure/telemetry/telemetry-limits.d.ts.map +1 -0
  235. package/dist/infrastructure/telemetry/telemetry-limits.js +35 -0
  236. package/dist/infrastructure/telemetry/telemetry-limits.js.map +1 -0
  237. package/package.json +110 -0
  238. package/skills/router-backlog-orchestrator/SKILL.md +183 -0
  239. package/skills/router-backlog-orchestrator/references/github-router-issue-template.md +78 -0
  240. package/skills/router-backlog-orchestrator/references/packet-from-issue.md +99 -0
  241. package/skills/router-backlog-orchestrator/references/prioritization-rubric.md +65 -0
  242. package/skills/router-backlog-orchestrator/scripts/collect-backlog.sh +44 -0
  243. package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +151 -0
  244. package/src/api/explain/.gitkeep +0 -0
  245. package/src/api/explain/router-explain.ts +146 -0
  246. package/src/api/middleware/.gitkeep +0 -0
  247. package/src/api/middleware/pi-router-middleware.ts +169 -0
  248. package/src/cli/smart-router-cli.ts +436 -0
  249. package/src/config/.gitkeep +0 -0
  250. package/src/config/defaults.ts +35 -0
  251. package/src/config/models-loader.ts +68 -0
  252. package/src/config/pi-model-mapper.ts +285 -0
  253. package/src/config/routing-clusters-loader.ts +148 -0
  254. package/src/domain/delegation/delegation-context.ts +111 -0
  255. package/src/domain/delegation/execution-ledger.ts +34 -0
  256. package/src/domain/delegation/output-headroom.ts +131 -0
  257. package/src/domain/matching/.gitkeep +0 -0
  258. package/src/domain/matching/cluster-matcher.ts +474 -0
  259. package/src/domain/matching/embedding-provider.ts +79 -0
  260. package/src/domain/matching/hydra-input.ts +53 -0
  261. package/src/domain/matching/hydra-matcher.ts +454 -0
  262. package/src/domain/pinning/.gitkeep +0 -0
  263. package/src/domain/pinning/cache-economics.ts +128 -0
  264. package/src/domain/pinning/loop-escalation.ts +181 -0
  265. package/src/domain/pinning/session-pinner.ts +388 -0
  266. package/src/domain/pinning/sub-route-policy.ts +119 -0
  267. package/src/domain/pipeline/.gitkeep +0 -0
  268. package/src/domain/pipeline/router-pipeline.ts +1139 -0
  269. package/src/domain/pipeline/safe-default.ts +70 -0
  270. package/src/domain/refactor-guardrails.ts +202 -0
  271. package/src/domain/routing/context-fit.ts +221 -0
  272. package/src/domain/routing/expected-cost.ts +320 -0
  273. package/src/domain/routing/p-success-classifier.ts +511 -0
  274. package/src/domain/routing/tier-features.ts +290 -0
  275. package/src/domain/routing/tool-history-guard.ts +159 -0
  276. package/src/domain/scoring/.gitkeep +0 -0
  277. package/src/domain/scoring/multi-objective.ts +175 -0
  278. package/src/domain/triage/.gitkeep +0 -0
  279. package/src/domain/triage/triage-engine.ts +373 -0
  280. package/src/domain/triage/turn-envelope.ts +104 -0
  281. package/src/domain/types/.gitkeep +0 -0
  282. package/src/domain/types/entities.ts +384 -0
  283. package/src/domain/types/index.ts +41 -0
  284. package/src/domain/types/schemas.ts +289 -0
  285. package/src/domain/types/store-port.ts +59 -0
  286. package/src/index.ts +103 -0
  287. package/src/infra/gemini-provider.ts +142 -0
  288. package/src/infra/telemetry.ts +169 -0
  289. package/src/infrastructure/delegation/provider-error.ts +251 -0
  290. package/src/infrastructure/gateway/.gitkeep +0 -0
  291. package/src/infrastructure/gateway/circuit-breaker.ts +196 -0
  292. package/src/infrastructure/gateway/gateway-dispatch.ts +444 -0
  293. package/src/infrastructure/hardware/.gitkeep +0 -0
  294. package/src/infrastructure/hardware/hardware-probe.ts +276 -0
  295. package/src/infrastructure/local/.gitkeep +0 -0
  296. package/src/infrastructure/local/local-zero-tier.ts +151 -0
  297. package/src/infrastructure/persistence/.gitkeep +0 -0
  298. package/src/infrastructure/persistence/memory-store.ts +122 -0
  299. package/src/infrastructure/persistence/sqlite-store.ts +900 -0
  300. package/src/infrastructure/pricing/.gitkeep +0 -0
  301. package/src/infrastructure/pricing/litellm-fetch.ts +215 -0
  302. package/src/infrastructure/pricing/price-broker.ts +183 -0
  303. package/src/infrastructure/pricing/pricing-monitor.ts +65 -0
  304. package/src/infrastructure/telemetry/.gitkeep +0 -0
  305. package/src/infrastructure/telemetry/dataset-limits.ts +39 -0
  306. package/src/infrastructure/telemetry/dataset-recorder.ts +215 -0
  307. package/src/infrastructure/telemetry/outcome-limits.ts +42 -0
  308. package/src/infrastructure/telemetry/outcome-recorder.ts +118 -0
  309. package/src/infrastructure/telemetry/routing-telemetry.ts +824 -0
  310. package/src/infrastructure/telemetry/telemetry-limits.ts +50 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Hardware probe — T044, FR-012.
3
+ *
4
+ * Three-state gate: inspects platform/arch, unified memory, and battery
5
+ * to return `full_local`, `classification_only`, or `disabled`.
6
+ *
7
+ * Pure function of SystemInfo × config; side-effect-free for testability.
8
+ * Default SystemInfo provider reads from Node.js `os` and platform-specific
9
+ * power sources (macOS pmset, Linux /sys/class/power_supply, Windows WMI).
10
+ */
11
+
12
+ import * as os from 'node:os';
13
+ import { execSync } from 'node:child_process';
14
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+
17
+ // ─── Types ───────────────────────────────────────────────────────────────────
18
+
19
+ export type HardwareProbeResult = 'full_local' | 'classification_only' | 'disabled';
20
+
21
+ export interface HardwareProbeConfig {
22
+ readonly min_memory_gb_full: number;
23
+ readonly min_memory_gb_classification: number;
24
+ readonly battery_threshold_pct: number;
25
+ }
26
+
27
+ export interface SystemInfo {
28
+ readonly totalMemoryGb: number;
29
+ readonly arch: string;
30
+ readonly platform: NodeJS.Platform;
31
+ readonly batteryLevel: number | null;
32
+ readonly isOnAcPower: boolean | null;
33
+ }
34
+
35
+ /** Port for dependency injection in tests. */
36
+ export interface SystemInfoPort {
37
+ getSystemInfo(): Promise<SystemInfo>;
38
+ }
39
+
40
+ // ─── Pure probe logic ────────────────────────────────────────────────────────
41
+
42
+ function isSupportedPlatform(info: SystemInfo): boolean {
43
+ if (info.platform === 'darwin' && info.arch === 'arm64') {
44
+ return true;
45
+ }
46
+ if (info.platform === 'linux' && (info.arch === 'x64' || info.arch === 'arm64')) {
47
+ return true;
48
+ }
49
+ if (info.platform === 'win32' && (info.arch === 'x64' || info.arch === 'arm64')) {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ export function probeHardware(
56
+ config: HardwareProbeConfig,
57
+ info: SystemInfo,
58
+ ): HardwareProbeResult {
59
+ if (!isSupportedPlatform(info)) {
60
+ return 'disabled';
61
+ }
62
+
63
+ if (
64
+ info.isOnAcPower === false &&
65
+ info.batteryLevel !== null &&
66
+ info.batteryLevel < config.battery_threshold_pct
67
+ ) {
68
+ return 'disabled';
69
+ }
70
+
71
+ if (info.totalMemoryGb >= config.min_memory_gb_full) {
72
+ return 'full_local';
73
+ }
74
+
75
+ if (info.totalMemoryGb >= config.min_memory_gb_classification) {
76
+ return 'classification_only';
77
+ }
78
+
79
+ return 'disabled';
80
+ }
81
+
82
+ // ─── Platform-specific system info providers ───────────────────────────────
83
+
84
+ function parseBatteryInfo(output: string): {
85
+ batteryLevel: number | null;
86
+ isOnAcPower: boolean;
87
+ } {
88
+ const pctMatch = /(\d+)%/.exec(output);
89
+ const batteryLevel = pctMatch?.[1] !== undefined
90
+ ? parseInt(pctMatch[1], 10)
91
+ : null;
92
+ const isOnAcPower = output.includes('AC Power');
93
+ return { batteryLevel, isOnAcPower };
94
+ }
95
+
96
+ function readLinuxPowerInfo(): {
97
+ batteryLevel: number | null;
98
+ isOnAcPower: boolean | null;
99
+ } {
100
+ const powerSupplyDir = '/sys/class/power_supply';
101
+ if (!existsSync(powerSupplyDir)) {
102
+ return { batteryLevel: null, isOnAcPower: true };
103
+ }
104
+
105
+ try {
106
+ const entries = readdirSync(powerSupplyDir);
107
+ let batteryLevel: number | null = null;
108
+ let isOnAcPower: boolean | null = null;
109
+
110
+ for (const name of entries) {
111
+ const base = join(powerSupplyDir, name);
112
+ const typePath = join(base, 'type');
113
+ if (!existsSync(typePath)) {
114
+ continue;
115
+ }
116
+
117
+ const type = readFileSync(typePath, 'utf8').trim();
118
+ if (type !== 'Battery') {
119
+ continue;
120
+ }
121
+
122
+ const capacityPath = join(base, 'capacity');
123
+ if (existsSync(capacityPath)) {
124
+ const capacity = parseInt(readFileSync(capacityPath, 'utf8').trim(), 10);
125
+ if (!Number.isNaN(capacity)) {
126
+ batteryLevel = capacity;
127
+ }
128
+ }
129
+
130
+ const statusPath = join(base, 'status');
131
+ if (existsSync(statusPath)) {
132
+ const status = readFileSync(statusPath, 'utf8').trim();
133
+ isOnAcPower = status === 'Charging' || status === 'Full' || status === 'Not charging';
134
+ }
135
+ break;
136
+ }
137
+
138
+ if (batteryLevel === null && isOnAcPower === null) {
139
+ return { batteryLevel: null, isOnAcPower: true };
140
+ }
141
+
142
+ return { batteryLevel, isOnAcPower };
143
+ } catch {
144
+ return { batteryLevel: null, isOnAcPower: true };
145
+ }
146
+ }
147
+
148
+ function buildBaseSystemInfo(): Pick<SystemInfo, 'totalMemoryGb' | 'arch' | 'platform'> {
149
+ return {
150
+ totalMemoryGb: os.totalmem() / 1024 ** 3,
151
+ arch: os.arch(),
152
+ platform: os.platform(),
153
+ };
154
+ }
155
+
156
+ const macOsSystemInfoPort: SystemInfoPort = {
157
+ async getSystemInfo(): Promise<SystemInfo> {
158
+ let batteryLevel: number | null = null;
159
+ let isOnAcPower: boolean | null = null;
160
+
161
+ try {
162
+ const output = execSync('pmset -g batt', {
163
+ timeout: 2000,
164
+ encoding: 'utf8',
165
+ });
166
+ const parsed = parseBatteryInfo(output);
167
+ batteryLevel = parsed.batteryLevel;
168
+ isOnAcPower = parsed.isOnAcPower;
169
+ } catch {
170
+ isOnAcPower = true;
171
+ }
172
+
173
+ return {
174
+ ...buildBaseSystemInfo(),
175
+ batteryLevel,
176
+ isOnAcPower,
177
+ };
178
+ },
179
+ };
180
+
181
+ const linuxSystemInfoPort: SystemInfoPort = {
182
+ async getSystemInfo(): Promise<SystemInfo> {
183
+ const power = readLinuxPowerInfo();
184
+ return {
185
+ ...buildBaseSystemInfo(),
186
+ batteryLevel: power.batteryLevel,
187
+ isOnAcPower: power.isOnAcPower,
188
+ };
189
+ },
190
+ };
191
+
192
+ function readWindowsPowerInfo(): {
193
+ batteryLevel: number | null;
194
+ isOnAcPower: boolean | null;
195
+ } {
196
+ try {
197
+ const output = execSync(
198
+ 'powershell -NoProfile -Command "Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue | Select-Object -First 1 EstimatedChargeRemaining, BatteryStatus | ConvertTo-Json -Compress"',
199
+ { timeout: 3000, encoding: 'utf8' },
200
+ ).trim();
201
+
202
+ if (!output) {
203
+ return { batteryLevel: null, isOnAcPower: true };
204
+ }
205
+
206
+ const parsed = JSON.parse(output) as {
207
+ EstimatedChargeRemaining?: number;
208
+ BatteryStatus?: number;
209
+ };
210
+
211
+ const batteryLevel = typeof parsed.EstimatedChargeRemaining === 'number'
212
+ ? parsed.EstimatedChargeRemaining
213
+ : null;
214
+
215
+ let isOnAcPower: boolean | null = null;
216
+ if (typeof parsed.BatteryStatus === 'number') {
217
+ // 1 = discharging; other values indicate AC/charging/full states.
218
+ isOnAcPower = parsed.BatteryStatus !== 1;
219
+ }
220
+
221
+ if (batteryLevel === null && isOnAcPower === null) {
222
+ return { batteryLevel: null, isOnAcPower: true };
223
+ }
224
+
225
+ return { batteryLevel, isOnAcPower };
226
+ } catch {
227
+ return { batteryLevel: null, isOnAcPower: true };
228
+ }
229
+ }
230
+
231
+ const windowsSystemInfoPort: SystemInfoPort = {
232
+ async getSystemInfo(): Promise<SystemInfo> {
233
+ const power = readWindowsPowerInfo();
234
+ return {
235
+ ...buildBaseSystemInfo(),
236
+ batteryLevel: power.batteryLevel,
237
+ isOnAcPower: power.isOnAcPower,
238
+ };
239
+ },
240
+ };
241
+
242
+ const genericSystemInfoPort: SystemInfoPort = {
243
+ async getSystemInfo(): Promise<SystemInfo> {
244
+ return {
245
+ ...buildBaseSystemInfo(),
246
+ batteryLevel: null,
247
+ isOnAcPower: null,
248
+ };
249
+ },
250
+ };
251
+
252
+ export function getDefaultSystemInfoPort(): SystemInfoPort {
253
+ switch (os.platform()) {
254
+ case 'darwin':
255
+ return macOsSystemInfoPort;
256
+ case 'linux':
257
+ return linuxSystemInfoPort;
258
+ case 'win32':
259
+ return windowsSystemInfoPort;
260
+ default:
261
+ return genericSystemInfoPort;
262
+ }
263
+ }
264
+
265
+ export async function getDefaultSystemInfo(): Promise<SystemInfo> {
266
+ return getDefaultSystemInfoPort().getSystemInfo();
267
+ }
268
+
269
+ // ─── Convenience: probe with real system info ────────────────────────────────
270
+
271
+ export async function probeHardwareDefault(
272
+ config: HardwareProbeConfig,
273
+ ): Promise<HardwareProbeResult> {
274
+ const info = await getDefaultSystemInfo();
275
+ return probeHardware(config, info);
276
+ }
File without changes
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Local zero-tier readiness pings — T045, FR-012, FR-013.
3
+ *
4
+ * Pings LM Studio and Ollama HTTP endpoints in parallel to determine
5
+ * whether a local model is loaded and ready. Combined latency target <15ms.
6
+ *
7
+ * Never throws — unreachable services return `available: false`.
8
+ * Port-based HTTP for testability; default uses Node.js global fetch.
9
+ */
10
+
11
+ // ─── Types ───────────────────────────────────────────────────────────────────
12
+
13
+ export interface ServicePingResult {
14
+ readonly available: boolean;
15
+ readonly hasLoadedModel: boolean;
16
+ readonly latencyMs: number;
17
+ }
18
+
19
+ export interface LocalReadinessResult {
20
+ readonly lmStudio: ServicePingResult;
21
+ readonly ollama: ServicePingResult;
22
+ readonly anyModelReady: boolean;
23
+ readonly combinedLatencyMs: number;
24
+ }
25
+
26
+ export interface LocalZeroTierConfig {
27
+ readonly lmStudioBaseUrl: string;
28
+ readonly ollamaBaseUrl: string;
29
+ readonly pingTimeoutMs: number;
30
+ }
31
+
32
+ /** Port for HTTP fetch — allows stubbing in tests without network. */
33
+ export interface HttpFetchPort {
34
+ fetch(url: string, init: { signal: AbortSignal }): Promise<{
35
+ ok: boolean;
36
+ json(): Promise<unknown>;
37
+ }>;
38
+ }
39
+
40
+ // ─── Defaults ────────────────────────────────────────────────────────────────
41
+
42
+ export const DEFAULT_LOCAL_CONFIG: LocalZeroTierConfig = {
43
+ lmStudioBaseUrl: 'http://127.0.0.1:1234',
44
+ ollamaBaseUrl: 'http://127.0.0.1:11434',
45
+ pingTimeoutMs: 500,
46
+ };
47
+
48
+ const UNAVAILABLE: ServicePingResult = {
49
+ available: false,
50
+ hasLoadedModel: false,
51
+ latencyMs: 0,
52
+ };
53
+
54
+ // ─── Ping implementations ────────────────────────────────────────────────────
55
+
56
+ interface LmStudioModelsResponse {
57
+ data?: readonly unknown[];
58
+ }
59
+
60
+ interface OllamaTagsResponse {
61
+ models?: readonly unknown[];
62
+ }
63
+
64
+ async function pingLmStudio(
65
+ baseUrl: string,
66
+ signal: AbortSignal,
67
+ httpFetch: HttpFetchPort,
68
+ ): Promise<ServicePingResult> {
69
+ const start = performance.now();
70
+ try {
71
+ const res = await httpFetch.fetch(`${baseUrl}/v1/models`, { signal });
72
+ const latencyMs = performance.now() - start;
73
+ if (!res.ok) {
74
+ return { available: true, hasLoadedModel: false, latencyMs };
75
+ }
76
+ const body = (await res.json()) as LmStudioModelsResponse;
77
+ const hasLoadedModel = Array.isArray(body.data) && body.data.length > 0;
78
+ return { available: true, hasLoadedModel, latencyMs };
79
+ } catch {
80
+ return { ...UNAVAILABLE, latencyMs: performance.now() - start };
81
+ }
82
+ }
83
+
84
+ async function pingOllama(
85
+ baseUrl: string,
86
+ signal: AbortSignal,
87
+ httpFetch: HttpFetchPort,
88
+ ): Promise<ServicePingResult> {
89
+ const start = performance.now();
90
+ try {
91
+ const res = await httpFetch.fetch(`${baseUrl}/api/tags`, { signal });
92
+ const latencyMs = performance.now() - start;
93
+ if (!res.ok) {
94
+ return { available: true, hasLoadedModel: false, latencyMs };
95
+ }
96
+ const body = (await res.json()) as OllamaTagsResponse;
97
+ const hasLoadedModel = Array.isArray(body.models) && body.models.length > 0;
98
+ return { available: true, hasLoadedModel, latencyMs };
99
+ } catch {
100
+ return { ...UNAVAILABLE, latencyMs: performance.now() - start };
101
+ }
102
+ }
103
+
104
+ // ─── Public API ──────────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * Default HTTP fetch port using Node.js global fetch.
108
+ */
109
+ export const defaultHttpFetch: HttpFetchPort = {
110
+ fetch: (url, init) => globalThis.fetch(url, init),
111
+ };
112
+
113
+ /**
114
+ * Ping LM Studio and Ollama in parallel. Returns readiness state for each
115
+ * service and whether any local model is loaded and ready for inference.
116
+ *
117
+ * Combined latency target: <15ms on a healthy system.
118
+ */
119
+ export async function pingLocalServices(
120
+ config: LocalZeroTierConfig = DEFAULT_LOCAL_CONFIG,
121
+ httpFetch: HttpFetchPort = defaultHttpFetch,
122
+ ): Promise<LocalReadinessResult> {
123
+ const overallStart = performance.now();
124
+ const controller = new AbortController();
125
+ const timeout = setTimeout(
126
+ () => controller.abort(),
127
+ config.pingTimeoutMs,
128
+ );
129
+
130
+ try {
131
+ const [lmStudio, ollama] = await Promise.all([
132
+ pingLmStudio(config.lmStudioBaseUrl, controller.signal, httpFetch),
133
+ pingOllama(config.ollamaBaseUrl, controller.signal, httpFetch),
134
+ ]);
135
+
136
+ const combinedLatencyMs = performance.now() - overallStart;
137
+ const anyModelReady = lmStudio.hasLoadedModel || ollama.hasLoadedModel;
138
+
139
+ return { lmStudio, ollama, anyModelReady, combinedLatencyMs };
140
+ } catch {
141
+ // FR-022: never crash — return both unavailable on unexpected error
142
+ return {
143
+ lmStudio: UNAVAILABLE,
144
+ ollama: UNAVAILABLE,
145
+ anyModelReady: false,
146
+ combinedLatencyMs: performance.now() - overallStart,
147
+ };
148
+ } finally {
149
+ clearTimeout(timeout);
150
+ }
151
+ }
File without changes
@@ -0,0 +1,122 @@
1
+ /**
2
+ * In-memory persistence store — fallback when SQLite is unavailable.
3
+ *
4
+ * Implements StorePort for process-local state. Data does not survive
5
+ * process restart. Used as a safety net per FR-025: when the SQLite
6
+ * state store is unavailable, degrade to in-memory rather than crash.
7
+ */
8
+
9
+ import type { ModelProfile, PriceCatalog, RoutingDatasetRecord, RoutingOutcomeRecord, RoutingTelemetry, SessionPin } from '../../domain/types/entities.js';
10
+ import type { ListDatasetOptions, ListOutcomeOptions, ListTelemetryOptions, StorePort } from '../../domain/types/store-port.js';
11
+ import {
12
+ DEFAULT_HISTORY_LIMIT,
13
+ MAX_HISTORY_LIMIT,
14
+ TELEMETRY_MAX_ENTRIES,
15
+ makeTelemetryRoom,
16
+ } from '../telemetry/telemetry-limits.js';
17
+ import {
18
+ makeDatasetRoom,
19
+ } from '../telemetry/dataset-limits.js';
20
+ import {
21
+ makeOutcomeRoom,
22
+ } from '../telemetry/outcome-limits.js';
23
+
24
+ export class MemoryStore implements StorePort {
25
+ private readonly pins = new Map<string, SessionPin>();
26
+ private readonly models: readonly ModelProfile[];
27
+ private priceCatalog: PriceCatalog | null = null;
28
+ private readonly telemetry: RoutingTelemetry[] = [];
29
+ private readonly dataset: RoutingDatasetRecord[] = [];
30
+ private readonly outcomes: RoutingOutcomeRecord[] = [];
31
+
32
+ constructor(models: readonly ModelProfile[] = []) {
33
+ this.models = models;
34
+ }
35
+
36
+ async getSessionPin(sessionId: string): Promise<SessionPin | null> {
37
+ return this.pins.get(sessionId) ?? null;
38
+ }
39
+
40
+ async putSessionPin(pin: SessionPin): Promise<void> {
41
+ this.pins.set(pin.session_id, pin);
42
+ }
43
+
44
+ async deleteSessionPin(sessionId: string): Promise<void> {
45
+ this.pins.delete(sessionId);
46
+ }
47
+
48
+ async getModelProfiles(): Promise<readonly ModelProfile[]> {
49
+ return this.models;
50
+ }
51
+
52
+ async getPriceCatalog(): Promise<PriceCatalog | null> {
53
+ return this.priceCatalog;
54
+ }
55
+
56
+ async putPriceCatalog(catalog: PriceCatalog): Promise<void> {
57
+ this.priceCatalog = catalog;
58
+ }
59
+
60
+ appendTelemetry(entry: RoutingTelemetry): void {
61
+ makeTelemetryRoom(this.telemetry, TELEMETRY_MAX_ENTRIES);
62
+ this.telemetry.push(entry);
63
+ }
64
+
65
+ async listTelemetry(options?: ListTelemetryOptions): Promise<readonly RoutingTelemetry[]> {
66
+ const limit = clampHistoryLimit(options?.limit);
67
+ const sessionId = options?.sessionId;
68
+
69
+ const filtered = sessionId
70
+ ? this.telemetry.filter((entry) => entry.session_id === sessionId)
71
+ : this.telemetry;
72
+
73
+ return [...filtered]
74
+ .reverse()
75
+ .slice(0, limit);
76
+ }
77
+
78
+ appendDatasetRecord(entry: RoutingDatasetRecord): void {
79
+ makeDatasetRoom(this.dataset);
80
+ this.dataset.push(entry);
81
+ }
82
+
83
+ async listDatasetRecords(
84
+ options?: ListDatasetOptions,
85
+ ): Promise<readonly RoutingDatasetRecord[]> {
86
+ const limit = clampHistoryLimit(options?.limit);
87
+
88
+ return [...this.dataset]
89
+ .reverse()
90
+ .slice(0, limit);
91
+ }
92
+
93
+ appendOutcomeRecord(entry: RoutingOutcomeRecord): void {
94
+ makeOutcomeRoom(this.outcomes);
95
+ this.outcomes.push(entry);
96
+ }
97
+
98
+ async listOutcomeRecords(
99
+ options?: ListOutcomeOptions,
100
+ ): Promise<readonly RoutingOutcomeRecord[]> {
101
+ const limit = clampHistoryLimit(options?.limit);
102
+ const requestId = options?.requestId;
103
+ const sessionId = options?.sessionId;
104
+
105
+ const filtered = requestId
106
+ ? this.outcomes.filter((entry) => entry.request_id === requestId)
107
+ : sessionId
108
+ ? this.outcomes.filter((entry) => entry.session_id === sessionId)
109
+ : this.outcomes;
110
+
111
+ return [...filtered]
112
+ .reverse()
113
+ .slice(0, limit);
114
+ }
115
+ }
116
+
117
+ function clampHistoryLimit(limit: number | undefined): number {
118
+ if (limit === undefined) {
119
+ return DEFAULT_HISTORY_LIMIT;
120
+ }
121
+ return Math.min(Math.max(1, Math.floor(limit)), MAX_HISTORY_LIMIT);
122
+ }