@hellcoder/companion 0.96.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 (242) hide show
  1. package/bin/cli.ts +168 -0
  2. package/bin/ctl.ts +528 -0
  3. package/bin/generate-token.ts +28 -0
  4. package/dist/apple-touch-icon.png +0 -0
  5. package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
  6. package/dist/assets/CronManager-EGwLJONv.js +1 -0
  7. package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
  8. package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
  9. package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
  10. package/dist/assets/Playground-BV3k0RbV.js +109 -0
  11. package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
  12. package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
  13. package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
  14. package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
  15. package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
  16. package/dist/assets/index-BhUa1e6X.css +1 -0
  17. package/dist/assets/index-DkqeP-R9.js +134 -0
  18. package/dist/assets/sw-register-BibwRdvC.js +1 -0
  19. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
  20. package/dist/favicon.svg +8 -0
  21. package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
  22. package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
  23. package/dist/icon-192.png +0 -0
  24. package/dist/icon-512.png +0 -0
  25. package/dist/index.html +20 -0
  26. package/dist/logo-codex.svg +14 -0
  27. package/dist/logo-docker.svg +4 -0
  28. package/dist/logo.svg +14 -0
  29. package/dist/manifest.json +24 -0
  30. package/dist/sw.js +2 -0
  31. package/package.json +104 -0
  32. package/server/agent-cron-migrator.test.ts +610 -0
  33. package/server/agent-cron-migrator.ts +85 -0
  34. package/server/agent-executor.test.ts +1108 -0
  35. package/server/agent-executor.ts +346 -0
  36. package/server/agent-store.test.ts +588 -0
  37. package/server/agent-store.ts +185 -0
  38. package/server/agent-types.ts +138 -0
  39. package/server/ai-validation-settings.test.ts +128 -0
  40. package/server/ai-validation-settings.ts +35 -0
  41. package/server/ai-validator.test.ts +387 -0
  42. package/server/ai-validator.ts +271 -0
  43. package/server/auth-manager.test.ts +83 -0
  44. package/server/auth-manager.ts +150 -0
  45. package/server/auto-namer.test.ts +252 -0
  46. package/server/auto-namer.ts +78 -0
  47. package/server/backend-adapter.test.ts +38 -0
  48. package/server/backend-adapter.ts +54 -0
  49. package/server/cache-headers.test.ts +98 -0
  50. package/server/cache-headers.ts +61 -0
  51. package/server/claude-adapter.test.ts +1363 -0
  52. package/server/claude-adapter.ts +889 -0
  53. package/server/claude-container-auth.test.ts +44 -0
  54. package/server/claude-container-auth.ts +30 -0
  55. package/server/claude-protocol-contract.test.ts +71 -0
  56. package/server/claude-protocol-drift.test.ts +78 -0
  57. package/server/claude-session-discovery.test.ts +132 -0
  58. package/server/claude-session-discovery.ts +157 -0
  59. package/server/claude-session-history.test.ts +158 -0
  60. package/server/claude-session-history.ts +410 -0
  61. package/server/cli-launcher.test.ts +1343 -0
  62. package/server/cli-launcher.ts +1298 -0
  63. package/server/cli.test.ts +16 -0
  64. package/server/codex-adapter.test.ts +5545 -0
  65. package/server/codex-adapter.ts +3062 -0
  66. package/server/codex-container-auth.test.ts +50 -0
  67. package/server/codex-container-auth.ts +24 -0
  68. package/server/codex-home.test.ts +61 -0
  69. package/server/codex-home.ts +26 -0
  70. package/server/codex-protocol-contract.test.ts +96 -0
  71. package/server/codex-protocol-drift.test.ts +123 -0
  72. package/server/codex-ws-proxy.cjs +226 -0
  73. package/server/commands-discovery.test.ts +179 -0
  74. package/server/commands-discovery.ts +81 -0
  75. package/server/constants.ts +7 -0
  76. package/server/container-manager.test.ts +1211 -0
  77. package/server/container-manager.ts +1053 -0
  78. package/server/cron-scheduler.test.ts +957 -0
  79. package/server/cron-scheduler.ts +243 -0
  80. package/server/cron-store.test.ts +422 -0
  81. package/server/cron-store.ts +148 -0
  82. package/server/cron-types.ts +63 -0
  83. package/server/env-manager.test.ts +268 -0
  84. package/server/env-manager.ts +161 -0
  85. package/server/event-bus-types.ts +64 -0
  86. package/server/event-bus.test.ts +244 -0
  87. package/server/event-bus.ts +124 -0
  88. package/server/execution-store.test.ts +307 -0
  89. package/server/execution-store.ts +170 -0
  90. package/server/fs-utils.ts +15 -0
  91. package/server/git-utils.test.ts +938 -0
  92. package/server/git-utils.ts +421 -0
  93. package/server/github-pr.test.ts +498 -0
  94. package/server/github-pr.ts +379 -0
  95. package/server/image-pull-manager.test.ts +303 -0
  96. package/server/image-pull-manager.ts +279 -0
  97. package/server/index.ts +396 -0
  98. package/server/linear-agent-bridge.test.ts +1157 -0
  99. package/server/linear-agent-bridge.ts +629 -0
  100. package/server/linear-agent.test.ts +473 -0
  101. package/server/linear-agent.ts +479 -0
  102. package/server/linear-cache.test.ts +136 -0
  103. package/server/linear-cache.ts +113 -0
  104. package/server/linear-connections.test.ts +350 -0
  105. package/server/linear-connections.ts +231 -0
  106. package/server/linear-credential-migration.test.ts +337 -0
  107. package/server/linear-credential-migration.ts +63 -0
  108. package/server/linear-oauth-connections-migration.test.ts +268 -0
  109. package/server/linear-oauth-connections.test.ts +365 -0
  110. package/server/linear-oauth-connections.ts +294 -0
  111. package/server/linear-project-manager.test.ts +162 -0
  112. package/server/linear-project-manager.ts +111 -0
  113. package/server/linear-prompt-builder.test.ts +74 -0
  114. package/server/linear-prompt-builder.ts +61 -0
  115. package/server/linear-staging.test.ts +276 -0
  116. package/server/linear-staging.ts +142 -0
  117. package/server/logger.test.ts +393 -0
  118. package/server/logger.ts +259 -0
  119. package/server/metrics-collector.test.ts +413 -0
  120. package/server/metrics-collector.ts +350 -0
  121. package/server/metrics-types.ts +108 -0
  122. package/server/middleware/managed-auth.test.ts +264 -0
  123. package/server/middleware/managed-auth.ts +195 -0
  124. package/server/novnc-proxy.test.ts +333 -0
  125. package/server/novnc-proxy.ts +99 -0
  126. package/server/path-resolver.test.ts +552 -0
  127. package/server/path-resolver.ts +186 -0
  128. package/server/paths.test.ts +31 -0
  129. package/server/paths.ts +11 -0
  130. package/server/pr-poller.test.ts +191 -0
  131. package/server/pr-poller.ts +162 -0
  132. package/server/prompt-manager.test.ts +211 -0
  133. package/server/prompt-manager.ts +211 -0
  134. package/server/protocol/claude-upstream/README.md +19 -0
  135. package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
  136. package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
  137. package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
  138. package/server/protocol/codex-upstream/README.md +18 -0
  139. package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
  140. package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
  141. package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
  142. package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
  143. package/server/protocol-monitor.ts +50 -0
  144. package/server/recorder.test.ts +454 -0
  145. package/server/recorder.ts +374 -0
  146. package/server/recording-hub/compat-validator.test.ts +150 -0
  147. package/server/recording-hub/compat-validator.ts +284 -0
  148. package/server/recording-hub/diagnostics.test.ts +140 -0
  149. package/server/recording-hub/diagnostics.ts +299 -0
  150. package/server/recording-hub/hub-config.test.ts +44 -0
  151. package/server/recording-hub/hub-config.ts +19 -0
  152. package/server/recording-hub/hub-routes.test.ts +417 -0
  153. package/server/recording-hub/hub-routes.ts +236 -0
  154. package/server/recording-hub/hub-store.test.ts +262 -0
  155. package/server/recording-hub/hub-store.ts +265 -0
  156. package/server/recording-hub/replay-adapter.test.ts +294 -0
  157. package/server/recording-hub/replay-adapter.ts +207 -0
  158. package/server/relay-client.test.ts +337 -0
  159. package/server/relay-client.ts +320 -0
  160. package/server/replay.test.ts +200 -0
  161. package/server/replay.ts +78 -0
  162. package/server/routes/agent-routes.test.ts +1400 -0
  163. package/server/routes/agent-routes.ts +409 -0
  164. package/server/routes/cron-routes.test.ts +881 -0
  165. package/server/routes/cron-routes.ts +103 -0
  166. package/server/routes/env-routes.test.ts +383 -0
  167. package/server/routes/env-routes.ts +95 -0
  168. package/server/routes/fs-routes.test.ts +1198 -0
  169. package/server/routes/fs-routes.ts +605 -0
  170. package/server/routes/git-routes.test.ts +813 -0
  171. package/server/routes/git-routes.ts +97 -0
  172. package/server/routes/linear-agent-routes.test.ts +721 -0
  173. package/server/routes/linear-agent-routes.ts +304 -0
  174. package/server/routes/linear-connection-routes.test.ts +927 -0
  175. package/server/routes/linear-connection-routes.ts +244 -0
  176. package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
  177. package/server/routes/linear-oauth-connection-routes.ts +129 -0
  178. package/server/routes/linear-routes.test.ts +1510 -0
  179. package/server/routes/linear-routes.ts +953 -0
  180. package/server/routes/metrics-routes.test.ts +103 -0
  181. package/server/routes/metrics-routes.ts +13 -0
  182. package/server/routes/prompt-routes.ts +67 -0
  183. package/server/routes/sandbox-routes.test.ts +513 -0
  184. package/server/routes/sandbox-routes.ts +127 -0
  185. package/server/routes/settings-routes.ts +270 -0
  186. package/server/routes/skills-routes.test.ts +690 -0
  187. package/server/routes/skills-routes.ts +100 -0
  188. package/server/routes/system-routes.test.ts +637 -0
  189. package/server/routes/system-routes.ts +228 -0
  190. package/server/routes/tailscale-routes.test.ts +176 -0
  191. package/server/routes/tailscale-routes.ts +22 -0
  192. package/server/routes.test.ts +4655 -0
  193. package/server/routes.ts +1277 -0
  194. package/server/sandbox-manager.test.ts +378 -0
  195. package/server/sandbox-manager.ts +168 -0
  196. package/server/service.test.ts +1419 -0
  197. package/server/service.ts +718 -0
  198. package/server/session-creation-service.test.ts +661 -0
  199. package/server/session-creation-service.ts +473 -0
  200. package/server/session-git-info.ts +104 -0
  201. package/server/session-linear-issues.test.ts +118 -0
  202. package/server/session-linear-issues.ts +88 -0
  203. package/server/session-names.test.ts +94 -0
  204. package/server/session-names.ts +67 -0
  205. package/server/session-orchestrator.test.ts +1784 -0
  206. package/server/session-orchestrator.ts +973 -0
  207. package/server/session-state-machine.test.ts +606 -0
  208. package/server/session-state-machine.ts +207 -0
  209. package/server/session-store.test.ts +290 -0
  210. package/server/session-store.ts +146 -0
  211. package/server/session-types.ts +509 -0
  212. package/server/settings-manager.test.ts +275 -0
  213. package/server/settings-manager.ts +173 -0
  214. package/server/tailscale-manager.test.ts +553 -0
  215. package/server/tailscale-manager.ts +451 -0
  216. package/server/terminal-manager.ts +240 -0
  217. package/server/update-checker.test.ts +306 -0
  218. package/server/update-checker.ts +197 -0
  219. package/server/usage-limits.test.ts +536 -0
  220. package/server/usage-limits.ts +225 -0
  221. package/server/worktree-tracker.test.ts +243 -0
  222. package/server/worktree-tracker.ts +84 -0
  223. package/server/ws-auth.test.ts +59 -0
  224. package/server/ws-auth.ts +41 -0
  225. package/server/ws-bridge-browser-ingest.test.ts +272 -0
  226. package/server/ws-bridge-browser-ingest.ts +72 -0
  227. package/server/ws-bridge-browser.ts +112 -0
  228. package/server/ws-bridge-cli-ingest.test.ts +302 -0
  229. package/server/ws-bridge-cli-ingest.ts +81 -0
  230. package/server/ws-bridge-codex.test.ts +1837 -0
  231. package/server/ws-bridge-codex.ts +266 -0
  232. package/server/ws-bridge-controls.test.ts +124 -0
  233. package/server/ws-bridge-controls.ts +20 -0
  234. package/server/ws-bridge-persist.test.ts +296 -0
  235. package/server/ws-bridge-persist.ts +66 -0
  236. package/server/ws-bridge-publish.test.ts +234 -0
  237. package/server/ws-bridge-publish.ts +79 -0
  238. package/server/ws-bridge-replay.test.ts +44 -0
  239. package/server/ws-bridge-replay.ts +61 -0
  240. package/server/ws-bridge-types.ts +106 -0
  241. package/server/ws-bridge.test.ts +4777 -0
  242. package/server/ws-bridge.ts +1279 -0
