@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,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disconnection diagnostics for recorded sessions.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes recording entries for connection lifecycle events and data gaps
|
|
5
|
+
* to identify disconnection patterns and potential causes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Recording } from "../replay.js";
|
|
9
|
+
import type { RecordingEntry } from "../recorder.js";
|
|
10
|
+
|
|
11
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface TimelineEntry {
|
|
14
|
+
ts: number;
|
|
15
|
+
event: string;
|
|
16
|
+
channel: "cli" | "browser";
|
|
17
|
+
detail?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DisconnectionEvent {
|
|
21
|
+
ts: number;
|
|
22
|
+
channel: "cli" | "browser";
|
|
23
|
+
closeCode?: number;
|
|
24
|
+
closeReason?: string;
|
|
25
|
+
reconnectedAt?: number;
|
|
26
|
+
gapMs: number;
|
|
27
|
+
messagesLostEstimate: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DisconnectionReport {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
backendType: string;
|
|
33
|
+
totalDuration: number;
|
|
34
|
+
totalDisconnections: number;
|
|
35
|
+
disconnections: DisconnectionEvent[];
|
|
36
|
+
patterns: string[];
|
|
37
|
+
dataGaps: DataGap[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DataGap {
|
|
41
|
+
startTs: number;
|
|
42
|
+
endTs: number;
|
|
43
|
+
gapMs: number;
|
|
44
|
+
channel: "cli" | "browser";
|
|
45
|
+
messagesBefore: number;
|
|
46
|
+
messagesAfter: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/** Gaps longer than this in CLI messages suggest a disconnection. */
|
|
52
|
+
const CLI_GAP_THRESHOLD_MS = 30_000;
|
|
53
|
+
/** Minimum number of disconnections to detect a pattern. */
|
|
54
|
+
const PATTERN_MIN_COUNT = 3;
|
|
55
|
+
/** Tolerance for regular interval detection (±20%). */
|
|
56
|
+
const INTERVAL_TOLERANCE = 0.2;
|
|
57
|
+
|
|
58
|
+
// ─── Analysis ────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Analyze a recording for disconnection patterns.
|
|
62
|
+
*
|
|
63
|
+
* Works with both legacy recordings (data messages only) and enhanced
|
|
64
|
+
* recordings that include connection lifecycle events.
|
|
65
|
+
*/
|
|
66
|
+
export function analyzeDisconnections(recording: Recording): DisconnectionReport {
|
|
67
|
+
const entries = recording.entries;
|
|
68
|
+
const header = recording.header;
|
|
69
|
+
|
|
70
|
+
// Build timeline from both lifecycle events and data gap analysis
|
|
71
|
+
const timeline = buildTimeline(recording);
|
|
72
|
+
const disconnections = detectDisconnections(entries, timeline);
|
|
73
|
+
const dataGaps = detectDataGaps(entries);
|
|
74
|
+
const patterns = detectPatterns(disconnections, dataGaps);
|
|
75
|
+
|
|
76
|
+
const firstTs = entries[0]?.ts ?? header.started_at;
|
|
77
|
+
const lastTs = entries[entries.length - 1]?.ts ?? firstTs;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
sessionId: header.session_id,
|
|
81
|
+
backendType: header.backend_type,
|
|
82
|
+
totalDuration: lastTs - firstTs,
|
|
83
|
+
totalDisconnections: disconnections.length,
|
|
84
|
+
disconnections,
|
|
85
|
+
patterns,
|
|
86
|
+
dataGaps,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build a timeline of connection events from a recording.
|
|
92
|
+
*
|
|
93
|
+
* Extracts both explicit lifecycle events (ws_open, ws_close, etc.) from
|
|
94
|
+
* enhanced recordings and infers events from data gaps in legacy recordings.
|
|
95
|
+
*/
|
|
96
|
+
export function buildTimeline(recording: Recording): TimelineEntry[] {
|
|
97
|
+
const timeline: TimelineEntry[] = [];
|
|
98
|
+
|
|
99
|
+
for (const entry of recording.entries) {
|
|
100
|
+
// Enhanced recordings have explicit lifecycle events
|
|
101
|
+
const enhanced = entry as RecordingEntry & { event?: string; meta?: Record<string, unknown> };
|
|
102
|
+
if (enhanced.event) {
|
|
103
|
+
timeline.push({
|
|
104
|
+
ts: entry.ts,
|
|
105
|
+
event: enhanced.event,
|
|
106
|
+
channel: entry.ch,
|
|
107
|
+
detail: enhanced.meta ? JSON.stringify(enhanced.meta) : undefined,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// For data messages, track key protocol events
|
|
113
|
+
if (entry.dir === "out" && entry.ch === "browser") {
|
|
114
|
+
try {
|
|
115
|
+
const msg = JSON.parse(entry.raw);
|
|
116
|
+
if (msg.type === "cli_connected") {
|
|
117
|
+
timeline.push({ ts: entry.ts, event: "cli_connected", channel: "cli" });
|
|
118
|
+
} else if (msg.type === "cli_disconnected") {
|
|
119
|
+
timeline.push({ ts: entry.ts, event: "cli_disconnected", channel: "cli" });
|
|
120
|
+
} else if (msg.type === "session_init") {
|
|
121
|
+
timeline.push({ ts: entry.ts, event: "session_init", channel: "cli" });
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Skip unparseable
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return timeline.sort((a, b) => a.ts - b.ts);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
function detectDisconnections(
|
|
135
|
+
entries: RecordingEntry[],
|
|
136
|
+
timeline: TimelineEntry[],
|
|
137
|
+
): DisconnectionEvent[] {
|
|
138
|
+
const disconnections: DisconnectionEvent[] = [];
|
|
139
|
+
|
|
140
|
+
// From explicit timeline events
|
|
141
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
142
|
+
const event = timeline[i];
|
|
143
|
+
if (event.event === "ws_close" || event.event === "cli_disconnected") {
|
|
144
|
+
// Find next reconnect/connect event on same channel
|
|
145
|
+
let reconnectedAt: number | undefined;
|
|
146
|
+
for (let j = i + 1; j < timeline.length; j++) {
|
|
147
|
+
if (
|
|
148
|
+
timeline[j].channel === event.channel &&
|
|
149
|
+
(timeline[j].event === "ws_open" || timeline[j].event === "cli_connected" || timeline[j].event === "reconnect_success")
|
|
150
|
+
) {
|
|
151
|
+
reconnectedAt = timeline[j].ts;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Estimate messages lost during the gap
|
|
157
|
+
let messagesLost = 0;
|
|
158
|
+
if (reconnectedAt) {
|
|
159
|
+
// Count messages that arrived on the other channel during the gap
|
|
160
|
+
messagesLost = entries.filter(
|
|
161
|
+
(e) =>
|
|
162
|
+
e.ts > event.ts &&
|
|
163
|
+
e.ts < reconnectedAt! &&
|
|
164
|
+
e.ch !== event.channel,
|
|
165
|
+
).length;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let meta: Record<string, unknown> | undefined;
|
|
169
|
+
try {
|
|
170
|
+
meta = event.detail ? JSON.parse(event.detail) : undefined;
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
disconnections.push({
|
|
176
|
+
ts: event.ts,
|
|
177
|
+
channel: event.channel,
|
|
178
|
+
closeCode: meta?.code as number | undefined,
|
|
179
|
+
closeReason: meta?.reason as string | undefined,
|
|
180
|
+
reconnectedAt,
|
|
181
|
+
gapMs: reconnectedAt ? reconnectedAt - event.ts : 0,
|
|
182
|
+
messagesLostEstimate: messagesLost,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Deduplicate: ws_close and cli_disconnected for the same outage.
|
|
188
|
+
// Only dedup if the second disconnect happens before the first one reconnected.
|
|
189
|
+
const deduped: DisconnectionEvent[] = [];
|
|
190
|
+
for (const d of disconnections) {
|
|
191
|
+
const isDuplicate = deduped.some(
|
|
192
|
+
(existing) =>
|
|
193
|
+
existing.channel === d.channel &&
|
|
194
|
+
// Only dedup if this disconnect happened before the previous one reconnected
|
|
195
|
+
// (i.e. same outage, not a new one after recovery)
|
|
196
|
+
(!existing.reconnectedAt || d.ts < existing.reconnectedAt),
|
|
197
|
+
);
|
|
198
|
+
if (!isDuplicate) deduped.push(d);
|
|
199
|
+
}
|
|
200
|
+
return deduped;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function detectDataGaps(entries: RecordingEntry[]): DataGap[] {
|
|
204
|
+
const gaps: DataGap[] = [];
|
|
205
|
+
|
|
206
|
+
// Group entries by channel
|
|
207
|
+
const cliEntries = entries.filter((e) => e.ch === "cli" && e.dir === "in" && !e.event);
|
|
208
|
+
const browserEntries = entries.filter((e) => e.ch === "browser" && e.dir === "in" && !e.event);
|
|
209
|
+
|
|
210
|
+
for (const [channel, channelEntries] of [
|
|
211
|
+
["cli", cliEntries],
|
|
212
|
+
["browser", browserEntries],
|
|
213
|
+
] as const) {
|
|
214
|
+
for (let i = 1; i < channelEntries.length; i++) {
|
|
215
|
+
const gapMs = channelEntries[i].ts - channelEntries[i - 1].ts;
|
|
216
|
+
if (gapMs > CLI_GAP_THRESHOLD_MS) {
|
|
217
|
+
gaps.push({
|
|
218
|
+
startTs: channelEntries[i - 1].ts,
|
|
219
|
+
endTs: channelEntries[i].ts,
|
|
220
|
+
gapMs,
|
|
221
|
+
channel,
|
|
222
|
+
messagesBefore: i,
|
|
223
|
+
messagesAfter: channelEntries.length - i,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return gaps.sort((a, b) => a.startTs - b.startTs);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function detectPatterns(
|
|
233
|
+
disconnections: DisconnectionEvent[],
|
|
234
|
+
dataGaps: DataGap[],
|
|
235
|
+
): string[] {
|
|
236
|
+
const patterns: string[] = [];
|
|
237
|
+
|
|
238
|
+
// Pattern: Keep-alive failure (regular interval disconnections)
|
|
239
|
+
if (disconnections.length >= PATTERN_MIN_COUNT) {
|
|
240
|
+
const cliDisconnections = disconnections.filter((d) => d.channel === "cli");
|
|
241
|
+
if (cliDisconnections.length >= PATTERN_MIN_COUNT) {
|
|
242
|
+
const intervals = [];
|
|
243
|
+
for (let i = 1; i < cliDisconnections.length; i++) {
|
|
244
|
+
intervals.push(cliDisconnections[i].ts - cliDisconnections[i - 1].ts);
|
|
245
|
+
}
|
|
246
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
247
|
+
const allClose = intervals.every(
|
|
248
|
+
(iv) => Math.abs(iv - avgInterval) / avgInterval < INTERVAL_TOLERANCE,
|
|
249
|
+
);
|
|
250
|
+
if (allClose) {
|
|
251
|
+
patterns.push(
|
|
252
|
+
`Regular CLI disconnections every ~${Math.round(avgInterval / 1000)}s — possible keep-alive or timeout issue`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Pattern: Rapid reconnect cycling
|
|
259
|
+
const rapidReconnects = disconnections.filter(
|
|
260
|
+
(d) => d.reconnectedAt && d.gapMs < 5000,
|
|
261
|
+
);
|
|
262
|
+
if (rapidReconnects.length >= PATTERN_MIN_COUNT) {
|
|
263
|
+
patterns.push(
|
|
264
|
+
`${rapidReconnects.length} rapid reconnections (< 5s gap) — possible flapping connection`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Pattern: Large data gaps without explicit disconnect events
|
|
269
|
+
const unexplainedGaps = dataGaps.filter((g) => {
|
|
270
|
+
// Check if any disconnection event falls within this gap
|
|
271
|
+
return !disconnections.some(
|
|
272
|
+
(d) => d.ts >= g.startTs && d.ts <= g.endTs,
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
if (unexplainedGaps.length > 0) {
|
|
276
|
+
patterns.push(
|
|
277
|
+
`${unexplainedGaps.length} data gap(s) without recorded disconnect events — possible silent connection drops`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Pattern: Asymmetric disconnection (CLI drops but browser stays)
|
|
282
|
+
const cliOnly = disconnections.filter((d) => d.channel === "cli");
|
|
283
|
+
const browserOnly = disconnections.filter((d) => d.channel === "browser");
|
|
284
|
+
if (cliOnly.length > 0 && browserOnly.length === 0) {
|
|
285
|
+
patterns.push(
|
|
286
|
+
`All ${cliOnly.length} disconnection(s) are CLI-side — browser connections are stable. Check CLI process health.`,
|
|
287
|
+
);
|
|
288
|
+
} else if (browserOnly.length > 0 && cliOnly.length === 0) {
|
|
289
|
+
patterns.push(
|
|
290
|
+
`All ${browserOnly.length} disconnection(s) are browser-side — CLI connection is stable. Check network/proxy.`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (patterns.length === 0 && disconnections.length === 0 && dataGaps.length === 0) {
|
|
295
|
+
patterns.push("No disconnection issues detected in this recording.");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return patterns;
|
|
299
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { isRecordingHubEnabled, getMaxUploadBytes } from "./hub-config.js";
|
|
3
|
+
|
|
4
|
+
describe("hub-config", () => {
|
|
5
|
+
const originalEnv = { ...process.env };
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
process.env = { ...originalEnv };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("isRecordingHubEnabled", () => {
|
|
12
|
+
it("returns false by default (hidden feature)", () => {
|
|
13
|
+
delete process.env.COMPANION_RECORDING_HUB;
|
|
14
|
+
expect(isRecordingHubEnabled()).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns true when COMPANION_RECORDING_HUB=1", () => {
|
|
18
|
+
process.env.COMPANION_RECORDING_HUB = "1";
|
|
19
|
+
expect(isRecordingHubEnabled()).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns true when COMPANION_RECORDING_HUB=true", () => {
|
|
23
|
+
process.env.COMPANION_RECORDING_HUB = "true";
|
|
24
|
+
expect(isRecordingHubEnabled()).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns false for other values", () => {
|
|
28
|
+
process.env.COMPANION_RECORDING_HUB = "0";
|
|
29
|
+
expect(isRecordingHubEnabled()).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("getMaxUploadBytes", () => {
|
|
34
|
+
it("defaults to 50MB", () => {
|
|
35
|
+
delete process.env.COMPANION_HUB_MAX_UPLOAD_MB;
|
|
36
|
+
expect(getMaxUploadBytes()).toBe(50 * 1024 * 1024);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("respects COMPANION_HUB_MAX_UPLOAD_MB", () => {
|
|
40
|
+
process.env.COMPANION_HUB_MAX_UPLOAD_MB = "100";
|
|
41
|
+
expect(getMaxUploadBytes()).toBe(100 * 1024 * 1024);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature gate for the Recording Hub.
|
|
3
|
+
*
|
|
4
|
+
* The hub is disabled by default. Enable with COMPANION_RECORDING_HUB=1.
|
|
5
|
+
* When disabled, hub routes are not registered and hub storage is not initialized.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_UPLOAD_MB = 50;
|
|
9
|
+
|
|
10
|
+
export function isRecordingHubEnabled(): boolean {
|
|
11
|
+
const env = process.env.COMPANION_RECORDING_HUB;
|
|
12
|
+
return env === "1" || env === "true";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getMaxUploadBytes(): number {
|
|
16
|
+
const parsed = Number(process.env.COMPANION_HUB_MAX_UPLOAD_MB);
|
|
17
|
+
const mb = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_UPLOAD_MB;
|
|
18
|
+
return mb * 1024 * 1024;
|
|
19
|
+
}
|