the-campfire 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.
- package/bin/cli.ts +182 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/MermaidDiagram-By1J3Whj.js +2 -0
- package/dist/assets/_basePickBy-Dq6gnD-p.js +1 -0
- package/dist/assets/_baseUniq-SpgN6cGc.js +1 -0
- package/dist/assets/arc-D9SEA8dX.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CoTcwLeR.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-Dvc63voV.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-tuDcG4_4.js +10 -0
- package/dist/assets/channel-CxhcHn7S.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-x1hChYu9.js +1 -0
- package/dist/assets/chunk-55IACEB6-BX6HWnWP.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-B4IlF03G.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-TpYb5rLg.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-DZX4LUPx.js +15 -0
- package/dist/assets/chunk-QN33PNHL-CuQs0NDY.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-CygWwVx8.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CtI-IdKV.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-BM0LptJF.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-BM0LptJF.js +1 -0
- package/dist/assets/clone-C6kt1rE7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-PdySs991.js +1 -0
- package/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/dist/assets/dagre-6UL2VRFP-J0SxEiEu.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-CW0HLXaH.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-DADHF-k4.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-9S0-PC3S.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-BEHAiY_q.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-Cu6n9Xnd.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-Cr_BHryu.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-D5pT35v5.js +65 -0
- package/dist/assets/graph-DKRbrxcs.js +1 -0
- package/dist/assets/index-Byjg9zgI.css +32 -0
- package/dist/assets/index-Cwj3m0hT.js +284 -0
- package/dist/assets/infoDiagram-HS3SLOUP-BaJlrgKV.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CXC4-DJv.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-BtHBu9cx.js +89 -0
- package/dist/assets/katex-BlHpptmG.js +261 -0
- package/dist/assets/layout-DnxSwLDJ.js +1 -0
- package/dist/assets/linear-CWbbIT8l.js +1 -0
- package/dist/assets/mermaid.core-DXn1cFAK.js +256 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-BR8br1hs.js +68 -0
- package/dist/assets/ordinal-BENe2yWM.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-B7RmXV7I.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-DK0FhyV_.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-3zAGxSpL.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-DZppKtjc.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-D1x3r3wV.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-2ByD08Vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Vm1cYjgV.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-l5S35biR.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-DIPadPiF.js +162 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-9GUvwS2b.js +7 -0
- package/dist/favicon.svg +3 -0
- package/dist/fonts/Geist-Variable.woff2 +0 -0
- package/dist/fonts/GeistMono-Variable.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +33 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo.svg +3 -0
- package/dist/manifest.json +26 -0
- package/dist/og-image.png +0 -0
- package/dist/sw.js +130 -0
- package/package.json +92 -0
- package/server/ADAPTERS.md +121 -0
- package/server/adapter-registry-types.ts +28 -0
- package/server/adapter-registry.ts +293 -0
- package/server/adapter-types.ts +51 -0
- package/server/agent-executor.ts +249 -0
- package/server/agent-mcp-bridge.ts +148 -0
- package/server/agent-mcp-stdio.ts +171 -0
- package/server/agent-mcp-tools.ts +76 -0
- package/server/agent-store.ts +183 -0
- package/server/agent-types.ts +79 -0
- package/server/aider-adapter.ts +525 -0
- package/server/auth.ts +153 -0
- package/server/auto-namer.ts +132 -0
- package/server/capability-discovery.ts +431 -0
- package/server/claude-container-auth.ts +40 -0
- package/server/claude-stdio-adapter.ts +452 -0
- package/server/clawhub-export.ts +218 -0
- package/server/cli-launcher.ts +1613 -0
- package/server/codex-adapter.ts +1987 -0
- package/server/codex-container-auth.ts +40 -0
- package/server/codex-home.ts +26 -0
- package/server/collective-intelligence.ts +401 -0
- package/server/commands-discovery.ts +160 -0
- package/server/constants.ts +5 -0
- package/server/container-manager.ts +328 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +149 -0
- package/server/cron-types.ts +63 -0
- package/server/deliberation-engine.ts +323 -0
- package/server/embedding.ts +89 -0
- package/server/env-manager.ts +146 -0
- package/server/environment-detector.ts +106 -0
- package/server/environment-rules.ts +106 -0
- package/server/gallery-store.ts +216 -0
- package/server/gallery-types.ts +52 -0
- package/server/gallery-votes.ts +101 -0
- package/server/git-utils.ts +384 -0
- package/server/github-pr.ts +379 -0
- package/server/goose-adapter.ts +929 -0
- package/server/image-pull-manager.ts +144 -0
- package/server/index.ts +363 -0
- package/server/linear-project-manager.ts +78 -0
- package/server/moltbook-client.ts +160 -0
- package/server/openclaw-adapter.ts +916 -0
- package/server/openclaw-channel/channel.ts +188 -0
- package/server/openclaw-channel/index.ts +68 -0
- package/server/openclaw-channel/package.json +19 -0
- package/server/openclaw-channel/types.ts +93 -0
- package/server/opencode-adapter.ts +951 -0
- package/server/openhands-adapter.ts +775 -0
- package/server/orchestrator-executor.ts +184 -0
- package/server/orchestrator-store.ts +106 -0
- package/server/orchestrator-types.ts +86 -0
- package/server/path-resolver.ts +168 -0
- package/server/pr-poller.ts +162 -0
- package/server/proactive-keepalive.ts +121 -0
- package/server/prompt-manager.ts +103 -0
- package/server/protocol-monitor.ts +197 -0
- package/server/race-controller.ts +295 -0
- package/server/race-store.ts +82 -0
- package/server/recorder.ts +359 -0
- package/server/recording-hub/compat-validator.ts +122 -0
- package/server/recording-hub/diagnostics.ts +289 -0
- package/server/recording-hub/hub-routes.ts +146 -0
- package/server/recording-hub/hub-store.ts +224 -0
- package/server/replay.ts +78 -0
- package/server/routes/adapter-routes.ts +31 -0
- package/server/routes/agent-mcp-routes.ts +35 -0
- package/server/routes/agent-routes.ts +185 -0
- package/server/routes/auth-routes.ts +101 -0
- package/server/routes/ci-routes.ts +165 -0
- package/server/routes/commands-routes.ts +94 -0
- package/server/routes/cron-routes.ts +100 -0
- package/server/routes/env-routes.ts +50 -0
- package/server/routes/folder-routes.ts +56 -0
- package/server/routes/fs-routes.ts +403 -0
- package/server/routes/gallery-routes.ts +410 -0
- package/server/routes/git-routes.ts +108 -0
- package/server/routes/index.ts +62 -0
- package/server/routes/linear-routes.ts +254 -0
- package/server/routes/monitor-routes.ts +11 -0
- package/server/routes/orchestrator-routes.ts +210 -0
- package/server/routes/prompt-routes.ts +47 -0
- package/server/routes/race-routes.ts +103 -0
- package/server/routes/recording-routes.ts +101 -0
- package/server/routes/route-deps.ts +32 -0
- package/server/routes/session-routes.ts +560 -0
- package/server/routes/settings-routes.ts +108 -0
- package/server/routes/skills-routes.ts +52 -0
- package/server/routes/system-routes.ts +243 -0
- package/server/routes/webhook-routes.ts +135 -0
- package/server/routes.ts +44 -0
- package/server/security-middleware.ts +100 -0
- package/server/semantic-memory.ts +389 -0
- package/server/service.ts +733 -0
- package/server/session-folders.ts +107 -0
- package/server/session-git-info.ts +89 -0
- package/server/session-linear-issues.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-store.ts +169 -0
- package/server/session-types.ts +415 -0
- package/server/settings-manager.ts +120 -0
- package/server/shared-context.ts +401 -0
- package/server/skills-manager.ts +250 -0
- package/server/sub-session-manager.ts +263 -0
- package/server/terminal-manager.ts +185 -0
- package/server/update-checker.ts +114 -0
- package/server/usage-limits.ts +192 -0
- package/server/webhook-manager.ts +228 -0
- package/server/webhook-store.ts +168 -0
- package/server/webhook-types.ts +66 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-bridge.ts +2322 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proactive Keepalive — auto-relaunches crashed CLI sessions.
|
|
3
|
+
*
|
|
4
|
+
* When a CLI process exits unexpectedly, this module schedules a relaunch
|
|
5
|
+
* with exponential backoff. Ensures autonomous sessions (agents, cron jobs)
|
|
6
|
+
* stay alive even without a browser connected.
|
|
7
|
+
*
|
|
8
|
+
* Excludes: intentional kills, archived sessions, clean exits (code 0).
|
|
9
|
+
*
|
|
10
|
+
* Stability window: the attempt counter is only reset after the CLI has been
|
|
11
|
+
* connected for at least STABILITY_WINDOW_MS. This prevents infinite
|
|
12
|
+
* relaunch loops when a session connects briefly then crashes immediately.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { CliLauncher } from "./cli-launcher.js";
|
|
16
|
+
|
|
17
|
+
const BASE_DELAY_MS = Number(process.env.CAMPFIRE_KEEPALIVE_DELAY_MS) || 3_000;
|
|
18
|
+
const MAX_ATTEMPTS = Number(process.env.CAMPFIRE_KEEPALIVE_MAX_ATTEMPTS) || 3;
|
|
19
|
+
const STABILITY_WINDOW_MS = 30_000;
|
|
20
|
+
|
|
21
|
+
export class ProactiveKeepalive {
|
|
22
|
+
private readonly launcher: CliLauncher;
|
|
23
|
+
private readonly timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
24
|
+
private readonly attempts = new Map<string, number>();
|
|
25
|
+
private readonly intentionalKills = new Set<string>();
|
|
26
|
+
/** Tracks when each session was last relaunched by keepalive. */
|
|
27
|
+
private readonly launchTimes = new Map<string, number>();
|
|
28
|
+
private destroyed = false;
|
|
29
|
+
|
|
30
|
+
constructor(launcher: CliLauncher) {
|
|
31
|
+
this.launcher = launcher;
|
|
32
|
+
|
|
33
|
+
// Listen for CLI process exits
|
|
34
|
+
launcher.onSessionExited((sessionId, exitCode) => {
|
|
35
|
+
this.handleExit(sessionId, exitCode);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Mark a session as intentionally killed (don't auto-relaunch). */
|
|
40
|
+
markIntentionalKill(sessionId: string): void {
|
|
41
|
+
this.intentionalKills.add(sessionId);
|
|
42
|
+
this.cancelTimer(sessionId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Cancel a pending relaunch timer. */
|
|
46
|
+
cancelTimer(sessionId: string): void {
|
|
47
|
+
const timer = this.timers.get(sessionId);
|
|
48
|
+
if (timer) {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
this.timers.delete(sessionId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Stop all timers (graceful shutdown). */
|
|
55
|
+
destroy(): void {
|
|
56
|
+
this.destroyed = true;
|
|
57
|
+
for (const timer of this.timers.values()) {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
}
|
|
60
|
+
this.timers.clear();
|
|
61
|
+
this.attempts.clear();
|
|
62
|
+
this.launchTimes.clear();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private handleExit(sessionId: string, exitCode: number | null): void {
|
|
66
|
+
if (this.destroyed) return;
|
|
67
|
+
|
|
68
|
+
// Skip intentional kills
|
|
69
|
+
if (this.intentionalKills.has(sessionId)) {
|
|
70
|
+
this.intentionalKills.delete(sessionId);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Skip clean exits (user typed /exit or session completed normally)
|
|
75
|
+
if (exitCode === 0) return;
|
|
76
|
+
|
|
77
|
+
// Skip archived sessions
|
|
78
|
+
const info = this.launcher.getSession(sessionId);
|
|
79
|
+
if (!info) return;
|
|
80
|
+
if (info.archived) return;
|
|
81
|
+
|
|
82
|
+
// Only reset the attempt counter if the CLI was stable for the
|
|
83
|
+
// full stability window. This prevents infinite loops when a
|
|
84
|
+
// session connects briefly then immediately crashes (e.g. missing
|
|
85
|
+
// --resume session ID or empty prompt).
|
|
86
|
+
const launchTime = this.launchTimes.get(sessionId);
|
|
87
|
+
if (launchTime && Date.now() - launchTime >= STABILITY_WINDOW_MS) {
|
|
88
|
+
this.attempts.delete(sessionId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check attempt count
|
|
92
|
+
const attempt = (this.attempts.get(sessionId) || 0) + 1;
|
|
93
|
+
if (attempt > MAX_ATTEMPTS) {
|
|
94
|
+
console.warn(`[keepalive] Session ${sessionId} exceeded max relaunch attempts (${MAX_ATTEMPTS}), giving up`);
|
|
95
|
+
this.attempts.delete(sessionId);
|
|
96
|
+
this.launchTimes.delete(sessionId);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.attempts.set(sessionId, attempt);
|
|
100
|
+
|
|
101
|
+
// Exponential backoff: 3s → 6s → 12s
|
|
102
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
103
|
+
console.log(`[keepalive] Session ${sessionId} exited (code=${exitCode}), scheduling relaunch #${attempt} in ${delay}ms`);
|
|
104
|
+
|
|
105
|
+
const timer = setTimeout(async () => {
|
|
106
|
+
this.timers.delete(sessionId);
|
|
107
|
+
if (this.destroyed) return;
|
|
108
|
+
try {
|
|
109
|
+
const success = await this.launcher.relaunch(sessionId);
|
|
110
|
+
if (success) {
|
|
111
|
+
console.log(`[keepalive] Session ${sessionId} relaunched successfully (attempt #${attempt})`);
|
|
112
|
+
this.launchTimes.set(sessionId, Date.now());
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(`[keepalive] Failed to relaunch session ${sessionId}:`, err);
|
|
116
|
+
}
|
|
117
|
+
}, delay);
|
|
118
|
+
|
|
119
|
+
this.timers.set(sessionId, timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
export interface Prompt {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
content: string;
|
|
15
|
+
scope: "global" | "project";
|
|
16
|
+
projectPath?: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
updatedAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PROMPTS_FILE = join(homedir(), ".campfire", "prompts.json");
|
|
22
|
+
|
|
23
|
+
function ensureDir(): void {
|
|
24
|
+
mkdirSync(dirname(PROMPTS_FILE), { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadAll(): Prompt[] {
|
|
28
|
+
try {
|
|
29
|
+
if (!existsSync(PROMPTS_FILE)) return [];
|
|
30
|
+
const raw = readFileSync(PROMPTS_FILE, "utf-8");
|
|
31
|
+
return JSON.parse(raw) as Prompt[];
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveAll(prompts: Prompt[]): void {
|
|
38
|
+
ensureDir();
|
|
39
|
+
writeFileSync(PROMPTS_FILE, JSON.stringify(prompts, null, 2), "utf-8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Returns true if the prompt should be visible in the given cwd. */
|
|
43
|
+
export function visibleForCwd(prompt: Prompt, cwd?: string): boolean {
|
|
44
|
+
if (prompt.scope === "global") return true;
|
|
45
|
+
if (!cwd || !prompt.projectPath) return false;
|
|
46
|
+
return cwd.startsWith(prompt.projectPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function listPrompts(opts?: { cwd?: string }): Prompt[] {
|
|
50
|
+
const all = loadAll();
|
|
51
|
+
if (!opts?.cwd) return all;
|
|
52
|
+
return all.filter((p) => visibleForCwd(p, opts.cwd));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getPrompt(id: string): Prompt | undefined {
|
|
56
|
+
return loadAll().find((p) => p.id === id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createPrompt(
|
|
60
|
+
name: string,
|
|
61
|
+
content: string,
|
|
62
|
+
scope: "global" | "project",
|
|
63
|
+
projectPath?: string,
|
|
64
|
+
): Prompt {
|
|
65
|
+
const all = loadAll();
|
|
66
|
+
const prompt: Prompt = {
|
|
67
|
+
id: randomUUID(),
|
|
68
|
+
name,
|
|
69
|
+
content,
|
|
70
|
+
scope,
|
|
71
|
+
projectPath: scope === "project" ? projectPath : undefined,
|
|
72
|
+
createdAt: Date.now(),
|
|
73
|
+
updatedAt: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
all.push(prompt);
|
|
76
|
+
saveAll(all);
|
|
77
|
+
return prompt;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function updatePrompt(
|
|
81
|
+
id: string,
|
|
82
|
+
updates: Partial<Pick<Prompt, "name" | "content" | "scope" | "projectPath">>,
|
|
83
|
+
): Prompt | undefined {
|
|
84
|
+
const all = loadAll();
|
|
85
|
+
const idx = all.findIndex((p) => p.id === id);
|
|
86
|
+
if (idx === -1) return undefined;
|
|
87
|
+
all[idx] = {
|
|
88
|
+
...all[idx],
|
|
89
|
+
...updates,
|
|
90
|
+
updatedAt: Date.now(),
|
|
91
|
+
};
|
|
92
|
+
saveAll(all);
|
|
93
|
+
return all[idx];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function deletePrompt(id: string): boolean {
|
|
97
|
+
const all = loadAll();
|
|
98
|
+
const idx = all.findIndex((p) => p.id === id);
|
|
99
|
+
if (idx === -1) return false;
|
|
100
|
+
all.splice(idx, 1);
|
|
101
|
+
saveAll(all);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol Monitor — real-time message tracking and health metrics.
|
|
3
|
+
*
|
|
4
|
+
* Tracks message counts, rates, errors, and protocol drift across
|
|
5
|
+
* all sessions and backends. Provides a rolling 5-minute window
|
|
6
|
+
* of statistics for the dashboard.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface MessageEvent {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
backendType: string;
|
|
14
|
+
direction: "in" | "out";
|
|
15
|
+
channel: "cli" | "browser";
|
|
16
|
+
messageType: string;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DriftEvent {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
backendType: string;
|
|
23
|
+
direction: "in" | "out";
|
|
24
|
+
messageType: string;
|
|
25
|
+
details: string;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SessionStats {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
backendType: string;
|
|
32
|
+
messageCount: number;
|
|
33
|
+
errorCount: number;
|
|
34
|
+
messagesPerMinute: number;
|
|
35
|
+
lastMessageAt: number;
|
|
36
|
+
messageTypes: Record<string, number>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MonitorSnapshot {
|
|
40
|
+
uptime: number;
|
|
41
|
+
totalMessages: number;
|
|
42
|
+
totalErrors: number;
|
|
43
|
+
globalMessagesPerMinute: number;
|
|
44
|
+
activeSessions: number;
|
|
45
|
+
sessionStats: SessionStats[];
|
|
46
|
+
recentDrifts: DriftEvent[];
|
|
47
|
+
messageTypeGlobal: Record<string, number>;
|
|
48
|
+
backendBreakdown: Record<string, { messages: number; errors: number }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const WINDOW_MS = 5 * 60 * 1000; // 5-minute rolling window
|
|
54
|
+
const MAX_DRIFTS = 50;
|
|
55
|
+
const MAX_EVENTS_PER_SESSION = 500;
|
|
56
|
+
|
|
57
|
+
// ─── Monitor ────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export class ProtocolMonitor {
|
|
60
|
+
private readonly startedAt = Date.now();
|
|
61
|
+
private readonly sessionEvents = new Map<string, MessageEvent[]>();
|
|
62
|
+
private readonly sessionBackends = new Map<string, string>();
|
|
63
|
+
private readonly drifts: DriftEvent[] = [];
|
|
64
|
+
private readonly driftSeen = new Set<string>();
|
|
65
|
+
private totalMessages = 0;
|
|
66
|
+
private totalErrors = 0;
|
|
67
|
+
|
|
68
|
+
/** Record a message passing through the bridge. */
|
|
69
|
+
recordMessage(
|
|
70
|
+
sessionId: string,
|
|
71
|
+
direction: "in" | "out",
|
|
72
|
+
channel: "cli" | "browser",
|
|
73
|
+
messageType: string,
|
|
74
|
+
backendType?: string,
|
|
75
|
+
): void {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
this.totalMessages++;
|
|
78
|
+
|
|
79
|
+
if (backendType) {
|
|
80
|
+
this.sessionBackends.set(sessionId, backendType);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const event: MessageEvent = {
|
|
84
|
+
sessionId,
|
|
85
|
+
backendType: backendType || this.sessionBackends.get(sessionId) || "unknown",
|
|
86
|
+
direction,
|
|
87
|
+
channel,
|
|
88
|
+
messageType,
|
|
89
|
+
timestamp: now,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let events = this.sessionEvents.get(sessionId);
|
|
93
|
+
if (!events) {
|
|
94
|
+
events = [];
|
|
95
|
+
this.sessionEvents.set(sessionId, events);
|
|
96
|
+
}
|
|
97
|
+
events.push(event);
|
|
98
|
+
|
|
99
|
+
// Cap per-session events to prevent memory bloat
|
|
100
|
+
if (events.length > MAX_EVENTS_PER_SESSION) {
|
|
101
|
+
events.splice(0, events.length - MAX_EVENTS_PER_SESSION);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Track errors
|
|
105
|
+
if (messageType === "error" || messageType === "parse_error") {
|
|
106
|
+
this.totalErrors++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Report a protocol drift (unexpected message format). */
|
|
111
|
+
reportDrift(
|
|
112
|
+
sessionId: string,
|
|
113
|
+
backendType: string,
|
|
114
|
+
direction: "in" | "out",
|
|
115
|
+
messageType: string,
|
|
116
|
+
details: string,
|
|
117
|
+
): void {
|
|
118
|
+
const key = `${backendType}:${direction}:${messageType}`;
|
|
119
|
+
if (this.driftSeen.has(key)) return;
|
|
120
|
+
this.driftSeen.add(key);
|
|
121
|
+
|
|
122
|
+
const drift: DriftEvent = {
|
|
123
|
+
sessionId,
|
|
124
|
+
backendType,
|
|
125
|
+
direction,
|
|
126
|
+
messageType,
|
|
127
|
+
details,
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
this.drifts.push(drift);
|
|
132
|
+
if (this.drifts.length > MAX_DRIFTS) {
|
|
133
|
+
this.drifts.shift();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.warn(`[protocol-monitor] Drift detected: ${backendType} ${direction} ${messageType} — ${details}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Get a snapshot of current metrics. */
|
|
140
|
+
getSnapshot(): MonitorSnapshot {
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const windowStart = now - WINDOW_MS;
|
|
143
|
+
|
|
144
|
+
const sessionStats: SessionStats[] = [];
|
|
145
|
+
const messageTypeGlobal: Record<string, number> = {};
|
|
146
|
+
const backendBreakdown: Record<string, { messages: number; errors: number }> = {};
|
|
147
|
+
let windowMessages = 0;
|
|
148
|
+
let activeSessions = 0;
|
|
149
|
+
|
|
150
|
+
for (const [sessionId, events] of this.sessionEvents) {
|
|
151
|
+
const recentEvents = events.filter((e) => e.timestamp >= windowStart);
|
|
152
|
+
if (recentEvents.length === 0) continue;
|
|
153
|
+
|
|
154
|
+
activeSessions++;
|
|
155
|
+
const bt = this.sessionBackends.get(sessionId) || "unknown";
|
|
156
|
+
const typeCount: Record<string, number> = {};
|
|
157
|
+
let errors = 0;
|
|
158
|
+
|
|
159
|
+
for (const e of recentEvents) {
|
|
160
|
+
typeCount[e.messageType] = (typeCount[e.messageType] || 0) + 1;
|
|
161
|
+
messageTypeGlobal[e.messageType] = (messageTypeGlobal[e.messageType] || 0) + 1;
|
|
162
|
+
if (e.messageType === "error" || e.messageType === "parse_error") errors++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
windowMessages += recentEvents.length;
|
|
166
|
+
|
|
167
|
+
if (!backendBreakdown[bt]) backendBreakdown[bt] = { messages: 0, errors: 0 };
|
|
168
|
+
backendBreakdown[bt].messages += recentEvents.length;
|
|
169
|
+
backendBreakdown[bt].errors += errors;
|
|
170
|
+
|
|
171
|
+
const elapsed = Math.max(1, (now - recentEvents[0].timestamp) / 60_000);
|
|
172
|
+
sessionStats.push({
|
|
173
|
+
sessionId,
|
|
174
|
+
backendType: bt,
|
|
175
|
+
messageCount: recentEvents.length,
|
|
176
|
+
errorCount: errors,
|
|
177
|
+
messagesPerMinute: Math.round(recentEvents.length / elapsed),
|
|
178
|
+
lastMessageAt: recentEvents.at(-1)?.timestamp ?? now,
|
|
179
|
+
messageTypes: typeCount,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
sessionStats.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
uptime: now - this.startedAt,
|
|
187
|
+
totalMessages: this.totalMessages,
|
|
188
|
+
totalErrors: this.totalErrors,
|
|
189
|
+
globalMessagesPerMinute: WINDOW_MS > 0 ? Math.round((windowMessages / WINDOW_MS) * 60_000) : 0,
|
|
190
|
+
activeSessions,
|
|
191
|
+
sessionStats,
|
|
192
|
+
recentDrifts: [...this.drifts].reverse(),
|
|
193
|
+
messageTypeGlobal,
|
|
194
|
+
backendBreakdown,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|