@@ -0,0 +1,350 @@
1
+ // In-memory runtime metrics collector for the Companion server.
2
+ // Subscribes to the event bus and provides direct instrumentation methods.
3
+ // All data is in-memory — resets on server restart.
4
+
5
+ import { companionBus } from "./event-bus.js";
6
+ import type { SessionPhase } from "./session-state-machine.js";
7
+ import type {
8
+ MetricsSnapshot,
9
+ HistogramSnapshot,
10
+ CounterMetrics,
11
+ GaugeMetrics,
12
+ } from "./metrics-types.js";
13
+
14
+ // ── Histogram bucket boundaries (ms) ──────────────────────────────────────
15
+
16
+ const TIMING_BUCKETS_MS = [50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000, 30_000, 60_000] as const;
17
+
18
+ // ── Internal histogram data structure ─────────────────────────────────────
19
+
20
+ interface Histogram {
21
+ count: number;
22
+ sum: number;
23
+ min: number;
24
+ max: number;
25
+ /** Frequency bucket counts. buckets[i] = count of values in (TIMING_BUCKETS_MS[i-1], TIMING_BUCKETS_MS[i]]. */
26
+ buckets: number[];
27
+ }
28
+
29
+ function createHistogram(): Histogram {
30
+ return {
31
+ count: 0,
32
+ sum: 0,
33
+ min: Infinity,
34
+ max: -Infinity,
35
+ buckets: new Array(TIMING_BUCKETS_MS.length + 1).fill(0), // +1 for +Infinity bucket
36
+ };
37
+ }
38
+
39
+ function recordHistogramValue(h: Histogram, value: number): void {
40
+ h.count++;
41
+ h.sum += value;
42
+ if (value < h.min) h.min = value;
43
+ if (value > h.max) h.max = value;
44
+
45
+ // Find the appropriate bucket
46
+ for (let i = 0; i < TIMING_BUCKETS_MS.length; i++) {
47
+ if (value <= TIMING_BUCKETS_MS[i]) {
48
+ h.buckets[i]++;
49
+ return;
50
+ }
51
+ }
52
+ // Falls into the +Infinity bucket
53
+ h.buckets[TIMING_BUCKETS_MS.length]++;
54
+ }
55
+
56
+ function serializeHistogram(h: Histogram): HistogramSnapshot {
57
+ const buckets: Record<string, number> = {};
58
+ for (let i = 0; i < TIMING_BUCKETS_MS.length; i++) {
59
+ buckets[String(TIMING_BUCKETS_MS[i])] = h.buckets[i];
60
+ }
61
+ buckets["Infinity"] = h.buckets[TIMING_BUCKETS_MS.length];
62
+
63
+ return {
64
+ count: h.count,
65
+ sum: h.sum,
66
+ min: h.count > 0 ? h.min : 0,
67
+ max: h.count > 0 ? h.max : 0,
68
+ avg: h.count > 0 ? Math.round(h.sum / h.count) : 0,
69
+ p50Bucket: computePercentileBucket(h, 0.5),
70
+ p95Bucket: computePercentileBucket(h, 0.95),
71
+ p99Bucket: computePercentileBucket(h, 0.99),
72
+ buckets,
73
+ };
74
+ }
75
+
76
+ /** Approximate the bucket boundary for a given percentile. */
77
+ function computePercentileBucket(h: Histogram, p: number): number {
78
+ if (h.count === 0) return 0;
79
+ const target = Math.ceil(h.count * p);
80
+ let cumulative = 0;
81
+ for (let i = 0; i < TIMING_BUCKETS_MS.length; i++) {
82
+ cumulative += h.buckets[i];
83
+ if (cumulative >= target) return TIMING_BUCKETS_MS[i];
84
+ }
85
+ return Infinity;
86
+ }
87
+
88
+ // ── Gauge data provider interface ─────────────────────────────────────────
89
+
90
+ /** Minimal interface for computing gauges at snapshot time. */
91
+ export interface GaugeDataProvider {
92
+ getSessionMemoryStats(): { id: string; browsers: number; historyLen: number; eventBufferLen: number; pendingMsgs: number }[];
93
+ getSessionPhases(): Map<string, SessionPhase>;
94
+ }
95
+
96
+ // ── MetricsCollector ──────────────────────────────────────────────────────
97
+
98
+ export class MetricsCollector {
99
+ private startedAt: number;
100
+
101
+ // Counters
102
+ private sessionsCreated = new Map<string, number>();
103
+ private sessionsTerminated = new Map<string, number>();
104
+ private autoRelaunches = { attempted: 0, succeeded: 0, exhausted: 0 };
105
+ private messagesProcessed = new Map<string, number>();
106
+ private permissions = { total: 0, autoApproved: 0, autoDenied: 0, userApproved: 0, userDenied: 0 };
107
+ private errors = new Map<string, number>();
108
+ private stateTransitions = new Map<string, number>();
109
+ private wsConnections = { cliOpened: 0, cliClosed: 0, browserOpened: 0, browserClosed: 0 };
110
+
111
+ // Histograms
112
+ private sessionInitTime = createHistogram();
113
+ private turnDuration = createHistogram();
114
+ private permissionDuration = createHistogram();
115
+
116
+ // Ephemeral timing state
117
+ private sessionSpawnedAt = new Map<string, number>();
118
+ private turnStartedAt = new Map<string, number>();
119
+ private permissionRequestedAt = new Map<string, number>();
120
+ /** Maps requestId → sessionId so permission timers can be cleaned up on session exit. */
121
+ private permissionRequestToSession = new Map<string, string>();
122
+
123
+ // Event bus unsubscribers (for cleanup in tests)
124
+ private unsubscribers: (() => void)[] = [];
125
+
126
+ constructor() {
127
+ this.startedAt = Date.now();
128
+ this.wireEventBus();
129
+ }
130
+
131
+ // ── Event bus wiring ──────────────────────────────────────────────────
132
+
133
+ private wireEventBus(): void {
134
+ this.unsubscribers.push(
135
+ companionBus.on("session:phase-changed", ({ sessionId, from, to }) => {
136
+ // Count state transitions
137
+ const key = `${from}→${to}`;
138
+ this.stateTransitions.set(key, (this.stateTransitions.get(key) ?? 0) + 1);
139
+
140
+ // Compute session init time: initializing → ready
141
+ if (to === "ready" && (from === "initializing" || from === "starting")) {
142
+ const spawned = this.sessionSpawnedAt.get(sessionId);
143
+ if (spawned != null) {
144
+ recordHistogramValue(this.sessionInitTime, Date.now() - spawned);
145
+ this.sessionSpawnedAt.delete(sessionId);
146
+ }
147
+ }
148
+ }),
149
+
150
+ companionBus.on("session:exited", ({ sessionId, exitCode }) => {
151
+ const key = String(exitCode ?? "null");
152
+ this.sessionsTerminated.set(key, (this.sessionsTerminated.get(key) ?? 0) + 1);
153
+
154
+ // Clean up ephemeral timing state
155
+ this.sessionSpawnedAt.delete(sessionId);
156
+ this.turnStartedAt.delete(sessionId);
157
+
158
+ // Evict orphaned permission timers for this session
159
+ for (const [reqId, sid] of this.permissionRequestToSession) {
160
+ if (sid === sessionId) {
161
+ this.permissionRequestedAt.delete(reqId);
162
+ this.permissionRequestToSession.delete(reqId);
163
+ }
164
+ }
165
+ }),
166
+
167
+ companionBus.on("message:result", ({ sessionId }) => {
168
+ const started = this.turnStartedAt.get(sessionId);
169
+ if (started != null) {
170
+ recordHistogramValue(this.turnDuration, Date.now() - started);
171
+ this.turnStartedAt.delete(sessionId);
172
+ }
173
+ }),
174
+ );
175
+ }
176
+
177
+ // ── Direct instrumentation methods ────────────────────────────────────
178
+
179
+ recordSessionCreated(backendType: string): void {
180
+ this.sessionsCreated.set(backendType, (this.sessionsCreated.get(backendType) ?? 0) + 1);
181
+ }
182
+
183
+ recordSessionSpawned(sessionId: string): void {
184
+ this.sessionSpawnedAt.set(sessionId, Date.now());
185
+ }
186
+
187
+ recordRelaunchAttempted(): void {
188
+ this.autoRelaunches.attempted++;
189
+ }
190
+
191
+ recordRelaunchSucceeded(): void {
192
+ this.autoRelaunches.succeeded++;
193
+ }
194
+
195
+ recordRelaunchExhausted(): void {
196
+ this.autoRelaunches.exhausted++;
197
+ }
198
+
199
+ recordTurnStarted(sessionId: string): void {
200
+ this.turnStartedAt.set(sessionId, Date.now());
201
+ }
202
+
203
+ recordPermissionRequested(requestId: string, sessionId?: string): void {
204
+ this.permissions.total++;
205
+ this.permissionRequestedAt.set(requestId, Date.now());
206
+ if (sessionId) {
207
+ this.permissionRequestToSession.set(requestId, sessionId);
208
+ }
209
+ }
210
+
211
+ recordPermissionResolved(requestId: string, behavior: "allow" | "deny", isAutomatic: boolean): void {
212
+ if (isAutomatic) {
213
+ if (behavior === "allow") this.permissions.autoApproved++;
214
+ else this.permissions.autoDenied++;
215
+ } else {
216
+ if (behavior === "allow") this.permissions.userApproved++;
217
+ else this.permissions.userDenied++;
218
+ }
219
+
220
+ const requested = this.permissionRequestedAt.get(requestId);
221
+ if (requested != null) {
222
+ recordHistogramValue(this.permissionDuration, Date.now() - requested);
223
+ this.permissionRequestedAt.delete(requestId);
224
+ this.permissionRequestToSession.delete(requestId);
225
+ }
226
+ }
227
+
228
+ recordWsConnection(kind: "cli" | "browser", event: "open" | "close"): void {
229
+ if (kind === "cli") {
230
+ if (event === "open") this.wsConnections.cliOpened++;
231
+ else this.wsConnections.cliClosed++;
232
+ } else {
233
+ if (event === "open") this.wsConnections.browserOpened++;
234
+ else this.wsConnections.browserClosed++;
235
+ }
236
+ }
237
+
238
+ recordMessageProcessed(messageType: string): void {
239
+ this.messagesProcessed.set(messageType, (this.messagesProcessed.get(messageType) ?? 0) + 1);
240
+ }
241
+
242
+ recordError(category: string): void {
243
+ this.errors.set(category, (this.errors.get(category) ?? 0) + 1);
244
+ }
245
+
246
+ // ── Snapshot ──────────────────────────────────────────────────────────
247
+
248
+ getSnapshot(gaugeProvider?: GaugeDataProvider): MetricsSnapshot {
249
+ const counters = this.buildCounters();
250
+ const gauges = this.buildGauges(gaugeProvider);
251
+ const histograms = {
252
+ sessionInitTimeMs: serializeHistogram(this.sessionInitTime),
253
+ turnDurationMs: serializeHistogram(this.turnDuration),
254
+ permissionDurationMs: serializeHistogram(this.permissionDuration),
255
+ };
256
+
257
+ return {
258
+ serverUptimeMs: Date.now() - this.startedAt,
259
+ snapshotAt: Date.now(),
260
+ counters,
261
+ gauges,
262
+ histograms,
263
+ };
264
+ }
265
+
266
+ private buildCounters(): CounterMetrics {
267
+ return {
268
+ sessionsCreated: Object.fromEntries(this.sessionsCreated),
269
+ sessionsTerminated: Object.fromEntries(this.sessionsTerminated),
270
+ autoRelaunches: { ...this.autoRelaunches },
271
+ messagesProcessed: Object.fromEntries(this.messagesProcessed),
272
+ permissionRequests: { ...this.permissions },
273
+ errors: Object.fromEntries(this.errors),
274
+ stateTransitions: Object.fromEntries(this.stateTransitions),
275
+ wsConnections: { ...this.wsConnections },
276
+ };
277
+ }
278
+
279
+ private buildGauges(provider?: GaugeDataProvider): GaugeMetrics {
280
+ const activeSessions: Partial<Record<SessionPhase, number>> = {};
281
+ let totalActive = 0;
282
+ let connectedBrowsers = 0;
283
+ let totalPending = 0;
284
+ let totalEventBuffer = 0;
285
+ let totalHistory = 0;
286
+
287
+ if (provider) {
288
+ // Compute phase distribution
289
+ for (const [, phase] of provider.getSessionPhases()) {
290
+ activeSessions[phase] = (activeSessions[phase] ?? 0) + 1;
291
+ if (phase !== "terminated") totalActive++;
292
+ }
293
+
294
+ // Compute memory stats
295
+ for (const stats of provider.getSessionMemoryStats()) {
296
+ connectedBrowsers += stats.browsers;
297
+ totalPending += stats.pendingMsgs;
298
+ totalEventBuffer += stats.eventBufferLen;
299
+ totalHistory += stats.historyLen;
300
+ }
301
+ }
302
+
303
+ const mem = process.memoryUsage();
304
+
305
+ return {
306
+ activeSessions,
307
+ totalActiveSessions: totalActive,
308
+ connectedBrowsers,
309
+ totalPendingMessages: totalPending,
310
+ totalEventBufferSize: totalEventBuffer,
311
+ totalHistoryMessages: totalHistory,
312
+ memory: {
313
+ rss: mem.rss,
314
+ heapUsed: mem.heapUsed,
315
+ heapTotal: mem.heapTotal,
316
+ external: mem.external,
317
+ },
318
+ };
319
+ }
320
+
321
+ // ── Reset (for testing) ───────────────────────────────────────────────
322
+
323
+ reset(): void {
324
+ this.startedAt = Date.now();
325
+ this.sessionsCreated.clear();
326
+ this.sessionsTerminated.clear();
327
+ this.autoRelaunches = { attempted: 0, succeeded: 0, exhausted: 0 };
328
+ this.messagesProcessed.clear();
329
+ this.permissions = { total: 0, autoApproved: 0, autoDenied: 0, userApproved: 0, userDenied: 0 };
330
+ this.errors.clear();
331
+ this.stateTransitions.clear();
332
+ this.wsConnections = { cliOpened: 0, cliClosed: 0, browserOpened: 0, browserClosed: 0 };
333
+ this.sessionInitTime = createHistogram();
334
+ this.turnDuration = createHistogram();
335
+ this.permissionDuration = createHistogram();
336
+ this.sessionSpawnedAt.clear();
337
+ this.turnStartedAt.clear();
338
+ this.permissionRequestedAt.clear();
339
+ this.permissionRequestToSession.clear();
340
+ }
341
+
342
+ /** Unsubscribe from all event bus listeners. */
343
+ destroy(): void {
344
+ for (const unsub of this.unsubscribers) unsub();
345
+ this.unsubscribers = [];
346
+ }
347
+ }
348
+
349
+ /** Singleton instance used by the server. */
350
+ export const metricsCollector = new MetricsCollector();
@@ -0,0 +1,108 @@
1
+ // Type definitions for the Companion runtime metrics system.
2
+ // Defines the shape of the JSON snapshot returned by GET /api/metrics.
3
+
4
+ import type { SessionPhase } from "./session-state-machine.js";
5
+
6
+ // ── Snapshot (top-level) ───────────────────────────────────────────────────
7
+
8
+ export interface MetricsSnapshot {
9
+ /** Milliseconds since server started. */
10
+ serverUptimeMs: number;
11
+ /** Unix timestamp (ms) when this snapshot was taken. */
12
+ snapshotAt: number;
13
+ counters: CounterMetrics;
14
+ gauges: GaugeMetrics;
15
+ histograms: HistogramMetrics;
16
+ }
17
+
18
+ // ── Counters (monotonically increasing) ────────────────────────────────────
19
+
20
+ export interface CounterMetrics {
21
+ /** Sessions created, keyed by backend type ("claude" | "codex"). */
22
+ sessionsCreated: Record<string, number>;
23
+ /** Sessions terminated, keyed by exit code (stringified). */
24
+ sessionsTerminated: Record<string, number>;
25
+ /** Auto-relaunch tracking. */
26
+ autoRelaunches: {
27
+ attempted: number;
28
+ succeeded: number;
29
+ exhausted: number;
30
+ };
31
+ /** Messages processed by the bridge, keyed by message type. */
32
+ messagesProcessed: Record<string, number>;
33
+ /** Permission request flow tracking. */
34
+ permissionRequests: {
35
+ total: number;
36
+ autoApproved: number;
37
+ autoDenied: number;
38
+ userApproved: number;
39
+ userDenied: number;
40
+ };
41
+ /** Errors by category (e.g. "invalid_state_transition", "parse_error"). */
42
+ errors: Record<string, number>;
43
+ /** State machine transitions, keyed by "from→to". */
44
+ stateTransitions: Record<string, number>;
45
+ /** WebSocket connection events. */
46
+ wsConnections: {
47
+ cliOpened: number;
48
+ cliClosed: number;
49
+ browserOpened: number;
50
+ browserClosed: number;
51
+ };
52
+ }
53
+
54
+ // ── Gauges (point-in-time values) ──────────────────────────────────────────
55
+
56
+ export interface GaugeMetrics {
57
+ /** Active sessions grouped by phase. */
58
+ activeSessions: Partial<Record<SessionPhase, number>>;
59
+ /** Total non-terminated sessions. */
60
+ totalActiveSessions: number;
61
+ /** Total connected browser WebSockets across all sessions. */
62
+ connectedBrowsers: number;
63
+ /** Total pending messages queued across all sessions. */
64
+ totalPendingMessages: number;
65
+ /** Total event buffer entries across all sessions. */
66
+ totalEventBufferSize: number;
67
+ /** Total message history entries across all sessions. */
68
+ totalHistoryMessages: number;
69
+ /** Process memory usage in bytes. */
70
+ memory: {
71
+ rss: number;
72
+ heapUsed: number;
73
+ heapTotal: number;
74
+ external: number;
75
+ };
76
+ }
77
+
78
+ // ── Histograms (distributions) ─────────────────────────────────────────────
79
+
80
+ export interface HistogramSnapshot {
81
+ /** Number of observations. */
82
+ count: number;
83
+ /** Sum of all observed values. */
84
+ sum: number;
85
+ /** Minimum observed value (0 if no observations). */
86
+ min: number;
87
+ /** Maximum observed value (0 if no observations). */
88
+ max: number;
89
+ /** Average (0 if no observations). */
90
+ avg: number;
91
+ /** Approximate bucket boundary for the 50th percentile. */
92
+ p50Bucket: number;
93
+ /** Approximate bucket boundary for the 95th percentile. */
94
+ p95Bucket: number;
95
+ /** Approximate bucket boundary for the 99th percentile. */
96
+ p99Bucket: number;
97
+ /** Cumulative counts per bucket boundary (stringified keys). */
98
+ buckets: Record<string, number>;
99
+ }
100
+
101
+ export interface HistogramMetrics {
102
+ /** Session initialization time: spawn → ready (ms). */
103
+ sessionInitTimeMs: HistogramSnapshot;
104
+ /** Turn duration: user message → result (ms). */
105
+ turnDurationMs: HistogramSnapshot;
106
+ /** Permission request duration: request → response (ms). */
107
+ permissionDurationMs: HistogramSnapshot;
108
+ }