@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.
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- 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
|
+
}
|